1、碰撞检测概述:什么是碰撞检测、碰撞检测的应用领域、碰撞检测的基本流程

大家好,我是这门课的主讲。在游戏和仿真领域摸爬滚打了十几年,今天咱们来聊聊一个最基础、也最容易被忽视的话题——碰撞检测。

说白了,碰撞检测就是判断两个或多个物体在空间上是否发生了重叠。你想想看,如果没有它,游戏里的子弹会穿过敌人身体,赛车会直接陷入地面,机器人手臂会无视障碍物直接穿模。嗯,那画面太美我不敢看。

1.1 什么是碰撞检测

从数学上讲,碰撞检测就是求解两个几何体之间的交集是否为空。如果非空,就说明发生了碰撞。但实际工程中,我们不光要知道「有没有撞」,很多时候还要知道「在哪撞的」「什么时候撞的」「撞的有多深」。这些信息后续会交给物理引擎去处理——比如计算反弹、产生伤害、触发事件。

我个人习惯把碰撞检测拆成两个阶段:

  • 粗略检测(Broad Phase):快速排除掉那些明显不可能碰撞的物体对,减少计算量。
  • 精细检测(Narrow Phase):对可能碰撞的物体对做精确的几何相交测试。

这个两阶段思路,几乎所有商业引擎都在用。我当年刚入行时,傻乎乎地对场景里所有物体做两两检测,结果一个只有几百个物体的场景就把帧率干到了个位数。后来才明白——先粗筛,再精测,这是铁律。

核心概念速览

术语 含义 典型输出
碰撞检测 判断几何体是否相交 是/否
碰撞响应 根据碰撞结果产生物理反馈 力、速度、位置修正
穿透深度 两个物体重叠的最大距离 浮点数
碰撞法线 碰撞接触面的法线方向 三维向量

1.2 碰撞检测的应用领域

碰撞检测不是游戏行业的专利。我这些年接触过的项目,横跨了好几个领域,每个领域对碰撞检测的要求都不太一样。

游戏领域

这是大家最熟悉的。从《超级马里奥》里踩蘑菇,到《绝地求生》里子弹命中判定,再到《原神》里角色与地形的交互,处处都是碰撞检测。游戏里对实时性要求极高——必须在 16ms(60fps)内完成所有检测。我曾经在一个手游项目里,因为碰撞检测多算了 2ms,导致低端机掉帧严重,被玩家骂了三个月。

物理仿真领域

比如汽车碰撞测试、布料模拟、流体与固体的交互。这里更看重精度,而不是速度。仿真一帧算几分钟甚至几小时都正常。我记得有个做汽车安全气囊仿真的客户,他们的碰撞模型精细到每个褶皱,检测精度到了微米级。嗯,这种场景下,游戏里的那套近似算法根本不能用。

机器人领域

机器人要在真实世界里运动,必须知道自己会不会撞到墙、撞到人、撞到其他机械臂。这里有个特殊要求——连续碰撞检测。因为机器人运动速度可能很快,离散的时间步长可能漏掉碰撞。我参与过一个工业机械臂项目,就因为漏检了一次碰撞,机械臂直接撞坏了夹具,损失十几万。从那以后,我对连续碰撞检测格外敏感。

领域对比一览

领域 核心要求 典型帧率/步长 常用方法
游戏 实时性 > 精度 30-120 FPS AABB、球体、凸包
物理仿真 精度 > 实时性 0.1-10 FPS 精确网格、BSP树
机器人 安全性 > 一切 1-1000 Hz 连续检测、距离场

1.3 碰撞检测的基本流程

不管在哪个领域,碰撞检测的流程骨架都差不多。我画了一张图,帮你把整个流程串起来。

碰撞检测基本流程图 碰撞检测基本流程 输入:物体状态 Broad Phase:粗略筛选 Narrow Phase:精确检测 输出:碰撞信息 碰撞响应 / 回调 Broad Phase 常用技术 空间划分(四叉树/八叉树) Sweep and Prune 网格/哈希 Narrow Phase 常用技术 GJK 算法 SAT 分离轴定理 三角形-三角形相交

流程其实不复杂,但每个环节都有坑。我拆开来说:

  1. 输入物体状态:位置、旋转、缩放、速度。这里有个常见问题——物体变换矩阵没更新就去做检测,结果全是错的。我曾经调试一个 bug 调了两天,最后发现是更新顺序搞反了。
  2. Broad Phase:用包围盒(AABB)或包围球快速筛选。这一步的目标是「宁可误报,不可漏报」。误报最多多算几次精细检测,漏报就直接穿模了。
  3. Narrow Phase:对候选对做精确检测。根据几何体类型选择算法——凸体用 GJK,多边形用 SAT,网格用三角形相交。这里要注意数值精度,浮点数比较永远不要用 ==。
  4. 输出碰撞信息:碰撞点、法线、穿透深度。这些数据要足够精确,否则后续的物理响应会抖动。
  5. 碰撞响应:触发回调、修正位置、计算冲量。这一步已经属于物理引擎的范畴了,但碰撞检测要为它提供可靠的输入。

避坑指南

我曾经在一个 VR 项目中,因为碰撞检测的穿透深度计算有 0.001 的误差,导致用户的手在抓取物体时一直轻微抖动。用户反馈说「像得了帕金森」。后来我把穿透深度的计算从浮点比较改成了带 epsilon 的容差比较,问题才解决。记住:浮点数比较永远加容差。

1.4 一个简单的代码示例

说了这么多理论,咱们看一段最基础的 AABB 碰撞检测代码。这是所有碰撞检测的入门砖。

// 2D AABB 碰撞检测
struct AABB {
    float x, y;      // 中心坐标
    float halfWidth;  // 半宽
    float halfHeight; // 半高
};

bool CheckAABBCollision(const AABB& a, const AABB& b) {
    // 计算两个包围盒在 X 轴上的距离
    float dx = fabs(a.x - b.x);
    // 计算两个包围盒在 Y 轴上的距离
    float dy = fabs(a.y - b.y);
    
    // 如果两个轴向上的距离都小于半宽/半高之和,则碰撞
    return (dx < a.halfWidth + b.halfWidth) &&
           (dy < a.halfHeight + b.halfHeight);
}

这段代码很简单,但有几个细节要注意:

  • 用 fabs 取值,避免方向判断。
  • 比较用的是 < 而不是 <=。用 <= 的话,两个物体刚好挨着也会被判定为碰撞,这通常不是我们想要的。
  • 如果物体有旋转,AABB 需要重新计算。我见过有人偷懒不更新,结果物体转了个方向,包围盒还是原来的,碰撞检测全乱套了。

个人经验

刚开始学碰撞检测时,我建议你先从 2D 的 AABB 和圆开始练手。别一上来就搞 3D 的 GJK 和 SAT,容易劝退。把 2D 的基础打牢了,3D 无非是多了一个维度,核心思想是一样的。

好了,这一章的内容就到这里。碰撞检测听起来简单,但要做好、做快、做稳,需要下不少功夫。后面的章节我们会一步步深入,从基础几何体检测一直讲到连续碰撞检测和性能优化。咱们下章见。