https://github.com/ahuglajbclajep/three-vrm-react-example
- https://vrm.dev/
- https://vrm-consortium.org/
- https://github.com/vrm-c/vrm-specification
- https://ja.wikipedia.org/wiki/GlTF
- https://threejs.org/docs/index.html#manual/en/introduction/Useful-links
- https://threejs.org/docs/index.html#manual/en/introduction/Creating-a-scene
- https://threejs.org/docs/index.html#manual/en/introduction/Loading-3D-models
- https://ics.media/tutorial-three/
- https://qiita.com/TakenokoTech/items/b3395d8fb26cf3237f15
- three-vrm を使わずに VRM を触っている例
- https://hub.vroid.com/
- https://qiita.com/saitoeku3/items/cf09c170eb16dca5f0d5
- https://github.com/react-spring/react-three-fiber
- https://blog.camph.net/event/pixiv-2019/
- https://qiita.com/drumath2237/items/2d43d7e3beb7024285ae
- https://www.m3tech.blog/entry/2019/12/19/133408
- https://threejs.org/docs/#api/en/core/Object3D
- https://gist.github.com/drcmda/974f84240a329fa8a9ce04bbdaffc04d
- https://ics.media/tutorial-three/camera_orbitcontrols/
- https://github.com/react-spring/react-three-fiber/blob/v4.0.20/examples/src/demos/dev/GltfAnimation.js
- https://qiita.com/Quarter-lab/items/151f06bddea1fc9cf4d7
- https://github.com/react-spring/react-three-fiber/tree/v4.0.20#using-3rd-party-non-three-namespaced-objects-in-the-scene-graph
- https://qiita.com/hppRC/items/63039b5b4be316a55ef6
- pmndrs/react-three-fiber#11 (comment)
- https://github.com/react-spring/react-three-fiber/blob/v4.0.20/src/three-types.ts
- https://qiita.com/uhyo/items/adf6cb83333a25097f25
- pmndrs/react-three-fiber#278
- https://threejs.org/docs/#api/en/helpers/GridHelper
- https://threejs.org/docs/#api/en/helpers/AxesHelper
- https://vrm.dev/vrm_about/
- https://vrm-consortium.org/
- https://github.com/vrm-c/vrm-specification/blob/master/specification/0.0/README.ja.md
- https://ja.wikipedia.org/wiki/GlTF
JSON として記述された標準的な 3D モデルなどを表現する規格である glTF2.0 をベースに、特に人型のキャラクターについて 座標系, スケール, 初期姿勢, 表情の表現方法, ボーンの入れ方, 一人称視点での視点の位置 などのモデルデータの差異を吸収し統一する目的で作られたファイルフォーマット。
プラットフォーム非依存な形式であり、通常の *.gltf
フォーマットをベースに 一人称視点の再現のための情報, 物理エンジンに依存しない揺れる物の設定, アバターを表示するのに適した専用のマテリアルの設定, ライセンス情報などのメタデータ などが利用できるようになっている。
「一般社団法人VRMコンソーシアム」という VRM の発起人であるドワンゴなどを中心としたグループが仕様などを策定している。
仕様を読むとわかるが、glTF のバイナリ形式である *.glb
に準拠しており、 *.glb
としても読むことが可能。
元々の *.glb
で保存できる値に関して統一性を持たせるためさらに強い制約を設け、VRM 用の専用のデータ領域を追加したという感じ。
- https://threejs.org/docs/index.html#manual/en/introduction/Useful-links
- https://threejs.org/docs/index.html#manual/en/introduction/Creating-a-scene
- https://threejs.org/docs/index.html#manual/en/introduction/Loading-3D-models
- https://ics.media/tutorial-three/
- https://qiita.com/TakenokoTech/items/b3395d8fb26cf3237f15
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight
);
camera.position.z = 5;
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
renderer.render(scene, camera);
基本的に必要なのは scene
, camera
, renderer
で、最終的には renderer.render(scene, camera);
のようにして描画する。
camera
は例えば new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight);
とかで作れる。
第 1 引数は FOV で第 2 引数はアスペクト比。
camera.position.z = 5;
でカメラを少し手前に持ってきている。
renderer
は new THREE.WebGLRenderer()
で作り、実体は <canvas>
で例えば document.body.appendChild(renderer.domElement);
のように反映する。
renderer.setSize()
で画面のサイズや内部解像度の設定ができる。
ここに箱を描画する場合は、まずメッシュを作りこれを scene.add(mesh);
のように scene
に追加する。
メッシュは new THREE.BoxGeometry()
でボーンのような物を作り、 new THREE.MeshBasicMaterial()
で表面の感じを指定し、new THREE.Mesh(geometry, material);
のようにして作る。
描画するだけなら renderer.render(scene, camera);
で終わりだが、動かすには requestAnimationFrame()
を使いメインループを構築する。
以下のようなコードで通常は 60fps で描画される。
requestAnimationFrame()
を使っているため、ユーザーが別のタブを見ている間は勝手に一時停止してくれ充電に優しい。
function animate() {
window.requestAnimationFrame(animate);
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
renderer.render(scene, camera);
}
animate();
基本的には glTF(*.gltf
, *.glb
)が推奨されるが、FBX, OBJ, COLLADA といった形式のものも扱えるらしい。
だいたい以下のような感じで、gltf => { ... }
がモデルのダウンロードが終了すると呼ばれ、ここでメッシュを scene.add()
するのが基本。
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
const loader = new THREE.GLTFLoader();
loader.load(
"path/to/model.glb",
gltf => {
scene.add(gltf.scene);
},
xhr => {
console.log(`${(xhr.loaded / xhr.total) * 100}% loaded`);
},
error => {
console.error(error);
}
);
これはざっと中心部のコードを読んでドキュメントも眺めてみた結果からだが、これはおそらく「モデルデータ *.vrm
を解析し、表情や視線などに関するパーツを探し出し、これを操作する API を構築するライブラリ」だと思われる。
基本的な使い方は以下のような感じ。
vrm
は VRM.from()
の VRM
と同じ VRM
クラスであり、要するに VRM.from()
がこのクラスの static なイニシャライザになっている。
import * as THREE from "three";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import { VRM } from "@pixiv/three-vrm";
const scene = new THREE.Scene();
const loader = new GLTFLoader();
loader.load(
"/models/three-vrm-girl.vrm",
gltf => {
VRM.from(gltf).then(vrm => {
scene.add(vrm.scene);
});
},
progress =>
console.log(
`Loading model... ${(progress.loaded / progress.total) * 100}%`
),
error => console.error(error)
);
ちなみに vrm.scene
は gltf.scene
はこれだけでは特になにもしていないことになる。
three-vrm
の本領はここからで、読み込んだモデルを簡単な API で操作できるというところにある。
例えば vrm.humanoid!.getBoneNode(VRMSchema.HumanoidBoneName.LeftUpperArm).rotation.x =
のようにすれば左腕が動いたり、 vrm.lookAt.target =
のようにすれば視線の方向を変更できたり、vrm.lendShapeProxy.setValue(VRMSchema.BlendShapePresetName.Fun, 0.7)
のようにすれば表情を変えたりといったことが可能になる。
- https://hub.vroid.com/
- https://qiita.com/saitoeku3/items/cf09c170eb16dca5f0d5
- https://github.com/react-spring/react-three-fiber
- https://blog.camph.net/event/pixiv-2019/
- https://qiita.com/drumath2237/items/2d43d7e3beb7024285ae
- https://www.m3tech.blog/entry/2019/12/19/133408
- https://threejs.org/docs/#api/en/core/Object3D
- https://gist.github.com/drcmda/974f84240a329fa8a9ce04bbdaffc04d
three-vrm-sample のようなものを作ってみる。
$ git clone [email protected]:ahuglajbclajep/my-react-template.git
$ cd my-react-template
$ yarn install
$ yarn add three react-three-fiber
とりあえず以下で three.js の最初の緑の四角が描画できる。 メッシュの構築やシーンへ追加する部分が宣言的に書けているという感じ。 背景は黒にしておいたほうがモデルがきれいに見える。
import React from "react";
import { Canvas } from "react-three-fiber";
const App: React.FC = () => {
return (
<Canvas>
<mesh>
<boxBufferGeometry attach="geometry" args={[1, 1, 1]} />
<meshBasicMaterial attach="material" color={0x00ff00} />
</mesh>
</Canvas>
);
};
export default App;
body {
color: white;
background-color: black;
}
#root {
width: 100vw;
height: 100vh;
}
回す場合は以下のように useFrame
と useRef
を使う。
useFrame
を使う場合は、直接 <Canvas><mesh>...</mesh></Canvas>
としないこと。
useFrame
を使うコンポートの JSX のトップレベルは <mesh>
じゃないとダメっぽい。
これは <Canvas>
が Context API の Provider にもなっており、useFrame
の実装で useContext()
されているために起きる。
つまり useFrame()
は <Canvas>
の子コンポーネントでしか動作しないということ。
const App: React.FC = () => {
const cube = useRef<import("three").Mesh>(null);
useFrame(() => {
if (cube.current) {
cube.current.rotation.x += 0.01;
cube.current.rotation.y += 0.01;
}
});
return (
<mesh ref={cube}>
<boxBufferGeometry attach="geometry" args={[1, 1, 1]} />
<meshBasicMaterial attach="material" color={0x00ff00} />
</mesh>
);
};
// see https://github.com/react-spring/react-three-fiber/issues/253
const Wrapper: React.FC = () => {
return (
<Canvas>
<App />
</Canvas>
);
};
yarn add @pixiv/three-vrm
する。
モデルを切り替えられるようにしたいので、まずはモデルを適宜ロードする関数と実際にモデルを操作できる vrm
をエクスポートするような hooks を作る。
呼ばれる度に loader
を作らなくていいようにするため、loader
は useRef()
で持っておく。
あとは初期状態を後からロードするいつのもパターンで書いている。
import { VRM } from "@pixiv/three-vrm";
import { useRef, useState } from "react";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
const useVRM = (): [VRM | null, (_: string) => void] => {
const { current: loader } = useRef(new GLTFLoader());
const [vrm, setVRM] = useState<VRM | null>(null);
const loadVRM = (url: string): void => {
loader.load(url, gltf => {
VRM.from(gltf).then(vrm => setVRM(vrm));
});
};
return [vrm, loadVRM];
};
export { useVRM };
本体はこんな感じで、ファイルを渡すと loadVRM
でデータが読まれ {vrm && <primitive object={vrm.scene} />}
で反映される。
<primitive>
は既存のメッシュをシーンに追加するためのコンポーネント。
このままではシーンが真っ暗なため、<directionalLight />
や <spotLight />
などの適当な光源を配置する。
const App: React.FC = () => {
const [vrm, loadVRM] = useVRM();
const handleFileChange = (
event: React.ChangeEvent<HTMLInputElement>
): void => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const url = URL.createObjectURL(event.target.files![0]);
loadVRM(url);
};
return (
<>
<input type="file" accept=".vrm" onChange={handleFileChange} />
<Canvas>
<directionalLight />
{vrm && <primitive object={vrm.scene} />}
</Canvas>
</>
);
};
初期状態ではモデルは後ろを向いているので、vrm.scene.rotation.y = Math.PI;
で前を向かせる。
これは useVRM()
の loadVRM()
内でやったりすればいい。
カメラは自分で作ることもできるが、デフォルトのものが用意されているのでこれの向きだけ変更する。
基本的にカメラには <Canvas camera={{ position: [0, 1, 2] }}>
のようにアクセスする。
このデフォルトのカメラは、他のコンポーネントからなどでも useThree()
経由で取得でき、例えば以下のようなコードを useVRM()
に追記するとカメラのほうを向いてくれるようになるはず。
import { useThree } from "react-three-fiber";
const { camera } = useThree();
useEffect(() => {
if (vrm && vrm.lookAt) vrm.lookAt.target = camera;
});
自分でカメラを作って設定する場合は以下のようになる。
new PerspectiveCamera()
のところとかは three.js の最初の例と同じ。
//@ts-ignore
がないと型エラーになるので、この方法はあまりよくないのかも。
import { useThree } from "react-three-fiber";
import { PerspectiveCamera } from "three";
const { aspect } = useThree(); // アスペクト比を取得
const { current: camera } = useRef(new PerspectiveCamera(75, aspect);
// camera が一度作られて終わりなので基本的には一度しか呼ばれない
useEffect(() => {
camera.position.set(0, 0.6, 4);
}, [camera]);
return (
<>
<input type="file" accept=".vrm" onChange={handleFileChange} />
<Canvas
//@ts-ignore
camera={camera}>
...
</Canvas>
</>
);
モデルに物理演算を適用するには vrm.update(delta);
のようにする。
当然 useFrame()
内でこれを呼ぶので、とりあえずは VRM.tsx
のようなコンポーネントを作る。
物理演算が有効になっているか確認しやすくするため、vrm.scene.rotation.y
で適当に回転させる。
clock
や delta
は const { clock } = useThree();
からでもとれるが、特に delta
については UseFrame()
の引数でとれるものを使わないと、挙動が不安定になる。
import React from "react";
import { useFrame } from "react-three-fiber";
type Props = {
vrm: import("@pixiv/three-vrm").VRM | null;
};
const VRM: React.FC<Props> = ({ vrm }) => {
useFrame(({ clock }, delta) => {
if (vrm) {
vrm.scene.rotation.y = Math.PI * Math.sin(clock.getElapsedTime());
vrm.update(delta);
}
});
return vrm && <primitive object={vrm.scene} />;
};
export default VRM;
ちなみに以下のようにすると目だけマウスに合わせて動かしたりもできる。
import { Vector3 } from "three";
if (vrm.lookAt) vrm.lookAt.lookAt(new Vector3(...mouse.toArray(), 0));
以下のように three.js の Object3D.lookAt()
を活用すると頭を動かしたりもできるが、座標系の関係か常に後ろを向いてしまうし、人間の関節の可動範囲を超えた動きをしてしまったりするので微妙。
import { VRMSchema } from "@pixiv/three-vrm";
if (vrm && vrm.humanoid) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const head = vrm.humanoid.getBoneNode(VRMSchema.HumanoidBoneName.Head)!;
head.lookAt(mouse.x, mouse.y, 2);
}
three.js は全体で 600KB ほどあり、モジュール毎にインポートはできるものの Tree Shaking には非対応らしい。 というわけでデフォルトでは react-three-fiber を使うとこれが全部ついてくる。 この挙動については three.js をカスタムビルドする場合と同様に、必要なモジュールだけをインポートしたオレオレミニ three.js を用意すればある程度は解決できる。 頑張れば大体 400KB くらいにはできるらしい。 three-minifier-webpack というプラグインを使うと特に何もしなくてもいい感じに必要なファイルだけを読んでバンドルしてくれるそうだが、react-three-fiber を使っていると効果がない模様。
- https://ics.media/tutorial-three/camera_orbitcontrols/
- https://github.com/react-spring/react-three-fiber/blob/v4.0.20/examples/src/demos/dev/GltfAnimation.js
- https://qiita.com/Quarter-lab/items/151f06bddea1fc9cf4d7
- https://github.com/react-spring/react-three-fiber/tree/v4.0.20#using-3rd-party-non-three-namespaced-objects-in-the-scene-graph
- https://qiita.com/hppRC/items/63039b5b4be316a55ef6
- pmndrs/react-three-fiber#11 (comment)
- https://github.com/react-spring/react-three-fiber/blob/v4.0.20/src/three-types.ts
- https://qiita.com/uhyo/items/adf6cb83333a25097f25
- pmndrs/react-three-fiber#278
- https://threejs.org/docs/#api/en/helpers/GridHelper
- https://threejs.org/docs/#api/en/helpers/AxesHelper
マウス操作などをハンドルしてモデルを動かすのには、three.js の OrbitControls
というカメラのコントローラーが使える。
react-three-fiber で OrbitControls
を使う例は react-three-fiber 自体の examples にいくつかあり、大体以下のようなコードを書くことになる。
extends()
は適当なオブジェクトを JSX として書けるようにするヘルパー関数で、OrbitControls
を渡すと <orbitControls />
と書けるようになる。
元のオブジェクトの頭文字が小文字になったコンポーネントが作られ、args
で配列としてコンストラクタの引数が渡せたり、宣言的にプロパティを指定したりできる。
<OrbitControls />
で必須なのは ref
と args
で、enableDamping
で動作を滑らかにし target
でカメラの方向を調整してる。
extend({ OrbitControls });
const Controls: React.FC = () => {
const controls = useRef<OrbitControls>(null);
const { camera, gl } = useThree();
useFrame(() => controls.current?.update());
return (
// @ts-ignore
<orbitControls
ref={controls}
args={[camera, gl.domElement]}
enableDamping
target={new Vector3(0, 1, -2)}
/>
);
};
ただし extends()
で作ったコンポーネントの型定義は存在しないため、 // @ts-ignore
で無視するか以下のようなコードを書く必要がある。
実は react-three-fiber で定義済みのコンポーネントも同様に宣言されている。
typeof OrbitControls
のようにしないと OrbitControls["new"]
ができなので *.d.ts
には書けない。
import { ReactThreeFiber } from "react-three-fiber";
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace JSX {
interface IntrinsicElements {
orbitControls: ReactThreeFiber.Object3DNode<
OrbitControls,
// `OrbitControls["new"]` does not work without `typeof`.
typeof OrbitControls
>;
}
}
}
ちなみに extends()
を使わず下記のように自分でコンポーネントを作る方法もあるが、controls.enableDamping = true;
などが機能しない。
import React, { useEffect, useRef } from "react";
import { useThree } from "react-three-fiber";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
const Controls: React.FC = () => {
const { camera, gl } = useThree();
const { current: controls } = useRef(
new OrbitControls(camera, gl.domElement)
);
useEffect(() => () => controls.current.dispose(), []);
useEffect(() => {
controls.enableDamping = true;
return () => controls.dispose();
}, [controls]);
useFrame(() => controls.update());
return null;
};
余談だが <gridHelper />
と <axesHelper />
を使うと、デバッグに便利な座標系が出せる。