WARNING

文章作者:Deepseek v4 flash

编辑:我

校对:你

包围盒简介和作用

什么是包围盒?

想象一下:你在玩《超级马里奥》,马里奥和蘑菇怪物撞在一起时,游戏怎么知道它们“碰到”了?

其实,游戏并不会真的去比对着两个角色的每一个像素是否重叠——那样太慢了!它会给每个角色套上一个简单的几何体,比如一个矩形,然后只判断这些简单的形状有没有碰到。

这个“简单的几何体”就叫做 包围盒(Bounding Volume,简称 BV)

包围盒有什么用?

应用场景说明
🎮 游戏碰撞检测快速判断两个物体是否相撞
🎬 计算机图形学加速光线追踪、视锥体剔除(只渲染屏幕内物体)
🏗️ 物理仿真粒子系统、刚体碰撞的初步筛选
🖥️ 3D建模软件选中物体时显示的外框(就是包围盒)

核心思想:先粗后细。 先用包围盒快速排除“肯定没碰上的”,只有包围盒重叠了,才去做精确的像素级检测。这能省下大量计算时间!

包围球(Bounding Sphere)

包围球就是用一个球体把物体包起来。只要确定球心位置半径,就定义好了。

         . . . . .
       .           .
      .      ●      .    ← 球心 (center)
      .     /|\     .
       .   r |     .
         . .|. . .
            ↓
         半径 r

那么包围球如何构造呢?

  • 球心:通常取物体所有顶点的中心点(平均值坐标)
  • 半径:物体上离球心最远的顶点到球心的距离

优缺点

👍 优点👎 缺点
碰撞检测极快(只需判断两个球心距离是否小于半径和)紧密度较差,对长条形物体会浪费很多空间
旋转不影响包围球(旋转后球不变)容易产生“误判”(包围球碰了,实际物体没碰)

碰撞检测公式

# 两个包围球是否碰撞?
if distance(center1, center2) < (radius1 + radius2):
    print("碰撞!")
else:
    print("没碰到")

AABB(轴对齐包围盒)简介

AABB = Axis-Aligned Bounding Box(轴对齐包围盒)

它是个矩形(2D)或长方体(3D),并且它的边永远和坐标轴平行(X轴、Y轴、Z轴)。

    2D 中的 AABB:
    
    y
    ↑
    |   ┌──────────┐
    |   │          │
    |   │   ●物体  │
    |   │          │
    |   └──────────┘
    |   min              max
    └────────────────────→ x

如何表示?

AABB 只需要两个点:

  • min:x、y、z 最小的那个角
  • max:x、y、z 最大的那个角
在 3D 中:
AABB = {
    min: (x_min, y_min, z_min),
    max: (x_max, y_max, z_max)
}

如何构造一个物体的 AABB?

遍历物体的所有顶点,分别找出 x、y、z 的最小值和最大值:

def make_aabb(vertices):
    xs = [v.x for v in vertices]
    ys = [v.y for v in vertices]
    zs = [v.z for v in vertices]
    return {
        "min": (min(xs), min(ys), min(zs)),
        "max": (max(xs), max(ys), max(zs))
    }

优缺点

👍 优点👎 缺点
构造简单、存储小(只需6个数)物体旋转后 AABB 会变大(不紧贴物体)
碰撞检测非常快对斜向物体贴合度差

⚠️ 重要特性:物体旋转时,AABB 会跟着重新计算(因为轴对齐的性质),所以如果你旋转了一个物体,AABB 就必须更新。


4️⃣ OBB(有向包围盒)简介

OBB = Oriented Bounding Box(有向包围盒 / 定向包围盒)

它也是一个矩形/长方体,但可以不平行于坐标轴,可以跟着物体的方向旋转。

    2D 中的 OBB:
    
    y
    ↑
    |      ┌──────┐
    |     /      /│
    |    /  ●   / │
    |   /      /  
    |  └──────┘
    └────────────────→ x

