先记住一句话:
AABB 就是用一个“不能歪的长方体盒子”把物体整个装起来。盒子的边永远和 X、Y、Z 轴平行,所以只用两个点(最小角、最大角)就能描述它,判断两个盒子是否相交只需要比较 6 次大小。它不精确,但极其便宜,适合先快速排除大量“肯定碰不到”的情况。
一、先用一个比喻理解
你要判断两件形状奇怪的家具会不会撞到一起:
精确做法:把两件家具的每个面、每个角都拿来比较 → 很慢 偷懒做法:先把每件家具装进一个纸箱,比较两个纸箱 → 纸箱都没碰到 → 家具肯定没碰到(直接排除) → 纸箱碰到了 → 家具可能碰到,再仔细检查AABB 就是那个纸箱,而且是一个规定“必须正着放、不能斜着放”的纸箱。
二、名字拆开看
| 词 | 意思 |
|---|---|
| Axis-Aligned(轴对齐) | 盒子的每条边都和坐标轴平行,不旋转 |
| Bounding(包围) | 物体所有部分都在盒子里面 |
| Box(盒子) | 3D 中是长方体,2D 中是矩形 |
二维示意:
y ↑ │ ┌─────────────┐ ← max (8, 6) │ │ ╱╲ │ │ │ ╱ ╲___ │ │ │ ╱ 物体 ╲ │ │ │ ╲________╱ │ │ └─────────────┘ │ min (2, 1) └──────────────────────→ x不管物体形状多复杂,盒子永远是“横平竖直”的。
三、两种表示方法
1. 最小点 + 最大点(Min / Max)
min = (minX, minY, minZ) 盒子最靠左、下、后的角 max = (maxX, maxY, maxZ) 盒子最靠右、上、前的角2. 中心 + 半尺寸(Center / Extents)
center = (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 时任何点都会把它“撑开”publicstaticAABBEmpty=>newAABB{min=newVector3(float.PositiveInfinity,float.PositiveInfinity,float.PositiveInfinity),max=newVector3(float.NegativeInfinity,float.NegativeInfinity,float.NegativeInfinity)};publicvoidEncapsulate(Vector3p){min=Vector3.Min(min,p);max=Vector3.Max(max,p);}publicstaticAABBFromPoints(Vector3[]points){AABBbox=Empty;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.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;}只有6 次比较,没有乘法、没有开方。而且只要有一个条件不满足就能立刻返回false,大多数“离得很远”的情况第一两次比较就结束了。
为什么只看 3 个轴就够了?
这是分离轴定理的特例:两个凸体不相交,当且仅当存在一个方向,把它们投影上去后不重叠。对于两个都不旋转的盒子,可能的分离方向只有 X、Y、Z 三个,所以只需检查这三个。
💡 旋转了的盒子(OBB)就没这么简单了,需要检查 15 个方向,这就是 AABB 便宜的根本原因。
六、核心操作 2:点是否在盒子里
publicboolContains(Vector3p){returnp.x>=min.x&&p.x<=max.x&&p.y>=min.y&&p.y<=max.y&&p.z>=min.z&&p.z<=max.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){Vector3t1=Vector3.Scale(min-origin,invDir);Vector3t2=Vector3.Scale(max-origin,invDir);Vector3tSmall=Vector3.Min(t1,t2);// 每个轴的进入时刻Vector3tBig=Vector3.Max(t1,t2);// 每个轴的离开时刻floattEnter=Mathf.Max(tSmall.x,Mathf.Max(tSmall.y,tSmall.z));floattExit=Mathf.Min(tBig.x,Mathf.Min(tBig.y,tBig.z));tHit=tEnter>=0?tEnter:tExit;// 起点在盒内时,取离开点returntExit>=Mathf.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 个点方法 B:Arvo 方法(更快)
新中心 = 矩阵 × 旧中心 + 平移 新半尺寸 = |矩阵| × 旧半尺寸 (|矩阵| 是把每个元素取绝对值)publicstatic(Vector3 center,Vector3 extents)Transform(Vector3c,Vector3e,Matrix4x4m){Vector3newCenter=m.MultiplyPoint3x4(c);Vector3newExtents=newVector3(Mathf.Abs(m.m00)*e.x+Mathf.Abs(m.m01)*e.y+Mathf.Abs(m.m02)*e.z,Mathf.Abs(m.m10)*e.x+Mathf.Abs(m.m11)*e.y+Mathf.Abs(m.m12)*e.z,Mathf.Abs(m.m20)*e.x+Mathf.Abs(m.m21)*e.y+Mathf.Abs(m.m22)*e.z);return(newCenter,newExtents);}为什么取绝对值?新盒子在 X 方向的半宽,等于旧盒子三条半轴在 X 方向上“投影长度”之和;长度不能是负的,所以取绝对值。
⚠️ 两种结果的松紧程度
| 做法 | 结果 |
|---|---|
| 每次用原始顶点重新算 | 最紧,但物体顶点多时很贵 |
| 用本地 AABB 变换后重算(方法 A / B) | 稍松,但非常快 |
| 用上一帧的世界 AABB再旋转一次 | ❌ 会越转越胖,不要这样做 |
正确做法是:每个物体保存一个本地空间的 AABB(固定不变),每帧用它和当前变换矩阵算出世界 AABB。
九、其他常用操作
// 合并两个盒子(求能装下两者的最小盒子)publicstaticAABBMerge(AABBa,AABBb)=>newAABB{min=Vector3.Min(a.min,b.min),max=Vector3.Max(a.max,b.max)};// 两个盒子的交集(结果可能为空:某个轴 min > max)publicstaticAABBIntersection(AABBa,AABBb)=>newAABB{min=Vector3.Max(a.min,b.min),max=Vector3.Min(a.max,b.max)};// 表面积(BVH 构建时常用的 SAH 代价指标)publicfloatSurfaceArea(){Vector3d=max-min;return2f*(d.x*d.y+d.y*d.z+d.z*d.x);}// 向外扩张(用于预测移动、留余量)publicvoidExpand(floatmargin){min-=Vector3.one*margin;max+=Vector3.one*margin;}十、AABB 用在哪里
1. 物理碰撞的“粗测阶段”(Broad Phase)
1000 个物体,两两检查 → 约 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){floatdist=Vector3.Dot(n,center)+d;// 中心到平面的距离floatr=Mathf.Abs(n.x)*extents.x// 盒子在法线方向上的“投影半径”+Mathf.Abs(n.y)*extents.y+Mathf.Abs(n.z)*extents.z;returndist<-r;// 整个盒子都在平面外侧}// 只要对 6 个平面中任意一个“完全在外”,就剔除⚠️ 这个测试是保守的:偶尔会把实际在视锥外的盒子(靠近视锥角落)当作可见,但绝不会错误地剔除可见物体。多画一点没关系,少画就是 bug。
4. 其他用途
· 鼠标点选:先用 AABB 筛选,再精确检测 · 遮挡剔除、LOD 计算:根据 AABB 在屏幕上的大小 · 阴影:计算阴影相机需要覆盖的范围 · 编辑器:选中物体时显示的框 · 空间划分:八叉树、四叉树的每个节点本身就是 AABB十一、和其他包围体比较
| 包围体 | 存储 | 相交测试 | 贴合程度 | 旋转后 |
|---|---|---|---|---|
| 包围球 | 中心 + 半径(4 个数) | 极快(比距离) | 细长物体很松 | 不变,最省心 |
| AABB | min + max(6 个数) | 极快(6 次比较) | 中等 | 需要重算,会变胖 |
| OBB(有向包围盒) | 中心 + 半尺寸 + 旋转(约 15 个数) | 较慢(最多 15 个分离轴) | 较紧 | 跟着旋转即可 |
| k-DOP | 多组方向上的区间 | 中等 | 更紧 | 需要重算 |
| 凸包 | 很多顶点 | 慢 | 最紧 | 跟着旋转 |
比喻:
包围球:用气球装东西,最好算,但很浪费空间 AABB: 用正着放的纸箱,好算,空间利用一般 OBB: 可以斜着放的纸箱,更省空间,但比较起来麻烦一个物体经常会同时使用多种:先用包围球或 AABB 快速排除,再用 OBB 或凸包精细检查。
十二、AABB 的缺点
1. 斜放的细长物体很不贴合
一根斜放的长棍: ┌──────────────┐ │╲ │ │ ╲ │ │ ╲ │ ← 盒子 90% 都是空的 │ ╲ │ → 很多“盒子相交、物体没碰到”的误判 │ ╲ │ └──────────────┘2. 旋转时要重算,而且大小会变
物体原地旋转,AABB 却在不停地变大变小,可能导致粗测结果不稳定。
3. 只能“排除”,不能“确认”
AABB 不相交 → 物体一定不相交 ✅ 可以放心排除 AABB 相交 → 物体不一定相交 ⚠️ 还需要精确检测十三、Unity 里的 AABB
| API | 坐标空间 | 说明 |
|---|---|---|
Mesh.bounds | 本地空间 | 网格顶点的包围盒,不随物体移动 |
Renderer.bounds | 世界空间 | 用于视锥剔除;物体旋转后会变大 |
Renderer.localBounds | 本地空间 | 渲染器的本地包围盒 |
Collider.bounds | 世界空间 | 碰撞体的 AABB |
Bounds.Intersects(other) | — | 两个 AABB 相交 |
Bounds.Contains(point) | — | 点在盒内 |
Bounds.IntersectRay(ray, out dist) | — | 射线检测 |
Bounds.Encapsulate(point / bounds) | — | 扩展以包含点或另一个盒子 |
Bounds.ClosestPoint(point) | — | 盒子上离点最近的位置 |
GeometryUtility.TestPlanesAABB(planes, bounds) | — | 视锥剔除测试 |
// 例:检查物体是否在摄像机视野内Plane[]planes=GeometryUtility.CalculateFrustumPlanes(Camera.main);boolvisible=GeometryUtility.TestPlanesAABB(planes,GetComponent<Renderer>().bounds);常见坑
· 蒙皮网格(SkinnedMeshRenderer)的包围盒默认不会每帧精确计算 → 角色动作幅度大时,可能被错误剔除(角色突然“消失”) → 解决:手动调大 localBounds,或勾选 Update When Offscreen(更贵) · 在 Shader 里用顶点动画移动了顶点(草摆动、波浪) → CPU 的包围盒不知道 → 边缘处物体被提前剔除 → 解决:手动扩大 Mesh.bounds · 粒子系统的包围盒也要留意:设置不当会导致特效在屏幕边缘突然消失十四、性能小技巧
1. 数据布局:SoA 方便 SIMD
AoS(每个盒子放一起): [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. 胖 AABB(Fat AABB)
动态 AABB 树里,常把盒子故意放大一点:
物体在“胖盒子”里小幅移动 → 树不用更新 移出胖盒子 → 才重新插入 → 用一点精度换取大量更新开销一句话总结
AABB 是一个各边与坐标轴平行的长方体,只用最小点和最大点(或中心和半尺寸)表示;因为不能旋转,两个盒子相交只需在 X、Y、Z 三个轴上各比较一次区间是否重叠,射线检测可以用“三对平板取区间交集”快速完成。它贴合度一般、旋转后要重算并会变胖,但胜在极其便宜,所以被广泛用作碰撞粗测、BVH、视锥剔除的第一道筛子:盒子不相交就一定不相交,盒子相交再做精确检测。