八叉树简述
定义
八叉树是一种对三维世界进行场景管理的理想的空间数据结构。八叉树中根节点包含一个立方体包围盒。每个非叶子节点都拥有八个子节点,它们将双亲节点细分为八分体。也就是说而且每个子节点表示一个立方体的体积元素,而且所有子节点的体积加起来是父节点的体积。
当满足用户所定义的标准被满足时,停止细分。常见的停止标准有:
- 节点包围盒达到一个特定大小
- 节点内包含的多边形数目达到最小数目
数据
八叉树的每个节点至少要包含以下数据:
- 包围盒 — 空间中的八叉树节点所包含的封闭立方体
- 几何体列表 — 包含在该节点内的所包含的几何体
- 子节点 — 指向每个子节点的指针
- 相邻节点 — 每个节点最多有六个相邻节点(立方体拥有六个平面)。在碰撞检测的过程中,对八叉树的遍历要求指向相邻节点的指针。
树的建立
包含多边形数目大于最低阈值POLY_THRESHOLD的节点,BuildOctree()会产生8个子节点。BuildNode()函数创建了节点数据,主要包含两个步骤:
- 为节点创建包围盒(通过父节点的包围盒确定宽度、高度以及深度,并通过索引i确定包围盒的中心位置是8个子节点中的哪一个)
- 确定哪些多边形位于节点的包围盒内部
BuildOctree(Node N) { if(NumPolys(N)>POLY_THRESHOLD) for(int i = 0; i < 8; i++) { BuildNode(N->Child[i], i, N); BuildOctree(N->Child[i]); } }
1 2 3 4 5 6 7 8 9 10 11 | BuildOctree(Node N) { if(NumPolys(N)>POLY_THRESHOLD) for(int i = 0; i < 8; i++) { BuildNode(N->Child[i], i, N); BuildOctree(N->Child[i]); }
}
|
计算包围体的大小与中心点
某层节点立方体包围盒的边长计算公式:
L(depth)=W/(2depth)
某层节点与其相邻节点包围盒中心间距的计算公式:
S(depth)=W/(2depth)
W:worldsize
depth:depth(root)=0
判断物体所属的包围盒
伪代码如下:
struct node{ vector3 CubeCenter; node* Child[2][2][2]; ObjectList Objects; }; int Classify(plane p,volume v){ if(v is completely behind p){ return 0; }else if(v is completely in front of p){ return 1; }else{ // v straddles p. return 2; } } void InsertObjectIntoTree(node* n,Object* o){ int xc = Classify(plane(1,0,0,CubeCenter.x)),o.BoundingVolume); int yc = Classify(plane(0,1,0,CubeCenter.y)),o.BoundingVolume); int zc = Classify(plane(0,0,1,CubeCenter.z)),o.BoundingVolume); if(xc ==2 || yc == 2 || zc == 2) { //Object straddles one pr more of the child partition planes, //and so won't fit in any child node,so store it in this node. Objects.Insert(o) }else{ //Object fits in one of the child nodes.Recurse to find the //correct descendant. InsertObjectIntoTree(Child[zc][yc][xc],o) } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | struct node{ vector3 CubeCenter; node* Child[2][2][2]; ObjectList Objects; };
int Classify(plane p,volume v){ if(v is completely behind p){ return 0; }else if(v is completely in front of p){ return 1; }else{ // v straddles p. return 2; } }
void InsertObjectIntoTree(node* n,Object* o){ int xc = Classify(plane(1,0,0,CubeCenter.x)),o.BoundingVolume); int yc = Classify(plane(0,1,0,CubeCenter.y)),o.BoundingVolume); int zc = Classify(plane(0,0,1,CubeCenter.z)),o.BoundingVolume); if(xc ==2 || yc == 2 || zc == 2) { //Object straddles one pr more of the child partition planes, //and so won't fit in any child node,so store it in this node. Objects.Insert(o) }else{ //Object fits in one of the child nodes.Recurse to find the //correct descendant. InsertObjectIntoTree(Child[zc][yc][xc],o) } }
|
松散八叉树
八叉树是一种典型有效的空间数据结构。但是它有一些缺点:较小的物体可能因为其特殊位置被存储到一个具有非常大的包围盒体积的八叉树节点中。在《Game Programming Gems》中该问题被描述为层次划分过程中产生的“粘性(Sticky)”区域,较小的物体却保持在树的较高层次,降低了划分的效率。
松散八叉树的建立
松散八叉树的“松散”是指调整节点的包围体大小,“放松”包围立方体,但是同时节点的层次和节点的中心不变。在松散八叉树中,包围立方体边长的计算公式修改为:
L(depth)=k∗W/(2depth)
节点的间距依旧与传统八叉树保持一致。这意味着同层节点的包围立方体会相互重叠,如图c)所示。
而k值的选择就是一个比较重要的问题,k值过小则无法体现松散八叉树减少粘性区域的优势,k值过大则会导致包围体过于松散。各类文献中基本都建议选择k=2,能够获得最好的效果。
假设一棵松散八叉树的k=2,我们可以计算出节点所在的深度,而物体在那一层深度中的哪个位置则取决于物体中心位置的坐标。
层次选择公式的推导过程:
已知深度的情况下,只需要选择距离物体中心最近的节点就可以确定物体应该被哪个节点的包围盒所包含。
index[x,y,z]=floor(object.[x,y,z]+W/2)/S(depth))