天天看點

Unity動畫轉Three.js動畫

一:應用場景

在工作中,由于算法給到的動畫檔案是

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,
}
           

繼續閱讀