空间排序(SPATIAL SORTING)
第四章讨论了渲染一个层次化结构的场景,场景中的物体绘制的顺序是由深度优先遍历所决定的,几乎所有的情况下,渲染的场景都以这种方式不正确的绘制着。例如,如果沿眼睛视线的两个不相交的物体要被绘制,那么更远的那个物体将会被首先绘制。在深度遍历中如果在最近的物体后面还有物体,则场景的绘制方式是不正确的。因此,一个场景如果要被正确的绘制,唯一的方式就是通过排序。上面给出的例子仅仅说明了排序的必要性。在真正的游戏中,在场景建模的时候就可以简化这种排序,例如在一个包含大量建筑的城镇模型里。真正的排序机制也许更复杂,特别是当物体之间并不相交,但是有一定缠绕的的时候。实际上,如果一个场景包括一个透明的物体,判定一个正确的绘制顺序是非常难的,甚至需要对物体进行分割。比如像室外场景中的树,他们每一个都是通过相交的a混合的多边形来实现。
空间排序的的基本前提是避免在屏幕上多次的绘制一个像素,术语深度复杂性(depth complexity)意味着一个像素被绘制多少次,因为整个屏幕每一帧都要绘制,所以理想的深度复杂性是1;也就是说:每个象素只被绘制一次。深度复杂性越高、屏幕刷新率就越低。
现在典型的排序方法是深度缓存,在第三章中已经讨论过了。这个方法是以像素为单位判定的,在最远和最近切割片之间的的深度值存储在深度缓存里。每个像素的颜色值存储在帧缓存里,假设深度缓存启动了测试和写的功能,一个像素的颜色若要绘制进帧缓存中,则它的深度值必须小于目前那个像素颜色的深度值,这个过程对于程序事先的渲染器来说是很慢的,但是有了硬件加速的支持,深度缓存是一个很好的通用的深度排序解决方案。
深度缓存需要一个三角形来处理包围住的像素是不被绘制的,但是当三角形不会被绘制的时候,就不要把它们送给渲染器去判定了,以物体为单位的判定(物空间算法)要胜过以像素为单位的判定(象空间算法)。本章将要讨论比较高级的空间排序算法.12.1节是一个对四叉树和八叉树的概要介绍,这两种树性结构提供了一种对场景的规则划分方案。四叉树是用来对平面的的矩形作划分的,八叉树是对长方体的作划分的。然而许多游戏所需要的排序方案是与游戏场景数据相关的。对一个室内场景来说,自然使用一种与视口(protal)相关的自然排序方式(12.2的主题),对一个室外场景和需要正确绘制包含a混合多边形的场景来讲:二叉空间分割树非常有用,12.3节将描述这种树,其中包括如何构建这种树以及它如何用于隐藏面切除、可见性判定、选择、和碰撞监测。
12.1 四叉树和八叉树
场景图提供了一种对物体选择的基本机制,节点绑定空间对于视锥体的比较对照可以极大地减少被送到渲染器的节点数量。如果绑定空间与视锥体相交,那它的以它的绑定节点为根结点的子树才会被进一步处理,但是这一种处理仅仅基于绑定空间的信息。系统中还有关于场景结构得更高级的信息可以被利用。例如在一个地形系统中,可以建立一个可见性图来帮助减少在当前视点位置看不见的地形片。特别是一个地形中有高山隐藏了山后情景的时候,所以那些隐藏片就没有必要被处理了,尽管他们的绑定空间信息与视锥体相交。
四叉树和八叉树都是用来将场景空间剖分为小单元的。可见性图也是以单元为基础的。当视点被放置到某一个单元的时候,与这个单元有关的一个潜在可视单元列表就会被产生,如果场景结构被精心设计来支持来这种策略的话,这个方案是非常有效的。
这种以单元为单位的可视性判定场景图的构造有两种方案,第一种:以平面位置为基础来构建,在这种情况下这个平面被剖分为一个四叉树;第二种方案是以空间位置为基础,这种情况下整个空间被剖分为八叉树。场景图中的一个节点代表一个具体的四叉树块或者是一个八叉树块。如果一个节点代表一个节点代表一个四叉树块,则它有四个子节点。如果它代表一个八叉树块,则有八个子节点。另外子节点都被用来在这些单元里的实际物体。如果物体随时间移动,则场景图需要不断地通过把物体连到节点或取消联系来重新设置。然而,基本的四叉树或八叉树结构在整个程序运行周期是一致的。
下面给出一个四叉树或八叉树场景图处理的伪代码。四叉树块的可见性列表存储一些指针,这些指针指向根据目前所在块判定的所有潜在可视块的节点。
cameraBlock=GetBlockOf(render.camera);
visibleList=GetVisibleCellsFrom(cameraBlock);
for(each node in visibleList)
renderer.Draw(node);
就像第四章提到的,Draw函数递归地遍历某一个确定的子树并且努力在绘制之前基于绑定空间进行选择。基于四叉树结构地子树划分在基于绑定空间比照的选择中被去除。
当然这个过程的难点是建立可见性列表。一个优秀的可见性判定的参考是Seth Teller 1992年的博士论文,而Hanan Samet在1989/1990年出版的两个小册子提供了所有关于四叉树和八叉树的一切。
12.2入口(portal)
四叉树和八叉树空间排序就努力在程序关于场景结构和物体的元信息的基础上建立一个可见性图。程序制作者有责任手工或通过某种自动方法建立可见性图。一个需要比较少的交互的方法是基于入口的系统。在这个系统中与其建立一个复杂的可见性图,倒不如游戏程序员增加附加的切割面把视锥体裁减城更小的。典型的情况是视点位置在一个屋子外面,但是却向房子里面看。那个门口就是一个使你看到屋内东西的入口(portal)。但是门口附近的墙会封闭很多屋内内容的视图。所以在绘制这个屋子的时候,被墙所所遮挡的屋内屋体可以被去除,甚至,如果物体是潜在隐藏的,通过门口框架组成的面和视点位置一起可以被用来建立切割面,这些切割面可以被用来裁减,而这些裁减可以作为消隐处理的补充。入口技术在室内风格的游戏中是非常有用的,因为在那种情况下有很多的墙和其他阻挡视线的物体,以便于消隐(culling)处理能够很有效。然而,入口技术的使用对一个室内场景是没有限制的。例如:一个对于视点可见的动画角色可能会跑到建筑物后面。假设建筑物足够的高,通过屋顶上面并不能看到角色的顶部,那些由视点位置和角色前面的建筑物的边构成的切割片能够在一个入口系统中使用。一旦角色完全在建筑物后面(也就是说角色在切割片的非可见性一面),它就会被完全的消除也就不会被送给渲染器去处理了。
图12.1过入口的可见性演示
图12.1说明了入口的一种典型情况,左图中的灰色区域说明的是在标准视图体中渲染器需要处理的空间。又边的图说明了入口切割片如何限制所考虑到的内容。如果使用第四章的层级化机制,这种多余切割面选择的实现是非常琐碎的。这种消隐机制保存一个六位的标志。每一位表示这个物体对于相对应的切割面片是否是可见的。这个标志可以扩展到使用任意多个位,试点可以存储另外消隐目的的切割面片,相同面片可以用来裁减。但是在硬件加速的API里,例如OpenGL和Direct3D都仅仅支持很小数量的切割面片,一个入口系统如果要利用好API就要限制相关多余切割面片的数量。
一个使用了入口技术的室内场景,空间必须能够被划分为多个凸体区域,通过这样做,区域的组成部分渲染的顺序就不重要了。其实入口自身就是一个凸多边形,这个凸边多形存在于讲两个凸体区域分割开来的面片上。从这种角度上理解:一个入口是双向的,尽管是为了有趣的效果,没有必要非要是这样。但是可以把两个临近的区域够建成一个这样的区域,可以从一个里面看另一个的区域,但从另一个里面就看不到前者了。实际上,第二个区域甚至不会对第一个有连结入口。这代表了单向远距离传屋的概念,我们将会假设入口是非双向的,如果两个临近区域通过一个公共的图形入口都是可见的,那么这两个区域将和这个入口连接起来也就是两个入口共存于空间的同一个位置。
区域和入口结合在一起就可以形成一个任意复杂的场景。例如:可以站在一个区域里,通过一个入口看另一个区域,也可以从那个区域里通过入口另一个区域。渲染机制必须以从后向前的顺序绘制区域以保证正确的虚拟效果。这个方案通过够建一个抽象的直接的图来实现,这种图里面区域就是图的节点、入口就是图的边。这种徒步是父子场景图。而是代表着相邻区域的关系。每一个区域以图中的节点表示,节点包括足够的信息来支持对相邻区域的遍历。入口(portal)作为区域的子节点连结在区域节点上来支持入口消隐。如果一个区域正被相邻图形访问。但是不一定这个区域所有的入口都在视图锥体内(或者是视锥体和切割面相交而定义出空间集的一部分)。对相邻场景图的连续遍历可以忽略这样的入口,有效完成另一种消隐。最后,区域节点还可以有其他的子节点代表区域的绑定面(墙,也就是说:如果区域是个房子)和在区域中的物体及在区域中可见的可绘制体。下面给出一个在入口系统中绘制一个凸区域的伪代码。物体planeSet是目前渲染器用来消隐和裁减的切割面集合。入口所含的是切割面都是通过入口凸多边形和视点位置形成的。
void Render(Region region)
{
if (not region.beingVisited)
{
region.beingVisited=true;
for(e