
three.js で VRM を表示する (6) - BVHファイルのアニメーション再生
「three.js」を使って「VRM」を表示し、「BVHファイル」のアニメーションを再生する方法をまとめました。
・three.js@0.133.1
・@pixiv/three-vrm 0.6.7
・Node.js v16.13.0
前回
1. 開発環境の準備
開発環境の準備手順は、次のとおり。
(1) Node.jsのインストール。
(2) プロジェクトの作成。
$ mkdir helloworld
$ cd helloworld
$ npm init -y
(3) 「webpack」と「live-server」と「three.js」と「@pixib」のインストール。
$ npm i -D webpack webpack-cli
$ npm i -g live-server
$ npm i -S three @pixiv/three-vrm
(4) プロジェクトフォルダ直下の「package.json」の「scripts」を以下のように編集。
・pakage.json
:
"scripts": {
"start": "live-server dist",
"build": "webpack",
"watch": "webpack -w"
},
:
(5) プロジェクトフォルダ直下に「webpack.config.js」を作成し、以下のように編集。
・webpack.config.js
module.exports = {
mode: "development",
entry: "./src/index.js",
output: {
path: `${__dirname}/dist`,
filename: "main.js"
},
resolve: {
extensions: [".js"]
}
};
2. VRMの表示
VRMを表示するだけのコードを書きます。
(1) プロジェクトフォルダ直下に 「src」フォルダを生成し、「src」フォルダ直下に「index.js」を作成し、以下のように編集。
・src/index.js
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 camera = new THREE.PerspectiveCamera(
45,
window.innerWidth / window.innerHeight,
0.1,
1000
);
// レンダラーの準備
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0x7fbfff, 1.0);
document.body.appendChild(renderer.domElement);
// ライトの準備
const light = new THREE.DirectionalLight(0xffffff);
light.position.set(-1, 1, -1).normalize();
scene.add(light);
// VRMの読み込み
const loader = new GLTFLoader();
loader.load("./alicia.vrm", (gltf) => {
VRM.from(gltf).then((vrm) => {
// 姿勢の指定
vrm.scene.position.y = -1;
vrm.scene.position.z = -3;
vrm.scene.rotation.y = Math.PI;
// シーンへの追加
scene.add(vrm.scene);
});
});
// アニメーションループの開始
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
animate();
(2) プロジェクトフォルダ直下に 「dist」フォルダを生成し、「dist」フォルダ直下に「index.html」を作成し、以下のように編集。
・dist/index.html
<html>
<head>
<meta charset="utf-8">
<style>
body { margin: 0; }
</style>
</head>
<body>
<script type="text/javascript" src="main.js"></script>
</body>
</html>
(3) 「ニコニ立体ちゃん (VRM)」からダウンロードして、「dist」フォルダ直下に「alicia.vrm」という名前で配置。
(4) ビルドと実行。
$ npm run build
$ npm run start
ブラウザが起動し、VRMが表示されます。

3. BVHファイルの準備
「BVHファイル」(Biovision Hierarchy)は、ボーンの階層構造を含むモーション定義ファイルです。モーションキャプチャデータの保存などに使われます。
(1) 以下のサイトからバク転するアニメーション「85_02.bvh」を取得し、distフォルダ直下に配置。
他のアニメーションでも、コードのファイル名を変えれば動きます。

