AABB:游戏碰撞检测的高效利器

发布时间:2026/10/15 1:57:56

AABB:游戏碰撞检测的高效利器 先记住一句话AABB 就是用一个“不能歪的长方体盒子”把物体整个装起来。盒子的边永远和 X、Y、Z 轴平行所以只用两个点最小角、最大角就能描述它判断两个盒子是否相交只需要比较 6 次大小。它不精确但极其便宜适合先快速排除大量“肯定碰不到”的情况。一、先用一个比喻理解你要判断两件形状奇怪的家具会不会撞到一起精确做法把两件家具的每个面、每个角都拿来比较 → 很慢 偷懒做法先把每件家具装进一个纸箱比较两个纸箱 → 纸箱都没碰到 → 家具肯定没碰到直接排除 → 纸箱碰到了 → 家具可能碰到再仔细检查AABB 就是那个纸箱而且是一个规定“必须正着放、不能斜着放”的纸箱。二、名字拆开看词意思Axis-Aligned轴对齐盒子的每条边都和坐标轴平行不旋转Bounding包围物体所有部分都在盒子里面Box盒子3D 中是长方体2D 中是矩形二维示意y ↑ │ ┌─────────────┐ ← max (8, 6) │ │ ╱╲ │ │ │ ╱ ╲___ │ │ │ ╱ 物体 ╲ │ │ │ ╲________╱ │ │ └─────────────┘ │ min (2, 1) └──────────────────────→ x不管物体形状多复杂盒子永远是“横平竖直”的。三、两种表示方法1. 最小点 最大点Min / Maxmin (minX, minY, minZ) 盒子最靠左、下、后的角 max (maxX, maxY, maxZ) 盒子最靠右、上、前的角2. 中心 半尺寸Center / Extentscenter (min max) / 2 盒子正中心 extents (max - min) / 2 从中心到各个面的距离半长、半宽、半高两者可以随时互相转换min center - extents max center extents表示擅长Min / Max相交测试、合并盒子、射线检测Center / Extents平移、旋转后重算、视锥剔除 Unity 的Bounds内部存的是center和extents同时提供min、max、size extents × 2属性。四、怎么算出一个 AABB遍历物体的所有顶点分别记下每个轴上的最小值和最大值publicstructAABB{publicVector3min;publicVector3max;// 空盒子min 设为无穷大max 设为无穷小// 这样第一次 Encapsulate 时任何点都会把它“撑开”publicstaticAABBEmptynewAABB{minnewVector3(float.PositiveInfinity,float.PositiveInfinity,float.PositiveInfinity),maxnewVector3(float.NegativeInfinity,float.NegativeInfinity,float.NegativeInfinity)};publicvoidEncapsulate(Vector3p){minVector3.Min(min,p);maxVector3.Max(max,p);}publicstaticAABBFromPoints(Vector3[]points){AABBboxEmpty;foreach(varpinpoints)box.Encapsulate(p);returnbox;}}⚠️ 常见错误把初始值设为(0,0,0)。如果物体全部在正半轴比如顶点都在 (5~10)盒子会被错误地拉到原点。五、核心操作 1两个 AABB 是否相交原理只要在任意一个轴上分开了两个盒子就一定不相交。先看一维两条线段[aMin, aMax]和[bMin, bMax]什么时候重叠不重叠 A: ├────┤ B: ├────┤ aMax bMin 不重叠 A: ├────┤ B: ├────┤ bMax aMin 重叠 A: ├──────┤ B: ├──────┤ aMin ≤ bMax 且 aMax ≥ bMin三维就是X、Y、Z 三个轴都要重叠publicstaticboolIntersects(AABBa,AABBb){returna.min.xb.max.xa.max.xb.min.xa.min.yb.max.ya.max.yb.min.ya.min.zb.max.za.max.zb.min.z;}只有6 次比较没有乘法、没有开方。而且只要有一个条件不满足就能立刻返回false大多数“离得很远”的情况第一两次比较就结束了。为什么只看 3 个轴就够了这是分离轴定理的特例两个凸体不相交当且仅当存在一个方向把它们投影上去后不重叠。对于两个都不旋转的盒子可能的分离方向只有 X、Y、Z 三个所以只需检查这三个。 旋转了的盒子OBB就没这么简单了需要检查 15 个方向这就是 AABB 便宜的根本原因。六、核心操作 2点是否在盒子里publicboolContains(Vector3p){returnp.xmin.xp.xmax.xp.ymin.yp.ymax.yp.zmin.zp.zmax.z;}点到盒子的最近点 / 距离把点在每个轴上“夹”到盒子范围内publicVector3ClosestPoint(Vector3p){returnVector3.Max(min,Vector3.Min(p,max));// 每个分量 clamp 到 [min, max]}publicfloatSqrDistance(Vector3p){return(ClosestPoint(p)-p).sqrMagnitude;// 点在盒内时结果为 0}这个技巧常用于球和盒子的碰撞球心到盒子的距离 ≤ 半径就相交。boolSphereIntersects(AABBbox,Vector3c,floatr)box.SqrDistance(c)r*r;// 比较平方省去开方七、核心操作 3射线检测Slab 平板法用于鼠标点选、子弹检测、光线追踪。原理把盒子看成**三对平行的“板”**交叉出来的区域X 方向两块竖着的板 x minX 和 x maxX Y 方向两块横着的板 y minY 和 y maxY Z 方向同理射线公式点 起点 t × 方向对每个轴算出射线“进入板之间”和“离开板之间”的 t → 射线真正在盒子里的那一段 三个区间的交集 y │ ╱ 射线 maxY ─┼─────●──────── ← 在 Y 方向进入于 tY1 │ ╱ │ │ minY ─┼─●───┼──────┼─ ← 在 Y 方向离开…… │╱ │ │ └─────┼──────┼────→ x minX maxX 进入盒子的时刻 tEnter 三个轴“进入时刻”中最晚的 离开盒子的时刻 tExit 三个轴“离开时刻”中最早的 tEnter ≤ tExit → 射线穿过盒子直观理解必须同时进入了所有三对板之间才算进入盒子只要离开了任意一对就离开了盒子。代码// invDir 1 / 方向提前算好对同一条射线测试很多盒子时只需算一次publicboolRaycast(Vector3origin,Vector3invDir,outfloattHit){Vector3t1Vector3.Scale(min-origin,invDir);Vector3t2Vector3.Scale(max-origin,invDir);Vector3tSmallVector3.Min(t1,t2);// 每个轴的进入时刻Vector3tBigVector3.Max(t1,t2);// 每个轴的离开时刻floattEnterMathf.Max(tSmall.x,Mathf.Max(tSmall.y,tSmall.z));floattExitMathf.Min(tBig.x,Mathf.Min(tBig.y,tBig.z));tHittEnter0?tEnter:tExit;// 起点在盒内时取离开点returntExitMathf.Max(tEnter,0f);// 盒子在射线后方也算没打中}细节方向分量为 0 怎么办方向 x 0 → invDir.x ∞ 或 -∞IEEE 浮点规则 → t1.x、t2.x 变成 ±∞ → 起点在两块板之间区间是 (-∞, ∞)不影响结果 ✅ → 起点在板外区间是空的判定为没打中 ✅所以这个算法天然处理了平行的情况。唯一的坑是起点正好落在板上且方向为 0 时会出现0 × ∞ NaN对精度要求高的场合需要特殊处理。八、物体移动和旋转后AABB 怎么更新1. 平移直接移min 位移 max 位移2. 缩放直接缩以原点为中心center * 缩放extents * |缩放|3. 旋转盒子会“变胖”AABB 不能旋转所以物体旋转后只能重新找一个更大的正盒子把它装住原来 旋转 45° 后 ┌──────┐ ┌──────────┐ │██████│ │ ◆ │ │██████│ → │ ◆███◆ │ ← 新 AABB 比物体大不少 └──────┘ │ ◆ │ └──────────┘方法 A变换 8 个角再重新算盒子取原 AABB 的 8 个角 → 都乘上变换矩阵 → 求新的 min / max 简单直观但要变换 8 个点方法 BArvo 方法更快新中心 矩阵 × 旧中心 平移 新半尺寸 |矩阵| × 旧半尺寸 |矩阵| 是把每个元素取绝对值publicstatic(Vector3 center,Vector3 extents)Transform(Vector3c,Vector3e,Matrix4x4m){Vector3newCenterm.MultiplyPoint3x4(c);Vector3newExtentsnewVector3(Mathf.Abs(m.m00)*e.xMathf.Abs(m.m01)*e.yMathf.Abs(m.m02)*e.z,Mathf.Abs(m.m10)*e.xMathf.Abs(m.m11)*e.yMathf.Abs(m.m12)*e.z,Mathf.Abs(m.m20)*e.xMathf.Abs(m.m21)*e.yMathf.Abs(m.m22)*e.z);return(newCenter,newExtents);}为什么取绝对值新盒子在 X 方向的半宽等于旧盒子三条半轴在 X 方向上“投影长度”之和长度不能是负的所以取绝对值。⚠️ 两种结果的松紧程度做法结果每次用原始顶点重新算最紧但物体顶点多时很贵用本地 AABB 变换后重算方法 A / B稍松但非常快用上一帧的世界 AABB再旋转一次❌ 会越转越胖不要这样做正确做法是每个物体保存一个本地空间的 AABB固定不变每帧用它和当前变换矩阵算出世界 AABB。九、其他常用操作// 合并两个盒子求能装下两者的最小盒子publicstaticAABBMerge(AABBa,AABBb)newAABB{minVector3.Min(a.min,b.min),maxVector3.Max(a.max,b.max)};// 两个盒子的交集结果可能为空某个轴 min maxpublicstaticAABBIntersection(AABBa,AABBb)newAABB{minVector3.Max(a.min,b.min),maxVector3.Min(a.max,b.max)};// 表面积BVH 构建时常用的 SAH 代价指标publicfloatSurfaceArea(){Vector3dmax-min;return2f*(d.x*d.yd.y*d.zd.z*d.x);}// 向外扩张用于预测移动、留余量publicvoidExpand(floatmargin){min-Vector3.one*margin;maxVector3.one*margin;}十、AABB 用在哪里1. 物理碰撞的“粗测阶段”Broad Phase1000 个物体两两检查 → 约 50 万对 每对都做精确网格碰撞 → 根本算不完 做法 粗测Broad Phase用 AABB 快速找出“可能碰到”的少数几对 精测Narrow Phase只对这几对做精确检测常见的粗测算法算法思路扫描与剪枝Sweep and Prune把所有 AABB 在 X 轴上按 min 排序只有 X 区间重叠的才继续检查 Y、Z。物体每帧移动少排序几乎不变很快网格 / 空间哈希把空间切成格子只检查同一格或相邻格的物体动态 AABB 树用 AABB 层层套起来组成树见下Box2D、Bullet 等都用这种2. 层次包围盒BVH把 AABB 套成一棵树┌─────────────────────┐ │ 整个场景 │ └──────────┬──────────┘ ┌──────────────┴──────────────┐ ┌────┴────┐ ┌────┴────┐ │ 左半边 │ │ 右半边 │ └────┬────┘ └────┬────┘ ┌───┴───┐ ┌───┴───┐ 树1 房子 人物 石头射线检测时射线没打中“左半边”的盒子 → 左边整棵子树全部跳过 → 从检查 N 个物体降到大约 log N 个光线追踪包括 GPU 硬件光追、物理引擎、碰撞查询都依赖它。3. 视锥剔除Frustum Culling摄像机看不见的物体不提交渲染。用 AABB 和视锥的 6 个平面比较// plane: 法线 n 指向视锥内部平面方程 dot(n, p) d 0boolIsOutside(Vector3center,Vector3extents,Vector3n,floatd){floatdistVector3.Dot(n,center)d;// 中心到平面的距离floatrMathf.Abs(n.x)*extents.x// 盒子在法线方向上的“投影半径”Mathf.Abs(n.y)*extents.yMathf.Abs(n.z)*extents.z;returndist-r;// 整个盒子都在平面外侧}// 只要对 6 个平面中任意一个“完全在外”就剔除⚠️ 这个测试是保守的偶尔会把实际在视锥外的盒子靠近视锥角落当作可见但绝不会错误地剔除可见物体。多画一点没关系少画就是 bug。4. 其他用途· 鼠标点选先用 AABB 筛选再精确检测 · 遮挡剔除、LOD 计算根据 AABB 在屏幕上的大小 · 阴影计算阴影相机需要覆盖的范围 · 编辑器选中物体时显示的框 · 空间划分八叉树、四叉树的每个节点本身就是 AABB十一、和其他包围体比较包围体存储相交测试贴合程度旋转后包围球中心 半径4 个数极快比距离细长物体很松不变最省心AABBmin max6 个数极快6 次比较中等需要重算会变胖OBB有向包围盒中心 半尺寸 旋转约 15 个数较慢最多 15 个分离轴较紧跟着旋转即可k-DOP多组方向上的区间中等更紧需要重算凸包很多顶点慢最紧跟着旋转比喻包围球用气球装东西最好算但很浪费空间 AABB 用正着放的纸箱好算空间利用一般 OBB 可以斜着放的纸箱更省空间但比较起来麻烦一个物体经常会同时使用多种先用包围球或 AABB 快速排除再用 OBB 或凸包精细检查。十二、AABB 的缺点1. 斜放的细长物体很不贴合一根斜放的长棍 ┌──────────────┐ │╲ │ │ ╲ │ │ ╲ │ ← 盒子 90% 都是空的 │ ╲ │ → 很多“盒子相交、物体没碰到”的误判 │ ╲ │ └──────────────┘2. 旋转时要重算而且大小会变物体原地旋转AABB 却在不停地变大变小可能导致粗测结果不稳定。3. 只能“排除”不能“确认”AABB 不相交 → 物体一定不相交 ✅ 可以放心排除 AABB 相交 → 物体不一定相交 ⚠️ 还需要精确检测十三、Unity 里的 AABBAPI坐标空间说明Mesh.bounds本地空间网格顶点的包围盒不随物体移动Renderer.bounds世界空间用于视锥剔除物体旋转后会变大Renderer.localBounds本地空间渲染器的本地包围盒Collider.bounds世界空间碰撞体的 AABBBounds.Intersects(other)—两个 AABB 相交Bounds.Contains(point)—点在盒内Bounds.IntersectRay(ray, out dist)—射线检测Bounds.Encapsulate(point / bounds)—扩展以包含点或另一个盒子Bounds.ClosestPoint(point)—盒子上离点最近的位置GeometryUtility.TestPlanesAABB(planes, bounds)—视锥剔除测试// 例检查物体是否在摄像机视野内Plane[]planesGeometryUtility.CalculateFrustumPlanes(Camera.main);boolvisibleGeometryUtility.TestPlanesAABB(planes,GetComponentRenderer().bounds);常见坑· 蒙皮网格SkinnedMeshRenderer的包围盒默认不会每帧精确计算 → 角色动作幅度大时可能被错误剔除角色突然“消失” → 解决手动调大 localBounds或勾选 Update When Offscreen更贵 · 在 Shader 里用顶点动画移动了顶点草摆动、波浪 → CPU 的包围盒不知道 → 边缘处物体被提前剔除 → 解决手动扩大 Mesh.bounds · 粒子系统的包围盒也要留意设置不当会导致特效在屏幕边缘突然消失十四、性能小技巧1. 数据布局SoA 方便 SIMDAoS每个盒子放一起 [minX minY minZ maxX maxY maxZ] [minX minY ...] ... SoA同类数据放一起 minX: [a b c d ...] minY: [a b c d ...] ... → 一条 SIMD 指令同时测试 4 / 8 个盒子Unity 的 Burst Job System 配合 SoA 布局可以非常快地批量做剔除。这也和之前讲的“缓存友好、连续存放”是同一个道理。2. 射线检测预先算倒数invDir 1 / dir 只算一次 → 每个盒子的测试只剩乘法和比较没有除法3. 胖 AABBFat AABB动态 AABB 树里常把盒子故意放大一点物体在“胖盒子”里小幅移动 → 树不用更新 移出胖盒子 → 才重新插入 → 用一点精度换取大量更新开销一句话总结AABB 是一个各边与坐标轴平行的长方体只用最小点和最大点或中心和半尺寸表示因为不能旋转两个盒子相交只需在 X、Y、Z 三个轴上各比较一次区间是否重叠射线检测可以用“三对平板取区间交集”快速完成。它贴合度一般、旋转后要重算并会变胖但胜在极其便宜所以被广泛用作碰撞粗测、BVH、视锥剔除的第一道筛子盒子不相交就一定不相交盒子相交再做精确检测。
延伸阅读

更多相关文章

2026/10/15 1:57:56

让 foobar2000 播放视频:foobox-cn 视频扩展三步启用

让 foobar2000 播放视频:foobox-cn 视频扩展三步启用 【免费下载链接】foobox-cn DUI 配置 for foobar2000 项目地址: https://gitcode.com/GitHub_Trending/fo/foobox-cn foobar2000 默认不认识视频文件,foobox-cn 的视频扩展可以把视频文件加入…

2026/10/15 2:53:05

Spring Security动态权限设计与落地实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 2:53:05

基于炬芯ATS308X的Find my防丢器查找器功能实现

# ATS3089P 设备查找与防丢报警本功能用于手机 BLE App 查找附近开发板,以及 BLE 连接丢失后的本机声音报警。PC 直接使用原有普通 UART 串口发送指令。这里的“Find My”是本地 BLE 查找功能,没有接入 Apple 查找网络。查找声音由固件实时合成&#xff…

2026/10/15 2:53:05

51单片机LED入门:从点亮到嵌入式系统构建

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 2:53:05

一篇看懂 JS 生成 UUID 的四种方案与选型

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 11:31:03

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/14 17:41:07

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/14 2:32:55

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

2026/10/15 0:02:22

防关联浏览器哪个好?三款产品实测差异解析

新手做多店铺,最纠结的就是防关联浏览器选哪个。飞跨、网易、站斧这三款被问得最多,先给结论:要一体化省心、长期便宜,选站斧;要网易招牌和稳,选网易;预算紧做东南亚 Lazada,选飞跨。…

2026/10/15 0:02:22

影刀RPA深度实战:大模型Fine-tuning定制RPA专用Prompt与指令生成

影刀RPA深度实战:大模型Fine-tuning定制RPA专用Prompt与指令生成 一、技术背景 大模型(LLM)在通用场景下表现强大,但当我们将 GPT-4 或 Qwen 直接用于生成影刀RPA操作指令和 XPath 定位时,准确率通常只有 40%-60%。原因…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