Elytra
Blog物理引擎

3D 碰撞检测:从海量物体到一组接触数据

碰撞检测不是「判断是否相撞」这么简单——它要在可接受的代价下,为求解器稳定地交付接触点、法线与深度。本文按管线分层讲清宽相、窄相、流形与 CCD。

物理回调 OnCollisionEnter 只告诉你「发生了碰撞」。但在回调之前,引擎已经做完一整套几何查询,并把结果整理成求解器能用的格式。

碰撞检测负责这件事:在 nn 个刚体、多种形状、有限 CPU 预算下,找出谁与谁接触,并给出接触点、法线、穿透深度或分离距离。物体之后弹开、滑动还是堆稳,属于碰撞响应与约束求解——紧挨着检测,却是下一阶段。

读前提示

本文只谈 3D 刚体 的碰撞检测。积分、分离、摩擦与求解器见 3D 物理引擎核心概念。SAT 里用到的叉积直觉,可参考 叉积与游戏开发。

1先记住交付物:接触流形

窄相的最终产物通常不是布尔值,而是一张接触流形(contact manifold):

字段含义下游用途
接触法线 n\mathbf{n}从 A 指向 B 的分离方向非穿透冲量、反射轴
穿透深度 dd 或分离距离两形状嵌入多深,或仍相距多远位置修正、Baumgarte 稳定化
接触点 p\mathbf{p}(常 1–4 个)代表点,分布在被支撑面上摩擦、扭矩、堆叠稳定
形状对 ID哪两个 Collider过滤、事件、材质

箱子平放在地上,若只留一个接触点,求解器无法表达「整面被支撑」——堆叠会晃、会穿。流形是检测与求解之间的数据契约;后面所有宽相、窄相、缓存与 CCD,都是为稳定产出这张表。

仿真之外,同一套几何查询还承担其他任务——「碰撞了」只是最贫乏的输出之一:

查询典型输出用途
Overlap布尔值或重叠对象列表触发区、爆炸范围
Distance / Closest Points距离与两侧最近点角色避障、预测接触
Raycast命中距离、位置、法线、Shape ID瞄准、拾取、视线
Shape Cast首次命中比例、位置、法线角色移动、高速物体
Simulation Contact接触点集、法线、分离距离或穿透深度约束求解

下文聚焦最后一行;但排错时要先分清你卡在哪种查询上——Raycast 漏检和仿真流形抖动,排查路径并不相同。

2整条管线:四层过滤

可以把检测想成四层筛子,越往后越贵、越精确:

Loading diagram…

一条铁律贯穿各层:前面可以误报(假阳性),不能漏报(假阴性)。宽相说「也许碰了」,窄相可以否决;宽相不能把真正会碰的对扔掉。

规则层排除不可能交互的对象(玩家武器、同类静态体等)。
用便宜包围体 + 空间结构,产出候选对。
对候选对做精确几何,写出流形。
高速或薄片场景下,补上帧间可能漏掉的接触。

3第 0 层:逻辑过滤

这是最容易被忽视、却往往最划算的一层。碰撞矩阵(Layer、Group、Mask)、Trigger 与 Sensor、运动学—运动学跳过、同层静态体互斥——都在窄相之前就把组合砍掉。

先减 n,再谈算法

候选对数量大致随 n2n^2 增长。把「不该测的对」在规则层拿掉,比换一套 BVH 更直接。


4第 1 层:宽相——用包围盒圈邻居

4.1为什么必须宽相

nn 个物体两两组合数是 (n2)=n(n−1)/2\binom{n}{2} = n(n-1)/2。100 个物体约 5,000 对;1,000 个约 50 万对。窄相一次球—凸包测试虽不算巨贵,乘上这个基数就吃不消。

组合数随 n 二次增长。宽相的目标:让窄相只跑在「空间上邻近」的少量对上。

宽相不碰三角面,只给每个形状套轴对齐包围盒(AABB),再用空间结构找「盒子重叠」的对。NVIDIA 对宽相/窄相分工的经典描述与此一致:宽相保守地删掉大部分组合,窄相只对候选对做真实几何。1 PhysX 等引擎也明确把宽相定义为 AABB 层面的潜在碰撞报告,真实相交留给窄相。2

4.2AABB:三条轴上的区间重叠

AABB 由两个角点定死:

min=(xmin,ymin,zmin),max=(xmax,ymax,zmax)\mathbf{min}=(x_{min},y_{min},z_{min}),\qquad \mathbf{max}=(x_{max},y_{max},z_{max})

两盒相交 ⟺ xx、yy、zz 三条轴上的投影区间全部重叠。以 xx 轴为例:

