一:應用場景
在工作中,由于算法給到的動畫檔案是
Unity
的
.anim
格式動畫檔案,這個格式不能直接在Web端用
Three.js
引擎運作。是以需要将
.anim
格式的動畫檔案轉換為
Three.js
的
AnimationClip
動畫對象。
二:.ANIM格式與AnimationClip對象的差異
1. AnimationClip對象格式如下:
// AnimationClip
{
duration: Number // 持續時間
name: String // 名稱
tracks: [ // 動畫所有屬性的關鍵幀軌道數組
{
name: String // 關鍵幀軌道辨別符
times: Float32Array // 時間數組
values: Float32Array // 與時間數組中的時間點對應的相關值
interpolation: Constant // 使用的插值類型
},
{...}
]
uuid: String // 執行個體的uuid
}
2. Unity的.anim格式如下:
它是用
YAML
寫的,這是一個專門用來寫配置檔案的語言。
注意坑點:unity的.anim用的是yaml 1.1版本, yaml現在新版是1.2.x了。解析的時候注意版本是否相容。我用
js-yaml
解析的時候發現它不相容1.1舊版了,
Unity (Game Engine) Yaml parsing #100
降
js-yaml
版本後解決
"js-yaml": "^3.6.1"
,
.anim格式化後的内容如下:
{
"AnimationClip": {
"m_ObjectHideFlags": 0,
"m_CorrespondingSourceObject": {
"fileID": 0
},
"m_PrefabInstance": {
"fileID": 0
},
"m_PrefabAsset": {
"fileID": 0
},
"m_Name": "Take 001",
"serializedVersion": 6,
"m_Legacy": 0,
"m_Compressed": 0,
"m_UseHighQualityCurve": 1,
"m_RotationCurves": [],
"m_CompressedRotationCurves": [],
"m_EulerCurves": [],
"m_PositionCurves": [],
"m_ScaleCurves": [],
"m_FloatCurves": [],
"m_PPtrCurves": [],
"m_SampleRate": 30,
"m_WrapMode": 0,
"m_Bounds": {},
"m_ClipBindingConstant": {},
"m_AnimationClipSettings": {},
"m_EditorCurves": [],
"m_EulerEditorCurves": [],
"m_HasGenericRootTransform": 0,
"m_HasMotionFloatCurves": 0,
"m_Events": []
}
}
三: anim格式轉AnimationClip對象格式
1. 骨骼蒙皮動畫
.anim檔案的時間資訊很可能不是按每幀給出的,如果直接轉換為AnimationClip格式,沒有進行插值運算(算出每一幀的資訊),這樣用three.js運作起來的實際效果會卡頓。
目前從網上找了個帶動畫的模型,測了下效果:
模型對象裡的原始AnimationClip運作效果(每秒30幀)
Unity動畫轉Three.js動畫: 模型原始的骨骼動畫效
将模型導入Unity後,生成.anim動畫檔案。再通過腳本将這個.anim動畫檔案 轉換為 AnimationClip對象 的運作效果如下:(沒有進行插值,缺幀導緻有點卡頓)
Unity動畫轉Three.js動畫: 轉換後卡頓的骨骼動畫
2. 頂點變形動畫(3d捏臉)
blendshape
動畫的轉換,沒有骨骼蒙皮動畫轉換缺幀的問題。它隻需要有初始值和末值,
three.js
會進行插值運算。
四:關鍵代碼:
import * as THREE from 'three';
interface AnimationClip {
name: string,
duration: number,
tracks: any[],
uuid: string,
}
const get_three_js_track_type: any = {
"scale": "vector",
"quaternion": "quaternion",
"position": "vector",
}
const parse_unity_curve = (curve: any, curve_type: string) => {
const type = get_three_js_track_type[curve_type];
const name = curve.path.split('/').slice(-1) + '.' + curve_type;
const values = [];
const times = [];
for (let cc of curve.curve.m_Curve) {
times.push(cc.time)
if (curve_type == "quaternion") {
values.push(cc.value.x)
values.push(-cc.value.y)
values.push(-cc.value.z)
values.push(cc.value.w)
} else if (curve_type == "position") {
values.push(-cc.value.x * 100)
values.push(cc.value.y * 100)
values.push(cc.value.z * 100)
} else if (curve_type == 'scale') {
values.push(cc.value.x)
values.push(cc.value.y)
values.push(cc.value.z)
}
}
// if (curve_type == "quaternion") {
// return new THREE.AnimationClip(name, times, values);
// }
// if (curve_type == "position") {
// return new THREE.VectorKeyframeTrack(name, times, values);
// }
return {
type,
name,
times,
values,
}
}
const getAnimateClip = (obj: any, type: string, morphTargetDictionary?: any) => {
const data: any = {
name: '',
duration: 0,
tracks: [],
uuid: "18A2138E-2ABF-4B83-AA15-C1D85BCE2F76",
}
data.name = obj.AnimationClip.m_Name;
data.duration = obj.AnimationClip.m_AnimationClipSettings.m_StopTime - obj.AnimationClip.m_AnimationClipSettings.m_StartTime;
if (obj.AnimationClip.m_ScaleCurves.length > 0) {
for(const curve of obj.AnimationClip.m_ScaleCurves) {
data.tracks.push(parse_unity_curve(curve, "scale"));
}
}
if (obj.AnimationClip.m_RotationCurves.length > 0) {
for (const curve of obj.AnimationClip.m_RotationCurves) {
data.tracks.push(parse_unity_curve(curve, "quaternion"));
}
}
if (obj.AnimationClip.m_PositionCurves.length > 0) {
for (const curve of obj.AnimationClip.m_PositionCurves) {
data.tracks.push(parse_unity_curve(curve, "position"));
}
}
if (obj.AnimationClip.m_FloatCurves.length > 0) {
for (const item of obj.AnimationClip.m_FloatCurves) {
let name = '';
if (type === 'fbx') {
name = item.path.split('/').slice(-1) + '.morphTargetInfluences[' + morphTargetDictionary[item.attribute.replace('blendShape.', '')] + ']'
} else if (type === 'glb') {
name = item.path.split('/').slice(-1) + '.morphTargetInfluences[' + morphTargetDictionary[item.attribute.split('.').slice(-1)[0]] + ']'
}
const values = [];
const times = [];
const firstCC = item.curve.m_Curve[0];
const lastCC = item.curve.m_Curve.slice(-1)[0]
times.push(firstCC.time);
times.push(lastCC.time);
values.push(/e-/.test(firstCC.value) ? 0 : (firstCC.value / 100))
values.push(/e-/.test(lastCC.value) ? 0 : (lastCC.value / 100))
const track = new THREE.NumberKeyframeTrack(name, times, values);
data.tracks.push(track)
}
}
return data;
}
export {
getAnimateClip,
}