效果图
前言
这是一对针对上臂和下臂的旋转表达式,你可以通过它们在 After Effects 中实现反向运动学(IK)。这个代码是基于 Brian Maffitt 为他的 “Total Training for After Effects” DVD 系列开发的表达式。本文代码在其基础之上进行了一些修改,允许从对象的边缘插入锚点。
正文
IK 允许你根据链条末端物体的运动来定位链条上的其他物体。
例如,移动一只手会导致上臂和下臂的关节旋转,以适应手的运动。
上臂旋转的代码
A = toWorld(anchorPoint); ///< 上臂
L = thisComp.layer("lowerArm");
B = L.toWorld(L.anchorPoint);
L = thisComp.layer("Null 1");
C = L.toWorld(L.anchorPoint);
L = thisComp.layer("hand");
H = L.toWorld(L.anchorPoint);
a = length(A,B);
b = length(B,C);
c = length(H,A);
x = (a*a - b*b + c*c)/(2*c); ///< 余弦定理
beta = Math.acos(clamp(x/a,-1,1));
D = H - A;
delta = Math.atan2(D[1],D[0]);
radiansToDegrees(delta - beta);
下面是逐行解析:
-
A = toWorld(anchorPoint);
:将当前层的锚点位置转换到 世界坐标系 中,并将其赋值给变量A
。这里通常指的是上臂的锚点位置。 -
L = thisComp.layer("lowerArm");
:获取名为 "==lowerArm==" 的图层。 -
B = L.toWorld(L.anchorPoint);
:获取 "==lowerArm==" 图层的锚点位置,并将其转换到 世界坐标系 中,赋值给B
。 -
接下来的几行重复了类似的操作,分别获取名为 "==Null 1==" 的空对象(作为效应器)和 "==hand=="(手)图层的 世界坐标系 下的锚点位置(==人为设置二者相同,这样调节更加直观,当然也可以不同==),并赋值给
C
和H
。 -
a = length(A,B);
:计算点A
和B
之间的距离,即上臂与下臂之间的距离,并赋值给a
。 -
b = length(B,C);
:计算点B
和C
之间的距离,这里是下臂与空对象(即手)的距离,赋值给b
。 -
c = length(H,A);
:计算目标位置H
(手)与起始位置A
(上臂)之间的距离,赋值给c
。 -
x = (a*a - b*b + c*c)/(2*c);
:应用 余弦定理 公式的一部分,计算用于后续求角度的中间变量x
。 -
beta = Math.acos(clamp(x/a,-1,1));
:计算角β
,它是基于之前计算的x
值的反余弦值。clamp
函数确保了传入Math.acos
的值在 -1 到 1 之间,避免数学域错误。 -
D = H - A;
:计算向量H
,即从上臂锚点A
指向手部H
的向量。 -
delta = Math.atan2(D[1],D[0]);
:使用atan2
函数计算这个向量与 ==x 轴正方向== 之间的角度δ
,提供完整的角度测量,包括负值,适用于所有象限。 -
radiansToDegrees(delta - beta);
:将角度差(δ-β
)从弧度转换为度数。这代表了 ==为了使手部达到目标位置,上臂需要旋转的角度调整量==。
综上所述,这段代码主要计算了 ==为了使 “手” 达到目标位置,上臂应该旋转的角度==,这是通过逆运动学原理实现的,涉及到空间几何和三角函数的计算。
下臂旋转的代码
B = toWorld(anchorPoint); ///< 下臂
L = thisComp.layer("upperArm");
A = L.toWorld(L.anchorPoint);
L = thisComp.layer("Null 1");
C = L.toWorld(L.anchorPoint);
L = thisComp.layer("hand");
H = L.toWorld(L.anchorPoint);
a = length(A, B);
b = length(B, C);
c = length(H, A);
x = (a * a - b * b + c * c) / (2 * c);
y = c - x;
alpha = Math.acos(clamp(y / b, -1, 1));
beta = Math.acos(clamp(x / a, -1, 1));
radiansToDegrees(alpha + beta);
关于上臂和下臂的旋转计算差异,主要是由于它们在逆运动学(IK)链路中的相对位置和作用不同导致的。
在 上臂部分,计算的目的是 ==确定上臂如何旋转以使手部达到目标位置。==
delta
计算的是从上臂到手的向量与 x 轴正方向的夹角,- 而
beta
是基于余弦定理计算的角,代表了理想状态下 上臂AB
与手部AH
应该形成的角。
因此,为了使手部到达目标,上臂需要逆着这个理想角度旋转,即delta - beta
,意味着上臂要回旋回到使得手部能够沿正确的方向移动。
至于 下臂部分,计算逻辑稍有变化。alpha
和 beta
依然通过余弦定理计算得到,==但它们代表的意义不同==。
在这里
alpha
通常关联于下臂BC
与上臂到手端AC
(或效应器AH
)的向量所形成的角,- 而
beta
关联于上臂AB
和手部AH
的旋转。因此,为了达到理想的手部位置,下臂需要沿着正确的方向旋转,即alpha + beta
,意味着 ==下臂顺着上臂的旋转趋势进一步拉近于目标位置==。
总结来说,delta - beta
和 alpha + beta
的差异反映了上臂和下臂在逆运动学链路中所处位置的不同角色以及它们如何影响手部达到目标位置所需的旋转调整
这背后的数学运算有点复杂,但如果你知道三角函数,那么搞明白它背后的原理将是很有趣的。
alpha
和beta
角如下
设置的方法
- 首先,为你的 IK 链导入部件(注意,如果你需要缩放部件,你应该先在另一个应用程序中这样做,因为如果在你的合成中这么做的话,将会破坏角度计算)。
还要 ==注意==,这些表达式会假设你的部件一开始是 水平 的。 如果它们开始是 垂直 的,你需要调整 ==上臂== 表达式(从结果中减去 90 度),即将最后一行改为:
radiansToDegrees(delta - beta) - 90;
-
接下来你需要将锚点移动到关节(==将上臂的锚点移动到肩部,将下臂移动到肘部,将手移动到手腕==)。然后,你把这些部件排列成链条。
-
然后创建一个空层(在本例中为 “
Null 1
” ),并将其定位,使其锚点(Anchor Point
)的位置都与手的锚点位置相同。
注意,需要在设置父级前对其它们。不是直接复制 Position,而是视觉上对齐
- 现在让上臂(可选:上臂的父级是身体)成为下臂的父级,下臂成为 Null 的父级。
注意,==手 是没有父级的==。
- 接着再应用旋转表达式(如果提前应用会改变上臂和下臂的位置,影响我们调整步骤)。
如果你已经做了所有正确的操作,你应该能够在合成中来回移动手,同时手臂将跟随其运动。
细心的朋友可能发现上图的动作非常反人类
为了得到 ==镜像== 的解决方案(即手臂部分向另一个方向弯曲),你可以改变 上臂 表达式的最后一行:
radiansToDegrees(delta + beta);
并将 下臂 表达式的最后一行改为:
-radiansToDegrees(alpha + beta);
这样效果看起来就正常多了 !(不考虑手的自动转向的话)