2. Babylonjs 的动画 frameRate 的概念

267 阅读4分钟

简单的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.floatInterpolateFunctiongradient这个百分比,进行插值。
插值的结果,就直接返回了,我们看看,插值的结果干了啥:

const currentValue = animation._interpolate(currentFrame, this._animationState);

// Set value
this.setValue(currentValue, weight);

oh,直接setValue了。

结论

Babylonjs中的animation中的帧的概念是一个比较数学的概念,他可以是小数,可以是任何大的数,不要以为frameRate = 100000就是一秒有100000帧

通过看源码,清楚的知道了,在设置这些动画的时候,我们只要关注一个事情即可:

  • 一切都是插值,无他。