3D 碰撞检测:从海量物体到一组接触数据
碰撞检测不是「判断是否相撞」这么简单——它要在可接受的代价下,为求解器稳定地交付接触点、法线与深度。本文按管线分层讲清宽相、窄相、流形与 CCD。
物理回调 OnCollisionEnter 只告诉你「发生了碰撞」。但在回调之前,引擎已经做完一整套几何查询,并把结果整理成求解器能用的格式。
碰撞检测负责这件事:在 个刚体、多种形状、有限 CPU 预算下,找出谁与谁接触,并给出接触点、法线、穿透深度或分离距离。物体之后弹开、滑动还是堆稳,属于碰撞响应与约束求解——紧挨着检测,却是下一阶段。
读前提示
本文只谈 3D 刚体 的碰撞检测。积分、分离、摩擦与求解器见 3D 物理引擎核心概念。SAT 里用到的叉积直觉,可参考 叉积与游戏开发。
1先记住交付物:接触流形
窄相的最终产物通常不是布尔值,而是一张接触流形(contact manifold):
| 字段 | 含义 | 下游用途 |
|---|---|---|
| 接触法线 | 从 A 指向 B 的分离方向 | 非穿透冲量、反射轴 |
| 穿透深度 或分离距离 | 两形状嵌入多深,或仍相距多远 | 位置修正、Baumgarte 稳定化 |
| 接触点 (常 1–4 个) | 代表点,分布在被支撑面上 | 摩擦、扭矩、堆叠稳定 |
| 形状对 ID | 哪两个 Collider | 过滤、事件、材质 |
箱子平放在地上,若只留一个接触点,求解器无法表达「整面被支撑」——堆叠会晃、会穿。流形是检测与求解之间的数据契约;后面所有宽相、窄相、缓存与 CCD,都是为稳定产出这张表。
仿真之外,同一套几何查询还承担其他任务——「碰撞了」只是最贫乏的输出之一:
| 查询 | 典型输出 | 用途 |
|---|---|---|
| Overlap | 布尔值或重叠对象列表 | 触发区、爆炸范围 |
| Distance / Closest Points | 距离与两侧最近点 | 角色避障、预测接触 |
| Raycast | 命中距离、位置、法线、Shape ID | 瞄准、拾取、视线 |
| Shape Cast | 首次命中比例、位置、法线 | 角色移动、高速物体 |
| Simulation Contact | 接触点集、法线、分离距离或穿透深度 | 约束求解 |
下文聚焦最后一行;但排错时要先分清你卡在哪种查询上——Raycast 漏检和仿真流形抖动,排查路径并不相同。
2整条管线:四层过滤
可以把检测想成四层筛子,越往后越贵、越精确:
一条铁律贯穿各层:前面可以误报(假阳性),不能漏报(假阴性)。宽相说「也许碰了」,窄相可以否决;宽相不能把真正会碰的对扔掉。
3第 0 层:逻辑过滤
这是最容易被忽视、却往往最划算的一层。碰撞矩阵(Layer、Group、Mask)、Trigger 与 Sensor、运动学—运动学跳过、同层静态体互斥——都在窄相之前就把组合砍掉。
先减 n,再谈算法
候选对数量大致随 增长。把「不该测的对」在规则层拿掉,比换一套 BVH 更直接。
4第 1 层:宽相——用包围盒圈邻居
4.1为什么必须宽相
个物体两两组合数是 。100 个物体约 5,000 对;1,000 个约 50 万对。窄相一次球—凸包测试虽不算巨贵,乘上这个基数就吃不消。
宽相不碰三角面,只给每个形状套轴对齐包围盒(AABB),再用空间结构找「盒子重叠」的对。NVIDIA 对宽相/窄相分工的经典描述与此一致:宽相保守地删掉大部分组合,窄相只对候选对做真实几何。1 PhysX 等引擎也明确把宽相定义为 AABB 层面的潜在碰撞报告,真实相交留给窄相。2
4.2AABB:三条轴上的区间重叠
AABB 由两个角点定死:
两盒相交 ⟺ 、、 三条轴上的投影区间全部重叠。以 轴为例:
、 同理;任一轴分离即可排除整对。
旋转物体会让 AABB 比真实形状胖一圈,于是宽相多出一些假阳性——这是预期行为,窄相会纠正。
4.3空间结构怎么选
| 结构 | 一句话 | 擅长 | 警惕 |
|---|---|---|---|
| Sweep and Prune | 排序轴端点,扫重叠区间 | 帧间位移小、排序近乎有序 | 大规模瞬移、堆叠密集时退化 |
| 均匀网格 / 哈希 | 物体进格子,只比邻格 | 尺寸均匀、分布中等 | 「茶壶进体育场」:极大/极小物体同场 |
| 动态 AABB 树 | 层次包围盒,增量更新 | 通用、尺寸参差 | 树平衡与更新成本 |
| 分区宽相 | 大世界切块 | 流式开放世界 | 跨区物体管理 |
没有银弹。均匀网格在物体尺寸悬殊时会浪费格子;SAP 在「全员挤一团」时仍可能接近 。3 工程上常组合:规则过滤 + 一种宽相 + 按场景调参。
5第 2 层:窄相——形状几何的「终审」
候选对进入窄相后,才看 Sphere、Box、Capsule、凸包或三角网格。算法按形状组合分流,没有单一万能公式。
5.1图元:优先解析,少绕路
球—球只需比较距离平方与半径和平方,避免 sqrt。球—平面、胶囊—盒等有成熟闭式或专用实现,分支少、接触点稳。能用图元表达碰撞体,就不要绑高模网格。
5.2凸体:分离轴与 GJK 两条路
对任意凸体,常见两条通用路线:
分离轴定理(SAT)——尝试有限条候选轴;若某轴上两形状的投影区间不重叠,则分离。两个 OBB 最多检验 15 条轴(各 3 条局部轴 + 两两叉积 9 条)。重叠最小的轴常给出法线与穿透量。叉积在这里的作用,是生成「同时垂直于两轴」的新候选方向:
GJK——把问题改写成:两凸集是否相交 ⟺ 它们的 Minkowski 差是否包含原点。4
算法不构造整块差集,只通过支撑映射沿搜索方向取最远点:
迭代维护一个点、线段、三角形或四面体组成的 simplex,把搜索方向不断推向离原点更近的位置:
simplex = { support(initialDirection) }
direction = -simplex.closestPoint
loop:
p = supportA(direction) - supportB(-direction)
if p 没有越过原点:
return 分离
把 p 加入 simplex,并删掉不可能包住原点的顶点
if simplex 包住原点:
return 相交
direction = 指向原点的下一搜索方向球、盒、胶囊、凸包可共用同一框架;距离版还能返回最近点,已穿透时常接 EPA 等估深度。
| SAT | GJK | |
|---|---|---|
| 直觉 | 找一条轴「把两者撕开」 | 在差集中追原点 |
| 强项 | OBB、多面体、接触特征清晰 | 任意凸体、距离查询统一 |
| 弱项 | 候选轴枚举与退化叉积 | 穿透深度需后处理;实现细节多 |
相交 ≠ 流形就绪
GJK 答「碰没碰」很快;求解器还要稳定法线、深度与多点分布。量产引擎会把 GJK/EPA、面裁剪、特征匹配和流形缓存叠在一起用。
5.3凹体与网格:别和凸体混为一谈
凹模型没有「沿任意方向单一外壳」。动态物体通常用凸包、复合凸体或胶囊组合近似;静态关卡才常用三角网格 + 内部 BVH,先剔除绝大多数三角形,再对少量三角形做精确测试。薄片、退化三角形、非流形边是网格碰撞的经典故障源。
6流形为什么要「记住」上一帧
每帧从零生成全部接触点,成本高,且堆叠接触会抖。成熟实现会跨帧复用流形:相对运动小时只刷新深度与法线,运动过大或接触丢失时再重建。PhysX 的 Persistent Contact Manifold(PCM)即此类思路。5
这和宽相里「排序近乎有序」、GJK 用上一帧方向 warm start 一样,都在吃时间相干性:物理世界很少每帧完全洗牌,上一帧的信息是性能资产。
7第 3 层:时间——离散采样会穿墙
前面各层都在某一时刻看几何。固定步长 下,物体从 跳到 ,两端可能都与薄墙无重叠,轨迹却已穿过——隧道效应(tunneling)。
粗略地说,当单步位移 接近或超过物体最薄尺度、障碍厚度或接触余量时,隧道风险会迅速上升。
把形状沿本步运动路径扫掠,求最早的 Time of Impact(TOI)。找到首次命中后,把物体推进到那个时刻,再处理剩余时间。几何意义清晰,但旋转运动比纯线性扫掠难处理,成本也更高。
Jolt 的 LinearCast、PhysX 的 sweep 与 speculative CCD 实现细节不同,可组合使用。6 7 实践上:子弹、高速抛射物开 CCD;普通刚体优先调 与子步,不必全场全开。
8检测在这里止步
窄相交出 、、 之后,检测的工作结束。求解器才用这些量算法向冲量、摩擦、反弹:
排错时先分清阶段——现象对不上层,往往是在错误的地方调参:
| 现象 | 更可能的问题层 |
|---|---|
| 完全没触发碰撞 | 过滤、AABB 未更新、Shape 缺失、CCD/步长 |
| 触发了但点/法线怪 | 窄相、网格烹饪、缩放、流形生成 |
| 点法线对但运动怪 | 质量、摩擦、恢复系数、求解器迭代(已不是检测) |
需要逐层排查时,按管线从前向后展开:
检查 Collision Layer / Mask、静态—静态与运动学—运动学过滤、Trigger 设置,以及运行时是否替换了 Shape。先确认候选对没有在逻辑层被删掉。
打开物理 Debug Draw。超大的 AABB 会制造大量假阳性;没有及时更新的 AABB 会漏检。复合体、粒子簇和跨区对象尤其值得观察。
检查凸包顶点、三角网格烹饪、退化面、法线、负缩放和非均匀缩放。若问题只发生在某一对 Shape 类型,先做最小复现场景。
比较 与物体最薄尺度。尝试固定时间步、substep、sweep test 或仅为高风险物体开启 CCD;不要先把全场景切到最高质量。
记录接触点、法线、深度和 TOI。如果这些数据正确而运动异常,问题已经进入质量、惯性、摩擦、恢复系数或迭代求解器。
性能分析也要分阶段看:候选对数量暴露宽相与过滤问题,窄相调用次数和 Shape 类型暴露几何问题,接触点与活跃约束数量更接近求解器负担。只看一个总的 Physics.Update 时间,很难知道该优化哪一层。
9形状怎么选:一张优先级表
| 优先级 | 形状 | 说明 |
|---|---|---|
| 1 | 球、胶囊、盒 | 便宜、稳,角色与道具首选 |
| 2 | 凸包 | 控顶点数,避免锯齿轮廓 |
| 3 | 复合碰撞体 | 多个凸块拼凹外形 |
| 4 | 三角网格 | 多用于静态环境 |
| 5 | 渲染高模 | 通常不参与动态窄相 |
缩放是常见的隐形变量
非均匀缩放会把球拉成椭球、让凸包支撑映射变复杂。有的引擎会重新烹饪形状,有的只支持有限缩放,有的直接退化到更慢的路径。发布前在目标引擎版本里实测,不要只看 Scene 视图。
10收束:四个问题,四层代价
遇到碰撞问题时,按代价从低到高问四句:
- 规则上该测吗? — Layer / Mask
- 空间上可能碰吗? — AABB、宽相结构
- 几何上真碰了吗? — 图元 / SAT / GJK / 网格 BVH
- 时间上有没有漏帧? — CCD、Shape Cast、 与物体厚度
四层答完,你手里应有一张流形,而不是一句「撞了」。宽相负责大胆怀疑,窄相负责严格求证,流形负责记住接触,CCD 负责补上帧间的缝——之后才是求解器让物体「表现得像真的碰了」。
一句话
碰撞检测的本质:在预算内,稳定地把几何接触翻译成求解器能消费的接触数据。
注释
-
NVIDIA GPU Gems 3,Chapter 32: Broad-Phase Collision Detection with CUDA。解释宽相/窄相分工、暴力检测的 组合数,以及 SAP 与空间划分的基本思路。 ↩
-
NVIDIA PhysX Documentation,Rigid Body Collision — Broad-phase Collision Detection。宽相基于 AABB 报告潜在碰撞,窄相负责真实相交判定。 ↩
-
Tero Karras, NVIDIA Technical Blog,Thinking Parallel, Part I: Collision Detection on the GPU(2012)。讨论均匀网格在物体尺寸悬殊时的效率问题。 ↩
-
Gilbert, Johnson, Keerthi,A Fast Procedure for Computing the Distance Between Complex Objects in Three-Dimensional Space,IEEE Journal of Robotics and Automation 4(2), 1988。GJK 原始论文。 ↩
-
NVIDIA PhysX Documentation,Persistent Contact Manifold (PCM)。跨帧复用与刷新接触的官方说明。 ↩
-
Jolt Physics Documentation,Continuous Collision Detection。
LinearCast与 tunneling 风险的说明。 ↩ -
NVIDIA PhysX Documentation,Continuous Collision Detection。sweep、speculative 与组合模式。 ↩
CONTINUE EXPLORING