GJK+EPA算法

GJK + EPA 算法详解

GJK(Gilbert--Johnson--Keerthi)和 EPA(Expanding Polytope Algorithm)是物理引擎(PhysX、Bullet、Box2D 等)中最核心的凸体碰撞检测与穿透计算算法。本文从零推导,覆盖数学原理、算法流程、数值稳定性和退化处理。


一、为什么需要 GJK

问题

给定两个凸体 A 和 B,判断它们是否相交。

关键变换:Minkowski Difference

定义 Minkowski Difference

复制代码
C = A ⊖ B = { a - b | a ∈ A, b ∈ B }

那么一个极其简洁的等价关系成立:

复制代码
A ∩ B ≠ ∅   ⇔   原点 0 ∈ C

证明

  • 如果 A ∩ B ≠ ∅,存在点 p 同时属于 A 和 B,那么 p - p = 0 属于 C。

  • 如果 0 ∈ C,则存在 a ∈ A, b ∈ B 使得 a - b = 0,即 a = b,所以 p = a = b 是两个集合的公共点。

为什么要求凸体

两个凸集的 Minkowski Difference 仍然是凸集。非凸体的 Minkowski Difference 可能不是凸的,GJK 只能工作在凸集上。处理非凸体需要先做凸分解。

思路转变

原来的问题"两个物体相交吗"变成了"一个凸集包含原点吗"。GJK 不显式构造整个 C(那太贵了),而是通过支撑函数按需采样。


二、支撑函数 Support Function

定义

复制代码
Support(A, B, d) = Support_A(d) - Support_B(-d)

其中:

复制代码
Support_A(d) = argmax { v · d | v ∈ A }

即 A 中沿方向 d 最远的点。

物理含义

Support(A, B, d) 给出 C 的边界上沿方向 d 的极值点。我们通过在不同方向上调用它,逐步"探测"C 的形状。

常见形状的支撑函数

球体 (center c, radius r)

复制代码
Support(d) = c + r · normalize(d)

Box (center c, half-extents h)

复制代码
Support(d) = c + (sign(dx)·hx, sign(dy)·hy, sign(dz)·hz)

Capsule:等价于球的 support,球心是线段两端中沿 d 更远的那个。

凸网格:遍历所有顶点取 max(Vᵢ · d),时间复杂度 O(n)。

计算量

支撑函数是 GJK/EPA 最频繁的调用。对球/Box/Capsule 是 O(1),对凸网格是 O(n)。这是为什么物理引擎里尽量用参数化形状而不是三角网格做碰撞。


三、GJK 核心算法

3.1 直觉

GJK 维护一个 simplex(在 2D 中最多 3 个顶点,在 3D 中最多 4 个顶点),这些顶点都是 C 中的点。算法逐步移动 simplex 使其"包围"或"逼近"原点。

每一步:

  1. 取当前 simplex 中离原点最近的点 p

  2. 用方向 d = -p(指向原点)调用 support,得到新顶点 w

  3. 如果 w · d ≤ 0(新点没有跨越原点方向)→ 原点在 C 外面,‖p‖ 就是最近距离。终止

  4. 把 w 加入 simplex

  5. 重新计算 simplex 中离原点最近的点,丢弃不再需要的顶点

  6. 返回步骤 1

关键不变量

每一轮 simplex 中离原点最近的点,是 C 中离原点最近的点的下界逼近

3.2 Simplex 的维数递进:Johnson 距离算法

对于 simplex 中离原点最近的点 p,我们需要确定 p 在 simplex 的哪个子集上(面、边、或顶点),并扔掉无关顶点。

0-simplex(一个点)

最近点就是那个点本身。

1-simplex(一条线段 AB)

求原点在线段上的投影。设参数 t ∈ [0,1]

复制代码
p = A + t(B - A)

p · (B - A)⊥ = 0,解出:

复制代码
t = -(A · (B - A)) / ‖B - A‖²

Clamp 到 [0,1]

  • t = 0 → 最近点是 A,丢弃 B

  • t = 1 → 最近点是 B,丢弃 A

  • 0 < t < 1 → 最近点在线段上

