
three.js で VRM を表示する (2) - キーフレームアニメーション
three.jsのアニメーションシステムを使って、キーフレームアニメーションを実装します。
・three.js@0.125.1
・typescript@4.1.3
・webpack@5.18.0
前回
1. キーフレームアニメーション
「キーフレームアニメーション」は、重要なポーズをキーフレームとして設定することで、キーフレーム間のポーズを自動補間するアニメーションです。
今回は、以下のキーフレームを設定します。
・0秒 : 首の角度0度。
・1秒 : 首の角度-40度。
・2秒 : 首の角度0度。

2. three.jsのアニメーションシステム
「three.js」は以下のクラスで「キーフレームアニメーション」を実現します。
・AnimationClip : アニメーションのタイムライン情報を持つ。
・AnimationAction : 個別のAnimationClipの実行と状態管理。
・AnimationMixer : 複数のAnimationActionの管理とミックス。
3. ボーンリストの生成
はじめに、アニメーションを適用するボーンのリスト生成します。
ボーンは「位置」「回転」「スケール」を指定することで、モデルのポーズを変更することができるオブジェクトです。
// ボーンリストの生成
const bones = [
VRMSchema.HumanoidBoneName.Head
].map((boneName) => {
return vrm.humanoid.getBoneNode(boneName)
})
4. AnimationClipの生成
AnimationClipを生成します。アニメーション開始から何ミリ秒時に、ボーンの「位置」「回転」「スケール」の値に何を指定するかというタイムライン情報を保持します。
// AnimationClipの生成
const clip = THREE.AnimationClip.parseAnimation({
hierarchy: [
{
keys: [
{
rot: new THREE.Quaternion().setFromEuler(new THREE.Euler(0, 0, 0)).toArray(),
time: 0
},
{
rot: new THREE.Quaternion().setFromEuler(new THREE.Euler(-40*Math.PI/180, 0, 0)).toArray(),
time: 1000
},
{
rot: new THREE.Quaternion().setFromEuler(new THREE.Euler(0, 0, 0)).toArray(),
time: 2000
}
]
}
]
}, bones)
VRMでAnimationClipを使う時は、トラック名を変更する必要があります。
.bones[Head].quaternion
↓
Head.quaternion
// トラック名の変更
clip.tracks.some((track) => {
track.name = track.name.replace(/^\.bones\[([^\]]+)\].(position|quaternion|scale)$/, '$1.$2')
})
5. AnimationMixerの生成
AnimationMixerを生成します。
// AnimationMixerの生成と再生
mixer = new THREE.AnimationMixer(vrm.scene)
フレーム毎の定期処理で、AnimationMixerのupdate()を呼びます。
// 最終更新時間
let lastTime = (new Date()).getTime()
// フレーム毎に呼ばれる
const update = () => {
requestAnimationFrame(update)
// 時間計測
let time = (new Date()).getTime()
let delta = time - lastTime;
// AnimationMixerの定期処理
if (mixer) {
mixer.update(delta)
}
// 最終更新時間
lastTime = time;
// レンダリング
renderer.render(scene, camera)
}
update()
6. AnimationActionの生成とアニメーションの再生
AnimationActionを生成し、play()でアニメーションの再生を行います。
// AnimationActionの生成とアニメーションの再生
let action = mixer.clipAction(clip)
action.play()
AnimationActionの主なプロパティは、次のとおりです。
・loop : ループ。
・THREE.LoopOnce : 1度だけ再生。
・THREE.LoopRepeat : 順方向にリピート再生。
・THREE.LoopPingPong : 順・逆方向交互にリピート再生。
・repetitions : リピート回数。
・weight : 重み (0〜1)。
AnimationActionの主なメソッドは、次のとおりです。
・play() : アニメーション再生開始。
・reset() : リセット。
・stop() : アニメーション再生停止。
・isRunning() : アニメーション再生中かどうか。
・fadeIn(durationInSeconds) : フェードイン。
・fadeOut(durationInSeconds) : フェードアウト。
・crossFadeFrom(fadeOutAction, durationInSeconds, warpBoolean) : クロスフェード。
・crossFadeTo(fadeInAction, durationInSeconds, warpBoolean) : クロスフェード。
詳しくは、APIリファレンスを参照。
7. 全スクリプト
import * as THREE from 'three'
import {GLTFLoader} from 'three/examples/jsm/loaders/GLTFLoader'
import {VRM, VRMSchema} from '@pixiv/three-vrm'
window.addEventListener("DOMContentLoaded", () => {
// canvasの取得
const canvas = document.getElementById('canvas')
// シーンの生成
const scene = new THREE.Scene()
// カメラの生成
const camera = new THREE.PerspectiveCamera(
45, canvas.clientWidth/canvas.clientHeight, 0.1, 1000)
camera.position.set(0, 1.3, -1)
camera.rotation.set(0, Math.PI, 0)
// レンダラーの生成
const renderer = new THREE.WebGLRenderer()
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setSize(canvas.clientWidth, canvas.clientHeight)
renderer.setClearColor(0x7fbfff, 1.0)
canvas.appendChild(renderer.domElement)
// ライトの生成
const light = new THREE.DirectionalLight(0xffffff)
light.position.set(-1, 1, -1).normalize()
scene.add(light)
// VRMの読み込み
let mixer
const loader = new GLTFLoader()
loader.load('./alicia.vrm',
(gltf) => {
VRM.from(gltf).then((vrm) => {
// シーンへの追加
scene.add(vrm.scene)
// アニメーションの設定
setupAnimation(vrm)
})
}
)
// アニメーションの設定
const setupAnimation = (vrm) => {
// ボーンリストの生成
const bones = [
VRMSchema.HumanoidBoneName.Head
].map((boneName) => {
return vrm.humanoid.getBoneNode(boneName)
})
// AnimationClipの生成
const clip = THREE.AnimationClip.parseAnimation({
hierarchy: [
{
keys: [
{
rot: new THREE.Quaternion().setFromEuler(new THREE.Euler(0, 0, 0)).toArray(),
time: 0
},
{
rot: new THREE.Quaternion().setFromEuler(new THREE.Euler(-40*Math.PI/180, 0, 0)).toArray(),
time: 1000
},
{
rot: new THREE.Quaternion().setFromEuler(new THREE.Euler(0, 0, 0)).toArray(),
time: 2000
}
]
}
]
}, bones)
// トラック名の変更
clip.tracks.some((track) => {
track.name = track.name.replace(/^\.bones\[([^\]]+)\].(position|quaternion|scale)$/, '$1.$2')
})
// AnimationMixerの生成
mixer = new THREE.AnimationMixer(vrm.scene)
// AnimationActionの生成とアニメーションの再生
let action = mixer.clipAction(clip)
action.play()
}
// 最終更新時間
let lastTime = (new Date()).getTime()
// フレーム毎に呼ばれる
const update = () => {
requestAnimationFrame(update)
// 時間計測
let time = (new Date()).getTime()
let delta = time - lastTime;
// AnimationMixerの定期処理
if (mixer) {
mixer.update(delta)
}
// 最終更新時間
lastTime = time;
// レンダリング
renderer.render(scene, camera)
}
update()
})