NSDT工具推荐Three.js AI纹理开发包 - YOLO合成数据生成器 - GLTF/GLB在线编辑 - 3D模型格式在线转换 - 可编程3D场景编辑器 - REVIT导出3D模型插件 - 3D模型语义搜索引擎 - Three.js虚拟轴心开发包 - 3D模型在线减面 - STL模型在线切割

最近,我一直在努力研究我的3D引擎Storm3D。 我花费大量时间的功能之一是开发一种通用且高效的八叉树数据结构,它将用于从碰撞检测到基于体素的渲染等多种用途。 在这里我将介绍构建八叉树的基本算法以及你可能遇到的一些障碍。

1、什么是八叉树?

八叉树是一种分层数据结构,其中每个内部节点都有八个子节点。 八叉树通常用于通过递归地将三维空间细分为八个八分圆来划分三维空间。 当使用八叉树来划分 3D 空间时,每个八叉树(子)空间都以立方体为界,有时使用长方体代替,每个立方体被递归地划分为八个立方体,直到算法达到终止标准。

八叉树用于加速某些成本高昂的操作,例如最近邻搜索、光线投射或碰撞检测。

八叉树通常使用链接技术来实现,其中每个节点都包含指向其八个子节点中每一个的指针(或引用)。 完整的八叉树要么有 8 个子节点作为内部节点,要么没有子节点作为叶节点。

图 2:Storm3D 引擎中可视化的具有 8 级八叉树的斯坦福兔子

2、构建八叉树

有两种基本方法可用于构建八叉树,第一种是基于场景中的几何图形构建八叉树,当几何图形是静态时,此方法最有用,因为通常不需要更新中的任何内容 树结构。 另一种方法是构建一定深度的八叉树,然后将几何实体插入其中。 当您有动态场景时,这可能会更有用。 图 2 显示了存储在八层八叉树数据结构中的斯坦福兔子三角形。 请注意每个立方体如何代表一个节点并根据多边形数据的分布进行划分。

基本的 OctreeNode 可能如下所示,首先需要为每个节点存储一个边界框,您可以存储半宽和中心,也可以存储完整的边界框类

struct OctreeNode
{
float halfWidth;
vec3 center;
AxisAlignedBox m_bounds;
std::vector<OctreeEntity*> m_objects; // You can use a (intrusive) linked list instead but I like vector because of its contiguous memory
OctreeNode* m_children[8];
};

3、自下而上的方法

该算法首先计算场景中整个网格的边界框。 有了边界框后,你可以计算与其相对应的立方体,也可以使用相同的边界框来构建树,如果你想计算立方体,则只需使用相同的中心和长度等于边界框的最大尺寸。 树是基于几何分布构建的,即每个包含一定数量几何实体的子空间都应该被分割,直到每个叶子中达到一定数量的实体和/或达到一定的深度级别。 不包含任何实体的其他子空间应终止该分支并停止细分。

你将每个框拆分为八个子框,如果子框包含任何几何数据,你可以通过递归调用该函数继续拆分框。 当盒子具有一定数量的几何实体或树达到一定的级别时,你可以终止递归。 终止树构建的每种条件应根据应用情况进行明智选择。

//
// Building an octree, code for educational reasons and not particularly optimized.
//
static OctreeNode* BuildOctreeFromMesh( Mesh* mesh, std::vector& objects, AxisAlignedBox& aabb, int stopDepth)
{ 
// stop if we reached certain depth or certain number of objects
if(stopDepth < 0 || objects.size() < 3) return NULL; // Split to 8 cubes AxisAlignedBox boxes[8]; aabb.SplitToEight( boxes ); // Create a node and attach the objects OctreeNode* node = new OctreeNode;  node->m_bounds = aabb;
node->m_objects = objects; // Warning: copying here best be avoided.

std::vector boxObjects[8]; 
//for each triangle check intersection with 8 boxes
for (int i=0; i < objects.size(); ++i) { Triangle& tri = mesh->GetTriangle(objects[i].triangleIdx);
for (int boxId=0; boxId<8; ++boxId)
{
vec3 center = boxes[boxId].GetCenter();
vec3 halfsize = boxes[boxId].GetHalfSize();
bool overlaps = TriBoxOverlap( center, halfsize, tri.vert );

if ( overlaps )
{
boxObjects[boxId].push_back( objects[i] ); // Potential memory allocation here. Could be optimized 
}
}
}

//Loop through boxes and divide them more.
for (int i=0; i < 8; ++i) { size_t size = boxObjects[i].size(); if ( size ) { node->m_children[i] = BuildOctreeFromMesh(mesh, boxObjects[i], boxes[i], stopDepth-1);
}
}

return node;
}

