接上回, 网上广为流传的ae万能弹性表达式, 在完全不理解的情况下, 其实不一定生效.

本文会通过解释表达式, 来分析表达式的效果, 什么情况下使用表达式.

更深一步可以知道如何修改表达式来应用到本不可以用到的场景.

表达式解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const amp = 0.1;
const freq = 2.0;
const decay = 2.0;

let n = 0;
if (numKeys > 0) {
n = nearestKey(time).index;
if (key(n).time > time) { n--; }
}
if (n == 0) { var t = 0; } else { var t = time - key(n).time; }

if (n > 0) {
let v = velocityAtTime(key(n).time - thisComp.frameDuration / 10);
value + v * amp * Math.sin(freq * t * 2 * Math.PI) / Math.exp(decay * t);
} else {
value
}

表达式先贴出来, 接下来我们会逐行读代码分析意义并总结使用效果和场景.

代码的运行环境和意义要去看上篇文章.

概述

这是一个与阻尼振动的s-t函数结合的ae表达式, 阻尼振动的场景用在表情角色的身体抖动上.

身体进行了一定加速后突然停止, 皮肤表面的振动就很接近阻尼振动.

另外荡秋千/钟摆的物品形成的接近简谐运动就是阻尼振动, 阻尼为0时的函数.

首先我们要明确简谐运动的s-t函数是个余弦函数, 阻尼振动是在此基础上变化, 所以会有余弦函数的一些变量.

变量介绍

表达式里有一些变量, 我们要明白他们的意义:

  • amp(amplitude): 振幅系数. amp本身是振幅, 但在这个表达式里解释为振幅系数, 之后会细说.
  • freq(frequence): 频率.
  • decay: 减速系数.

上面这些是与正弦函数相关的参数, 还有一些表达式里使用的变量, 在看变量前要先理解表达式做了什么:

  1. 假设一个动效, 人物的脸从a移动到了b.
  2. 那么b点人物制动以后, 脸上的肉会因为惯性开始振动. (受力分析和方向先不讨论)
  3. 那么我们把b点(运动结束的kf)称作”起振点”.
  4. 无论b点以后是否有后续kf, 表达式会把”以b点初速度方向的阻尼位移”作为后续属性值的增量. (如果没有后续kf, 表现就是在b点的属性值上做加减.)

我们以刚才的”起振点”概念, 来继续理解其他3个变量:

  • n: 起振点的kf索引. 即”起振点是第几个关键帧”.
  • t: 以起振点作为原点, t轴的值.
  • v: 起振点初速度.

读代码

第一部分: 正弦函数参数的变量设置, 使用者可以调整这些变量来修改效果.

第二部分: 把上一个kf作为”起振点”, 算出n和t的值.

第三部分: 把起振点初速度 * 振幅系数作为振幅, 代入阻尼振动的s-t函数. 作为增量加到属性值上.

那么为什么要把起振点初速度 * 振幅系数作为振幅呢, 这点我觉得值得深入讨论一下.

初速度的讨论

这个初速度需要深入说两点:

  • 初速度是个矢量, 其实s-t图的s也是个矢量, 所以公式里光乘以振幅系数是得不到矢量的.
  • 如果b点后面还有kf, 那么b点是不可导的, 这个速度其实是s关于t在b点的左导数.

说到这里, 没有理解ae表达式执行环境的读者已经坐不住了.

这里的value根据不同属性, 可以是值, 也可以是数组. (表达式最后的value也比较特殊, 还是得去前一篇文章看执行环境的介绍)

当value是一个数组(比如表达式是关于位置的情况下), +操作符的执行其实还附带map()的效果.

velocityAtTime()的返回结果是个数组(矢量), value的+也会获取v的对应维度来进行加减.

如此这样, 位置属性被有方向地加上了 由t经过函数得到的s 的增量 了.

表达式效果总结和使用场景

由上面的分析可以知道, 这个弹性公式是适用于不同属性的. 在二维属性上, 还是支持矢量增量的.

除了位置, 弹性公式还可以应用到缩放上. (其他属性还不知道, 因为我电脑没有ae)

