几乎所有目前的 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
场景中,一个三角面可能有些地方远,有些地方近,因为三角面有三个顶点,而这三个顶点和观察者的距离,通常都是不同的。所以,要以哪个顶点来排序呢?或是以三角面的中心来排序?事实上,不管以什么为依据来排序,都可能会有问题。下图是一个「画家算法」无法解决的情形:
上图中,三个三角面互相遮住对方,所以不管用什么顺序去画,都无法得到正确的结果。另外,这个方法也无法处理三角面有交叉的情形。
当然,如果相当确定场景中不会出现这么奇怪的情形,那「画家算法」一般还是可以用的。不过,它还有一个很大的问题,就是效率不佳。首先,画家算法需要对场景中,在视角范围内所有的三角面做一个排序的动作。最好的排序算法也需要
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
值。这样一来,就可以用任意的顺序去画这些三角面,即可得到正确的绘制结果。下图是一个例子:
上图中,红色的三角面虽然先画出来,但是因为使用了 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 aliasing 无 Z aliasing
前面把 Z buffer 的原理做了一个大概的说明,听起来 Z buffer 似乎是个很理想的技术。但是,实际上 Z buffer 有一个很大的问题,就是精确度的问题。
在前一页后面所提到的,两个非常接近的平面所出现的
Z fighting 情形,其实是相当少见,而且很容易避免的。当然,遇而还是会看到有一些游戏会出现这种情形。不过,Z buffer
最严重的问题是在离观察者较远的部分。如果 Z buffer
的精确度不够,而场景又很远的的话,那远处的东西就会出现一些非常奇怪的现象。下图是一个例子:
当然,上面的例子是比较极端的情形。实际上一般情形下并不会有这么夸张的 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 值却不是线性的变化。因此,就无法用线性内插来计算三角形内部的 pixel 的 Z 值。但是,要正确计算出每个 pixel
上的 Z 值,会需要一个除法的动作,而除法是很讨厌、很花时间的动作。
早期的显示芯片无法花费一个除法器在 Z buffer
上面。所以,一个方法是在 Z buffer 中,不要存放 eye space 的 Z 值,而改成存放 projection space 的 Z
值。这样一来,Z 值在 projection space 就会变成是线性的,就可以简单地用线性内插来计算三角形内部的 pixel 的 Z
值了。这也是目前几乎所有显示芯片的 Z buffer 的设计。
不过,在 projection space 中的 Z
值,就像上面的图所显示的一样,有一个很重要的特性:它所对映的 eye space 的 Z 值间隔,在愈远的地方就愈大。所以,Z buffer
的精确度,如果以 eye space
来看的话(这样看才有意义),就会变成不平均的分布。离观察者愈近的地方,其精确度会比远的地方更高。而这个精确度的变化,会取决于 Z near
平面和 Z far 平面的位置。Z near 平面离观察者愈近,且 Z far
平面离观察者愈远,则精确度的变化就会愈大,也就是远的地方的精确度会愈差。
在这一页最前面的两张图中,其 Z far 平面的位置是一样的,但是左图的 Z near 平面的位置,比右图的 Z near 平面的位置近了一千倍。所以,在左图中就出现了严重的 Z aliasing 现象,但是右图就没有出现这种现象。
所以,要尽可能避免
Z aliasing 现象,就要尽可能把 Z near 平面拉远,而把 Z far
平面拉近。但是,实际上很多情形下,是无法允许这样的设计的。比如说,在一个场景中,玩家可能会看到 50
公分远的桌子上的东西,而同时看到窗外在一公里外的一座大基地。因此,Z near 平面不能设得比 50 公分要远,但是 Z far
平面又得到一公里远。以 16 位 Z buffer 来看,那最远处的间隔(也就是一公里远的地方)会达到 30 公尺,也就是如果两个 pixel
的间距小于 30 公尺的话,Z buffer 将无法分辨出正确的顺序!而它在 Z near 处(也就是 50 公分的地方)的精确度则高达
0.0000076 公尺。这显示出精确度分布是如此的不平均和不适当。如果改用 24 位 Z buffer
的话,那情形会有相当程度的改善,在一公里远处的精确度会提高到约 12 公分。这也是为什么 24 位 Z buffer 很少会显示出 Z
aliasing 的情形。
不过,即使是 24 位 Z buffer 也不见得是完全理想。以上面的例子来说,如果 Z near
平面移到 10 公分处,在远处的精确度就会从 12 公分降低为 60 公分。有些人可能会觉得,在一公里远的地方,又有谁能分辨 60 公分,或是
12 公分呢?但是,问题在于,当两个大的平面的距离小于 60 公分时,因为 Z buffer
无法分辨出正确的顺序,就可能会在这一框是某个平面被画出来,而在下一框却变成是另一个平面被画出来。如果这两个平面的颜色差别很大,就会产生闪烁的现象,任何人都会很容易就注意到的。
有些显示芯片采取一些方法来解决这个问题。一个简单的想法是在 Z buffer
中使用浮点数,而不使用定点数。经过适当的设计,浮点数可以在某个特定的数字附近,提供更大的精确度范围(一般情形是在 0 附近)。而一般的 Z
buffer 在 Z far 附近会需要更高的精确度,所以可以把 Z buffer 在 Z far 平面以 0 表示,而 Z near 平面以 1
表示。这样就可以得到更高的精确度。不过,浮点数在计算上比较麻烦。特别是 Z buffer
的运算中,常需要做加法和比较的运算,这都会比定点数的运算要麻烦很多。
另外一个方法是用非线性的 Z buffer。例如,可以把 Z buffer 切成很多个小区间,而每一个小区间中都是一般的线性 Z buffer。但是,可以在远方分配更多的小区间,让它的精确度可以提高。这也是一种解决精确度问题的方式。
其实,要解决
Z buffer 精确度问题,最简单的方法就是在 eye space 中做线性内插。但是,前面已经说过,在 eye space 中的线性,在
projection space
并不一定是线性,所以它会需要额外的除法器。不过,有一个方法可以避免使用除法器,而只需要「倒数器」,「倒数器」比完整的除法器要简单一些。这个方法就是先以较高的精确度,在
projection space 中,对 Z 做线性内插。对每个内插得到的结果,再用倒数器算出其倒数,也就是所谓的 W 值。这个 W
值的精确度可以较低,因为它在 eye space 中的分布是平均的。最后,再把这个 W 值和 "W buffer"
里面的值做比较,就可以得到正确的顺序。这个方法,相信有些人已经猜到了,就是W buffering。
当然,另外还有一些别的方法可以实作出
W buffer。不过,不管是用什么方法实作 W buffer,其最重要的性质就是在 eye space 中为线性分布。因此,16 位的 W
buffer 在远处的精确度是非常理想的。以前面的例子来说,即使是 24 位 Z buffer,在一公里远处的精确度也只能到 12 公分。但是
16 位 W buffer 则是每个地方的精确度都是 1.5 公分。因此,在这个例子中,16 位 W buffer 在远处的表现,甚至比 24 位
Z buffer 更好。
而且,W buffer 还有一个好处,就是其 W near 平面(相对于 Z buffer 中的 Z
near 平面)的位置是不重要的。也就是说,W buffer 可以同时兼顾眼前的桌子,和数公里外的巨大基地。而用 Z buffer
的话,如果想要能正确显示出数公里外的巨大基地,那可能就得牺牲眼前的桌子了。
不过,因为 W buffer
的精确度是平均分布,它在 Z near 处的精确度就明显不如 Z buffer 了。虽然说 Z buffer 在 Z near
处的精确度是过于高了(像是 0.0000076 公尺),但是,W buffer 却可能会过于低。比如说,1.5
公分的精确度对于远处的物体是绝对足够的,但是对于靠近观察者的物体,则是明显的不足。比如说,桌上可能有一本厚度小于 1.5 公分的书。这时,1.5
公分的精确度是完全不够的。
这样听起来,好像 W buffer 也无法解决问题嘛!其实并不是这样的。如果有 24 位的 W
buffer,同时可以看到十公里外的东西(这应该算是非常的远了),那它的精确度还是有约 0.6
公厘左右。这样的精确度一般来说是相当足够的了。而且 W buffer 也很容易使用,不需要对程序有什么重大的修改。
目前 W buffer 最大的问题就是支持度不够。有些显示芯片根本就不支持 W buffer,而有些则只支持 16 位的 W buffer。不过,目前很多显示芯片都已经开始支持 W buffer,所以将来应该会有更多游戏使用吧!
原文地址:https://www.cnblogs.com/aibox222/p/9682657.html