第一部分:
这个是3D图形学中很重要的内容,虽然基础,但对理解3D图形世界非常关键。所以了解透彻是很有用处的。
下面先讲讲Z坐标。Z坐标和X、Y坐标一样。在变换、裁减和透视除法后,Z的范围为-1.0~1.0。DepthRange映射指定Z坐标的变换,这与用于将X和Y映射到窗口坐标的视口变换类似,但DepthRange映射又与视口映射有所不同,因为深度缓存的硬件方案对应用程序来说是隐藏的。调用DepthRange的参数是[0.0,1.0],与一片断相联的Z值(深度值)表示到眼睛的距离。在默认情况下,最接近眼睛的片断(在近截面上)被映射到0.0,离眼睛最远的片断(在远截面上)映射到1.0。片断可以映射为深度缓存范围的子集(通过在DepthRange中指定更小的值)。映射也可以相反,这样的话离眼睛最远的片断在0.0,最近的片断在1.0(调用DepthRange(1.0,0.0)),虽然这样反向映射是可以的,但对实际应用作用不大。
要理解为什么渲染质量上会不一致,最重要的是要理解屏幕Z坐标的特性。Z值指定了从片断到眼睛的距离。在正交投影中距离和Z值的关系是线性的,但在透视投影中却不是的。在透视投影中这种关系是非线性的,而且非线性的程度与Frustum函数中的far/near(或gluPerspective函数中的zFar/zNear)成比例。这种非线性在靠近近截面时增加了Z值的精度,而且增加了深度缓存的效率;但是在视见体的其它部分则降低了精度,也就减少了深度缓存的精确性。根据经验,far/near比值大于1000会有这种不好的效果。所以一般far/near比值应小于1000。要想解决这个问题,最简单的方法是通过将近截面远离眼睛来降低far/near比值,其唯一的副作用是离眼睛很近的物体可能会被裁减掉,但在特定的应用程序中这很少是个问题,近截面的位置对X、Y坐标的投影没有影响,因此这对图像的影响很小。
还有OpenGL光栅化和深度缓存的一些其他方面值得一提。一个大问题是光栅化过程使用不精确的算法,所以很难处理共面的图元,除非它们共享相同的平面方程。这个问题在有限精度的深度缓存实现中更加严重。这些问题包括:贴花(decaling)、隐藏线消除、轮廓多边形和阴影等。不过现在已经提出许多方法来解决这些问题,如PolygonOffset技术等。
深度缓存的位数是衡量深度缓存精度的参数。深度缓存位数越高,则精确度越高,目前的显卡一般都可支持16位的Z Buffer,一些高级的显卡已经可以支持32位的Z Buffer,但一般用24位Z Buffer就已经足够了。
第二部分:
几乎所有目前的 3D 显示晶片都有 Z buffer 或 W buffer。不过,还是常常可以看到有人对 Z buffer 和 W buffer 有一些基本的问题,像是 Z buffer 的用途、Z buffer 和 W buffer 的差别、或是一些精确度上的问题等等。这篇文章的目的就是要简单介绍一下 Z buffer 和 W buffer。
Z buffer 和 W buffer 是做什么用的呢?它们的主要目的,就是去除隐藏面,也就是 Hidden surface elimination(或是找出可见面,Visible surface detemination,这是同样意思)。在 3D 绘图中,只要有两个以上的三角面,就可能会出现某个三角面会遮住另一个三角面的情形。这是很明显的现象,因为近的东西总是会遮住远的(假设这些三角面都是不透明的)。所以,在绘制 3D 场景时,要画出正确的结果,就一定要处理这个问题。
不过,这个问题是相当困难的,因为它牵扯到三角面之间的关系,而不只是某个三角面本身而已。所以,在做去除隐藏面的动作时,是需要考虑场景中所有的三角面的。这让问题变得相当的妖杂。而且,三角面往往并不是整个被遮住,而常常是只有一部分被遮住。所以,这让问题变得更妖杂。
要做到去除隐藏面的最简单方法,就是「画家演算法」(Painter's algorithm)。这个方法的原理非常简单,也就是先画远的东西,再画近的东西。这样一来,近的东西自然就会盖住远的东西了。因为油画的画家通常会用这样的方法,所以这个方法被称为「画家演算法」。下图是一个例子:
上图中,红色的圆形最远,所以最先画。然后是黄色的三角形,最后是灰色的方形。照远近的顺序来画,就可以达到去除隐藏面的效果。所以,只要把 3D 场景中的三角面,以对观察者的距离远近排序,再从远的三角面开始画,应该就可以画出正确的结果了。
不过,实际上并没有这么理想。在 3D 场景中,一个三角面可能有些地方远,有些地方近,因为三角面有三个顶点,而这三个顶点和观察者的距离,通常都是不同的。所以,要以哪个顶点来排序呢?或是以三角面的中心来排序?事实上,不管以什么为依据来排序,都可能会有问题。下图是一个「画家演算法」无法解决的情形:
![](http://www.linuxmine.com/imgfiles/2004.12.28.15.58.33.2.jpg)
上图中,三个三角面互相遮住对方,所以不管用什么顺序去画,都无法得到正确的结果。另外,这个方法也无法处理三角面有交叉的情形。
当然,如果相当确定场景中不会出现这么奇怪的情形,那「画家演算法」一般还是可以用的。不过,它还有一个很大的问题,就是效率不佳。首先,画家演算法需要对场景中,在视角范围内所有的三角面做一个排序的动作。最好的排序演算法也需要 O(n log n) 的时间。也就是说,(大致上来说)如果三角面的数目从一千个变一万个,排序需要的时间会变成约 13.3 倍。而且,因为这需要对场景中所有的三角面来做,因此也不适合用特别的硬体来做加速。另外,这个方法还有一个很大的问题,就是它会花很多时间去画一些根本就会被遮住的部分,因为每个三角面的每个 pixel 都需要画出来。这也会让效率变差。
如果场景是静态(不动)的,只有观察者会变动的话,那是有方法可以加快排序的速度。一个很常用的方法是 binary space paritioning(BSP)。这个方法需要事先对场景建立一个树状结构。建立这个结构后,不管观察者的位置、角度是如何,都可以很快找出正确的绘制顺序。而且,BSP 会视需要切开三角面,以处理像上图那样,三个三角面互相遮住对方的情形。
不过,BSP 结构在建立时,要花很多时间,所以不太可能即时运算。因此,通常只能用在场景中的静态部分,而会动的部分还是需要另外排序。而且,BSP 常会需要切开三角面,也会让三角面的数目增加。另外,BSP 仍然无法解决需要画出那些被遮住的 pixel 的问题。
另一种去除隐藏面的方法,是直接以 pixel 为单位,而不是以三角面为单位,来考虑这个问题。其中最简单的方法是由 Catmull 在 1974 年时提出来的,也就是 Z buffer(或称 depth buffer)。这个方法非常简单,又容易由特别设计的硬体来执行,所以在记忆体容量不再是问题后,就变得非常受欢迎。
Z buffer 的原理非常简单。在绘制 3D 场景时,除了存放绘制结果的 frame buffer 外,另外再使用一个额外的空间,也就是 Z buffer。Z buffer 记录 frame buffer 上,每个 pixel 和观察者的距离,也就是 Z 值。在开始绘制场景前,先把 Z buffer 中所有的值先设定成无限远。然后,在绘制三角面时,对三角面的每个 pixel 计算该 pixel 的 Z 值,并和 Z buffer 中存放的 Z 值相比较。如果 Z buffer 中的 Z 值较大,就表示目前要画的 pixel 是比较近的,所以应该要画上去,并同时更新 Z buffer 中的 Z 值。如果 Z buffer 中的 Z 值较小,那就表示目前要画的 pixel 是比较远的,会被目前 frame buffer 中的 pixel 遮住,所以就不需要画,也不用更新 Z 值。这样一来,就可以用任意的顺序去画这些三角面,即可得到正确的绘制结果。下图是一个例子:
![](http://www.linuxmine.com/imgfiles/2004.12.28.15.58.53.3.jpg)
上图中,红色的三角面虽然先画出来,但是因为使用了 Z buffer,所以后画的黄色方块还是只会遮住适当的部分,而不会连较近的部分都遮住。这就显示出 Z buffer 的效果。
实际上 Z buffer 中能存放的数字当然会有一定的限度,所以通常会把 Z 值缩小到 0 ~ 1 的范围。因此,在绘制 3D 场景时,就会需要把可能出现的 Z 值限制在某个范围内。通常是用两个和投影平面平行的平面,把所有超出这两个平面范围的三角面都切掉。这两个平面通常分别称为 Z near 和 Z far,分别表示较近的平面和较远的平面。而在 Z near 平面的 Z 值为 0,在 Z far 的 Z 值为 1。
在效率上 Z buffer 并不一定会比「画家演算法」要快。但是,它比较简单。而且,它的效率和三角面的数目并没有太大的关系,而是和绘制的 pixel 数目有关。所以,而且可以很容易设计出特定的 3D 硬体来做这个动作,而不需要由 CPU 来做。而 Z buffer 所需要的额外记忆体,在今天已经显得不是很重要。所以现在几乎所有的 3D 显示晶片都是使用 Z buffer。
不过,Z buffer 并非全无问题。一个很大的问题是在於精确度上。如果有两个三角面很靠近,而其中一个完全在另一个之前,那应该只能看到一个三角面才对。但是,如果 Z buffer 的精确度不够,那这两个三角面每个 pixel 的 Z 值可能会很接近。再加上计算出来的 Z 值一定会有误差,所以,很可能会造成应该被遮住的三角面,却有一些 pixel 没有被遮住。这种情形称为 Z fighting。下图中,球在地面上的影子就是一个例子:
![](http://www.linuxmine.com/imgfiles/2004.12.28.15.59.14.4.jpg)
要避免这类问题,就要避免在场景中出现太过靠近,且接近平行的三角面。一般的场景不太会出现这个情形。不过,Z buffer 的精确度问题并不只是这样而已。在下一部分会对这个问题有更详细的说明。
前面把 Z buffer 的原理做了一个大概的说明,听起来 Z buffer 似乎是个很理想的技术。但是,实际上 Z buffer 有一个很大的问题,就是精确度的问题。
在前一页后面所提到的,两个非常接近的平面所出现的 Z fighting 情形,其实是相当少见,而且很容易避免的。当然,遇而还是会看到有一些游戏会出现这种情形。不过,Z buffer 最严重的问题是在离观察者较远的部分。如果 Z buffer 的精确度不够,而场景又很远的的话,那远处的东西就会出现一些非常奇怪的现象。下图是一个例子:
![](http://www.linuxmine.com/imgfiles/2004.12.28.16.2.11.5.jpg)
Z aliasing
![](http://www.linuxmine.com/imgfiles/2004.12.28.16.6.7.6.jpg)
无 Z aliasing
当然,上面的例子是比较极端的情形。实际上一般情形下并不会有这么夸张的 Z aliasing 现象。不过,我相信大家多少都在一些场景较大的游戏中,看过类似的情形。
为什么会有这样的现象呢?这就要从 Z buffer 的结构谈起了。如果前一页所说的,一般的显示晶片,是把 Z 值限制在 0 ~ 1 的范围,再用一个定点数去表示它。例如,一个 16 位元的 Z buffer,可能会用 0 ~ 65535(一个 16 位元数字可表示的范围)来表示这个 0 ~ 1 之间的 Z 值。
如果 Z buffer 的分布在 eye space 中是线性的,也就是它的每个数字之间的间隔都相等的话,那这样的精确度应该是蛮高的才对。因为,假设观察者可以看到一公里远的东西,那每个间隔就是约 1.5 公分。如果用更高精确度的数字来表示的话(像是 24 位元数字),那精确度还会更高。然而,Z buffer 在 eye space 中并不是线性的。它是在 projection space 中为线性。
如果你觉得这些听起来像是外星话的话,现在就要来「翻译」这些外星话。首先,先来看一张示意图:
![](http://www.linuxmine.com/imgfiles/2004.12.28.16.6.36.7.jpg)
上图是一个眼睛在透视投影的情形下,观看场景中的一个红色平面的情形。靠近眼睛的平面(上面有黄色点的)是代表投影平面,也就是 3D 绘图中的萤幕。黄色的点红色平面投影到萤幕上的 pixel,他们当然是等间距的。但是,注意看这些「等间距」的 pixel,他们所对映的 Z 值(也就是 Z 轴上的那些灰色的点),并不是等间距的。实际上,离眼睛愈远的 pixel,其 Z 轴上的间距就愈大。
这其实透视投影的一个明显的性质。因为在透视投影的情形下,愈远的东西看起来愈小,所以,在萤幕上同样的间距,在比较远的地方,就会变得比较大。因此,虽然三角面是平面,但是它在每个 pixel 上的 Z 值却不是线性的变化。因此,就无法用线性内插来计