news 2026/10/15 1:57:50

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
AABB:游戏碰撞检测的高效利器

先记住一句话:

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 个数)极快(比距离)细长物体很松不变,最省心
AABBmin + 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、视锥剔除的第一道筛子:盒子不相交就一定不相交,盒子相交再做精确检测。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 1:57:10

微信扫码点餐小程序全开源部署实战指南

简介&#xff1a;这是一套面向餐饮从业者、小程序开发者及流量主的全开源外卖扫码点餐微信小程序源码&#xff0c;旨在帮助中小餐厅快速落地数字化点餐服务&#xff0c;同时为技术学习者提供完整的前后端实战项目。资源包含2000个文件&#xff0c;主体为6898个PHP后端逻辑文件&…

作者头像 李华
网站建设 2026/10/15 1:56:55

Qt纯代码部件国际化:tr上下文与翻译失效全解析

/* 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 1:56:41

传统赤糖熬制工艺P1596解析与创新应用

1. 项目背景与核心价值"P1596zhaochitang"这个看似神秘的编号背后&#xff0c;其实隐藏着一个极具实用价值的传统工艺项目。作为一名在传统食品工艺领域深耕多年的从业者&#xff0c;我首次接触到这个编号时&#xff0c;就意识到它代表着一套完整的赤糖&#xff08;红…

作者头像 李华
网站建设 2026/10/15 1:55:58

CRMEB 日志体系与 PSR-3 规范:从 psr/log 接口到 ThinkPHP 实际应用

后端电商前端 【免费下载链接】CRMEB &#x1f525;CRMEB开源商城免费开源多语言商城系统&#xff0c;Tp6框架商城&#xff0c;系统可商用&#xff1b;包含小程序商城、H5商城、公众号商城、PC商城、App&#xff0c;支持分销、拼团、砍价、秒杀、优惠券、积分、会员等级、小程序…

作者头像 李华
网站建设 2026/10/15 1:55:19

从零实现前端性能监控SDK:性能、行为与错误上报解析

这个月&#xff0c;我把下班后的碎片时间全砸进了一个代号叫Apollo-11的项目里。它不是登月计划&#xff0c;而是一个属于自己的前端性能监控 SDK。为什么起这么唬人的名字&#xff1f;因为我的目标很直白&#xff1a;像登月一样&#xff0c;从零把一块石头送上天。具体来说&am…

作者头像 李华