Open7
React と VRM
環境構築
npx create-next-app@latest . --ts --eslint
yarn add three @react-three/fiber @react-three/drei @pixiv/three-vrm
yarn add -D @types/three
シンプルに VRM を表示
import { Canvas, useLoader } from "@react-three/fiber";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
const SampleModel = () => {
const gltf = useLoader(GLTFLoader, "/vrms/model.vrm");
return <primitive object={gltf.scene} />;
};
export default function DemoPage() {
return (
<div className="h-screen">
<Canvas>
<SampleModel />
</Canvas>
</div>
);
}
VRM 1.0 の仕様を読む
VRM は、現状 0.0 と 1.0 のバージョンがあり、アプリケーションによって扱う
2018 年に発表した 3D アバターファイルフォーマット「VRM」において、 利用が進むにつれて判明した課題やエラーを修正し、 今後必要になると考えられる機能を追加した規格となります。
従来のVRMは今後 VRM 0.x と分類され非推奨となり、今後は VRM 1.0 が正式な規格となる予定です。
VRM 1.0 は、2022年9月にリリースされました。
VRM 1.0 の仕様が公開されているリポジトリである vrm-specification を見てみると、いくつかのドキュメントがある。
specification/
├── 0.0
├── VRMC_materials_hdr_emissiveMultiplier-1.0
├── VRMC_materials_mtoon-1.0
├── VRMC_node_constraint-1.0
├── VRMC_springBone-1.0
├── VRMC_vrm-1.0
└── VRMC_vrm_animation-1.0
VRMC_vrm-1.0/README.ja.md
メモ
概要
- glTF はシーンを表現するのに対して、VRM は VR アバター向けの人間型モデル一体を表現する
モデル空間
- VRM では、 VRM モデルを構成する glTF シーンの原点から相対にトランスフォームを観測する「モデル空間」を定義する。これは、 VRM モデルを扱うアプリケーション上のワールド空間とは区別される
- モデル空間は
VRMC_node_constraint
拡張で利用される
形式と拡張子
-
.glb
形式で保存し、.vrm
を用いる
座標の単位
glTF に準拠してメートル単位を用いる
JSON Schema
{
"extensionsUsed": [
"VRMC_vrm"
],
"extensions": {
"VRMC_vrm": {
// VRM extension
"specVersion": "1.0",
"humanoid": {},
"meta": {},
"firstPerson": {},
"expression": {},
"lookAt": {},
},
"VRMC_springBone": {},
"VRMC_node_constraint": {}
},
// glTF-2.0
"materials": [
"extensions": {
"VMRC_materials_mtoon": {}
}
],
}
VRMC_vrm.humanoid
ノードへのヒューマノイドボーンの割当て (必須)
- 人型モデルを定義するために、人体の部位を
glTF.Node
に割り当てる
VRMC_vrm.meta
モデル情報(必須)
VRMC_vrm.firstPerson
一人称(オプション)
- VR を想定した一人称視点の設定を定義
Expression、LookAt、SpringBone、Constraints の適用順序
- ヒューマノイドボーンを解決
- 頭の位置が決まるため、 LookAt を解決
- Bone型:leftEye, rightEye ボーンを回転する
- Expression型:
- ExpressionUpdate
- 喜怒哀楽 コントローラなど外部入力 → Expression ウェイトを設定
- LipSync → Expression ウェイトを設定
- AutoBlink → Expression ウェイトを設定
- Expression 型の LookAt → Expression のウェイトを設定
- Expression を Apply
- Constraint を解決
- SpringBone を解決
VRMC_vrm.expression
表情(オプション)
VRMC_vrm.lookAt
視線制御(オプション)
VRMC_vrm.humanoid.ja.md
extensions.VRMC_vrm.humanoid = {
"humanBones": {
"hips": {
"node": 1 // index of glTF.Node
},
"spine": {
"node": 2
},
// 省略
}
}
ヒューマノイドボーンの一覧
胴
ボーン名前 | 必須 | 親ボーン | 位置の目安 | 親ボーンの存在が必須 | 備考 |
---|---|---|---|---|---|
hips | 必須 | (root) | 股間 | - | 通常、このボーンだけが移動し、他のボーンは回転だけします |
spine | 必須 | hips | 骨盤の上端 | - | |
chest | spine | 胸郭の下端 | - | 0.X では必須でした | |
upperChest | chest | Yes | chest が存在する場合にのみ、このボーンは存在できます | ||
neck | upperChest | 首の付け根 | No | 0.X では必須でした |
頭
ボーン名前 | 必須 | 親ボーン | 位置の目安 | 親ボーンの存在が必須 | 備考 |
---|---|---|---|---|---|
head | 必須 | neck | 首の上端 | No | |
leftEye | head | - | VRMC_vrm.lookAt 視線制御(オプション) |
||
rightEye | head | - | VRMC_vrm.lookAt 視線制御(オプション) |
||
jaw | head | - |
脚
ボーン名前 | 必須 | 親ボーン | 位置の目安 | 親ボーンの存在が必須 | 備考 |
---|---|---|---|---|---|
leftUpperLeg | 必須 | hips | 脚の付け根 | - | |
leftLowerLeg | 必須 | leftUpperLeg | 膝 | - | |
leftFoot | 必須 | leftLowerLeg | 足首 | - | |
leftToes | leftFoot | 足の指の付け根 | - | ||
rightUpperLeg | 必須 | hips | 脚の付け根 | - | |
rightLowerLeg | 必須 | rightUpperLeg | 膝 | - | |
rightFoot | 必須 | rightLowerLeg | 足首 | - | |
rightToes | rightFoot | 足の指の付け根 | - |
腕
ボーン名前 | 必須 | 親ボーン | 位置の目安 | 親ボーンの存在が必須 | 備考 |
---|---|---|---|---|---|
leftShoulder | upperChest | No | |||
leftUpperArm | 必須 | leftShoulder | 上腕の付け根 | No | |
leftLowerArm | 必須 | leftUpperArm | 肘 | - | |
leftHand | 必須 | leftLowerArm | 手首 | - | |
rightShoulder | upperChest | No | |||
rightUpperArm | 必須 | rightShoulder | 上腕の付け根 | No | |
rightLowerArm | 必須 | rightUpperArm | 肘 | - | |
rightHand | 必須 | rightLowerArm | 手首 | - |
指
ボーン名前 | 必須 | 親ボーン | 位置の目安 | 親ボーンの存在が必須 | 備考 |
---|---|---|---|---|---|
leftThumbMetacarpal | leftHand | - | |||
leftThumbProximal | leftThumbMetacarpal | Yes | |||
leftThumbDistal | leftThumbProximal | Yes | |||
leftIndexProximal | leftHand | - | |||
leftIndexIntermediate | leftIndexProximal | Yes | |||
leftIndexDistal | leftIndexIntermediate | Yes | |||
leftMiddleProximal | leftHand | - | |||
leftMiddleIntermediate | leftMiddleProximal | Yes | |||
leftMiddleDistal | leftMiddleIntermediate | Yes | |||
leftRingProximal | leftHand | - | |||
leftRingIntermediate | leftRingProximal | Yes | |||
leftRingDistal | leftRingIntermediate | Yes | |||
leftLittleProximal | leftHand | - | |||
leftLittleIntermediate | leftLittleProximal | Yes | |||
leftLittleDistal | leftLittleIntermediate | Yes | |||
rightThumbMetacarpal | rightHand | - | |||
rightThumbProximal | rightThumbMetacarpal | Yes | |||
rightThumbDistal | rightThumbProximal | Yes | |||
rightIndexProximal | rightHand | - | |||
rightIndexIntermediate | rightIndexProximal | Yes | |||
rightIndexDistal | rightIndexIntermediate | Yes | |||
rightMiddleProximal | rightHand | - | |||
rightMiddleIntermediate | rightMiddleProximal | Yes | |||
rightMiddleDistal | rightMiddleIntermediate | Yes | |||
rightRingProximal | rightHand | - | |||
rightRingIntermediate | rightRingProximal | Yes | |||
rightRingDistal | rightRingIntermediate | Yes | |||
rightLittleProximal | rightHand | - | |||
rightLittleIntermediate | rightLittleProximal | Yes | |||
rightLittleDistal | rightLittleIntermediate | Yes |
ヒューマノイドボーンの親子関係
- root(ヒューマノイドボーンではない。原点)
- hips
- spine
- (chest)
- (upperChest)
- (neck)
- head
- (leftEye)
- (rightEye)
- (jaw)
- head
- (leftShoulder)
- leftUpperArm
- leftLowerArm
- leftHand
- (fingers) 省略
- leftHand
- leftLowerArm
- leftUpperArm
- (rightShoulder)
- rightUpperArm
- rightLowerArm
- rightHand
- (fingers) 省略
- rightHand
- rightLowerArm
- rightUpperArm
- (neck)
- (upperChest)
- (chest)
- leftUpperLeg
- leftLowerLeg
- leftFoot
- (leftToes)
- leftFoot
- leftLowerLeg
- rightUpperLeg
- rightLowerLeg
- rightFoot
- (leftToes)
- rightFoot
- rightLowerLeg
- spine
- hips
VRMC_vrm-1.0/meta.ja.md
meta
フィールドでは、モデルに関するメタ情報を記述する
- モデルの名前
- モデルの作者
- モデルの利用条件、ライセンス
名前 | 値 | 説明 | 必須 |
---|---|---|---|
name | string |
モデルの名前 | ✅ Yes |
version | string |
モデルのバージョン | No |
authors | string[] |
モデルの作者名 | ✅ Yes |
copyrightInformation | string |
モデルの著作権者 | No |
contactInformation | string |
モデルの作者(代表者)への連絡先 | No |
references | string[] |
モデルの「親作品」となるようなものがあれば、その情報 | No |
thirdPartyLicenses | string |
モデルのサードパーティライセンス表記 | No |
thumbnailImage | integer |
モデルのサムネイルとなる画像へのインデックス | No |
licenseUrl | string |
このモデルが参照するVRMライセンス文書へのURL | ✅ Yes |
avatarPermission | string |
このモデルに人格を与えることの許諾範囲 | No, 初期値: OnlyAuthor
|
allowExcessivelyViolentUsage | boolean |
このモデルの過剰な暴力表現を含むコンテンツでの利用を許可するか | No, 初期値: false
|
allowExcessivelySexualUsage | boolean |
このモデルの過剰な性的表現を含むコンテンツでの利用を許可するか | No, 初期値: false
|
commercialUsage | string |
このモデルを利用した商用利用の許可範囲 | No, 初期値: personalNonProfit
|
allowPoliticalOrReligiousUsage | boolean |
このモデルの政治・宗教用途での利用を許可するか | No, 初期値: false
|
allowAntisocialOrHateUsage | boolean |
このモデルの反社会的・憎悪表現を含むコンテンツでの利用を許可するか | No, 初期値: false
|
creditNotation | string |
このモデルのクレジット表記の強制および放棄の指定 | No, 初期値: required
|
allowRedistribution | boolean |
このモデルの再配布を許可するか | No, 初期値: false
|
modification | string |
このモデルの改変の許可範囲 | No, 初期値: prohibited
|
otherLicenseUrl | string |
その他のライセンス条件があれば、そのURL | No |
VRM に首を振らせる
import { VRM, VRMLoaderPlugin } from "@pixiv/three-vrm";
import { useFrame } from "@react-three/fiber";
import { useEffect, useState } from "react";
import { Object3D } from "three";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
type AgentProps = {};
type BonesState = {
neck: Object3D | undefined | null;
};
const loader = new GLTFLoader();
loader.register((parser) => new VRMLoaderPlugin(parser));
function Agent(props: AgentProps) {
// const { scene, camera } = useThree();
const [bonesState, setBones] = useState<BonesState>();
const [vrmState, setVrm] = useState<VRM>();
useEffect(() => {
loader.load("VRM1_Constraint_Twist_Sample.vrm", (gltf) => {
const vrm = gltf.userData.vrm as VRM;
setVrm(vrm);
const bones = {
neck: vrm.humanoid.getNormalizedBoneNode("neck"),
};
setBones(bones);
});
}, []);
useFrame(({ clock }, delta) => {
if (vrmState) vrmState.update(delta);
const t = clock.getElapsedTime();
if (bonesState?.neck) {
bonesState.neck.rotation.x = (Math.PI / 4) * Math.sin(t * Math.PI);
}
});
return vrmState && <primitive object={vrmState.scene} />;
}
export default Agent;