通过之前的知识,我们可以创建一个完整的场景,添加几何模型,光源,材质等等。但是它是一个静态的场景,接下来,我们将实现让场景动起来。
设置场景中的动画主要有两种方式。第一种是方式是定义一组对象,并在每一个对象上面定义模型的情况。第二种方法更复杂的动画,就是在动画运行时修改。
基本动画
我们需要使用Babylon.js封装的Animation类去实现动画。动画由各种属性和一组键定义。每个键给定当前时间的动画信息。
为了实现今天的动画,我们首先创建一个场景:
function createScene() {
//这里创建场景的基本对象:场景,光,相机
//创建一个立方体
var box1 = BABYLON.Mesh.CreateBox("Box1", 10.0, scene);
box1.position.x = -20;
我们的目标是移动这个立方体。首先创建一个Animation对象:
var animationBox = new BABYLON.Animation("myAnimation", "scaling.x", 30, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE);
参数有很多的内容:
参数1 - 此动画的名称,仅此而已。
参数2 - 变更的属性。这里可以写模型网格的任何属性,具体内容取决于你要修改的属性。这里我们让立方体沿X轴方向缩放,所以书写的为scaling.x
。
参数3 - 当前动画的每秒帧率,此动画最高帧率。
参数4 - 更改的属性值的类型。此处填写你修改的属性的值的类型,比如案例是修改沿X轴方向缩放的值是一个数字,我们就填写BABYLON.Animation.ANIMATIONTYPE_FLOAT
。其它可填写的值为:
- BABYLON.Animation.ANIMATIONTYPE_FLOAT 数字
- BABYLON.Animation.ANIMATIONTYPE_VECTOR2 二维矢量
- BABYLON.Animation.ANIMATIONTYPE_VECTOR3 三维矢量
- BABYLON.Animation.ANIMATIONTYPE_QUATERNION 四元数
- BABYLON.Animation.ANIMATIONTYPE_MATRIX 矩阵
- BABYLON.Animation.ANIMATIONTYPE_COLOR3 颜色
注意:默认情况下,使用矩阵的为值不会在键之间进行插值,这就意味着动画执行到两个键之间时,当前动画的值也是某个键的值。你可以通过设置BABYLON.Animation.AllowMatricesInterpolation = true;
来开启矩阵插值。如果启用了矩阵插值,则可以使用Matrix.Lerp或Matrix.DecomposeLerp作为插值工具。你还可以可以通过设置BABYLON.Animation.AllowMatrixDecomposeForInterpolation
属性来选择插值方式。
你可以在这里找到一个矩阵插值的案例演示:点击这里
参数5: - 最后的属性设置此动画执行完成后的行为(比如使用以前的值继续递增,重新开始动画,保持动画的最后一帧的效果)
可能的值有:
- 使用以前的值继续递增运算: BABYLON.Animation.ANIMATIONLOOPMODE_RELATIVE
- 从初始的值重新动画: BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE
- 保持最后的效果不动:BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT
现在我们学会了创建Animation
对象,下面我们将创建一组修改模型的键。在我们例子中,我们要缩放我们的盒子,但不是以线性的方式。而是缩小时速度快,而放大时速度慢。所以:
// 存放动画键的数组
var keys = [];
//第一个键,第0帧,缩放值为1
keys.push({
frame: 0,
value: 1
});
//在动画第20帧时,缩放值为 0.2
keys.push({
frame: 20,
value: 0.2
});
//在第100帧时,缩放值为1
keys.push({
frame: 100,
value: 1
});
对于Vector2,Vector3和Quaternion,您还可以提供具有inTangent和outTangent值的键,以使用样条插值而不是线性插值:
var keys = [];
keys.push({
frame: 0,
value: BABYLON.Vector3.Zero(),
outTangent: new BABYLON.Vector3(1, 0, 0)
});
keys.push({
frame: 20,
inTangent: new BABYLON.Vector3(1, 0, 0),
value: new BABYLON.Vector3(1, 1, 1),
outTangent: new BABYLON.Vector3(-1, 0, 0)
});
keys.push({
frame: 100,
inTangent: new BABYLON.Vector3(-1, 0, 0),
value: BABYLON.Vector3.Zero()
});
接下来的两个重要步骤:
- 将动画组添加到动画对象:
animationBox.setKeys(keys);
- 将此动画链接到我们的框:
box1.animations = [];
box1.animations.push(animationBox);
最后,你可以在应用程序中随时在一行代码中启动动画:
scene.beginAnimation(box1, 0, 100, true);
您还可以通过变换开始到结束的帧反向运行动画:
scene.beginAnimation(box1, 100, 0, true);
scene.beginAnimation的参数:
名称 | 类型 | 描述 | 是否必须要设置 |
---|---|---|---|
target | any | 当前动画的模型对象 | 必须 |
from | number | 动画的起始帧 | 必须 |
to | number | 动画的结束帧 | 必须 |
loop | boolean | 如果为true,动画将会循环(取决于BABYLON.Animation.ANIMATIONLOOPMODE) | 可选 |
speedRatio | number | 默认值为1,动画的速度比 | 可选 |
onAnimationEnd | () => void | 设置的动画结束时的回调函数,即使是手动停止的动画 (也取决于ANIMATIONLOOPMODE) | 可选 |
animatable | Animatable | 可选的特定动画对象 | 可选 |
stopCurrent | boolean | 结束时是否需要结束当前动画,默认为true,是 | 可选 |
当前函数会返回一个BABYLON.Animatable对象,你可以通过该对象访问单个动画(例如使用getAnimationByTargetProperty函数)。
该BABYLON.Animatable对象还支持以下功能:
- pause()
- restart()
- stop()
- reset()
接下来我们将通过获取BABYLON.Animatable对象来控制示例动画:
首先获取到对象:
var newAnimation = scene.beginAnimation(box1, 0, 100, true);
然后暂停:
newAnimation.pause();
这些命令将应用于Animatable的._animations数组中包含的每个动画对象。您还可以BABYLON.Animatable使用scene.getAnimatableByTarget()提供目标对象来访问当前运行的对象。
现在,你已经实现了修改盒子缩放的动画。也许你还想制作一个修改Y轴缩放的动画。不要犹豫,你可以给模型制作多个动画,并放入到animations组内即可。
动画和promises
从Babylon.js的v3.3版本开始,你可以使用promises等待动画结束:
var anim = scene.beginAnimation(box1, 0, 100, false);
console.log("动画执行之前");
await anim.waitAsync();
console.log("动画执行完成");
你可以在这里找到一个例子:点击这里
控制动画
每个Animation都有一个名为currentFrame的属性,用于指示当前的关键帧。
对于高级关键帧动画,你还可以定义用于在键之间的插值(转换)的函数。这些函数的默认值如下:
BABYLON.Animation.prototype.floatInterpolateFunction = function (startValue, endValue, gradient) {
return startValue + (endValue - startValue) * gradient;
};
BABYLON.Animation.prototype.quaternionInterpolateFunction = function (startValue, endValue, gradient) {
return BABYLON.Quaternion.Slerp(startValue, endValue, gradient);
};
BABYLON.Animation.prototype.vector3InterpolateFunction = function (startValue, endValue, gradient) {
return BABYLON.Vector3.Lerp(startValue, endValue, gradient);
};
以下是你可以更改的函数列表:
- floatInterpolateFunction
- quaternionInterpolateFunction
- quaternionInterpolateFunctionWithTangents
- vector3InterpolateFunction
- vector3InterpolateFunctionWithTangents
- vector2InterpolateFunction
- vector2InterpolateFunctionWithTangents
- sizeInterpolateFunction
- color3InterpolateFunction
- matrixInterpolateFunction
辅助函数
你可以使用扩展功能来创建快速动画:
Animation.CreateAndStartAnimation = function(name, mesh, targetProperty, framePerSecond, totalFrame, from, to, loopMode);
为了能够使用这个函数,你需要知道:
- 你的动画需要定义关键帧(需要定义两个:开始帧和结束帧)
- 动画仅适用于AbstractMesh对象
- 动画在调用方法之后会立即执行动画
以下是使用CreateAndStartAnimation()函数的简单示例:
BABYLON.Animation.CreateAndStartAnimation('boxscale', box1, 'scaling.x', 30, 120, 1.0, 1.5);
混合动画
你可以设置enableBlending = true 启动动画混合模式。混合动画将插入当前的对象的状态。这对于用户控制角色的行走或对输入设备的值的变化做出反应会非常方便。
在下面的一个案例中,每次点击右上角的FPS的标记时,新动画都会与立方体的当前位置混合:点击这里
虽然这个案例是将相同的动画混合到一起,但是更常见的是,将不同的动画混合到默认动画中。例如,从行走变为跑步动画
动画权重
从Babylon.js的v3.2版本开始,你可以使用特定权重开始动画。这意味着你可以使用此API在同一个目标上运行多个动画。最终值将是通过多个动画根据权重计算出来的值。
要使用权重开始动画,你需要使用新的API:scene.beginWeightedAnimation
// 权重为1
var idleAnim = scene.beginWeightedAnimation(skeleton, 0, 89, 1.0, true);
// 权重为0
var walkAnim = scene.beginWeightedAnimation(skeleton, 90, 124, 0, true);
// 权重为0
var runAnim = scene.beginWeightedAnimation(skeleton, 125, 146, 0, true);
这个函数接受以下参数:
名称 | 类型 | 描述 | 是否可选 |
---|---|---|---|
target | any | 当前的动画的目标对象 | 必填 |
from | number | fps起始帧 | 必填 |
to | number | fps结束帧 | 必填 |
weight | number | 当前动画的权重,默认为1 | 可选 |
loop | boolean | 如果为true,则动画将循环(取决于BABYLON.Animation.ANIMATIONLOOPMODE) | 可选 |
speedRatio | number | default : 1. The speed ratio of this animation | 可选 |
onAnimationEnd | () => void | 在动画结束时触发的功能,即使手动停止动画(也取决于ANIMATIONLOOPMODE) | 可选 |
animatable | Animatable | 可选的特定动画 | 可选 |
就像beginAnimation,这个函数也会返回一个动画对象animatable ,但这次将其weight属性设置为了一个可修改值。
你可以随时修改weight值来修改动画的权重。该值介于0到1之间。同样,你可以将其设置为-1关闭权重模式。如果权重值设置为0,则动画将被视为暂停。
var idleAnim = scene.beginWeightedAnimation(skeleton, 0, 89, 1.0, true);
var runAnim = scene.beginWeightedAnimation(skeleton, 125, 146, 0, true);
idleAnim.weight = 0.5;
runAnim.weight = 0.5
如果你动画帧长度不同,则需要使用以下代码打开动画同步:
idleAnim.syncWith(runAnim);
要禁用动画同步,只需调用即可animation.syncWith(null)。
可在此处找到完整的案例:点击此处
覆盖属性
如果模型具有多个动画或者骨骼,则可以使用animationPropertiesOverride同时为所有子动画指定一些常规属性。这些属性将覆盖默认的动画属性:
var overrides = new BABYLON.AnimationPropertiesOverride();
overrides.enableBlending = true;
overrides.blendingSpeed = 0.1;
skeleton.animationPropertiesOverride = overrides;
以下是可以覆盖的属性列表:
- enableBlending
- blendingSpeed
- loopMode
注意,如果动画目标没有使用scene.animationPropertiesOverride,则将自动使用。
easing功能
您可以使用easing功能为动画添加一些行为。如果您想了解有关缓动函数的更多信息,请参阅以下链接:
MSDN Easing functions documentation
Easing functions cheat sheet
所有这些easing功能都在BABYLON中实现,允许您将自定义数学公式应用于动画。
以下是您可以使用的预定义缓动函数:
- BABYLON.CircleEase()
- BABYLON.BackEase(amplitude)
- BABYLON.BounceEase(bounces, bounciness)
- BABYLON.CubicEase()
- BABYLON.ElasticEase(oscillations, springiness)
- BABYLON.ExponentialEase(exponent)
- BABYLON.PowerEase(power)
- BABYLON.QuadraticEase()
- BABYLON.QuarticEase()
- BABYLON.QuinticEase()
- BABYLON.SineEase()
- BABYLON.BezierCurveEase()
您可以使用EasingMode属性来更改缓动函数的行为方式,即更改动画插值的方式。您可以为EasingMode提供三种可能的值: - BABYLON.EasingFunction.EASINGMODE_EASEIN :插值遵循与缓动函数关联的数学公式。
- BABYLON.EasingFunction.EASINGMODE_EASEOUT :插值遵循100%插值减去与缓动函数关联的公式的输出。
- BABYLON.EasingFunction.EASINGMODE_EASEINOUT :Interpolation在动画的前半部分使用EaseIn,在下半部分使用EaseOut。
以下是在CircleEase缓动函数中为环面设置动画的简单示例:
//创建一个30帧的三维向量动画
var animationTorus = new BABYLON.Animation("torusEasingAnimation", "position", 30, BABYLON.Animation.ANIMATIONTYPE_VECTOR3, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE);
//环面的目标位置
var nextPos = torus.position.add(new BABYLON.Vector3(-80, 0, 0));
// 动画帧
var keysTorus = [];
keysTorus.push({ frame: 0, value: torus.position });
keysTorus.push({ frame: 120, value: nextPos });
animationTorus.setKeys(keysTorus);
//创建一个 easing function
var easingFunction = new BABYLON.CircleEase();
// 对于 easing function, 你可以设置 EASEIN (default), EASEOUT, EASEINOUT
easingFunction.setEasingMode(BABYLON.EasingFunction.EASINGMODE_EASEINOUT);
// 将 easing function 添加到动画
animationTorus.setEasingFunction(easingFunction);
// 将创建的动画添加到环面对象上
torus.animations.push(animationTorus);
//最后,启动动画,设置从0帧到100帧,循环运行
scene.beginAnimation(torus, 0, 120, true);
您也可以使用Bezier曲线算法,使用BezierCurveEase(x1,y1,x2,y2)函数。为此,这里有一个很好的参考来创建您的曲线算法:http://cubic-bezier.com/
这是一个使用贝塞尔曲线算法的非常酷的实现:
var bezierEase = new BABYLON.BezierCurveEase(0.32, -0.73, 0.69, 1.59);
最后,您可以扩展EasingFunction基本函数以创建自己的缓动函数,如下所示:
var FunnyEase = (function (_super) {
__extends(FunnyEase, _super);
function FunnyEase() {
_super.apply(this, arguments);
;}
FunnyEase.prototype.easeInCore = function (gradient) {
// 下面是核心算法,来实现你自己的 Easing Function
// Gradient是一个百分比的值
return Math.pow(Math.pow(gradient, 4), gradient);
};
return FunnyEase;
})(BABYLON.EasingFunction);
你可以在官方案例中找到完整的演示:Easing function playground
复杂动画
复杂动画允许你选中设置每一帧的值。运行时代码必须要设置在此函数中:
scene.registerBeforeRender(function () {
//你书写的代码
});
此功能对于像游戏这样的复杂动画非常有用,其中角色必须根据许多参数移动。
结合这些多种功能,我相信大家可以做的非常好。
不要忘记访问官方API文档,了解Babylon.js动画和Babylon.js动画类的更多信息。
将事件添加到动画
从Babylon.js版本2.3开始,您可以将动画事件附加到动画上的特定帧。
事件将会在设置的帧数上触发。
这样做非常简单:
// 创建事件的三个参数:
// - 触发事件的帧数
// - 触发的事件
// -如果只触发一次,则为false(默认值),多次触发设置为true
var event1 = new BABYLON.AnimationEvent(50, function() { console.log("Yeah!"); }, true);
// 绑定到动画上面
animation.addEvent(event1);
确保同步
有时,我们需要确保动画,物理和游戏逻辑的运行同步并通过帧速率方差解耦。这可能有助于在给定相同的初始条件和输入的情况下重放场景的演变方式,或者在多用户环境中最小化多个客户端的差异。
原理是通过以离散时间步长更新固定频率的状态来量化状态执行时间,从而使累加器保持超过下一帧更新的时间。
为此,需要通过以下两个选项创建Babylon引擎:
this.engine = new BABYLON.Engine(theCanvas, true, {
deterministicLockstep: true,
lockstepMaxSteps: 4
});
这样,场景将通过物理引擎中设置的timeStep量的离散块来渲染量化物理和动画步骤。例如:
var physEngine = new BABYLON.CannonJSPlugin(false);
newScene.enablePhysics(this.gravity, physEngine);
physEngine.setTimeStep(1/60);
使用上面的代码,引擎将以每秒60帧的速度运行不连续的步骤,并且在有延迟帧的渲染的情况下,它将尝试计算最后4步(lockstepMaxSteps)以恢复最终累积延迟,然后再渲染帧。
请注意,在显式创建CannonJSPlugin时,必须在其构造函数中将false作为_useDeltaForWorldStep参数传递,以禁用CannonJS内部累加器。
要与步骤同步运行逻辑代码,场景中有以下两个可观察对象:
newScene.onBeforeStepObservable.add(function(theScene){
console.log("执行游戏逻辑, 获取动画和物理之前的stepId : "+theScene.getStepId());
});
newScene.onAfterStepObservable.add(function(theScene){
console.log("执行游戏逻辑, 获取动画和物理之后的stepId: "+theScene.getStepId());
});
使用它们可以在动画和物理更新之前和之后运行任意逻辑代码。
在下面的示例中,您可以在控制台中看到其中球体被视为静止的stepId和旋转框的旋转值。无论帧速率如何,多次运行始终会产生相同的值。点击这里查看案例