Skip to content

BabylonJS - 动画

Babylon.js - 动画、精灵和粒子系统

Section titled “Babylon.js - 动画、精灵和粒子系统”

动画让你的 3D 场景栩栩如生。Babylon.js 提供了一个强大而灵活的动画系统,能够随时间动画化对象的几乎任何属性,包括位置、旋转、缩放、材质颜色、灯光强度等。本节还涵盖了精灵(Sprite,3D 空间中的 2D 图像)和粒子系统(Particle System,用于火、烟等效果),它们通常涉及动画原理。

Babylon.js 动画系统的核心包括:

  • BABYLON.Animation 对象:定义要动画化哪个属性(例如,position.x)、数据的类型(例如,浮点数、向量、颜色)、动画速度 (framePerSecond) 和循环行为。
  • 关键帧 (Keyframes):一个数组,定义在特定帧(时间点)动画化属性的值。
  • 目标 (Target):属性被动画化的对象(例如,网格、灯光、相机)。
  • scene.beginAnimation:在目标对象上启动动画。
import * as BABYLON from '@babylonjs/core';
// 假设 'scene' 和 'targetMesh' 已定义
// 1. 创建 Animation 对象
const frameRate = 30;
const propertyToAnimate = "position.y"; // 动画化垂直位置
const yAnimation = new BABYLON.Animation(
"yAnim", // 动画名称
propertyToAnimate, // 要动画化的属性(可以是嵌套的,例如 'material.alpha')
frameRate, // 每秒帧数
BABYLON.Animation.ANIMATIONTYPE_FLOAT, // 数据类型 (FLOAT, VECTOR3, COLOR3, QUATERNION 等)
BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE // 循环行为 (CYCLE, RELATIVE, CONSTANT)
);
// 2. 创建关键帧
const keys = [];
// 在第 0 帧,position.y = 0
keys.push({ frame: 0, value: 0 });
// 在第 30 帧 (1 秒),position.y = 5
keys.push({ frame: 30, value: 5 });
// 在第 60 帧 (2 秒),position.y = 0
keys.push({ frame: 60, value: 0 });
// 3. 将关键帧分配给动画
yAnimation.setKeys(keys);
// 4. 将动画链接到目标网格
// 确保网格有一个已初始化的 'animations' 数组
if (!targetMesh.animations) {
targetMesh.animations = [];
}
targetMesh.animations.push(yAnimation);
// 5. 启动动画
// 参数: 目标, 开始帧, 结束帧, 循环 (布尔值), 速度比, 动画结束回调函数
const animatable = scene.beginAnimation(targetMesh, 0, 60, true, 1.0);
// animatable 允许控制正在运行的动画 (暂停, 停止等)
// animatable.pause();
  • ANIMATIONLOOPMODE_CYCLE:从起始值重新开始动画。
  • ANIMATIONLOOPMODE_RELATIVE:将结束值用作下一次循环迭代的起始点(适合连续旋转或移动)。
  • ANIMATIONLOOPMODE_CONSTANT:在结束值停止并保持该值。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Babylon.js - Basic Animation</title>
<script src="https://cdn.babylonjs.com/babylon.js"></script>
<style>
html, body { overflow: hidden; width: 100%; height: 100%; margin: 0; padding: 0; }
#renderCanvas { width: 100%; height: 100%; touch-action: none; }
</style>
</head>
<body>
<canvas id="renderCanvas"></canvas>
<script type="text/javascript">
const canvas = document.getElementById("renderCanvas");
const engine = new BABYLON.Engine(canvas, true);
const createScene = function () {
const scene = new BABYLON.Scene(engine);
scene.clearColor = new BABYLON.Color4(0.1, 0.2, 0.1, 1.0);
const camera = new BABYLON.ArcRotateCamera("Camera", -Math.PI / 2, Math.PI / 2.5, 15, BABYLON.Vector3.Zero(), scene);
camera.attachControl(canvas, true);
const light = new BABYLON.HemisphericLight("light", new BABYLON.Vector3(0, 1, 0), scene);
light.intensity = 0.8;
const box = BABYLON.MeshBuilder.CreateBox("box", { size: 2 }, scene);
box.position.x = -3;
const boxMat = new BABYLON.StandardMaterial("boxMat", scene);
boxMat.diffuseColor = BABYLON.Color3.Red();
box.material = boxMat;
const box2 = BABYLON.MeshBuilder.CreateBox("box2", { size: 2 }, scene);
box2.position.x = 3;
const box2Mat = new BABYLON.StandardMaterial("box2Mat", scene);
box2Mat.diffuseColor = BABYLON.Color3.Blue();
box2.material = box2Mat;
// --- 盒子 1 的动画 (缩放 X) ---
const scaleXAnim = new BABYLON.Animation("scaleXAnim", "scaling.x", 30,
BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE);
const scaleKeys = [];
scaleKeys.push({ frame: 0, value: 1 });
scaleKeys.push({ frame: 30, value: 0.2 });
scaleKeys.push({ frame: 60, value: 1.5 });
scaleKeys.push({ frame: 90, value: 1 });
scaleXAnim.setKeys(scaleKeys);
box.animations.push(scaleXAnim);
scene.beginAnimation(box, 0, 90, true);
// ---------------------------------------
// --- 盒子 2 的动画 (旋转 Y - 相对) ---
const rotateYAnim = new BABYLON.Animation("rotateYAnim", "rotation.y", 30,
BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_RELATIVE);
const rotateKeys = [];
rotateKeys.push({ frame: 0, value: 0 });
rotateKeys.push({ frame: 60, value: Math.PI * 2 }); // 在 2 秒内完成一整圈旋转
rotateYAnim.setKeys(rotateKeys);
box2.animations.push(rotateYAnim);
scene.beginAnimation(box2, 0, 60, true);
// ----------------------------------------
return scene;
};
const scene = createScene();
engine.runRenderLoop(function () { scene.render(); });
window.addEventListener("resize", function () { engine.resize(); });
</script>
</body>
</html>