如何表示?

OBB 需要存储:

  • 中心点(位置)
  • 三个方向轴(局部坐标系的方向)
  • 三个半边长(沿每个方向轴的一半长度)
OBB = {
    center: (cx, cy, cz),       # 中心
    axes: [u, v, w],            # 三个互相垂直的方向轴
    half_extents: (hx, hy, hz)  # 半边长
}

如何构造?

更复杂,常见方法有:

  1. 主成分分析(PCA):对物体所有顶点的协方差矩阵做特征值分解,得到主方向
  2. 基于凸包:先求物体的凸包,再找最小体积的包围盒

优缺点

👍 优点👎 缺点
紧密度最好,贴合物体形状构造复杂,计算量大
物体旋转后 OBB 只需跟着旋转,不需要重建碰撞检测也更复杂(需要分离轴定理 SAT)
适合细长、倾斜的物体存储需要更多数据

三种包围盒的对比

特性包围球AABBOBB
紧密度⭐⭐⭐⭐⭐
碰撞检测速度⭐⭐⭐⭐⭐⭐⭐⭐
构造难度⭐⭐⭐⭐⭐
旋转友好度⭐⭐⭐⭐⭐⭐

实战:用 AABB 做碰撞检测和求重叠区域

这一节我们用 AABB 来实际做两件事:

  1. 判断两个盒子是否碰撞
  2. 如果碰撞了,求出它们重叠的区域

5.1 碰撞检测

两个 AABB 碰撞的条件其实很简单:

在 2D 中:两个矩形在 X 轴和 Y 轴上的投影都重叠时,它们就碰撞了。 在 3D 中:再加上 Z 轴,三个轴上都重叠才碰撞。

核心原理:

对每个轴(x, y, z):
   如果 盒子A的最大值 < 盒子B的最小值 → 肯定不碰撞(A在B左边)
   或者 盒子B的最大值 < 盒子A的最小值 → 肯定不碰撞(B在A左边)
   否则 → 在这个轴上重叠

所有轴都重叠 → 碰撞!

代码实现:

def aabb_collision(A, B):
    """
    A 和 B 是 AABB,格式:
    {"min": (x_min, y_min, z_min), "max": (x_max, y_max, z_max)}
    """
    # 检查 x, y, z 三个轴
    for i in range(3):  # 0=x, 1=y, 2=z
        if A["max"][i] < B["min"][i] or B["max"][i] < A["min"][i]:
            return False  # 只要有一个轴不重叠,就不碰撞
    return True  # 三个轴都重叠,碰撞!

实际理解一下:

    ┌─── A ──┐        ┌─── B ──┐
    │        │        │        │
    │        │        │        │
    └────────┘        └────────┘
   min_A    max_A    min_B    max_B
   
   ← 不重叠!因为 max_A < min_B → 返回 False

    ┌─── A ──┐
    │        │
    │   ┌─── B ──┐
    │   │        │
    └───│────────┘
        │        │
        └────────┘
   
   ← 重叠了!三个轴都检查一下... 重叠 → 返回 True

5.2 求重叠区域

如果两个 AABB 碰撞了,那么它们重叠的部分本身也是一个 AABB

重叠的 AABB 可以这样算:

def overlap_aabb(A, B):
    """返回 A 和 B 重叠区域的 AABB,如果不重叠则返回 None"""
    # 先判断是否碰撞
    if not aabb_collision(A, B):
        return None  # 没碰撞就没有重叠区域
    
    # 重叠区域的 min = 两个 AABB min 的逐分量最大值
    overlap_min = (
        max(A["min"][0], B["min"][0]),  # x
        max(A["min"][1], B["min"][1]),  # y
        max(A["min"][2], B["min"][2])   # z
    )
    
    # 重叠区域的 max = 两个 AABB max 的逐分量最小值
    overlap_max = (
        min(A["max"][0], B["max"][0]),  # x
        min(A["max"][1], B["max"][1]),  # y
        min(A["max"][2], B["max"][2])   # z
    )
    
    return {"min": overlap_min, "max": overlap_max}

