核心原因是:
精确碰撞检测比较昂贵,不能对场景中所有形状两两执行。PhysX 先用低成本方法筛选候选对,再对少量候选对进行精确计算。
两阶段分别回答不同的问题:
Broad Phase(宽相) 哪些形状可能发生碰撞? ↓ Narrow Phase(窄相) 这些形状是否形成接触? 接触在哪里、法线是什么、分离距离是多少?严格来说,小规模场景可以直接两两检测,并不是数学上“必须”分成两阶段。但对大型实时物理场景,这种分工非常重要。
一、为什么不能直接做精确检测?
假设场景中有 (N) 个碰撞形状,全部两两配对的数量为:
[
P=\frac{N(N-1)}{2}
]
当 (N=10,000) 时:
[
P=49,995,000
]
即接近5000 万个形状对。
如果每个物理步都对这些配对进行凸体、三角网格等精确检测,成本通常不可接受。
而且绝大多数配对根本没有意义:
城市东侧的汽车 vs 城市西侧的垃圾桶它们相距几公里,不应该进入精确几何计算。
因此需要:
大量形状 ↓ 低成本空间筛选 少量候选对 ↓ 精确几何检测 有效接触二、Broad Phase:用便宜的边界替代复杂形状
1. 核心职责
Broad Phase 不直接研究形状表面的细节,而是使用包围体进行空间筛选。
PhysX 的模拟宽相主要围绕AABB,轴对齐包围盒工作:
实际碰撞形状 ↓ 包住它的世界空间 AABB ↓ 检查哪些包围盒可能重叠AABB 的三个方向始终与坐标轴平行,可以用最小点和最大点表示:
structAABB{Vec3 min;Vec3 max;};两个 AABB 是否重叠,只需要检查三个坐标轴上的区间:
booloverlap(constAABB&a,constAABB&b){returna.min.x<=b.max.x&&a.max.x>=b.min.x&&a.min.y<=b.max.y&&a.max.y>=b.min.y&&a.min.z<=b.max.z&&a.max.z>=b.min.z;}这比复杂凸体或网格之间的精确接触生成便宜得多。
2. 只是换成 AABB,还不够
一个容易忽视的点是:
如果仍然把所有 AABB 两两比较,复杂度还是 (O(N^2))。
因此 Broad Phase 不仅使用简单边界,还使用空间组织算法,避免检查大量明显不相关的配对。
PhysX 不同版本和配置提供的宽相算法包括:
- SAP:Sweep and Prune;
- MBP:Multi Box Pruning;
- ABP:Automatic Box Pruning;
- GPU Broad Phase。
例如,SAP 利用坐标轴上的区间排序,空间分区类方法则利用区域划分减少候选配对。
Broad Phase 的价值是“便宜的表示 + 高效的候选搜索”,不是只有包围盒。
三、为什么 Broad Phase 不能直接判定碰撞?
因为包围盒只是实际形状的保守近似。
以两个球为例:
球 A:圆心 (0, 0, 0),半径 1 球 B:圆心 (1.5, 1.5, 0),半径 1它们的 AABB 在三个轴上都有重叠。
但球心距离为:
[
d=\sqrt{1.52+1.52}\approx2.121
]
而半径之和为:
[
r_A+r_B=2
]
所以两个球实际没有相交。
这就是:
AABB 重叠 ≠ 实际几何相交对于旋转后的细长物体,包围盒中的空白区域可能更大,误选也更加明显。
宽相的设计原则
对于需要检测的交互,Broad Phase 应尽量做到:
- 允许误选:包围盒重叠,但几何不接触;
- 避免漏选:应该进一步检测的对象对没有被提交。
误选可以由窄相排除;漏选则可能导致碰撞丢失。
这里的“避免漏选”以正确的边界、更新和配置为前提。只使用离散位置的边界,并不能自动避免高速物体穿墙,还需要相应的 CCD 机制。
四、Narrow Phase:把候选对变成接触数据
1. 核心职责
窄相接收通过宽相和过滤的候选对,执行具体几何算法:
球体 vs 球体 球体 vs 盒体 胶囊 vs 凸体 凸体 vs 凸体 凸体 vs 三角网格对于普通接触,其任务不只是返回一个true或false,还需要生成求解器所需的接触信息。
典型输出包括:
| 数据 | 用途 |
|---|---|
| 接触位置 | 确定接触作用位置及力臂 |
| 接触法线 | 确定主要约束方向 |
| 分离距离 | 描述距离或穿透情况 |
| 接触流形 | 组织一组相关接触 |
| 几何附加信息 | 支持材质、面索引等后续处理 |
触发器交互则通常只需要重叠状态,不进入普通接触冲量求解。
2. 不同形状使用不同算法
例如:
- 球体与球体:可以使用简单解析计算;
- 凸体之间:可能使用 GJK、EPA、SAT 及其他专用路径;
- 凸体与网格:通常先筛选相关三角形,再进行局部检测;
- 持续接触:可以利用持久接触流形减少重复计算。
所以,不能把 PhysX 的整个窄相概括为“统一执行一次 GJK”。
3. 接触不一定要求已经穿透
PhysX 可以根据接触生成参数,在几何尚未实际穿透时生成接触。
因此更准确的表述是:
Narrow Phase 判断是否需要生成接触,并计算接触数据,而不只是判断几何体是否相交。
例如,两个形状可能已经进入接触生成距离,但还没有达到静止时的目标间距。
五、两者如何配合?
以一个箱子落到地面为例:
第一步:更新边界
箱子的位置与旋转 ↓ 更新箱子的世界空间 AABB第二步:宽相筛选
箱子的边界与地面的边界形成候选重叠 ↓ 产生或维持候选对此时只能确认“值得继续检查”。
第三步:碰撞过滤
检查:
- 是否允许这两类对象交互;
- 是否为触发器;
- 是否需要接触响应;
- 是否请求通知等。
第四步:窄相生成接触
箱子几何 + 地面几何 ↓ 计算接触位置、法线和分离距离第五步:交给约束求解器
接触数据 + 质量 + 惯性 + 速度 + 材质 ↓ 约束准备与求解 ↓ 计算支撑、摩擦或反弹响应Broad Phase 和 Narrow Phase 都不负责最终决定“把箱子弹多远”。运动响应属于后续约束求解。
六、这种拆分究竟节省了什么?
可以用一个简化成本模型表示:
[
T
\approx
T_{\text{边界更新}}
+
T_{\text{宽相搜索}}
+
K \cdot C_{\text{窄相}}
]
其中:
- (K):进入窄相的有效候选对数量;
- (C_{\text{窄相}}):平均单对精确检测成本。
如果直接两两精确检测:
[
T_{\text{直接}}
\approx
\frac{N(N-1)}{2}\cdot C_{\text{窄相}}
]
两阶段设计的关键,是在典型场景里让:
[
K \ll \frac{N(N-1)}{2}
]
不过,宽相没有消灭最坏情况。
如果大量物体挤在同一个区域,所有边界都互相重叠,候选对数量仍可能接近 (O(N^2))。不能笼统地说 Broad Phase 把任何场景都变成了 (O(N\log N))。
七、PhysX 还会利用“上一物理步”的结果
实际引擎不会每步完全从零开始。
Broad Phase 的时间相干性
维护候选对变化:
新增重叠对 保持重叠的关系 已经分离的对对于变化较小的场景,增量更新比全量重建更有效。
Narrow Phase 的时间相干性
复用接触缓存和持久接触流形:
上一物理步的接触 + 本步的相对运动 ↓ 验证、更新或补充接触例如静止在桌面上的箱子,不需要每步把全部几何关系当作第一次见到来处理。
八、对应源码应该看哪里?
以常见 PhysX 4.x/5.x 源码布局为例:
| 职责 | 主要阅读位置 |
|---|---|
| AABB 管理、Broad Phase | source/lowlevelaabb/ |
| 几何测试、接触生成、流形 | source/geomutils/ |
| 底层碰撞任务与接触处理组织 | source/lowlevel/ |
| 交互生命周期与场景控制 | source/simulationcontroller/ |
| 接触约束准备与响应求解 | source/lowleveldynamics/ |
这些职责会跨模块协作,不是一个目录严格对应一个串行阶段。
另外,Scene Query 不等于模拟碰撞的 Broad Phase。射线、Sweep、Overlap 使用自己的查询组织与加速机制,虽然也会复用几何算法。
总结
| 对比项 | Broad Phase | Narrow Phase |
|---|---|---|
| 核心问题 | 谁可能碰到谁? | 是否需要接触,接触情况如何? |
| 主要依据 | 包围盒与空间组织 | 具体几何与接触缓存 |
| 处理范围 | 场景中的大量边界 | 筛选后的有效候选对 |
| 输出 | 候选对及其变化 | 接触或重叠数据 |
| 成本特点 | 强调低成本、大规模筛选 | 单对通常更昂贵 |
| 是否计算最终碰撞响应 | 否 | 否 |
一句话概括:Broad Phase 负责不让昂贵的计算浪费在无关对象上,Narrow Phase 负责把“可能碰撞”变成可供求解器使用的精确接触信息。