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 碰撞检测的基本流程
不管在哪个领域,碰撞检测的流程骨架都差不多。我画了一张图,帮你把整个流程串起来。
流程其实不复杂,但每个环节都有坑。我拆开来说:
- 输入物体状态:位置、旋转、缩放、速度。这里有个常见问题——物体变换矩阵没更新就去做检测,结果全是错的。我曾经调试一个 bug 调了两天,最后发现是更新顺序搞反了。
- Broad Phase:用包围盒(AABB)或包围球快速筛选。这一步的目标是「宁可误报,不可漏报」。误报最多多算几次精细检测,漏报就直接穿模了。
- Narrow Phase:对候选对做精确检测。根据几何体类型选择算法——凸体用 GJK,多边形用 SAT,网格用三角形相交。这里要注意数值精度,浮点数比较永远不要用
==。 - 输出碰撞信息:碰撞点、法线、穿透深度。这些数据要足够精确,否则后续的物理响应会抖动。
- 碰撞响应:触发回调、修正位置、计算冲量。这一步已经属于物理引擎的范畴了,但碰撞检测要为它提供可靠的输入。
避坑指南
我曾经在一个 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 无非是多了一个维度,核心思想是一样的。
好了,这一章的内容就到这里。碰撞检测听起来简单,但要做好、做快、做稳,需要下不少功夫。后面的章节我们会一步步深入,从基础几何体检测一直讲到连续碰撞检测和性能优化。咱们下章见。