图形化理解:

两个 AABB 碰撞:
A: min=(1,1), max=(5,5)
B: min=(3,2), max=(7,6)

坐标图:
y
6 ───────────────
  │             │
5 │   ┌───A──┐  │
  │   │      │  │
4 │   │  ┌───│──┤── B
  │   │  │   │  │ │
3 │   │  │   │  │ │
  │   │  │   │  │ │
2 │   └──┼───┘  │ │
  │      │       │ │
1 │      └───────┘─│
  │                │
0 └───────────────────→ x
  0  1  2  3  4  5  6  7

重叠区域:
min = (max(1,3), max(1,2)) = (3, 2)
max = (min(5,7), min(5,6)) = (5, 5)

所以重叠的 AABB 是:min=(3,2), max=(5,5)

5.3 完整示例(Python)

def make_aabb_from_vertices(vertices):
    xs = [v[0] for v in vertices]
    ys = [v[1] for v in vertices]
    zs = [v[2] for v in vertices]
    return {
        "min": (min(xs), min(ys), min(zs)),
        "max": (max(xs), max(ys), max(zs))
    }
 
# 物体1:一个正方体
obj1_verts = [(0,0,0), (2,0,0), (2,2,0), (0,2,0),
              (0,0,2), (2,0,2), (2,2,2), (0,2,2)]
# 物体2:另一个正方体,向右偏移
obj2_verts = [(1,1,1), (3,1,1), (3,3,1), (1,3,1),
              (1,1,3), (3,1,3), (3,3,3), (1,3,3)]
 
A = make_aabb_from_vertices(obj1_verts)
B = make_aabb_from_vertices(obj2_verts)
 
print(f"AABB A: min={A['min']}, max={A['max']}")
print(f"AABB B: min={B['min']}, max={B['max']}")
 
if aabb_collision(A, B):
    print("✅ 碰撞了!")
    overlap = overlap_aabb(A, B)
    print(f"重叠区域 AABB: min={overlap['min']}, max={overlap['max']}")
else:
    print("❌ 没碰到")

输出结果:

AABB A: min=(0, 0, 0), max=(2, 2, 2)
AABB B: min=(1, 1, 1), max=(3, 3, 3)
✅ 碰撞了!
重叠区域 AABB: min=(1, 1, 1), max=(2, 2, 2)

6️⃣ 总结

内容一句话总结
包围盒给物体套一个简单的几何体,用来快速判断碰撞
包围球用球包裹,检测最快但贴合最差
AABB边和坐标轴平行,简单高效,旋转后需更新
OBB可旋转能贴合物体,最准确但计算最复杂
实战AABB 碰撞 = 三个轴都重叠;重叠区域 = 取 min 的最大值和 max 的最小值

日常记忆口诀 🎯

包围球:滚着走,最快但最松 AABB:站得正,简单又实用 OBB:转得动,精准但费功 先粗后细:用包围盒筛一遍,再精细检测

进阶阅读方向

  • 📖 分离轴定理(SAT):OBB 之间、任意凸多边形之间的碰撞检测
  • 📖 BVH(Bounding Volume Hierarchy):用树形结构组织包围盒,加速大规模场景碰撞
  • 📖 GJK 算法:更高级的凸体碰撞检测算法

🧠 记住:包围盒的精髓就是 “先粗筛,后细查”——用简单的几何体快速排除不可能的情况,只在必要时做精确计算。这是计算机图形学和游戏开发中最基础也最重要的优化思想之一!

希望这篇教程能帮你理解包围盒的世界,下次玩游戏的时候,你就能知道屏幕背后那些看不见的 “盒子” 在默默地工作啦!🎮✨