在工作中,由于算法给到的动画文件是Unity
的.anim
格式动画文件,这个格式不能直接在Web端用Three.js
引擎运行。因此需要将.anim
格式的动画文件转换为Three.js
的AnimationClip
动画对象。
// AnimationClip
{
duration: Number // 持续时间
name: String // 名称
tracks: [ // 动画所有属性的关键帧轨道数组
{
name: String // 关键帧轨道标识符
times: Float32Array // 时间数组
values: Float32Array // 与时间数组中的时间点对应的相关值
interpolation: Constant // 使用的插值类型
},
{...}
]
uuid: String // 实例的uuid
}
它是用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格式,没有进行插值运算(算出每一帧的信息),这样用three.js运行起来的实际效果会卡顿。
目前从网上找了个带动画的模型,测了下效果:
模型对象里的原始AnimationClip运行效果(每秒30帧)
Unity动画转Three.js动画: 模型原始的骨骼动画效
将模型导入Unity后,生成.anim动画文件。再通过脚本将这个.anim动画文件 转换为 AnimationClip对象 的运行效果如下:(没有进行插值,缺帧导致有点卡顿)
Unity动画转Three.js动画: 转换后卡顿的骨骼动画
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,
}