OBB 详解:从基础概念到碰撞检测算法(全伪代码实现)

OBB 详解:从基础概念到碰撞检测算法(全伪代码实现)

1. 什么是 OBB?

OBB(Oriented Bounding Box)有向包围盒,是计算几何与游戏开发中比 AABB 更精确的碰撞检测体积。

它的核心特征有两点:

  • 包围盒:一个完全包裹住物体的最小六面体(2D 下为矩形)。
  • 有向:该包围盒可以任意旋转,不平行于世界坐标轴,而是与物体自身的局部坐标系对齐。

正因为"有向",OBB 能更紧密地包裹旋转后的物体,大幅减少 AABB 中常见的"空白区域",但代价是计算复杂度显著提升。


2. OBB 的数据结构

在 3D 空间中,一个 OBB 由 中心点三个半轴向量三个轴方向 共同定义:

结构 OBB:
    向量3 center      // 包围盒中心点
    向量3 axis_x      // 局部 X 轴方向(单位向量)
    向量3 axis_y      // 局部 Y 轴方向(单位向量)
    向量3 axis_z      // 局部 Z 轴方向(单位向量)
    浮点数 half_x     // 沿 axis_x 方向的半边长
    浮点数 half_y     // 沿 axis_y 方向的半边长
    浮点数 half_z     // 沿 axis_z 方向的半边长

在 2D 场景中,去掉 Z 轴分量即可:

结构 OBB2D:
    向量2 center      // 包围盒中心点
    向量2 axis_x      // 局部 X 轴方向(单位向量)
    向量2 axis_y      // 局部 Y 轴方向(单位向量)
    浮点数 half_x     // 沿 axis_x 方向的半边长
    浮点数 half_y     // 沿 axis_y 方向的半边长

关键约束

  • axis_xaxis_yaxis_z 两两正交且为单位向量(即构成标准正交基)。
  • 半边长均为正数。

3. 核心操作与伪代码

3.1 从顶点列表生成 OBB(主成分分析法)

生成物体的 OBB 通常使用主成分分析(PCA) 计算顶点分布的主方向:

函数 生成OBB(顶点列表 points) -> OBB:
    如果 points 为空:
        返回 空OBB

    n = points 的长度

    // 步骤1:计算质心(中心点)
    center = (0, 0, 0)
    对于 每个点 p 属于 points:
        center = center + p
    center = center / n

    // 步骤2:计算协方差矩阵 3x3
    cov = 零矩阵(3, 3)
    对于 每个点 p 属于 points:
        d = p - center
        cov[0][0] = cov[0][0] + d.x * d.x
        cov[0][1] = cov[0][1] + d.x * d.y
        cov[0][2] = cov[0][2] + d.x * d.z
        cov[1][1] = cov[1][1] + d.y * d.y
        cov[1][2] = cov[1][2] + d.y * d.z
        cov[2][2] = cov[2][2] + d.z * d.z
    cov = cov / n
    // 填充对称元素
    cov[1][0] = cov[0][1]
    cov[2][0] = cov[0][2]
    cov[2][1] = cov[1][2]

    // 步骤3:计算协方差矩阵的特征向量和特征值
    // (此处调用标准库或数值方法,如幂迭代法)
    (eigenvalues, eigenvectors) = 计算特征分解(cov)

    // 步骤4:按特征值从大到小排序,取前三个特征向量作为轴
    // 特征值最大的方向为 axis_x,次大为 axis_y,最小为 axis_z
    按特征值降序排列 eigenvectors
    axis_x = eigenvectors[0]
    axis_y = eigenvectors[1]
    axis_z = eigenvectors[2]

    // 步骤5:将所有顶点投影到三个轴上,计算半边长
    half_x = 0
    half_y = 0
    half_z = 0
    对于 每个点 p 属于 points:
        d = p - center
        proj_x = 绝对值(点积(d, axis_x))
        proj_y = 绝对值(点积(d, axis_y))
        proj_z = 绝对值(点积(d, axis_z))
        half_x = 最大值(half_x, proj_x)
        half_y = 最大值(half_y, proj_y)
        half_z = 最大值(half_z, proj_z)

    返回 OBB(center, axis_x, axis_y, axis_z, half_x, half_y, half_z)

时间复杂度:O(n),n 为顶点数量(特征分解部分为常数时间,因为协方差矩阵固定为 3x3)。


3.2 判断点是否在 OBB 内部

