如何让THREE.js的AnimationMixer叠加骨骼动画而非按权重混合?
解决Three.js中多骨骼平移动画叠加而非加权平均的问题
这个问题我之前也踩过坑——Blender的NLA轨道叠加逻辑和Three.js默认的动画混合逻辑确实不一样:Blender是直接累加变换,而Three.js默认会根据动画权重做线性加权平均。要实现你要的平移叠加效果(2+4=6),有几种可行的方案,我给你详细拆解下:
方案一:自定义属性混合函数(推荐,灵活可控)
Three.js的PropertyBinding允许我们为特定的动画属性自定义混合逻辑,把默认的加权平均改成叠加模式。针对骨骼的平移、旋转、缩放,我们可以分别设置对应的混合函数:
1. 仅处理平移叠加
如果只需要解决平移的叠加问题,给所有骨骼的position属性设置自定义混合函数即可:
// 覆盖平移属性的混合逻辑,实现叠加效果 THREE.PropertyBinding.prototype.setBlendFunction('bones[*].position', (destValue, srcValue, alpha) => { // destValue:当前已混合的属性值 // srcValue:当前动画帧的目标值 // alpha:当前动画Action的权重(0~1) // 按权重把当前动画的平移值加到目标值上 destValue.add(srcValue.multiplyScalar(alpha)); });
2. 完整TRS属性叠加(平移+旋转+缩放)
如果还需要处理旋转和缩放的叠加,补充对应的函数模拟Blender的叠加逻辑:
// 平移叠加(向量相加) THREE.PropertyBinding.prototype.setBlendFunction('bones[*].position', (dest, src, alpha) => { dest.add(src.multiplyScalar(alpha)); }); // 旋转叠加(四元数相乘) THREE.PropertyBinding.prototype.setBlendFunction('bones[*].quaternion', (dest, src, alpha) => { // 先根据权重插值当前动画的旋转,再和已有旋转相乘 const tempQuat = src.clone().slerp(THREE.Quaternion.identity, 1 - alpha); dest.multiply(tempQuat); }); // 缩放叠加(按权重幂次相乘) THREE.PropertyBinding.prototype.setBlendFunction('bones[*].scale', (dest, src, alpha) => { dest.x *= Math.pow(src.x, alpha); dest.y *= Math.pow(src.y, alpha); dest.z *= Math.pow(src.z, alpha); });
3. 应用动画
设置好混合函数后,把两个动画Action的权重设为1,直接播放即可:
const mixer = new THREE.AnimationMixer(yourSkinnedMesh); const actionTranslate2 = mixer.clipAction(clipTranslate2); const actionTranslate4 = mixer.clipAction(clipTranslate4); // 两个动画都设为最大权重,播放 actionTranslate2.setEffectiveWeight(1).play(); actionTranslate4.setEffectiveWeight(1).play();
这样两个动画的平移值就会直接叠加得到6的结果。如果需要调整单个动画的贡献度,只需要修改对应的effectiveWeight即可(比如设为0.5的话,就会回到20.5+40.5=3的加权平均逻辑)。
方案二:双Mixer手动叠加变换
如果不想修改全局的混合逻辑,可以用两个独立的AnimationMixer分别处理两个动画,然后手动把第二个动画的骨骼变换叠加到第一个上:
// 初始化两个mixer,绑定到同一个蒙皮网格(或克隆网格保证骨骼结构一致) const mixer1 = new THREE.AnimationMixer(yourSkinnedMesh); const mixer2 = new THREE.AnimationMixer(yourSkinnedMesh.clone()); const action1 = mixer1.clipAction(clipTranslate2); const action2 = mixer2.clipAction(clipTranslate4); action1.play(); action2.play(); // 在动画循环中手动叠加变换 function animate() { requestAnimationFrame(animate); const delta = clock.getDelta(); // 更新两个mixer mixer1.update(delta); mixer2.update(delta); // 获取目标骨骼(通过名称或索引) const targetBone = yourSkinnedMesh.skeleton.getBoneByName('YourTargetBone'); const boneFromMixer2 = mixer2.getRoot().skeleton.getBoneByName('YourTargetBone'); // 叠加平移 targetBone.position.add(boneFromMixer2.position); // 叠加旋转 targetBone.quaternion.multiply(boneFromMixer2.quaternion); // 叠加缩放 targetBone.scale.multiply(boneFromMixer2.scale); // 更新骨骼矩阵 yourSkinnedMesh.skeleton.update(); renderer.render(scene, camera); }
这个方案的好处是不会影响其他动画的混合逻辑,但需要手动维护骨骼的变换叠加,代码量稍大。
方案三:Blender端合并动画(适合固定叠加场景)
如果你的两个动画不需要单独控制开关或权重,直接在Blender里把它们合并成一个叠加后的动画再导出:
- 在Blender的NLA编辑器中,把两个动画轨道的混合模式改成Add(叠加);
- 导出glTF时选择合并后的动画轨道;
- 导入Three.js后,直接播放这个合并的动画就能得到叠加后的平移效果。
这种方法最简单,但缺乏动态控制的灵活性,适合固定叠加的场景。
内容的提问来源于stack exchange,提问作者Kit Callen
相关产品推荐
相关产品推荐

