Three.js AnimationAction weight以及相关API

494 阅读3分钟

weight以及相关API

与动画播放相关的核心属性有两个:timeweight。有关time的内容介绍,请看我的另一篇文章Three.js Animation time和timeScale - 掘金 (juejin.cn)

time用来驱动动画的播放进度,而weight则表示动画的影响程度。例如,一个动画是线性的从1到2(对应time为0和1)改变一个属性。则当time === 0.5时,这个属性的值为1.5。这是在time那篇文章中了解到的内容,但是这里有一个前提没有说,那就是weight === 1。如果weight === 0.5,上面time === 0.5时,属性的值为0.5 * 1.5 = 0.75。这就是weight的作用,用来控制一个动画对属性的影响。

_effectiveWeight

这个属性和_effectiveTimeScale很类似,是真正起作用的weight

同样是在AnimationAction._update()中(建议先看一下上面提到的time相关的文章),接着timeScale下方的代码:

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

// note: _updateTime may disable the action resulting in
// an effective weight of 0
const weight = this._updateWeight(time);

在这个方法中,使用了_updateWeight()方法:

先不管其他的代码,只关注最后两句。

_updateWeight(time) {

  let weight = 0;

  if (this.enabled) {

    weight = this.weight;
    const interpolant = this._weightInterpolant;

    if (interpolant !== null) {

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

      weight *= interpolantValue;

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

        this.stopFading();

        if (interpolantValue === 0) {

          // faded out, disable
          this.enabled = false;

        }

      }

    }

  }

  this._effectiveWeight = weight;
  return weight;

}

最后两句,就是将this._effectiveWeight的值返回,而更新动画时,使用的就是这个值,所以说,真正影响动画的,是_effectiveWeight

再仔细看上面的代码,在以下两种情况都满足时,weight === _effectiveWeight

  1. enabled === true

  2. _weightInterpolant === null

反之,在enabled === false时,_effectiveWeight === 0,这就导致了,动画对属性没有影响。

_weightInterpolant !== null时,也就是说,_effectiveWeight被线性插值,与timeScale几乎一摸一样的逻辑。

_effectiveWeight进行线性插值,就是让动画有一定的“过渡”,如果动画的某个属性的起始值和这个属性的当前值差距比较大,动画开始播放时,这个值瞬间变化到动画的初始属性,感觉是瞬间移动过去的。为了让过渡阶段平滑一些,可以把weight(_effectiveWeight)从0线性变化到1。这就是AnimationAction.fadeIn()方法的作用。

fadeIn(duration) {

  return this._scheduleFading(duration, 0, 1);

}

fadeIn()的实现也就是通过_scheduleFading()方法设置一个线性插值,在duration的时间,把weight从0变化到1。接着看_scheduleFading()方法的代码:

_scheduleFading(duration, weightNow, weightThen) {

  const mixer = this._mixer, now = mixer.time;
  let interpolant = this._weightInterpolant;

  if (interpolant === null) {

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

  }

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

  times[0] = now;
  values[0] = weightNow;
  times[1] = now + duration;
  values[1] = weightThen;

  return this;
}

和对timeScale插值一样,通过mixer_lendControlInterpolant()创建一个LinearInterpolant对象,然后设置插值的参数。最终,在AnimationAction._updateWeight()获得某一个时刻_effectiveWeight的值。

除了有fadeIn,还有fadeOut方法,在动画结束时进行过度,将weight从0线性变化到1:

fadeOut(duration) {
  return this._scheduleFading(duration, 1, 0);
}

还有另外两个过度的方法:crossFadeFrom()crossFadeTo()

crossFadeFrom(fadeOutAction, duration, warp) {

  fadeOutAction.fadeOut(duration);
  this.fadeIn(duration);

  if (warp) {

    const fadeInDuration = this._clip.duration,
      fadeOutDuration = fadeOutAction._clip.duration,

      startEndRatio = fadeOutDuration / fadeInDuration,
      endStartRatio = fadeInDuration / fadeOutDuration;

    fadeOutAction.warp(1.0, startEndRatio, duration);
    this.warp(endStartRatio, 1.0, duration);

  }

  return this;
}

crossFadeFrom让“自己”过渡开始,同时使指定的action过渡结束。如果指定了warp = true,还会对timeScale进行线性变化。

crossFadeTo刚好是反过来的,“自己”过渡结束,同时让另一个过度进入。

crossFadeTo(fadeInAction, duration, warp) {

  return fadeInAction.crossFadeFrom(this, duration, warp);

}

上述的API都是通过线性插值的方式改变_effectiveWeight,还有一个setEffectiveWeight()方法,用来直接设置:

setEffectiveWeight(weight) {

  this.weight = weight;

  // note: same logic as when updated at runtime
  this._effectiveWeight = this.enabled ? weight : 0;

  return this.stopFading();

}

当然,它也有对应的getter:

getEffectiveWeight() {

  return this._effectiveWeight;

}

总结

timeScale类似,weight在内部实现时,也分成了Animation.weightAnimation._effectiveWeight,在enable === false或者对_effectiveWeight进行线性插值时(调用了各种fade方法),weight_effectiveWeight是不相等的。