AEJoy —— 表达式之模拟弹簧【JS】

546 阅读3分钟

效果图

一层链接一层,就像用一根松紧带连接起来一样

097.gif

想法

那些使用 After Effects 足够长时间并记得 Motion Math 的人可能还记得一个非常酷的脚本 spring.mm 。这个脚本使用弹簧运动方程在两个层之间附加一个 “弹簧” 。当表达式出现时,大多数的 Motion Math 脚本都很容易转换为表达式,但是 spring.mm 神秘地消失了。

事实证明,在一些任务中,Motion Math 比表达式更擅长。这是因为在 Motion Math 的操作方式上有一个关键的区别。Motion Math 的优势在于可以一次性完成所有帧的计算。这意味着 Motion Math 能够保存从一帧到下一帧的信息,这对于这样的模拟非常重要。正如您现在可能已经发现的那样,表达式没有这样的方式将信息从一个帧传递到下一个帧

所以我们在这里做的是考虑到表达式的限制,尽可能想出一些方法来复制 Motion Math 的 spring 脚本的功能,。

设计

经验丰富的表达式编写人员会开发许多技术,以避免表达式中缺乏持久数据所遇到的问题。当没有其他选项可用时,还有一种最后的方法,即使用蛮力方法,即设置表达式,以便在每一帧上重新创建在前一帧上发生的所有事情。这意味着,例如,在第 100 帧时,表达式必须运行 100 次计算(之前的每一帧运行一次,加上当前帧运行一次)。显然,如果计算很复杂,而且计算很长,这真的会让事情陷入困境。

然而,对于较短的序列,这种技术或许是可行的,也是我们将在这里使用的。所以这个想法是相当简单的 —— 我们将创建一个循环,表达式从第 0 帧开始,并为每一帧更新一次 spring 计算,直到它到达当前帧。我们将假设建立运动的图层被称为 “leader” ,我们将应用表达式到另一个图层的位置属性上。

这种逐帧技术还有一个不那么极端的版本,如果你正在做的事情只涉及到发生在前一帧的最近发生的某个事件,那么就可以使用它。这方面的一个例子便是当音频级别超过某个阈值时便会触发动画。在这种情况下,您将从当前帧向后循环,直到找到那个事件。

表达式代码

restLength = 20; ///< 从 leader 层余出的距离(像素)
damp = .95; ///< 定义了链接的弹性衰减因子
leader = thisComp.layer("leader");

fDur = thisComp.frameDuration;
currFrame = Math.round(time / fDur); ///< 当前帧数

p2 = position.valueAtTime(0); ///< 时刻 0 时本层的位置
v2 = 0;
for (f = 0; f <= currFrame; f++) { ///< 从第 0 帧开始,循环到当前帧数

    /// @note 以下就是从 Motion Math 脚本借鉴的弹簧计算
    t = f * fDur;
    p1 = leader.transform.position.valueAtTime(t); ///< leader 层对应帧所处时刻的位置
    delta = p2 - p1;
    nDelta = normalize(delta); ///< 确定 leader 与本层位置的方向
    a = 2 * nDelta * (length(delta) - restLength) * fDur;
    v2 = (v2 - a) * damp;
    p2 += v2; ///< 更新本层的位置
}
p2

注意,如果这两个层始终占据相同的位置,这个表达式将生成一个除零错误。当你第一次设置合成时,你可能会碰到这个问题,解决方法也很简单,只需要稍微移动其中一个图层并重新启用表达式。