4、包围体基础:AABB(轴对齐包围盒)的定义与性质、AABB的构建与更新

各位好,欢迎来到碰撞检测的第四讲。今天咱们聊聊包围体里最基础、也最常用的一个——AABB,也就是轴对齐包围盒。

说白了,AABB 就是一个把物体严严实实包起来的长方体。它有个硬性要求:所有边都必须和坐标轴平行。X 轴方向、Y 轴方向、Z 轴方向,各管各的,不能歪。

我刚开始做碰撞检测的时候,总觉得 AABB 太粗糙,不够精确。后来踩过几次坑才明白——在 90% 的场景下,AABB 的性价比是最高的。你想想看,一个复杂的角色模型可能有几万个三角形,如果每帧都去检测三角形之间的碰撞,CPU 直接冒烟。但先用 AABB 粗筛一遍,能过滤掉 80% 以上的无效计算。

核心思想:用简单的几何体(AABB)包裹复杂物体,先做快速排除,再做精确检测。这是碰撞检测领域最经典的「分阶段」策略。

4.1 AABB 的数学定义

AABB 的数学表达其实很简单。在三维空间中,一个 AABB 只需要两个点就能描述:最小点和最大点。

  • 最小点 (min):所有坐标分量都取最小值,即 (x_min, y_min, z_min)
  • 最大点 (max):所有坐标分量都取最大值,即 (x_max, y_max, z_max)

为什么叫「轴对齐」?因为它的六个面分别垂直于 X、Y、Z 轴。这意味着,判断一个点是否在 AABB 内部,只需要做三次比较:

// 判断点 P 是否在 AABB 内部
bool IsPointInside(const Vector3& p, const AABB& box) {
    return (p.x >= box.min.x && p.x <= box.max.x) &&
           (p.y >= box.min.y && p.y <= box.max.y) &&
           (p.z >= box.min.z && p.z <= box.max.z);
}

嗯,这里要注意:比较次数是固定的 3 次,不会因为物体形状复杂而增加。这就是 AABB 高效的根本原因。

4.2 AABB 的构建方法

构建 AABB 的过程,说白了就是「找极值」。给定一组顶点,遍历它们,记录每个轴上的最小值和最大值。

我记得第一次写这个函数时,犯了个低级错误——忘了初始化 min 和 max。结果跑出来的包围盒全是 NaN,排查了半天才发现。

// 从顶点列表构建 AABB
AABB BuildAABB(const std::vector<Vector3>& vertices) {
    AABB box;
    // 初始化:用第一个顶点
    box.min = box.max = vertices[0];
    
    for (size_t i = 1; i < vertices.size(); ++i) {
        const Vector3& v = vertices[i];
        // 更新最小值
        if (v.x < box.min.x) box.min.x = v.x;
        if (v.y < box.min.y) box.min.y = v.y;
        if (v.z < box.min.z) box.min.z = v.z;
        // 更新最大值
        if (v.x > box.max.x) box.max.x = v.x;
        if (v.y > box.max.y) box.max.y = v.y;
        if (v.z > box.max.z) box.max.z = v.z;
    }
    return box;
}

这个算法的时间复杂度是 O(n),n 是顶点数量。对于静态物体,构建一次就够了。但对于动态物体,每帧都需要重新构建或更新。

个人经验:如果物体顶点数超过 1000 个,可以考虑用 SIMD 指令并行处理。我在一个项目里用 SSE 指令重写了这个函数,速度提升了 3 倍左右。

4.3 AABB 的更新策略

物体动了,AABB 也得跟着动。更新策略主要有三种:

策略 原理 适用场景 性能
完全重建 每帧重新遍历所有顶点 物体变形(如布料、软体) O(n),较慢
平移更新 将 AABB 的 min/max 加上位移向量 刚体平移(不旋转) O(1),极快
旋转后重建 先旋转顶点,再重建 AABB 刚体旋转 O(n),中等

这里有个坑,我曾经踩过:刚体旋转后,不能直接平移 AABB。因为旋转会改变物体的朝向,原来的 AABB 可能已经包不住物体了。必须先把顶点旋转到新位置,再重新计算 AABB。

避坑指南:我曾经在一个物理引擎里,对旋转后的刚体直接平移了 AABB。结果碰撞检测漏报率高达 30%,角色经常穿模。排查了两天才找到原因——旋转后的 AABB 比实际物体小了一圈。

4.4 AABB 的核心性质

聊完了构建和更新,咱们看看 AABB 有哪些值得记住的性质:

  1. 快速相交测试:两个 AABB 是否相交,最多只需要 6 次比较(每个轴 2 次)。
  2. 存储紧凑:只需要 6 个浮点数(2 个 Vector3),内存占用极小。
  3. 方向敏感:物体旋转后,AABB 会「膨胀」,导致包围不紧密。
  4. 可嵌套:可以用 AABB 树(AABB Tree)组织层次结构。

说到相交测试,代码也很简单:

// 判断两个 AABB 是否相交
bool IntersectAABB(const AABB& a, const AABB& b) {
    // 只要有一个轴不重叠,就不相交
    if (a.max.x < b.min.x || a.min.x > b.max.x) return false;
    if (a.max.y < b.min.y || a.min.y > b.max.y) return false;
    if (a.max.z < b.min.z || a.min.z > b.max.z) return false;
    return true; // 三个轴都重叠
}

你想想看,这个函数里没有任何乘除法,只有比较和逻辑运算。现代 CPU 执行这种代码,一个时钟周期就能搞定。

4.5 AABB 的局限性

当然,AABB 不是万能的。它的最大问题就是方向敏感。一个细长的物体,比如一把剑,当它斜着放的时候,AABB 会变得很大,里面大部分都是空的。

我做过一个测试:一把长 2 米、宽 0.1 米的剑,水平放置时 AABB 体积是 0.2 立方米。旋转 45 度后,AABB 体积变成了 1.0 立方米,膨胀了 5 倍!

这种情况下,AABB 的过滤效果就很差了。后续章节我们会讲到 OBB(有向包围盒)和 k-DOP,它们能更好地处理这类问题。

4.6 知识结构总览

下面这张图总结了 AABB 的核心知识点,我建议你保存下来,写代码时对照着看:

AABB 知识体系总览 AABB 轴对齐包围盒 数学定义 min = (x_min, y_min, z_min) max = (x_max, y_max, z_max) 构建方法 遍历顶点,找极值 时间复杂度 O(n) 更新策略 完全重建 / 平移 / 旋转 根据运动类型选择 核心性质 快速相交测试(6次比较) 存储紧凑(6个float) 方向敏感 局限性 旋转后包围不紧密 细长物体膨胀严重 不适合精确碰撞 典型应用 粗筛阶段(Broad Phase) AABB树层次结构 空间分区加速 AABB 是碰撞检测的基石,理解它才能用好更高级的包围体 建议:先掌握 AABB,再学习 OBB、k-DOP、Sphere

4.7 实战建议

最后,给各位几个实战中的建议:

  • 优先用 AABB 做粗筛:在 Broad Phase 阶段,AABB 的相交测试是最快的,没有之一。
  • 静态物体缓存 AABB:不动的物体,AABB 构建一次就够了,别每帧重复计算。
  • 动态物体区分处理:平移用 O(1) 更新,旋转用 O(n) 重建,别混用。
  • 注意浮点误差:AABB 的 min/max 边界处,浮点误差可能导致漏报。我习惯加一个很小的 epsilon(比如 1e-6)做缓冲。