three.js で VRM を表示する (3) - Unityからのアニメーションのエクスポート
前回、three.jsのアニメーションを実装しましたが、タイムライン情報を手作業で作るのは面倒なので、Unityのアニメーションをエクスポートして利用してみます。
前回
1. UnityでVRMをアニメーション
UnityでVRMをアニメーションさせます。
詳しくは以下を参照。
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) スクリプトを実行。
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の関節の場所は、以下が参考になる。
「Unity Humanoid Avatarの解説 [VirtualCast]」より。
次回
この記事が気に入ったらサポートをしてみませんか?