(转!)Z buffer和W 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
场景中,一个三角面可能有些地方远,有些地方近,因为三角面有三个顶点,而这三个顶点和观察者的距离,通常都是不同的。所以,要以哪个顶点来排序呢?或是以三角面的中心来排序?事实上,不管以什么为依据来排序,都可能会有问题。下图是一个「画家算法」无法解决的情形:

上图中,三个三角面互相遮住对方,所以不管用什么顺序去画,都无法得到正确的结果。另外,这个方法也无法处理三角面有交叉的情形。
当然,如果相当确定场景中不会出现这么奇怪的情形,那「画家算法」一般还是可以用的。不过,它还有一个很大的问题,就是效率不佳。首先,画家算法需要对场景中,在视角范围内所有的三角面做一个排序的动作。最好的排序算法也需要
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

时间: 2024-10-06 10:47:12

(转!)Z buffer和W buffer简介的相关文章

SQL Server 环形缓冲区(Ring Buffer) -- RING BUFFER CONNECTIVITY 的深入理解

SQL Server 环形缓冲区(Ring Buffer) -- RING BUFFER CONNECTIVITY 的深入理解 首先我们从连接的Ring Buffer数据返回的XML来入手. SELECT CAST(record as xml) AS record_data FROM sys.dm_os_ring_buffers WHERE ring_buffer_type= 'RING_BUFFER_CONNECTIVITY' 执行上面的语句,得到下面的结果: 点击XML的超链接,打开文件内容

directx12中vetex buffer、index buffer和constant buffer绑定piple line的时机

类别 时机 函数 建Heap vetex buffer 在Draw函数中 ID3D12GraphicsCommandList::IASetVertexBuffer 否 index buffer 在Draw函数中 ID3D12GraphicsCommandList::IASetIndexBuffer 否 constant buffer 在程序初始化时 ID3D12Device::CreateConstantBufferView 是

The Bip Buffer - The Circular Buffer with a Twist

Introduction The Bip-Buffer is like a circular buffer, but slightly different. Instead of keeping one head and tail pointer to the data in the buffer, it maintains two revolving regions, allowing for fast data access without having to worry about wra

insert buffer/change buffer double write buffer,双写 adaptive hash index(AHI) innodb的crash recovery innodb重要参数 innodb监控

https://yq.aliyun.com/articles/41000 http://blog.itpub.net/22664653/viewspace-1163838/ http://www.cnblogs.com/MYSQLZOUQI/p/5602206.html https://yq.aliyun.com/articles/222 主从不一致性的3种可能原因1.binlog format是不是row2.session级关闭binlog3.人工在slave修改数据 set sql_log_

Direct Buffer vs. Heap Buffer

1. 劣势:创建和释放Direct Buffer的代价比Heap Buffer得要高: 2. 区别:Direct Buffer不是分配在堆上的,它不被GC直接管理(但Direct Buffer的JAVA对象是归GC管理的,只要GC回收了它的JAVA对象,操作系统才会释放Direct Buffer所申请的空间),它似乎给人感觉是"内核缓冲区(buffer in kernel)".Heap Buffer则是分配在堆上的,或者我们可以简单理解为Heap Buffer就是byte[]数组的一种

C语言创建循环缓冲区(环形缓冲区)-- Circular Buffer(Ring Buffer)

由于嵌入式系统的资源有限性,循环缓冲区数据结构体(Circular Buffer Data Structures)被大量的使用. 循环缓冲区(也称为环形缓冲区)是固定大小的缓冲区,工作原理就像内存是连续的且可循环的一样.在生成和使用内存时,不需将原来的数据全部重新清理掉,只要调整head/tail 指针即可.当添加数据时,head 指针前进.当使用数据时,tail 指针向前移动.当到达缓冲区的尾部时,指针又回到缓冲区的起始位置. 目录: 为什么使用循环缓冲区 C 实例 使用封装 API设计 确认

week6

关于io流: I\O操作指的是输入和输出流的操作,相对内存而言,当我们从数据源中将数据读取到内存中,就是输入流,也叫读取流.当我们将内存中处理好的数据写入数据源,就是输出流,也叫写入流. 流按照内容分类:分为三种, 字节流字符流对象流 其实真正的流只有一种:字节流,数据的传输在底层都是以二进制方式传输,所以无论是哪种流,都是字节流.而字符流和对象流是在字节流基础上,做了一层封装,以方便对字符数据和对象数据进行操作. 所有字节流的父类,InputStream和OutputStream所有字符流的父

Java之IO流基础流对象

输入流和输出流是相对于内存设备而言 即将外设中的数据读取到内存中就是输入 将内存中的数据写入到外设中就是输出 字符流的由来: 其实就是:字节流读取文字字节数据后,不直接操作而是先查指定的编码表,获取对应的文字. 再对这个文字进行操作,其实就是字节流+编码表 字节流的两个顶层父类: 1,InputStream  2,OutputStream 字符流的两个顶层父类: 1,Reader   2,Writer 这些体系的子类都以父类名作为后缀. 而子类名的前缀就是该对象的功能. 如果要操作文字数据,建议

黑马程序员——Java基础--IO流(一)---字符流和字节流

------Java培训.Android培训.iOS培训..Net培训.期待与您交流! ------- 一.IO流的介绍及体系 IO流用来处理设备之间的数据传输.java对数据的操作是通过流的方式.java用于操作流的对象都在IO包中. 输入流和输出流相对于内存设备而言:将外设中的数据读取到内存中:输入.将内存中的数据写出到外设中:输出. 流按照操作数据分为两种:字节流和字符流. 字符流的由来:其实就是字节流读取文字字节数据后,不直接操作而是先查指定的编码表,获取对应的文字,再对这个文字进行操作