見出し画像

three.js で VRM を表示する (3) - Unityからのアニメーションのエクスポート

前回、three.jsのアニメーションを実装しましたが、タイムライン情報を手作業で作るのは面倒なので、Unityのアニメーションをエクスポートして利用してみます。

・three.js@0.125.1
・typescript@4.1.3
・webpack@5.18.0

前回

1. UnityでVRMをアニメーション

UnityでVRMをアニメーションさせます。

画像1

詳しくは以下を参照。

2. Unityからのアニメーションのエクスポート

(1) モデル(AliciaSolid)に「Add Component」で以下のスクリプトを追加。
0.2秒毎に55個のボーンの角度をCSVに出力するスクリプトです。

・AnimationExpoter

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO; 

// アニメーションクスポーター
public class AnimationExporter : MonoBehaviour
{
    private float timeleft = 0.2f; // 時間経過
    private float fps = 0.2f; // FPS
   
    // 初期化
    void Start()
    {
        // CSVの削除
        File.Delete(Application.dataPath + "/anim.csv");
    }

    // フレーム毎に呼ばれる
    void Update()
    {
        // FPS制限
        this.timeleft -= Time.deltaTime;
        if (this.timeleft > 0.0) return;
        this.timeleft = this.fps;
       
        // 55個のボーンの角度を表す文字列の生成
        string line = "";
        for (int i = 0; i < 55; i++) {
            Transform bone = GetComponent<Animator>().GetBoneTransform((HumanBodyBones)i);
            if (bone == null) {
                line += string.Format("{0:F3},{1:F3},{2:F3},{3:F3},",0,0,0,0);
            } else {
                line += string.Format("{0:F3},{1:F3},{2:F3},{3:F3},", 
                    bone.localRotation.x,
                    bone.localRotation.y,
                    bone.localRotation.z,
                    bone.localRotation.w);
            }
        }        
       
        // CSVの追加保存
        FileInfo fi = new FileInfo(Application.dataPath + "/anim.csv");
        StreamWriter sw = fi.AppendText();
        sw.WriteLine(line);
        sw.Flush();
        sw.Close();        
    }
}

(2) Unityエディタを実行開始して、アニメーション完了したあたりで実行停止。
成功すると、Assetsに「anim.csv」が出力されています。

・anim.csv

