three.js 骨骼动画的clone
程序员文章站
2024-03-16 18:07:46
...
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);
} );