手軽に3Dモデルを使ってみたい。でもUnityは学習コスト高そうだし、手慣れたWeb系の言語でどうにかできないものか。
というわけで今回、タイトルにあるライブラリを使ってVRMモデルを表示させてみました。
なおここでは、React環境の作り方などの説明は割愛します。
環境
Package | Version | Explanation |
---|---|---|
React | 16.13 | りあくと |
Typescript | 3.8.3 | たいぷすくりぷと |
Three | 0.115.0 | WebGLを使ってJSで3Dモデルを描画できるライブラリ |
react-three-fiber | 4.0.27 | React上でThree.jsを手軽に使えるやーつ |
@pixiv/three-vrm | 0.3.3 | Three.js上で簡単にVRMファイルを扱える便利なやつ |
react-three-renderer
というのもあるが凍結しているようで、開発陣も「代わりにfiber応援してネ」って言ってる。ちなみにreact-three-fiber
はReactNativeにも対応しているらしい。
そもそもVRMファイルとは
ドワンゴが作った、人型3Dデータ用のファイル形式です。
最近VRChatを始めいろんなところで3Dモデルの需要が増えている中で、ソフトごとに形式が違ってはせっかく作った3Dキャラもったいない!ということで作られたフォーマットらしいです。画像の jpg
とかpng
とか、ハードウェアで言うならUSB
とかと同じですね。
共通フォーマットができたことで、Vroid Hubみたいな、有志の方が作成した3Dキャラを手軽に見ることができるようになったり、中には無償で配布されているVRMファイルもあります!
やってみる
0. React環境を作る
create-react-app
でも大丈夫です。
1. パッケージを追加
とりあえずパッケージを追加する。
yarn add -D three react-three-fiber @pixiv/three-vrm @types/three
2. 一旦react-three-fiber
に慣れる
初めて使うので、一旦適当なオブジェクトを表示してみよう。と言うことで、立方体を表示してみます。
import React from 'react'
import { Canvas } from 'react-three-fiber'
import styled from 'styled-components'
import SampleBox from 'components/SampleBox'
export default function App() {
return (
<Container>
<Canvas>
<SampleBox />
</Canvas>
</Container>
)
}
const Container = styled.div`
width: 100vw;
height: 100vh;
`
import React, { useRef } from 'react'
import { Mesh } from 'three'
import { useFrame } from 'react-three-fiber'
export default function SampleBox() {
const ref = useRef({} as Mesh)
useFrame(() => (ref.current.rotation.z += 0.01))
return (
<mesh ref={ref}>
<boxBufferGeometry attach='geometry' />
<meshBasicMaterial attach='material' color='hotpink' opacity={0.5} transparent />
</mesh>
)
}
SampleBox
コンポーネントが立方体です。
useFrame
を使うことで、毎フレームごとにオブジェクトの位置や向きを変えられます。今回は向きのz座標を0.01ずつ足すことで回転を加えています。
ちなみにStyledComponent
は、コンポーネントに直接styleを適用出来るライブラリです。個人的に可読性が爆上がりするのでオススメ。
ピンクの立方体(?)がくるくる回っていますね。
しかしこのままだと立方体なのか平面なのかよくわからないので、カメラの位置をマウスで動かせるようにしましょう。
3. OrbitControl
を追加する
マウスで拡大したり、カメラのアングルを自由に変えられるようにしましょう。
って言うのがOrbitControl
です。
実はreact-three-fiber
にあるんですが、これをtypescriptで使うと型参照できずエラーに。そこで型を定義しつつコンポーネントを作ってラップします。
https://github.com/react-spring/react-three-fiber/issues/27
import React, { useRef } from 'react'
import { extend, ReactThreeFiber, useThree, useFrame } from 'react-three-fiber'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
extend({ OrbitControls })
declare global {
namespace JSX {
interface IntrinsicElements {
readonly orbitControls: ReactThreeFiber.Object3DNode<OrbitControls, typeof OrbitControls>
}
}
}
export default function Controls(props: ReactThreeFiber.Object3DNode<OrbitControls, typeof OrbitControls>) {
const {
camera,
gl: { domElement }
} = useThree()
const controls = useRef({} as OrbitControls)
useFrame(() => controls.current.update())
return <orbitControls {...props} ref={controls} args={[camera, domElement]} />
}
import React from 'react'
import { Canvas } from 'react-three-fiber'
import styled from 'styled-components'
import SampleBox from 'components/SampleBox'
+ import Controls from 'utils/Controls'
export default function App() {
return (
<Container>
<Canvas>
<SampleBox />
+ <Controls />
+ <gridHelper /> {/* わかりやすいようにGridPanelを表示 */}
</Canvas>
</Container>
)
}
const Container = styled.div`
width: 100vw;
height: 100vh;
`
使い方としては、<Canvas>
内で呼び出すだけ。実行してみましょう。
マウスで動かすことができました!
4. VRMを描画する
ここからがやっと本題。
4-1. モデルファイルを配置する
まずは描画するためのVRMを用意しましょう。
今回はサンプルとして、three-vrm
に入っている女の子をお借りします。可愛い。黒髪ロング最高。
https://github.com/pixiv/three-vrm/blob/dev/examples/models/three-vrm-girl.vrm
用意したVRMファイルはsrcではなく、distやpublicなどの配信ディレクトリに配置しましょう。
イメージとしては以下のような感じ。
├── public/
│ ├── models/
│ │ └── [ ファイル名 ].vrm <-- ここ
│ ├── bundle.js
│ └── index.html
├── src/
│ ├── components/
│ ├── App.tsx
│ └── index.tsx
├── package.json
├── webpack.config.js
└── tsconfig.json
4-2. VRMコンポーネントを作成
VRMファイルを表示するためのコンポーネントを作ります。
import React, { useState, useEffect } from 'react'
import { useLoader } from 'react-three-fiber'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
import { VRM, VRMUtils, VRMSchema } from '@pixiv/three-vrm'
import { Scene, Group } from 'three'
interface Props {
url: string
}
export default function VRMAsset({ url }: Props) {
const [scene, setScene] = useState<Scene | Group | null>(null)
const gltf = useLoader(GLTFLoader, url)
useEffect(() => {
VRMUtils.removeUnnecessaryJoints(gltf.scene)
VRM.from(gltf).then(vrm => {
setScene(vrm.scene)
// 初期描画で背中が映ってしまうので向きを変えてあげる
const boneNode = vrm.humanoid?.getBoneNode(VRMSchema.HumanoidBoneName.Hips)
boneNode?.rotateY(Math.PI)
})
}, [gltf, setScene])
if (scene === null) {
return null
}
return <primitive object={scene} dispose={null} />
}
import React, { Suspense } from 'react'
import VRMAsset from 'utils/VRMAsset'
export default function SampleModel() {
return (
<Suspense fallback={null}>
<VRMAsset url='./models/model.vrm' />
</Suspense>
)
}
Propsで、VRMファイルのurlを受け取ります。ここに、先ほど配置したファイルのパスを指定します。
またVRM.from
はPromiseを返すので、Suspenseで囲ってあげましょう(本当はここでローディングを出すとそれっぽくなったりする)
import React from 'react'
import { Canvas } from 'react-three-fiber'
import styled from 'styled-components'
+ import SampleModel from 'components/SampleBox'
- import SampleBox from 'components/SampleBox'
import Controls from 'utils/Controls'
export default function App() {
return (
<Container>
<Canvas>
+ <SampleModel />
- <SampleBox />
<Controls />
+ <directionalLight position={[1, 1, 1]} />
<gridHelper />
</Canvas>
</Container>
)
}
const Container = styled.div`
width: 100vw;
height: 100vh;
`
さっきまで試してたSampleBox
を、新しく作ったSampleModel
に置き換えます。
それからもう一つ。照明を追加しましょう。
<directionalLight>
ってやつです。これがないと、真っ黒いモデルが出てきます。僕はこれを忘れて、小一時間「ポケモンだ〜れだ」をやってました。
React + react-three-fiber + three-vrmを使ってVRMを表示してみたけど…
— sho (@sskmy1024r) April 10, 2020
テクスチャが表示されない🙄 pic.twitter.com/ZizEldydnZ
できたー!
番外編
今のままだと、カメラの初期位置が低すぎたり遠すぎたりするので、そこら辺を調整しました。最終的なコードがこちら。
import React, { useRef, useEffect } from 'react'
import { extend, ReactThreeFiber, useThree, useFrame } from 'react-three-fiber'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
extend({ OrbitControls })
declare global {
namespace JSX {
interface IntrinsicElements {
readonly orbitControls: ReactThreeFiber.Object3DNode<OrbitControls, typeof OrbitControls>
}
}
}
interface Props extends ReactThreeFiber.Object3DNode<OrbitControls, typeof OrbitControls> {
defaultCameraPosition?: [number, number, number]
}
export default function Controls(props: Props) {
const {
camera,
gl: { domElement }
} = useThree()
const controls = useRef<OrbitControls>()
const { defaultCameraPosition } = props
useFrame(() => controls.current?.update())
useEffect(() => {
if (defaultCameraPosition !== undefined) {
camera.position.set(...defaultCameraPosition)
}
}, [camera, defaultCameraPosition])
return <orbitControls ref={controls} args={[camera, domElement]} screenSpacePanning {...props} />
}
import React from 'react'
import { Canvas } from 'react-three-fiber'
import styled from 'styled-components'
import Controls from 'utils/Controls'
import SampleModel from 'components/SampleModel'
import { Vector3 } from 'three'
export default function App() {
return (
<Container>
<Canvas>
<SampleModel />
<Controls defaultCameraPosition={[0, 1.25, 1]} target={new Vector3(0, 1, 0)} />
<directionalLight position={[1, 1, 1]} />
<gridHelper />
</Canvas>
</Container>
)
}
const Container = styled.div`
width: 100vw;
height: 100vh;
`
defaultCameraPosition
(名前長い)を追加して、初期カメラ位置を指定出来るように。
最終結果
まとめ
VRコンテンツが増える中で、3Dモデルはもっと身近なものになっていく気がします。というかすでに乗り遅れている気が……。
そんな3Dモデルを手軽に扱えて、しかもブラウザ上で動くなんていいですね!
今度はこの3Dモデルを動かしてみたいと思うので、また進展があれば記事にしてみたいと思います。ではでは〜