3、包围盒技术:OBB(有向包围盒)原理与实现
各位好,我们接着聊碰撞检测。上一节讲了AABB,那个东西简单,但有个硬伤——物体一转,包围盒就跟着膨胀,空隙大得吓人。今天要聊的OBB,说白了就是给包围盒加了个“方向”,让它能跟着物体一起转。
我个人习惯把OBB理解成“会旋转的AABB”。你想想看,一辆车斜着停,AABB得包住四个角,浪费一大片空间。OBB呢?它直接跟着车身方向走,贴合度好太多了。我在项目中遇到过好几次,用AABB做卡车碰撞检测,明明没撞上,系统却报了警。换成OBB之后,误报率直接降了60%。
3.1 什么是OBB?
OBB的全称是Oriented Bounding Box,有向包围盒。它的核心思想是:用一个任意方向的长方体去包裹物体,这个长方体的朝向由物体的主方向决定。
和AABB最大的区别在哪?
- AABB:轴对齐,永远朝世界坐标轴方向。物体一转,它不变。
- OBB:方向可变,跟着物体旋转。物体怎么转,它就怎么转。
重要结论:OBB的紧致性远优于AABB。对于细长物体或旋转频繁的物体,OBB是更优选择。
嗯,这里要注意:OBB虽然好,但计算量比AABB大。不是所有场景都值得用。我一般只在车辆、行人这类有明显朝向的物体上用OBB,静态障碍物还是用AABB省事。
3.2 OBB的数学表示
一个OBB需要哪些信息才能唯一确定?说白了就三样:
- 中心点:包围盒的几何中心,记作
C - 三个轴向量:互相垂直的单位向量,记作
u、v、w - 三个半边长:沿每个轴方向的延伸长度,记作
h_u、h_v、h_w
用数学公式表示,OBB内的任意一点 P 满足:
P = C + a * h_u * u + b * h_v * v + c * h_w * w
其中 a, b, c ∈ [-1, 1]
这个公式看着复杂,其实很好理解。你想象一个盒子,中心在C,沿着u方向走半边长h_u,再沿v方向走半边长h_v,最后沿w方向走半边长h_w——八个顶点就这么出来了。
我的小技巧:实际编码时,我习惯把OBB存成三个向量加一个中心点。这样做碰撞检测时,直接拿轴向量做投影,不用每次都重新计算旋转矩阵。
3.3 OBB的构建方法
怎么从一堆顶点里算出OBB?常用的方法有两种。
方法一:基于协方差矩阵
这是最正统的做法。步骤是这样的:
- 计算所有顶点的均值,作为OBB中心
- 构建协方差矩阵
- 对协方差矩阵做特征值分解
- 三个特征向量就是OBB的三个轴方向
- 把顶点投影到轴上,找到最大最小值,确定半边长
代码实现大概长这样:
// 伪代码:基于协方差矩阵构建OBB
Vector3 center = ComputeMean(vertices);
Matrix3 covMatrix;
for each vertex v:
Vector3 diff = v - center;
covMatrix += diff * diff.transpose();
// 特征值分解
EigenSolver solver(covMatrix);
Vector3 eigenValues = solver.eigenvalues();
Matrix3 eigenVectors = solver.eigenvectors();
// 三个轴方向
Vector3 axisU = eigenVectors.col(0);
Vector3 axisV = eigenVectors.col(1);
Vector3 axisW = eigenVectors.col(2);
// 计算半边长
float hU = 0, hV = 0, hW = 0;
for each vertex v:
Vector3 local = v - center;
hU = max(hU, abs(dot(local, axisU)));
hV = max(hV, abs(dot(local, axisV)));
hW = max(hW, abs(dot(local, axisW)));
这个方法精度高,但计算量大。我记得有一次做实时仿真,每帧要对几千个物体构建OBB,特征值分解成了性能瓶颈。后来我换了方法二。
方法二:基于PCA降维
说白了,PCA和协方差矩阵本质是一回事。但PCA有个好处——你可以用迭代的方式近似求解,不用做完整的特征值分解。
我常用的做法是:
- 用所有顶点计算质心
- 用Power Iteration法求最大特征值对应的特征向量
- 把顶点投影到该向量上,找到最大最小值
- 从顶点中减去该方向的分量,重复步骤2-3
注意:PCA方法对顶点分布敏感。如果物体顶点分布不均匀(比如一侧点特别密),算出来的OBB可能会偏。我曾经因为这个原因,在仿真一辆只有车头有密集点云的车辆时,OBB歪了15度。后来我加了顶点均匀化预处理才解决。
3.4 OBB的碰撞检测:分离轴定理
两个OBB之间怎么判断是否碰撞?业界标准做法是分离轴定理(SAT,Separating Axis Theorem)。
SAT的核心思想很简单:如果能找到一条直线,两个OBB在这条直线上的投影不重叠,那它们就没撞上。这条直线就叫“分离轴”。
对于两个OBB,最多需要检测15条轴:
| 轴来源 | 数量 | 说明 |
|---|---|---|
| 物体A的三个轴 | 3 | uA, vA, wA |
| 物体B的三个轴 | 3 | uB, vB, wB |
| 轴A与轴B的叉积 | 9 | uA×uB, uA×vB, ... |
为什么是15条?你想想看,两个凸多面体之间,分离轴要么是某个面的法线,要么是两条棱的叉积。OBB有6个面(3组平行面),所以有3条面法线。两个OBB就是6条。再加上棱与棱的叉积,3×3=9条。总共15条。
实际检测时,只要有一条轴上的投影不重叠,就可以直接返回“未碰撞”。
// 伪代码:OBB碰撞检测(SAT)
bool TestOBBOBB(OBB& a, OBB& b) {
// 获取两个OBB的轴和半边长
Vector3 axesA[3] = {a.u, a.v, a.w};
Vector3 axesB[3] = {b.u, b.v, b.w};
// 检测15条轴
Vector3 testAxes[15];
// 前6条:两个OBB各自的轴
for (int i = 0; i < 3; i++) {
testAxes[i] = axesA[i];
testAxes[i+3] = axesB[i];
}
// 后9条:叉积
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
testAxes[6 + i*3 + j] = cross(axesA[i], axesB[j]);
}
}
// 对每条轴做投影检测
for (int i = 0; i < 15; i++) {
Vector3 axis = testAxes[i];
// 跳过零向量
if (axis.length() < 1e-6) continue;
axis.normalize();
// 计算两个OBB在轴上的投影区间
float projA = ProjectOBB(a, axis);
float projB = ProjectOBB(b, axis);
// 如果不重叠,直接返回
if (!Overlap(projA, projB)) {
return false;
}
}
return true; // 所有轴都重叠,碰撞
}
性能优化建议:实际项目中,15条轴不用全测。我一般先测两个OBB中心连线方向,再测各自的主轴方向。大部分情况下,前3-4条轴就能判定结果。只有少数边界情况才需要测完15条。这个优化能让碰撞检测快3-5倍。
3.5 实际项目中的坑与经验
做了这么多年自动驾驶仿真,OBB这块我踩过的坑不少。挑几个典型的说说:
- 数值稳定性:两个OBB的轴如果几乎平行,叉积会接近零向量。这时候直接跳过这条轴,否则会出现误判。我吃过这个亏,两个车明明没撞上,因为数值误差被判成了碰撞。
- 动态物体的OBB更新:车辆每帧都在动,OBB的轴和中心都要跟着更新。我建议不要每帧重新构建OBB,而是存一个“本地OBB”(相对于物体坐标系),每帧只做一次坐标变换。这样省去了特征值分解的开销。
- 半透明物体的处理:有些仿真场景里有半透明障碍物(比如玻璃墙),OBB的构建要排除这些顶点。我曾经忘了做这个过滤,结果OBB比实际物体大了一圈。
好了,OBB的原理和实现就聊到这儿。下一节我们讲更高级的包围盒——凸包。那个东西更紧致,但计算量也更大。各位先把OBB吃透,后面才好理解。