Unity动画转Three.js动画

一:应用场景

在工作中,由于算法给到的动画文件是Unity.anim格式动画文件,这个格式不能直接在Web端用Three.js引擎运行。因此需要将.anim格式的动画文件转换为Three.jsAnimationClip动画对象。

二:.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,
}

猜你喜欢

转载自blog.csdn.net/weixin_40693643/article/details/129229654