three.js中普通Mesh的clone沒有任何問題,但是牽扯到SkinnedMesh的clone,不是很好處理,而且3ds max和Maya當前無法直接導出多個動畫(這意味着動畫不是在同一時間線上)到一個文件中。
這篇文章將主要討論如何clone SkinnedMesh 和如何 將3ds max中導出的骨骼動畫進行剪輯提取不同的動畫切片。
1、 首先看看如何clone SkinnedMesh, 3ds max中導出的ShinnedMesh的結構一般是 一個Group節點下面包含一個Bone和一個SkinnedMesh,可以先按照這個結構來推導其他結構。
- Group中包含的信息是Bone 和 SkinnedMesh
- Group中的Bone是一個骨骼的樹結構的根節點,每一根骨骼都有名稱,骨骼動畫會根據這個名稱找到骨骼,然後變換骨骼矩陣。
- ShinnedMesh中存儲的信息是 Geometry,Material, Skelen。
- Geometry內部會包含 骨骼索引 和 骨骼權重信息,Geometry可以在各個ShinnedMesh中共享。
- Material信息中與骨骼相關的信息是skinning,這個沒有什麼可解釋的。
- Skelen信息是骨骼動畫的關鍵,Skelen中會存儲bones信息,這個bones是所有骨骼的數組,注意這個數組是有順序的,這個順序
- 是與Geometry中的骨骼索引信息對應的,不能錯亂。
具體的clone代碼如下:
//將多有的骨骼節點存儲到一個數組中
function getBoneArray(bone, bonearr){
for(let i=0; i<bone.children.length; i++){
bonearr[bone.children[i].name] = bone.children[i];
getBoneArray(bone.children[i], bonearr);
}
}
var loader = new THREE.FBXLoader();
loader.load( 'models/test.fbx', function ( object ) {
for(let i=0; i<2; i++){
//拷貝對象
let obj = object.clone(false);
//拷貝整個骨骼樹
let rootBone = object.children[0].clone();
//將骨骼樹的根節點放到object中,這個骨骼樹會在object更新世界座標系時被更新
obj.add(rootBone);
//將所有的骨骼節點存儲到一個數組中
let bonearr = {};
getBoneArray(rootBone, bonearr);
//拷貝SkinnedMesh
let skinmesh = object.children[1].clone();
//按照object中骨骼數組的順序構建骨骼數組,這個順序與geometry中的骨骼索引順序一致
let newArr = [];
object.children[1].skeleton.bones.forEach(item=>{
newArr.push(bonearr[item.name]);
});
//綁定骨骼,和offsetMatrix(世界座標系初始姿態)
skinmesh.bind(new THREE.Skeleton(newArr, object.children[1].boneInverses));
//將skinnedMesh添加到新對象當中
obj.add(skinmesh);
//修改位置和縮放
obj.position.set(i*10,0,0);
obj.scale.set(10,10,10);
//添加到場景中
scene.add( obj );
}
} );
2、3ds max和Maya當前無法直接導出多個動畫,所有的動畫都在一個時間線上,如何分離動畫切片,這個要明白動畫切片即AnimationClip的結構。
- AnimationClip包含name(動畫名稱)、duration(動畫總時長) 、tracks(關鍵幀軌道)
- name 的用處是AnimationMixer.clipAction(clip, optionalRoot),第一個參數可以使AnimationClip 的name。
- duration 動畫總時長
- tracks是一個數組,存儲的是所有骨骼節點在運動過程中對應的位移、旋轉、顏色對應的關鍵幀,一個骨骼可以使移動、旋轉等,一個位移或者旋轉會在關鍵中持續存在。軌跡包括BooleanKeyframeTrack、ColorKeyframeTrack、ColorKeyframeTrack、NumberKeyframeTrack、QuaternionKeyframeTrack、StringKeyframeTrack、VectorKeyframeTrack,每一個軌跡都有一個名稱,這個名稱和骨骼是對應的,例如leg.position,其中leg對應的是骨骼的名稱,position代表的是骨骼的位置變化。
具體的代碼如下:
let timeNode = [0, 0.3, 0.52, 1.0]; //假設將骨骼動畫分成3個動畫切片
let duration = object.animations[ 0 ].duration; //骨骼動畫的總時長
let animations = []; //保存動畫切片
for(let n=0; n<timeNode.length-1; n++){
let clipDuration = (timeNode[n+1] - timeNode[n]) * duration; //計算單個切片的時長
let tracks = []; //單個動畫的軌跡
object.animations[ 0 ].tracks.forEach(item=>{
let values = [];
let times = [];
let step = 0;
if(item.ValueTypeName =='vector') //關鍵幀軌跡是那種類型
step = 3; //這種類型的分量有幾個
else if(item.ValueTypeName =='quaternion') //關鍵幀軌跡是那種類型
step = 4; //這種類型的分量有幾個
let length = item.times.length; //關鍵幀的時間點
for(let j=0; j<length; j++){
if(timeNode[n] * duration <= item.times[j] && item.times[j] < timeNode[n+1] * duration){
times.push(item.times[j] - timeNode[n] * duration); //所有的動畫切片的時間收是從0開始的
for(let m=0; m<step; m++){
values.push(item.values[step * j + m]); //旋轉、移動對應的具體數據
}
}
}
if(item.ValueTypeName =='vector'){
tracks.push(new THREE.VectorKeyframeTrack(item.name, times, values)); //創建移動軌跡
} else if(item.ValueTypeName =='quaternion'){
tracks.push(new THREE.QuaternionKeyframeTrack(item.name, times, values)); //創建旋轉軌跡
}
});
let clip = new THREE.AnimationClip(n + '', clipDuration, tracks); //構建一個動畫切片
animations.push(clip);
最後動畫的播放代碼:
let mixer = new THREE.AnimationMixer(); //動畫混合器
var action = mixer.clipAction( animations[ 0 ], obj); //創建一個活動動畫(動畫可以在多個對象之間共享)
action.play(); //設置播放
其中Bone的克隆代碼如下:
function Bone() {
Object3D.call( this );
this.type = 'Bone';
}
Bone.prototype = Object.assign( Object.create( Object3D.prototype ), {
constructor: Bone,
isBone: true,
clone: function () {
let bone = new Bone( );
bone.copy( this );
return bone;
},
} );
完整代碼
function getBoneArray(bone, bonearr){
for(let i=0; i<bone.children.length; i++){
bonearr[bone.children[i].name] = bone.children[i];
getBoneArray(bone.children[i], bonearr);
}
}
var loader = new THREE.FBXLoader();
loader.load( 'kaunggongzoupao/tongyongkuanggong(1).FBX', function ( object ) {
let timeNode = [0, 0.3, 0.52, 1.0];
let duration = object.animations[ 0 ].duration;
let animations = [];
for(let n=0; n<timeNode.length-1; n++){
let clipDuration = (timeNode[n+1] - timeNode[n]) * duration;
let tracks = [];
object.animations[ 0 ].tracks.forEach(item=>{
let values = [];
let times = [];
let step = 0;
if(item.ValueTypeName =='vector')
step = 3;
else if(item.ValueTypeName =='quaternion')
step = 4;
let length = item.times.length;
for(let j=0; j<length; j++){
if(timeNode[n] * duration <= item.times[j] && item.times[j] < timeNode[n+1] * duration){
times.push(item.times[j] - timeNode[n] * duration);
for(let m=0; m<step; m++){
values.push(item.values[step * j + m]);
}
}
}
if(item.ValueTypeName =='vector'){
tracks.push(new THREE.VectorKeyframeTrack(item.name, times, values));
} else if(item.ValueTypeName =='quaternion'){
tracks.push(new THREE.QuaternionKeyframeTrack(item.name, times, values));
}
});
let clip = new THREE.AnimationClip(n + '', clipDuration, tracks);
animations.push(clip);
}
let mixer = new THREE.AnimationMixer();
for(let i=0; i<2; i++){
let obj = object.clone(false);
let rootBone = object.children[0].clone();
let bonearr = {};
cloneBone(rootBone, bonearr);
obj.add(rootBone);
let skinmesh = object.children[1].clone();
let newArr = [];
object.children[1].skeleton.bones.forEach(item=>{
newArr.push(bonearr[item.name]);
});
skinmesh.bind(new THREE.Skeleton(newArr, object.children[1].boneInverses));
obj.add(skinmesh);
var quat = new THREE.Quaternion();
quat.setFromUnitVectors(new THREE.Vector3(0, 1, 0), new THREE.Vector3(1, 1, -1));
obj.rotation.setFromQuaternion(quat);
obj.position.set(i*10,0,0);
obj.scale.set(10,10,10);
scene.add( obj );
action = mixer.clipAction( animations[ 1 ], obj);
action.play();
}
g_mixer.push(mixer);
} );