【AE表达式】逼真的反弹和过冲
金鱼不反弹。
——巴特·辛普森

有时您可能想用一些物理真实感来修饰动画动作。比如有一个图层从 0% 快速放大到 100%,并且想要添加一点过冲和振荡,最终稳定在 100%。另一个示例是,有一个对象落入地面中,希望它在触底时反弹一点。

这两个场景看起来相似,但它们代表了非常不同的物理过程。可以使用表达式创建这些模拟中的任何一个,但选择正确的模拟很重要。在本文中,我将详细介绍这些动画工具,并提供有关如何以及何时应用它们的一些提示。

反弹与过冲

在反弹和过冲情况下,都在处理衰减幅度。对于过冲,通常会处理谐波振荡,就像处理钟摆或弹簧一样。这意味着随着振幅的衰减,频率保持不变(在物体的共振频率上)。

通常用指数衰减的正弦波来模拟。这是一个简单的解决方案,尽管绝对有一个技巧可以使初始幅度与传入的动画正确匹配)。

这是用于模拟谐波共振的指数衰减正弦波的波形。请注意,随着幅度衰减,但频率是恒定的。

Bounce(反弹) 是一个完全不同的现象。当物体反弹时,每次反弹都会损失能量,这会影响振幅频率。随着反弹幅度的减小,它们发生的频率更高。这意味着正弦波不足以进行反弹。实际上,反弹波形实际上是一系列幅度递减的抛物线。物体停在弹跳的顶部,然后加速(由于某种类似重力的现象),所以涉及的数学完全不同。

该波形由弹跳仿真表达式生成。请注意,随着物体失去能量,反弹越发频繁

您可能会想通过获取振荡正弦波的绝对值(使用 JavaScriptMath.abs()函数)并将频率变量链接到一个滑块来模拟这种反弹行为,然后设置置关键帧加快速度。最好不要这样做。如果很好奇惑,请参阅“表达速度和频率控制”文章,了解正确执行此操作涉及哪些方面。

过冲细节

看一看指数衰减正弦波的基本表达式:

amp = 80;
freq = 1;
decay = 1;

t = time - inPoint;
amp*Math.sin(t*freq*Math.PI*2)/Math.exp(t*decay);

当它处于静止状态时,此表达式将在图层的入点处触发衰减的正弦波振荡。

前三行设置波形的参数:最大幅度为 80,每秒振荡频率为 1,衰减(幅度减小的速度)值为 1。变量t用于计算自图层入点以来的时间。

所有真正的数学都发生在最后一行。

最后一行发生了三件事。Math.sin()函数生成一个频率为freq的正弦波,振幅在正负1之间变化。Math.exp()函数生成一条曲线,该曲线以指数形式增加,其速度由衰减变量decay决定。正弦波乘以振幅变量(amp),结果除以指数曲线的值,得出所需的指数衰减正弦波。

这是一个方便的表达方式。一般情况够用了,但更多的时候,你想用指数衰减的正弦波在另一个运动的末端提供一些振荡的过冲量。诀窍是使过冲振荡的振幅传入速度相匹配。实现这一目标的方式取决于你的动画性质。在某些情况下,你可能有一个动画,其中传入的速度是由表达式本身决定的。例如,假设你想让一个图层在很短的时间内从零扩展到200%,然后过冲一点,在200%的位置稳定下来。我们将对它进行设置,使动画在该层的In Point处触发。下面是一个基本的表达式,它将在十分之一秒内将图层从零扩展到100%,从图层的In Point开始。

t = time - inPoint;
startVal = [0,0];
endVal = [200,200];
dur = 0.1;
linear(t,0,dur,startVal,endVal);

计算过冲

现在我们将添加一些过冲量。在这种情况下,由于传入的动画是由一个线性()函数生成的,我们可以很容易地计算出进入过冲的速度。结果发现,速度只是终点值减去起点值,再除以持续时间,在这种情况下就是(endVal - startVal)/dur。因此,我们需要修改扩展表达式,使其上升到endVal,然后切换到超调振荡。最后的表达式看起来像这样。

