4. 三维碰撞检测

三维碰撞检测,说白了就是判断两个3D物体有没有「碰到一起」。很多刚入行的朋友觉得这玩意儿玄乎,其实拆开来看,核心就那么几种几何体的组合。今天咱们就聊聊最常用的三种:球体、AABB和OBB。我个人习惯把球体检测当作「开胃菜」,AABB是「家常便饭」,OBB加上SAT才是「硬菜」。

4.1 球体与球体碰撞检测

球体检测是3D碰撞里最简单的。你想想看,两个球撞没撞上,就看球心距离是不是小于半径之和。我在项目中遇到过不少新手,一上来就开平方根,其实没必要——平方比较就够了。

核心公式: 如果 (C1 - C2)² ≤ (r1 + r2)²,则碰撞发生。
// 球体碰撞检测 —— 避免开平方
bool SphereSphereCollision(const Sphere& a, const Sphere& b) {
    Vector3 diff = a.center - b.center;
    float distSq = diff.x * diff.x + diff.y * diff.y + diff.z * diff.z;
    float radSum = a.radius + b.radius;
    return distSq <= radSum * radSum;
}
小技巧: 如果球体数量多(比如粒子系统),可以用空间哈希或网格把球体分组,避免O(n²)的全量检测。

4.2 AABB与AABB碰撞检测

AABB就是「轴对齐包围盒」,说白了就是每个面都平行于坐标轴的盒子。检测逻辑很直观:三个轴分别看投影区间有没有重叠。只要有一个轴没重叠,就没撞上。

我曾经在调试一个物理引擎时,发现两个AABB明明看着重叠了,检测却说没碰撞。查了半天,原来是其中一个盒子的min和max搞反了——min比max还大。嗯,这种低级错误,写代码时一定要加断言检查。

// AABB碰撞检测 —— 分离轴测试(三个轴)
bool AABBCollision(const AABB& a, const AABB& b) {
    // X轴
    if (a.max.x < b.min.x || a.min.x > b.max.x) return false;
    // Y轴
    if (a.max.y < b.min.y || a.min.y > b.max.y) return false;
    // Z轴
    if (a.max.z < b.min.z || a.min.z > b.max.z) return false;
    return true;
}
特性 AABB OBB
更新开销 低(只需重算min/max) 高(需重算三个轴方向)
紧密度 较差(旋转物体空隙大) 好(贴合物体朝向)
碰撞检测复杂度 O(3) 轴测试 O(15) 轴测试(SAT)
适用场景 静态场景、粗略检测 动态物体、精确检测

4.3 OBB与OBB的分离轴定理(SAT)

OBB(有向包围盒)比AABB麻烦得多。它跟着物体旋转,所以检测时不能只看世界坐标轴。分离轴定理(SAT)就是干这个的——它说:如果能找到一条轴,让两个凸体在这条轴上的投影不重叠,那它们就没撞上。

对于OBB,我们需要检测15条轴:

  • 两个OBB各自的3个面法线(共6条)
  • 两个OBB各取一条边做叉积(3×3=9条)
注意: 这15条轴里,很多轴是冗余的。实际实现时可以跳过长度接近零的叉积结果,避免数值不稳定。
// OBB碰撞检测 —— SAT核心逻辑(伪代码)
bool OBBCollision(const OBB& a, const OBB& b) {
    // 收集所有待测试的轴
    Vector3 axes[15];
    int axisCount = 0;
    
    // 1. 两个OBB的6个面法线
    for (int i = 0; i < 3; i++) {
        axes[axisCount++] = a.axis[i];
        axes[axisCount++] = b.axis[i];
    }
    
    // 2. 边叉积(9条)
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 3; j++) {
            Vector3 cross = Cross(a.axis[i], b.axis[j]);
            if (cross.LengthSq() > 1e-6f) { // 跳过零向量
                axes[axisCount++] = Normalize(cross);
            }
        }
    }
    
    // 对每条轴做投影测试
    for (int i = 0; i < axisCount; i++) {
        if (!OverlapOnAxis(a, b, axes[i])) {
            return false; // 找到分离轴,无碰撞
        }
    }
    return true; // 所有轴都重叠,碰撞
}
性能优化: 实际项目中,我会先做AABB粗检测,筛掉明显不相交的物体对,再对候选对做OBB-SAT检测。这样能省下70%以上的计算量。

4.4 三种方法的对比与选型

我建议你按这个思路选型:

  1. 球体: 适合子弹、粒子、角色粗略碰撞。计算量最小。
  2. AABB: 适合静态场景、地形、建筑物。更新快,但旋转物体会产生「空隙」。
  3. OBB + SAT: 适合车辆、武器、需要精确朝向的物体。精度最高,但开销也最大。

记得有一次做赛车游戏,用AABB检测车身碰撞,结果车一转弯就「穿模」——因为AABB没法跟随车身旋转。换成OBB后,问题立刻解决。你想想看,一个倾斜的车身,AABB的包围盒比车本身大了将近一倍,这检测能准吗?

4.5 知识结构图

下面这张图梳理了本章的核心逻辑,从最简单的球体到最复杂的OBB-SAT,你可以看到它们之间的递进关系。

三维碰撞检测方法对比 球体 vs 球体 AABB vs AABB OBB vs OBB (SAT) 特性 • 计算量:极低 • 精度:低(空隙大) • 适用:粗略检测 特性 • 计算量:低 • 精度:中(不旋转) • 适用:静态场景 特性 • 计算量:高 • 精度:高(贴合) • 适用:动态物体 选型建议:粗筛用球体 → 中筛用AABB → 精筛用OBB-SAT 实际项目中常组合使用,平衡精度与性能

4.6 避坑指南

最后分享几个我踩过的坑:

  • 浮点精度: 两个OBB刚好相切时,投影重叠可能为0。我习惯加一个很小的epsilon(比如1e-5f)做容差。
  • 轴重复: SAT的15条轴里,有些轴方向相同或相反。检测前先去重,能省下不少计算。
  • 物体高速运动: 如果物体一帧移动距离超过自身尺寸,SAT会漏检。这时候需要做「连续碰撞检测」(CCD),把运动轨迹扫成体来检测。

我曾经在一个VR项目中,用户快速挥动手柄,结果OBB检测总是漏掉。后来加上CCD,问题才解决。嗯,高速运动的物体,千万别只用静态SAT。