动画混合核心原理
动画混合(Animation Blending) 就像一位 DJ 同时播放多首歌,并将它们自然过渡拼接成一首新曲。它让 3D 模型(比如游戏角色)能在不同动作之间平滑切换(如从"走路"渐变到"跑步"),不会出现卡顿或突兀的跳跃。
想象你的角色有两个动作:
- 站立不动(动画 A)
- 挥手打招呼(动画 B)
直接切换会显得僵硬🤖,就像突然从静止跳到挥手。动画混合的作用是:
-
权重(Weight)控制:给每个动作分配一个"音量旋钮"(权重值 0~1),比如:
- 初始:动画 A 权重=1(站立),动画 B 权重=0(挥手关闭)
- 过渡期:动画 A 权重从 1 → 0,动画 B 权重从 0 → 1
- 最终:动画 A 权重=0,动画 B 权重=1(完全挥手)
-
实时混合计算 :每一帧,Three.js 会根据权重比例,同时播放两个动作并叠加效果,生成中间过渡状态。
动画混合的常见用途
场景 | 效果说明 | 混合策略 |
---|---|---|
角色动作切换 | 走路 → 跑步 → 跳跃 自然过渡 | 权重渐变(0→1/1→0) |
复合动作 | 边走路边挥手 | 多个动作权重同时为 1 |
情绪叠加 | 受伤时跛脚行走 | 正常行走 + 受伤姿态混合 |
使用注意
- 权重总和 ≤ 1:所有动作的权重相加不超过 1,否则动作会"变形"
- 时间同步:混合的动画需时间轴对齐(如都是从左脚起步)。
- 性能优化:过多动画混合会增加计算量,需合理控制数量
向量值定义动画案例
效果如图

需要了解的API
动画混合器(Animation Mixer)
- 角色动画的"总控台",负责管理所有动画片段和混合效果
动画行为(Animation Action)
- 每个动作(如走路、跑步)都是一个独立的
AnimationAction
对象,其用于创建并控制动画行为
动画剪辑 (AnimationClip)
- 用于存储和管理关键帧动画数据,整合多个轨道,形成完整的动画片段,其本身不执行动画
实现思路
使用VectorKeyframeTrack
来定义动画运动轨迹,
js
const track = new THREE.VectorKeyframeTrack('.position', [0,1],[
5, 0, -5,
-5, 0, 5
])
这里定义了两个时间节点,相对应的是两个坐标点,这意味着0时对应在第一个坐标位置,1时在第二个坐标位置
js
const clip = new THREE.AnimationClip('move', -1, [track]);
const mixer = new THREE.AnimationMixer(mesh);
const action = mixer.clipAction(clip);
action.play();
这里定义了一个名为move
的动画剪辑,其作用于mesh
模型上,并将无限循环播放
js
const update = (frame, frameMax) => {
const a_frame = frame / frameMax; // 当前帧占总帧数的比例
const a_framesin = ( Math.sin( Math.PI *2 * a_frame) + 1) / 2;
mixer.setTime( 1 * a_framesin); // 设置动画时间
}
mixer.setTime
用于精确控制动画的播放时间点
JSON格式的动画效果案例
效果如图

需要了解的API
AnimationClip.parse
该api是一个静态方法,用于将JSON格式的动画数据反序列化为 AnimationClip
对象
JSON 数据结构要求
字段 | 类型 | 说明 | 是否必需 |
---|---|---|---|
name |
string |
动画名称 | ✅ |
tracks |
Array<Object> |
关键帧轨迹数据 | ✅ |
duration |
number |
动画总时长(秒) | ✅ |
blendMode |
number |
混合模式(如 NormalAnimationBlendMode ) |
❌ |
uuid |
string |
唯一标识符 | ❌ |
在这个案例中,使用AnimationClip.parse
将给定的json数据转换为AnimationClip
对象
js
const str_json = `{
"name": "scale",
"duration":3,
"tracks":[
{
"name":".scale",
"times":[0,1.5,3],
"values":[ 1,1,1, 0.5,3,0.5, 1,1,1],
"type":"vector"
},
{
"name":".position",
"times":[0, 0.5, 1.5, 2.5, 3],
"values":[ 0,0.5,0, 2,1.5,0, -2,1.5,0, -2,1.5,2, 0,0.5,0],
"type":"vector"
}
],
"uuid":"eff507b6-8c41-47d4-a62e-77119a5ee288",
"blendMode":2500
}`
const clip = THREE.AnimationClip.parse( JSON.parse(str_json) )
const mixer = new THREE.AnimationMixer(mesh);
const action = mixer.clipAction(clip);
action.play();
从代码中可以看到定义了两个轨道动画状态,一个是缩放,一个是位置
加载json格式场景数据案例
如图效果

