3D游戏中的场景管理(八叉树和BSP树简介)

如何很好地表示出包含着成千上万物体的复杂场景,是设计系统必须要考虑的。这也是场景管理需要做得,给场景提供良好的层次关系,以便更好地进行筛选(Culling)和隐藏面消除(Hidden surface removal)。场景管理涉及到可视性处理(Visibility processing)和碰撞检测(Collision detection),系统需要判断场景的哪些部分在视见约束体之内,另外如果两个物体有碰撞关系,则需要计算碰撞点的值。
可见性剔除
为了达到游戏中的实时效果,传统的技术不可能适用,因为场景己经非常复杂,如果只采用Z缓冲的方法进行可见性处理是不现实的。目前己经有了将场景分层的方法,可以把辅助数据结构应用于场景中,先把场景分区,再分物体,甚至一直分割到多边形。如在室内场景管理中有两个经常用到的层次体系:BSP(Binary Space Partitioning)树,这是八叉树的推广,和包围体树(Boundingvolume tree)。前者用于加速剔除,而后者主要用于碰撞检测。
本节简单讨论如何使用层次体系进行更加高效的筛选以及可以采用什么样的数据结构来组织场景。
1.2 场景的组织和管理
场景的组织结构是渲染系统最基础和最重要的部分,也是一个实现的难点。它的决定会决定很多后续的工作,如碰撞检测,消隐,阴影等。
首先要涉及到的概念是空间细分,空间细分考虑整个物体空间并且根据物体的空间占有(Object occupancy)对空间中的每一个点进行分类。可以把世界空间中的物体细分为立方体素(voxel),再对体素进行分类。八叉树(octree)是一种描述三维空间的树状数据结构,它可以描述一个三维场景内物体的分布情况,并简单地将体素安排在层次结构中。
因此场景管理可以在预处理的时候建立一棵树,这里可以忽略物体的表示方法,而把焦点集中在场景的划分上。在树建立起来之后,通过实时遍历这棵树来发现是否有两个物体占据了同一个空间而发生冲突,或者一个物体的空间是否不在视见约束体之内。这样,所有筛选等操作都可以简化为对树的遍历,这是一个线形时间的操作。
另一种表示场景的数据结构是BSP树,它被广泛应用于室内场景的处理中,它使用一个分离面(splitting plane)对每一层一分为二,从而实现对空间的划分,其中用于分割的平面可以出现在任何方位。BSP的想法最早在Fuchs(1980)中被提出,起初的目的是为了解决实时地消除隐藏面。BSP可以说是八叉树的一般化。前人在这方面已经做了很多有效的工作,Fuchs首次将BSP技术中剖分平面的定侧性质应用于多边形场景的剖分,建立起空间二叉树结构.该二叉树的每一结点表示一个子空间及空间内所包含的多边形。在每一结点空间中,选取其中一平面作为剖分平面,将该空间继续剖分成正负两子空间,分别作为该结点的两个子结点,其中与剖分平面有交的多边形被分割成两个多边形,分别归入相应的子空间中。上述过程是一个递归过程,直至每一子空间仅包含一个多边形为止。
与八叉树剖分相比,BSP树具有内存耗费小,剖分方式灵活,产生的无效区域较小的优点;且对大部分场景来说,BSP树较八叉树更为平衡。
另外,由于是二分空间,因此方向性很强,在判断上要比八叉树容易,既可以代替Z-Buffer解决遮挡问题,因为BSP是可以确定物体的绘制顺序的,按照这个顺序就可以保证没有Z-Buffer也能够显示正确,还可以方便地执行碰撞检测。
1、BSP TreeBinary space partitioning原理
首先,将整个场景包围在一个AABB(外包盒)中,然后以递归形式将此外包盒分为若干比较小的盒子。通常是选取盒子的一个轴,生成与之垂直的平面,将其分为两个小盒子。一般是将盒子分为完全相同的两个部分。与分割平面相交的物体,或存储在此层次上,成为两个子集中的一员;或被这个平面分割成两个不同的物体。重复这个平面分割过程,就可以对每个AABB进行递归细分,直到满足某个标准。通常这个标准是用户定义的树的最大深度,或者是盒子内所包含的集合图元数量低于用户定义的某个值,或是到达叶子节点(全部节点都为凸包– 最小凸多边形 )。
简单说来,这种思想就是以平面来递归分割场景的。场景中会有许多物体,我们以每个物体的每个 Polygon 当成一个平面,而每个平面会有正反两个面,就可把场景分两部分,先从第一个平面开始分,再从这分出的两部分各别再以同样方式细分……如此进行,就可把场景建构出一颗二叉树。
将此思想简化到二维平面,就 如下图所示, 而在三维场景中的构建方式是与之类似的。
(注:该图取自en.wikipedia.org)
1 、A 是树的根节点也是整个多边形面
2 、A 被分为B和C
3 、B 被分为D和E.
4 、D 被分为F和G,其中G是凸包( 最小凸多边形 ),因此成为了该树的叶子节点,不可再分
2、BSP Tree的存贮结构
本例中 BSP Tree 的存贮结构用一个有(若干 +2 )个字段的记录来表示树中的每个结点。其中若干字段用来描述该结点的特性 ( 本例中的特性为:节点的值和节点坐标 ) ,其余的 2 个字段用来作为存放指向其 2 个子结点的指针。
3、本例实现BSP Tree的若干关键技术及截图
A、主界面:
Main 函数中使用 while(true) 产生类似Trinigy引擎中的渲染循环效果,等待用户输入操作。通过IF判断用户的输入并进行相应的操作。
1
2
3
4
5
6
7
8
9
10
11
cin>>choiced;
while(true)
{
        if(choiced == 0)
   return 0;
        else if(choiced == 1)
        {
             …………
        }
 …………
}
B 、创建 BSP Tree 界面,需要用户输入递归次数和外包盒在三维场景中的坐标
本例使用3层深度,外包盒坐标为(1,100,1,100,1,100)
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
34
35
36
37
38
39
//定义BSP树节点类
struct BSPTreeNode
{
    T data; //节点数据
 T xmin,xmax; //节点坐标,即六面体各顶点的坐标
 T ymin,ymax;
 T zmin,zmax;
    BSPTreeNode <T> *left,*right; //该节点的个子结点,即左右两个子六面体节点
}
//创建BSP树
int scale = -1;  
//切割平面属性初始化,根据切割平面属性决定当前切割的面是与X轴垂直的平面,还是Y轴的或Z轴的
template <class T>
void createBSPTree(BSPTreeNode<T> * &root,int maxdepth,double xmin,double xmax,double ymin,double ymax,double zmin,double zmax)
{
 maxdepth=maxdepth-1; //每递归一次就将最大递归深度-1
 scale++; //每递归一次就将切割平面属性+1,使下一次切割的平面更改一下
 if(3==scale) scale=0; //如果切割属性达到峰值,则初始化
 if(maxdepth>=0)
 {
  root=new BSPTreeNode<T>();
  root->xmin=xmin; //为节点坐标赋值
…………
  double xm=(xmax-xmin)/2;
  double ym=(ymax-ymin)/2;
  double zm=(zmax-zmin)/2; //计算节点3个维度上的半边长
  //递归创建子树。
  if(0==scale) //切割属性为0,沿与X轴垂直的平面切割
  {
//根据当前切割平面的属性决定其子结点的坐标
   createBSPTree(root->left,maxdepth,xmin,xmax-xm,ymin,ymax,zmin,zmax);
   createBSPTree(root->right,maxdepth,xmax-xm,xmax,ymin,ymax,zmin,zmax);
  }
  else if(1==scale) //切割属性为1,沿与Y轴垂直的平面切割
…………
  else if(2==scale) //切割属性为2,沿与Z轴垂直的平面切割
…………
 }
}
通过以上的方式循环切割当前节点产生BSP树,切割面的顺序为:
(a) 沿与X轴垂直的平面切割
(b) 沿与Y轴垂直的平面切割
(c) 沿与Z轴垂直的平面切割
依次循环
C 、创建成功后先序遍历此 BSP Tree
1
2
3
4
5
6
7
8
9
10
11
12
13
14
int i=1;
template <class T>
void preOrder( BSPTreeNode<T> * & p)
{
    if(p)
    {
  cout<<i<<".当前节点的值为:"<<p->data<<"/n坐标为:";
  cout<<" xmin: "<<p->xmin<<" xmax: "<<p->xmax;
…………
  i+=1;
        preOrder(p->left);
        preOrder(p->right);
    }
}
共7个节点。
D 、查看此树的深度
即查找左节点的个数
1
2
3
4
5
6
7
template<class T>
int depth(BSPTreeNode<T> *& p)
{
    if(p == NULL)  return -1;
    int h = depth(p->left);
    return h+1;
}
4、BSP Tree和Octree对比
a) BSP Tree使用1个面分割场景,而Octree使用3个面分割。
b) BSP Tree每个节点有2个子结点,而Octree有8个子结点
因此BSP Tree可以用在不论几维的场景中,都没有问题,而Octree则常用于三维场景(拓展到N维中就会十分复杂了)。
/*
Author   : 张聪
Date  : 2008/05/01
Filename : bsptree.cpp
Platform : VC++ 2005
BSP树的实现
功能:
1、创建BSP树。
   此BSP树为满树,即所有节点/叶子全部创建。
   用户可以自定义此BSP树的深度和所处的三维场景中的位置。
   注a:由于创建树时为满树创建,故层数太大时创建时间可能会比较久,请耐心等待。
   注b:创建顺序为(1)切割平面左边的节点-(2)切割平面右边的节点-(1)-(2)……
2、先序遍历BSP树。
   BSP树创建成功后用户可调用此子模块查看此BSP树,会显示每个结点的编号,值和在场景中的坐标。
3、查看BSP树的深度。
*/
#include <iostream>
using namespace std;
//定义BSP树节点类
template<class T>
struct BSPTreeNode
{
 T data; //节点数据
 T xmin,xmax; //节点坐标,即六面体各顶点的坐标
 T ymin,ymax;
 T zmin,zmax;
 BSPTreeNode <T> *left,*right;  //该节点的个子结点,即左右两个子六面体节点
 BSPTreeNode //节点类
 (T nodeValue = T(),
 T xminValue = T(),T xmaxValue = T(),
 T yminValue = T(),T ymaxValue = T(),
 T zminValue = T(),T zmaxValue = T(),
 BSPTreeNode<T>* left_Node = NULL,
 BSPTreeNode<T>* right_Node = NULL )
 :data(nodeValue),
 xmin(xminValue),xmax(xmaxValue),
 ymin(yminValue),ymax(ymaxValue),
 zmin(zminValue),zmax(zmaxValue),
 left(left_Node),
 right(right_Node){}
};
//创建BSP树
int scale = -1;  //切割平面属性初始化,根据切割平面属性决定当前切割的面是与X轴垂直的平面,还是Y轴的或Z轴的
template <class T>
void createBSPTree(BSPTreeNode<T> * &root,int maxdepth,double xmin,double xmax,double ymin,double ymax,double zmin,double zmax)
{
 cout<<"处理中,请稍候……"<<endl;
 maxdepth=maxdepth-1; //每递归一次就将最大递归深度-1
 scale++; //每递归一次就将切割平面属性+1,使下一次切割的平面更改一下
 if(3==scale) scale=0; //如果切割属性达到峰值,则初始化
 if(maxdepth>=0)
 {
  root=new BSPTreeNode<T>();
  root->data = 9; //为节点赋值,可以存储节点信息,如物体可见性。由于是简单实现BSP树功能,简单赋值为。
  root->xmin=xmin; //为节点坐标赋值
  root->xmax=xmax;
  root->ymin=ymin;
  root->ymax=ymax;
  root->zmin=zmin;
  root->zmax=zmax;
  double xm=(xmax-xmin)/2;//计算节点个维度上的半边长
  double ym=(ymax-ymin)/2;
  double zm=(zmax-zmin)/2;
  //递归创建子树
  if(0==scale) //切割属性为,沿与X轴垂直的平面切割
  {
   createBSPTree(root->left,maxdepth,xmin,xmax-xm,ymin,ymax,zmin,zmax);//根据当前切割平面的属性决定其子结点的坐标
   createBSPTree(root->right,maxdepth,xmax-xm,xmax,ymin,ymax,zmin,zmax);
  }
  else if(1==scale) //切割属性为,沿与Y轴垂直的平面切割
  {
   createBSPTree(root->left,maxdepth,xmin,xmax,ymin,ymax-ym,zmin,zmax);//根据当前切割平面的属性决定其子结点的坐标
   createBSPTree(root->right,maxdepth,xmin,xmax,ymax-ym,ymax,zmin,zmax);
  }
  else if(2==scale) //切割属性为,沿与Z轴垂直的平面切割
  {
   createBSPTree(root->left,maxdepth,xmin,xmax,ymin,ymax,zmin,zmax-zm);//根据当前切割平面的属性决定其子结点的坐标
   createBSPTree(root->right,maxdepth,xmin,xmax,ymin,ymax,zmax-zm,zmax);
  }
 }
}
int i=1;
//先序遍历BSP树
template <class T>
void preOrder( BSPTreeNode<T> * & p)
{
 if(p)
 {
  cout<<i<<".当前节点的值为:"<<p->data<<"/n坐标为:";
  cout<<" xmin: "<<p->xmin<<" xmax: "<<p->xmax;
  cout<<" ymin: "<<p->ymin<<" ymax: "<<p->ymax;
  cout<<" zmin: "<<p->zmin<<" zmax: "<<p->zmax;
  i+=1;
  cout<<endl;
  preOrder(p->left);
  preOrder(p->right);
  cout<<endl;
 }
}
//求BSP树的深度
template<class T>
int depth(BSPTreeNode<T> *& p)
{
 if(p == NULL)
  return -1;
 int h = depth(p->left);
 return h+1;
}
//计算单位长度,为查找点做准备
int cal(int num)
{
 int result=1;
 if(1==num)
  result=1;
 else
 {
 for(int i=1;i<num;i++)
  result=2*result;
 }
 return result;
}
//main函数
int main ()
{
 BSPTreeNode<double> * rootNode = NULL;
 int choiced = 0;
 int maxdepth=0;
 while(true)
 {
  double xmin,xmax,ymin,ymax,zmin,zmax;
  system("cls");
  cout<<"请选择操作:/n";
  cout<<"1.创建BSP树 2.先序遍历BSP树/n";
  cout<<"3.查看树深度0.退出   /n/n";
  cin>>choiced;
  if(choiced == 0)
   return 0;
  else if(choiced == 1)
  {
   system("cls");
   cout<<"请输入最大递归深度:"<<endl;
   cin>>maxdepth;
   cout<<"请输入外包盒坐标,顺序如下:xmin,xmax,ymin,ymax,zmin,zmax"<<endl;
   cin>>xmin>>xmax>>ymin>>ymax>>zmin>>zmax;
   if(maxdepth>=0 || xmax>xmin || ymax>ymin || zmax>zmin || xmin>0 || ymin>0 ||zmin>0)
   {
    scale = -1;  //切割平面属性初始化
    createBSPTree(rootNode,maxdepth,xmin,xmax,ymin,ymax,zmin,zmax);
   }
   else
   {
    cout<<"输入错误!";
    return 0;
   }
  }
  else if(choiced == 2)
  {
   system("cls");
   cout<<"先序遍历BSP树结果:/n";
   i=1;
   preOrder(rootNode);
   cout<<endl;
   system("pause");
  }
  else if(choiced == 3)
  {
   system("cls");
   int dep = depth(rootNode);
   cout<<"此BSP树的深度为"<<dep+1<<endl;
   system("pause");
  }
  else
  {
   system("cls");
   cout<<"/n/n错误选择!/n";
   system("pause");
  }
 }
}
  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
