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 有哪些值得记住的性质:
- 快速相交测试:两个 AABB 是否相交,最多只需要 6 次比较(每个轴 2 次)。
- 存储紧凑:只需要 6 个浮点数(2 个 Vector3),内存占用极小。
- 方向敏感:物体旋转后,AABB 会「膨胀」,导致包围不紧密。
- 可嵌套:可以用 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 的核心知识点,我建议你保存下来,写代码时对照着看:
4.7 实战建议
最后,给各位几个实战中的建议:
- 优先用 AABB 做粗筛:在 Broad Phase 阶段,AABB 的相交测试是最快的,没有之一。
- 静态物体缓存 AABB:不动的物体,AABB 构建一次就够了,别每帧重复计算。
- 动态物体区分处理:平移用 O(1) 更新,旋转用 O(n) 重建,别混用。
- 注意浮点误差:AABB 的 min/max 边界处,浮点误差可能导致漏报。我习惯加一个很小的 epsilon(比如 1e-6)做缓冲。