表达式功能总结: 根据每个关键帧的初速度, 给后续帧加上阻尼振动公式产生的增量. 并且有以下特点:

  • 如果只有2个kf, 效果是第二个kf后属性值会正负振动, 减缓到0.
  • 如果有多个kf, 第二个后的每个kf都会振动, 这个振动会叠加到属性值的变化上.
  • 如果有多个kf, 在振动还未减缓到0前就进入了下一个kf, 这个振动不会带入下一个kf.

这个公式应该还可以用到角度, 透明度, 缩放, 锚点上. 颜色行不行我还不知道.

在理解了这个公式的含义后, 进行操作, 应该可以做到路径/颜色的弹性, 也可以做到确定某个kf后做弹性.

阻尼函数简单解释

那么还剩一个问题, 阻尼函数是怎么得到的? 公式的正确性是很重要的, 公式不对动效就会不自然, 虽然用户说不上哪里有问题, 但用户会说”感觉不生动”.

要推出物体的s-t函数, 就需要一定程度的物理知识和数学知识, 我对数学知识忘记得比较多, 于是看了<<普林斯顿微积分读本>>来复习并记录一下, 这里微分和积分的概念引入都会用运动物体s-t变化为例讨论.

微分

有一个物体在运动, 路程-时间 (s - t) 的关系是: $$s = f(t)$$. (如果全程能脑子里想着图像会更好理解)

现在我们想求一个 瞬时速度-路程 (v - t) 的函数. 于是在t轴上取一小段时间, 称作 d.

来算一下 v - t 关系: $$v_{瞬}=\frac{f(t + d) - f(t)}{d}$$. 这个式子中, d越小, 瞬时速度就越精确.

当d趋向于无穷小的时候, 这个(v - t)函数就是精确的了: $$v=\lim_{d \to 0}\frac{f(t + d) - f(t)}{d}$$.

而每次都写limit很麻烦, 我们用$$\mathrm{d} s$$和$$\mathrm{d} t$$来代表 d 趋向于0的时候, s 和 t 的变化.

上面的式子有了个等价的写法: $$v=\frac{\mathrm{d} s}{\mathrm{d} t}$$. 意思是速度等于在很小的时间段里, 路程和时间的比值.


故事说完了, 接下来我们总结一下并提炼一些概念.

  • 上面这种关于原函数 $$f(t)$$ 的”斜率函数”, 称作 $$f(t)$$ 的导函数, 记作 f’(t).
  • 导数的计算方法是, 取一段 $$f(t)$$ 的变化, 除以 $$t$$ 的变化, 计算变化值趋向于0的极限.

所以, 基于 $$s = f’(t)$$, 以下几个概念是等价的:

$$v=f’(t)=\lim_{d \to 0}\frac{f(t + d) - f(t)}{d}=\frac{\mathrm{d} s}{\mathrm{d} t}$$.

描述为: v是s关于t的导数.


我们会发现, 速度也是会随着时间变化而变化的, 这种变化用导数表示也很容易, 只要把$$\frac{\mathrm{d} s}{\mathrm{d} t}$$当做之前的$$s$$处理.

学过物理的我们已经知道这正是加速度, 这种把导数关于$$t$$再次求导数的结果成为二次导数, 记作$$f’’(t)$$:

$$a=f’’(t)=\frac{\mathrm{d}\frac{\mathrm{d}s}{\mathrm{d}t}}{\mathrm{d}t}$$. 这个式子其实是没法简化的, 于是我们把他规定写成: $$\frac{\mathrm{d}^2s}{\mathrm{d}t^2}$$.

描述为: a是s关于t的二阶导数.

导数是可以多阶的, 可以记作: $$f^{(n)}(t)=\frac{\mathrm{d}^ns}{\mathrm{d}t^n}$$.


导数的求法有2种, 一种是根据定义代入求极限, 另外一种是根据定义得出的一些运算法则, 基于第一种的结果求导.

积分

积分的主角变成了 $$v=g(t)$$. 在知道 (v - t) 函数的前提下, 我们想求 t 从 a 到 b, 物体经过的路程.

