AEJoy —— 表达式之反向动力学(Inverse Kinematics)【五】【JS】

212 阅读5分钟

本文是基于前文 AEJoy —— 表达式之反向动力学(Inverse Kinematics)【四】【JS】 并结合我实际的使用进行补充的,所以建议先了解一下前文

  • 前文的素材层级关系

  • 我的素材层级关系

image.png

  • IK 原理图

请看这篇文章 AEJoy —— Ae 表达式之手臂的反向动力学(Inverse Kinematics)【JS】

效果图

  • 实际使用的效果(无 Leveling

    在这里插入图片描述

  • 实际使用的效果(有 Leveling

    在这里插入图片描述

表达式代码详解

以下代码是用来计算逆运动学(IK)中关节旋转角度的表达式代码,既适用于上肢也适用于下肢的运动。

代码首先定义了一些布尔变量和层的名称(根据自己的素材进行修改),接着定义了辅助函数和计算过程。

cw = false; ///< true:顺时针 false: 逆时针
upper = true; ///< true: 上肢 false:下肢
/// @note 三个(上肢、下肢、肢端)层名
upperLimb = "l upper arm";
lowerLimb = "l lower arm";
extremity = "l hand"; 
/// @note 效应器的层名(引导层,解决肢端从下肢移得太远而脱离的问题)
effector = "l hand effector";

/// @note 返回世界坐标系下的层的位置
/// 这么做的好处:以防该层被作为其他层的子级
function getWorldPos(theLayerName) {
    L = thisComp.layer(theLayerName);
    return L.toWorld(L.anchorPoint);
}

/// @note 对应上图三角形各顶点的位置
A = getWorldPos(upperLimb);
B = getWorldPos(lowerLimb);
C = getWorldPos(extremity);
E = getWorldPos(effector); ///< 与 C 的位置相同

/// @note 上图三角形的边长
a = length(B, C);
b = length(E, A);
c = length(A, B);

x = (b * b + c * c - a * a) / (2 * b); ///< 利用余弦定理
alpha = Math.acos(clamp(x / c, -1, 1)); ///< 计算 alpha 角

if (upper) { ///< 应用于上肢部分
    D = E - A;
    delta = Math.atan2(D[1], D[0]);
    result = radiansToDegrees(delta - (cw ? 1 : -1) * alpha); ///< 计算该关节的旋转 (1. 如果没有 IK 效果,则上肢会直接转到 D 的方向,即 delta 弧度; 2. 因为有 IK 效果,所以再转过 alpha 弧度)
    V = B - A;
    adj1 = radiansToDegrees(Math.atan2(V[1], V[0]));
    result - adj1 + value; ///< 考虑到初始旋转方向,进行必要的调整
} else { ///< 应用于下肢部分
    y = b - x;
    gamma = Math.acos(clamp(y / a, -1, 1)); ///< 计算 gamma 角
    result = (cw ? 1 : -1) * radiansToDegrees(gamma + alpha); ///< 计算该关节的旋转
    V1 = B - A;
    adj1 = radiansToDegrees(Math.atan2(V1[1], V1[0]));
    V2 = C - B;
    adj2 = radiansToDegrees(Math.atan2(V2[1], V2[0]));
    result + adj1 - adj2 + value; ///< 考虑到初始旋转方向,进行必要的调整
}

以下是详细的解析:

变量定义

  • cw: 一个布尔值,表示 旋转的方向偏好true 表示顺时针,false 表示逆时针。
  • upper: 布尔值,指示 是处理上肢 (true) 还是下肢(false)。
  • upperLimblowerLimbextremityeffector: 分别定义了 上肢下肢肢端(例如手或脚)以及 效应器(目标位置)的层名称。

辅助函数

  • getWorldPos(theLayerName): 该函数接收一个层名称作为参数,然后找到该层并返回其在 ==世界坐标系== 下的锚点位置。这样做是因为图层可能作为其他图层的子级存在,直接获取世界坐标可以避免坐标变换带来的问题。

主体计算

  1. 获取顶点坐标:通过调用 getWorldPos 函数,获取上肢、下肢、肢端和效应器的锚点在 ==世界坐标系== 下的位置,分别存储在变量ABCE中。

  2. 计算边长:利用length函数计算三角形 ABC(上肢、下肢、肢端组成)的三边长度,存储在变量abc中。

  3. 应用余弦定理计算角度

    • 计算变量 x,它是根据 ==余弦定理== 用于计算角度 α 的一个中间变量。
    • 使用 Math.acos 和 clamp 函数计算角度 α,确保结果在有效范围内。
  4. 分情况讨论

    • 上肢部分

      • 计算向量 D,并利用 Math.atan2 得到旋转角度的初始值 delta
      • 根据 cw 的值调整旋转方向后,减去初始角度调整量 adj1,再加上一个外部输入的 value 来考虑初始旋转状态,最终得到旋转角度 result
    • 下肢部分

      • 计算另一个中间变量 y,并使用余弦定理计算角度 γ 。
      • 同样根据 cw 调整旋转方向,但这次是将 γ 和 α 相加。
      • 对于下肢,还需考虑两个调整量 adj1 和 adj2,以及外部输入的 value,来进行最终的旋转角度调整。

[!note]

在代码二中,加入 adj1 和 adj2 是为了 ==更精确地调整最终的关节旋转角度,以考虑初始关节的初始旋转状态或特定的朝向==。具体来说:

  • adj1 是针对上肢(上肢部分或下肢)计算的调整,它基于从关节的起始位置到上臂(AB)的向量(即 V1)与 x 轴的夹角。这有助于确定了关节的起始姿态,调整旋转以匹配动画的连贯性。
  • 而 adj2 是针对 下肢部分 的特别调整,计算,它是基于从下肢到下臂(BC)的向量(即 V2)与 x 轴的夹角。这部分调整意在处理下肢的旋转时,确保了与上肢的运动逻辑一致,即从关节的起始姿态考虑,这样旋转后达到整体关节链的连续和自然。

简言之,adj1 和 adj2 都是为了保证了关节旋转计算在逆运动学中的旋转角度时,考虑到了关节的初始旋转状态,这样确保了动画的流畅和关节链的连贯性,防止了突然的旋转突变或不自然的动作。

总结

这段代码通过分析肢体(上肢或下肢)的结构,利用逆运动学原理,计算出为了让肢体的末端(如手或脚)达到指定效应器位置所需的关节旋转角度。它综合运用了向量运算、三角函数以及 余弦定理 等数学工具,同时考虑了初始关节朝向和旋转方向偏好,以实现自然且准确的运动模拟。