2-simplex(三角形 ABC)

Voronoi 区域 方法。三角形有三条边。每条边定义一个外侧半空间:

复制代码
如果原点在边 BC 的外侧

    → 丢弃 A(对角顶点),降为线段 BC,重新计算

如果原点在边 CA 的外侧

    → 丢弃 B

如果原点在边 AB 的外侧

    → 丢弃 C

如果三条边都不需要丢弃

    → 原点在三角形内部,最近点在三角形面上

"外侧"的判断方式:

复制代码
对边 BC(对顶点 A):

    取法线 n = (C - B) × ((A - B) × (C - B))  // 指向 A 的反方向

    如果 p · n < 0  →  p 在 BC 外侧  →  丢弃 A
3-simplex(四面体,仅 3D)

推广到 3D:对四面体的每个三角面,如果原点在该面的外侧(即 p 在该面法线的外侧),丢弃对面的顶点。

如果原点在所有面的内侧 → simplex 包含原点 → 相交,进入 EPA。

3.3 初始方向

第一个 support 方向通常取两个物体中心连线方向,或 (1, 0, 0)。选得不好只会多迭代几步,不影响正确性。

3.4 收敛判定

非相交情况

复制代码
如果 w · d ≤ 0

    → 原点在 C 外面

    → 返回距离 = ‖p‖, 最近点 = p

或者:

复制代码
如果 ‖p‖ 的变化 < ε

    → 收敛,同样返回距离

相交情况

复制代码
如果 simplex 包含原点(在 3D 中即四面体 4 个顶点且原点在内部)

    → 进入 EPA

3.5 数值稳定性问题

实际实现中常见的坑:

退化 simplex:三个点几乎共线 / 四个点几乎共面。面积/体积判断可能除零。加 epsilon 容差,退化时跳过。

方向退化p ≈ 0(原点在 simplex 面/边界上)。此时 d = -p 几乎是零向量。处理方法:检测 p 长度低于阈值则认为相交。

浮点误差累积:反复的 support → simplex → 投影 可能让 simplex 中的点出现微小不一致。处理方式是重新计算或回退。


四、EPA(Expanding Polytope Algorithm)

GJK 告诉你"相交了",但不告诉你:

  • 穿透了多深

  • 碰撞法线是哪个方向

  • 怎么把物体推开

这些是 EPA 的工作。

4.1 核心直觉

GJK 结束时留下一个包含原点的四面体。

EPA 把这个四面体逐步扩展成越来越大的凸多面体(polytope),每次往"离原点最近的面"的外侧加一个支撑点。当多面体足够逼近 C 的真实表面时,最近面到原点的距离就是穿透深度 ,面法线就是碰撞法线

4.2 关键几何量

polytope 上每个三角面定义一个到原点的距离:

复制代码
d_face = face_vertex · face_normal

其中 face_normal 是该面的指向原点方向的法线。

EPA 每轮找 d_face 最小的面------也就是离原点最近的面------来扩展。

4.3 EPA 迭代过程

初始化:把 GJK 的四面体做成初始 polytope。4 个三角面,法线全部指向四面体内部(原点方向)。

每轮迭代

  1. 从 polytope 的所有面中,选出 d_face 最小的面 F(离原点最近的面)

  2. 取 F 的面法线 n(指向原点方向),做 support:w = Support(A, B, n)

  3. 如果 w · n - d_face < ε

    • 当前面已经近乎是 C 的真实边界

    • 收敛 :穿透深度 = d_face,碰撞法线 = n

  4. 找到 polytope 中所有被 w "看到"的面(新点在面的外侧)

  5. 删除这些面

  6. 找到删除和保留之间的边界边(每条边界边只被一个保留面引用)

  7. 每条边界边和 w 构成新的三角面,加入 polytope

  8. 回到步骤 1

4.4 "看得到"的面

新顶点 w 在 polytope 外部。在它"视线"内的面需要删除。

判断:

复制代码
如果 (w - face_vertex) · face_normal > 0

    → 这个面被 w 看到

    → 删除

这和 incremental convex hull 构建算法(QuickHull)中的删面-补面操作完全一致。

