<Real Time 3D Terrain Engines Using C++ And DX9>
Chapter 5. Fundamental 3D Objects
经过了漫长的前戏(rpwt -_-|||),终于到了讲核心技术的时候了。这是最后一章基础课了(基础课不是在Part I的时候都讲完了么?),讲的就是3D里面非常基础的一个话题——空间分割,果然是基础。
空间分割就是把world分成若干个部分,然后确定出哪些部分引擎应该去渲染,而哪些部分不需要。而且还可以用来确定每个部分的细节(就是该用什么级别的细节去渲染)。在Gaia中,空间分割是用四叉树来实现的。
the Motivation behind Scene Organization
任何render pipeline的第一步都是要确定哪些物体需要被渲染,这就是我们做空间分割的动机。查看一个物体是否可见,就是查看这个物体是否在视锥(frustum)当中。上次有人在bbs上问:那个三角形的东西是什么?我立即ft了。
物体只要有任何一部分在frustum中,就是可见的。比较简单的方法是用一个边界的box装住物体,那么一个物体只要测6次就可以知道它是否可见了(box的6个面)。
视锥中的每个面都可以看成是空间中的平面。每个都用一个法线(指向视锥内部)来表示正的半平面。如果一个物体的某个部分都是在这些半平面的正半面方向,那就是可见了。
有种简化的方法:把一个大空间分成小矩形,叫sector。那么只需要确定sector是否可见,如果可见,那么sector里面所有的object都要送去渲染。
这种方法获得了效率,却丢掉了精度。因为很多可见的sector中的object也是不可见的。那么有个改进方法:所有和frustum边界相交的sector都要再进一步的测试其中的每个object。
如果物体足够分散的话,其实还是相当于test了所有的object,这一点儿也没有效率上的提高。那么继续改进:把sector分成大块,然后再细分,变成一棵树。如果双亲不可见,那么孩子一定不可见。如果双亲可见,就继续test他们的孩子,递归下去。
the Basic Quadtree
四叉树和八叉树都是用树来表示空间关系。四叉树用于2D,将一个矩形分成4等分,每个可以继续细分。八叉树用于3D,把每个立方体分成8等分,然后继续细分。
因为Gaia中的垂直高度比较低,所以可以看成是一个平面,这也是不用octree而用quadtree的原因。不过以后我们可以对quadtree扩展,让它变成一个伪3D的树。
理论上quadtree中的节点数目是最小化的,就是说,如果一个分支上没有物体,它就是null了,它下面的节点也都被忽略了。但是这样做虽然节省空间,但是却缺乏动态效率。一旦有运动物体的话,增加删除节点的操作就太频繁了。
为了避免这种情况,Gaia中使用的是表示全部节点的quadtree,哪怕是null节点也要表示出来。这样就不用考虑增删节点了,不过得耗不少内存。
其实quadtree应该可以看成是一个2维的线段树。查找一个物体的algo如下:
Step 1. 查看当前节点所有的孩子。如果没孩子,goto Step 3。
Step 2. 如果该物体被其中一个孩子节点完全包住,那么把这个孩子节点作为当前节点,然后goto Step 1。
Step 3. 这个物体就是属于这个节点了,添加,然后Exit。
Enhancing the Quadtree
因为查找添加节点的过程太耗时间了,如果是一个动态性很强的游戏的话,那么大部分的时间都要用于查找节点。所以,如果有一种直接能够确定节点插入位置的方法就好了。所幸的是Matt Pritchard给我们提供了一个这样的方法。(为了弄明白这个我还特意去看的<Game Programming Gems 2>)
如果一个四叉树每一层都可以用一个二维数组来表示的话,那么每个节点应该这么表示:
Node[Level][X][Y]
另外,对于这种方法有两个限制,不过Pritchard说这些都不算限制:
1)坐标轴必须是2的幂,比如256x256。如果不是的话是不是就不能用呢?哎,缩放一下就好了啊。
2)树的层数要提前确定。这个对于很多情况下都是提前确定了的。记最大层数为MaxLevel。
对于一个物体,如何确定它的Level和XY的值呢?比如我们的坐标是256x256的,MaxLevel=7,有一个矩形物体,左上角是(190,110),右下角是(195,125)。那么我们需要计算两个量,一个是它在X轴上的两端的值取异或,一个是它在Y轴上的两端的值取异或。如下:
10111110 (190)
Xor 11000011 (195)
--------------------
01111101
01101110 (110)
Xor 01111101 (125)
--------------------
00010011
这时候,查看他们的最高位所在的位置,然后用最大层数去减,确定在特定轴上的层数:
LevelX = MaxLevel - HighBitSet(1111101) = 7 - 6 = 1
LevelY = MaxLevel - HighBitSet(10011) = 7 - 4 = 3
然后这个物体的层数是他们的较小者:
Level = MIN( LevelX, LevelY ) = 1
这样就确定了它的层数。下面就需要确定它的位置了。位置是使用物体的任意一个点作右移,右移的位数应为:
ShiftBit = MaxLevel - Level + 1 = 7 - 1 + 1 = 7
然后,分别计算XY的值(取的任意点为物体左上角的点,X1=190,Y1=110):
X = X1>>ShiftBit = 1
Y = Y1>>ShiftBit = 0
则这个物体所属的节点就是Node[1][1][0]了。
都是位运算,看上去都觉得有效率了……
Adding Another Dimension to the Quadtree
如果想给空间加上Z轴的话,就要使用octree了。但是这样的话效率就大打折扣了(quadtree总比octree复杂度低吧)。那么,采用一个比较折衷的方法来建立一个伪Z轴,就是在Z轴上设32个层,用32bit来描述。如果一个物体在第4567层的话,它的高度就是11110000(前24位都是0)。
那么如果插入一个物体的话,就用OR一下节点的高度域。如果测试物体是否属于特定层的话,就AND一下(得到非零值就是有了)。
又是位运算,看来引擎的确是个追求效率的东西。
Fast Quadtree Searches
想查找一个3D的体积,就得把它转化为一个2D的Shape和一个32bits的高度,然后用Shape和这个高度查找。
比如有个给定的体积,那么就先查根。如果这个节点的z包括了这个体积的z,那么就查它的四个孩子。这样一直查下去,如果这个节点的某个object和这个体积相交了,就把这个节点加到一个Link List后面。当查找完成的时候,这个Link List就是最后的结果了。
另外,如果一个节点的z-mask改变了的话,它的祖先也要相应进行调整。
Slow Quadtree Searches
有快的为啥要慢的?慢的意味着更多的比较和操作。其实,用这个的主要原因就是用来查找视锥中的物体集合的。
视锥的形状不规矩,是一个锥形的。那么你可以用一个box来包住视锥,然后用box + Fast Search进行查找,这样的话效率是有了,但是会错误的查到很多本不属于视锥但是却属于box的物体。所以,为了避免这种情况,就直接用视锥去查了。
这个是可选的。如果使用快速渲染的话,就可以用Fast Search,因为多出来的物体就算渲染了也费不了太多时间。但是如果是使用一大堆Shader作高级渲染的话,还是采用Slow Search的好,因为这样会节省更多的渲染时间。
学过了这章,至少明白了什么是Quadtree,虽然还没有机会使用它,却已经感受到了它的强大。本来这章不需要看这么久的,只是这周忽然莫名其妙的大病了一场,所以耽误了许久,今天终于把剩下的一点点看完。基础知识都学完了,以后就要进入真正的地形学习了。