几乎所有目前的 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 场景时,要画出正确的结果,就一定要处理这个问题。
如果场景是静态(不动)的,只有观察者会变动的话,那是有方法可以加快排序的速度。一个很常用的方法是 binary space paritioning(BSP)。这个方法需要事先对场景建立一个树状结构。建立这个结构后,不管观察者的位置、角度是如何,都可以很快找出正确的绘制顺序。而且,BSP 会视需要切开三角面,以处理像上图那样,三个三角面互相遮住对方的情形。
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 值。这样一来,就可以用任意的顺序去画这些三角面,即可得到正确的绘制结果。下图是一个例子:
上图中,红色的三角面虽然先画出来,但是因为使用了 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。下图中,球在地面上的影子就是一个例子:
前面把 Z buffer 的原理做了一个大概的说明,听起来 Z buffer 似乎是个很理想的技术。但是,实际上 Z buffer 有一个很大的问题,就是精确度的问题。
在前一页后面所提到的,两个非常接近的平面所出现的 Z fighting 情形,其实是相当少见,而且很容易避免的。当然,遇而还是会看到有一些游戏会出现这种情形。不过,Z buffer 最严重的问题是在离观察者较远的部分。如果 Z buffer 的精确度不够,而场景又很远的的话,那远处的东西就会出现一些非常奇怪的现象。下图是一个例子:
Z aliasing
无 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 中为线性。
如果你觉得这些听起来像是外星话的话,现在就要来「翻译」这些外星话。首先,先来看一张示意图:
上图是一个眼睛在透视投影的情形下,观看场景中的一个红色平面的情形。靠近眼睛的平面(上面有黄色点的)是代表投影平面,也就是 3D 绘图中的萤幕。黄色的点红色平面投影到萤幕上的 pixel,他们当然是等间距的。但是,注意看这些「等间距」的 pixel,他们所对映的 Z 值(也就是 Z 轴上的那些灰色的点),并不是等间距的。实际上,离眼睛愈远的 pixel,其 Z 轴上的间距就愈大。
这其实透视投影的一个明显的性质。因为在透视投影的情形下,愈远的东西看起来愈小,所以,在萤幕上同样的间距,在比较远的地方,就会变得比较大。因此,虽然三角面是平面,但是它在每个 pixel 上的 Z 值却不是线性的变化。因此,就无法用线性内插来计