4.5 补面

被删除的面和保留的面之间形成一条闭合的边界(一系列边)。每条边界边只连接一个保留面和一个被删除面。

对每条边界边 (v₁, v₂),和 w 构成新三角形:

复制代码
新面 = (v₁, v₂, w)

法线 = normalize((v₂ - v₁) × (w - v₁))

确保法线指向 polytope 内部(即原点方向)。法线方向判断:

复制代码
如果 (w - face_vertex) · new_normal > 0  →  翻转法线

4.6 收敛判定

EPA 收敛条件是:

复制代码
w · n - d_face < ε

含义是:新 support 点沿面法线方向没有超出当前面多远。面和 C 的真实边界已经足够接近了。

ε 通常取 1e-41e-6,取决于场景尺度和精度需求。

4.7 最大迭代次数

EPA 通常 2-20 次迭代就能收敛。但在极端退化情况下可能不收敛。实现中通常设上限 64-128,超过后取当前最优结果(最小 d_face 的面)。

4.8 退化情况处理

精度不够,面太小:法线不稳定。限制最大迭代次数,超限后返回当前最优。

法线退化到零:补面时检测三角形面积,退化则跳过。

原点在 polytope 表面上 :GJK 刚好检测到接触但没有穿透(擦边)。EPA 返回 d_face = 0 和最近的 simplex 面法线。


五、算法复杂度

GJK

项目 量级
每次迭代 O(1),取决于支撑函数实现
迭代次数(分离情况) 典型 3-10 次
迭代次数(相交情况) 典型 1-5 次到达四面体

EPA

项目 量级
每次迭代 O(k),k 是删除面数
总迭代次数 典型 2-20 次
polytope 面数 通常 ≤ 50 个
最大迭代限制 64-128

六、完整流程伪代码

text 复制代码
输入:凸体 A,凸体 B

输出:(intersecting, distance, normal, penetration_depth)



初始化:

    simplex = ∅

    取初始方向 d = A.center - B.center(或 (1,0,0))



┌─────────────────────────────────────────┐

│              GJK                         │

│                                          │

│ 循环:                                    │

│   1. w = Support(A, B, d)                │

│   2. 如果 w · d ≤ 0:                     │

│      返回 (false, ‖p‖, normalize(-p), 0)  │

│   3. 把 w 加入 simplex                    │

│   4. (p, simplex) = closest_to_origin(simplex) │

│   5. d = -p                              │

│   6. 如果 simplex 是四面体且包含原点:      │

│       进入 EPA                           │

│   7. 如果 ‖p‖ 变化 < ε:                  │

│       返回 (false, ‖p‖, normalize(-p), 0)  │

│                                          │

└─────────────────────────────────────────┘

              ↓ (相交)

┌─────────────────────────────────────────┐

│              EPA                         │

│                                          │

│ 1. polytope = GJK 的四面体 simplex       │

│    把 4 个三角面初始化(法线指向内部)    │

│                                          │

│ 循环 (最多 64 次):                       │

│   2. 找离原点最近的面 F:                  │

│       d = F.vertex · F.normal            │

│       n = F.normal                       │

│   3. w = Support(A, B, n)                │

│   4. 如果 w · n - d < ε:                 │

│        返回 (true, NaN, n, d)             │

│   5. 找到所有被 w "看到"的面 → 删除      │

│   6. 找到边界边                          │

│   7. 每条边界边 + w → 新三角形面         │

│   8. 如果 polytope 面数 > 128:           │

│        取当前最优面,返回                 │

│                                          │

│ 超限: 返回当前最优面的结果                │

└─────────────────────────────────────────┘

七、GJK / EPA 在 Minkowski Difference 中的可视化理解

把问题放在 Minkowski Difference 空间里就变得很直观:

复制代码
C = A ⊖ B



GJK 在做什么?

    在 C 中移动一个 simplex,每次往原点的方向扩展一个新顶点。

    最终要么 simplex 包住原点(相交),要么把 simplex 收缩到 C 上离原点最近的点(分离)。



