Elytra
Blog物理引擎

3D 物理引擎核心概念:碰撞、分离、约束与并行

从刚体与形状出发,沿一次物理更新的管线理解碰撞检测、接触分离、约束求解、休眠、岛屿划分与多线程——不绑定具体引擎实现,建立可迁移的 mental model。

打开 Unity 的 Rigidbody、Godot 的 CharacterBody3D,或自研引擎里的一行 world.step(dt),背后往往是一套高度结构化的仿真管线。它并不神秘:把连续世界离散成固定时间步,在每一步里预测运动、找出相交、修正穿透、满足约束,再把结果写回游戏逻辑。

本文从 3D 刚体物理引擎 的共性出发,梳理各核心模块在做什么、彼此如何衔接。不深入某个库的源码细节;若你关心 PhysX、Jolt、Bullet 等具体选型,可继续读 游戏物理引擎横评。若「时间步如何推进、积分器为何影响稳定性」还不清楚,数值积分与游戏物理 是很好的前置。

范围说明

下文聚焦 刚体动力学 + 碰撞 + 约束 这一层——角色控制器、布料、流体、破坏等常在其上再建一层,或走完全不同的求解器。

1引擎在管什么:四个核心对象

在 mental model 里,先把这几样东西分开:

概念职责典型数据
刚体(Rigid Body)会动、有质量、受力和力矩位姿、线/角速度、质量与惯性张量
碰撞形状(Shape / Collider)定义「占据哪块空间」球、盒、胶囊、凸包、三角网格等
接触与约束(Contact / Constraint)描述物体之间「不能怎样」接触法线、穿透深度、关节限制
仿真世界(World / Scene)调度一步更新里的全部阶段宽相结构、岛屿、休眠表、求解器参数

刚体 可以 带多个形状(复合碰撞体);形状 不一定 等于渲染网格——高精度网格往往只用于静态环境,动态物体更常用凸包或简单图元,这是性能与稳定性的工程取舍,而非数学必然。

2一步更新里发生了什么

大多数实时 3D 引擎把物理帧拆成固定顺序。名字因实现而异,逻辑大致如下:

Loading diagram…

先建立这条主线

碰撞检测 回答「谁碰到了谁」;分离(position correction) 与 约束求解 回答「碰到之后怎样才不穿模、怎样摩擦、关节怎样约束」;休眠 与 岛屿 回答「哪些东西这一步可以不算」;并行 则在这些阶段里找可独立的工作块。

固定时间步(fixed timestep)很常见:渲染帧率波动时,逻辑仍用 dt = 1/50 或 1/60 推进,必要时在一帧内 子步(substep) 多次,避免大步长导致隧道效应或求解发散。1


3碰撞检测:从「可能」到「确实」

碰撞检测通常分两阶段,因为 O(n2)O(n^2) 的 pairwise 测试在数百个动态体时就吃不消。

3.1宽相(Broad Phase)

目标:快速筛出 可能 相交的刚体对,丢弃明显远离的组合。

常见结构:

结构思路适合场景
动态 AABB 树(BVH)层次包围盒,增量更新分布较散、物体尺寸差异大
空间哈希 / 均匀网格按格子索引 AABB规模中等、实现简单
Sweep and Prune排序轴上的区间重叠2D 或某一轴主导的场景

宽相只处理 轴对齐包围盒(AABB) 或 定向包围盒(OBB) 的相交,结果是一批 候选对(broad phase pair),仍可能包含假阳性。

3.2窄相(Narrow Phase)

对每一对候选,用 精确几何 判断是否相交、穿透多深、接触点在哪。

  • 球、盒、胶囊 等解析图元:有闭式或专用算法,便宜且稳定。
  • 凸体 vs 凸体:GJK(Gilbert–Johnson–Keerthi)求最近点/是否相交;若已穿透,常接 EPA(Expanding Polytope Algorithm)估深度与法线。2
  • 凹体 / 三角网格:通常先分解为凸包,或只对 静态 网格做更贵的测试;网格自相交、薄墙、非流形是经典坑点。

输出不是「碰了/没碰」这么简单,而是 接触流形(contact manifold):少量代表点(常 1–4 个)、接触法线 n\mathbf{n}、穿透深度 dd。流形在下一步会转成 非穿透约束。

3.3连续碰撞检测(CCD)

离散步长下,薄物体或高速子弹可能在两帧之间 穿过 薄墙——所谓 tunneling。CCD 在步内沿运动方向做 扫掠测试(sweep test) 或 保守推进,在 TOI(time of impact)处提前产生接触。3