此示例显示了两个立方体。左侧的红色立方体沿其 X 轴(宽度)持续缩放,缩小,然后变大,然后恢复正常大小,循环此过程。右侧的蓝色立方体使用相对循环模式绕其 Y 轴连续旋转,使其无限旋转。

scene.beginAnimation 返回的 Animatable 对象允许控制动画:

  • animatable.pause():暂停动画。
  • animatable.restart():从头开始重启动画。
  • animatable.stop():停止动画并将其移除。
  • animatable.goToFrame(frameNumber):跳到特定帧。

使用 CreateAndStartAnimation 更轻松地创建动画

Section titled “使用 CreateAndStartAnimation 更轻松地创建动画”

对于两个值之间的简单动画,可以使用静态辅助函数:

// 将 box 的 position.x 从当前值动画到 5,持续 120 帧(30fps 下 4 秒)
BABYLON.Animation.CreateAndStartAnimation(
'boxSlide', // 动画名称
box, // 目标网格
'position.x',// 要动画化的属性
30, // FPS
120, // 总帧数
box.position.x, // 起始值
5, // 结束值
BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT // 循环模式 (例如,播放一次)
);

缓动函数控制动画中变化速率,使运动看起来更自然(例如,开始慢,加速,然后减速)。你可以将缓动函数应用于 Animation 对象。

// 创建一个缓动函数 (例如,二次方缓入/缓出)
const easingFunc = new BABYLON.QuadraticEase();
// 设置缓动模式 (EaseIn, EaseOut, EaseInOut)
easingFunc.setEasingMode(BABYLON.EasingFunction.EASINGMODE_EASEINOUT);
// 在设置关键帧或启动之前将其应用于动画对象
myAnimation.setEasingFunction(easingFunc);

常见的缓动函数包括:LinearEase、CircleEase、BackEase、BounceEase、CubicEase、ElasticEase、ExponentialEase、PowerEase、QuadraticEase、QuarticEase、QuinticEase、SineEase。

你可以在动画的特定帧触发代码。

// 创建一个事件,在第 50 帧触发
const animEvent = new BABYLON.AnimationEvent(
50, // 帧号
() => { console.log("Reached frame 50!"); }, // 操作回调函数
true // 只触发一次?
);
// 将事件添加到动画对象
myAnimation.addEvent(animEvent);

精灵是在 3D 场景中使用的 2D 图像。它们对于渲染大量简单对象(如树木、草、粒子或面向相机的角色)非常高效。精灵默认始终面向相机(billboarding)。

精灵由 SpriteManager 管理,它优化了渲染。每当你想要使用一个精灵图集(纹理)时,你就创建一个管理器。

// 1. 创建一个精灵管理器
// 参数: 名称, 纹理 URL, 容量 (最大精灵数), 单元格大小 (宽/高), 场景
const spriteManager = new BABYLON.SpriteManager(
"treeManager",
"textures/tree.png", // 包含精灵的图片
2000, // 此管理器能处理的最大树木数量
{ width: 64, height: 64 }, // 纹理中每个精灵单元格的大小
scene
);
// 2. 创建精灵实例
const treeSprite = new BABYLON.Sprite("tree", spriteManager);
// 3. 定位并配置精灵实例
treeSprite.position = new BABYLON.Vector3(5, 0.5, 10);
treeSprite.size = 2.0; // 调整显示大小
// treeSprite.angle = Math.PI / 4; // 旋转精灵平面
// treeSprite.cellIndex = 0; // 如果精灵图集有多个单元格,选择一个
// 你可以使用同一个管理器创建许多精灵
for (let i = 0; i < 100; i++) {
const anotherTree = new BABYLON.Sprite("tree" + i, spriteManager);
anotherTree.position = new BABYLON.Vector3(Math.random() * 50 - 25, 0.5, Math.random() * 50 - 25);
anotherTree.size = 1.5 + Math.random(); // 随机大小
}