如果速度是个常数, 保持不变的, 我们是知道 $$s=v*t$$的, 但在别的任何情况下, 我们就要进一步思考了.

于是我们的思路是: 把面积切成一个个长条, 计算长条的面积和.

在长条的宽无限小, 数量无限大的情况下, 长条的面积是个长方形了, 而面积的和就等于要求的面积. (非常不严谨, 要用夹逼定理)

并且我们(并不是)又发明了一个新符号来表示积分: $$\int_{a}^{b}f(t)\mathrm{d}t=\lim_{n \to \infty} \sum_{j = 1}^{n}g(c_{j})(t_{j}-t_{j - 1}) $$.

其中, $$c_{j}$$代表在$$[t_{j}, t_{j - 1}]$$区间里的时间, 并且 $$a < t_{0} < … < t_{n} < b$$.

这个式子挺复杂的, 还需要下面的话描述, 才是完整的式子.


和微分一样, 第一种求积分的方法, 就是利用定义求积分了, 定义里无限方块的面积和叫做黎曼和.

使用这种方法求积分相当麻烦, 需要一边代入, 一边思考式子的意义, 再采取正确的简化.


还有一个别的求积分的方法, 需要用到微积分的2个基本定理.

第一条: 图形面积的变化, 其实就等于围面积那条函数.

我们用之前的例子举例. 路程s随着时间t的变化, 其实就是速度, 就是函数v.

假设 $$s=f(t), v=g(t)$$ , 那么 $$s’=f’(t)=g(t)=v$$ .


我们再来观察$$s=f(t)$$, 通过时间t求面积的时候, 自变量实际意义上是积分的上限, 而长方形的面积的表示可以换一个字母来区别于自变量t了: $$s=f(t)=\int_{a}^{t}g(x)\mathrm{d}x $$.


第二条定理是: 一个函数从 a 到 b 的定积分, 等于 反导数(b) - 反导数(a).

用图像来看就是 0~b 的面积 减去 0~a 的面积, 就是 a~b 的面积了, 比较好理解.


其实不写积分的上下限也可以表示”函数的反函数”, 不确定积分的上下限, 只会差一个常数C.

这种形式叫做不定积分, 我们用这个形式来表示”函数的反函数集合”.

即: $$\int g(t)=f(t)+ C$$, 可以用这个理解下: $$\int v = s + C$$

微分方程

微分方程是方程的一种, 所以方程是干嘛的, 他就是干嘛的, 只是有微积分的符号.

那微积分符号是用来干嘛的? 用来把一个物理量拆解成基础量, 减少变量的.

相对地, 微分方程会有一些被推出来的定理和运算规则, 偶尔能解决一些问题, 但总的来说, 微分方程比较难解, 符号不容易化开.

下面开始阻尼的公式分析, 就会对微分方程有所体会了.

阻尼振动分析

理想的阻尼振动受力分析很简单, 弹簧的力和阻力. 设位移是x.

弹簧的力和位移有关: $$F=-kx$$. 阻力和速度相关: $$f_{阻}=-\gamma v_{x}$$. 这2个式子和牛二联立:

$$F=ma=-kx-\gamma v$$ . $$m\frac{\mathrm{d}^2x}{\mathrm{d}t^2}=-kx-\gamma \frac{\mathrm{d}x}{\mathrm{d}t}$$.

怎么解方程好复杂不想看了, 方程的解是 $$x=Ae^{-\beta t}cos(\omega t+\varphi) $$.

如果能看到这里, 你肯定发现这个解和ae表达式里的不同.

表达式里的公式是: value += $$vAe^{-\beta t}sin(2\pi f t)$$. 有三处不同, 稍作解释:

  1. A乘了一个v, 上面有提到, 原函数的x是忽略方向的, 需要v来提供表达式的位移方向.
  2. $$\omega$$ 是角频率, 所以要把我们设置的f乘一个$$2\pi$$.
  3. 原函数是余弦函数, $$t=0$$处的值是A. 我们令$$\varphi = \frac{\pi}{2}$$, 余弦就变正弦了.