☂️

Z-buffer

场景中有很多物体,要把他们放到屏幕上,自然涉及到顺序的问题,那么先放什么后放什么才能让画面看起来是对的呢?
很自然就能想到先画远的,再画近的覆盖掉远处的物体。从远到近就能把正确的结果画出来,这也是以前油画画家的画法。

Painter’s Algorithm

我们先对远处物体做光栅化,再对近处的物体慢慢处理完即可,这种算法叫画家算法
画油画,可以先画远处的山,再画地面,再画树。
画立方体,也可以先画最远的面,再画周围四个面,再画最前面的面。
但画立方体周围四个面时也需要顺序,我觉得周围四个面距离我都差不多,那么顺序怎么定呢?我们想要定义深度其实不容易。
notion image
画家算法可以按照深度从远到近排序,需要 的排序时间复杂度。
但有一种情况比较特别,下面三个三角形如何判断哪个三角形在前哪个三角形在后呢?
三角形两两覆盖,互相遮挡,就没办法定义其深度关系,就无法按照画家算法排序。
notion image
因此图形学引入了一个概念——Z-Buffer(深度缓冲)。

Z-Buffer

既然空间中三角形不好排远近顺序,但我对像素来说挺容易分析的。每个像素在像素内永远记录像素所表示的几何的最浅的深度。
我们渲染出成品的图像的同时,也会生成另一个图像,只存每个像素记录的对应几何最浅深度的信息,这个图叫做深度图、深度缓存。再利用深度缓存来维护遮挡信息。
也就是说,我们需要两个缓冲:
  • 成品图像的颜色:frame buffer
  • 深度缓存:depth buffer (z-buffer)
前面我们的相机放在原点往 方向看,这样我们看到所有的 都是负的,数字小反而离我们远。那么为了简化计算,我们认为 是这个点到摄像机的距离,永远是正的,越小越近。

Z-Buffer Example

notion image
上面两张图就是前面我们说的两种缓存。
深度图越黑,代表离视点越近,深度越小。越白代表离视点越远,深度越大。
越黑也是因为深度越小,颜色值越小,那么对应的颜色越黑。
💡
那么如何做算法?
一张图中,我们有很多个三角形,每个像素都可能会对应几个三角形(同一个像素上有地板、立方体等),那么像素上就记录最前的深度。
  • 首先认为深度缓存中所有的深度,一开始记录为 无限远。
  • 然后对一个个三角形做光栅化,对于任意一个三角形,我们都能光栅化成一个像素,那么我们就比较这个像素的深度和三角形的深度。如果这个深度小于深度缓存之前记录好的深度,则更新像素的深度。
下面是光栅化的过程:
for (each triangle T) for (each sample (x,y,z) in T) if (z < zbuffer[x,y]) // closest sample so far framebuffer[x,y] = rgb; // update color zbuffer[x,y] = z; // update depth else ; // do nothing, this sample is occluded
notion image
最后我们维护出一个逐像素的深度,就能得出结果了。
 
前面说到画家算法需要 的时间。那么这里如果我们认为如果三角形不是特别大特别小,会覆盖常数项像素,那么深度缓存算法复杂度为 常数乘以三角形个数。我们这里没有排序,每个像素只记录当前看到的最小值。
💡
画红三角和紫三角,如果我们先画紫三角再画红三角,结果是否会一样?
深度缓存算法和顺序没关系,不管通过什么顺序画三角形,只要维护对了深度缓存的算法,就会得到一样的结果。
我们假设不会两个三角形在同一个深度。因为图形学中,我们很多数字都是通过浮点型的数字来表示,我们可以认为两个浮点型数字不会相同。但现实中会出现深度完全一样,z-fighting,但这里不多说。
这个算法被实现到 GPU 的硬件中。
为了做反走样用 MSAA,一个像素中取很多个采样点,那么我们就需要对采样点做 z-buffer 的记录。
💡
透明物体 z-buffer 处理不了,需要特殊处理。
 

存在的问题

  • 场景中存在半透明物体的正确渲染
  • 半透明物体间相互交叠的问题
  • 半透明物体的自身交叠问题及双面渲染

场景中存在半透明物体的正确渲染

由于通过透明物体是可以看到被其遮挡的物体的,因此对于深度缓存中的深度值就不能直接替换更新,深度值应该是离相机最近的不透明物对应的片元的深度值,否则就会出现透明物体挡住不透明物体的渲染错误。
因此在渲染半透明物体时,需要关闭深度写入,但要保留深度测试。(深度测试的目的在于判断:是否因为遮挡而舍弃当前片元,如果该透明片元在不透明物体后,它被遮挡了,深度值比较大,当然应该被丢弃。当它在不透明物体前,它就不会被丢弃,但是又不会将当前的深度值写入,还是会以不透明物体的深度信息为准。)
同时对于颜色也不可以和以前一样替换更新,而是要做透明度混合,即用帧缓存中已有的颜色和透明片元的透明色混合出一个新的颜色。
场景总存在透明物体时的渲染步骤:
  • 先渲染不透明物体,后渲染透明物体
  • 对透明物体进行排序,然后从远到近的顺序依次渲染。(因为只有这样才能正确地叠上,并混合出正确的颜色)
  • 并开启它们的深度测试,但关闭深度写入。
💡
但此时还存在两个问题: 1)仍然无法解决透明物体相互交叠的问题。 2)透明物体内部如果自身互相交叠也会存在问题。无法判断片元的先后顺序。
📖
总是会有一些情况打乱我们的阵脚,但由于上述方法足够有效并且容易实现,因此大多数游戏引擎都使用了这样的方法。为了减少错误排序的情况,我们可以尽可能让模型是凸面体, 并且考虑将复杂的模型拆分成可以独立排序的多个子模型等。其实就算排序错误结果有时也不会非常糟糕,如果我们不想分割网格,可以试着让透明通道更加柔和,使穿插看起来并不是那么明显。我们也可以使用开启了深度写入的半透明效果来近似模拟物体的半透明(详见8.5 节)。 《Unity Shader 入门精要》
notion image

半透明物体间相互交叠的问题

半透明物体的自身交叠问题及双面渲染

  • 自身交叠
    • 可以先用一个 Pass 写深度,第二个 Pass 再正常地混合颜色
notion image
notion image
  • 双面渲染
    • 关闭背面剔除,将双面渲染的工作分成两个pass,渲染两次:
    • 第一个pass只渲染背面 Cull Front
    • 第二个pass只渲染正面 Cull Back
    • 由于 Unity 会顺序执行 SubShader 中的各个 Pass,因此我们可以保证背面总是在正面被渲染之前渲染,从而可以保证正确的深度渲染关系。
      notion image