babylonjs动画详解
更新时间: 2025-08-19 14:12:27
# 动画基础:从“翻书”到3D动画
# 什么是3D动画
想象一本漫画书,每页画着人物的不同动作,快速翻动时人物“动”了起来——这就是关键帧动画的原理。
在Babylon.js中,3D动画通过关键帧(Keyframe)定义物体属性(如位置,旋转,缩放)在不同时间点的值,引擎自动计算中间帧,让物体“平滑运动”
# 动画核心概念
- 关键帧(Keyframe):动画中的关键画面,包含时间(帧编号)和属性值(如位置Vector3(0,0,0))
- 动画曲线:关键帧之间的过渡路径(线性,缓动等),决定动画的“运动节奏”
- 动画目标:被动画的对象(如网络,相机,灯光),属性路径格式为“属性.子属性”(如“position.x”控制X轴位置)
# 核心API:动画系统的“工具箱”
# Animation类:创建单个动画
作用:定义单个属性的动画(如移动,旋转,缩放)
构造函数:
new BABYLON.Animation(name, propertyPath, frameRate, animationType, loopMode)
1
参数 | 说明 | 示例值 |
---|---|---|
name | 动画名称(自定义) | "cubeMove" |
propertyPath | 目标属性路径(支持嵌套属性) | "position.x"(X 轴位置)、"rotation.y"(Y 轴旋转) |
frameRate | 帧率(每秒帧数,控制动画流畅度) | 30(每秒 30 帧) |
animationType | 动画数据类型(属性值的类型) | BABYLON.Animation.ANIMATIONTYPE_VECTOR3(3D 向量) |
loopMode | 循环模式 | BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE(循环)、ANIMATIONLOOPMODE_CONSTANT(结束后保持最后一帧) |
关键方法
- setKeys(keys): 设置关键帧数组,格式为[{frame: 0, value: 值}, {frame: 30, value: 值}]
# AnimationGroup类:管理多个动画
作用: 组合多个Animation, 实现同时/顺序播放,统一控制(暂停/停止)
构造函数:
new BABYLON.AnimationGroup(name, scene)
1
参数 | 说明 | 示例值 |
---|---|---|
name | 动画组名称 | "cubeAnimations" |
scene | 动画组所属的场景 | scene |
核心方法
方法 | 说明 |
---|---|
addTargetedAnimation(anim, target) | 将动画绑定到目标对象(如网格) |
play(loop) | 播放动画组,loop=true表示循环播放 |
stop() | 停止动画组,重置到初始状态 |
pause() | 暂停动画组,可通过restart()恢复 |
normalize(startFrame, endFrame) | 统一组内所有动画的帧数(避免动画时长不一致) |
事件监听
onAnimationGroupEndObservable.add(() => { ... }):动画组播放结束时触发回调
# 场景动画控制方法
方法 | 说明 |
---|---|
scene.beginAnimation(target, startFrame, endFrame, loop, speed, onEnd) | 直接播放目标对象的动画,onEnd为动画结束回调 |
scene.stopAnimation(target, animName) | 停止目标对象上指定名称的动画 |
scene.stopAllAnimations() | 停止场景中所有动画 |
# 从零开始:创建你的第一个动画
# 基础案例:让立方体“动起来”
目标:创建一个立方体,实现“移动+旋转”的组合动画
步骤1:准备场景
// 创建引擎和场景
const canvas = document.getElementById("renderCanvas");
const engine = new BABYLON.Engine(canvas, true);
const scene = new BABYLON.Scene(engine);
// 添加相机和灯光
const camera = new BABYLON.ArcRotateCamera("camera", -Math.PI/2, Math.PI/2.5, 10, BABYLON.Vector3.Zero(), scene);
camera.attachControl(canvas, true);
const light = new BABYLON.HemisphericLight("light", new BABYLON.Vector3(0, 1, 0), scene);
// 创建立方体
const cube = BABYLON.Mesh.CreateBox("cube", 2, scene);
cube.position = new BABYLON.Vector3(0, 1, 0);
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
步骤 2:创建移动动画(沿 X 轴)
// 创建动画:移动X轴位置
const moveAnim = new BABYLON.Animation(
"moveX", // 名称
"position.x", // 属性路径:X轴位置
30, // 帧率
BABYLON.Animation.ANIMATIONTYPE_FLOAT, // 类型:浮点数
BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE // 循环模式:循环
);
// 设置关键帧:0帧时X=0,30帧时X=5,60帧时X=0(往返移动)
const moveKeys = [
{ frame: 0, value: 0 },
{ frame: 30, value: 5 },
{ frame: 60, value: 0 }
];
moveAnim.setKeys(moveKeys);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
步骤 3:创建旋转动画(绕 Y 轴)
// 创建动画:绕Y轴旋转
const rotateAnim = new BABYLON.Animation(
"rotateY", // 名称
"rotation.y", // 属性路径:Y轴旋转
30, // 帧率
BABYLON.Animation.ANIMATIONTYPE_FLOAT, // 类型:浮点数
BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE // 循环模式:循环
);
// 设置关键帧:0帧时旋转0,30帧时旋转2π(一圈)
const rotateKeys = [
{ frame: 0, value: 0 },
{ frame: 30, value: 2 * Math.PI }
];
rotateAnim.setKeys(rotateKeys);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
步骤 4:用AnimationGroup组合动画
// 创建动画组
const animGroup = new BABYLON.AnimationGroup("cubeAnimGroup", scene);
// 将动画添加到组,并绑定到立方体
animGroup.addTargetedAnimation(moveAnim, cube);
animGroup.addTargetedAnimation(rotateAnim, cube);
// 归一化动画(确保两个动画时长一致,都是60帧)
animGroup.normalize(0, 60);
// 播放动画组(循环)
animGroup.play(true);
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
步骤 5:渲染场景
engine.runRenderLoop(() => {
scene.render();
});
1
2
3
2
3
# 动画队列:让动画“排队执行”
# 两种实现方式对比
方式 | 原理 | 优点 | 缺点 |
---|---|---|---|
AnimationGroup | 多个动画在同一时间轴播放 | 控制简单(统一暂停 / 停止) | 无法严格顺序执行(需手动控制时间) |
事件链(回调) | 前一个动画结束后触发下一个 | 严格顺序执行 | 代码嵌套深(“回调地狱”) |
# 方式1:用AnimationGroup实现顺序播放
通过调整动画的关键帧时间,让动画依次开始
示例:立方体先移动(0-30帧),再旋转(30-60帧)
// 移动动画关键帧(0-30帧)
const moveKeys = [
{ frame: 0, value: 0 },
{ frame: 30, value: 5 } // 30帧时移动到X=5
];
// 旋转动画关键帧(30-60帧,前30帧保持0)
const rotateKeys = [
{ frame: 0, value: 0 }, // 0-30帧不旋转
{ frame: 30, value: 0 },
{ frame: 60, value: 2 * Math.PI } // 30-60帧旋转一圈
];
// 归一化动画组为0-60帧,确保同步
animGroup.normalize(0, 60);
animGroup.play(false); // 播放后,移动和旋转会按时间轴顺序执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 方式2:用onAnimationEnd事件链
前一个动画结束时,通过回调出发下一个动画
示例:先移动,移动结束后旋转
scene.beginDirectAnimation(
cube,
[moveAnim], // 只播放移动动画
0, 30,
false,
1,
() => {
scene.beginDirectAnimation(
cube,
[rotateAnim], // 只播放旋转动画
0, 30, // 这里可以安全使用0-30帧,因为指定了动画对象
false,
1
);
}
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
优化:用数组和递归谜面“回调地狱”
// 1. 初始化场景和立方体(完整环境)
const scene = new BABYLON.Scene(engine);
const cube = BABYLON.Mesh.CreateBox("cube", 2, scene);
cube.position = new BABYLON.Vector3(0, 1, 0);
// 移动动画(控制position属性)
const moveAnim = new BABYLON.Animation(
"moveAnim",
"position",
30,
BABYLON.Animation.ANIMATIONTYPE_VECTOR3,
BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT
);
moveAnim.setKeys([
{ frame: 0, value: new BABYLON.Vector3(0, 1, 0) },
{ frame: 30, value: new BABYLON.Vector3(5, 1, 0) } // 移动到X=5位置
]);
// 旋转动画(控制rotation属性)
const rotateAnim = new BABYLON.Animation(
"rotateAnim",
"rotation",
30,
BABYLON.Animation.ANIMATIONTYPE_VECTOR3,
BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT
);
rotateAnim.setKeys([
{ frame: 0, value: new BABYLON.Vector3(0, 0, 0) },
{ frame: 30, value: new BABYLON.Vector3(0, Math.PI, 0) } // 旋转180度
]);
// 3. 定义带标识的动画队列
// 每个元素包含:动画对象、帧范围、名称(用于调试)
const animQueue = [
{
name: "移动动画",
animation: moveAnim,
start: 0,
end: 30
},
{
name: "旋转动画",
animation: rotateAnim,
start: 0,
end: 30
}
];
// 4. 增强版递归播放函数
function playQueue(index) {
if (index >= animQueue.length) {
console.log("动画队列播放完成");
return;
}
const currentAnim = animQueue[index];
console.log(`正在播放: ${currentAnim.name} (${currentAnim.start}-${currentAnim.end}帧)`);
// 使用beginDirectAnimation显式指定要播放的动画
scene.beginDirectAnimation(
cube, // 目标对象
[currentAnim.animation], // 显式指定当前动画对象
currentAnim.start, // 起始帧
currentAnim.end, // 结束帧
false, // 不循环
1, // 速度
() => {
playQueue(index + 1); // 递归播放下一个
}
);
}
// 5. 启动队列
playQueue(0);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# 动画控制:暂停,停止与取消队列
# 暂停与恢复
- AnimationGroup
animGroup.pause() 暂停
animGroup.restart() 恢复
// 暂停
document.getElementById("pauseBtn").addEventListener("click", () => {
animGroup.pause();
});
// 恢复
document.getElementById("resumeBtn").addEventListener("click", () => {
animGroup.restart(); // 从暂停处继续
});
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
- 单个动画
通过scene.beginAnimation返回的Animatable对象控制
const animatable = scene.beginAnimation(cube, 0, 30, false);
// 暂停
animatable.pause();
// 恢复
animatable.restart();
1
2
3
4
5
2
3
4
5
# 停止动画与取消队列
- 方法1:停止AnimationGroup
// 停止动画组(重置到初始状态)
animGroup.stop();
1
2
2
- 方法2:停止特定动画
// 停止立方体上名为"moveX"的动画
scene.stopAnimation(cube, "moveX");
1
2
2
- 方法3:停止所有动画(清空场景)
// 停止场景中所有动画
scene.stopAllAnimations();
1
2
2
- 方法4:取消事件链队列
需维护动画引用数组,停止所有未执行的动画
// 存储未执行的动画引用
const pendingAnims = [];
// 修改playQueue函数,记录未执行动画
function playQueue(index) {
if (index >= animQueue.length) return;
const { start, end } = animQueue[index];
const animatable = scene.beginAnimation(cube, start, end, false, 1, () => {
pendingAnims.splice(pendingAnims.indexOf(animatable), 1); // 移除已执行动画
playQueue(index + 1);
});
pendingAnims.push(animatable); // 添加到待执行队列
}
// 取消队列:停止所有待执行动画
function cancelQueue() {
pendingAnims.forEach(anim => anim.stop());
pendingAnims.length = 0; // 清空队列
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 高级特性:让动画更“自然”
# 缓动函数
让动画速度变化更自然(如加速,减速,弹跳)
示例:添加“先慢后快”的缓动效果
// 创建缓动函数:正弦缓动(EASEINOUT:先加速后减速)
const easeFunction = new BABYLON.SineEase();
easeFunction.setEasingMode(BABYLON.EasingFunction.EASINGMODE_EASEINOUT);
// 绑定到动画
moveAnim.setEasingFunction(easeFunction);
1
2
3
4
5
6
2
3
4
5
6
常用缓动类型:
- SineEase:平滑的正弦曲线过渡
- CubicEase:三次方曲线(更强烈的加速 / 减速)
- BounceEase:弹跳效果
# 动画权重与混合
实现多个动画的平滑过渡(如角色从“走”到“跑”)
步骤:
- 启用动画混合
scene.animationPropertiesOverride = new BABYLON.AnimationPropertiesOverride();
scene.animationPropertiesOverride.enableBlending = true;
scene.animationPropertiesOverride.blendingSpeed = 0.2; // 过渡速度(越小越平滑)
1
2
3
2
3
- 播放第二个动画时,设置权重逐渐从0变1
// 播放“走”动画(权重1)
walkAnimGroup.play(true);
// 触发“跑”动画时,逐渐过渡
runAnimGroup.setWeightForAllAnimatables(0); // 初始权重0
runAnimGroup.play(true);
// 每帧增加权重,直到1
scene.registerBeforeRender(() => {
if (runAnimGroup.weight < 1) {
runAnimGroup.setWeightForAllAnimatables(runAnimGroup.weight + 0.02);
walkAnimGroup.setWeightForAllAnimatables(1 - runAnimGroup.weight);
}
});
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
# 性能优化:避免动画卡顿
# 减少关键帧数量
关键帧越多,计算量越大。非必要时,用较少关键帧 + 缓动函数实现平滑效果。
# 避免动画叠加
停止旧动画后再播放新动画,避免同一属性被多个动画同时控制