2. 几何基元与包围盒
碰撞检测的第一步,说白了就是「能不能快速判断两个物体挨上了?」。你想想看,如果每次检测都把两个网格模型的几万个三角形挨个算一遍,那帧率直接崩了。所以我们需要一种更聪明的办法——用简单的几何体把复杂物体包起来,先做粗略判断。
这就是包围盒技术的核心思想。我个人习惯把包围盒分成两大类:轴对齐和有向。再加上包围球和胶囊体,基本覆盖了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 | ★★★★☆ | ★★★☆☆ | ★★☆☆☆ | 动态物体、精确碰撞 |
| 包围球 | ★★☆☆☆ | ★★★★★ | ★★★★★ | 第一层粗筛、粒子 |
| 胶囊体 | ★★★★★ | ★★★★☆ | ★★★★☆ | 角色、武器、肢体 |
知识结构图:包围盒选择流程
核心原则:没有完美的包围盒,只有合适的组合。我建议你根据物体的运动特性、精度要求和性能预算,灵活搭配使用。