技术博客

骨骼动画:Skeleton3D 的 rest 与 pose 双变换 —— 骨骼矩阵的两套坐标

第 138 / 149 篇 · 2025-05-27 · godot · 动画系统骨骼动画3D

3D 骨骼动画的入门困惑几乎总是同一个:DCC 工具里摆好的骨架,进了引擎为什么"多了一层变换"?import 设置里的 rest pose 选项改了又改,角色要么扭曲要么漂走。根源在 Skeleton3D 的存储模型:每根骨骼存两份变换,而动画只改其中一份。这篇把两份变换的数学讲透。

每根骨骼的两份局部变换

Skeleton3D 的骨骼列表是平坦数组,每根骨骼记录 parent 索引和两份变换:

  • rest:绑定姿势(bind pose)下的局部变换——骨骼相对父骨骼的位置/旋转/缩放,建模时在 T-pose 下确定;
  • pose:动画写入的局部变换,初始等于 rest,动画 track 每帧覆盖它。

渲染时要的是每根骨骼的全局变换(蒙皮顶点乘的是骨骼全局矩阵),它沿骨骼链复合。设骨骼 $i$ 的父骨骼为 $p(i)$,局部 rest 为 $R_i$、局部 pose 为 $P_i$,则

$$G_i = G_{p(i)} \cdot P_i, \qquad G_{\text{root}} = P_{\text{root}}$$

注意复合链上只有 pose,rest 似乎消失了——它藏在初始化里:不做动画时 $P_i = R_i$,骨架保持 T-pose。rest 的真正用处在蒙皮:Godot(和 glTF 一样)存的是逆绑定矩阵 $R_i^{-1}$ 的 global 版,蒙皮公式为

$$v' = \sum_k w_k \cdot G_k \cdot G_k^{\text{bind},-1} \cdot v$$

顶点先被逆绑定矩阵变换回骨骼局部空间,再被当前全局矩阵带去新位置。$G_k^{\text{bind}}$ 就是在绑定时刻算出来的 $G_k$(此时 $P = R$)——所以 rest 定义了"不动时蒙皮恒等"的参考系,动画 pose 是绕它的增量。

动画 track 存的是 pose

明白双变换后,Animation 资源里骨骼 track 存什么就顺理成章:track 路径指向 Skeleton3D: bones/12/pose,值是完整的局部 pose 变换(不是相对 rest 的增量——这是 Godot 和某些引擎不同的一点)。每帧 mixer 把 track 值写进 pose,Skeleton3D 的 get_bone_global_pose() 做上面的链式复合,Skeleton3DModifier(物理骨骼、IK,见九-4/九-7)也在这一层插进来改 pose。

由此能推导出几个实务结论:

  1. rest 改了,动画就废一半:动画存的是绝对 pose,rest 只决定"无动画长什么样"和蒙皮参考系。从 Blender 改了绑定姿势重新导出,旧动画会让角色摆出诡异的偏移姿势——因为 pose 值是相对旧 rest 摆出来的,蒙皮参考系却是新的。
  2. 加骨骼不改 rest,蒙皮不会动:新增一根骨骼只要它的 $G \cdot G^{\text{bind},-1} = I$(即 pose = rest),对渲染结果零影响,纯加挂点。
  3. set_bone_pose_* 系列设的是局部 pose,想做"全局朝向某个方向"要 set_bone_global_pose_override 或自己解逆运动学(九-4)。

骨骼链:pose 沿父链复合,蒙皮乘逆绑定矩阵 root spine(父 0) arm(父 1) 每根骨骼存两份局部变换 rest R_i:绑定姿势(T-pose),蒙皮参考系 pose P_i:动画 track 每帧覆盖,初值 = rest 全局:G_i = G_parent · P_i(链上只有 pose) 动画无动作时 P_i = R_i,渲染恒等 track 存绝对 pose,不是相对 rest 的增量 蒙皮(每个顶点) v' = Σ w_k · G_k · G_bind,k⁻¹ · v G_bind:绑定时刻的全局矩阵(P = R 时算出) 不动时 G · G_bind⁻¹ = I,顶点不动 rest 改了 → G_bind 变 → 旧动画整体错位

rest 不一致:重定向的地雷

想让骨架 A 的动画驱动骨架 B(人形动画库是刚需),光拷贝 pose 值是不行的:A 的"手臂平举"对应的局部旋转,套到 B 的骨骼上,因为 B 的 rest 朝向不同,摆出来是另一个姿势。正确做法是重定向:对每个骨骼求出 A 的 pose 相对其 rest 的"增量",再施加到 B 的 rest 上:

$$P_i^{B} = R_i^{B} \cdot (R_i^{A})^{-1} \cdot P_i^{A}$$

增量 $R_A^{-1} P_A$ 是局部空间里的旋转偏移(identity 表示"保持 rest"),左乘 B 的 rest 就得到 B 的等价姿势——前提是两根骨骼的局部空间定义可比对(轴向约定一致)。轴向不一致时还要插入对齐旋转,这就是 4.3 的 SkeletonProfile 干的事:它定义人形骨骼的标准朝向(SkeletonProfileHumanoid),导入时把两边骨架的骨骼都映射到标准轴向后做增量传递。所以"重定向结果歪 90°"这类问题,九成是骨架 B 的 rest 轴向和 humanoid 标准不符——先修骨架的 rest,再谈重定向(九-9 展开现状)。

一个自检技巧

怀疑蒙皮或动画错位时,逐层隔离:get_bone_pose() 看局部动画值对不对;get_bone_global_pose() 看链式复合对不对;把 show_rest_only 打开看骨架本身摆得对不对;最后才看蒙皮。四层各管一段,二分定位比对着最终网格瞎猜快得多。

本文基于 Godot 4.2–4.4;SkeletonProfileHumanoid 为 4.3 新增,4.2 及以前重定向要靠第三方插件。