1、第七章 隐面消除目录:7.1 背面剔除(back culling)算法7.2 从后到前排序7.3 顺序列表和八叉树7.4 入口(Portals)7.5 二叉空间分割树7.6 Beam 树7.7 扫描线算法7.8 Z-buffer 算法引言到目前为止,我们完全忽略了一些问题:很明显,它们是由于屏幕上的一些图元被另一些图元挡住所造成的。例如,当我们要描绘一个由多边形面组成的三维物体时,那么它的一部分必然要被挡住。我们要在屏幕上显示的必须是可见的东西。打个比方,对于一个立方体,无论从哪个方向进行透视处理,我们最多只能看到其中的三个面。这样,我们就要想出一种方法来决定哪些面是我们所能看到的。如果我们使
2、用从屏幕到世界的视处理方法,那么很自然的就能保证只有图元上正确的部分才显示在屏幕上。在这种视处理中,可见性在屏幕的每一个像素上进行判断。我们从人眼发出一条射线,穿过一个给定的像素,那么首先与这条射线相交的表面在这一个像素上就是可见的。从这个表面反射的光线能够进入我们的眼睛。在这一章中,我们将讨论用于隐面消除的一些方法。由于最普通的图元就是多边形,所以我们将要讨论的许多技术都是只针对多边形模型的。我们也将重点讨论用于多边形地形、体素模型的一些技术,最后将讨论一种适用于任何图元的一般性的算法。尽管隐面消除对于光线投射方法是本身就具备的,但是它的计算量是很大的。这样,我们还将使用一些用于从世界到屏幕
3、视处理方法的隐面消除算法,它们将与光线投射算法结合使用,从而减少计算量。7.1 背面剔除(back culling)算法许多三维物体,它们所占据的空间都被一些连续的表面所包围。当我们观察这些物体时,只能看到这些包围表面中正面部分,而对背面就无法看到了。背面剔除算法就是将这些我们看不到的背面多边形去除掉。(见图 7.1)2图 7.1:包围表面的可见与不可见部分在前面的章节中,我们已经遇到过凸多边形的概念。这一概念也可以引申到多面体上。如果位于表面上的任意两个点的连线都没有超出边界的话,那么这个多面体就是一个凸多面体。凹多面体没有这样的特性。(见图 7.2)图 7.2:凸多面体与凹多面体如果我们对
4、一个多边形模型执行背面剔除,并且这个模型是一个凸多面体,那么经过这样的处理之后,我们就已经消除了所有的隐藏表面。由于这些模型的形状,所有隐藏的多边形就是组成背面的多边形。但是,在消除凹多面体的隐藏面时,这一通用的技术就会出现一些问题。有可能某些对象的正面被其他多边形中同样是正面的面遮挡(见图 7.3)。这时,位于背面的多边形仍然是不可见的,明确地消除它们将对此有帮助,即便不能解决这一问题,那么至少减小了其复杂性。图 7.3:向前的多边形被遮挡的情况同样的推理也应用于光线投射算法。尽管我们完成隐面消除要归功于这种算法的自然本性,但背面剔除算法可以减少场景的复杂度,使我们不用再同那些复杂的隐藏面一
5、起进行考虑,这样就加快了视处理的过程。让我们来设计一种技术,来决定一个多边形是位于物体表面的正面还是背面。我们已经看到了法向量在描述一个多边形的朝向时所起到的作用。这样,当一个多边形的法向量与观察方向之间的夹角大于 90时,就表示这个多边形位于物体的背面。3我们已经讨论过矢量积和标量积,它们对于解决上面的问题很有帮助。首先,我们计算位于一个给定多边形平面上的某两个向量的矢量积,得到这个多边形的法向量。这两个向量可以通过多边形顶点的差分来得到。接下来,计算观察方向与法向量之间标量积的符号,由此决定它们之间是否形成了大于 90的角。如果真的大于 90,这个多边形就要被剔除掉,也就不用再考虑进行视处
6、理过程了。我们要注意,根据定义,两个向量的矢量积的结果也是一个向量,它与两个做向量乘法的向量组成的平面正交。这也就是说,根据多边形平面上形成这两个向量的方式的不同,我们可以得到两种可能的法向量,它们的方向正好相反。(见图 7.4)V0V21()0()21nV()02V0 2V1()10()21nV()()1021(a): (b):图 7.4:顶点顺序与方向量方向的关系如图 7.4 所示,如果我们在连续顶点 和 建立两个向量,那么法向量的01,2方向将会依赖于这两个向量的顺序。我们可以很方便的将多边形表面的正面与顶点的反时针顺序联系起来(见图 7.4 (a),这也正是定义表面法向量的基础。我们已
7、经见到了两种不同的视处理过程,并且还有两种不同的投影变换。使用这些技术,我们可以在渲染管道中的某一特定阶段完成背面剔除算法。对于平行投影方式,投影线都具有相同的方向,并且与观察方向一致。就在投影阶段之前,观察方向向量指向 Z 方向,可以用(0,0,1)来进行描述。这样当然也就减少了标量积的计算量,其结果就等于法向量的 z 分量。这样,我们就只需要计算法向量的 z 分量值就可以了。对于透视投影,投影线相交在观察者的眼中,它们的方向是不同的。我们可以在世界或观察空间中的任一点上构造一个指向观察者眼睛的向量,并使它指向该点方向,这样就得到了这一点的观察方向。(见图 7.5)490 90Viewer图
8、 7.5:寻找背面多边形然而,一旦模型经过透视变换从观察空间映射到屏幕空间之后,用来执行背面剔除的观察方向恒定,使我们只需要对法向量的 z 分量来进行简单的计算就可以了。背面剔除可以在渲染管道的开始阶段执行,也可以在世界空间甚至对象空间中来进行。我们越早剔除掉背面多边形,那么要执行的多余的操作就会也少。我们还要考虑对顶点进行的 3-D 变换,这样,如果我们可以剔除掉不属于任何前表面的顶点的话,在对象空间执行背面剔除运算将会很有好处。我们可以对每一个顶点设置一个布尔变量,当包含某个顶点的多边形位于前表面时,就将该顶点的布尔变量值设为“ true”。检查完所有多边形之后,我们就可以将任何情况下都没
9、有被设置为“true”的顶点剔除掉。这样就可以避免对被剔除的顶点执行 3-D 变换,从而减少了计算量。如果我们不选择执行上述操作,那我们同样可以在世界空间中来进行背面剔除运算。在世界空间中执行剔除运算,可以避免对一些多边形进行计算量很大的裁剪计算。但是剔除运算本身的计算量将比在光栅化之前执行大一些。除了在运行时计算多边形表面的法向量之外,我们还可以在程序的渲染循环之前预先计算好这些向量,并将它们与每一个多边形表面联系起来。为了在世界空间中得到法向量,我们可以应用变换将顶点变换到世界空间中。应该注意到,为了达到对一个向量的变化目的,我们只能应用变换的线性部分(也就是旋转和非归一化缩放变换) 。平
10、移变换没有必要使用,因为它对向量没有影响。将一个法向量从一个空间变换到另一个空间中的计算量往往要比直接在目标空间中建立该法向量还要大。但是,由于光线处理过程也要使用到单位法向量,这样我们就可以将两种运算目的合并起来。然而,在一个空间中建立一个单位法向量的计算量又要比从另一个空间中变换过来的计算量大的多。因此,我们还是要对法向量进行变换。同样的推理最可能应用在从屏幕到世界的视处理情形中,正如我们注意到的,我们可以对这种方式进行一些修改,避免计算光线和背面多边形的交叉点。由于在这种视处理方法中,我们没有明确地把坐标变换到视空间中,它留下的只是用于可能的剔除应用的世界和对象空间。将在下一章中,我们可
11、能会使用递归射线来追踪环境反射。由于这种光线的方向很难被提前预见,因此它们可能正好与观察者不可见的多边形相交。作为结果,在对象空间中剔除没有太多的意义。7.2 从后到前排序如我们在前一节看到的,背面剔除对多边形模型中的隐面消除来说是不够的。5一般情况下,在使用从世界到屏幕方法时,要找到所有被遮挡起来的多边形部分是比较困难的。要解决这一问题,我们可以对每一个多边形相对于所有其它的多边形进行反复的裁剪,并检查得到的面片的深度信息。如果面片沿着观察方向比裁剪多边形更远的话,那么它就一定被遮挡住了,就要被剔除掉。在这一处理过程结束时,我们会得到一系列新的多边形,它们都是可见的。毫无疑问,这种方法的计算
12、量将会很大,并不一定实用。我们还有一种方法,它充分利用了图形硬件的帧缓存结构。不管场景中的多边形挡没挡住其它的多边形,我们只要按照从后向前顺序光栅化图元,就可以正确的显示所有的图元。离观察者最近的一个多边形最后进行光栅化处理。这种方法就是我们常说的画家算法。我们必须注意,当多边形光栅化处理的计算量太大时,例如我们使用比较复杂的纹理映射和光线,再使用从后到前顺序就不太好了,因为许多多边形会被遮挡住,白白浪费了大量的工作。这时,使用我们前面所说的一些方法可能会比较适合。为了得到从后向前的顺序,沿着观察方向按照多边形的深度信息对它们进行排序是一种比较好的方法。如果使用了透视变换,我们在视空间中就不能
13、使用排序方法了,因为观察方法相对于不同的多边形会发生变化。观察方向只在光栅化处理阶段前才是一个常量,这时就可以使用排序方法了。为了沿着观察方向进行排序,必须找到一个多边形比较的判据。最简单的方法就是比较一个多边形上所有顶点中最大 z 坐标(离观察者最远) 。也可以比较多边形顶点 z 坐标的平均值。还有其它许多的排序算法。最直截了当的方法复杂度为 。也就是说为了On()2产生一个排序列表,这些算法中使用的大量的基本运算都与 成比例。这种算法的一个例子就是气泡排序法(bubble-sort) 。在这种算法中,我们在列表中将一个物体与它前面的物体进行比较,如果不满足排序判据,就将它们的位置进行交换。
14、如果我们从列表中的第一个物体开始对每一个物体进行比较,那么对于单个物体来说,它就会逐渐被交换到正确的位置,就像一个气泡被逐渐推到水面一样。我们将一个物体移动到正确的位置只需要进行 n-1 比较。(见图 7.6)4 3 5 1 2 3 4 5 1 2 3 4 5 1 2 3 4 1 5 2 3 4 1 2 53 4 1 2 5 3 4 1 2 5 3 1 4 2 5 3 1 2 4 5etc .One element sorted.Two elements sorted.图 7.6:气泡排序法具有平方复杂度的算法的计算量仍然是很大的。还有一类排序算法,它们的复杂度为 。这类算法都使用了各种不同的
15、细分和克服策略。例如快速排序算On(lg)法(quicksort ) ,它将一个列表分为两部分,并预先选出一个重心值,这样,列表一部分中的所有对象都比重心小,而另一部分中的所有对象都大于或等于重心。 如果我们递归地进行快速排序,那么最终就会得到排序的原始列表。这种算法的复杂度大约是 。(见图 7.7)nlg)67 3 5 1 2 4 3 1 2 4 5 7 3 1 2 5 7etc.Splitusing“4”asapivot:Aplyrecursively:图 7.7:快速排序算法的复杂程度已经很有效了,但我们还有一类排序算法,它们利用了这On(lg)样的一个事实,那就是我们的排序总有一定的范
16、围,这样它的复杂度就更小。例如,我们经常用 32 位来存储一个整型数,这也是我们能够表示一个整型数的界限。基数排序算法(Radix sort)就是这类算法中的一种,它的复杂度大约只有 ,()On但是它往往要求比较大的空间,有时又对基本操作或存储单元又不同的要求。这样,我们还是经常使用复杂度为 的一些算法。On(lg)基数排序(Radix sort)算法基于这么一个事实,即对来自一定范围的数进行排序比较容易。例如,如果允许的排序索引表(sorting key)的范围为0,3,那么我们就可以按照下面的步骤来放置标有这些索引表数值的对象。计算标有每个索引表数值的元素的总的序号。这些序号定义了一些偏移
17、量,根据偏移量每一组的元素必须放置在最终的列表中。比如说,标有 1 的对象必须跟在标有 0 的对象后面,并由此放置在最终的列表中。我们再一次遍历原始列表,这次根据已知的偏移量将对象放置在最终的列表中。这一算法就是计数排序算法(counting sort) ,当排序索引的范围较小时,它是比较适合的。基数排序算法需要一个有限的但不必非常小的范围,并且它通过多次引用一个与计数算法类似的过程来进行工作,每一次排序都按照不同的位来安排索引表数值。(见图 7.8,左图用低 2 位进行遍历排序,右图则再用高 2 位排序) Dec Hex Dec Hex Dec Hex Dec Hex4 0100 4 010
18、0 4 0100 1 0001 3 0011 5 0101 5 0101 2 00105 0101 1 0001 1 0001 3 0011 1 0001 2 0010 2 0010 4 01002 0010 3 0011 3 0011 5 0101Sorting using twolower bits. Sorting using twohigher bits.图 7.8: 基数排序从图 7.8 的例子可以看到,这种算法的复杂度也为 ,但是,它需要多次的O()穿越列表,这样它的执行效率就会降低。通常,快速排序算法在列表较小时执行的比较好。基数排序算法只有在列表较长,要表示的值的范围较窄时,它
19、的优点才会显现出来。我们使用上述算法的目的就为了将多边形按照从后向前的顺序放置。但是,所讨论的算法的性能依赖于要被排序的列表的混乱程度。许多 级的算法在整个n()2列表都被进行排序时工作得较好,这时,只需要很少的操作就可以完成工作。当列7表比较混乱时,快速排序算法工作的较好。只有在列表比较大、数值范围比较窄时,基数排序算法才比较适用。在三维图形程序中,我们经常希望慢慢的旋转多边形物体。这时,多边形的顺序在帧与帧之间变化不大,我们使用通常看起来效率不太高的算法就可以达到很好的性能了。另一方面,如果要确保通常情况下的比较平均的性能,那么快速排序算法比较适合。还应该强调,我们进行排序算法是基于这样的
20、一个假设的,就是我们已经具有一些排序判据来将多边形按照从后向前的顺序放置。不幸的是,并不完全是这样。有时按照最大 z 坐标来进行比较并不正确。(见图 7.9)ABmaxZA图 7.9:最大 z 坐标判据实效的情况在图 7.9 中,基于最大 z 坐标判据,多边形 A 应该首先被绘制,但是实际上它挡住了多边形 B 的一部分。类似的情况也会发生在使用平均 z 坐标判据的情况。在某些情况下,我们可以容忍上述的问题。当我们保证场景中的多边形的大小都相同时,基于最大或平均 z 坐标的排序算法是可以接受的。例如,当我们将某种解析形状如球、或双三次面片镶嵌成多边形时,上述这种情况就会发生。这时,简单的排序通常
21、是比较有效的。有时,我们不能保证多边形的大小完全相等,我们可以尝试使用更复杂的排序判据。如果我们可以找到一个点,它属于两个多边形的屏幕投影的交集,我们就可以进一步计算这一点的深度,并用结果作为判据来进行比较。为了定位这样一个点,我们不得不找到一个多边形的每个边与其他多边形的每个边的交集,还要检查一个多边形的屏幕投影被包含在其他多边形的屏幕投影中的情况。这样的一种方法可能在每个多边形的边较少时比较有效。当这一情况不能保证时,我们将不得不进行一定的条件放宽,并使用一些更经典的方法。例如,如果我们假设多边形是凸多边形,我们可以首先找到它们的公共切线,也就是一条将多边形的顶点都留在一侧的线(见图 7.
22、10) ,接下来,将会得到帆状或沙漏多边形来找到一个交点或显示不存在这种现象。(见图 7.11)为了找到公共切线,我们可以首先检查两个多边形的边延长线,例如可以从具有最小 y 坐标的顶点开始。既然在一个凸多边形中,一条边的延长线将所有的顶点都置于同一侧,这条线就可以称为一条切线。我们可以检查两个多边形的这些切线的关系。例如,在图 7.10 中的边 d,它位于边 a 延长线的右侧。我们处理下面的边,得到同样的情况:属于第二个多边形的边 e 位于 b 的右侧。但是,当我们考8虑相邻的边时,情况发生了变化,第一个多边形中的边 c 位于第二个多边形中的边f 的右侧。由于有切线相互交叉,换句话说当我们从
23、一个多边形中的方向 b 变化到方向 c,并且从另一个多边形中的方向 e 到方向 f 时,它们的关系会发生变化,因此,存在一条公共的切线,使得两个多边形的所有顶点都位于它的同一侧,这条切线就是图中点 A 和 D 的连线。(见图 7.10)abdcCommon tangent fAD图 7.10:找到两个多边形之间的桥梁必须注意,切线从不相交的情况是并不是不可能的。如果我们遇到一个多边形包含在另一个多边形内的情况,那么前者的所有点都可以被用来进行深度比较。一条公共切线就象一座桥一样将两个多边形联系在一起。它同样定义了一个类似帆的形状,它由两个交叉的凸多边形链组成,每一个都来自于一个多边形 。(见图
24、 7.11) ADCBIJKLLeft chain. Right chain.图 7.11:寻找交点为了找到两个链的交点,我们可以反复减小问题的大小直到找到交点为止。我们来看一下上图中的三角形 ABI 和 IJA。很明显,每一个都是一个 ear,也就是一个不包含在任何其他凸多边形的内部的三角形。为了验证这一点,可以检查 B 是否在 AJ 上,同样,也要检查 J 是否在 IB 上。确定了 ABI 是一个凸状体之后,我们可以将它去除掉,并继续进行最初的子问题一个 BI 位于顶部的帆。最后,CK 成为帆当前的顶,在这一点处,D 不在 CL 上,L 也不在 DK 上。这就意味着,我们已经找到了 CD
25、和 KL 的交点,并且可以进一步使用第五章中的技术来找到两个多边形在交点处的 z 坐标。应该注意,这两个多边形可能不会相交,这时,代替帆状多边形,我们将得到一个沙漏多边形。调整暗示的方法来找到合适发生这种情况并不困难。9这种方法的另一个加速的办法,就是使用约束体,并且在多边形的关系很明显时避免大量的计算。例如,如果一个多边形最小的 z 值比另一个多边形最大的 z 值还要大,那么,我们就可以确定它们的顺序了。(见图 7.12)ABminZAmaxZinZBinB图 7.12:多边形比较的扩展。当 z 值出现重叠时,我们再使用计算量较大的比较方法。除了计算量较大之外,上述的比较多边形进行排序的方法
26、还有它内在的问题。我们不能真正找到一个能够建立很好的顺序的判据。本质上,一个建立得很好的顺序就需要在任何的一系列的元素中都有最小的一个。(见图 7.13)BA C图 7.13:多边形比较的困难所在在图 7.13 中,多边形 A、 B 和 C 应该按照先 A 再 B 然后 C 的顺序排列,这是由于 B 挡住了 A 的一部分,而 C 又挡住了 B 的一部分。但是,如果在排序处理过程中,我们要对 A 和 C 进行比较,那我们就不能这么说了。这些多边形具有相同的最大和平均 z 坐标值,它们在屏幕上的投影也并不相交。我们将尝试着认为它们“相等” ,并且对我们来说,应该按照什么顺序进行渲染并不重要。但这并
27、不是真的:多边形 B 确实在 A 和 C 之间,并且它们的顺序也是很重要的。这种情况在有多重交叠存在是将更加严重(见图 7.14)。这些多边形中没有一个明确的最小者。 .10ABC图 7.14:多边形的多重交叠在这个例子中,Am_root-m_verteces0*M_LNG_OBJECT_VERTEX,verteces+order-m_root-m_verteces3*M_LNG_OBJECT_VERTEX, verteces+order-m_root-m_verteces6*M_LNG_OBJECT_VERTEX, plane); if(plane30) MI_render_polygons
28、(order-m_negative,verteces); M_render_polygon(order-m_root,verteces); MI_render_polygons(order-m_positive,verteces); else MI_render_polygons(order-m_positive,verteces); M_render_polygon(order-m_root,verteces); MI_render_polygons(order-m_negative,verteces); 考 查 平 面 方 程 的 D依 据 D的 符 号 以 不同 顺 序 遍 历 子 树在
29、 当 前 节 点 计 算 多 边 形 平 面 方 程表 7.1: 遍历一个 BSP 树我们考查了树的创建和遍历之后,仍然有一些问题需要解决。当我们构建一个树时,我们可以选择剩余多边形中的任何一个来分割空间。选择不同的多边形会导致不同的树的结构。因此,我们就应该考虑选择哪个多边形有助于算法的效率。有些多边形会导致剩余多边形更多的分割(见图 7.21、7.23 和 7.24)。每一个多边形在通过渲染管道时都有一定的系统消耗,因此多边形越少,性能就越好。我19们可以利用判据来选择有较少分割的多边形。使用判据来平衡 BSP 树,并不需要每一级中的子树中的多边形的数量都相同,因为它不会影响运行时间。树的
30、遍历总是假设至少每次都取一个多边形,因此平衡不会影响性能我们仍然不得不每次取每一个多边形。另一方面,一个平衡的树可以以较少的迭代调用来执行遍历。我们可以将平衡作为第二判据来使用。总之,使用 BSP 树来进行从后向前排序的最大优点就是算法运行的复杂性较低。这种方法也解决了多边形的多重交叠和多边形穿越问题。但是,通过使用一个预计算结构,我们已经失去了一定的灵活性。如果多边形的排列在运行时发生了改变,BSP 树就必须发生相应的改变。由于计算量非常巨大,我们不能这样做,因此,这种算法对于场景在运行时发生改变的情况就不能使用了。应该注意,只有当多边形经历不同的变换设置时树才会受到影响。如果所有的多边形都
31、使用同样的变换,那么分割仍然是正确的,树也将不会受到影响。这样,场景中的一个动态物体,它在世界中移动或者是旋转,仍然可以使用同样的 BSP树。如果物体中的一部分相对于其他物体发生了移动,那我们就要使用其它的算法了。7.6 Beam 树尽管使用前一节提及的 BSP 树能够有效地创建可用于画家隐面消除算法的多边形从后到前的顺序,但它仍然有某些缺点。当我们要绘制纹理或明暗处理多边形的时候,存在着画家算法的基本问题透支,其开销很大,不能够忽视。早先也注意到,在沿着所有其它的多边形执行裁剪的时候,看起来也是个低效率的解决方案,实际上,分析并抛弃遮住的部分可能对继续下去更有吸引力,因为它避免了透支。非常有
32、趣的是,BSP 树可能同样对后者有极大的帮助。我们即将讨论的Beam 树方法,可以同 BSP 树一同用于这两个方面:对多边形的排序和追踪屏幕上哪个区域被绘制。在前一节中,我们已经讨论了使我们能够使用 BSP 树获得多边形从后到前的顺序的算法。也可以用同样的方式获得从前到后的顺序,只需逆转 BSP 遍历算法中的调用顺序。因此,更靠近观察者的半空间将首先被遍历,然后是根多边形和较远的半空间遍历,产生需要的从前到后的顺序。获得的顺序中,第一个多边形最靠近观察者。既然没什么东西能遮挡它,这个多边形完整地在屏幕上可见,由此被光栅处理。所有其它的多边形可能完全或部分被第一个遮挡。如果我们沿着第一个多边形的
33、边界裁剪剩余多边形在屏幕上的投影,并抛除被遮挡的部分,我们实际上把原始问题的大小减小了一个。在列表开头的新多边形也是能被完整地看到的(因为它已经被沿着原来第一个多边形裁剪过了) ,而且,剩下的多边形如前面一样可能完全或部分被顺序列表中新的第一个多边形遮挡。必须承认,我们必须做大量的裁剪,这相当大地恶化了这种算法。为了更有效地管理裁剪,引入了平面 BSP 树,用来追踪屏幕上哪个区域剩了下来可用于绘制。不象在前面考虑的树,在这个 BSP 树中附加的叶节点将描述最终的凸多边形区域,其结果形成了细分区域,且没有相关联的多边形。该区域被标记为占用或空。由于2D 屏幕边界裁剪的基本目的基本是一样的寻找可被
34、光栅处理的图元部分,这20两个处理过程实际上是统一的。因此我们从描述屏幕区域为空(可绘制)的 BSP树开始,相对应的,在屏幕外的空间被标记为占用。图 7.27 演示了初始化的 BSP树和导致的分割部分。AFDCBOOOOADCBScreen partitioning: Initial beam tree:图 7.27: 用于空屏幕的 Beam 树当一个多边形必须被绘制的时候,它向下过滤 BSP 树。在某些节点可能被分割,这样该部分可以在正确的子树内被明确地检查。当某一块到达树叶,且该叶被标记为占用,我们就知道了这个多边形实际上被遮挡可以被抛除。另一方面,当多边形某块到达标记为空的叶时,该块被光
35、栅处理,由于多边形所在的区域现在被占用了,必须更新 BSP 树来反映这一点。考虑图 7.28 中的例子。在这个例子中,要绘制包含 E、G、H 边的多边形。首先沿着追踪当前屏幕上可见区域的 BSP 树根检验。在 BSP 树根,A 边分割了指定的多边形。A 左侧的较小部分要沿着左侧子树检验,剩余部分沿着右侧子树检验。在前者的情形下,该小块到达被占用的节点,因此被抛弃。对后者来说,多边形沿着 B 边检验,被上边分割的要被抛弃。低处的部分进一步到描述屏幕矩形的节点进行检验(参见图 7.27,标记为 F 的节点) 。在该点上,我们能够安全地光栅处理多边形剩余的部分,并更新树,使用多边形边进行进一步的分割
36、,同时标记多边形区域为占用,剩下的标记为空。显然,当多边形小块到达叶节点的时候,该块整个在叶描述的区域内。因此,任何必要的树的替换方案都位于子树的这个区域,并以该叶作为根节点。(参见图 7.28).21AFDCBOOOOGEFOAHGEDCBScreen partitioning: Updated beam-tree:图 7.28:在多边形被绘制之后的 Beam 树在屏幕平面创建的 BSP 树追踪没有被遮挡的视线(beam of view) ,这也是它的名称的由来。总的来说,在这种算法中,我们从前到后地拾取多边形,一次只拾取一个,并在 BSP 树中向下进行过滤,同时关联着屏幕。当多边形的一部分
37、或多个部分被光栅处理的时候,树就被更新,以追踪剩余的自由空间,这样,每个连续的多边性能一致的被检验。这样,透支问题被极大地避免了,尽管如此,这种算法的裁剪数量较大以及实现起来要更复杂。我们在下一章中讨论到阴影产生的时候,还要遇到一个使用 BSP 树的例子。显然,这个和其他的分割方案在解决计算机图形处理的许多问题的时候具有极大的重要性。7.7 扫描线算法在第六章中我们已经讨论过多边形模型的另一种表度方式,其中的多边形以边的形式来描述而不是用顶点的形式。这类表度方式能避免多余的裁剪且对隐面消除来说也相当地实用,在这一节中,我们对这种方法作一下分析。扫描线隐面消除的思路是把可见性确定从多边形层次转变
38、到单个的多边形像素线(参见图 7.29) 。这种算法可以被认为是通用多边形光栅处理的扩展,这种方法我们曾与凹多边形一起讨论过。我们应该看到,这两种算法的许多思想是一致的。22图 7.29:在每条扫描线上确定隐藏面在这种算法中,场景中的所有多边形同时被光栅处理,可见性判定在平面内执行,该平面与当前屏幕上的扫描线正交,我们将要在屏幕上判定交叉的多边形的关系。图 7.30 演示了一个例子,三个多边形被使用这种算法进行光栅处理。能够看出,多边形中与扫描线相交的边的顺序的信息对这个算法来说是最至关紧要的。我们能够在每条扫描线上使用与对凹多边形光栅处理应用该算法时相同的方法得到这样的边顺序。有了这个顺序后
39、,可见性的判定可以通过相当直截了当的方式完成,假定我们也有多边形平面方程。让我们分析一下在图 7.30 中的扫描线 1。IBCDEFAB,AC2:4:Presorted edges:AB,AC,DE,DF,BC,KI,KJ,EF,IJ1: DE,DFAB,AC,BC,DE,AC,DFJAKIJ,EF,BC,DF,AC,KJ3:Active edges in scan-lines:图 7.30: 不同扫描线上的活动边在图 7.30 中的扫描线 1 只与多边形 ABC 相交,因此,我们在这些边中光栅化该多边形。扫描线 2 交叉了两个多边形,但交叉边的顺序是这样的:在遇到多边形 DEF 之前我们结束
40、了多边形 ABC 的光栅处理。此情形对扫描线 3 来说不是这样的。我们开始渲染多边形 ABC,在结束之前,遇到了属于多边形 EDF 的边DE。在此阶段,我们必须在该点上比较这两个多边形的深度以判定可见性。如果在此位置对两个多边形求平面方程的值,就可以完成判定。在这个特别的情形下,多边形 DEF 的深度较小,因此我们先渲染它。当我们进一步遇到 AC 边的时候,我们可以忽略它,因为多边形 DEF 的光栅处理还没有结束,且 AC 边属于早就被判定为较远的多边形。在某种意义上,每条边在端点间定义了一个范围,在其中的被光栅处理。如果在某些点上多边形被遮挡,我们仍然必须稍后对其进行光栅处理。这种情形就是扫
41、描线 4 所演示的(参见图 7.30) 。因此,在交叉 DF 边之后,我们必须分析多边形ABC 和 IJK 的深度,在此情形中,光栅处理多边形 ABC。当我们越过 AC 边的时候,我们仍然在一个多边形的范围内,对此多边形仍然要进行处理。正如我们看到的,这种分割算法相当容易被实现。我们也取决于这么一个事实,即在每条扫描线上,我们都有多边形的排序。如我们所讨论的,这种顺序可以通过以最小垂直顶点坐标预排序所有的多边形来获得,在两条边相同的情形中,使用第二个评判标准,比较具有最大垂直坐标的端点的水平坐标。有了这个顺序后,我们能够以增量方式为每条扫描线更新当前边。一旦我们找到了当前边,也必须根据当23前
42、水平坐标排列。必须强调的是这种算法的优点在于它能够忽略被遮挡的扫描线的光栅处理。正如我们看到的,如果在场景中使用了复杂的纹理映射或照明的时候,这变得极其重要。这种算法也正确地处理了多边形相互重叠的情形,但是在形式不变的时候,它不能处理多边形穿越的情形。使用这种算法也暗示了对已存在的多边形管道的修改,因为全部多边形同时被光栅处理有时候并不合算。7.8 Z-buffer 算法迄今为止的大多数算法都有一个很大的限制,它们处理的对象都模拟为多边形集。有时候这不是问题。我们光栅处理的内容可能表示为其它各种各样的图元形式。即便是在多边形模型的情形中,当多边形数量增加的时候,大多数隐面消除方法的性能出现了不
43、成比例的退化。在本节我们要考虑的算法适合以任何一种方法光栅化任何一种图元,并且在本质上其工作时间是线性的,这就是说,复杂性与场景中的图元数量成比例。Z-buffer 算法的思路是,它把寻找哪些是可见的,哪些是被遮挡的处理过程从图元层次或扫描线层次上进一步转变到了单个的像素层次上。换句话说,每次我们要判定某些图元的一个像素在图元光栅处理前是否应该被绘制时,我们把该像素的颜色同 z(深度)坐标存储在一起。如果某个属于另一个甚至是同一个图元的像素必须被绘制在同一个位置上,必须比较 z 值,且如果新像素实际上更靠近观察者,则它将替代前面被绘制上的像素。如果新像素被判定距离更远,我们在该位置上保留原先的
44、像素。图 7.31 演示了两个被光栅化的图元位置,使用 Z-buffer 算法用于隐面消除。4535.53 5.5 ZZ-buferScren图 7.31:使用 Z-buffer 隐面消除的光栅处理从图 7.31 中,我们能够看出,每个屏幕像素,除了一些图像位图的存储单元之外,还必须分配空间存储 z 值。所有 z 值的数组被存在“Z-buffer”中。在帧渲染的开始,我们必须在 Z-buffer 中以选定的精度用最远的 z 值初始化所有的位置。作为结果,在任意位置获得的第一个像素将有必要通过算法逻辑的比较允许其被绘制。在多边形情形中,z 坐标的判定可以通过线性插值来完成。我们使用与光栅化24明
45、暗强度相同的算法来判定,该方法在光栅处理中内插明暗以及在线性纹理映射中内插纹理坐标。因此,我们将在保留每个像素上的 z 直到光栅处理阶段,沿着边内插它然后沿着扫描线在边上使用该值。必须注意到,如果我们应用了透视投影变换,在可用空间中的 z 坐标不是线性地变化的。比较有吸引力的是使用 来代替深度标准, 在这样的空间中是CzC线性变化的。图 6.32:使用 Z-buffer 算法处理对象的交叉在 Z-buffer 算法的众多优点中,可能它的简单性是最大的一个优点。由于这种简单性,它成为了最可能通过硬件实现的算法。它的通用性和本质上的线性运行时间使它对最高级的应用充满了吸引力。Z-buffer 算法
46、的问题可能来自这么一个事实,即我们只有极为有限的位数来表度屏幕上像素的 z 坐标。在某些场合下,我们对 z值可能的舍入或截尾引起了对像素的人为干扰,位数的减少可能引起可见性的错误判定。当然,对实现来说,我们必须在光栅处理例程内循环中加入一定数量的代码。这也导致了某些性能上的恶化,使这种算法对有中等数量图元的应用没什么吸引力。我们必须也要注意到,Z-buffer 数组也是相当大的,虽然随着时间的推移,内存的限制越来越小,但对某些应用来说,用初始值填充 Z-buffer 可能导致相当可观的花费。这个算法也很容易受到透支问题的困扰,因为被遮挡的图元仍然必须被光栅处理。小结总的说来,当我们将图元从世界
47、投影到屏幕上而获得虚拟世界的图像的时候,隐藏面带来的问题就会出现在从世界到屏幕的视处理过程中。一些图元可能会遮挡住屏幕投影中的其他图元, 这样我们就需要一些方法来将隐藏面去除掉。对于我们已经讨论过的几种消除隐藏面的方法,许多都只能应用于使用多边形表示的模型。25一种比较普通的方法就是按照从后向前的顺序对多边形进行光栅化处理,使得靠近观察者的多边形能够覆盖掉远离的观察者的多边形。我们有很多方法来将多边形按照从后向前的顺序进行排列。具体的算法包括排序、空间分割等。但是, 这种方法在执行光栅化处理的时候系统开销会很大,因为它要光栅化所有的多边形,包括被遮挡住的。这时,我们就要考虑采取其它一些看起来效
48、率低下的方法,例如使用 Beam树对多边形进行反复的裁剪,以避免对不必要的多边形进行光栅化。Z-buffer 方法对图元的形状没有任何的要求,并且它也是最简单的隐面消除算法,还经 常通过硬件来执行。这种算法同样要我们对 无用的多边形进行处理。 扫描线隐面消除算法使我们避免了这种情况,但是它只适用于多边形模型,同时还要求对多边形管道进行相当大的改变。隐面消除算法的运行时间是很难进行比较的,因为它们都有一个基本的不同复杂性的步骤。这样,具有线性运行时间的算法执行起来可能比具有指数性运行时间的算法执行起来更糟糕。前一种形式的优点通常只在多边形的数On(lg)量非常巨大的时候才能显现出来。只有基于一些特殊的情况和对条件进行适当的放宽,才能 够确定选择哪一种策略。