freq = 3;
decay = 5;
t = time - inPoint;
startVal = [0,0];
endVal = [200,200];
dur = 0.1;
if (t < dur){
  linear(t,0,dur,startVal,endVal);
}else{
  amp = (endVal - startVal)/dur;
  w = freq*Math.PI*2;
  endVal + amp*(Math.sin((t-dur)*w)/Math.exp(decay*(t-dur))/w);
}

过冲动画与传入动画的速度相匹配。

使两个动画的速度匹配的诀窍是神秘的变量w。这里,w代表振荡的角速度。在不深入了解的情况下,事实证明,用传入的速度除以振荡的角速度可以得到一个完全匹配的过冲量。在实践中,这意味着振荡频率越高,产生的过冲幅度就越小。对于过冲,需要记住的一件事是,你不能直接控制过冲的幅度(安培变量是由表达式计算的)。如果你想要一个更大的过冲,你需要增加传入速度,或者降低振荡频率。值得玩一玩这个表达式,看看频率、传入速度和产生的过冲幅度之间的互动。

这是一个非常有用和通用的表达方式。这里有一个有趣的变化,你可以在一个文本层上使用,让文本字符随机地从0到100%扩展,并带有过冲。为了让它发挥作用,你可以在你的文本层上添加一个Scale Animator,将Scale值设置为百分之零,添加一个Expression Selector,(然后你可以删除Range Selector),最后将Expression Selector的Amount属性的默认表达式替换成这样。

freq = 3;
decay = 5;
maxDelay = 1.0;

seedRandom(textIndex,true);
myDelay = random(maxDelay);
t = time - (inPoint + myDelay);
startVal = [100,100];
endVal = [0,0];
dur = 0.1;
if (t < dur){
  linear(t,0,dur,startVal,endVal);
}else{
  amp = (endVal - startVal)/dur;
  w = freq*Math.PI*2;
  endVal + amp*(Math.sin((t-dur)*w)/Math.exp(decay*(t-dur))/w);
}

你会注意到,这个表达式与前一个版本的唯一真正区别是根据字符的textIndex值计算随机延迟变量(myDelay)的那几行。使用textIndex作为随机种子,可以确保每个字符都能得到一个独特的、随机的延迟。

在这里,过冲表达式已经与随机文本缩放相结合。

这里有另一种变化,你可以用它来让文本层的3D字符以超调的方式依次摆动到视野中。首先,你需要将文本层的锚点移动到文本的顶部。你可以用锚点动画器来做这个。另外,确保你已经启用了每个字符的3D。为旋转添加一个新的动画师(不要使用你用来调整锚点的那个)。设置X旋转的值,使文字被旋转到视野之外(与屏幕垂直)。添加一个表达式选择器并删除范围选择器。用这个替换表达式选择器的Amount属性的默认表达式。

freq = 2;
decay = 5;
delay = .15;
dur = .12;

myDelay = (textIndex-1)*delay;
t = time - (inPoint + myDelay);
startVal = 100;
endVal = 0;

if(t < dur){
  linear(t,0,dur,startVal,endVal);
}else{
  amp = (endVal - startVal)/dur;
  w = freq*Math.PI*2;
  endVal + amp*(Math.sin(t*w)/Math.exp(decay*t)/w);
}

 

关键帧过冲

也许一个更常见的过冲应用是为关键帧动画添加过冲。这实际上是相当直接的,因为表达式语言让你可以访问一个属性的速度。在这种情况下,我们将使用velocityAtTime()函数来获取最近一个关键帧的入射速度。这就是关键帧过冲表达式。

freq = 3;
decay = 5;

n = 0;
if (numKeys > 0){
  n = nearestKey(time).index;
  if (key(n).time > time) n--;
}
if (n > 0){
  t = time - key(n).time;
  amp = velocityAtTime(key(n).time - .001);
  w = freq*Math.PI*2;
  value + amp*(Math.sin(t*w)/Math.exp(decay*t)/w);
}else
  value

和以前一样,前两行只是定义了控制振荡频率和衰减的变量。接下来的部分是一个方便的小程序,它可以找到最近的关键帧。表达式的其余部分提取该属性在该关键帧的速度,并使用该速度作为过冲计算的振幅。注意,这个表达式实际上是在关键帧之前的0.001秒获取速度,以确保它获取的是进入的(而不是离开的)速度。

