简单的demo是什么样的
根据官网的例子,我们可以得知,一个最简单的动画demo是这样的:
const frameRate = 10;
const xSlide = new BABYLON.Animation("xSlide", "position.x", frameRate, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE);
const keyFrames = [];
keyFrames.push({
frame: 0,
value: 2
});
keyFrames.push({
frame: frameRate,
value: -2
});
keyFrames.push({
frame: 2 * frameRate,
value: 2
});
xSlide.setKeys(keyFrames);
box.animations.push(xSlide);
scene.beginAnimation(box, 0, 2 * frameRate, true);
这里面有一个比较难懂的东西,就是 frame 相关的东西。 例如:
- frameRate
- keyFrames中的frame字段
- beginAnimation中的 from to 字段
上面的三个字段,都和 frame 相关。
标准的说法是什么
按照官方的说法,frameRate是来定义这个动画一秒有几帧,但这个一秒有几帧不是说一秒渲染几帧:
frames per second:
the number of animation frames per second
(independent of the scene rendering frames per second)
最后的英语说的就是:和场景的渲染帧率没什么关系。
然后我们再来看keyFrames中的frame字段,我们设置的时候,其实就是说在这一帧的时候,我们想要目标的value是什么值。挺正常的,到目前为止。
再来看,beginAnimation 的from 和to,也还行,就是说这个动画,从第几帧开始跑,然后跑到第几帧停。当然最后的true,就是说,跑到to之后,又重头跑,就是循环的意思。
有什么问题吗?
有,如果我把第一行写成这样:
const frameRate = 0.0001;
或者这样:
const frameRate = 100000;
你猜怎么着?
你跑起来试试吧,效果一模一样,没有任何变化。
本篇文章就是来讲,这个frameRate,为什么随便写,在这个代码下,为什么效果没变化。
当我frameRate = 0.0001的时候,
scene.beginAnimation(box, 0, 2 * frameRate, true);
其实就是:
scene.beginAnimation(box, 0, 0.0002, true);
如何理解,从第0帧,一直跑到第0.0002帧,帧tm的还能有小数???
你当这是哈利波特呢,搞个什么九又四分之三站台。
分析过程
- 第一步,绞尽脑汁想:结果GG,完全想不通。
- 第二步,试图看官方文档:结果GG,完全没有相关的说明。
- 第三步,去官方论坛上提问题:结果还没出来(在写本篇文章时)。
- ultimate,看源码吧:
结果释然了。
源码分析
先去github clone吧。然后根据beginAnimation这个关键字,去整个工程里搜,看这个函数的实现。
反正一步一步看吧,看到最后,核心代码是这个:
// 源码路径: packages/dev/core/src/Animations/runtimeAnimation.ts
// 函数:RuntimeAnimation.animate, RuntimeAnimation类的animate 函数
函数声明:
public animate(elapsedTimeSinceAnimationStart: number, from: number, to: number, loop: boolean, speedRatio: number, weight = -1.0)
关键的参数有:
- elapsedTimeSinceAnimationStart
- from
- to
上面,from to,就是最初的from to,就是从哪帧,到哪帧,是我们的代码中直接透传进去的。
elapsedTimeSinceAnimationStart,顾名思义,就是说动画开始一直到当前进入这个函数的时候,总共执行了的时间,单位是毫秒。
我们带着这个问题来分析此函数的实现:
- from 和 to 为什么可以是小数?
我们这里先看看elapsedTimeSinceAnimationStart起了什么作用:
let absoluteFrame = (elapsedTimeSinceAnimationStart * (animation.framePerSecond * speedRatio)) / 1000.0 + this._absoluteFrameOffset;
看这种稍微有点长度的数学计算时,应该用假设法:
- 假设speedRatio是1
- 假设this._absoluteFrameOffset是0 好了,等式简化:
let absoluteFrame = (elapsedTimeSinceAnimationStart * 0.0001) / 1000.0;
这也挺好理解的,0.0001 / 1000.0 其实就是每毫秒的帧数,那么乘以总的毫秒数,就是总的帧数,跟变量名一致。实际上,这个结果,还是小数,不管了,接着往下看。
这段代码是我精简了的,反正就是通过absoluteFrame接着算出另一个东西:
currentFrame = from + (absoluteFrame % (to - from));
如果from是0:
currentFrame = absoluteFrame % to;
实际上,在大部分情况下,可以直接简化:
currentFrame = absoluteFrame;
再往下看,就是进入一个关键函数:
const currentValue = animation._interpolate(currentFrame, this._animationState);
看名字就知道,要拿着currentFrame这个值,要去插值了。
我们看看最后这个函数是怎么插值的:
const frameDelta = endKey.frame - startKey.frame;
// gradient : percent of currentFrame between the frame inf and the frame sup
let gradient = (currentFrame - startKey.frame) / frameDelta;
这个 gradient 其实就是在算一个百分比,他自己代码中的注释都很明显的说明了。
最后的最后:
switch (this.dataType) {
// Float
case Animation.ANIMATIONTYPE_FLOAT: {
const floatValue = useTangent
? this.floatInterpolateFunctionWithTangents(startValue, startKey.outTangent * frameDelta, endValue, endKey.inTangent * frameDelta, gradient)
: this.floatInterpolateFunction(startValue, endValue, gradient);
switch (state.loopMode) {
case Animation.ANIMATIONLOOPMODE_CYCLE:
case Animation.ANIMATIONLOOPMODE_CONSTANT:
case Animation.ANIMATIONLOOPMODE_YOYO:
return floatValue;
case Animation.ANIMATIONLOOPMODE_RELATIVE:
case Animation.ANIMATIONLOOPMODE_RELATIVE_FROM_CURRENT:
return (state.offsetValue ?? 0) * state.repeatCount + floatValue;
}
break;
}
...
...
}
就很简单,通过this.floatInterpolateFunction和gradient这个百分比,进行插值。
插值的结果,就直接返回了,我们看看,插值的结果干了啥:
const currentValue = animation._interpolate(currentFrame, this._animationState);
// Set value
this.setValue(currentValue, weight);
oh,直接setValue了。
结论
Babylonjs中的animation中的帧的概念是一个比较数学的概念,他可以是小数,可以是任何大的数,不要以为frameRate = 100000就是一秒有100000帧。
通过看源码,清楚的知道了,在设置这些动画的时候,我们只要关注一个事情即可:
- 一切都是插值,无他。