Three.js Animation time和timeScale

789 阅读5分钟

Time和TimeScale

Time

在three.js的动画系统中,总共有两个timeAnimationMixer.time以及AnimationAction.time

AniamtionMixer.time:一个全局的时刻,会影响所有的AninmationAction.time

AnimationAction.time:一个局部的时刻,用来表示AnimationClip播放到“哪里了”,所以,time的范围是[0...AnimationClip.duration]

AnimationMixer.time是一个全局的时间,它的范围不会是某一个AnimationClip.time,在一般情况下,这个时刻会在动画循环中不断推进,因此,会把AnimnationMixer.time映射为AnimationAction.time,当AnimationMixer.time改变时,所有的AnimationAction.time都会跟着改变,从而动画被驱动。

TimeScale

AnmationMixer.timeScale

AnimationMixer.timeScale用来控制时刻(时间)推进的速率。瞅一眼源码,发现这个属性只在一个地方使用:

update( deltaTime ) {

    deltaTime *= this.timeScale;

    //...
}

在一般的用法中,需要在动画循环中调用AnimationMixer.update(delta)用来推进全局时刻,方法的delta参数,通过Clock来获取,获取的值为现实世界的时间间隔。当需要改变“时间推进的速率”时,可以通过设置timeScale来实现。

举一个形象一点的例子,在我们看视频的时候,默认情况是1倍速播放;当我么你需要快进时,就需要选择倍速播放中的2倍速甚至3倍速。类比到这里也是一样,可以通过设置timeScale来控制全局播放倍速

可以通过设置timeScale = 0来进行全局暂停。

它甚至可以是负值,这样就会进行“倒放”。

在调用AnimationMixer.setTime()时,也是通过调用update实现的,因此,setTime()方法也会受到timeScale的影响。

AnimationAction.timeScale

AnimationAciton.timeScale是一个局部(相对于AnimationMixer.timeScale)的控制。它会对全局的时刻再次进行缩放(调整映射方式),从而局部的控制每一个AnimationClip的播放速率。

AnimationAction.timeScale和AnimationAction._effectiveTimeScale

_effectiveTimeScale:getter、setter

在查看有关于timeSccale的API时,会找到_effectiveTimeScalesettergetter

setEffectiveTimeScale( timeScale ) {

    this.timeScale = timeScale;
    this._effectiveTimeScale = this.paused ? 0 : timeScale;

    return this.stopWarping();
}

通过_effectiveTimeScale这个名字就可以看出来,真正对time产生影响的是_effectiveTimeScale

至于getter,就是相当简单的返回这个内部的属性。

getEffectiveTimeScale() {

    return this._effectiveTimeScale;
}

可以看到,_effectiveTimeScale会受到paused属性的影响,如果pausedtrue,表示当前是暂停状态,也就是说,现在time的值是不会变的,因此,将_effectTimeScale设置为0,当全局时间改变时,AnimationAction局部的time受到_effectTimeScale的影响,就不会发生改变,实现暂停pause )的效果。

内部方法:_updateTimeScale()

在上面我们说到过,引起动画推进需要调用mixer.update(),我们跟踪一下源码,可以看到mixer.update()调用了action._update()方法。

AnimationAction._update()方法中,我们可以找到以下代码:

// apply time scale and advance time

deltaTime *= this._updateTimeScale( time );
const clipTime = this._updateTime( deltaTime );

也就是对时间推进的变化量deltaTime应用timeScale,然后推进time

接着跟踪代码到_updateTimeScale中。

_updateTimeScale(time) {

    let timeScale = 0;

    if (!this.paused) {

        timeScale = this.timeScale;

        const interpolant = this._timeScaleInterpolant;

        if (interpolant !== null) {

            const interpolantValue = interpolant.evaluate(time)[0];

            timeScale *= interpolantValue;

            if (time > interpolant.parameterPositions[1]) {

                this.stopWarping();

                if (timeScale === 0) {

                    // motion has halted, pause
                    this.paused = true;

                } else {

                    // warp done - apply final time scale
                    this.timeScale = timeScale;

                }

            }

        }

    }

    this._effectiveTimeScale = timeScale;
    return timeScale;

}

先不管其他代码,只看最后两句

    this._effectiveTimeScale = timeScale;
    return timeScale;

这个返回值在_update()方法中应用到deltaTime。也就是说,真正起到作用的,是内部的_effectiveTime

那为什么我们设置this.timeScale,也会有效呢?

继续往下看就可以发现(先不考虑interpolant这个分支),在不是暂停状态(即paused === false)时,会进入if分支,在这个分支里有timeScale = this.timeScale,最后timeScale赋值给了_effectiveTimeScale

接下来进入比较难理解的地方了,也就是上面没有谈到的interpolant分支。

