游戏引擎自研碰撞检测:八叉树与AABB的C++实现与优化

📅 2026/8/9 17:51:23
游戏引擎自研碰撞检测:八叉树与AABB的C++实现与优化
1. 项目概述为什么要在游戏引擎里自研碰撞检测做游戏引擎尤其是3D游戏引擎碰撞检测是绕不开的核心组件。它直接决定了游戏世界的物理真实感、角色交互的流畅度以及最终玩家的体验。市面上有很多成熟的物理引擎比如Bullet、PhysX直接集成它们似乎是最快、最稳妥的选择。但当你真正深入一个自研引擎项目尤其是对性能、内存布局、跨平台一致性有极致要求时外部的“黑盒”往往会成为瓶颈。我这次要分享的就是在我们自研的C游戏引擎中从零开始实现一个基于八叉树和AABB轴对齐包围盒的碰撞检测组件的全过程。这不仅仅是“实现一个算法”更是对引擎底层数据结构、空间划分策略、以及并行计算架构的一次深度整合。选择八叉树AABB是因为它在动态场景的宽阶段Broad Phase碰撞检测中在性能和实现复杂度之间取得了非常好的平衡。AABB计算简单八叉树能高效管理三维空间对象两者结合非常适合处理游戏中大量动态物体的碰撞初筛。这个组件最终的目标是给定一个充满各种动态、静态物体的3D场景能够快速、准确地筛选出所有可能发生碰撞的物体对交给后续的窄阶段Narrow Phase进行精确的三角形级或形状级检测。整个过程我们从最基础的AABB结构体开始一步步构建八叉树并实现基于该树的碰撞查询。我会把其中关键的思路、踩过的坑以及针对性能的优化技巧都摊开来讲。2. 核心架构与数据结构设计在动手写代码之前好的数据结构设计是成功的一半。碰撞检测系统特别是结合了空间划分算法的系统对数据局部性、内存访问模式非常敏感。一个糟糕的设计可能会让CPU缓存效率低下从而抵消了算法本身的优势。2.1 AABB包围盒一切碰撞检测的起点AABB即轴对齐包围盒是一个各边都平行于坐标轴的立方体或长方体。它用两个三维向量就能完整定义一个最小点min一个最大点max。在C中我们的基础结构体是这样定义的struct AABB { glm::vec3 min; // 使用glm数学库包含x, y, z最小值 glm::vec3 max; // x, y, z最大值 // 默认构造函数 AABB() : min(FLT_MAX), max(-FLT_MAX) {} // 通过最小/最大点构造 AABB(const glm::vec3 min, const glm::vec3 max) : min(min), max(max) {} // 最重要的方法扩展AABB以包含一个点 void expand(const glm::vec3 point) { min glm::min(min, point); max glm::max(max, point); } // 扩展AABB以包含另一个AABB void expand(const AABB other) { min glm::min(min, other.min); max glm::max(max, other.max); } // 判断两个AABB是否相交宽阶段检测的核心 bool intersects(const AABB other) const { return (max.x other.min.x min.x other.max.x) (max.y other.min.y min.y other.max.y) (max.z other.min.z min.z other.max.z); } // 计算AABB的中心点常用于八叉树划分 glm::vec3 getCenter() const { return (min max) * 0.5f; } // 计算AABB的尺寸长宽高 glm::vec3 getExtents() const { return (max - min) * 0.5f; } };注意这里使用了glm::vec3它是GLM数学库的一部分在图形和游戏开发中几乎是标准选择。如果你不想引入额外依赖也可以自己实现一个简单的Vec3类包含基本的加减乘除和比较操作。为什么选择AABB而不是OBBOBB有向包围盒虽然更紧密地包裹物体能减少“漏报”但它的相交测试计算量远大于AABB需要分离轴定理SAT最多15次测试。在宽阶段我们的首要任务是快速剔除绝对不可能碰撞的物体对而不是精确判断。AABB的相交测试只需6次比较速度极快。而且当物体旋转时OBB需要实时更新方向计算更复杂而AABB虽然会变大包裹旋转后的OBB但更新简单只需重新计算顶点极值即可。对于动态场景这种“用空间换时间”的策略往往是更优的。2.2 碰撞体抽象连接游戏对象与物理系统在引擎中游戏对象GameObject可能携带一个或多个碰撞体Collider。我们需要一个基类来抽象不同类型的碰撞体Box, Sphere, Mesh等但它们都必须能提供自己的AABB。这是八叉树能够统一管理的基础。class Collider { public: virtual ~Collider() default; // 获取当前帧的世界空间AABB virtual AABB getWorldAABB() const 0; // 获取关联的游戏对象ID或指针用于碰撞回调 virtual GameObject* getGameObject() const 0; // 窄阶段精确碰撞检测与其他碰撞体这里先留空 virtual bool intersects(const Collider* other) const { return false; } // 标记是否为静态物体优化用 bool isStatic false; };例如一个简单的球体碰撞体实现class SphereCollider : public Collider { public: glm::vec3 center; // 局部坐标中心 float radius; AABB getWorldAABB() const override { GameObject* obj getGameObject(); glm::vec3 worldCenter obj-transform-applyTransform(center); glm::vec3 extent(radius); return AABB(worldCenter - extent, worldCenter extent); } // ... 其他方法 };2.3 八叉树节点设计空间与数据的桥梁八叉树节点的设计直接决定了树的构建、更新和查询效率。我们的节点需要存储它代表的空间区域一个AABB。落入该区域的碰撞体列表或索引。指向其八个子节点的指针或索引。一些辅助信息如深度、是否已分裂等。struct OctreeNode { AABB bounds; // 该节点对应的空间范围 std::vectorCollider* colliders; // 当前节点内的碰撞体非子节点中的 OctreeNode* children[8]; // 八个子节点指针nullptr表示未分裂 OctreeNode* parent; int depth; // 节点深度根节点为0 // 判断一个碰撞体是否完全包含在本节点边界内 bool fullyContains(const Collider* collider) const { const AABB colAABB collider-getWorldAABB(); return bounds.min.x colAABB.min.x bounds.max.x colAABB.max.x bounds.min.y colAABB.min.y bounds.max.y colAABB.max.y bounds.min.z colAABB.min.z bounds.max.z colAABB.max.z; } // 判断一个碰撞体的AABB是否与本节点区域相交 bool intersects(const AABB aabb) const { return bounds.intersects(aabb); } // 析构时递归释放子节点内存 ~OctreeNode() { for (int i 0; i 8; i) { delete children[i]; children[i] nullptr; } } };实操心得使用对象池管理节点在动态场景中八叉树节点可能会频繁创建和销毁尤其是物体移动导致树结构变化时这容易引起内存碎片和性能抖动。一个高级的优化是使用对象池Object Pool。我们可以预先分配一大块连续内存来存储OctreeNode并用一个空闲列表管理。节点“销毁”时只是放回池中标记为可用而非真正调用delete。这能极大提升运行时性能特别是在主机或移动平台。实现时可以将children指针数组改为存储池内索引int进一步优化内存访问。3. 八叉树的构建与动态更新策略有了数据结构接下来就是如何将场景中的一堆碰撞体高效地组织进一棵八叉树里。这里分为初始构建和动态更新两部分。3.1 树的初始化与递归划分构建八叉树的第一步是确定根节点的边界。一个常见的做法是遍历所有碰撞体计算一个能包裹所有AABB的全局边界。class Octree { public: OctreeNode* root nullptr; int maxDepth 8; // 最大深度限制防止过度细分 int maxObjectsPerNode 10; // 节点内物体数量阈值超过则分裂 // 根据碰撞体列表构建八叉树 void build(const std::vectorCollider* allColliders) { // 1. 计算全局边界 AABB worldBounds; for (auto collider : allColliders) { worldBounds.expand(collider-getWorldAABB()); } // 给边界增加一点容差避免物体刚好在边界上 glm::vec3 padding(1.0f); worldBounds.min - padding; worldBounds.max padding; // 2. 创建根节点 root new OctreeNode(); root-bounds worldBounds; root-depth 0; // 3. 递归插入所有碰撞体 for (auto collider : allColliders) { insert(root, collider); } } private: // 递归插入碰撞体到节点 void insert(OctreeNode* node, Collider* collider) { // 情况1如果当前节点是叶子节点未分裂 if (node-children[0] nullptr) { // 将碰撞体加入当前节点列表 node-colliders.push_back(collider); // 检查是否需要分裂节点物体数量超限且未达到最大深度 if (node-colliders.size() maxObjectsPerNode node-depth maxDepth) { splitNode(node); } return; } // 情况2当前节点已分裂尝试将碰撞体放入子节点 const AABB colAABB collider-getWorldAABB(); for (int i 0; i 8; i) { if (node-children[i]-fullyContains(collider)) { // 如果能完全放入某个子节点则递归插入 insert(node-children[i], collider); return; } } // 情况3碰撞体横跨多个子节点留在当前节点 node-colliders.push_back(collider); } // 分裂节点创建八个子节点 void splitNode(OctreeNode* node) { glm::vec3 center node-bounds.getCenter(); glm::vec3 halfExtents node-bounds.getExtents() * 0.5f; for (int i 0; i 8; i) { node-children[i] new OctreeNode(); node-children[i]-parent node; node-children[i]-depth node-depth 1; // 计算第i个子节点的边界 // i的二进制位xyz分别表示在中心点的哪一侧 (0为负1为正) glm::vec3 childMin, childMax; childMin.x (i 1) ? center.x : node-bounds.min.x; childMax.x (i 1) ? node-bounds.max.x : center.x; childMin.y (i 2) ? center.y : node-bounds.min.y; childMax.y (i 2) ? node-bounds.max.y : center.y; childMin.z (i 4) ? center.z : node-bounds.min.z; childMax.z (i 4) ? node-bounds.max.z : center.z; node-children[i]-bounds AABB(childMin, childMax); } // 将当前节点中的碰撞体重新分配到子节点或留在本节点 std::vectorCollider* toRedistribute std::move(node-colliders); node-colliders.clear(); for (auto collider : toRedistribute) { // 尝试放入子节点 bool placedInChild false; for (int i 0; i 8; i) { if (node-children[i]-fullyContains(collider)) { insert(node-children[i], collider); placedInChild true; break; } } // 如果横跨多个子节点则留回本节点 if (!placedInChild) { node-colliders.push_back(collider); } } } };关键参数解析maxDepth和maxObjectsPerNodemaxDepth限制树的最大深度防止因一个非常小的物体导致无限细分。通常设置为6-10具体取决于场景尺度。深度每增加一级节点数量呈8倍增长内存消耗需警惕。maxObjectsPerNode节点分裂的阈值。设置太小会导致树过深增加遍历开销设置太大会降低空间划分的效率增加节点内碰撞检测的负担。通常需要根据场景中物体的平均大小和数量进行性能测试后确定10-20是一个常见的起始值。3.2 动态物体的更新与树的重构游戏中的物体是运动的。一个简单的做法是每一帧都销毁整棵树然后重建rebuild。这对于完全动态的场景是可行的但效率低下。更优的策略是增量更新。更新标记每个Collider记录它上一帧的AABBlastWorldAABB。检查移动每一帧比较当前AABB和上一帧AABB。如果变化超过某个阈值或直接使用相交测试判断是否还在原节点边界内则标记该碰撞体为“脏”。从树中移除将“脏”碰撞体从其当前所在的节点列表中移除。注意一个碰撞体可能被记录在多个节点如果它横跨节点边界。重新插入将“脏”碰撞体重新插入八叉树调用insert函数。节点合并优化当一个节点的所有子节点中的物体数量总和很少远低于maxObjectsPerNode并且该节点本身物体也不多时可以考虑合并删除子节点将子节点中的物体提升到本节点。这可以防止物体离开后留下大量空节点。void Octree::updateDynamicObjects() { std::vectorCollider* dirtyColliders; // 遍历所有动态碰撞体检查AABB是否变化显著 for (auto collider : allDynamicColliders) { AABB currentAABB collider-getWorldAABB(); if (!currentAABB.intersects(collider-lastWorldAABB) || glm::distance(currentAABB.getCenter(), collider-lastWorldAABB.getCenter()) moveThreshold) { dirtyColliders.push_back(collider); collider-lastWorldAABB currentAABB; // 更新记录 } } // 从树中移除脏碰撞体需要实现一个从节点中移除指定碰撞体的函数 for (auto collider : dirtyColliders) { removeColliderFromTree(root, collider); } // 重新插入脏碰撞体 for (auto collider : dirtyColliders) { insert(root, collider); } // 可选尝试合并稀疏节点 tryMergeNodes(root); }踩坑实录更新阈值的选择直接比较AABB是否相交intersects作为移动判断可能过于敏感物体会因为浮点数误差或微小程序抖动而被频繁标记为“脏”。我最初就遇到了这个问题导致更新开销巨大。后来引入了移动阈值和AABB中心点距离判断。只有当物体移动超过一定距离例如其包围盒尺寸的5%才认为它需要更新在树中的位置。这个阈值需要根据游戏场景的尺度和物体运动速度来微调。4. 基于八叉树的碰撞查询实现树建好了更新也搞定了最终目的是用它来快速找出潜在的碰撞对。碰撞查询通常有两种形式一对多查询给定一个碰撞体找出所有可能与它碰撞的物体和全局查询找出场景中所有可能碰撞的物体对。我们主要实现前者后者可以通过遍历所有物体并调用前者来实现虽然还有更优的全对检测算法如Sweep and Prune结合八叉树。4.1 递归查询算法查询的基本思想是从根节点开始递归地向下搜索如果查询碰撞体AABB与当前节点边界不相交则直接返回该节点分支无需继续。如果相交则检查当前节点存储的所有碰撞体将与查询AABB相交的碰撞体加入结果列表。如果当前节点有子节点则对每一个子节点递归执行步骤1和2。void Octree::queryCollisions(OctreeNode* node, const AABB queryAABB, std::vectorCollider* outResults) const { // 剪枝如果查询范围与本节点区域不相交直接返回 if (!node-bounds.intersects(queryAABB)) { return; } // 检查本节点内存储的碰撞体这些是横跨子节点或完全在本节点的物体 for (auto collider : node-colliders) { if (collider-getWorldAABB().intersects(queryAABB)) { outResults.push_back(collider); } } // 如果本节点是叶子节点则结束 if (node-children[0] nullptr) { return; } // 递归查询所有子节点 for (int i 0; i 8; i) { queryCollisions(node-children[i], queryAABB, outResults); } } // 对外接口查询可能与给定碰撞体发生碰撞的所有其他碰撞体 std::vectorCollider* Octree::queryPotentialCollisions(Collider* collider) const { std::vectorCollider* results; AABB queryAABB collider-getWorldAABB(); queryCollisions(root, queryAABB, results); // 注意结果中可能包含自己需要在外部过滤掉 return results; }4.2 宽阶段碰撞检测全流程将查询功能整合就形成了完整的宽阶段碰撞检测流程class CollisionSystem { Octree octree; std::vectorCollider* allColliders; public: void update(float deltaTime) { // 1. 更新所有动态碰撞体的变换并计算新的AABB updateColliderTransforms(); // 2. 增量更新八叉树处理移动的物体 octree.updateDynamicObjects(); // 3. 执行宽阶段碰撞检测 std::vectorColliderPair broadPhasePairs; for (auto collider : allColliders) { if (collider-isStatic) { // 静态物体通常作为被查询方不需要主动查询所有可优化 continue; } auto potentialColliders octree.queryPotentialCollisions(collider); for (auto other : potentialColliders) { // 避免重复添加碰撞对 (A, B) 和 (B, A) if (collider other) { // 利用指针地址比较来生成唯一对 broadPhasePairs.emplace_back(collider, other); } } } // 4. 将潜在的碰撞对传递给窄阶段进行精确检测 narrowPhaseDetection(broadPhasePairs); } void narrowPhaseDetection(const std::vectorColliderPair pairs) { for (const auto pair : pairs) { if (pair.first-intersects(pair.second)) { // 触发碰撞事件通知游戏逻辑 onCollision(pair.first, pair.second); } } } };性能考量上面的全遍历查询在物体很多时O(N)仍有优化空间。更高效的全对检测可以使用扫描与剪枝Sweep and Prune算法它利用AABB在坐标轴上的投影的连贯性能在接近O(N)的时间复杂度内找出重叠对常与八叉树结合使用先用八叉树将空间分区在每个分区内再用Sweep and Prune。5. 高级优化与并行化探索当场景中物体数量达到数千甚至上万时即使是O(N log N)的算法也可能成为瓶颈。现代CPU都是多核心的我们必须考虑并行化。5.1 多线程八叉树更新与查询八叉树的结构天生适合并行。例如在更新动态物体时检查每个物体是否移动updateDynamicObjects中的第一步可以完全并行。重新插入“脏”物体到树中则需小心因为插入操作可能修改树结构分裂节点需要加锁或使用无锁数据结构这可能会成为瓶颈。一个更实用的并行策略是并行构建如果采用每帧重建Rebuild策略可以并行地计算全局AABB然后并行地对碰撞体列表进行排序和划分最后并行地构建子树。这类似于并行快速排序的思想。并行查询queryPotentialCollisions对于不同的碰撞体是完全独立的可以轻松地放入线程池并行执行。这是收益最明显的部分。// 使用C17的并行算法或线程池 void CollisionSystem::parallelBroadPhase() { std::vectorColliderPair pairs; std::mutex pairsMutex; // 假设我们有一个线程池 pool for (auto collider : allDynamicColliders) { pool.enqueue([collider, pairs, pairsMutex, this]() { auto potential octree.queryPotentialCollisions(collider); std::vectorColliderPair localPairs; for (auto other : potential) { if (collider other) { localPairs.emplace_back(collider, other); } } // 合并结果时需要加锁 std::lock_guardstd::mutex lock(pairsMutex); pairs.insert(pairs.end(), localPairs.begin(), localPairs.end()); }); } pool.wait(); // 等待所有查询任务完成 // 后续进行窄阶段检测... }5.2 基于GPU的并行八叉树构建与碰撞检测正如参考内容中提到的利用GPU如CUDA进行并行八叉树构建和碰撞检测能将性能提升一个数量级。其核心思想是自底向上Bottom-Up构建和基于Z-order曲线Morton Code的排序。核心步骤简述计算Morton Code为每个碰撞体的AABB中心点计算一个64位或32位的Morton码。该码将三维空间位置编码成一维的整数保持了空间局部性。并行排序使用GPU的并行排序算法如Radix Sort对所有碰撞体按其Morton码进行排序。并行构建树排序后的列表相邻的物体在空间上也接近。通过并行地比较相邻物体的Morton码可以找到它们的最近公共祖先LCA从而自底向上地构建出八叉树的内部节点。这个过程完全并行每个GPU线程处理一对相邻的叶子节点。并行碰撞检测构建好的八叉树结构通常以数组形式存储在GPU内存中可以用于并行碰撞检测。每个线程负责一个碰撞体沿着树向下遍历快速找到与其AABB相交的节点并收集潜在的碰撞对。重要提示GPU并行方案虽然高效但实现复杂涉及GPU内存管理、内核函数编写、以及CPU-GPU数据同步。它更适合在碰撞体数量极大数万以上、且每帧都需要完全重建树的场景。对于大多数中小型游戏项目优化良好的CPU多线程方案已经足够。我建议先实现并优化CPU版本在确实遇到性能瓶颈且 profiling 确认碰撞检测是热点后再考虑GPU方案。5.3 内存布局优化面向数据的设计DOD传统的面向对象设计OOD在这里可能导致缓存不友好。例如遍历std::vectorCollider*实际上是在跳跃访问分散在堆内存中的各个Collider对象。面向数据设计Data-Oriented Design, DOD的思路是将数据按使用方式连续存储。将AABB数据连续存储创建一个std::vectorAABB所有碰撞体的AABB按索引连续存储。八叉树节点不再存储Collider*而是存储AABB的索引。这样在遍历节点进行相交测试时CPU缓存命中率会大幅提升。SoAStruct of Arrays对于碰撞体属性可以用多个并行数组来存储例如vectorglm::vec3 centers,vectorfloat radii对于球体而不是一个vectorSphereCollider。// DOD风格的数据存储 struct CollisionData { std::vectorAABB aabbs; // 连续存储的AABB std::vectorint objectIds; // 对应的游戏对象ID std::vectorbool isStatic; // 静态标志 // ... 其他按需添加的属性数组 }; // 八叉树节点存储索引 struct OctreeNodeDOD { AABB bounds; std::vectorint colliderIndices; // 存储的是在CollisionData中的索引 int children[8]; // 存储子节点在节点数组中的索引-1表示无子节点 };这种改造需要大幅调整代码结构但对于性能关键的系统带来的提升是显著的。它让数据流动更符合CPU的“预期”减少了缓存未命中Cache Miss。6. 调试、性能剖析与常见问题实现完功能只是第一步让它稳定、高效地运行才是真正的挑战。6.1 可视化调试看见你的八叉树在3D渲染器中可视化八叉树的边界框是调试的终极利器。你可以为每个OctreeNode的bounds生成一个线框立方体并渲染出来。void Octree::debugDraw(OctreeNode* node, DebugRenderer* debugRenderer) const { if (!node) return; // 绘制当前节点边界例如用白色线框 debugRenderer-drawAABB(node-bounds, glm::vec3(1.0f, 1.0f, 1.0f)); // 递归绘制子节点 for (int i 0; i 8; i) { if (node-children[i]) { // 可以给不同深度的节点用不同颜色 glm::vec3 color getColorByDepth(node-children[i]-depth); debugRenderer-drawAABB(node-children[i]-bounds, color); debugDraw(node-children[i], debugRenderer); } } }通过可视化你可以一目了然地看到树的划分是否均匀合理物体是否正确地被分配到了节点中一个物体应该只出现在它完全包含的最深的节点或者横跨的父节点。动态更新后树的结构是否正确变化6.2 性能剖析与参数调优使用性能分析工具如Visual Studio Profiler, VerySleepy, Tracy来定位热点。热点函数很可能是AABB::intersects、Octree::queryCollisions的递归调用、或者std::vector的push_back。关键指标树构建时间每帧重建 vs 增量更新的开销。单次查询平均耗时随着物体数量增加的变化曲线。缓存命中率通过DOD优化后观察L1/L2缓存未命中率是否下降。调优maxDepth和maxObjectsPerNode没有银弹参数。你需要针对你的典型游戏场景如一个RTS游戏的战场一个FPS游戏的地图进行测试。记录不同参数下平均查询时间和内存占用的变化找到一个平衡点。6.3 常见问题与解决方案速查表问题现象可能原因解决方案碰撞检测漏报该碰的没碰1. AABB计算错误未包含物体全部顶点。2. 动态物体移动后八叉树未及时更新isStatic标志错误或更新阈值太大。3. 物体横跨多个八叉树节点但查询时只检查了部分节点。1. 确保getWorldAABB()正确计算了变换后的所有顶点。2. 检查动态物体的更新逻辑可视化其AABB和所在节点。3. 确保查询算法递归检查了所有相交的子节点。碰撞检测误报不该碰的报了宽阶段正常现象AABB本身比物体大。依赖窄阶段进行精确检测来过滤。确保窄阶段算法如球体、OBB、三角形相交测试正确。帧率随着物体增加急剧下降1. 算法复杂度高未利用空间划分优势如maxObjectsPerNode设置过大。2. 内存访问模式差缓存未命中高。3. 每帧都在重建整棵树。1. 调整八叉树参数确保树深度适中。2. 考虑采用DOD优化内存布局。3. 改为增量更新或降低静态树的更新频率。物体移动时画面卡顿增量更新中removeColliderFromTree或insert操作可能触发节点分裂/合并导致单帧开销突增。1. 将树的更新操作分摊到多帧进行时间切片。2. 限制每帧最多分裂/合并的节点数量。3. 使用对象池避免内存分配峰值。内存占用过高1.maxDepth设置过大产生过多节点。2. 节点中std::vectorCollider*的预留空间过多。1. 降低maxDepth。2. 对colliders向量使用shrink_to_fit()。3. 考虑使用更紧凑的数据结构存储索引。7. 集成到游戏引擎与后续扩展实现一个独立的碰撞检测组件很棒但要让它在一个真正的游戏引擎里发挥作用还需要做好“对接”。与场景图Scene Graph集成你的Collider需要绑定到场景图中的实体Entity或组件Component。通常这会是一个CollisionComponent引擎在更新场景图变换后会自动更新该组件的世界变换并标记其碰撞体为“需要更新”。事件系统当窄阶段检测到碰撞后不应直接处理游戏逻辑。应该通过引擎的事件系统Event System发送一个CollisionEvent包含发生碰撞的两个实体/组件信息。游戏逻辑脚本或其他系统监听这些事件并做出反应。物理材质与响应扩展Collider增加物理材质属性如摩擦力、弹性系数。碰撞检测系统可以生成碰撞信息接触点、法线、穿透深度传递给物理响应系统如刚体动力学来计算碰撞后的速度和位置变化。射线检测Raycasting八叉树同样可以高效支持射线与场景的求交。实现一个raycast函数递归地检查射线与节点AABB的相交快速跳过空区域最终找到射线击中的第一个物体。视锥体剔除Frustum Culling渲染系统需要知道哪些物体在摄像机视野内。利用八叉树可以高效地进行视锥体与节点AABB的相交测试快速剔除大量不可见物体提升渲染性能。从零开始实现一个基于八叉树的碰撞检测组件是一次对3D游戏引擎底层核心机制的深度之旅。它强迫你去思考空间、数据、算法和硬件之间的协同。虽然过程充满挑战但当你看到自己引擎中的物体流畅、准确地碰撞交互时那种成就感是无可替代的。这个组件不是一个孤立的模块它的优化思路如空间划分、并行计算、DOD会深刻影响你对整个引擎架构的理解。