GJK算法实现细节

GJK 实现细节

本文档基于 GJKCollisionDetector.csSimplex.csMyCollider.cs 及子类源码,参考 PhysX 4.1 GuGJK.h 实现,按算法执行流程逐步梳理每一步的细节。


目录

  1. 整体调用链路
  2. [Margin 机制(表皮机制)](#Margin 机制(表皮机制))
  3. [Support 支撑函数调用链](#Support 支撑函数调用链)
  4. [GJK 主循环详解](#GJK 主循环详解)
  5. 单纯形更新详解(Simplex.DoSimplex)
  6. 单纯形收敛后还原最近点(GetClosestPoint)
  7. [GJK 的四种退出路径总结](#GJK 的四种退出路径总结)

1. 整体调用链路

1.1 场景入口

CollisionTest.Awake() 自动发现场景中所有 MyCollider,注册到 CollisionManager 并构建八叉树:

csharp 复制代码
// CollisionTest.cs 第 9-11 行
private void Awake()
{
    MyCollider[] colliders = FindObjectsOfType<MyCollider>();
    CollisionManager.Instance.InsertCollider(new List<MyCollider>(colliders));
}

1.2 每帧更新顺序

复制代码
CollisionManager.FixedUpdate()
  ├── 1. ColliderPosUpdate()          // 遍历所有 MyCollider → 更新 AABB
  ├── 2. OctreeUpdate()              // 检查碰撞体是否需要换八叉树节点
  └── 3. CollisionDetectorUpdate()   // 碰撞检测调度
          ├── 八叉树 Query   → 获取同节点内的潜在碰撞对
          ├── AABB 粗筛       → GJKCollisionDetector.AABBCollisionCheck()
          ├── GJK 精确检测    → GJKCollisionDetector.GJKEPA() / GJKCollisionCheck()
          └── 对比 _prevState ↔ _currState → 派发 Enter/Stay/Exit 事件
csharp 复制代码
// CollisionManager.cs 第 151-156 行
private void FixedUpdate()
{
    ColliderPosUpdate();
    OctreeUpdate();
    CollisionDetectorUpdate();
}

1.3 碰撞检测核心调度

从八叉树拿到潜在碰撞对后,先 AABB 粗筛,再 GJK 精确检测:

csharp 复制代码
// CollisionManager.cs 第 220-255 行(CollisionDetectorUpdate 第三部分节选)
foreach (var colA in _colliders)
{
    _octree.Query(colA, _queryTemp);   // 八叉树粗筛:只查同节点的碰撞体

    foreach (var colB in _queryTemp)
    {
        if (colA == colB) continue;
        var pair = MakePair(colA, colB);
        if (_checkedPairs.Contains(pair)) continue;  // 去重
        _checkedPairs.Add(pair);

        // 第一层:AABB 粗筛
        if (GJKCollisionDetector.AABBCollisionCheck(colA.AABB, colB.AABB))
        {
            // 第二层:GJK 精确检测
            GjkStatus status = GJKCollisionDetector.GJKEPA(colA, colB,
                out GjkOutput output, contactDistance);
            // ... 根据 status 处理 Enter/Stay/Exit 事件
        }
    }
}

筛选层级:八叉树空间划分 → AABB 重叠检测 → GJK 精确检测

1.4 GJK 对外接口

GJKCollisionDetector 是静态工具类,提供三层接口:

接口 用途 返回值
GJKCollisionCheck(a, b) 快速判断是否碰撞 bool(true=重叠)
GJKCheckCollision(a, b, out distance) 判断碰撞 + 分离距离 bool + float distance
GJKClosest(a, b, out ...) 完整查询:最近点/法线/距离 GjkStatus

三个接口最终都调用同一个 Gjk() 私有方法,区别在于参数和输出的取舍。

csharp 复制代码
// GJKCollisionDetector.cs 第 62-73 行 --- 快速碰撞检测
public static bool GJKCollisionCheck(MyCollider colliderA, MyCollider colliderB)
{
    GjkStatus status = Gjk(colliderA, colliderB, InitialDir(colliderA, colliderB), 0f,
        out _, out _, out _, out _);
    return status == GjkStatus.Contact;
}

// GJKCollisionDetector.cs 第 78-92 行 --- 带距离的碰撞检测
public static bool GJKCheckCollision(MyCollider colliderA, MyCollider colliderB,
    out float distance)
{
    // 用极大 contactDist 强制 GJK 收敛,从而总能拿到最近距离
    GjkStatus status = Gjk(colliderA, colliderB, InitialDir(colliderA, colliderB),
        LARGE_CONTACT_DIST, out _, out _, out _, out distance);
    return status == GjkStatus.Contact;
}

// GJKCollisionDetector.cs 第 99-113 行 --- 完整查询
public static GjkStatus GJKClosest(MyCollider colliderA, MyCollider colliderB,
    out Vector3 closestA, out Vector3 closestB, out Vector3 normal,
    out float distance, float contactDist = LARGE_CONTACT_DIST)
{
    return Gjk(colliderA, colliderB, InitialDir(colliderA, colliderB), contactDist,
        out closestA, out closestB, out normal, out distance);
}

1.5 GjkStatus 状态枚举

csharp 复制代码
// GJKCollisionDetector.cs 第 7-22 行
public enum GjkStatus
{
    NonIntersect,  // 两形状分离,且间隙大于 contactDist
    Close,         // 两形状分离但在 contactDist 内,返回最近点/距离信息
    Contact,       // 两形状重叠
    // 以下为 EPA 使用,本文不展开
    EpaContact, EpaDegenerate, EpaFail
}

2. Margin 机制(表皮机制)

参考 PhysX,所有形状有一层"表皮"(margin)。GJK 对不同形状采取不同策略:

类型 Margin IsMarginEqRadius GJK 做法
Box MinHalfExtent × 0.15 ❌ false core 即完整角点,margin 仅影响终止 eps 和退化容差
Sphere Radius ✅ true core 收缩为点,GJK 收敛后最近点沿法线加回 margin
Capsule Radius ✅ true core 收缩为线段,GJK 收敛后最近点沿法线加回 margin
csharp 复制代码
// MyBoxCollider.cs 第 71-76 行
private float MinHalfExtent => Mathf.Min(Size.x, Mathf.Min(Size.y, Size.z)) * 0.5f;
public override float Margin => MinHalfExtent * 0.15f;
public override float MinMargin => MinHalfExtent * 0.05f;
public override bool MarginIsRadius => false;

// MySphereCollider.cs 第 47-49 行
public override float Margin => Radius;
public override float MinMargin => Radius;
public override bool MarginIsRadius => true;

// MyCapsuleCollider.cs 第 74-76 行
public override float Margin => Radius;
public override float MinMargin => Radius;
public override bool MarginIsRadius => true;

2.1 为什么区分 quadratic 与非 quadratic

  • Quadratic 形状(球、胶囊) :GJK 对凸多面体才能正确收敛。球/胶囊是曲面体,所以将它们的 core 收缩为点/线段,radius 作为 margin。GJK 在 core 形状(点/线段)上迭代,收敛后再把最近点沿法线"推回"真实表面,距离减去 sumMargin

  • 非 quadratic 形状(Box):core 就是完整角点,margin 不收缩形状,仅用于终止 eps 和退化容差。

csharp 复制代码
// GJKCollisionDetector.cs 第 194-200 行(Gjk 方法内的初始化)
bool aQuadratic = a.MarginIsRadius;
bool bQuadratic = b.MarginIsRadius;

// sumMargin 只累加 quadratic 形状的 margin
float sumMargin = (aQuadratic ? a.Margin : zero) + (bQuadratic ? b.Margin : zero);
float separatingDist = sumMargin + contactDist;

2.2 Margin 如何影响各退出路径

退出路径 Margin 的作用
Case 1 (NonIntersect) 分离判据 separatingDist = sumMargin + contactDist 包含 margin
Case 2 (Close) 最近点从 core 沿法线推回 margin 距离;表面距离 dist - sumMargin
Case 3 (Contact) 不需要 margin 修正(已确认重叠)
Case 4 (退化) acceptanceDistsumMargin0.2 × minMargin

3. Support 支撑函数调用链

GJK 每轮迭代需要获取闵可夫斯基差(Minkowski Difference)上的支撑点:

复制代码
support = A.SupportCore(-closest) - B.SupportCore(closest)

其中 closest 是原点到当前单纯形的最近点,-closest 是从最近点指向原点的方向。

3.1 调用链

复制代码
GJK 传入世界方向 worldDir
  → MyCollider.SupportCore(worldDir)      // 模板方法
    → InverseRotateYXZ(worldDir, RotEuler) // 世界方向 → 本地方向(旋转矩阵转置)
    → 本地几何计算                        // 按形状类型取极值点
    → RotateYXZ(localPt, RotEuler)         // 本地支撑点 → 世界支撑点
    → Center + worldSupport               // 返回世界空间支撑点

3.2 旋转约定

项目使用旋转用欧拉角 YXZ 顺序 + 手动旋转矩阵实现:

csharp 复制代码
// MyCollider.cs 第 114-130 行 --- 正向旋转(本地 → 世界)
protected Vector3 RotateYXZ(Vector3 v, Vector3 rotEuler)
{
    float rx = rotEuler.x * Mathf.Deg2Rad;
    float ry = rotEuler.y * Mathf.Deg2Rad;
    float rz = rotEuler.z * Mathf.Deg2Rad;

    float cx = Mathf.Cos(rx); float sx = Mathf.Sin(rx);
    float cy = Mathf.Cos(ry); float sy = Mathf.Sin(ry);
    float cz = Mathf.Cos(rz); float sz = Mathf.Sin(rz);

    // YXZ 顺序旋转矩阵 R
    float x = v.x * (cy * cz + sx * sy * sz) + v.y * (cz * sx * sy - cy * sz) + v.z * (cx * sy);
    float y = v.x * (cx * sz)                     + v.y * (cx * cz)                     + v.z * (-sx);
    float z = v.x * (cy * sx * sz - cz * sy)      + v.y * (sy * sz + cy * cz * sx)      + v.z * (cx * cy);

    return new Vector3(x, y, z);
}

// MyCollider.cs 第 139-155 行 --- 逆向旋转(世界 → 本地),R 的转置 Rᵀ
protected Vector3 InverseRotateYXZ(Vector3 v, Vector3 rotEuler)
{
    // 同样的 sin/cos 预计算 ...
    // Rᵀ 矩阵
    float x = v.x * (cy * cz + sx * sy * sz) + v.y * (cx * sz) + v.z * (cy * sx * sz - cz * sy);
    float y = v.x * (cz * sx * sy - cy * sz) + v.y * (cx * cz) + v.z * (sy * sz + cy * cz * sx);
    float z = v.x * (cx * sy)                  + v.y * (-sx)     + v.z * (cx * cy);

    return new Vector3(x, y, z);
}

设计要点 :三角函数只在 UpdatePos 中调用;SupportCore 热路径只需点积和数乘,零分配。

3.3 Box 的 SupportCore

Box 是非 quadratic 形状,core 即完整角点。按方向各分量的符号取对应顶点:

csharp 复制代码
// MyBoxCollider.cs 第 51-68 行
public override Vector3 SupportCore(Vector3 dir)
{
    // 世界方向 → 本地方向
    Vector3 localDir = InverseRotateYXZ(dir, RotEuler);

    // 按方向分量符号取 ±halfSize 顶点
    Vector3 halfSize = Size * 0.5f;
    Vector3 localSupport = new Vector3(
        Mathf.Sign(localDir.x) * halfSize.x,
        Mathf.Sign(localDir.y) * halfSize.y,
        Mathf.Sign(localDir.z) * halfSize.z
    );

    // 本地角点 → 世界角点
    Vector3 worldSupport = RotateYXZ(localSupport, RotEuler);
    return Center + worldSupport;
}

3.4 Sphere 的 SupportCore

球是 quadratic 形状,core 收缩为球心(点):

csharp 复制代码
// MySphereCollider.cs 第 40-44 行
public override Vector3 SupportCore(Vector3 dir)
{
    // 球的 core 是球心,半径作为 margin 在 GJK 中处理
    return Center;
}

3.5 Capsule 的 SupportCore

胶囊是 quadratic 形状,core 收缩为线段。取沿方向投影更远的端点:

csharp 复制代码
// MyCapsuleCollider.cs 第 65-71 行
public override Vector3 SupportCore(Vector3 dir)
{
    // 胶囊的 core 是线段 p0-p1,取沿 dir 投影更远的端点
    float dotA = Vector3.Dot(_pointA, dir);
    float dotB = Vector3.Dot(_pointB, dir);
    return dotA > dotB ? _pointA : _pointB;
}

4. GJK 主循环

4.1 核心常量

csharp 复制代码
// GJKCollisionDetector.cs 第 33-43 行
private const int MAX_ITERATIONS = 32;          // 最大迭代次数
private const float EPS_REL = 0.000225f;        // 精度阈值 1.5% 的平方
private const float LARGE_CONTACT_DIST = 1e18f;  // 距离查询用的极大 contactDist

// 静态复用,热路径零分配
private static readonly Simplex _simplex = new Simplex();

4.2 初始搜索方向

取两 AABB 中心连线作为初始方向,这是让 GJK 收敛最快的经典选择。若两点重合(零向量),退化为 X 轴:

csharp 复制代码
// GJKCollisionDetector.cs 第 165-168 行
private static Vector3 InitialDir(MyCollider a, MyCollider b)
{
    return b.AABB.Center - a.AABB.Center;
}

4.3 Gjk() 方法逐步分解

完整方法签名:

csharp 复制代码
// GJKCollisionDetector.cs 第 173-175 行
private static GjkStatus Gjk(MyCollider a, MyCollider b, Vector3 initialSearchDir,
    float contactDist, out Vector3 closestA, out Vector3 closestB,
    out Vector3 normal, out float distance)

步骤 0:初始化
csharp 复制代码
// GJKCollisionDetector.cs 第 176-208 行
closestA = closestB = normal = Vector3.zero;
distance = 0f;

Simplex s = _simplex;
s.Reset();                                    // Size = 0,清空单纯形

const float zero = 0f;

// 初始搜索方向:退化则取 X 轴
Vector3 closest = Vector3.Dot(initialSearchDir, initialSearchDir) > zero
    ? initialSearchDir : Vector3.right;
Vector3 v = closest.normalized;               // 搜索方向的单位向量

// eps:两形状重叠的终止判据,取最小 margin 的 10%,下限 1e-6
float minMargin = Mathf.Min(a.MinMargin, b.MinMargin);
float eps = Mathf.Max(1e-6f, minMargin * 0.1f);

float relDif = 1f - EPS_REL;                  // ≈ 0.999775

// 识别 quadratic 形状
bool aQuadratic = a.MarginIsRadius;
bool bQuadratic = b.MarginIsRadius;
float sumMargin = (aQuadratic ? a.Margin : zero) + (bQuadratic ? b.Margin : zero);
float separatingDist = sumMargin + contactDist;  // 分离判据阈值

float dist = float.MaxValue;                  // 原点到单纯形的最近距离
float prevDist = dist;
Vector3 prevDir = v;

bool notDegenerated = true;                   // 迭代是否没有退化
bool reachedOrigin = false;                   // 单纯形是否包住了原点
int iter = 0;

关键变量含义

变量 含义
closest 原点到单纯形最近点的向量(闵可夫斯基差空间)
v 搜索方向单位向量,与 closest 同向
dist `
eps 距离终止阈值,dist ≤ eps 认为是重叠
separatingDist 分离判据阈值 = sumMargin + contactDist
relDif 收敛相对判据 1 - EPS_REL ≈ 0.999775
notDegenerated 跟踪距离是否在缩小(prevDist > dist
reachedOrigin 退出循环后判断是否因到达原点而终止

步骤 1:取闵可夫斯基支撑点
csharp 复制代码
// GJKCollisionDetector.cs 第 214-218 行(循环体内)
// -closest 是最近点指向原点的向量
// 在 core 形状上取支撑点
Vector3 supportA = a.SupportCore(-closest);
Vector3 supportB = b.SupportCore(closest);
Vector3 support = supportA - supportB;   // 闵可夫斯基差点
步骤 2:分离轴判定(Case 1 → NonIntersect)
csharp 复制代码
// GJKCollisionDetector.cs 第 220-228 行
float signDist = Vector3.Dot(v, support);

// 找到分离轴,且间隙超过 separatingDist → 两形状确定不相交
if (signDist > separatingDist)
{
    Debug.Log($"[GJKCase1] {a.name} vs {b.name} → NonIntersect, 迭代 {iter + 1} 次");
    return GjkStatus.NonIntersect;
}

判定原理

signDist = v · support 是新支撑点在搜索方向 v 上的投影。

  • vclosest 同向(v = closest / dist),都指向原点
  • signDist > separatingDist,说明支撑点已越过原点足够远(超出 margin + contactDist)
  • 这意味着两凸体沿方向 v 存在足够大的分离间隙
  • 找到了一个分离轴 → 两形状确定不相交,立即返回

步骤 3:收敛判定(Case 2 → Close)
csharp 复制代码
// GJKCollisionDetector.cs 第 230-240 行
// 支撑点投影已逼近当前最近距离,无法再前进
if (signDist > sumMargin && signDist > relDif * dist)
{
    Vector3 n = -v;   // 法线 = 由 A 指向 B
    s.GetClosestPoint(closest, out Vector3 closA, out Vector3 closB);
    // 球/胶囊:把见证点从 core 推回真实表面
    closestA = aQuadratic ? closA + n * a.Margin : closA;
    closestB = bQuadratic ? closB - n * b.Margin : closB;
    normal = n;
    distance = Mathf.Max(zero, dist - sumMargin);
    Debug.Log($"[GJKCase2] {a.name} vs {b.name} → Close(dist={distance:F4}), 迭代 {iter + 1} 次");
    return GjkStatus.Close;
}

判定原理

条件 signDist > relDif × dist(即 signDist > 0.999775 × dist):

  • 新支撑点到原点的投影 ≈ 当前原点到单纯形的距离
  • 说明新支撑点几乎就在当前最近距离的平面上,无法再显著缩小距离
  • GJK 已收敛到稳定的最近距离

收敛后的处理

  1. 法线 n = -vvclosest 同向指向原点 → -v 由 A 指向 B
  2. 从单纯形取 core 上的最近点 (closA, closB)
  3. 对于 quadratic 形状,把 core 最近点沿法线推回真实表面
  4. 表面距离 = max(0, dist - sumMargin)

步骤 4:扩展单纯形 + 计算最近点
csharp 复制代码
// GJKCollisionDetector.cs 第 242-248 行
s.Add(support, supportA, supportB);

// 计算原点到单纯形的最近点,同时缩减单纯形(移除无用顶点)
closest = s.DoSimplex(support);

dist = closest.magnitude;
// dist > eps 时更新方向;否则保持上一轮方向继续
v = dist > eps ? closest / dist : v;

关键操作

  • s.Add(support, supportA, supportB):将新闵可夫斯基差点 + 对应的 A/B 支撑点加入单纯形
  • s.DoSimplex(support):求原点到当前单纯形的最近点,同时剔除不再贡献的顶点(详见第 5 章)
  • 方向更新:dist > epsv = closest/dist(单位化);dist ≤ epsv 保持上一轮方向不变

步骤 5:终止检查
csharp 复制代码
// GJKCollisionDetector.cs 第 250-257 行
notDegenerated = prevDist > dist;  // 本迭代是否仍在缩小距离
bool notTerminated = dist > eps && notDegenerated;
if (!notTerminated)
{
    // notDegenerated==true 表示因 dist<=eps 退出 → 原点已到达
    reachedOrigin = notDegenerated;
    break;
}

终止条件分析

情况 dist ≤ eps notDegenerated reachedOrigin 含义
到达原点 true 距离收缩到 eps 以内 → 重叠
退化 false 距离不再缩小 → 迭代终止
继续 --- 循环继续

步骤 6:循环后处理

循环结束有两种可能:到达原点(重叠)或退化退出。

Case 3:单纯形包住原点 → Contact

csharp 复制代码
// GJKCollisionDetector.cs 第 261-267 行
if (reachedOrigin)
{
    distance = zero;
    normal = -prevDir;
    Debug.Log($"[GJKCase3] {a.name} vs {b.name} → Contact, 迭代 {iter} 次(单纯形包住原点)");
    return GjkStatus.Contact;
}

reachedOrigin = true 意味着循环因 dist ≤ eps 退出(且未退化)→ 单纯形已逼近原点 → 两形状重叠

Case 4:退化 → 近似判定

csharp 复制代码
// GJKCollisionDetector.cs 第 269-283 行
float acceptanceMargin = 0.2f * Mathf.Min(a.Margin, b.Margin);
float acceptanceDist = sumMargin > zero ? sumMargin : acceptanceMargin;

Vector3 nn = -v;
s.GetClosestPoint(closest, out Vector3 pClosA, out Vector3 pClosB);
closestA = aQuadratic ? pClosA + nn * a.Margin : pClosA;
closestB = bQuadratic ? pClosB - nn * b.Margin : pClosB;
normal = nn;

float finalDist = Mathf.Max(zero, dist - sumMargin);
distance = finalDist;
GjkStatus state = finalDist > acceptanceDist ? GjkStatus.Close : GjkStatus.Contact;
Debug.Log($"[GJKCase4] {a.name} vs {b.name} → {state}, 迭代 {iter} 次(退化/超限)");
return state;

退化时的判定逻辑

  • 有 quadratic 形状时:acceptanceDist = sumMargin(更宽松)
  • 无 quadratic 形状时:acceptanceDist = 0.2 × minMargin
  • 最终表面距离 finalDist > acceptanceDistClose(分离);否则 → Contact(视作重叠)

5. 单纯形更新(Simplex.DoSimplex)

单纯形(Simplex)是 GJK 维护的核心数据结构,表示闵可夫斯基差空间中一组点构成的凸包(最多 4 个点)。每轮迭代加入新支撑点后,需要计算原点到当前单纯形的最近点,并剔除不再需要的顶点。

5.1 数据结构

csharp 复制代码
// Simplex.cs 第 14-17 行
public readonly Vector3[] Q = new Vector3[4];  // 闵可夫斯基差点
public readonly Vector3[] A = new Vector3[4];  // 凸体 A 的 core 支撑点
public readonly Vector3[] B = new Vector3[4];  // 凸体 B 的 core 支撑点
public int Size;  // 当前顶点数 (0~4)

热路径零分配 :固定长度数组,不使用 List<T>

5.2 添加顶点

csharp 复制代码
// Simplex.cs 第 26-32 行
public void Add(Vector3 q, Vector3 sa, Vector3 sb)
{
    Q[Size] = q;
    A[Size] = sa;
    B[Size] = sb;
    Size++;
}

5.3 DoSimplex 按顶点数分发

csharp 复制代码
// Simplex.cs 第 37-47 行
public Vector3 DoSimplex(Vector3 support)
{
    switch (Size)
    {
        case 1: return support;                     // 单点:最近点就是该点
        case 2: return ClosestPtPointSegment();     // 线段
        case 3: return ClosestPtPointTriangle();    // 三角形
        case 4: return ClosestPtPointTetrahedron(); // 四面体
        default: return support;
    }
}

5.4 线段:原点到 Q0-Q1 的最近点

csharp 复制代码
// Simplex.cs 第 84-102 行
private Vector3 ClosestPtPointSegment()
{
    Vector3 a = Q[0];
    Vector3 b = Q[1];
    Vector3 ab = b - a;
    float denom = Vector3.Dot(ab, ab);     // |ab|²
    float nom = Vector3.Dot(-a, ab);       // (-a)·ab = 投影系数分子

    // 两点几乎重合,退化为单点
    if (denom <= DEGEN_EPS)                // DEGEN_EPS = 1e-9
    {
        Size = 1;
        return Q[0];
    }

    // 投影系数夹到 [0,1],保证最近点在线段上而非延长线
    float t = Mathf.Clamp01(nom / denom);
    return a + ab * t;
}

原理

复制代码
原点 O 到线段 ab 的最近点 P = a + t·ab

求投影系数:t = (O→a · ab) / |ab|²
由于 O 是原点,O→a = a,而 (-a)·ab = 使 t 取 a→b 方向的投影
实际:t = (-a · ab) / |ab|²

clamp t 到 [0,1]:
  t ≤ 0 → 最近点是 a
  t ≥ 1 → 最近点是 b
  0<t<1 → 最近点在线段内部

退化处理:若 |ab|² ≤ 1e-9,两点几乎重合 → Size=1,返回 a

5.5 三角形:原点到 Q0-Q1-Q2 的最近点

csharp 复制代码
// Simplex.cs 第 108-147 行
private Vector3 ClosestPtPointTriangle()
{
    Size = 3;
    Vector3 a = Q[0], b = Q[1], c = Q[2];

    Vector3 ab = b - a;
    Vector3 ac = c - a;
    Vector3 signArea = Vector3.Cross(ab, ac);       // 2×面积向量
    float area = Vector3.Dot(signArea, signArea);    // 4×面积²

    // 三点共线,退化成线段
    if (area <= DEGEN_EPS)
    {
        Size = 2;
        return ClosestPtPointSegment();
    }

    _indices[0] = 0; _indices[1] = 1; _indices[2] = 2;
    ClosestPtPointTriangleBaryCentric(a, b, c, _indices, out int featSize,
        out Vector3 closestPt);

    // 最近特征是边或顶点时,按 _indices 缩减重排 Q/A/B
    if (featSize != 3)
    {
        int i0 = _indices[0], i1 = _indices[1];
        Q[0] = Q[i0]; Q[1] = Q[i1];
        A[0] = A[i0]; A[1] = A[i1];
        B[0] = B[i0]; B[1] = B[i1];
        Size = featSize;
    }

    return closestPt;
}
5.5.1 三角形重心坐标 Voronoi 区域判定

这是核心算法------通过重心坐标 + 边投影,判断原点的投影落在三角形哪个 Voronoi 区域:

csharp 复制代码
// Simplex.cs 第 156-175 行
private float ClosestPtPointTriangleBaryCentric(Vector3 a, Vector3 b, Vector3 c,
    int[] indices, out int size, out Vector3 closestPt)
{
    size = 3;

    Vector3 ab = b - a;
    Vector3 ac = c - a;
    Vector3 n = Vector3.Cross(ab, ac);          // 三角形法线(2×面积向量)
    float nn = Vector3.Dot(n, n);              // 4×面积²
    if (nn < DEGEN_EPS) { /* 退化返回 */ }

    // 三个重心权重,符号表示原点投影在三角形的哪一侧
    float va = Vector3.Dot(n, Vector3.Cross(b, c));  // 顶点 A 的权重
    float vb = Vector3.Dot(n, Vector3.Cross(c, a));  // 顶点 B 的权重
    float vc = Vector3.Dot(n, Vector3.Cross(a, b));  // 顶点 C 的权重

重心坐标 (va, vb, vc) 是原点 O 在三角形 ABC 上的未归一化重心坐标。va ≥ 0 && vb ≥ 0 && vc ≥ 0 表示原点在三角形面内的投影落在三角形内部。

然后计算一系列点积用于判断落在哪条边/顶点的 Voronoi 区域:

csharp 复制代码
    // Simplex.cs 第 188-198 行
    Vector3 ap = -a, bp = -b, cp = -c;     // 各顶点指向原点的向量
    float d1 = Vector3.Dot(ab, ap);         // A 处 ab 边投影
    float d2 = Vector3.Dot(ac, ap);         // A 处 ac 边投影
    float d3 = Vector3.Dot(ab, bp);         // B 处 ab 边投影
    float d4 = Vector3.Dot(ac, bp);         // B 处 ac 边投影
    float d5 = Vector3.Dot(ab, cp);         // C 处 ab 边投影
    float d6 = Vector3.Dot(ac, cp);         // C 处 ac 边投影
    float unom = d4 - d3;                    // BC 边投影参数分子
    float udenom = d5 - d6;                  // BC 边投影参数分母
5.5.2 七种 Voronoi 区域判定
区域 条件 最近特征 最近点计算
❶ 面内 va≥0 && vb≥0 && vc≥0 三角形面 原点在法线 n 上的投影(垂足)
❷ AB 边 vc≤0 && d1≥0 && d3≤0 AB 边 a + ab × d1/(d1-d3)
❸ BC 边 va≤0 && d4≥d3 && d5≥d6 BC 边 b + bc × unom/(unom+udenom)
❹ AC 边 vb≤0 && d2≥0 && d6≤0 AC 边 a + ac × d2/(d2-d6)
❺ 顶点 A d1≤0 && d2≤0 顶点 A a
❻ 顶点 B d3≥0 && d3≥d4 顶点 B b
❼ 顶点 C 其余 顶点 C c

区域 ❶ 面内的实现

csharp 复制代码
    // Simplex.cs 第 178-185 行
    if (va >= 0f && vb >= 0f && vc >= 0f)
    {
        float t = Vector3.Dot(n, a) / nn;   // 原点到三角形平面的投影参数
        Vector3 q = n * t;                   // 最近点 = 法线方向的投影(垂足)
        closestPt = q;
        size = 3;                            // 三角形完整保留
        return Vector3.Dot(q, q);            // 返回距离平方
    }

区域 ❷ AB 边的实现

csharp 复制代码
    // Simplex.cs 第 201-209 行
    if (vc <= 0f && d1 >= 0f && d3 <= 0f)
    {
        float t = SafeDiv(d1, d1 - d3);      // 投影系数
        Vector3 q = a + ab * t;
        closestPt = q;
        return Vector3.Dot(q, q);
    }

区域 ❸ BC 边的实现 (注意 _indices 重排,把 B 和 C 的索引放到前两位):

csharp 复制代码
    // Simplex.cs 第 212-224 行
    if (va <= 0f && d4 >= d3 && d5 >= d6)
    {
        Vector3 bc = c - b;
        float t = SafeDiv(unom, unom + udenom);
        indices[0] = indices[1];              // 前移:B 的索引 → [0]
        indices[1] = indices[2];              // 前移:C 的索引 → [1]
        Vector3 q = b + bc * t;
        closestPt = q;
        return Vector3.Dot(q, q);
    }

区域 ❹ AC 边的实现

csharp 复制代码
    // Simplex.cs 第 227-234 行
    if (vb <= 0f && d2 >= 0f && d6 <= 0f)
    {
        float t = SafeDiv(d2, d2 - d6);
        indices[1] = indices[2];              // C 的索引 → [1](A 的索引保持在 [0])
        Vector3 q = a + ac * t;
        closestPt = q;
        return Vector3.Dot(q, q);
    }

区域 ❺❻❼ 顶点的实现

csharp 复制代码
    // Simplex.cs 第 237-258 行
    size = 1;

    // 顶点 A
    if (d1 <= 0f && d2 <= 0f)
    {
        closestPt = a;
        return Vector3.Dot(a, a);
    }

    // 顶点 B
    if (d3 >= 0f && d3 >= d4)
    {
        indices[0] = indices[1];              // B 的索引 → [0]
        closestPt = b;
        return Vector3.Dot(b, b);
    }

    // 顶点 C(兜底)
    indices[0] = indices[2];                  // C 的索引 → [0]
    closestPt = c;
    return Vector3.Dot(c, c);

当特征为边或顶点时 ,调用方重排 Q/A/B 数组并缩减 Size

csharp 复制代码
    // 回到 ClosestPtPointTriangle 中:
    if (featSize != 3)
    {
        // 按 _indices 把选中的顶点重排到数组前 featSize 个位置
        int i0 = _indices[0], i1 = _indices[1];
        Q[0] = Q[i0]; Q[1] = Q[i1];
        A[0] = A[i0]; A[1] = A[i1];
        B[0] = B[i0]; B[1] = B[i1];
        Size = featSize;  // 收缩单纯形
    }

5.6 四面体:原点到 Q0-Q1-Q2-Q3 的最近点

csharp 复制代码
// Simplex.cs 第 264-313 行
private Vector3 ClosestPtPointTetrahedron()
{
    Vector3 a = Q[0], b = Q[1], c = Q[2], d = Q[3];

    // 退化检测 1: 前三个顶点共面
    Vector3 nRaw = Vector3.Cross(b - a, c - a);
    float nLen = nRaw.magnitude;
    if (nLen <= DEGEN_EPS)
    {
        Size = 3;
        return ClosestPtPointTriangle();
    }
    // 退化检测 2: 第四个顶点在前三个顶点的平面上
    Vector3 n = nRaw / nLen;
    float signDist = Vector3.Dot(n, d - a);
    if (Mathf.Abs(signDist) < TETRA_PLANE_EPS)   // TETRA_PLANE_EPS = 1e-4
    {
        Size = 3;
        return ClosestPtPointTriangle();
    }

    // 判断原点在四面体四个面的内/外侧
    PointOutsideOfPlane4(a, b, c, d, out bool o0, out bool o1, out bool o2, out bool o3);

    // ★ 核心判定:原点在所有面内侧 → 包住原点 → 两形状重叠!
    if (!o0 && !o1 && !o2 && !o3)
    {
        return Vector3.zero;  // 返回零向量,最近距离 = 0
    }

    // 否则:在所有"外侧"的面里取离原点最近的那个三角形
    _indices[0] = 0; _indices[1] = 1; _indices[2] = 2;
    Vector3 closest = GetClosestPtPointTriangle(o0, o1, o2, o3, _indices, out int featSize);
    Size = featSize;

    // 按选中的三角形索引重排 Q/A/B
    int i0 = _indices[0], i1 = _indices[1], i2 = _indices[2];
    Q[0] = Q[i0]; Q[1] = Q[i1]; Q[2] = Q[i2];
    A[0] = A[i0]; A[1] = A[i1]; A[2] = A[i2];
    B[0] = B[i0]; B[1] = B[i1]; B[2] = B[i2];

    return closest;
}
5.6.1 PointOutsideOfPlane4:判断原点在四面体各面的内外
csharp 复制代码
// Simplex.cs 第 371-401 行
private void PointOutsideOfPlane4(Vector3 a, Vector3 b, Vector3 c, Vector3 d,
    out bool o0, out bool o1, out bool o2, out bool o3)
{
    Vector3 ab = b - a, ac = c - a, ad = d - a;
    Vector3 bd = d - b, bc = c - b;

    // 四个面的法线(未归一化,方向朝外,由右手定则确定)
    Vector3 v0 = Vector3.Cross(ab, ac);   // 面 ABC 的法线
    Vector3 v1 = Vector3.Cross(ac, ad);   // 面 ACD 的法线
    Vector3 v2 = Vector3.Cross(ad, ab);   // 面 ABD 的法线
    Vector3 v3 = Vector3.Cross(bd, bc);   // 面 BCD 的法线

    // 计算每个面上"原点"与"对面顶点"相对法线的投影符号
    float signa0 = Vector3.Dot(v0, a);    // 原点在面 ABC 的投影符号
    float signd0 = Vector3.Dot(v0, d);    // 对顶点 D 在面 ABC 的投影符号

    float signa1 = Vector3.Dot(v1, a);    // 原点在面 ACD
    float signd1 = Vector3.Dot(v1, b);    // 对顶点 B

    float signa2 = Vector3.Dot(v2, a);    // 原点在面 ABD
    float signd2 = Vector3.Dot(v2, c);    // 对顶点 C

    float signa3 = Vector3.Dot(v3, b);    // 原点在面 BCD
    float signd3 = Vector3.Dot(v3, a);    // 对顶点 A

    // 核心逻辑:若原点与对顶点在同侧 → 原点在该面外侧
    // 即 signaᵢ × signdᵢ ≥ -PLANE4_EPS
    const float th = -PLANE4_EPS;   // PLANE4_EPS = 1e-6
    o0 = signa0 * signd0 >= th;     // 原点在面 ABC 外侧?
    o1 = signa1 * signd1 >= th;     // 原点在面 ACD 外侧?
    o2 = signa2 * signd2 >= th;     // 原点在面 ABD 外侧?
    o3 = signa3 * signd3 >= th;     // 原点在面 BCD 外侧?
}

判定逻辑图解

复制代码
对四面体每个面 F:
  取法线 N_F(朝外)
  取该面的对顶点 V_opposite(不在该面上的那个顶点)
  计算:
    sign_origin = N_F · O(原点投影)
    sign_opposite = N_F · V_opposite(对顶点投影)

  若 sign_origin × sign_opposite ≥ -ε:
    → 原点与对顶点同侧
    → 原点在该面外侧

若原点在全部 4 个面内侧:
  → 原点被四面体包围
  → O ∈ 四面体
  → 两形状重叠!
5.6.2 GetClosestPtPointTriangle:遍历外侧面的最近点

当原点不在四面体内部时,对每个"外侧"面调用 ClosestPtPointTriangleBaryCentric,取最近的那个:

csharp 复制代码
// Simplex.cs 第 318-365 行
private Vector3 GetClosestPtPointTriangle(bool o0, bool o1, bool o2, bool o3,
    int[] indices, out int size)
{
    float bestSq = float.MaxValue;
    Vector3 closestPt = Vector3.zero;
    size = 3;

    if (o0)  // 面 ABC:顶点 0,1,2
    {
        indices[0] = 0; indices[1] = 1; indices[2] = 2;
        bestSq = ClosestPtPointTriangleBaryCentric(Q[0], Q[1], Q[2], indices,
            out size, out closestPt);
    }
    if (o1)  // 面 ACD:顶点 0,2,3
    {
        _idx[0] = 0; _idx[1] = 2; _idx[2] = 3;
        float sq = ClosestPtPointTriangleBaryCentric(Q[0], Q[2], Q[3], _idx,
            out int sz, out Vector3 cp);
        if (bestSq > sq)
        {
            bestSq = sq; closestPt = cp; size = sz;
            indices[0] = _idx[0]; indices[1] = _idx[1]; indices[2] = _idx[2];
        }
    }
    // o2、o3 同理...
    return closestPt;
}

6. 单纯形收敛后还原最近点(GetClosestPoint)

GJK 收敛后,需要从单纯形上的最近点还原出两个凸体上对应的最近点。原理:用最近点在当前单纯形上的重心坐标 ,对 A[](凸体 A 的支撑点)和 B[](凸体 B 的支撑点)做相同插值

csharp 复制代码
// Simplex.cs 第 53-78 行
public void GetClosestPoint(Vector3 closest, out Vector3 closA, out Vector3 closB)
{
    switch (Size)
    {
        case 2:  // 线段:一维重心坐标
        {
            BarycentricSegment(closest, Q[0], Q[1], out float v);
            closA = A[0] + (A[1] - A[0]) * v;
            closB = B[0] + (B[1] - B[0]) * v;
            break;
        }
        case 3:  // 三角形:二维重心坐标
        {
            BarycentricTriangle(closest, Q[0], Q[1], Q[2], out float v, out float w);
            closA = A[0] + (A[1] - A[0]) * v + (A[2] - A[0]) * w;
            closB = B[0] + (B[1] - B[0]) * v + (B[2] - B[0]) * w;
            break;
        }
        default:  // 单点或兜底
        {
            closA = A[0];
            closB = B[0];
            break;
        }
    }
}

6.1 线段重心坐标

已知线段端点 a, b 和线上一点 p,求参数 v 使 p ≈ a + v·(b-a)

csharp 复制代码
// Simplex.cs 第 406-414 行
private void BarycentricSegment(Vector3 p, Vector3 a, Vector3 b, out float v)
{
    Vector3 v0 = a - p;
    Vector3 v1 = b - p;
    Vector3 d = v1 - v0;             // d = (b-p) - (a-p) = b-a = ab
    float denom = Vector3.Dot(d, d); // |ab|²
    float num = Vector3.Dot(-v0, d); // (p-a)·(b-a)
    v = denom > 0f ? num / denom : 0f;
}

6.2 三角形重心坐标

已知三角形顶点 a, b, c 和面内一点 p,求参数 (v, w) 使 p ≈ a + v·(b-a) + w·(c-a)

csharp 复制代码
// Simplex.cs 第 419-434 行
private void BarycentricTriangle(Vector3 p, Vector3 a, Vector3 b, Vector3 c,
    out float v, out float w)
{
    Vector3 v0 = b - a;
    Vector3 v1 = c - a;
    Vector3 v2 = p - a;
    float d00 = Vector3.Dot(v0, v0);   // |b-a|²
    float d01 = Vector3.Dot(v0, v1);   // (b-a)·(c-a)
    float d11 = Vector3.Dot(v1, v1);   // |c-a|²
    float d20 = Vector3.Dot(v2, v0);   // (p-a)·(b-a)
    float d21 = Vector3.Dot(v2, v1);   // (p-a)·(c-a)

    // 解 2×2 线性方程组
    float denom = d00 * d11 - d01 * d01;          // 三角形面积平方
    float inv = Mathf.Abs(denom) > FEPS ? 1f / denom : 0f;
    v = (d11 * d20 - d01 * d21) * inv;             // (b-a) 的系数
    w = (d00 * d21 - d01 * d20) * inv;             // (c-a) 的系数
    // u = 1 - v - w 是顶点 a 的系数
}

7. GJK 的四种退出路径总结

Case 条件 返回值 含义
Case 1 signDist > separatingDist NonIntersect 找到分离轴且间隙 > margin+contactDist,确定两形状不相交
Case 2 signDist > sumMargin && signDist > relDif × dist Close GJK 收敛,返回两形状表面最近点/法线/距离
Case 3 dist ≤ eps 且未退化(reachedOrigin = true Contact 单纯形逼近原点,两形状重叠
Case 4 退化(prevDist ≤ dist)或 32 次迭代耗尽 CloseContact 无法继续收敛,用 acceptanceDist 阈值判定

Case 4 的接受阈值

  • 有 quadratic 形状时:acceptanceDist = sumMargin
  • 无 quadratic 形状时:acceptanceDist = 0.2 × min(a.Margin, b.Margin)
  • finalDist > acceptanceDistClose;否则 → Contact

关键文件索引

文件 职责
Assets/Collision/GJK/GJKCollisionDetector.cs GJK 主循环 + AABB 粗筛 + 对外接口
Assets/Collision/GJK/Simplex.cs 单纯形数据结构 + 更新逻辑(点/线/三角形/四面体)
Assets/Collision/Collider/MyCollider.cs 碰撞体基类:AABB + SupportCore 模板方法 + 欧拉角旋转
Assets/Collision/Collider/MyBoxCollider.cs Box SupportCore:按方向分量符号取角点
Assets/Collision/Collider/MySphereCollider.cs Sphere SupportCore:core 收缩为点,返回球心
Assets/Collision/Collider/MyCapsuleCollider.cs Capsule SupportCore:core 收缩为线段,返回较远端点
Assets/Test/CollisionManager.cs 碰撞检测调度:八叉树 → AABB → GJK → 事件派发
Assets/CollisionTest.cs 场景入口:自动发现碰撞体 + 注册到管理器
相关推荐
AI科技星1 小时前
第六卷:量天尺传奇(几何学)
网络·人工智能·算法·概率论·学习方法·几何学·拓扑学
Y_Bk1 小时前
第十七届蓝桥杯C/C++A组省赛
c语言·数据结构·c++·算法·蓝桥杯
帅小伙―苏1 小时前
力扣76最小覆盖子串
算法·leetcode
RH2312112 小时前
2026.5.24 数据结构 KMP算法实现
数据结构·算法
江屿风2 小时前
C++图论基础单源最短路-常规版dijkstra算法/堆优化版dijkstra算法/bellman-ford 算法/spfa 算法流食般投喂
开发语言·c++·笔记·算法·图论
浮芷.2 小时前
鸿蒙 6.1 新特性-60fps流畅人物跳跃功能算法深度解析-鸿蒙PC端正弦值计算法
算法·华为·harmonyos·鸿蒙·鸿蒙系统
AI科技星2 小时前
数术工坊·第八卷 大道归一录・番外・下篇 零界封神・万法归元终章
网络·人工智能·算法·几何学·拓扑学
Misnearch2 小时前
Leetcode热题100
算法·leetcode·职场和发展
我是一颗柠檬2 小时前
【Java项目技术亮点】滑动窗口限流算法
java·开发语言·算法