正向运动学从关节角度计算末端位置;反向运动学(Inverse Kinematics, IK)则从目标位置反求关节角度。拖动角色的手、脚或武器时,动画软件背后执行的就是这个反问题。对二维上臂和前臂组成的两段链,不必使用迭代优化,余弦定理可以给出稳定的解析解。
一、目标是否可达
设两段骨骼长度分别为 a 和 b,肩部为 S,目标为 T,距离 d = ||T - S||。目标可达的充要条件是:
|a - b| <= d <= a + b
当 d > a + b 时,手臂只能完全伸直指向目标;当 d < |a-b| 时,目标落在链条不能折回到达的内圆内。工程实现通常把 d 截断到这个闭区间,而不是让 acos 接收超出 [-1, 1] 的数值。
二、用余弦定理求肘角
肩、肘、目标构成三角形。肘部内角 gamma 满足:
cos(gamma) = (a^2 + b^2 - d^2) / (2ab)
gamma = acos(clamp(cos(gamma), -1, 1))
骨骼系统使用的“前臂相对上臂旋转”通常是 pi - gamma,具体正负号取决于屏幕坐标 y 轴方向和肘部朝向。
三、求肩角与肘部翻转
目标方向为 phi = atan2(Ty-Sy, Tx-Sx)。肩部三角形夹角 alpha 为:
alpha = acos((a^2 + d^2 - b^2) / (2ad))
shoulder = phi +/- alpha
+/- 对应同一个目标的两组镜像解,也就是“肘朝上”和“肘朝下”。动画系统必须保存 pole direction 或 elbow sign,否则目标跨过骨骼直线时,肘部可能突然翻面。
四、数值例子
令 a=150、b=125、目标距离 d=200:
cos(gamma) = (150^2 + 125^2 - 200^2) / (2*150*125)
= -0.05
gamma = acos(-0.05) = 92.87°
cos(alpha) = (150^2 + 200^2 - 125^2) / (2*150*200)
= 0.78125
alpha = 38.62°
只要再加上目标方向 phi,肩角和肘坐标就能直接得到。下面可以拖动蓝色目标点,并改变两段骨长。把目标拖到可达圆之外,观察解如何被截断到最大伸展位置。
几何 · IK
双骨骼反向运动学
用余弦定理求肩肘角度,并处理不可达目标和肘部翻转。
拖动蓝色目标点
五、解析 IK 的边界处理
- 浮点误差:进入
acos前必须把余弦值限制到[-1, 1]。 - 接近完全伸直:两组解趋于重合,肘部方向不稳定,应沿用上一帧符号。
- 目标穿过肩部:
atan2比atan(y/x)更可靠,因为它保留象限信息。 - 关节限制:求出解析解后,再把肩、肘角限制到角色设计允许的范围,并重新计算末端位置。
六、边界用例与预期结果
使用 a=150、b=125 时,可达距离是 [25, 275]。把目标放在边界两侧,可以验证截断、浮点保护和肘部连续性是否同时正确。
| 原始距离 d | 求解距离 | 预期姿态 | 必须检查 |
|---|---|---|---|
| 300 | 275 | 手臂完全伸直指向目标 | 末端不越过外圆,余弦不大于 1 |
| 200 | 200 | 肘内角约 92.87° | 两组镜像解到达同一末端 |
| 25 | 25 | 两段骨骼处于最大折叠边界 | 不因分母很小产生跳变 |
| 0 | 25 | 沿上一帧方向保持稳定折叠 | 避免用零距离计算肩角 |
| 目标跨过骨骼直线 | 保持原距离 | 肘部方向由保存的 sign 决定 | 不得在相邻两帧突然镜像翻转 |
七、一个数值稳定的求解框架
delta = target - shoulder
raw_d = length(delta)
direction = raw_d > eps ? delta / raw_d : previous_direction
d = clamp(raw_d, abs(a - b) + eps, a + b - eps)
cos_elbow = clamp((a*a + b*b - d*d) / (2*a*b), -1, 1)
cos_offset = clamp((a*a + d*d - b*b) / (2*a*d), -1, 1)
phi = atan2(direction.y, direction.x)
shoulder_angle = phi + elbow_sign * acos(cos_offset)
elbow_angle = elbow_sign * (pi - acos(cos_elbow))
eps 不是用来改变可见姿态,而是避免恰好落在奇异点时出现除零或不可导跳变。求解后应重新用正向运动学计算末端,并记录目标误差 ||end-target_clamped||。可达目标的误差应只剩浮点量级;不可达目标的误差应等于被截断的距离,而不是随机漂移。
手臂锁死那一下:软化伸展极限
把 d 截断到可达区间可以保证数学上不出错,但视觉上会留下一个很明显的瑕疵:目标一旦越过最大伸展距离,手臂就瞬间完全伸直并停在那里。角色伸手够远处的东西时,会看到肘部在某一帧「啪」地锁死,之后再远也不动了。真实的肢体不是这样的——它会越来越接近伸直,但永远保留一点余量。
业界的做法叫软化 IK:在接近极限的最后一段距离里,用一个指数函数把输入距离压缩到永远不达上限。设总长 L = a + b,软化区宽度为 s:
if d < L - s:
d_soft = d # 远离极限,原样通过
else:
d_soft = L - s * exp(-(d - (L - s)) / s)
这个函数在 d = L - s 处的值和一阶导数都与直线段相接,所以不会有拐点;d 趋于无穷时 d_soft 渐近于 L,永远取不到。把 d_soft 代替 d 送进余弦定理,肘部就永远保留一个微小的弯曲。
s 通常取总长的 3% 到 8%。取太大会让手臂在正常范围内就够不到目标,看起来像够不着;太小则软化效果不明显,锁死感还在。
同样的处理也适用于内侧边界,只是很少需要——角色很少把手完全折进肩膀里。
目标点属于哪个坐标空间,必须显式声明
前面所有公式都默认目标 T 和肩部 S 在同一个坐标系里。实现时这个坐标系选哪一个,是个会直接决定动作对不对的问题,而它经常被默认掉。
脚踩在地面上时,目标应该在世界空间:角色身体前移,脚要留在原地,直到迈出下一步。这正是脚不打滑的实现方式。
手扶在腰上时,目标应该在角色局部空间:角色走动时手要跟着身体一起走。如果这个目标写成了世界坐标,角色一移动,手就会留在原地被拉长,看起来像被钉在了空中。
手扶在一扇移动的门上时,目标应该在那扇门的空间里——既不是世界也不是角色。
所以 IK 目标不能只存一个坐标,必须同时存它的父空间:
{ "target": [120, 40], "space": "world" } # 落地的脚
{ "target": [18, -30], "space": "character" } # 叉腰的手
{ "target": [4, 12], "space": "door_handle" } # 扶门的手
求解前先把目标变换到肩部所在的空间,再套用前面的公式。这一步在结构上属于 IK 的输入准备,不属于求解本身,所以很容易被写在调用方而各处不一致。
值得注意的是,「脚打滑」这个最常见的 2D 行走瑕疵,绝大多数不是 IK 算错了,而是目标空间选错了。先确认这一点,再去查求解器,能省下大量时间。
加上关节限制之后,手就够不到目标了
前面的检查清单里提到「求出解析解后再把角度限制到允许范围」。这句话轻描淡写,但它其实引入了一个必须显式决策的问题:角度被限制之后,末端就不在目标位置上了。
解析 IK 的解是唯一的(给定肘部朝向),所以一旦你改动了它给出的角度,末端必然偏离。这时候有三种处理方式,它们对应完全不同的表现,没有默认答案:
接受误差。手停在够不到的位置,和目标之间留一段距离。适合目标只是一个引导(比如视线跟随),不适合手要抓住某个具体物体的情况——观众会看到手和杯子分开。
移动根节点。让肩膀(或整个身体)朝目标方向偏移,直到手能够到。这是真实人体的做法——够不到就探身。代价是根节点的移动会影响其它所有部件,需要控制偏移量并且让它平滑,否则身体会跟着手抖。
拉伸骨骼。允许骨长在小范围内变化(通常 ±10% 以内)。这在写实风格里不可接受,但在夸张的 2D 角色上非常常用,而且观众几乎察觉不到——因为手臂本来就有袖子和肌肉的形变作为掩护。
关键不在于选哪个,而在于必须选一个并写进配置。默认行为是第一种(静默接受误差),而它恰好是最容易在成品里被看出来的那一种。
无论选哪种,求解之后都应该记录末端与目标的实际距离。这个值在可达且无限制时应该只剩浮点量级;一旦它稳定地大于零,说明某个限制正在起作用——这时候是调限制、换策略,还是改动画,才是一个有依据的决定。
目标抖一个像素,肘部抖十度
解析 IK 有一个不那么直观的性质:它在极限附近会放大输入噪声。目标点抖动一个像素,求出来的关节角可能抖动十几度。理解这一点需要看 acos 的导数。
d/dx acos(x) = -1 / sqrt(1 - x^2)
当 x 趋近 ±1 时,这个导数发散到无穷。而 x 正是余弦定理算出来的那个比值——手臂接近完全伸直时它趋近 1,接近完全折叠时趋近 -1。也就是说,在两个边界附近,输入的微小变化被放大成角度的巨大变化。
这解释了几个看起来不相干的现象:手臂快伸直时肘部会神经质地颤动;用鼠标拖动目标点,在可达圆边缘附近手感突然变得「滑」;同一段动画在不同机器上因为浮点末位不同而产生可见差异。它们都是同一个放大效应。
上一节的软化 IK 顺带缓解了外侧边界的这个问题——因为 d_soft 永远取不到 L,那个比值就永远到不了 1。这是软化除了观感之外的第二个好处,也是它比简单截断更值得用的原因:截断把值停在了导数最大的那个点上,软化则让它永远离那个点有一段距离。
如果目标本身来自有噪声的源(手写输入、网络同步、物理模拟),还应该在 IK 之前对目标位置做平滑,而不是在之后平滑关节角。原因是在 IK 之后平滑,滤波器要处理的是已经被放大过的信号,需要很强的平滑才压得住,而那会让整个动作变得迟钝。噪声要在被放大之前处理掉。
八、解析 IK 与 CCD/FABRIK 的选择
两段链优先用解析解:计算量固定、结果可解释、不会因迭代次数不足而抖动。脊柱、尾巴或触手等多关节链可使用 CCD 或 FABRIK,但仍需要可达性约束、关节限制和上一帧连续性。
九、下一步:让图像在关节附近连续变形
IK 只改变骨骼姿态,不会自动解决图层接缝。下一篇线性混合蒙皮与接缝处理将把网格顶点绑定到两根骨骼,并解释白点、裂缝和关节塌陷的来源。前置内容是骨骼变换与正向运动学。