公司动态

AABB与OBB碰撞检测:从数学原理到工业级C++实现

📅 2026/7/15 12:58:22
AABB与OBB碰撞检测:从数学原理到工业级C++实现
1. 项目概述从游戏到工业仿真碰撞检测为何是基石在三维世界的数字孪生中无论是游戏角色挥剑砍向怪物还是工业机器人手臂精准抓取零件一个核心问题始终存在两个物体是否发生了接触这个看似简单的问题背后是计算机图形学、物理仿真乃至机器人学中至关重要的基础技术——碰撞检测。而AABB轴对齐包围盒与OBB有向包围盒则是其中应用最广泛、也最经典的两种包围体Bounding Volume算法。我接触碰撞检测有十多年了从早期游戏开发到后来的工业仿真项目深刻体会到选对包围盒项目就成功了一半。很多新手一上来就追求最精确的三角面片级检测结果性能惨不忍睹。实际上在绝大多数场景下我们采用的都是“包围盒层级检测”策略先用一个简单的几何体如AABB或OBB快速排除掉明显不相交的物体对只有那些“疑似碰撞”的物体对才会进入更耗时的精确检测阶段。AABB和OBB就是这个策略中的第一道也是最关键的一道“过滤器”。简单来说AABB就是一个其边与坐标轴平行的长方体。想象一个快递纸箱无论里面的物品形状多不规则我们用一个方方正正的、与地面和墙壁平行的箱子把它装起来。判断两个这样的箱子是否相交计算极其简单快速只需要比较它们在X、Y、Z轴上的最大最小值区间是否有重叠即可。但它的缺点是紧密性Tightness较差尤其是对于旋转后的长条形物体AABB会变得非常“臃肿”产生大量误报即实际上没碰着但算法认为碰着了导致不必要的精确检测开销。OBB则解决了这个问题。它同样是一个长方体但其方向可以是任意的由物体的局部坐标系决定。这就好比为那个不规则物品量身定制了一个更贴合的包装盒无论物品如何旋转这个盒子都跟着它一起转始终紧密包裹。因此OBB的紧密性远优于AABB能更有效地减少误报。但代价是判断两个OBB是否相交的算法分离轴定理SAT要比AABB复杂得多。所以这个项目的核心不是简单地调用某个库的API而是要深入理解这两种包围盒的数学原理、性能特性和适用场景并设计出工业级的代码。这意味着代码不仅要正确更要高效、健壮、可维护能处理浮点数误差能优雅地集成到更大的物理引擎或仿真系统中。接下来我将带你从数学原理拆解到C实现揭秘那些教科书上不会写的工程细节和性能陷阱。2. 核心数学原理分离轴定理SAT是灵魂要理解OBB碰撞检测必须吃透分离轴定理。这是整个算法的灵魂也是面试中常考的重点。我们可以用一个非常生活化的比喻来理解它判断两个投影在墙上是否有重叠。想象你在黑暗的房间里用手电筒从不同角度照射两个物体在身后的白墙上留下它们的影子投影。如果存在某一个角度使得两个物体的影子完全分开中间有缝隙那么我们就可以百分之百确定这两个物体在三维空间中没有碰撞。这个“手电筒照射的角度”在数学上就是一条“分离轴”。而我们要做的就是找到是否存在这样一条轴。对于两个凸多面体AABB和OBB都是凸多面体SAT定理告诉我们如果两个凸体不相交那么必定存在一条分离轴该轴平行于其中一个多面体的某条边或者平行于两个多面体各一条边的叉积方向。对于两个OBB都是长方体我们需要检查的候选分离轴一共有15条第一个OBB的3条本地坐标轴X, Y, Z。第二个OBB的3条本地坐标轴。上述两组轴两两叉积得到的9条轴3x3。但实际上由于叉积可能得到零向量当两条轴平行时以及一些轴是冗余的在实现时我们需要进行归一化和去重处理。在每一条候选分离轴上我们需要进行一个核心操作将两个OBB的所有顶点投影到这条轴上得到两个投影区间然后判断这两个区间是否重叠。这个“投影区间”就是物体在该轴上的“影子”范围。注意浮点数精度问题。这是工业级代码的第一个坑。在比较两个投影区间是否分离时直接写if (maxA minB || maxB minA)是危险的。由于浮点数计算存在舍入误差两个理论上刚好接触的物体可能被误判为分离或相交。通常的解决方案是引入一个微小的容差epsilon例如if (maxA epsilon minB || maxB epsilon minA)。这个epsilon的值需要根据你的场景尺度是毫米级还是千米级谨慎选择。对于AABB事情就简单多了。因为它的边与世界坐标轴平行所以它的分离轴就是固定的X、Y、Z轴。我们只需要在这三条轴上检查投影区间是否重叠即可。这本质上就是比较两个长方体在三个维度上的坐标范围代码就是几个简单的min/max比较速度极快。理解SAT之后我们就掌握了判断OBB是否相交的“尺子”。接下来我们要用C把这把“尺子”造出来并且要造得既精准又耐用。3. 工业级数据结构设计面向性能与缓存代码设计的第一步是定义数据结构。糟糕的数据结构会让再精妙的算法也步履维艰。我们的目标是内存紧凑、访问快速、利于向量化优化。3.1 OBB的经典表示中心、轴向、半长一个OBB最自然的表示是中心点center一个Vector3表示包围盒的中心在世界坐标系中的位置。轴向axes三个互相垂直的Vector3通常为单位向量分别表示OBB的本地X轴、Y轴、Z轴在世界坐标系中的方向。这构成了一个旋转矩阵。半长extents一个Vector3表示从中心点到每个面沿对应轴向的距离即长、宽、高的一半。// 示例一个基础的OBB结构体 struct OBB { Vector3 center; // 中心点 Vector3 axes[3]; // 三个本地轴向最好是归一化的单位向量 Vector3 extents; // 半长 (half-widths) // 可选用于快速访问的变换矩阵 Matrix3x3 rotation; // 由axes构成 // ... 成员函数 };这里有一个关键设计决策是否存储完整的变换矩阵存储axes[3]本质上就是一个3x3旋转矩阵。在很多计算中如顶点变换直接使用矩阵乘法会更方便。我个人的经验是同时存储axes和由它构成的rotation矩阵虽然有一点点冗余但用空间换取了时间。在碰撞检测这种每帧可能调用成千上万次的函数里避免重复计算叉积和矩阵构造是值得的。3.2 AABB的优化表示Min-Max vs Center-ExtentsAABB通常有两种表示法Min-Max表示法用两个Vector3分别存储包围盒在三个轴上的最小坐标和最大坐标。struct AABB_MinMax { Vector3 min; Vector3 max; };Center-Extents表示法类似OBB存储中心点和半长。struct AABB_Center { Vector3 center; Vector3 extents; // 半长此时方向隐含为世界坐标轴 };如何选择Min-Max表示法在更新AABB例如物体移动后时非常直观新的AABB就是所有顶点在新位置下的min/max。而在进行AABB-AABB相交测试时代码也非常简洁return !(a.max.x b.min.x || a.min.x b.max.x || ...)。然而Center-Extents表示法在与现代SIMD指令集如SSE、AVX结合时具有巨大优势。因为相交测试的核心是计算两个区间的重叠情况这可以转化为计算中心距离与半长之和的比较abs(centerA.x - centerB.x) (extentA.x extentB.x)。这种形式的计算非常容易进行向量化并行处理一次能处理4个甚至8个维度比较。在工业级引擎如Unity的Physics、Unreal的Chaos中底层大量使用了SIMD优化。因此我推荐使用Center-Extents表示法为未来的性能优化留出空间。同时可以提供一个成员函数来快速获取min/max以备不时之需。struct AABB { Vector3 center; Vector3 extents; Vector3 getMin() const { return center - extents; } Vector3 getMax() const { return center extents; } // 快速相交测试为SIMD优化做准备 bool intersectsSIMDFriendly(const AABB other) const; };3.3 内存布局与缓存友好性当需要处理成千上万个包围盒时例如一个大型场景的所有静态物体内存访问模式成为瓶颈。如果这些OBB或AABB对象是std::vectorOBB这样存储的并且我们频繁需要访问它们的center和extents来进行相交测试那么结构体数组AoS的布局可能导致缓存利用率低下因为每次加载一个OBB会把整个结构体包括可能暂时用不到的axes拉入缓存。一种高级优化技巧是使用数组结构SoA布局。即将所有包围盒的同类型数据分别存储在连续的数组中// SoA布局示例概念性代码 class OBBBatch { std::vectorVector3 centers; std::vectorVector3 extents; std::vectorMatrix3x3 rotations; // 或存储axes // ... 对整批数据进行批量测试的函数 };这样在进行批量相交测试时算法可以连续地读取所有centers然后所有extents这种顺序访问模式对CPU缓存和预取器极其友好配合SIMD指令能获得数量级的性能提升。这在需要每帧进行大规模Broad-Phase粗略阶段碰撞检测的游戏中是关键技术。4. AABB碰撞检测实现极致的速度与优化AABB检测是碰撞系统中最基础的单元其速度直接决定了整个Broad-Phase的效率。一个朴素的实现如下bool AABB::intersects(const AABB other) const { Vector3 minA getMin(); Vector3 maxA getMax(); Vector3 minB other.getMin(); Vector3 maxB other.getMax(); // 检查在任一轴上是否分离 if (maxA.x minB.x || minA.x maxB.x) return false; if (maxA.y minB.y || minA.y maxB.y) return false; if (maxA.z minB.z || minA.z maxB.z) return false; return true; // 所有轴都重叠则相交 }这个实现清晰易懂但调用了6次getMin()/getMax()涉及12次加减法。我们可以利用Center-Extents表示法进行优化bool AABB::intersectsOptimized(const AABB other) const { // 计算中心点距离的绝对值 Vector3 d (center - other.center).abs(); // 假设Vector3有.abs()成员函数 // 计算半长之和 Vector3 sum extents other.extents; // 只要在一个维度上距离大于半长之和即分离 if (d.x sum.x) return false; if (d.y sum.y) return false; if (d.z sum.z) return false; return true; }这个版本减少了计算量。但真正的工业级优化会走得更远1. SIMD向量化使用像Eigen、GLM这样的数学库或者直接使用编译器 intrinsics可以一次性完成三个维度的比较。#include xmmintrin.h // SSE bool AABB::intersectsSSE(const AABB other) const { // 假设center和extents是__m128类型 __m128 d _mm_sub_ps(center, other.center); d _mm_andnot_ps(_mm_set1_ps(-0.0f), d); // 取绝对值andnot(负零, x) __m128 s _mm_add_ps(extents, other.extents); __m128 cmp _mm_cmpgt_ps(d, s); // 比较 d s int mask _mm_movemask_ps(cmp); return mask 0; // 所有比较位都为0表示都不大于即相交 }2. 提前返回与分支预测上面的代码使用了三个条件判断。在现代CPU上分支预测失败代价很高。一种“无分支”的尝试是将比较结果合并return (d.x sum.x) (d.y sum.y) (d.z sum.z); // 注意是位与不是逻辑与但这需要将布尔条件转换为整型通常是0或1并且所有比较都必须执行可能不如有良好预测模式的分支高效。通常编译器在开启优化如-O3后能很好地处理简单的连续if语句。我的建议是先写出最清晰的代码用性能分析工具如VTune、perf定位热点再考虑这种微优化。3. 合并AABBBounding Volume Hierarchy, BVH构建在动态场景中物体移动后需要更新其AABB。更高级的应用是构建BVH树其中父节点的AABB需要包围合并其子节点的AABB。合并两个AABB的算法也必须高效AABB merge(const AABB a, const AABB b) { Vector3 newMin Vector3::min(a.getMin(), b.getMin()); Vector3 newMax Vector3::max(a.getMax(), b.getMax()); return AABB::fromMinMax(newMin, newMax); // 一个从min/max构造的工厂函数 }这个操作在BVH的构建和更新中会被频繁调用。5. OBB碰撞检测实现SAT的工程化落地OBB的检测是重头戏。我们将SAT理论转化为代码。核心步骤是遍历15条候选分离轴在每条轴上计算投影并判断区间是否重叠。5.1 基础实现框架首先我们需要一个函数来计算一个OBB在给定轴单位向量上的投影半径即从中心到最远点的投影距离。对于一个中心在c半长为e轴向为u,v,w的OBB在轴L上的投影半径为radius e.x * |L·u| e.y * |L·v| e.z * |L·w|其中·表示点积| |表示绝对值。这个公式的几何意义是将OBB的半长在其各个轴向上的分量投影到测试轴L上然后求和。float OBB::getProjectionRadius(const Vector3 axis) const { // 确保axis是单位向量或者在此处归一化 return extents.x * std::abs(axis.dot(axes[0])) extents.y * std::abs(axis.dot(axes[1])) extents.z * std::abs(axis.dot(axes[2])); }有了这个工具函数OBB相交测试的主体逻辑如下bool OBB::intersects(const OBB other) const { // 1. 计算中心点向量 Vector3 centerVec other.center - center; // 从this指向other // 2. 检查15条分离轴 // 为了方便我们定义this的轴为A[0], A[1], A[2]other的轴为B[0], B[1], B[2] // 轴1-3: A的本地轴 for (int i 0; i 3; i) { Vector3 L axes[i]; float rThis extents[i]; // 在自身轴上投影半径就是半长 float rOther other.getProjectionRadius(L); float distance std::abs(centerVec.dot(L)); if (distance rThis rOther EPSILON) return false; } // 轴4-6: B的本地轴 for (int i 0; i 3; i) { Vector3 L other.axes[i]; float rThis this-getProjectionRadius(L); float rOther other.extents[i]; float distance std::abs(centerVec.dot(L)); if (distance rThis rOther EPSILON) return false; } // 轴7-15: A的轴与B的轴的叉积 (共9条) for (int i 0; i 3; i) { for (int j 0; j 3; j) { Vector3 L axes[i].cross(other.axes[j]); // 重要如果两个轴平行或接近平行叉积结果接近零向量需要跳过 float lengthSq L.lengthSquared(); if (lengthSq 1e-8f) continue; // 跳过零向量或接近零的向量 L.normalize(); // 需要归一化因为我们用的是点积 float rThis this-getProjectionRadius(L); float rOther other.getProjectionRadius(L); float distance std::abs(centerVec.dot(L)); if (distance rThis rOther EPSILON) return false; } } // 所有轴上都未发现分离则相交 return true; }5.2 关键优化与工程细节上面的基础实现正确但效率不高。以下是提升到工业级的关键点1. 预计算与缓存每次检测都计算15次点积、绝对值、叉积和归一化开销巨大。一个常见的优化是预计算旋转矩阵。在OBB创建或更新时我们不仅存储三个轴向axes还预计算一个3x3的旋转矩阵R以及它的转置R^T。同时可以预计算两个OBB之间的相对旋转矩阵C A.R^T * B.R。这样在计算投影半径时很多点积运算就变成了矩阵元素的直接访问省去了大量的点积计算。2. 分离轴计算的简化在预计算了矩阵C后15条轴的测试可以简化为对矩阵C元素的操作。例如测试轴A[i]时|L·B[j]|其实就是|C[i][j]|。这被称为“基于矩阵的SAT实现”是诸如《Real-Time Collision Detection》等经典书籍中介绍的标准高效算法。它能将大量动态计算转化为静态数据访问。3. 提前归一化与容差处理注意叉积轴L需要归一化。我们可以在循环外预先计算好所有可能的、归一化后的叉积轴吗理论上可以但9条轴里有很多会因为原始轴平行而被跳过。动态计算并缓存结果可能更划算。另外EPSILON容差的选择至关重要太小了会有漏报太大了会有误报。通常根据场景的单位尺度来定例如1e-4或1e-5。4. 最坏情况与性能考量OBB检测即使在优化后也比AABB慢一个数量级以上。因此绝对不要直接用OBB去检测场景中的所有物体对。标准的管线是Broad Phase粗略阶段使用空间数据结构如BVH、四叉树、八叉树或简单算法如Sweep and Prune利用AABB快速找出所有可能相交的物体对候选列表。Narrow Phase精确阶段对Broad Phase产生的候选对依次进行OBB检测。如果物体形状更复杂OBB检测通过后可能还需要进行更精确的凸包甚至三角面片检测。6. 性能对比与实战场景选择指南了解了实现我们更需要知道何时该用谁。下面这个表格总结了AABB和OBB的核心特性对比特性AABB (轴对齐包围盒)OBB (有向包围盒)紧密性差尤其物体旋转后好始终贴合物体方向相交测试速度极快(3次区间比较)慢(最多15次投影测试)更新成本低 (合并顶点即可)高 (需要根据变换重新计算轴向)内存占用小 (中心半长或minmax)较大 (中心3个轴向半长)适用场景Broad-Phase、动态物体、形状近似轴对齐的物体Narrow-Phase、静态或少旋转的复杂物体、需要高剔除精度的场合实战选择策略动态 vs 静态物体对于位置和旋转频繁变化的动态物体每帧更新其OBB的代价很高。一个折中方案是为动态物体使用AABB为静态物体使用OBB。在Broad Phase用动态物体的AABB和静态物体的OBB的AABB即外包AABB进行快速筛选在Narrow Phase再对静态物体使用精确的OBB。或者对动态物体使用动态AABB树。形状特性对于本身就是轴对齐或接近轴对齐的物体如墙壁、地板、方盒子AABB的紧密性已经足够好绝对首选AABB。对于长条形且可能旋转的物体如剑、棍子、机械臂OBB的优势非常明显。层次包围体BVH在构建BVH时父节点包围盒的选择至关重要。虽然OBB更紧密但合并两个OBB生成一个新OBB的算法复杂且结果不一定是最优OBB。因此BVH的节点通常使用AABB因为它合并快速且测试简单即使紧密性差一些但树结构的层次性能有效弥补。内存与缓存考量在移动端或对内存带宽敏感的场景需要评估大量OBB带来的缓存压力。有时使用球体包围盒Sphere进行最粗粒度的筛选再用AABB最后用OBB形成多级过滤可能是更均衡的策略。我的踩坑经验曾在一个机器人仿真项目中对所有零件都用OBB做Broad Phase结果帧率暴跌。分析后发现80%的OBB检测都浪费在了明显远离的物体对上。后来改为第一级用球体只比较距离第二级用AABB第三级用OBB性能提升了20倍以上。记住碰撞检测的第一原则是“快速拒绝”。7. 高级话题与代码健壮性7.1 浮点数误差处理全攻略浮点数误差是碰撞检测中“幽灵bug”的主要来源。除了前面提到的分离判断容差EPSILON还有更多陷阱归一化零向量在计算叉积轴时如果两个轴平行叉积结果接近零。调用normalize()会导致除零或产生NaN非数。必须检查向量长度或长度的平方是否大于一个极小阈值。矩阵正交性漂移OBB的三个轴向理论上应互相垂直且为单位长。但在长时间的物理模拟或数值积分中由于累积误差它们可能不再正交。定期或在使用前进行正交化处理如施密特正交化是必要的但这会带来额外开销。一种方案是存储旋转四元数Quaternion而非矩阵四元数更容易保持归一化。穿透深度计算如果检测到碰撞通常还需要计算穿透深度和方向用于物理反馈。SAT算法可以扩展来提供这些信息即找到重叠最小的那条轴及其重叠量。但计算重叠量时浮点误差可能导致方向相反。需要仔细处理符号。7.2 与其他形状的碰撞检测AABB和OBB很少单独使用它们是复杂碰撞形状的“守卫”。AABB/OBB vs 球体非常快速。对于球体只需比较圆心到包围盒最近点的距离与球半径。AABB/OBB vs 射线/线段用于鼠标拾取、子弹射击等。有标准的 slabs 方法。AABB/OBB vs 平面用于室内场景的视锥体剔除、地面碰撞等。AABB/OBB vs 三角形网格这就是我们说的精确检测。通常先用包围盒快速剔除大量不相交的三角形再对剩余的少数三角形进行精确的射线-三角形或三角形-三角形相交测试。7.3 测试与调试可视化是王道碰撞检测的Bug难以通过日志定位。“看到”发生了什么至关重要。绘制包围盒在Debug模式下用线条绘制出所有AABB和OBB的轮廓。观察它们是否与物体模型贴合在运动时是否正确更新。高亮碰撞对当检测到碰撞时用不同颜色高亮发生碰撞的两个包围盒甚至暂停游戏/仿真逐帧检查。分离轴可视化对于OBB可以绘制出导致分离的那条轴如果存在或者绘制出所有测试轴及投影区间这对于理解算法和调试极端情况无比有用。8. 从理论到工业集成与最佳实践最后我们来谈谈如何将这套碰撞检测模块打造成一个工业级系统中可靠的部件。1. 接口设计设计一个清晰的基类CollisionShape然后派生出AABBShape,OBBShape,SphereShape,CapsuleShape等。使用多态或类型标识符如enum来分发不同的碰撞检测函数。提供一个统一的CollisionDetector静态类里面包含各种形状两两相交的静态函数如bool Check(const AABB, const OBB)这是策略模式的应用。2. 数据驱动与序列化包围盒数据中心、轴向、半长应该能够随着场景资源一起被序列化保存/加载。对于静态物体可以在资源预处理阶段就计算好其OBB运行时直接加载使用避免实时计算。3. 与物理引擎的集成如果你是在自研物理引擎那么碰撞检测模块是其核心。你需要管理一个“碰撞对”列表由Broad Phase生成由Narrow Phase消费并产生“碰撞信息”接触点、法线、穿透深度传递给后续的碰撞解析求解约束阶段。 如果你是在使用现有的引擎如Bullet、PhysX通常你不需要自己实现OBB引擎提供了高度优化的btBoxShape或PxBoxGeometry其内部实现原理与我们讨论的SAT一致但经过了极致的优化和平台适配。4. 性能剖析Profiling永远是朋友永远不要猜测性能瓶颈在哪里。使用性能分析工具精确找出是Broad Phase慢还是Narrow Phase慢是OBB的SAT计算慢还是矩阵更新慢。优化要有的放矢。实现一个健壮、高效的AABB/OBB碰撞检测系统是深入理解三维空间几何和性能优化的一堂大师课。它涉及的远不止是数学公式更多的是在速度、精度、内存和工程复杂度之间寻找最佳平衡点的艺术。希望这篇从原理到实战的拆解能帮你避开我当年踩过的那些坑写出真正属于工业级的碰撞代码。