warp(): 对_effectiveTimeScale线性插值

继续上面的代码,首先可以看到,const interpolant = this._timeScaleInterpolant;。要找一下this._timeScaleInterpolant在哪里被设置。

通过搜索代码可以找到,在warp()方法中被设置。

warp(startTimeScale, endTimeScale, duration) {

  const mixer = this._mixer,
    now = mixer.time,
    timeScale = this.timeScale;

  let interpolant = this._timeScaleInterpolant;

  if (interpolant === null) {

    interpolant = mixer._lendControlInterpolant();
    this._timeScaleInterpolant = interpolant;

  }

 //...

  return this;

}

this._timeScaleInterpolant的创建是通过mixer._lendControlInterpolant()完成的,看一下这个方法:

_lendControlInterpolant() {

  const interpolants = this._controlInterpolants,
    lastActiveIndex = this._nActiveControlInterpolants++;

  let interpolant = interpolants[lastActiveIndex];

  if (interpolant === undefined) {

    interpolant = new LinearInterpolant(
      new Float32Array(2), new Float32Array(2),
      1, _controlInterpolantsResultBuffer);

    interpolant.__cacheIndex = lastActiveIndex;
    interpolants[lastActiveIndex] = interpolant;

  }

  return interpolant;

}

这个方法就是返回了一个线性插值LinearInterpolant对象。关于LinearInterpolant,可以通过我的另一篇博客了解:Three.js Interpolant - 掘金 (juejin.cn)

接着返回到wrap()函数:

warp(startTimeScale, endTimeScale, duration) {

  const mixer = this._mixer,
    now = mixer.time,
    timeScale = this.timeScale;

  let interpolant = this._timeScaleInterpolant;

  if (interpolant === null) {

    interpolant = mixer._lendControlInterpolant();
    this._timeScaleInterpolant = interpolant;

  }

  const times = interpolant.parameterPositions,
    values = interpolant.sampleValues;

  times[0] = now;
  times[1] = now + duration;

  values[0] = startTimeScale / timeScale;
  values[1] = endTimeScale / timeScale;

  return this;

}

后面部分的代码,就是给线性插值设置采样参数。需要注意的地方就是,value并不是直接将startTimeScaleendTimeScale赋值,而是都“除以timeScale”,在_updateTimeScale方法中,获取当前timeScale时的语句为timeScale *= interpolantValue。相当于timeScalestartTimeScale线性变化到endTimeScale

其他改变TimeScale的API

在上面,已经介绍了setEffectiveTimeScale()warp()方法。

还有一个使用warp()实现的方法halt(),它的作用就是将timeScale线性变化到0

halt(duration) {
  return this.warp(this._effectiveTimeScale, 0, duration);
}

可以看到,上面的代码变化的起点是_effectiveTimeScale,再次证明了,某个时间点,正在起作用的timeScale_effectiveTimeScale

如果设置了paused = true,在_updateTimeScale中,会将_effectiveTimeScale置为0

对于setDuration(),即设置动画的时长,也是通过调整timeScale实现的。

setDuration(duration) {

  this.timeScale = this._clip.duration / duration;

  return this.stopWarping();
}

对于syncWith()方法,通过方法的名称也可以发现一些端倪,也就是要和另一个action同步,也就是设置相同的timetimeScale

syncWith(action) {

  this.time = action.time;
  this.timeScale = action.timeScale;

  return this.stopWarping();
}

最后,说一下我们上面一直没有谈到的stopWarping(),它的作用简单描述就是停止timeScale的线性变化(通过warp()halt()开启)。停止的方法也非常简单,只需要将this._timeScaleInterpolant设置为null,这样在_updataTimeScale中,直接使用this.timeScale作为_effectiveTimeScale

stopWarping() {

  const timeScaleInterpolant = this._timeScaleInterpolant;

  if (timeScaleInterpolant !== null) {

    this._timeScaleInterpolant = null;

    // mixer也保存了timeScaleInterpolant,这里是为了解除mixer中的引用,防止内存泄漏
    this._mixer._takeBackControlInterpolant(timeScaleInterpolant);
  }
  return this;

}

总结

本文主要介绍了timetimeScale以及相关的API的实现源码。

AnimationAction.time类似于一个“进度条”,表示当前的AnimationClip播放到哪里了;AnimationAction.timeSclae类似于视频播放的播放倍率,但在内部实现时,某个时刻真正起作用的是_effectiveTimeScale

在一般情况下timeScale_effectiveTimeScale一样的,在以下情况中,会出现不相等的情况:

  1. paused = true时,_effectiveTimeScale = 0

  2. _effectiveTimeScale开启线性插值:

    • warp()

    • halt():实际上还是调用了warp()