EPA 在做什么?

    从包含原点的四面体出发,把它变大成多面体,

    逼近 C 的真实表面。离原点最近的面 → 穿透深度和方向。

这比直接思考两个物体的碰撞直观得多------因为整个问题变成了"原点到一个凸集的距离"这个标准几何问题。


八、需要 GJK 还是 GJK + EPA

场景 需要什么 说明
仅判断是否碰撞(trigger 检测) 仅 GJK 返回 bool 即可,不需要穿透/法线
求分离距离(不穿透) 仅 GJK 返回最近距离
需要穿透深度和碰撞法线(推开物体) GJK + EPA EPA 在 GJK 确认相交后运行
需要接触点和 contact manifold GJK + EPA + 接触生成 EPA 只给法线和深度

九、为什么主流物理引擎选 GJK + EPA

优势 说明
通用性强 任何凸体都行,只要提供支撑函数
复杂度低 每次迭代只做 1 次 support + O(1) 的 simplex 维护
精度高 EPA 收敛快,穿透方向准确
实现简洁 核心循环不超过 50 行代码
天然支持连续碰撞 可直接用于 TOI (Time of Impact) 搜索

与 Separating Axis Theorem (SAT) 相比:

  • SAT 在 Box-Box 上很快,但对凸网格需要遍历所有面,O(n²)

  • GJK 对网格也只需要 O(迭代次数 × n),通常快得多

  • SAT 在分离时天然给出分离轴,GJK 对分离情况也天然给出最近方向

  • PhysX 内部对常见形状混合使用两种方法以取长补短


十、工程实现注意事项

  1. support 函数是性能热点。对网格做缓存(比如空间哈希、AABB 树)可以大幅加速。

  2. GJK 不依赖物体是世界变换后的坐标系。可在局部空间做 Minkowski Difference,减少数值误差。

  3. 浮点 delta 需要根据场景尺度调。太大导致虚假碰撞,太小导致不收敛。

  4. EPA 初始化时的四面体法线方向必须指向多面体内部(即原点方向)。方向反了会导致 EPA 向错误方向膨胀。

  5. EPA 面数限制要严格。极端情况下的面爆炸会导致性能崩溃。

  6. 不要忽略 GJK 分离情况下的最近点。它可以用于 margin 判断、提前碰撞预测等。

  7. Simplex 退化时重新取一个随机方向重启 GJK是一个有效的兜底策略。


总结

问题 答案
GJK 解决了什么 判断两个凸体是否相交;不相交时求最近距离
关键抽象 Minkowski Difference:相交 ⇔ 原点在其中
计算手段 支撑函数按方向采样极值点,逐步构造 simplex
GJK 核心循环 取最近点 → 沿 -p 方向 support → Johnson 收缩判断
EPA 解决了什么 已知相交,求穿透深度和碰撞法线
EPA 核心循环 找最近面 → 沿法线 support → 删面补面 → 收敛
精度保障 ε 容差、最大迭代次数、退化检测
非凸体处理 需要先做凸分解 或 SDF
相关推荐
木井巳1 小时前
【DFS解决floodfill算法】岛屿数量
java·算法·leetcode·深度优先
好评笔记2 小时前
深度学习面试八股——循环神经网络RNN
人工智能·rnn·深度学习·神经网络·算法·机器学习·aigc
凯瑟琳.奥古斯特2 小时前
力扣1003题C++解法详解
开发语言·c++·算法·leetcode·职场和发展
计算机安禾2 小时前
【算法分析与设计】第48篇:流算法与数据概要技术
java·服务器·网络·数据库·算法
hunterkkk(c++)2 小时前
SPFA最短路径算法(c++)
java·c++·算法
weixin_446260852 小时前
HANDOFF:基于蒸馏互补教师的人形机器人任务空间整体控制
人工智能·算法·机器人
商业模式源码开发3 小时前
知识付费推三返一模式详解:规则设计、分红算法与合规架构
算法·架构·推三返一
fengfuyao9853 小时前
基于MATLAB的HHT变换完整实现(含EMD分解与三维时频谱生成)
开发语言·算法·matlab
剑挑星河月3 小时前
98.验证二叉搜索树
java·算法·leetcode