如果你的精灵纹理包含动画的多帧(精灵图集),你可以使用 sprite.playAnimation() 对其进行动画化。

// 假设 'playerSpriteSheet.png' 在单元格 0 到 7 中有动画帧
const playerManager = new BABYLON.SpriteManager("playerMgr", "textures/playerSpriteSheet.png", 1, {width: 64, height: 64}, scene);
const player = new BABYLON.Sprite("player", playerManager);
player.position.y = 0.5;
// 播放从单元格 0 到 7 的动画,循环,每帧延迟 100 毫秒
player.playAnimation(0, 7, true, 100);

粒子系统用于模拟涉及许多微小元素的现象,例如火、烟、爆炸、雨、火花或魔法效果。Babylon.js 有基于 CPU (ParticleSystem) 和高性能基于 GPU (GPUParticleSystem) 的粒子系统。

你通过定义属性来配置粒子系统,例如发射器(粒子起源地)、纹理、生命周期中的颜色、生命周期中的大小、发射速率、生命周期持续时间、速度、重力等。

// 1. 创建一个粒子系统(使用 GPU 以获得更好的性能)
const particleSystem = new BABYLON.GPUParticleSystem("particles", { capacity: 5000 }, scene);
// 对于 CPU: new BABYLON.ParticleSystem("particles", 5000, scene);
// 2. 设置粒子纹理
particleSystem.particleTexture = new BABYLON.Texture("textures/flare.png", scene);
// 3. 设置发射器(粒子来源)
// 可以是一个 Vector3 点或一个网格
// particleSystem.emitter = new BABYLON.Vector3(0, 1, 0); // 从一个点发射
const fountainMesh = BABYLON.MeshBuilder.CreateBox("fountainEmitter", {size: 0.1}, scene);
fountainMesh.isVisible = false; // 隐藏发射器网格本身
particleSystem.emitter = fountainMesh;
// 或者从体积发射(例如,盒子里)
particleSystem.minEmitBox = new BABYLON.Vector3(-0.5, 0, -0.5);
particleSystem.maxEmitBox = new BABYLON.Vector3(0.5, 0, 0.5);
// 4. 配置粒子外观和行为
// 颜色
particleSystem.color1 = new BABYLON.Color4(1, 0.5, 0, 1.0); // 起始颜色 (例如,橙色)
particleSystem.color2 = new BABYLON.Color4(1, 0, 0, 1.0); // 中间颜色 (例如,红色)
particleSystem.colorDead = new BABYLON.Color4(0.2, 0, 0, 0.0); // 结束颜色 (例如,深红色,透明)
// 大小
particleSystem.minSize = 0.1;
particleSystem.maxSize = 0.5;
// 生命周期
particleSystem.minLifeTime = 0.5; // 秒
particleSystem.maxLifeTime = 2.0; // 秒
// 发射速率
particleSystem.emitRate = 1000; // 每秒粒子数
// 发射方向(示例:向上喷泉)
particleSystem.direction1 = new BABYLON.Vector3(-0.2, 0.8, -0.2);
particleSystem.direction2 = new BABYLON.Vector3(0.2, 0.8, 0.2);
// 速度
particleSystem.minEmitPower = 1;
particleSystem.maxEmitPower = 3;
particleSystem.updateSpeed = 0.01; // 模拟速度调整
// 重力
particleSystem.gravity = new BABYLON.Vector3(0, -9.81, 0);
// 混合模式(用于透明纹理)
particleSystem.blendMode = BABYLON.ParticleSystem.BLENDMODE_ONEONE; // 加法混合通常适用于火/魔法效果
// 5. 启动系统
particleSystem.start();
// 停止: particleSystem.stop();
// 释放: particleSystem.dispose();

想象一下结合这些概念。创建一个地面平面。使用 SpriteManager 在其上随机散布数百个树木精灵。创建一个 GPUParticleSystem,使用 flare.png 纹理,将其配置为看起来像烟雾(偏灰的颜色,缓慢向上移动,低重力,更长的生命周期),设置其发射器位置,然后启动它。结果将是一个具有许多树木和从特定点升起烟雾效果的景观。