2、包围盒技术:AABB(轴对齐包围盒)原理与实现
各位同学,今天我们来聊聊碰撞检测里最基础、也最实用的技术——AABB包围盒。
我记得刚入行那会儿,第一次做车辆与行人碰撞检测,上来就用精确的三角面片去算。结果呢?一帧要算几十毫秒,仿真根本跑不动。后来老工程师跟我说:「小伙子,你先用AABB筛一遍,筛不中的根本不用算。」嗯,从那以后,AABB就成了我工具箱里的常客。
2.1 什么是AABB?
AABB,全称Axis-Aligned Bounding Box,轴对齐包围盒。说白了,就是一个各边都平行于坐标轴的长方体盒子,把你的物体严严实实地包在里面。
你想想看,一个三维空间里的长方体,只要知道两个对角点就能确定它。我们通常用最小点(min)和最大点(max)来表示:
- min:所有坐标分量最小的点(左下后角)
- max:所有坐标分量最大的点(右上前角)
举个例子,一辆车的AABB可能是这样的:min = (-2.0, 0.0, -4.5),max = (2.0, 1.8, 4.5)。这表示车长9米、宽4米、高1.8米。
核心要点:AABB的六个面永远平行于坐标平面。这意味着无论物体怎么旋转,AABB都会跟着变大——这是它的局限,也是它计算快的根本原因。
2.2 为什么自动驾驶仿真离不开AABB?
我在项目中遇到过这样一个场景:高速公路上有几十辆车,每辆车周围还有行人、锥桶、路沿。如果每对物体都做精确碰撞检测,CPU直接冒烟。
AABB的价值就在这里——它是个高效的「过滤器」:
- 速度快:两个AABB的相交测试只需要6次比较运算
- 内存小:每个AABB只存6个float,24字节
- 更新简单:物体移动时,只需要平移min和max
实际流程通常是:先用AABB做粗筛,筛掉明显不相交的物体对。剩下的候选对,再用更精确的算法(比如OBB、GJK)去算。这一下就能过滤掉80%-90%的无效计算。
2.3 AABB的相交检测原理
两个AABB是否相交,判断起来非常直观。我习惯用「投影法」来理解:
把两个盒子分别投影到X、Y、Z三个轴上。如果三个轴上的投影区间都重叠,那么两个盒子就相交。只要有一个轴不重叠,它们就绝对不相交。
为什么会这样?因为AABB的边和坐标轴平行,所以两个盒子的空间关系可以分解到三个轴向上独立判断。这是AABB最巧妙的地方。
个人经验:我建议你在实现时,先检查X轴,再Y轴,最后Z轴。因为大多数场景下,物体在水平方向(XZ平面)分布较广,Y轴(高度)往往最先筛掉不相交的物体。这样能提前退出,省下两次比较。
2.4 代码实现:C++版
下面是我在实际项目中用的一套AABB实现。代码不长,但该有的都有:
struct AABB {
float min_x, min_y, min_z;
float max_x, max_y, max_z;
// 构造:从两个点创建AABB
AABB(float x1, float y1, float z1,
float x2, float y2, float z2) {
min_x = std::min(x1, x2);
min_y = std::min(y1, y2);
min_z = std::min(z1, z2);
max_x = std::max(x1, x2);
max_y = std::max(y1, y2);
max_z = std::max(z1, z2);
}
// 判断两个AABB是否相交
bool intersect(const AABB& other) const {
// X轴检查
if (max_x < other.min_x || min_x > other.max_x)
return false;
// Y轴检查
if (max_y < other.min_y || min_y > other.max_y)
return false;
// Z轴检查
if (max_z < other.min_z || min_z > other.max_z)
return false;
return true;
}
// 获取中心点
float center_x() const { return (min_x + max_x) * 0.5f; }
float center_y() const { return (min_y + max_y) * 0.5f; }
float center_z() const { return (min_z + max_z) * 0.5f; }
// 获取半边长
float half_x() const { return (max_x - min_x) * 0.5f; }
float half_y() const { return (max_y - min_y) * 0.5f; }
float half_z() const { return (max_z - min_z) * 0.5f; }
};
这段代码里,intersect函数就是核心。你注意看,它用了「提前返回」的技巧——一旦发现某个轴不重叠,立刻返回false。这在平均情况下能省不少时间。
2.5 AABB的更新策略
仿真中物体会移动,AABB也得跟着更新。这里有三种策略,我按推荐程度排个序:
| 策略 | 原理 | 适用场景 | 性能 |
|---|---|---|---|
| 完全重建 | 每帧重新计算所有顶点的min/max | 物体变形或旋转剧烈 | 慢(O(n)顶点数) |
| 平移更新 | 将min/max加上位移向量 | 物体只平移不旋转 | 极快(O(1)) |
| 膨胀更新 | 给AABB加一个安全边距,减少更新频率 | 物体缓慢移动或旋转 | 最快(隔几帧更新一次) |
我个人最常用的是「平移更新+定期重建」的组合。平时每帧只做平移,每30帧或物体旋转超过5度时,做一次完全重建。这样既保证了精度,又控制了计算量。
避坑指南:我曾经在项目中犯过一个低级错误——物体旋转后忘了重建AABB。结果AABB还是原来的大小,导致碰撞检测漏报。一辆车明明已经转过去了,AABB还横在路中间。嗯,从那以后我每次旋转操作后面都强制加一句rebuild()。
2.6 AABB的局限性
任何技术都有短板,AABB也不例外。我总结了几点:
- 不紧致:细长物体旋转45度后,AABB会变得很大,产生大量误报
- 不支持旋转:物体旋转后必须重建,不能直接复用
- 精度有限:只能做粗筛,不能给出精确的碰撞点、碰撞法线
那怎么办?我的做法是:AABB做第一层过滤,通过了的再用OBB(有向包围盒)或GJK算法做精确检测。这样既快又准。
2.7 实际项目中的小技巧
最后分享几个我在项目中积累的经验:
- 空间哈希配合AABB:把场景划分成网格,每个网格维护一个AABB列表。检测时只查相邻网格,能进一步减少比较次数
- 利用AABB做LOD:远处的物体用更大的AABB,近处的用精确的。人眼对远处的碰撞不敏感,可以省算力
- 多线程更新:每个物体的AABB更新是独立的,非常适合并行计算。我在一个项目里用OpenMP,8线程下更新速度提升了6倍多
好了,AABB的原理和实现就讲到这里。下一节我们会聊OBB——有向包围盒,它能解决AABB「不紧致」的问题。但记住,AABB永远是碰撞检测的第一道防线,用好了能省下大把时间。
一句话总结:AABB用6个float、6次比较,帮你过滤掉80%的无效碰撞检测。这笔买卖,怎么算都划算。
公众号:蓝海资料掘金营,微信deep3321