Open3D是一个用于3D数据处理的开源库,支持许多3D数据操作和算法。开发者可以使用Open3D来处理点云数据、三维重建、图形可视化等。 在Open3D,八叉树是一种常用的数据结构,用于管理和索引3D空间的点云数据。八叉树将空间划分为一个个小的立方体单元,每个立方体单元称为一个节点。每个节点可以进一步分割成8个子节点,这样一级一级的分割下去,最终形成了一棵结构。 遍历八叉树是指按照某种方式访问的每一个节点。在Open3D,可以通过调用`octree.access()`函数来实现遍历八叉树。`octree.access()`函数通常需要传入一个callback函数作为参数,用于对每一个节点进行具体的操作。 以下是一个简单的遍历八叉树的例子: ```python import open3d as o3d def print_node(node): print(node) def main(): pcd = o3d.io.read_point_cloud("cloud.ply") octree = o3d.geometry.Octree(max_depth=5) octree.convert_from_point_cloud(pcd) octree.access(print_node) if __name__ == "__main__": main() ``` 在这个例子,首先使用`o3d.io.read_point_cloud()`函数读取一个点云数据文件。然后,创建一个八叉树实例,并使用`convert_from_point_cloud()`函数将点云数据转换为八叉树。 接下来,定义一个名为`print_node()`的callback函数,用于打印每个节点。最后,通过调用`octree.access(print_node)`函数来遍历八叉树,并将每个节点传递给callback函数进行处理。 这是一个简单的遍历八叉树的例子,实际应用可能需要根据具体需求对每个节点进行更复杂的操作和处理。希望这个回答能够帮助到您。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值