技术博客

关节系统:6DOF 与 hinge 约束是怎么被求解的

第 69 / 149 篇 · 2025-12-27 · godot · 物理引擎约束求解Jolt

两个刚体用铰链连在一起,物理上意味着它们的相对自由度从 6 个(三维平动+转动)被砍掉几个。hinge 砍到 1 个(绕轴转动),6DOF 砍到 0~6 个可调。这篇文章不罗列节点参数,而是回答一个更根本的问题:solver 怎么让两个刚体"听话"?理解了求解器,参数表上的 stiffness、damping、bias 才有意义。

约束的数学形式:Jacobian

每个约束条件写成 $C(x) = 0$:某个相对运动量必须保持为零(hinge 的锚点重合、轴向垂直)或在限值内(6DOF 的 limit)。对时间求导得到速度级约束:

$$ J v + b = 0 $$

$J$ 是约束雅可比(一个 $m \times 12$ 矩阵,$m$ 是约束维数,12 是两个刚体各 6 个速度分量),$v$ 是堆叠的速度向量,$b$ 是偏置项——它允许约束"有目标速度",比如马达要让 hinge 以 $\omega$ 转动时,$b$ 取 $-\omega$。

解约束就是找一对冲量 $\lambda$(每个约束维一个标量),作用到两个刚体上使约束方程成立。把"冲量 → 速度变化"关系 $v^+ = v + M^{-1} J^T \lambda$ 代入约束方程,整理出核心方程:

$$ \left( J M^{-1} J^T \right) \lambda = -(J v + b) $$

$M$ 是堆叠的质量/惯量矩阵,$J M^{-1} J^T$ 叫有效质量(effective mass),是 $m \times m$ 的小矩阵。hinge 有 5 个约束维(3 平动 + 2 角限),6DOF 最多 6 维。所以这个方程组本身很小——关节求解的难点不在解方程,而在它和接触约束混在一起时的收敛。

为什么是迭代而不是直接解

一个门框上挂着两个铰链,门板上还堆着书,一个 island 里有接触约束、关节约束、摩擦约束,几十上百个。把它们联立成一个大的稀疏方程组直接解(比如 LCP pivoting),精度高但 $O(n^3)$ 级别的成本扛不住实时。现代引擎全部用迭代冲量法:每次取一个约束,把其他约束当成已解,单独求这个约束的 $\lambda$:

$$ \lambda_i = \frac{-(J_i v + b_i)}{J_i M^{-1} J_i^T} $$

这就是 Gauss-Seidel 的味道——逐约束更新,更新立即可见。收敛速度取决于约束之间的耦合程度:关节链条越长,一个约束的解对另一个的影响要经过多轮迭代才能传导过去,所以链式结构(门铰→门框)比星型结构(一堆箱子压着一块板)收敛慢,抖动也更容易出现在链条末端。

Godot Physics 与 Jolt 的实现差异

Godot Physics 3D 的关节在 servers/physics_3d/joints/(godot_joint_3d.cpp 及子类),实现思路是经典的"每个关节一个约束求解步骤",穿插在接触求解的迭代轮次里:同一轮迭代中,先解所有接触,再解所有关节,如此往复。它做了 warm starting(把上一步的 $\lambda$ 当作本轮初值),堆叠稳定度因此比冷启动好不少,但关节与接触平分迭代预算,关节多了会挤占接触求解的轮次。

Jolt 走的是另一条路(这也是 4.4 接入后很多人感觉"关节硬了"的原因之一):

  • 约束分类求解:Jolt 把接触约束和关节约束分离开,关节用更高精度求解(EConstraintSpace、SolvePositionConstraint 独立阶段),4.x 里 Jolt 的位置修正(position correction)用 NGS(non-linear Gauss-Seidel)单独跑一轮,关节的刚度感明显强于 Godot Physics 的线性修正。
  • 6DOF 的实现粒度:Jolt 的 SixDOFConstraint 原生支持 per-axis 的 motor/free/limited/locked 四种模式,Godot Physics 的 Generic6DOFJoint3D 也是按轴配置,但 limit 的软硬度( softness )参数在两边语义不同——Godot 的 softness 直接除进有效质量,Jolt 用弹簧模型(frequency/damping),同一组参数迁移过去手感完全不同。
  • hinge 的 limit 与 motor:hinge 的限位在两个引擎里都是"限位约束",但 Jolt 的马达是速度伺服 + 位置伺服的组合,Godot 的 motor 只提供目标速度。需要"转到指定角度停住"的场合,Jolt 的 position motor 开箱即用,Godot Physics 要自己在 _physics_process 里用 PID 凑。

调参的物理含义

关节面板上的参数,对应回方程 $J M^{-1} J^T \lambda = -(Jv + b)$:

参数 方程里的角色 调大效果
bias / erp $b$ 里的位置误差恢复项 错误纠正快,但过大会注入能量(抖动)
damping 方程外的速度衰减 抑制振荡,过大发黏
softness 除进有效质量 约束变软,允许少量违反
motor target velocity $b$ 的目标速度 直接决定马达转速

实测经验:门/吊桥这类"重力对抗关节"的结构,Godot Physics 下把 solver 迭代数(physics/solver/solver_iterations)从默认 8 提到 16~24,效果比调 stiffness 明显得多——链条末端的下垂本质是迭代没收敛,不是约束不够硬。换 Jolt 后同样场景 8 次迭代基本够,省下的预算可以加给接触质量。

迭代冲量求解:接触与关节共享迭代轮次 单次迭代 接触约束 λ₁…λₙ 关节约束 λ 摩擦约束 更新立即可见(Gauss-Seidel) warm start:λ 以上一轮为初值 × N 轮 结果 收敛:约束 残差小 未收敛: 链条末端 下垂/抖动 Godot Physics 关节穿插在接触迭代中 线性位置修正 需较多迭代数 Jolt 关节独立高精度求解 NGS 非线性位置修正 limit 用弹簧模型

本文方程推导参考 Godot 4.x servers/physics_3d/joints/godot_joint_3d.cpp(hinge、Generic6DOF 的求解步骤)与 Jolt 5.x 的 SixDOFConstraint、HingeConstraint 实现。