如何很好地表示出包含着成千上万物体的复杂场景,是设计系统必须要考虑的。这也是场景管理需要做得,给场景提供良好的层次关系,以便更好地进行筛选(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 Tree(Binary 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判断用户的输入并进行相应的操作。
cin>>choiced;
while(true)
{
if(choiced == 0)
return 0;
else if(choiced == 1)
{
…………
}
…………
}
B、创建BSP Tree界面,需要用户输入递归次数和外包盒在三维场景中的坐标
本例使用3层深度,外包盒坐标为(1,100,1,100,1,100)
//定义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。
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、查看此树的深度
即查找左节点的个数
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");
}
}
}