构建八叉树最具挑战性的部分是几何实体和包围体/三角形之间的相交测试。 对于点和球体等对象来说,这有时相当简单。 但对于三角形来说可能更复杂,可用于三角形框相交的最佳算法是基于分离轴定理,你可以在此处找到它。

最后一点是使用 OctreeNode* node = new OctreeNode; 分配节点。 可能会导致缓存未命中并显着降低树的性能。 我建议您使用内存池,以防你的树性能(尤其是遍历)达不到最佳状态。 使用内存池将使内存具有传染性,从而避免由于内存不适合缓存而可能发生的潜在问题。

4、自上而下的方法

另一种方法首先构建一个达到一定深度的完整八叉树,然后将对象插入其中。 这对于动态场景可能很有用,但会消耗更多内存。 通过测试与根节点的交集来开始插入,确定对象与节点的哪个子节点相交后,递归地重复该操作,直到到达叶节点。 如果一个对象与多个子对象相交,你可以在它们之间拆分它或为每个路径复制它,这可能更容易实现,但实际上拆分它可能更有效。

5、存储几何实体

尽管我在这里基本上讨论的是三角形,但八叉树数据结构可以包含多种类型的几何实体,虽然我可以选择设计我的结构以在同一棵树中包含多种类型,但做出的决定是使树可以包含三角形网格或包围体(一次仅一种类型)。 做出这个决定是为了更容易地对每个几何实体进行某些优化。 仅包含三角形网格的八叉树通常用于光线投射或碰撞检测,而其他包围体是可见性确定的更好候选者。 尽管这不是最终版本并且可以更改,但我发现这种方式更容易使用,特别是可以直接实现某些优化。

每个几何实体由 OctreeEntity表示,并存储在每个八叉树节点中的八叉树实体向量中。 将 OctreeEntity 视为一个句柄,它实际上并不包含几何对象,而是一个可用于锁定列表中实际三角形/对象的索引。

class OctreeEntity
{
// here we store a bounding sphere that could come handy in some intersection tests. but it's optional
vec3 center; // optional
float radius; // optional
unsigned int Idx;
};

处理 OctreeEntity 而不是直接处理几何对象使数据结构更加通用。 通过这种方式,我们可以有效地隐藏几何实体,而不是在树代码中硬编码任何内容。 您可能会问我们如何处理不同的相交测试,同时保持代码通用,这可以通过将相交求解器隐藏在模板函数或模板类中来简单地完成。 你甚至可以使用模板来存储不同的几何属性来实现特定的优化,在模板术语中,这些属性正式称为特征(traits)。

6、在 OPENGL 3.2 中渲染八叉树以进行调试

渲染八叉树可以方便地进行调试,有时还可以实现一些很酷的效果。 在 Storm3D 中,我有一个函数可以遍历树并将其转换为可渲染的网格。 但在此之前,你需要记住两件事,特别是如果你使用 OpenGL 3.2,  GL_QUADS 已被弃用,顶点缓冲区对象是绘制事物的唯一方法。

首先,为了在不使用两个三角形的情况下绘制线框框,以便在没有分割线的情况下可以正确渲染四边形,你需要使用 GL_LINE_STRIP。 在性能方面,你需要在一个 VBO 中填充八叉树(尽管可能并不总是最佳),这将使你需要使用  glRestartIndex,因此你可以在使用 GL_LINE_STRIP 的单个 VBO 中存储多个图元,而无需复制顶点。

至于实际的转换机制。 这个想法是以广度/深度优先的方式遍历树,并将每个边界框转换为可渲染的顶点和索引,然后将它们添加到 VBO。 完成后,您可以通过一次绘制调用渲染所有八叉树。


原文链接:Constructing an Octree Datastructure

BimAnt翻译整理,转载请标明出处