几何基元与包围盒:AABB、OBB、包围球、胶囊体

碰撞检测的第一步,说白了就是「能不能快速判断两个物体挨上了?」。你想想看,如果每次检测都把两个网格模型的几万个三角形挨个算一遍,那帧率直接崩了。所以我们需要一种更聪明的办法——用简单的几何体把复杂物体包起来,先做粗略判断。

这就是包围盒技术的核心思想。我个人习惯把包围盒分成两大类:轴对齐和有向。再加上包围球和胶囊体,基本覆盖了90%以上的实战场景。

AABB:轴对齐包围盒

AABB(Axis-Aligned Bounding Box)是最简单、最常用的包围盒。它的边永远与世界坐标系的轴平行,不会跟着物体旋转。

核心定义:用两个点描述——最小点 (x_min, y_min, z_min) 和最大点 (x_max, y_max, z_max)。

我在项目中遇到过一个问题:一个长条形的角色在斜着走时,AABB会变得特别大,导致很多不该碰撞的物体被误判。这就是AABB的固有缺陷——它不能旋转。

AABB碰撞检测:说白了就是比大小。两个AABB相交,当且仅当它们在三个轴上的投影区间都重叠。

// AABB vs AABB 检测
bool AABBvsAABB(const AABB& a, const AABB& b) {
    return (a.min.x <= b.max.x && a.max.x >= b.min.x) &&
           (a.min.y <= b.max.y && a.max.y >= b.min.y) &&
           (a.min.z <= b.max.z && a.max.z >= b.min.z);
}

我的经验:AABB更新很快。物体移动时,只需要平移两个点;物体旋转时,需要重新计算。我一般每帧只更新动态物体的AABB,静态物体直接缓存。

OBB:有向包围盒

OBB(Oriented Bounding Box)解决了AABB「不能旋转」的问题。它有自己的朝向,可以紧贴物体。

OBB的定义比AABB复杂一些:一个中心点 + 三个轴方向 + 三个半长。你可以把它想象成一个可以任意旋转的长方体。

OBB碰撞检测:用的是分离轴定理(SAT)。简单说,如果两个OBB在任意一条轴上的投影不重叠,它们就不相交。

// OBB vs OBB 检测(分离轴定理简化版)
bool OBBvsOBB(const OBB& a, const OBB& b) {
    // 需要检测15条轴:3条a的轴 + 3条b的轴 + 9条叉积轴
    for (int i = 0; i < 15; i++) {
        if (!overlapOnAxis(a, b, testAxes[i]))
            return false;  // 找到一条分离轴,不相交
    }
    return true;  // 所有轴都重叠,相交
}

我曾经踩过的坑:OBB的更新代价很高。物体旋转时,三个轴方向都要跟着转。如果场景里有几百个动态物体,OBB的更新开销会吃掉大量CPU。我后来改用「AABB做粗筛 + OBB做精筛」的两阶段方案,性能提升很明显。

特性 AABB OBB
紧密度 较差(斜向物体空隙大) 好(贴合物体朝向)
更新代价 低(平移只需改两个点) 高(旋转需更新三个轴)
检测复杂度 O(1) 三次比较 O(15) 投影计算
适用场景 静态物体、快速粗筛 动态物体、需要精确朝向

包围球

包围球(Bounding Sphere)是所有包围盒里最简单的。一个球心 + 一个半径,搞定。

为什么还要用包围球?因为它的检测速度是O(1)的——只需要算两个球心的距离,跟半径和比较一下就行。

// 包围球 vs 包围球
bool SpherevsSphere(const Sphere& a, const Sphere& b) {
    Vector3 diff = a.center - b.center;
    float distSq = diff.dot(diff);
    float radiusSum = a.radius + b.radius;
    return distSq <= radiusSum * radiusSum;
}

实战建议:包围球适合做「第一层粗筛」。我通常先用包围球快速排除掉明显不相交的物体对,再对剩下的做AABB或OBB检测。这样能过滤掉70%-80%的无效计算。

不过包围球也有缺点——对于长条形物体,空隙特别大。你想想看,一根棍子的包围球,球体半径几乎是棍子长度的一半,检测精度很差。

胶囊体

胶囊体(Capsule)是「圆柱 + 两个半球」的组合体。它特别适合表示人体、手臂、武器等细长物体。

我记得第一次做角色碰撞时,用AABB包一个人体,结果手臂稍微一抬,包围盒就大了好几倍。后来换成胶囊体,紧密度好太多了。

胶囊体的碰撞检测:核心是把胶囊体简化为一条线段 + 半径。检测两个胶囊体是否相交,就是算两条线段之间的最近距离,然后跟半径和比较。

// 胶囊体 vs 胶囊体
bool CapsulevsCapsule(const Capsule& a, const Capsule& b) {
    // 计算两条线段之间的最近距离
    float dist = segmentSegmentDistance(a.segment, b.segment);
    float radiusSum = a.radius + b.radius;
    return dist <= radiusSum;
}

我的习惯:在动作游戏中,角色的身体用胶囊体,武器用OBB。这样既能保证身体碰撞的平滑性,又能精确检测武器挥砍的命中判定。

四种包围盒对比总结

类型 紧密度 检测速度 更新代价 典型应用
AABB ★★☆☆☆ ★★★★★ ★★★★★ 场景物体、粗筛
OBB ★★★★☆ ★★★☆☆ ★★☆☆☆ 动态物体、精确碰撞
包围球 ★★☆☆☆ ★★★★★ ★★★★★ 第一层粗筛、粒子
胶囊体 ★★★★★ ★★★★☆ ★★★★☆ 角色、武器、肢体

知识结构图:包围盒选择流程

包围盒选择决策流程 输入:物体模型 物体是否需要旋转? 否 是 AABB 需要高精度? 静态物体 / 粗筛 OBB 否 物体是否细长? 是 否 胶囊体 包围球(粗筛) 角色 / 武器 / 肢体 动态物体 / 精确碰撞 粒子 / 第一层粗筛 输出:选定的包围盒类型

核心原则:没有完美的包围盒,只有合适的组合。我建议你根据物体的运动特性、精度要求和性能预算,灵活搭配使用。