碰撞检测原理:AABB包围盒、OBB包围盒、GJK算法、连续碰撞检测
各位工程师朋友,咱们今天聊点实在的——碰撞检测。
做机械臂避障,说白了就是回答一个问题:我的机器人,有没有碰到别的东西?
这个问题听起来简单,但真做起来,坑不少。我刚开始做运动规划那会儿,就吃过亏。明明仿真里跑得好好的,一上真机就撞了。后来一查,是碰撞检测模型太粗糙,漏检了。
嗯,咱们今天就把这块彻底讲透。
一、为什么需要碰撞检测?
机械臂在运动时,它的连杆、关节、末端执行器,都可能与环境中的障碍物发生碰撞。碰撞检测的目的,就是实时判断这些部件是否与障碍物相交。
你想想看,如果检测不准,轻则规划路径无效,重则损坏设备、伤人。所以,碰撞检测是避障算法的基石。
我个人习惯把碰撞检测分为两大类:
- 静态碰撞检测:判断两个物体在某一时刻是否相交。
- 连续碰撞检测:判断两个物体在运动过程中是否相交。
咱们先讲静态的,再讲连续的。
二、AABB包围盒(轴对齐包围盒)
AABB,全称Axis-Aligned Bounding Box。说白了,就是用一个与坐标轴对齐的矩形框,把物体包起来。
为什么用AABB?因为它简单、快。
判断两个AABB是否相交,只需要比较它们在x、y、z轴上的投影区间是否重叠。如果三个轴都重叠,就相交;否则不相交。
核心公式:
两个AABB相交的条件:
max(A.min_x, B.min_x) ≤ min(A.max_x, B.max_x) 且
max(A.min_y, B.min_y) ≤ min(A.max_y, B.max_y) 且
max(A.min_z, B.min_z) ≤ min(A.max_z, B.max_z)
代码实现也很直接:
bool AABBIntersect(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包一下,先快速排除掉明显不相交的物体。我在项目中经常用它做第一层过滤,效率很高。
注意:AABB的缺点是包裹不紧密。如果物体是斜着的,AABB会留下大量空白区域,导致误报。这时候就需要OBB了。
三、OBB包围盒(有向包围盒)
OBB,Oriented Bounding Box。它和AABB的区别在于:OBB可以旋转,与物体的实际姿态对齐。
说白了,OBB就是给物体穿上一件更合身的衣服。
判断两个OBB是否相交,用的是分离轴定理(SAT)。原理是:如果两个凸多边形在任意一条轴上的投影不重叠,那么它们就不相交。
对于3D的OBB,最多需要检测15条轴:
- 两个OBB各自的3个面法线(共6条)
- 两个OBB的边方向叉积(共9条)
代码实现比AABB复杂不少,但好在有成熟的库可以用。我个人建议,除非你是在做底层引擎,否则直接用现成的数学库,比如Eigen或Bullet。
什么时候用OBB?
当机械臂的连杆是细长形状,或者姿态经常变化时,OBB比AABB更准确。比如末端执行器,用OBB包裹,能减少很多误报。
四、GJK算法(Gilbert-Johnson-Keerthi)
GJK算法,名字有点长,但思路很巧妙。它用来判断两个凸多边形是否相交,并且可以计算出它们之间的最近距离。
GJK的核心思想是:如果两个凸多边形相交,那么它们的明可夫斯基差一定包含原点。
嗯,这句话有点绕。我换个说法:
你把两个物体的所有点坐标相减,得到一堆新点。如果这些新点构成的形状包含了原点,那这两个物体就撞上了。
GJK算法通过迭代搜索,不断缩小包含原点的单纯形(simplex),直到找到答案。
我的经验:GJK算法非常优雅,而且效率极高。我在做机械臂的精细碰撞检测时,经常用它。不过要注意,GJK只适用于凸多边形。如果物体是凹的,需要先分解成多个凸体。
伪代码大致如下:
bool GJKIntersect(ConvexShape shapeA, ConvexShape shapeB) {
// 初始化单纯形
Simplex simplex;
// 选择一个初始方向
Vector3 direction = shapeA.center - shapeB.center;
// 迭代
while (true) {
// 在direction方向上找到支撑点
Vector3 support = Support(shapeA, shapeB, direction);
// 如果支撑点不在direction方向上,则不相交
if (dot(support, direction) < 0) return false;
// 将支撑点加入单纯形
simplex.Add(support);
// 检查单纯形是否包含原点
if (simplex.ContainsOrigin()) return true;
// 更新搜索方向
direction = simplex.GetNewDirection();
}
}
五、连续碰撞检测(CCD)
前面讲的都是静态检测,判断某一时刻是否碰撞。但机械臂是运动的,如果运动速度很快,可能上一帧没撞,下一帧就穿模了。
这就是隧道效应。我遇到过,仿真里看着没事,实际运行时机械臂直接穿过了障碍物。
连续碰撞检测(CCD)就是为了解决这个问题。它把物体的运动轨迹考虑进去,判断在一段时间内是否发生碰撞。
常用的CCD方法有两种:
- 扫掠体法:把物体在时间区间内的运动轨迹,生成一个扫掠体,然后判断扫掠体是否与障碍物相交。
- 时间步进法:把时间步长细分,在每个小步长内做静态检测。如果检测到碰撞,再用二分法找到精确的碰撞时刻。
我的建议:对于高速运动的机械臂,一定要开启CCD。但要注意,CCD计算量较大,建议只在关键连杆(比如末端执行器)上使用,其他部分用静态检测就够了。
六、知识体系总览
下面这张图,帮你理清本章的知识结构:
七、避坑指南
最后,分享几个我踩过的坑:
- 不要只用AABB做最终检测。 我曾经在一个项目中,为了省性能,全程用AABB。结果机械臂斜着靠近障碍物时,明明还有很大空间,却报碰撞。后来换成OBB,问题解决。
- GJK算法要注意数值稳定性。 当两个物体刚好相切时,浮点误差可能导致误判。建议加一个很小的容差阈值。
- CCD不是万能的。 如果时间步长太大,或者物体运动轨迹太复杂,CCD也可能漏检。我一般会把CCD和静态检测结合使用,双重保险。
好了,碰撞检测的原理就讲到这里。记住一句话:没有最好的算法,只有最合适的组合。 根据你的机械臂速度、精度要求、计算资源,灵活选择吧。
小提示:实际工程中,我建议把碰撞检测做成一个分层架构:
- 第一层:AABB粗检测(快速排除)
- 第二层:OBB或GJK精检测(准确判断)
- 第三层:CCD连续检测(防止穿模)
这样既保证了效率,又保证了精度。