技术博客

静态体的优化:StaticBody3D 为什么不进 solver

第 74 / 149 篇 · 2025-06-02 · godot · 物理引擎性能优化源码分析

做关卡地形最常见的做法是:一个 MeshInstance 配一个 StaticBody3D + CollisionShape3D。一个关卡几千个静态碰撞体再正常不过。直觉上会担心"这么多刚体 solver 不累吗"——答案是静态体几乎不进 solver 的核心循环,但"几乎"两个字里藏着成本分布的反转。这篇按物理步的流程逐个阶段拆。

无限质量的代数意义

物理引擎表示"这个物体不动"的方法不是布尔标记,而是把逆质量设为零:$m^{-1} = 0$、惯量张量逆 $I^{-1} = 0$。回看一下第 10 篇的约束求解方程 $(J M^{-1} J^T)\lambda = -(Jv + b)$:有效质量矩阵里凡是有静态体参与的行,$M^{-1}$ 的零元素直接把该项消掉——冲量作用在静态体上不产生速度变化($M^{-1}J^T\lambda = 0$),约束的修正全部落到动态体一侧。

这个表示法的好处是优雅:求解器代码不用写"if static then skip",静态体自然地从方程里消失,数学上自洽。代价是静态体不能被任何力推动——想做一个"能被推动的箱子"就必须用 RigidBody3D(mode 改 static 也是把逆质量清零,原理相同)。

步进流程中静态体的真实开销

按第 2 篇的 step 流程逐个阶段看(servers/physics_3d/godot_space_3d.cpp):

broad phase:静态体照常进哈希网格,成本与动态体相同——AABB 更新、格子插入、pair 生成。一个静态体和一个动态体的 broad phase 成本没有区别,这一万静态体的开销是实打实的。好在静态体的 AABB 不变,pair 一旦建立就常驻,没有动态体那种"每帧 pair 重建"的 churn。

island 构建:物理引擎把"通过接触/关节连在一起、需要一起解"的动态体打包成 island 做独立求解。静态体不被分配进 island——它出现在接触里时,只是 island 里动态体的一个外部参照。island 的数量决定 solver 的循环次数,一万个静态体不增加 island 数。

integrate:只对 island 内的动态体做。静态体没有速度可积,零成本。

迭代求解:约束矩阵里静态侧被 $m^{-1}=0$ 消去。静态体参与求解的"成本"是零次浮点乘加——它只作为约束的参照坐标存在。

所以总成本公式大致是:$O(\text{静态体})$ 只在 broad phase 和 pair 缓存,$O(\text{动态体} + \text{接触数})$ 才是 solver 大头。一万静态体 + 十个动态体的场景,solver 时间只和十个动态体及其接触相关。

反转:什么时候静态体成了瓶颈

有一个场景会反转上面的结论:大量动态体压在静态地形上。接触约束的两侧,一侧是静态体(逆质量为零),另一侧是动态体。约束的 Jacobian 每轮迭代都要从静态体一侧取当前位置做几何计算(锚点在哪、法线朝哪),这部分计算量和接触数成正比,静态体数量不直接影响——但每个接触的"静态侧"计算一分钱都省不掉。所以"一万静态体 + 五千接触"的场景,瓶颈是五千接触,不是一万静态体。

另一个反转是 CollisionShape 的共享。静态体用凸包形状(ConvexPolygonShape3D)时,多个 StaticBody 共享同一个 Shape 资源,窄相的缓存(比如 hull 的支持函数预处理数据)只算一份。给每个静态体都 new 一个 Shape 会白白复制这些数据。关卡工具(比如从 glTF 批量生成碰撞)记得复用 Shape 资源。

static 与 kinematic 的分界

RigidBody3D 的 freeze 和 freeze_mode(static / kinematic)经常被拿来和 StaticBody3D 比较,两者的语义差别值得写清楚:

  • static 冻结:逆质量清零,和 StaticBody3D 完全等价,移动它只能靠改 transform(移动后 broad phase 会更新,但 solver 不认为它"有速度")。做"暂时不可动的平台"用这个。
  • kinematic 冻结:逆质量同样清零,但引擎把它的速度当作外部给定——你用代码设置 transform,引擎从位移差分里算出速度喂给接触(传送带、移动平台推动箱子就靠这个:箱子脚下的接触感受到 kinematic 面的速度,被摩擦带走)。StaticBody3D 没有"被推着走"的语义,移动中的 StaticBody3D 对动态体来说是一堵瞬移的墙,接触求解时速度为零,箱子会被"卡住"而不是被推走。

实践结论:会动的障碍物用 RigidBody3D + freeze_mode=kinematic 或者 AnimatableBody3D(它的语义就是 kinematic 静态),纯粹的墙壁地板用 StaticBody3D;混合场景下 Shape 资源能共享就共享。

静态体在物理步各阶段的成本分布 broad phase(哈希网格) 静态体照常参与:AABB 常驻、pair 建立后无 churn island 构建 静态体不进 island,只作动态体的外部参照 integrate / solver 迭代 m⁻¹=0 消去静态侧,零成本 静态体 约束方程中的静态侧: (J M⁻¹ Jᵀ)λ = −(Jv + b),M⁻¹ 含 0 → 修正全落到动态体 反转场景:大量接触的静态地形,瓶颈是接触数而非静态体数 另一个反转:移动中的 StaticBody3D 对动态体是瞬移的墙,要用 kinematic 冻结

本文基于 Godot 4.x servers/physics_3d/godot_space_3d.cpp 的 island 构建与求解流程、body_3d_sw.h 的 mode/质量属性设置。