0.030,-0.014,-0.005,0.999,-0.397,0.056,0.039,0.915,-0.172,0.002,-0.025,0.985,0.695,0.024,-0.044,0.718,0.322,-0.002,-0.004,0.947,0.117,-0.016,0.026,0.993,-0.033,-0.021,0.046,0.998,0.060,-0.005,0.001,0.998,0.156,0.014,0.006,0.988,-0.095,0.008,-0.005,0.995,-0.091,0.007,-0.005,0.996,-0.004,-0.062,0.012,0.998,0.000,0.000,0.119,0.993,0.092,-0.046,0.517,0.850,-0.472,-0.405,0.306,0.721,-0.035,0.685,-0.063,0.725,-0.177,-0.035,0.021,0.983,0.021,0.144,0.022,0.989,0.119,-0.046,0.254,0.959,0.000,0.000,0.000,1.000,0.000,0.000,0.000,1.000,0.000,0.000,0.000,1.000,0.000,0.000,0.000,1.000,0.090,0.000,0.000,0.996,0.062,-0.091,0.080,0.991,-0.022,-0.143,-0.002,0.990,0.005,-0.208,0.001,0.978,0.076,-0.156,0.091,0.981,-0.001,-0.001,0.407,0.914,-0.004,-0.002,0.589,0.808,0.021,-0.036,0.137,0.990,0.000,0.000,0.535,0.845,0.000,0.000,0.736,0.677,-0.030,0.048,0.359,0.931,-0.001,0.000,0.279,0.960,0.019,0.007,0.853,0.522,-0.047,0.066,0.089,0.993,0.000,0.000,0.303,0.953,0.021,0.008,0.780,0.625,-0.030,-0.113,0.019,-0.993,0.006,-0.182,-0.001,-0.983,-0.030,-0.267,0.003,-0.963,-0.045,-0.069,0.117,-0.990,0.002,-0.001,0.455,-0.890,0.004,-0.002,0.608,-0.794,0.016,0.073,0.217,-0.973,0.000,0.000,0.504,-0.864,-0.001,0.000,0.675,-0.738,0.058,0.127,0.333,-0.933,0.002,-0.001,0.250,-0.968,-0.015,0.006,0.755,-0.656,0.048,0.076,0.012,-0.996,0.000,0.000,0.308,-0.951,-0.021,0.008,0.785,-0.619,0.000,0.000,0.000,1.000,
0.028,-0.013,-0.004,0.999,0.127,0.035,0.023,0.991,-0.496,-0.005,0.000,0.868,0.216,-0.001,-0.018,0.976,0.490,-0.009,0.006,0.872,0.284,-0.013,-0.021,0.959,0.039,-0.006,0.006,0.999,0.066,-0.005,0.001,0.998,0.157,0.062,-0.001,0.986,-0.075,-0.020,0.000,0.997,-0.075,-0.019,-0.003,0.997,-0.004,-0.062,-0.001,0.998,0.000,0.000,0.119,0.993,-0.114,0.351,0.435,0.821,-0.480,-0.450,0.332,0.676,-0.049,0.700,-0.080,0.707,-0.172,-0.035,0.021,0.984,0.021,0.213,0.030,0.976,0.119,-0.045,0.258,0.958,0.000,0.000,0.000,1.000,0.000,0.000,0.000,1.000,0.000,0.000,0.000,1.000,0.000,0.000,0.000,1.000,0.090,0.000,0.000,0.996,0.062,-0.091,0.080,0.991,-0.022,-0.143,-0.002,0.990,0.005,-0.208,0.001,0.978,0.076,-0.156,0.091,0.981,-0.001,-0.001,0.407,0.914,-0.004,-0.002,0.589,0.808,0.021,-0.036,0.137,0.990,0.000,0.000,0.535,0.845,0.000,0.000,0.736,0.677,-0.030,0.048,0.359,0.931,-0.001,0.000,0.279,0.960,0.019,0.007,0.853,0.522,-0.047,0.066,0.089,0.993,0.000,0.000,0.303,0.953,0.021,0.008,0.780,0.625,-0.030,-0.113,0.019,-0.993,0.006,-0.182,-0.001,-0.983,-0.030,-0.267,0.003,-0.963,-0.045,-0.069,0.117,-0.990,0.002,-0.001,0.455,-0.890,0.004,-0.002,0.608,-0.794,0.016,0.073,0.217,-0.973,0.000,0.000,0.504,-0.864,-0.001,0.000,0.675,-0.738,0.058,0.127,0.333,-0.933,0.002,-0.001,0.250,-0.968,-0.015,0.006,0.755,-0.656,0.048,0.076,0.012,-0.996,0.000,0.000,0.308,-0.951,-0.021,0.008,0.785,-0.619,0.000,0.000,0.000,1.000,
    :

4. three.jsでのアニメーションの利用

(1) distフォルダに「anime.csv」と「bone.txt」を置く。

・bone.txt

hips
leftUpperLeg
rightUpperLeg
leftLowerLeg
rightLowerLeg
leftFoot
rightFoot
spine
chest
neck
head
leftShoulder
rightShoulder
leftUpperArm
rightUpperArm
leftLowerArm
rightLowerArm
leftHand
rightHand
leftToes
rightToes
leftEye
rightEye
jaw
leftThumbProximal
leftThumbIntermediate
leftThumbDistal
leftIndexProximal
leftIndexIntermediate
leftIndexDistal
leftMiddleProximal
leftMiddleIntermediate
leftMiddleDistal
leftRingProximal
leftRingIntermediate
leftRingDistal
leftLittleProximal
leftLittleIntermediate
leftLittleDistal
rightThumbProximal
rightThumbIntermediate
rightThumbDistal
rightIndexProximal
rightIndexIntermediate
rightIndexDistal
rightMiddleProximal
rightMiddleIntermediate
rightMiddleDistal
rightRingProximal
rightRingIntermediate
rightRingDistal
rightLittleProximal
rightLittleIntermediate
rightLittleDistal
upperChest     