这个表达式的一个非常好的特点是它与属性无关,这意味着它几乎可以与任何可以用关键帧的属性一起工作。请看两个侧边栏的电影,其中有两个关键帧动画的例子,该表达式提供了过冲量。第一个演示了流行的X旋转过冲,其中各层被关键帧快速从100度旋转到零,表达式提供了过冲动画。第二个演示了应用于关键帧 "位置 "属性的过冲。

过冲表达式被应用到关键帧的X旋转属性上。

这里,过冲表达式被应用到关键帧的位置属性。

弹跳概述

一个二维弹跳模拟,使用发射角度、初始速度、重力、弹性和摩擦力

 

我们在这里的最终目的是得到一个表达式,它将模拟关键帧运动结束时的反弹。为了理解弹跳模拟是如何工作的,我们可以从一个可能比较熟悉的场景开始--当弹丸击中地面/地板的时候会弹跳。为了简单起见,我们将其限制在两个维度上。当你发射一个二维弹丸时,有许多因素在起作用:重力、物体的弹性、发射角度、初始速度,有时还有摩擦。模拟这种运动的表达式基本上必须将初始速度分成x和y两个部分。重力在Y方向上起作用。在每次反弹时,物体会根据弹性失去Y方向的速度,根据摩擦力失去X方向的速度。考虑到所有这些因素,你可以得到一个二维弹跳的表达式,看起来像这样。

freq = 3;
decay = 5;

n = 0;
if (numKeys > 0){
  n = nearestKey(time).index;
  if (key(n).time > time) n--;
}
if (n > 0){
  t = time - key(n).time;
  amp = velocityAtTime(key(n).time - .001);
  w = freq*Math.PI*2;
  value + amp*(Math.sin(t*w)/Math.exp(decay*t)/w);
}else
  value

在这个表达式中,有几件事需要注意。弹跳参数--发射角(elev)、初始速度(v)、弹性(e)、摩擦力(f)和重力(g)--都在表达式的顶部定义。请注意,你还必须定义最大弹跳次数(nMax),以防止表达式消失在越来越小的弹跳的无尽计算中。这个版本的表达式还包括一个变量来控制发射时间(tLaunch)。

一个二维弹跳模拟,使用发射角度、初始速度、重力、弹性和摩擦力。

关键帧弹回

应用于位置、旋转和缩放属性的回弹表达式

现在我们来看看这个表达式,它才是本节的真正重点。这个表达式使用一个属性进入关键帧的速度来计算一个反弹(在与进入的动画相反的方向),并有一系列的递减反弹。这个表达式使用了基本的二维弹跳表达式的大部分逻辑,除了不需要担心发射角度和初始速度(这些都是从关键帧中获取的),也不需要担心摩擦。这个表达式已经被设计成可以与大多数属性一起使用。请看边栏电影中应用于缩放、位置(2D和3D)和旋转属性的例子。这里是表达式。

e = .7;
g = 5000;
nMax = 9;

n = 0;
if (numKeys > 0){
  n = nearestKey(time).index;
  if (key(n).time > time) n--;
}
if (n > 0){
  t = time - key(n).time;
  v = -velocityAtTime(key(n).time - .001)*e;
  vl = length(v);
  if (value instanceof Array){
    vu = (vl > 0) ? normalize(v) : [0,0,0];
  }else{
    vu = (v < 0) ? -1 : 1;
  }
  tCur = 0;
  segDur = 2*vl/g;
  tNext = segDur;
  nb = 1; // number of bounces
  while (tNext < t && nb <= nMax){
    vl *= e;
    segDur *= e;
    tCur = tNext;
    tNext += segDur;
    nb++
  }
  if(nb <= nMax){
    delta = t - tCur;
    value +  vu*delta*(vl - g*delta/2);
  }else{
    value
  }
}else
  value

和以前一样,您可以通过调整弹性 ( e) 和重力 ( g) 变量来控制弹跳特性。

进一步探索

在在这篇文章中,你已经看到了过冲和弹跳模拟的区别。希望这能帮助你为你自己的动画选择合适的模拟。你也看到了如何确保你的过冲模拟与你的传入动画相匹配。

评论(1)