代价更高,一般只对标记为「高速」的物体开启。


4分离:先把穿透「掰开」

检测到穿透后,引擎必须 减少几何重叠。玩家感知上的「穿模」,往往来自这一步做得不够,或只做速度层修正、位置仍嵌在一起。

4.1速度层 vs 位置层

  • 速度层(velocity-level):通过冲量改变 v,ω\mathbf{v}, \boldsymbol{\omega},使相对法向速度满足非穿透趋势;适合 即将接触 或 轻微擦碰。
  • 位置层(position-level / separation):直接微调位姿 x,q\mathbf{x}, \mathbf{q},把深度 dd 消掉一部分;适合 已经深度嵌入 的情况。

许多引擎在求解器里 同时 做两者:先位置修正(split impulse / position iterations),再速度冲量(velocity iterations),名称因库而异。1

4.2Baumgarte 稳定化

纯硬约束在离散时间下容易 抖动或弹飞。Baumgarte 把「期望的法向相对速度」写成与穿透深度成比例的一项:

vtarget=−βdΔtv_{\text{target}} = -\beta \frac{d}{\Delta t}

β∈(0,1]\beta \in (0,1] 控制 多快把洞填平;过大则像硬弹开,过小则长时间轻微穿模。这是 稳定化,不是真实材料的物理定律——游戏物理里大量参数都属于这一类「看起来对」的旋钮。

4.3分离与堆叠

堆箱子时,底层接触多、穿透深度小但 约束耦合强。只做一次分离往往不够;引擎会用 多次 position iteration,并在深度很小时 允许微小穿透(slop) 换稳定。这也是为什么「solver iterations」是 Inspector 里常见的调参项。

分离 ≠ 求解完成

把两物体推开只解决 几何重叠。摩擦、反弹、关节、堆叠重量分配,仍要靠下一节的 约束求解 在速度空间迭代完成。


5约束求解:冲量、摩擦与关节

接触可以看成 不等式约束:法向只能推、不能拉(除非用粘性模型);切向摩擦有 Coulomb 上限。铰链、滑块、弹簧则是 等式或带界限的等式约束。统一写法下,一步更新末尾常解一个 ** complementarity problem** 或在其上的 迭代近似。1

5.1序列冲量(Sequential Impulse)

Erin Catto 在 Box2D 里推广的做法:把全局问题拆成 逐约束标量冲量,Gauss–Seidel 式 依次更新 每个约束,多轮迭代逼近 simultaneous 解。1

对单个接触点,法向冲量 λn\lambda_n 限制物体沿 n\mathbf{n} 的相对运动;若 λn>0\lambda_n > 0,再算切向摩擦 λt\lambda_t,并钳在 μλn\mu \lambda_n 以内(μ\mu 为摩擦系数)。

关节(hinge、fixed、6-DOF spring)同样化为 Jacobian 行:约束对两个刚体速度的微分关系,以及 有效质量(effective mass) JM−1JT\mathbf{J M^{-1} J^T} 的逆用于求冲量步长。

5.2迭代次数与收敛

参数典型作用
velocity iterations摩擦、反弹、关节「跟手」程度
position iterations穿模深度、堆叠稳定性
ERP / CFM 等软约束、数值柔顺(因引擎而异)

迭代 不是越多越好:收益递减,且每轮都要扫一遍约束。竞技或 VR 项目常在「稳定堆叠」与「CPU 预算」之间折中。

5.3与积分的先后

常见顺序(与上文流程图一致):

  1. 用 半隐式 Euler 或类似积分器,从力更新速度;
  2. 碰撞 + 约束求解 修正速度(有时也修正位置);
  3. 再用速度更新位姿。

若先更新位姿再求解,穿透会更深;若积分器与求解器不匹配,会出现 数值积分文章 里说的 能量爆炸 或 假阻尼。


6休眠:不动的物体别算

堆好的家具、掉线后停住的 debris,若每帧仍走完整管线,是纯浪费。休眠(sleeping / deactivation) 把 动能低于阈值 且 持续若干帧 的刚体标记为 sleep,从宽相活跃集、岛屿求解里剔除,直到被 唤醒(wake)。

典型唤醒条件:

  • 与 活跃 刚体发生碰撞或产生冲量;
  • 被脚本 施加力/冲量 或 teleport;
  • 关节 连到活跃体,整条链可能被拉起。

