Dec 27, 2023
Using VRM in R3F: Experiments and Results
Preconditions
- Fundamental knowledge of React.js and Three.js
- A VRM model
Dependencies
- @react-three/fiber
- @react-three/drei
- @react-three/postprocessing
- @pixiv/three-vrm
- three
File Tree (Simplified)
📁 alice-in-moonlight
|
├─ 📁 public
| └─ 📄 alice.vrm
|
├─ 📁 src
| |
| ├─ 📁 hooks
| | └─ 📄 use-vrm-loader.ts
| |
| ├─ 📁 components
| | └─ 📄 VRMRender.tsx
| |
| ├─ 📁 utils
| | └─ 📄 humanoid.ts
| |
| ├─ 📁 scenes
| | └─ 📄 Moonlight.tsx
| |
| └─ 📁 models
| └─ 📄 Alice.tsx
|
└─ 📄 package.json
1. Create the `useVRMLoader` Hook for Loading VRM Files.
/hooks/use-vrm-loader.ts
import { VRM, VRMLoaderPlugin } from '@pixiv/three-vrm'
import { useLoader } from '@react-three/fiber'
import {
GLTFLoader,
GLTFLoaderPlugin,
type GLTFParser,
} from 'three/examples/jsm/loaders/GLTFLoader.js'
export const useVRMLoader = (assetUrl: string): VRM => {
return useLoader(GLTFLoader, assetUrl, (loader) => {
loader.register((parser: GLTFParser): GLTFLoaderPlugin => {
return new VRMLoaderPlugin(parser)
})
}).userData.vrm
}
2. Create a VRM Renderer Component.
/components/VRMRender.tsx
import { type FC } from 'react'
import { VRM } from '@pixiv/three-vrm'
export interface VRMRenderProps {
vrm: VRM
}
export const VRMRender: FC<VRMRenderProps> = (props) => {
const { vrm } = props
if (!vrm.scene) {
return null
}
return <primitive object={vrm.scene} dispose={null} />
}
3. Create a VRM Instance and Render it in the Moonlight Scene.
/models/Alice.tsx
import { VRMRender } from '../components/VRMRender'
import { useVRMLoader } from '../hooks/use-vrm-loader'
export const Alice = () => {
const vrm = useVRMLoader('/models/alice.vrm')
return <VRMRender vrm={vrm} />
}
/scenes/Moonlight.tsx
import { Canvas, SceneProps } from '@react-three/fiber'
import { FC, Suspense } from 'react'
import { Center } from '@react-three/drei'
import { Alice } from '../models/Alice'
export const Moonlight: FC<SceneProps> = () => {
return (
<Suspense fallback={null}>
<Canvas
style={{
width: '100vw',
height: '100vh',
}}
camera={{
fov: 20,
near: 1,
position: [0, 0, 4],
}}
flat={true}
>
<Center>
<Alice />
</Center>
<ambientLight intensity={0.475} />
</Canvas>
</Suspense>
)
}
4. Create a Utility Tool `humanoid`, for Quick Access to VRM Bone Nodes.
/utils/humanoid.ts
import { VRM, VRMHumanBoneName } from '@pixiv/three-vrm'
import { Object3D } from 'three'
export const humanoid = (vrm: VRM) => {
const getBoneNode = (boneName: VRMHumanBoneName) => {
return vrm.humanoid.getNormalizedBoneNode(boneName)!
}
const boneNodes = Object.values(VRMHumanBoneName).reduce(
(acc, value) => {
acc[value] = getBoneNode(value)
return acc
},
{} as Record<VRMHumanBoneName, Object3D>,
)
return { boneNodes }
}
5. Adjust the Pose of the VRM Instance.
/models/Alice.tsx
import { useFrame } from '@react-three/fiber'
import { VRMRender } from '../components/VRMRender'
import { useVRMLoader } from '../hooks/use-vrm-loader'
import { VRMExpressionPresetName } from '@pixiv/three-vrm'
import { humanoid } from '../utils/humanoid'
export const Alice = () => {
const vrm = useVRMLoader('/models/alice.vrm')
const { boneNodes } = humanoid(vrm)
const {
leftUpperArm,
rightUpperArm,
leftLowerArm,
rightLowerArm,
leftHand,
rightHand,
head,
chest,
hips,
spine,
leftLowerLeg,
rightLowerLeg,
} = boneNodes
leftUpperArm.rotation.set(-1, 1, -1)
rightUpperArm.rotation.set(-1, -1, 1)
leftLowerArm.rotation.set(0, 2.5, -1.75)
rightLowerArm.rotation.set(0, -2.5, 1.75)
leftHand.rotation.set(2, 0, 0)
rightHand.rotation.set(2, 0, 0)
head.rotation.set(0.5, 0, 0)
chest.rotation.set(1, 0, 0)
hips.rotation.set(-1.5, 0, 0)
spine.rotation.set(0.25, 0, 0)
leftLowerLeg.rotation.set(1, 0, 0)
rightLowerLeg.rotation.set(0.5, 0, 0)
// Close eyes
vrm.expressionManager?.setValue(VRMExpressionPresetName.Blink, 1)
vrm.expressionManager?.update()
useFrame((_, delta) => {
vrm.update(delta)
})
return <VRMRender vrm={vrm} />
}
6. Add Ambient Light, Bloom, Floating Effects, and Loader to `Moonlight` Scene.
/scenes/Moonlight.tsx
import { Canvas, SceneProps } from '@react-three/fiber'
import { FC, Suspense } from 'react'
import {
Bloom,
DepthOfField,
EffectComposer,
} from '@react-three/postprocessing'
import { Center, Float, Loader, OrbitControls } from '@react-three/drei'
import { Alice } from '../models/Alice'
export const Moonlight: FC<SceneProps> = () => {
return (
<Suspense fallback={<Loader />}>
<Canvas
style={{
width: '100vw',
height: '100vh',
}}
camera={{
fov: 20,
near: 1,
position: [0, 0, 4],
}}
flat={true}
>
<Center>
<Float
scale={1.2}
speed={1.5}
position={[0, 0, 0]}
rotation={[0, 0.6, 0]}
>
<Alice />
</Float>
</Center>
<EffectComposer disableNormalPass>
<Bloom
luminanceThreshold={0}
mipmapBlur
luminanceSmoothing={0.0}
intensity={6}
/>
<DepthOfField
target={[0, 0, 10]}
focalLength={0.25}
bokehScale={15}
height={800}
/>
</EffectComposer>
<ambientLight intensity={0.475} />
<OrbitControls enablePan enableDamping enableZoom />
</Canvas>
</Suspense>
)
}