AEJoy —— Ae 表达式之手臂的反向动力学(Inverse Kinematics)【JS】

125 阅读6分钟

效果图

在这里插入图片描述

前言

这是一对针对上臂和下臂的旋转表达式,你可以通过它们在 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);

下面是逐行解析:

  1. A = toWorld(anchorPoint);:将当前层的锚点位置转换到 世界坐标系 中,并将其赋值给变量 A。这里通常指的是上臂的锚点位置。

  2. L = thisComp.layer("lowerArm");:获取名为 "==lowerArm==" 的图层。

  3. B = L.toWorld(L.anchorPoint);:获取 "==lowerArm==" 图层的锚点位置,并将其转换到 世界坐标系 中,赋值给 B

  4. 接下来的几行重复了类似的操作,分别获取名为 "==Null 1==" 的空对象(作为效应器)和 "==hand=="(手)图层的 世界坐标系 下的锚点位置(==人为设置二者相同,这样调节更加直观,当然也可以不同==),并赋值给 CH

  5. a = length(A,B);:计算点 AB 之间的距离,即上臂与下臂之间的距离,并赋值给 a

  6. b = length(B,C);:计算点 BC 之间的距离,这里是下臂与空对象(即手)的距离,赋值给 b

  7. c = length(H,A);:计算目标位置 H(手)与起始位置 A(上臂)之间的距离,赋值给 c

  8. x = (a*a - b*b + c*c)/(2*c);:应用 余弦定理 公式的一部分,计算用于后续求角度的中间变量 x

  9. beta = Math.acos(clamp(x/a,-1,1));:计算角 β,它是基于之前计算的 x 值的反余弦值。clamp 函数确保了传入Math.acos 的值在 -1 到 1 之间,避免数学域错误。

  10. D = H - A;:计算向量 H,即从上臂锚点 A 指向手部 H 的向量。

  11. delta = Math.atan2(D[1],D[0]);:使用 atan2 函数计算这个向量与 ==x 轴正方向== 之间的角度 δ,提供完整的角度测量,包括负值,适用于所有象限。

  12. 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,意味着上臂要回旋回到使得手部能够沿正确的方向移动。

至于 下臂部分,计算逻辑稍有变化。alphabeta 依然通过余弦定理计算得到,==但它们代表的意义不同==

在这里

  • alpha 通常关联于下臂 BC与上臂到手端 AC(或效应器 AH)的向量所形成的角,
  • beta 关联于上臂 AB 和手部 AH 的旋转。因此,为了达到理想的手部位置,下臂需要沿着正确的方向旋转,即 alpha + beta,意味着 ==下臂顺着上臂的旋转趋势进一步拉近于目标位置==

总结来说,delta - betaalpha + beta 的差异反映了上臂和下臂在逆运动学链路中所处位置的不同角色以及它们如何影响手部达到目标位置所需的旋转调整

这背后的数学运算有点复杂,但如果你知道三角函数,那么搞明白它背后的原理将是很有趣的。

  • alphabeta 角如下 在这里插入图片描述

设置的方法

  1. 首先,为你的 IK 链导入部件(注意,如果你需要缩放部件,你应该先在另一个应用程序中这样做,因为如果在你的合成中这么做的话,将会破坏角度计算)。

还要 ==注意==,这些表达式会假设你的部件一开始是 水平 的。 如果它们开始是 垂直 的,你需要调整 ==上臂== 表达式(从结果中减去 90 度),即将最后一行改为:

radiansToDegrees(delta - beta) - 90;
  1. 接下来你需要将锚点移动到关节(==将上臂的锚点移动到肩部,将下臂移动到肘部,将手移动到手腕==)。然后,你把这些部件排列成链条。 在这里插入图片描述

  2. 然后创建一个空层(在本例中为 “Null 1” ),并将其定位,使其锚点(Anchor Point)的位置都与手的锚点位置相同。 在这里插入图片描述

注意,需要在设置父级前对其它们。不是直接复制 Position,而是视觉上对齐

  1. 现在让上臂(可选:上臂的父级是身体)成为下臂的父级,下臂成为 Null 的父级。

注意,==手 是没有父级的==。 在这里插入图片描述

  1. 接着再应用旋转表达式(如果提前应用会改变上臂和下臂的位置,影响我们调整步骤)。

如果你已经做了所有正确的操作,你应该能够在合成中来回移动手,同时手臂将跟随其运动。

在这里插入图片描述

细心的朋友可能发现上图的动作非常反人类

为了得到 ==镜像== 的解决方案(即手臂部分向另一个方向弯曲),你可以改变 上臂 表达式的最后一行:

radiansToDegrees(delta + beta);

并将 下臂 表达式的最后一行改为:

-radiansToDegrees(alpha + beta);

这样效果看起来就正常多了 !(不考虑手的自动转向的话)

在这里插入图片描述

参考链接