4. BVHファイルの読み込み
BVHファイルの読み込み手順は、次のとおりです。
(1) index.jsを以下のように編集。
import * as THREE from "three";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import { BVHLoader } from "three/examples/jsm/loaders/BVHLoader";
import { VRM, VRMSchema } from "@pixiv/three-vrm";
// シーンの準備
const scene = new THREE.Scene();
// カメラの準備
const camera = new THREE.PerspectiveCamera(
45,
window.innerWidth / window.innerHeight,
0.1,
1000
);
// レンダラーの準備
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0x7fbfff, 1.0);
document.body.appendChild(renderer.domElement);
// ライトの準備
const light = new THREE.DirectionalLight(0xffffff);
light.position.set(-1, 1, -1).normalize();
scene.add(light);
// アニメーションの準備
let mixer = null;
// VRMの読み込み
const loader = new GLTFLoader();
loader.load("./alicia.vrm", (gltf) => {
VRM.from(gltf).then((vrm) => {
// 姿勢の指定
vrm.scene.position.y = -1;
vrm.scene.position.z = -3;
vrm.scene.rotation.y = Math.PI;
// シーンへの追加
scene.add(vrm.scene);
// BVHの読み込み
const loader = new BVHLoader();
//01_01, 85_02
loader.load("85_02.bvh", function (bvh) {
// AnimationClipの生成
const clip = createClip(vrm, bvh);
// AnimationMixerの生成
mixer = new THREE.AnimationMixer(vrm.scene);
mixer.clipAction(clip).setEffectiveWeight(1.0).play();
});
});
});
// アニメーションループの開始
let lastTime = new Date().getTime();
function animate() {
requestAnimationFrame(animate);
// AnimationMixerの更新
let time = new Date().getTime();
if (mixer) mixer.update(time - lastTime);
lastTime = time;
renderer.render(scene, camera);
}
animate();
// トラックの取得
function findTrack(name, tracks) {
for (let i = 0; i < tracks.length; i++) {
if (tracks[i].name == name) return tracks[i];
}
return null;
}
// 配列をQuaternionに変換
function values2quaternion(values, i) {
return new THREE.Quaternion(
values[i * 4],
values[i * 4 + 1],
values[i * 4 + 2],
values[i * 4 + 3]
);
}
// キーリストの生成
function createKeys(id, tracks) {
const posTrack = findTrack(".bones[" + id + "].position", tracks);
const rotTrack = findTrack(".bones[" + id + "].quaternion", tracks);
const keys = [];
const rate = 0.008; // サイズの調整
for (let i = 0; i < posTrack.times.length; i++) {
const key = {};
// 時間
key["time"] = parseInt(posTrack.times[i] * 1000);
// 回転
if (id == "rButtock" || id == "lButtock") {
const id2 = id == "rButtock" ? "rThigh" : "lThigh";
let q1 = values2quaternion(rotTrack.values, i);
const rotTrack2 = findTrack(".bones[" + id2 + "].quaternion", tracks);
q1.multiply(values2quaternion(rotTrack2.values, i));
key["rot"] = [-q1.x, q1.y, -q1.z, q1.w];
} else {
key["rot"] = [
-rotTrack.values[i * 4],
rotTrack.values[i * 4 + 1],
-rotTrack.values[i * 4 + 2],
rotTrack.values[i * 4 + 3],
];
}
// 位置
if (id == "hip") {
key["pos"] = [
-posTrack.values[i * 3] * rate,
posTrack.values[i * 3 + 1] * rate,
-posTrack.values[i * 3 + 2] * rate,
];
}
keys.push(key);
}
if (keys.length == 0) return null;
return keys;
}
// クリップの生成
function createClip(vrm, bvh) {
// ボーンリストの生成
const nameList = [
VRMSchema.HumanoidBoneName.Head,
VRMSchema.HumanoidBoneName.Neck,
VRMSchema.HumanoidBoneName.Chest,
VRMSchema.HumanoidBoneName.Spine,
VRMSchema.HumanoidBoneName.Hips,
VRMSchema.HumanoidBoneName.RightShoulder,
VRMSchema.HumanoidBoneName.RightUpperArm,
VRMSchema.HumanoidBoneName.RightLowerArm,
VRMSchema.HumanoidBoneName.RightHand,
VRMSchema.HumanoidBoneName.LeftShoulder,
VRMSchema.HumanoidBoneName.LeftUpperArm,
VRMSchema.HumanoidBoneName.LeftLowerArm,
VRMSchema.HumanoidBoneName.LeftHand,
VRMSchema.HumanoidBoneName.RightUpperLeg,
VRMSchema.HumanoidBoneName.RightLowerLeg,
VRMSchema.HumanoidBoneName.RightFoot,
VRMSchema.HumanoidBoneName.LeftUpperLeg,
VRMSchema.HumanoidBoneName.LeftLowerLeg,
VRMSchema.HumanoidBoneName.LeftFoot,
];
const idList = [
"head",
"neck",
"chest",
"abdomen",
"hip",
"rCollar",
"rShldr",
"rForeArm",
"rHand",
"lCollar",
"lShldr",
"lForeArm",
"lHand",
"rButtock",
"rShin",
"rFoot",
"lButtock",
"lShin",
"lFoot",
];
const bones = nameList.map((boneName) => {
return vrm.humanoid.getBoneNode(boneName);
});
// AnimationClipの生成
const hierarchy = [];
for (let i = 0; i < idList.length; i++) {
const keys = createKeys(idList[i], bvh.clip.tracks);
if (keys != null) {
hierarchy.push({ keys: keys });
}
}
const clip = THREE.AnimationClip.parseAnimation(
{ hierarchy: hierarchy },
bones
);
// トラック名の変更
clip.tracks.some((track) => {
track.name = track.name.replace(
/^\.bones\[([^\]]+)\].(position|quaternion|scale)$/,
"$1.$2"
);
});
return clip;
}
BVHLoaderでBVHを読み込んだあと、VRM用のAnimationClipに変換してます。BVHとVRMは仕様の違いがあるので、調整処理を入れています。
・BVHに1=1mのような決められたサイズ仕様はないので、パラメータで調整。
const rate = 0.008; // サイズの調整
・BVHとVRMのモデルの体型が違うので、位置を使うのは「hip」のみ。
・X軸とZ軸の向きが逆。(BVHに軸の仕様はある?)
// 位置
if (id == "hip") {
key["pos"] = [
-posTrack.values[i * 3] * rate,
posTrack.values[i * 3 + 1] * rate,
-posTrack.values[i * 3 + 2] * rate,
];
}
・BVHにはUpplerLegにあたる関節が2つ(ButtockとThigh)あるので乗算値を利用。
// 回転
if (id == "rButtock" || id == "lButtock") {
// BVHにはUpperLegに2つ関節あるので掛け合わせる
const id2 = id == "rButtock" ? "rThigh" : "lThigh";
let q1 = values2quaternion(rotTrack.values, i);
const rotTrack2 = findTrack(".bones[" + id2 + "].quaternion", tracks);
q1.multiply(values2quaternion(rotTrack2.values, i));
key["rot"] = [-q1.x, q1.y, -q1.z, q1.w];
} else {
(2) ビルドと実行。
$ npm run build
$ npm run start
ニコニコ立体ちゃんがバク転してくれます。