(2) 前回のスクリプトに、「anim.csv」と「bone.txt」を読み込む関数を追加。
CSVからAnimationClipのhierarchy生成時に、Unityとthree.jsで左手系と右手系の違いがあるため、XとYにマイナスを付加してます。

  // http → str
  const http2str = (url) => {
    try {
      let request=new XMLHttpRequest()
      request.open("GET",url,false)
      request.send(null)
      if (request.status==200 || request.status==0) {
        return request.responseText.trim()
      }
    } catch (e) {
      console.log(e)
    }
    return null
  }    
 
  // CSV → hierarchy
  const csv2hierarchy = (csv, fps) => {
    // 文字列 → 配列
    let lines =  csv.trim().split('\n')
    let data: number[][] = []
    for (let j=0; j<lines.length; j++) {
      data[j] = []
      let strs = lines[j].split(',')
      for (let i=0; i<55*4; i++) {
        data[j][i] = Number(strs[i])            
      }
    }    
   
    // 配列 → hierarchy
    let hierarchy = []
    for (let i=0; i<55; i++) {
      let keys = []
      for (let j=0; j<data.length; j++) {
        keys[j] = {
          rot: new THREE.Quaternion(-data[j][i*4],-data[j][i*4+1],data[j][i*4+2],data[j][i*4+3]).toArray(),
          time: fps*j                       
        }
      }
      hierarchy[i] = {'keys': keys}
    } 
    return hierarchy
  }

(3) 前回のスクリプトの「ボーンリストの生成」と「AnimationClipの生成」を変更。

    // ボーンリストの生成
    const bones = http2str('./bone.txt').split('\n').map((boneName) => {
      return vrm.humanoid.getBoneNode(boneName)
    })
     
    // AnimationClipの生成
    const clip = THREE.AnimationClip.parseAnimation({
      hierarchy: csv2hierarchy(http2str('./anim.csv'), 200)
    }, bones)

(4) スクリプトを実行。

画像2

5. 全スクリプト

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)
      })
    }
  )
 
  // http → str
  const http2str = (url) => {
    try {
      let request=new XMLHttpRequest()
      request.open("GET",url,false)
      request.send(null)
      if (request.status==200 || request.status==0) {
        return request.responseText.trim()
      }
    } catch (e) {
      console.log(e)
    }
    return null
  }    
 
  // CSV → hierarchy
  const csv2hierarchy = (csv, fps) => {
    // 文字列 → 配列
    let lines =  csv.trim().split('\n')
    let data: number[][] = []
    for (let j=0; j<lines.length; j++) {
      data[j] = []
      let strs = lines[j].split(',')
      for (let i=0; i<55*4; i++) {
        data[j][i] = Number(strs[i])            
      }
    }    
   
    // 配列 → hierarchy
    let hierarchy = []
    for (let i=0; i<55; i++) {
      let keys = []
      for (let j=0; j<data.length; j++) {
        keys[j] = {
          rot: new THREE.Quaternion(-data[j][i*4],-data[j][i*4+1],data[j][i*4+2],data[j][i*4+3]).toArray(),
          time: fps*j                       
        }
      }
      hierarchy[i] = {'keys': keys}
    } 
    return hierarchy
  }
 
  // アニメーションの設定
  const setupAnimation = (vrm) => {
    // ボーンリストの生成
    const bones = http2str('./bone.txt').split('\n').map((boneName) => {
      return vrm.humanoid.getBoneNode(boneName)
    })
     
    // AnimationClipの生成
    const clip = THREE.AnimationClip.parseAnimation({
      hierarchy: csv2hierarchy(http2str('./anim.csv'), 200)
    }, 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;

    // アニメーションの定期処理
    if (mixer) {
      mixer.update(delta)
    }

    // 最終更新時間
    lastTime = time;

    // レンダリング
    renderer.render(scene, camera)
  }
  update()
})

【おまけ】 Humanoidの関節の場所

Humanoidの関節の場所は、以下が参考になる。

画像3

Unity Humanoid Avatarの解説 [VirtualCast]」より。

次回


この記事が気に入ったらサポートをしてみませんか?