技术博客

Chaos 约束系统:关节与 PBD 投影

第 46 / 100 篇 · 2026-07-01 · ue5 · Chaos物理引擎数值方法源码分析

求解器主流程那篇讲了 FPBDRigidsEvolutionGBF 一帧的管线:积分、碰撞、求解。这篇聚焦"求解"里最常用的部分——关节(Joint)约束。接触过 PhysX 的同行大多熟悉冲量法求解器,Chaos 的思路不同:它把关节当作位置约束,用 PBD(Position Based Dynamics)的投影迭代去满足。理解这个选择,是调通 ragdoll、载具悬挂、机械臂的关键。

冲量法的问题在哪

经典刚体动力学求解器(包括 PhysX 的 TGS 之前的版本)把约束写成力/冲量空间的问题:约束 $C(x) = 0$ 通过拉格朗日乘子 $\lambda$ 转化为关节冲量 $J^T \lambda$,解一个 KKT 系统。这个方法物理上"正确",但对** stiff 约束**(几乎不可伸长的绳子、硬关节)数值上很难伺候:显式积分下弹簧越硬,允许的时间步越小,否则能量爆炸。

游戏里的关节绝大多数是理想刚性的——人的膝关节不该有任何拉伸。与其模拟一个极硬的弹簧,不如直接断言位置约束成立:这就是 PBD 的思路。

PBD 投影:一个公式说清

PBD 一帧的结构(Müller 2007 的原始版本):

  1. 预测:先不管约束,按外力积分出预测位置 $x^* = x + v\,\Delta t$;
  2. 投影:对每条约束 $C(x) = 0$,把 $x^*$ 朝约束流形拉回去一步;
  3. 速度重建:约束满足后,用 $v = (x - x_{prev}) / \Delta t$ 反推出新速度。

投影这一步对单约束的解析解是沿梯度方向走最短距离:

$$\Delta x = -\frac{C(x)}{\lVert \nabla C \rVert^2}\,\nabla C(x)$$

直观理解:$\nabla C$ 指向约束违反量增长最快的方向,除以它的模长平方相当于归一化成"每单位位移能消掉多少违反"。两个刚体的约束则按逆质量加权分配修正量:$w_i = 1/m_i$,$\Delta x_i = \pm \frac{w_i}{w_1 + w_2} \Delta x$。轻的物体动得多,重的几乎不动——和冲量法的质量分配规则一致。

稳定性从哪来:第三步速度是从"已经满足约束的位置"反推的,约束修正本身不产生动能。一个被暴力拉回来的粒子,位置合法、速度也只反映实际位移,系统不会因为修正而发热。这就是"位置级求解比冲量稳"的数学根源。

XPBD:把刚度找回来

朴素 PBD 有个著名缺陷:刚度不是物理量,是数值量。同一个"硬"关节,迭代 4 次和迭代 20 次的硬度完全不同,而且时间步一变刚度跟着变。XPBD(Macklin 2016)的修补是给每条约束引入顺应性(compliance)$\alpha \geq 0$,$\alpha = 0$ 是理想刚性,$\alpha \to \infty$ 是自由。每次投影改为更新拉格朗日乘子:

$$\Delta \lambda = \frac{-C(x) - \tilde{\alpha}\,\lambda}{w_1 + w_2 + \tilde{\alpha}}, \qquad \tilde{\alpha} = \frac{\alpha}{\Delta t^2}$$

对比朴素 PBD 的 $\Delta x$ 公式,分母多了 $\tilde{\alpha}$ 一项:约束越"软",单步允许的位置修正越小,表现出一个可真实度量的弹性。Chaos 的软约束(如 PhysicsConstraint 开了 Soft 限位、布料、弹簧)走的都是带 compliance 的路径。注意 $\tilde{\alpha}$ 里的 $\Delta t^2$——时间步缩小时软约束变硬,这是 XPBD 和物理直觉一致的地方(同样截断误差下小步长更准),也是它和朴素 PBD 的本质区别。

迭代:Gauss-Seidel 的顺序依赖

一个关节常含多条约束(3 个线性轴 + 3 个角轴,再加上限位),整个岛可能有几百条。逐条投影、每条都基于刚被上一条改过的位置——这就是 Gauss-Seidel 迭代。它收敛慢(线性收敛)但实现简单、无矩阵需求,而且前几轮就把大部分违反量消掉了,是物理引擎最爱的折中。

迭代次数由 p.Chaos.Solver.Iterations(位置迭代)控制,默认个位数。两条实践推论:

  • 迭代不足的直接症状是约束打架:拉着的箱子被别的东西挤压时,两条约束互相覆盖修正量,看起来在抖。加迭代数能缓解,但更便宜的办法是调整约束优先级或减少冲突约束;
  • Gauss-Seidel 的顺序效应意味着求解结果与约束注册顺序微相关。Chaos 内部按 constraint container 分组排序,一般不需要管,但做帧级回放对拍时这是非确定性的来源之一。

关节的流形:swing-twist 分解

PhysicsConstraint 组件暴露的 AngularSwing1/Swing2/Twist 不是三个独立旋钮,背后是一个四元数空间的分解。把相对旋转 $q_{rel}$ 从参考系里拆成:

$$q_{rel} = q_{swing} \cdot q_{twist}$$

twist 是绕参考系的某个轴(默认 X,可在组件上改)的纯旋转,swing 是剩余部分——两个 swing 角加上 twist 角,正好对应 PhysX 风格的三个角限位。Chaos 在 Chaos/Joint/ChaosJoint.h(UE5.3 路径 Engine/Source/Runtime/Experimental/Chaos/Public/Chaos/Joint/)里把每条轴配置成 Locked / Limited / Free 三态:Locked 生成等式约束(直接投影),Limited 生成单侧不等式约束(超出限位才投影),Free 不产生约束。线性轴同理,再加上 Linear Drive 的弹簧项(XPBD 软约束)。

限位约束在限位内不激活,刚越界那一帧会突然引入一条硬约束,冲击感比冲量法明显。PhysicsConstraint 的 Soft 限位本质上就是把这条硬约束换成有 compliance 的软约束,用一点穿透换平滑——这是调门、舱盖类铰链的标准做法。

源码入口

  • 求解主循环:FPBDRigidsEvolutionGBF::Solve,位置迭代里逐 constraint container 调 Apply;
  • 关节约束本体:FPBDJointConstraint(Chaos/PBDRigidsJoints.h),看 ApplyPositionConstraints 里 swing-twist 的分解和逐轴投影;
  • 容器组织:FConstraintContainer 子类(JointConstraintContainer / SuspensionConstraintContainer 等),负责把约束按类型分组喂给迭代。

下面这张图是单帧内约束求解的数据流:

PBD 单帧:预测 → 迭代投影 → 速度重建 预测积分 x* = x + v·dt 约束投影(Gauss-Seidel) Δλ = (−C − α̃λ)/(w₁+w₂+α̃) 速度重建 v = (x − x_prev)/dt 写回 + 休眠检测 事件队列 迭代 p.Chaos.Solver.Iterations 轮 位置级修正天然不注入能量 → 硬约束无条件稳定;代价是刚度依赖迭代数与 dt(XPBD 补回物理刚度)

调关节时的实用结论:抖动先查迭代数和冲突约束,不要急着调质量比;限位的生硬感用 Soft 限位(compliance)换平滑;需要精确刚性的场景(机械传动)把迭代数拉高比换其他求解器更实际——Chaos 没有提供全局的 direct solver 开关,迭代质量就是它的精度旋钮。