将点从世界空间变换到 OBB 的局部空间,然后判断是否在局部 AABB 范围内:

函数 点是否在OBB内(点 p, OBB box) -> 布尔值:
    // 将点转换到 OBB 局部坐标系
    d = p - box.center

    local_x = 点积(d, box.axis_x)
    local_y = 点积(d, box.axis_y)
    local_z = 点积(d, box.axis_z)

    // 在局部坐标系中判断是否在 AABB 范围内
    返回 (local_x >= -box.half_x 且 local_x <= box.half_x) 且
           (local_y >= -box.half_y 且 local_y <= box.half_y) 且
           (local_z >= -box.half_z 且 local_z <= box.half_z)

3.3 两个 OBB 的相交测试(分离轴定理 SAT)

核心原理:两个凸多面体不相交,当且仅当存在一条分离轴,使得两个物体在该轴上的投影不重叠。

对于 OBB,潜在分离轴包括:

  1. 第一个 OBB 的 3 个轴方向(axis_x1, axis_y1, axis_z1
  2. 第二个 OBB 的 3 个轴方向(axis_x2, axis_y2, axis_z2
  3. 以上 6 个轴两两叉积得到的 9 个轴(共计 15 条轴)
函数 OBB是否相交(OBB a, OBB b) -> 布尔值:
    // 收集所有待测试的轴
    axes = []

    // 添加 a 的三个轴
    axes.追加(a.axis_x)
    axes.追加(a.axis_y)
    axes.追加(a.axis_z)

    // 添加 b 的三个轴
    axes.追加(b.axis_x)
    axes.追加(b.axis_y)
    axes.追加(b.axis_z)

    // 添加两两叉积的 9 个轴
    对于 每个轴 i 属于 [a.axis_x, a.axis_y, a.axis_z]:
        对于 每个轴 j 属于 [b.axis_x, b.axis_y, b.axis_z]:
            cross_axis = 叉积(i, j)
            如果 cross_axis 不是零向量:
                axes.追加(归一化(cross_axis))

    // 对每条轴进行投影测试
    对于 每个轴 axis 属于 axes:
        如果 在轴上投影是否分离(a, b, axis) 为 真:
            返回 不相交  // 找到一条分离轴即可判定不相交

    返回 相交  // 所有轴上都重叠

单轴投影测试函数

函数 在轴上投影是否分离(OBB a, OBB b, 向量3 axis) -> 布尔值:
    // 计算 a 在 axis 上的投影区间
    center_a_proj = 点积(a.center, axis)
    // 计算 a 的半边长在 axis 上的投影(半径)
    radius_a = a.half_x * 绝对值(点积(a.axis_x, axis)) +
               a.half_y * 绝对值(点积(a.axis_y, axis)) +
               a.half_z * 绝对值(点积(a.axis_z, axis))
    min_a = center_a_proj - radius_a
    max_a = center_a_proj + radius_a

    // 计算 b 在 axis 上的投影区间
    center_b_proj = 点积(b.center, axis)
    radius_b = b.half_x * 绝对值(点积(b.axis_x, axis)) +
               b.half_y * 绝对值(点积(b.axis_y, axis)) +
               b.half_z * 绝对值(点积(b.axis_z, axis))
    min_b = center_b_proj - radius_b
    max_b = center_b_proj + radius_b

    // 判断区间是否分离
    返回 (max_a < min_b) 或 (max_b < min_a)

注意:SAT 是 OBB 碰撞检测的标准算法,但需要测试 15 条轴(3D 情况下),2D 情况下只需测试 4 条轴(两个矩形的 2 个轴 + 2 个叉积轴)。


3.4 计算 OBB 的 8 个角点(用于渲染或调试)

函数 OBB转顶点列表(OBB box) -> 顶点列表[8]:
    // 计算三个半轴向量
    half_vec_x = box.axis_x * box.half_x
    half_vec_y = box.axis_y * box.half_y
    half_vec_z = box.axis_z * box.half_z

    // 8 个角点 = center ± half_vec_x ± half_vec_y ± half_vec_z
    p000 = box.center - half_vec_x - half_vec_y - half_vec_z
    p100 = box.center + half_vec_x - half_vec_y - half_vec_z
    p010 = box.center - half_vec_x + half_vec_y - half_vec_z
    p110 = box.center + half_vec_x + half_vec_y - half_vec_z
    p001 = box.center - half_vec_x - half_vec_y + half_vec_z
    p101 = box.center + half_vec_x - half_vec_y + half_vec_z
    p011 = box.center - half_vec_x + half_vec_y + half_vec_z
    p111 = box.center + half_vec_x + half_vec_y + half_vec_z

    返回 [p000, p100, p010, p110, p001, p101, p011, p111]

3.5 将 OBB 转换为 AABB(用于粗筛)

将 OBB 的所有角点投影到世界坐标轴上,生成包裹它的最小 AABB:

函数 OBB转AABB(OBB box) -> AABB:
    vertices = OBB转顶点列表(box)
    return 生成AABB(vertices)  // 调用 AABB 的生成函数

3.6 OBB 的膨胀与收缩

函数 膨胀OBB(OBB box, 浮点数 margin) -> OBB:
    // 在三个半轴方向上同时增加边距
    返回 OBB(
        center = box.center,
        axis_x = box.axis_x,
        axis_y = box.axis_y,
        axis_z = box.axis_z,
        half_x = box.half_x + margin,
        half_y = box.half_y + margin,
        half_z = box.half_z + margin
    )

3.7 旋转 OBB(绕任意轴旋转)

函数 旋转OBB(OBB box, 向量3 axis, 浮点数 angle) -> OBB:
    // 构造旋转矩阵
    rot_mat = 构造旋转矩阵(axis, angle)

    // 旋转三个轴方向
    new_axis_x = rot_mat * box.axis_x
    new_axis_y = rot_mat * box.axis_y
    new_axis_z = rot_mat * box.axis_z

    // 中心点也绕同一轴旋转(假设物体绕自身中心旋转)
    // 若绕世界原点旋转,则需先平移再旋转再平移回去
    new_center = rot_mat * box.center

    返回 OBB(new_center, new_axis_x, new_axis_y, new_axis_z,
              box.half_x, box.half_y, box.half_z)

4. OBB 与射线求交

4.1 算法原理

将射线变换到 OBB 的局部坐标系中,然后使用 AABB 与射线求交算法(Slab 法):

函数 射线与OBB求交(射线 r, OBB box) -> 布尔值 或 交点信息:
    // 步骤1:将射线原点变换到 OBB 局部空间
    d = r.origin - box.center
    local_origin.x = 点积(d, box.axis_x)
    local_origin.y = 点积(d, box.axis_y)
    local_origin.z = 点积(d, box.axis_z)

    // 步骤2:将射线方向变换到 OBB 局部空间
    local_dir.x = 点积(r.direction, box.axis_x)
    local_dir.y = 点积(r.direction, box.axis_y)
    local_dir.z = 点积(r.direction, box.axis_z)

    // 步骤3:构造局部 AABB
    local_box.min = (-box.half_x, -box.half_y, -box.half_z)
    local_box.max = (box.half_x, box.half_y, box.half_z)

    // 步骤4:使用 AABB 射线求交算法
    返回 射线与AABB求交(局部射线(local_origin, local_dir), local_box)

5. OBB 与 AABB 的相交测试

混合测试中,将 AABB 视为一种特殊的 OBB(轴为单位向量,半轴为 AABB 的半边长),然后使用 SAT 算法:

函数 AABB与OBB是否相交(AABB aabb, OBB obb) -> 布尔值:
    // 将 AABB 转换为 OBB
    center_a = (aabb.min + aabb.max) / 2
    half_a = (aabb.max - aabb.min) / 2
    obb_a = OBB(center_a, (1,0,0), (0,1,0), (0,0,1), half_a.x, half_a.y, half_a.z)

    // 调用标准 OBB-OBB 相交测试
    返回 OBB是否相交(obb_a, obb)

6. 实际应用中的注意事项

6.1 浮点数精度问题

与 AABB 类似,OBB 的 SAT 测试中对边界情况也需要容差处理:

函数 在轴上投影是否分离带容差(OBB a, OBB b, 向量3 axis, 浮点数 eps) -> 布尔值:
    // ... 计算 min_a, max_a, min_b, max_b ...
    返回 (max_a < min_b - eps) 或 (max_b < min_a - eps)

6.2 生成 OBB 时的退化情况

当物体近乎线性或平面分布时,协方差矩阵可能产生非常小的特征值,导致某些轴方向不稳定:

函数 生成OBB安全(顶点列表 points) -> OBB:
    obb = 生成OBB(points)

    // 检查半边长是否过小
    如果 obb.half_x < 极小值:
        // 使用备用轴(如世界轴)替代
        obb.axis_x = (1, 0, 0)
        // 重新计算该轴上的半边长
        // ...

    如果 obb.half_y < 极小值 或 obb.half_z < 极小值:
        // 类似处理

    返回 obb

6.3 OBB 的适用场景与局限

优点缺点
比 AABB 更紧密地包裹旋转物体生成算法(PCA)计算量较大
减少碰撞检测的"假阳性"SAT 相交测试需测试 15 条轴(3D)
适用于刚体物理引擎存储开销是 AABB 的 3 倍以上
可参与更精确的碰撞响应更新旋转后需重新计算轴方向

7. 完整示例:OBB 类的伪代码接口

类 OBB:
    // 成员变量
    私有 向量3 center
    私有 向量3 axis_x
    私有 向量3 axis_y
    私有 向量3 axis_z
    私有 浮点数 half_x
    私有 浮点数 half_y
    私有 浮点数 half_z

    // 构造函数
    函数 初始化(向量3 c, 向量3 ax, 向量3 ay, 向量3 az,
                 浮点数 hx, 浮点数 hy, 浮点数 hz):
        // 检查正交性和单位性
        如果 不是单位向量(ax) 或 不是单位向量(ay) 或 不是单位向量(az):
            抛出异常 "轴必须是单位向量"
        如果 绝对值(点积(ax, ay)) > 极小值 或 ...:
            抛出异常 "轴必须两两正交"
        如果 hx <= 0 或 hy <= 0 或 hz <= 0:
            抛出异常 "半边长必须为正"

        this.center = c
        this.axis_x = ax
        this.axis_y = ay
        this.axis_z = az
        this.half_x = hx
        this.half_y = hy
        this.half_z = hz

    // 查询方法
    函数 获取中心() -> 向量3: 返回 this.center
    函数 获取轴X() -> 向量3: 返回 this.axis_x
    函数 获取轴Y() -> 向量3: 返回 this.axis_y
    函数 获取轴Z() -> 向量3: 返回 this.axis_z
    函数 获取半边长() -> (浮点数, 浮点数, 浮点数):
        返回 (this.half_x, this.half_y, this.half_z)

    // 判定方法
    函数 包含点(点 p) -> 布尔值:
        返回 点是否在OBB内(p, this)

    函数 与OBB相交(OBB other) -> 布尔值:
        返回 OBB是否相交(this, other)

    // 修改方法(返回新实例)
    函数 旋转(向量3 axis, 浮点数 angle) -> OBB:
        返回 旋转OBB(this, axis, angle)

    函数 平移(向量3 offset) -> OBB:
        返回 OBB(this.center + offset, this.axis_x, this.axis_y,
                  this.axis_z, this.half_x, this.half_y, this.half_z)

    函数 膨胀(浮点数 margin) -> OBB:
        返回 膨胀OBB(this, margin)

    // 工具方法
    函数 转顶点列表() -> 顶点列表[8]:
        返回 OBB转顶点列表(this)

    函数 转AABB() -> AABB:
        返回 OBB转AABB(this)

8. OBB 与 AABB 的对比总结

对比维度AABBOBB
数据结构2 个点(6 个浮点数)中心 + 3 轴 + 3 半长(15 个浮点数)
存储开销中高(AABB 的 2.5 倍)
生成算法O(n) 简单遍历O(n) + 特征分解(3x3 矩阵)
相交测试6 次比较(3D)SAT:15 条轴测试
对旋转物体的包裹性差(大量空白)好(紧密贴合)
更新旋转成本需从顶点重新生成仅旋转 3 个轴方向
适用场景静态场景、粗筛刚体物理、精确碰撞

9. 总结

OBB 是 AABB 的"旋转增强版",它在保留包围盒简洁性的同时,通过引入局部坐标系实现了对旋转物体的紧密包裹。

其核心价值在于:

  1. 更精确的碰撞体积——减少了 AABB 中常见的误报(false positive)。
  2. 成熟的数学工具——SAT 分离轴定理为 OBB 提供了完备的相交测试方案。
  3. 适合刚体系统——与物体的变换紧密结合,更新成本可控。

尽管 OBB 的存储和计算成本均高于 AABB,但在需要较高碰撞精度的场合(如物理引擎、机器人仿真、CAD 系统),OBB 是不可或缺的基础几何工具。

一句话记住 OBB:中心加三轴,半长定大小,SAT 测相交,十五轴见分晓。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

王小玗

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值