見出し画像

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度。

画像1

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()
})

次回


いいなと思ったら応援しよう!