碰撞检测原理: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包围盒 OBB包围盒 GJK算法 扫掠体法 时间步进法 粗检测 → 精检测 → 连续检测 AABB(快) → OBB(准) → GJK(精) → CCD(防穿模) 💡 我的经验:先用AABB做粗过滤,再用OBB或GJK做精检测 高速运动时开启CCD,但只对关键连杆使用

七、避坑指南

最后,分享几个我踩过的坑:

  • 不要只用AABB做最终检测。 我曾经在一个项目中,为了省性能,全程用AABB。结果机械臂斜着靠近障碍物时,明明还有很大空间,却报碰撞。后来换成OBB,问题解决。
  • GJK算法要注意数值稳定性。 当两个物体刚好相切时,浮点误差可能导致误判。建议加一个很小的容差阈值。
  • CCD不是万能的。 如果时间步长太大,或者物体运动轨迹太复杂,CCD也可能漏检。我一般会把CCD和静态检测结合使用,双重保险。

好了,碰撞检测的原理就讲到这里。记住一句话:没有最好的算法,只有最合适的组合。 根据你的机械臂速度、精度要求、计算资源,灵活选择吧。

小提示:实际工程中,我建议把碰撞检测做成一个分层架构:

  1. 第一层:AABB粗检测(快速排除)
  2. 第二层:OBB或GJK精检测(准确判断)
  3. 第三层:CCD连续检测(防止穿模)

这样既保证了效率,又保证了精度。

可可精品资料库