需要了解的API
ObjectLoader().parse 该方法将JSON格式的 3D 场景数据实时解析为 Three.js 可操作对象(场景、模型、动画等),实现动态场景加载与重建。
数据结构如下
js
const str_json = `{
"metadata": {
"version": 4.3,
"type": "Object",
"generator": "Hand Coded"
},
"textures": [],
"images": [],
"geometries": [
{
"uuid": "bce89f32-4cab-41c8-b3f6-15e04b1dd68e",
"type": "BufferGeometry",
"data": {
"attributes": {
"position": {
"itemSize": 3,
"type": "Float32Array",
"array": [0,0,0, 4,0,0, 0,0,4 ],
"normalized": false
}
},
"morphTargetsRelative": true,
"morphAttributes": {
"position": [
{
"itemSize":3,
"type":"Float32Array",
"array":[
0.0, 3.0, 0.0,
-4.0, 3.0, 0.0,
0.0, 3.0,-4.0
],
"normalized":false
}
]
}
}
}
],
"materials": [
{
"uuid": "0246dafa-bf34-4460-a7eb-f5098b2120af",
"type": "PointsMaterial",
"size": 1,
"color": 65480
}
],
"animations": [
{
"name": "converge",
"duration": 1,
"tracks": [
{
"name": ".morphTargetInfluences[0]",
"times": [0, 0.5, 1],
"values": [0, 1, 0],
"type": "number"
}
],
"uuid": "584702c7-efb2-4fa1-ae37-43d18b5f7fb5",
"blendMode": 2500
}
],
"object": {
"uuid": "ad1ecebd-b665-4e10-9ead-6d0205bec011",
"type": "Scene",
"matrix": [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ],
"children": [
{
"uuid": "6b95325d-5bcc-4f85-a330-fbca8f271287",
"name": "tri_one",
"type": "Points",
"geometry": "bce89f32-4cab-41c8-b3f6-15e04b1dd68e",
"material": "0246dafa-bf34-4460-a7eb-f5098b2120af",
"matrix": [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ],
"animations": ["584702c7-efb2-4fa1-ae37-43d18b5f7fb5"]
}
]
}
}`;
这是一个场景数据结构,包含了几何体、材质、动画,模型对象等内容
js
scene = new THREE.ObjectLoader().parse( JSON.parse(str_json) );
const points = scene.getObjectByName('tri_one');
const mixer = new THREE.AnimationMixer(points);
const action = mixer.clipAction(points.animations[0]);
action.play();
蝴蝶动画案例
先看效果

需要了解的API
BufferGeometryLoader
该api 用于加载预序列化的BufferGeometry
数据,跳过完整场景解析,直接获取几何体对象。
NumberKeyframeTrack
用于在动画中精准控制单一数值属性的关键帧变化(如透明度、强度、进度条等)
js
const loader = new THREE.BufferGeometryLoader();
loader.load(
'./json/tri12-butterfly/set1-buffergeometry/0.json',
(geometry) =>{
// 创建一个网格,使用几何体和基本材质
state.mesh = new THREE.Mesh(
geometry,
new THREE.MeshBasicMaterial({vertexColors: true, side: THREE.DoubleSide})
)
// 将网格添加到场景中
scene.add(state.mesh)
// 创建一个数字关键帧轨道,用于控制网格的变形
const track = new THREE.NumberKeyframeTrack('.morphTargetInfluences[0]',
[ 0, 0.25, 0.5, 0.75, 1], // 时间点
[ 0, 0.30, 0.5, 0.15, 0] // 变形程度
)
// 创建一个动画剪辑,包含一个关键帧轨道
const clip = new THREE.AnimationClip('flap', -1, [ track ] );
// 将动画剪辑添加到网格的动画数组中
state.mesh.animations.push(clip)
// 创建一个动画混合器,用于控制网格的动画
state.mixer = new THREE.AnimationMixer(state.mesh)
// 获取网格的第一个动画剪辑
const action = state.mixer.clipAction( state.mesh.animations[0] );
// 播放动画
action.play();
animation()
})
这里要注意使用NumberKeyframeTrack
创建了一个数字关键帧轨道,来控制变形强弱
总结动画实现步骤
- 创建动画剪辑
- 创建混合器并播放
- 在动画循环中更新
以上便是本篇的所有内容,自己动手练一练,感受一下实际效果