工程意义

休眠是开放世界与物理 puzzle 能否「scalable」的关键之一。设计关卡时,大量「装饰性物理」若默认 Dynamic,可能在不知不觉中吃掉整个物理线程。

阈值设得太松,物体会在「几乎不动」时仍抖动而无法入睡;太紧则轻微碰撞后难以再次稳定——需要结合 linearSleepingThreshold、angularSleepingThreshold 等(各引擎命名不同)与 solver 迭代 一起调。


7岛屿:谁和谁必须一起算

岛屿(island) 是 约束图上的连通分量:通过接触或关节 直接或间接相连 的刚体属于同一岛;与当前岛无任何约束联系的,可以在本步 独立求解。

Loading diagram…

静态体、运动学体、休眠体通常 不进入活跃岛,或作为 零质量/无限质量 端点参与约束但不积分。

岛屿划分的作用:

  1. 正确性:求解器只在有耦合的子集上迭代,避免把无关物体绑进同一线性系统。
  2. 并行:不同岛屿之间 无数据依赖,可分配到不同 worker(见下节)。
  3. 增量:仅 活跃岛 需要重建接触与迭代;睡着的岛整岛可跳过。

8并行:哪里能拆,哪里不能

现代 3D 引擎(Jolt、PhysX 3+、Bullet 多线程分支等)普遍用 任务/job 系统 做并行,但 并非每一步都能任意并行。

阶段并行性说明
宽相较好更新 BVH、扫描格子可分区或并行遍历
窄相中等各 候选对 独立,但需避免写同一刚体的 race
约束求解岛级并行岛内 多为顺序 GS;岛与岛之间 可并行
积分较好各活跃刚体位姿更新通常独立

确定性(determinism) 与 并行 常冲突:同一输入若 worker 调度顺序不同,浮点累加顺序变化会导致联机回滚或 replays 不一致。竞技联机有时强制 单线程求解 或固定调度;单机则优先吞吐。4

射线检测、overlap query 常与 动态场景查询结构 共享 BVH,可在仿真步之外 只读并行,这也是「物理引擎 = 碰撞 + 动力学」一体的原因。


9把概念对齐到调参面板

读文档或 Inspector 时,可以把旋钮映射回上文:

你看到的参数对应模块
Collision layers / groups宽相前的 过滤,减少无效对
Contact offset / skin width窄相与 提前生成接触,减抖动
Solver iterations分离 + 约束 轮数
Bounce / friction约束求解里的 恢复系数与 Coulomb 摩擦
Sleep threshold休眠 判定
CCD / continuous窄相扫掠,防隧道
Max depenetration velocity分离 时限制单步推开速度,防弹飞

没有「一组万能默认值」:堆叠 puzzle、第一人称射击、载具链式关节,最优迭代次数和 sleep 策略会差很多。


10小结

3D 物理引擎的核心可以压缩成一条链:

形状定义空间 → 宽相/窄相找接触 → 分离减少穿透 → 迭代冲量满足法向/摩擦/关节 → 积分推进 → 休眠与分岛跳过无关工作 → 多线程吃满岛屿与独立对。

理解这条链之后,再看具体引擎文档,重点就不是背 API,而是问:它的宽相结构、求解器模型、CCD 与网格策略、线程模型 是否符合你的项目瓶颈。下一篇自然可以是 游戏物理引擎横评,把 mental model 落到选型与集成成本上。

注释

  1. Catto, Erin. Soft Constraints / Modeling and Solving Constraints(GDC 系列讲义,Box2D 作者)。阐述序列冲量、Baumgarte 稳定化、接触流形与 position/velocity 迭代的分工,是理解 2D/3D 游戏物理求解器的常用入口。 ↩ ↩2 ↩3 ↩4

  2. van den Bergen, G. Collision Detection in Interactive 3D Environments;以及 Ericson, Real-Time Collision Detection 中 GJK/EPA 章节。说明凸体窄相在实时引擎中的标准用法。 ↩

  3. Bullet Physics Documentation, Continuous Collision Detection and Ray Test。概述 sweep 与 CCD 在离散仿真中缓解 tunneling 的角色。 ↩

  4. Jolt Physics, Architecture overview 与文档中关于 job system、island 并行与 determinism 的说明。Jolt 明确以多核友好与可配置确定性为设计目标之一。 ↩

CONTINUE EXPLORING

沿着这篇文章继续

在地图中查看

On this page