Amax,x≥Bmin,x∧Bmax,x≥Amin,xA_{max,x}\ge B_{min,x} \quad\land\quad B_{max,x}\ge A_{min,x}

yy、zz 同理;任一轴分离即可排除整对。

拖动橙色中心点 B。下方与左侧粗线分别表示 x、y 轴投影;任一轴分离,就不可能碰撞。

旋转物体会让 AABB 比真实形状胖一圈,于是宽相多出一些假阳性——这是预期行为,窄相会纠正。

4.3空间结构怎么选

结构一句话擅长警惕
Sweep and Prune排序轴端点,扫重叠区间帧间位移小、排序近乎有序大规模瞬移、堆叠密集时退化
均匀网格 / 哈希物体进格子,只比邻格尺寸均匀、分布中等「茶壶进体育场」:极大/极小物体同场
动态 AABB 树层次包围盒,增量更新通用、尺寸参差树平衡与更新成本
分区宽相大世界切块流式开放世界跨区物体管理

没有银弹。均匀网格在物体尺寸悬殊时会浪费格子;SAP 在「全员挤一团」时仍可能接近 O(n2)O(n^2)。3 工程上常组合:规则过滤 + 一种宽相 + 按场景调参。


5第 2 层:窄相——形状几何的「终审」

候选对进入窄相后,才看 Sphere、Box、Capsule、凸包或三角网格。算法按形状组合分流,没有单一万能公式。

5.1图元:优先解析,少绕路

球—球只需比较距离平方与半径和平方,避免 sqrt。球—平面、胶囊—盒等有成熟闭式或专用实现,分支少、接触点稳。能用图元表达碰撞体,就不要绑高模网格。

5.2凸体:分离轴与 GJK 两条路

对任意凸体,常见两条通用路线:

分离轴定理(SAT)——尝试有限条候选轴;若某轴上两形状的投影区间不重叠,则分离。两个 OBB 最多检验 15 条轴(各 3 条局部轴 + 两两叉积 9 条)。重叠最小的轴常给出法线与穿透量。叉积在这里的作用,是生成「同时垂直于两轴」的新候选方向:

两局部轴 a、b;绿色 a×b 是 SAT 的一条候选分离轴。

GJK——把问题改写成:两凸集是否相交 ⟺ 它们的 Minkowski 差是否包含原点。4

A⊖B={a−b∣a∈A,b∈B}A\ominus B=\{\mathbf a-\mathbf b\mid \mathbf a\in A,\mathbf b\in B\}

算法不构造整块差集,只通过支撑映射沿搜索方向取最远点:

support⁡A⊖B(d)=support⁡A(d)−support⁡B(−d)\operatorname{support}_{A\ominus B}(\mathbf d) = \operatorname{support}_A(\mathbf d) - \operatorname{support}_B(-\mathbf d)

迭代维护一个点、线段、三角形或四面体组成的 simplex,把搜索方向不断推向离原点更近的位置:

simplex = { support(initialDirection) }
direction = -simplex.closestPoint

loop:
    p = supportA(direction) - supportB(-direction)
    if p 没有越过原点:
        return 分离
    把 p 加入 simplex,并删掉不可能包住原点的顶点
    if simplex 包住原点:
        return 相交
    direction = 指向原点的下一搜索方向

球、盒、胶囊、凸包可共用同一框架;距离版还能返回最近点,已穿透时常接 EPA 等估深度。

SATGJK
直觉找一条轴「把两者撕开」在差集中追原点
强项OBB、多面体、接触特征清晰任意凸体、距离查询统一
弱项候选轴枚举与退化叉积穿透深度需后处理;实现细节多

相交 ≠ 流形就绪

GJK 答「碰没碰」很快;求解器还要稳定法线、深度与多点分布。量产引擎会把 GJK/EPA、面裁剪、特征匹配和流形缓存叠在一起用。

5.3凹体与网格:别和凸体混为一谈

凹模型没有「沿任意方向单一外壳」。动态物体通常用凸包、复合凸体或胶囊组合近似;静态关卡才常用三角网格 + 内部 BVH,先剔除绝大多数三角形,再对少量三角形做精确测试。薄片、退化三角形、非流形边是网格碰撞的经典故障源。


6流形为什么要「记住」上一帧

每帧从零生成全部接触点,成本高,且堆叠接触会抖。成熟实现会跨帧复用流形:相对运动小时只刷新深度与法线,运动过大或接触丢失时再重建。PhysX 的 Persistent Contact Manifold(PCM)即此类思路。5

这和宽相里「排序近乎有序」、GJK 用上一帧方向 warm start 一样,都在吃时间相干性:物理世界很少每帧完全洗牌,上一帧的信息是性能资产。


