碰撞检测原理:AABB、OBB、GJK 算法简介,如何选择检测精度与速度
碰撞检测,说白了就是回答一个问题:我的机械臂跟障碍物碰上了没有?
这个问题听起来简单,但在实际项目中,它往往是整个运动规划的性能瓶颈。我见过不少团队,算法选型时没想清楚,结果机械臂一跑起来,碰撞检测占了 80% 的计算时间,路径规划根本没法实时。
这一章,我就带你把这几种主流碰撞检测算法吃透。咱们不搞数学推导,重点讲清楚它们各自适合什么场景,以及怎么在精度和速度之间做取舍。
为什么碰撞检测需要分层?
你想想看,机械臂的连杆是三维几何体,障碍物也是。如果每帧都拿两个复杂的三角网格去做精确求交,计算量会爆炸。
所以工业界的做法是分层检测:先用粗筛快速排除明显不相交的物体,再用细筛处理那些可能碰撞的候选对。粗筛阶段用的就是包围盒算法。
核心思路:用简单的几何体(盒子、球体)把复杂的物体包起来,先判断盒子之间是否相交。盒子都不相交,那里面装的物体肯定不相交。
AABB:最简单,也最常用
AABB 的全称是 Axis-Aligned Bounding Box,轴对齐包围盒。它的特点是:盒子的每条边都平行于世界坐标系的轴。
为什么它快?因为判断两个 AABB 是否相交,只需要做 6 次比较——分别检查三个轴上的投影区间是否重叠。没有任何三角函数,没有矩阵运算。
// 两个 AABB 的碰撞检测
bool AABBOverlap(const AABB& a, const AABB& b) {
// 检查 X 轴
if (a.max.x < b.min.x || a.min.x > b.max.x) return false;
// 检查 Y 轴
if (a.max.y < b.min.y || a.min.y > b.max.y) return false;
// 检查 Z 轴
if (a.max.z < b.min.z || a.min.z > b.max.z) return false;
return true;
}
我在项目中遇到过一个问题:机械臂的末端执行器旋转时,AABB 会变得特别大。因为盒子不能跟着物体旋转,只能把整个旋转范围包进去。结果就是,明明没碰到,AABB 却说碰到了——这就是虚警。
适用场景:静态障碍物、环境地图、机械臂的固定基座。这些物体不旋转,AABB 的紧致性很好。
OBB:更紧致,但更贵
OBB 是 Oriented Bounding Box,有向包围盒。它的盒子可以跟着物体一起旋转,所以包裹得更紧。
判断两个 OBB 是否相交,用的是分离轴定理(SAT)。简单说,就是找一条轴,让两个盒子在这条轴上的投影不重叠。如果能找到,它们就不相交。
但问题是,两个 OBB 最多需要检查 15 条轴(每个盒子的 3 个面法向量 + 9 条边叉积)。每条轴都要做投影计算,涉及点积和比较。所以 OBB 的计算量是 AABB 的几十倍。
我曾经踩过的坑:在动态避障中,对每个障碍物都实时计算 OBB。结果机械臂每走一步,光更新包围盒就花了 5 毫秒。后来改成:障碍物移动时才更新 OBB,静止时复用缓存。性能直接翻倍。
什么时候用 OBB?当物体长宽比很大,而且经常旋转时。比如机械臂的细长连杆,用 AABB 会包出很大一片虚警区域,用 OBB 就能紧贴连杆表面。
GJK:精确碰撞检测的利器
AABB 和 OBB 都是保守检测——它们说没碰到,那肯定没碰到;但说碰到了,不一定真碰到(虚警)。
GJK(Gilbert-Johnson-Keerthi)算法则不同。它直接对两个凸多面体做精确求交,能告诉你到底碰没碰到,以及最近距离是多少。
GJK 的核心思想是:如果两个凸体相交,它们的明可夫斯基差一定包含原点。算法通过迭代搜索,不断缩小包含原点的单纯形,直到收敛。
// GJK 伪代码(简化版)
bool GJK(ConvexShape shapeA, ConvexShape shapeB) {
// 初始化:取一个支撑点作为起始单纯形
Simplex simplex;
simplex.add(support(shapeA, shapeB, Vec3(1,0,0)));
// 迭代搜索
Vec3 direction = -simplex.getLast();
while (true) {
Vec3 p = support(shapeA, shapeB, direction);
if (p.dot(direction) < 0) return false; // 没碰到
simplex.add(p);
if (simplex.containsOrigin()) return true; // 碰到了
// 更新搜索方向
direction = simplex.getNewDirection();
}
}
GJK 的优点是精度高,缺点是计算不稳定。当两个物体几乎相切时,迭代次数会暴增。我一般只在最终验证阶段用 GJK,或者在需要计算穿透深度时用。
如何选择检测精度与速度?
这个问题没有标准答案,但有一个经验法则:
| 场景 | 推荐算法 | 原因 |
|---|---|---|
| 静态环境碰撞检测 | AABB | 障碍物不移动,AABB 紧致且极快 |
| 机械臂自碰撞检测 | OBB + 空间哈希 | 连杆旋转频繁,OBB 减少虚警 |
| 动态障碍物避障 | AABB + 时间戳缓存 | 需要高频更新,AABB 计算量最低 |
| 最终路径验证 | GJK | 需要精确结果,避免误报导致路径无效 |
| 高精度抓取规划 | OBB + GJK 混合 | 先用 OBB 粗筛,再用 GJK 精确验证 |
我的个人习惯:在运动规划的主循环里只用 AABB 或 OBB 做粗筛。只有找到候选路径后,才在最后一步用 GJK 做精确验证。这样既保证了实时性,又不会漏掉碰撞。
知识体系总览
下面这张图总结了三种算法的定位和关系:
实际项目中的调参经验
光知道算法原理还不够,调参才是真功夫。我分享几个实战中总结出来的经验:
- 包围盒的膨胀系数:别把包围盒做得跟物体一模一样。留 2-5mm 的余量,可以避免因为传感器误差或控制抖动导致的误报。我一般设 3mm,效果不错。
- 更新频率:AABB 可以每控制周期(1ms)更新一次。OBB 建议每 5-10 个周期更新一次,因为它的计算量更大。GJK 只在路径规划完成后调用一次。
- 空间分区:当障碍物数量超过 20 个时,别做两两检测。用八叉树或网格把空间分成小块,只检测同一块或相邻块里的物体对。
- 缓存机制:如果障碍物上一帧没动,这一帧就别重新算它的包围盒。我见过有人每帧都重建 OBB,白白浪费 CPU。
我曾经犯过的错:在调试阶段把碰撞检测的精度调得特别高,结果机械臂动不动就报碰撞,路径规划几乎没法收敛。后来发现是传感器噪声导致包围盒轻微抖动。加了一个 2mm 的膨胀系数和一个低通滤波器,问题就解决了。