Time和TimeScale
Time
在three.js的动画系统中,总共有两个time:AnimationMixer.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时,会找到_effectiveTimeScale的setter和getter:
setEffectiveTimeScale( timeScale ) {
this.timeScale = timeScale;
this._effectiveTimeScale = this.paused ? 0 : timeScale;
return this.stopWarping();
}
通过_effectiveTimeScale这个名字就可以看出来,真正对time产生影响的是_effectiveTimeScale。
至于getter,就是相当简单的返回这个内部的属性。
getEffectiveTimeScale() {
return this._effectiveTimeScale;
}
可以看到,_effectiveTimeScale会受到paused属性的影响,如果paused为true,表示当前是暂停状态,也就是说,现在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并不是直接将startTimeScale和endTimeScale赋值,而是都“除以timeScale”,在_updateTimeScale方法中,获取当前timeScale时的语句为timeScale *= interpolantValue。相当于timeScale从startTimeScale线性变化到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同步,也就是设置相同的time和timeScale:
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;
}
总结
本文主要介绍了time,timeScale以及相关的API的实现源码。
AnimationAction.time类似于一个“进度条”,表示当前的AnimationClip播放到哪里了;AnimationAction.timeSclae类似于视频播放的播放倍率,但在内部实现时,某个时刻真正起作用的是_effectiveTimeScale。
在一般情况下timeScale和_effectiveTimeScale一样的,在以下情况中,会出现不相等的情况:
-
当
paused = true时,_effectiveTimeScale = 0 -
对
_effectiveTimeScale开启线性插值:-
warp() -
halt():实际上还是调用了warp()
-