7第 3 层:时间——离散采样会穿墙

前面各层都在某一时刻看几何。固定步长 Δt\Delta t 下,物体从 t0t_0 跳到 t1t_1,两端可能都与薄墙无重叠,轨迹却已穿过——隧道效应(tunneling)。

两帧采样都未重叠,但位移轨迹已穿墙——这不是浮点 bug,是离散时间带来的盲区。

粗略地说,当单步位移 ∥v∥Δt\|\mathbf v\|\Delta t 接近或超过物体最薄尺度、障碍厚度或接触余量时,隧道风险会迅速上升。

把形状沿本步运动路径扫掠,求最早的 Time of Impact(TOI)。找到首次命中后,把物体推进到那个时刻,再处理剩余时间。几何意义清晰,但旋转运动比纯线性扫掠难处理,成本也更高。

Jolt 的 LinearCast、PhysX 的 sweep 与 speculative CCD 实现细节不同,可组合使用。6 7 实践上:子弹、高速抛射物开 CCD;普通刚体优先调 Δt\Delta t 与子步,不必全场全开。


8检测在这里止步

窄相交出 n\mathbf{n}、dd、p\mathbf{p} 之后,检测的工作结束。求解器才用这些量算法向冲量、摩擦、反弹:

法线 n 来自检测;反射、滑动、冲量分配属于响应阶段。

排错时先分清阶段——现象对不上层,往往是在错误的地方调参:

现象更可能的问题层
完全没触发碰撞过滤、AABB 未更新、Shape 缺失、CCD/步长
触发了但点/法线怪窄相、网格烹饪、缩放、流形生成
点法线对但运动怪质量、摩擦、恢复系数、求解器迭代(已不是检测)

需要逐层排查时,按管线从前向后展开:

性能分析也要分阶段看:候选对数量暴露宽相与过滤问题,窄相调用次数和 Shape 类型暴露几何问题,接触点与活跃约束数量更接近求解器负担。只看一个总的 Physics.Update 时间,很难知道该优化哪一层。


9形状怎么选:一张优先级表

优先级形状说明
1球、胶囊、盒便宜、稳,角色与道具首选
2凸包控顶点数,避免锯齿轮廓
3复合碰撞体多个凸块拼凹外形
4三角网格多用于静态环境
5渲染高模通常不参与动态窄相

缩放是常见的隐形变量

非均匀缩放会把球拉成椭球、让凸包支撑映射变复杂。有的引擎会重新烹饪形状,有的只支持有限缩放,有的直接退化到更慢的路径。发布前在目标引擎版本里实测,不要只看 Scene 视图。


10收束:四个问题,四层代价

遇到碰撞问题时,按代价从低到高问四句:

  1. 规则上该测吗? — Layer / Mask
  2. 空间上可能碰吗? — AABB、宽相结构
  3. 几何上真碰了吗? — 图元 / SAT / GJK / 网格 BVH
  4. 时间上有没有漏帧? — CCD、Shape Cast、∥v∥Δt\|v\|\Delta t 与物体厚度

四层答完,你手里应有一张流形,而不是一句「撞了」。宽相负责大胆怀疑,窄相负责严格求证,流形负责记住接触,CCD 负责补上帧间的缝——之后才是求解器让物体「表现得像真的碰了」。

一句话

碰撞检测的本质:在预算内,稳定地把几何接触翻译成求解器能消费的接触数据。

注释

  1. NVIDIA GPU Gems 3,Chapter 32: Broad-Phase Collision Detection with CUDA。解释宽相/窄相分工、暴力检测的 n(n−1)/2n(n-1)/2 组合数,以及 SAP 与空间划分的基本思路。 ↩

  2. NVIDIA PhysX Documentation,Rigid Body Collision — Broad-phase Collision Detection。宽相基于 AABB 报告潜在碰撞,窄相负责真实相交判定。 ↩

  3. Tero Karras, NVIDIA Technical Blog,Thinking Parallel, Part I: Collision Detection on the GPU(2012)。讨论均匀网格在物体尺寸悬殊时的效率问题。 ↩

  4. 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 原始论文。 ↩

  5. NVIDIA PhysX Documentation,Persistent Contact Manifold (PCM)。跨帧复用与刷新接触的官方说明。 ↩

  6. Jolt Physics Documentation,Continuous Collision Detection。LinearCast 与 tunneling 风险的说明。 ↩

  7. NVIDIA PhysX Documentation,Continuous Collision Detection。sweep、speculative 与组合模式。 ↩

CONTINUE EXPLORING

沿着这篇文章继续

在地图中查看

On this page