之前学习渲染管线的时候,没有对混合阶段的各种测试展开细说,本篇将详细介绍模板测试和深度测试有关的知识,主要内容包括:
- 渲染管线终极版
- 模板测试
- 深度测试
- Early-Z 和 Z-Prepass
1 渲染管线终极版
之前的学习中我们已经对渲染管线有了一个整体流程上的认识,但具体细节和阶段内部的顺序并没有过多关注,在学习模板测试和深度测试有关的内容之前,有必要为之前的渲染管线加上一些“细节”,以便于后续的理解。
下图展示了整个渲染管线的流程,包含了各阶段内部和操作内部的顺序以及流程细节,可以作为最终版放在脑中:
图中:
- 绿色的阶段都是完全可编程的
- 蓝色的阶段可配置,但不可编程
- 黄色的阶段完全固定
- 虚线为可选阶段
2 模板测试(Stencil Test)
模板测试简单来说就是根据模板缓冲区的数值决定该像素的颜色值,最简单的比如只有模板缓冲为 1 的像素才显示,为 0 则不显示,类似于一个 mask 操作,如下图所示:
模板缓冲区与颜色缓冲区和深度缓冲区类似,模板缓冲区可以为屏幕上的每个像素点保存一个无符号整数值(通常是 8 位整数)。这个值的具体意义视程序的具体应用而定。在渲染的过程中,可以用这个值与一个预先设定的参考值相比较,根据比较的结果来决定是否更新相应的像素点的颜色值。这个比较的过程被称为模板测试。模板测试发生在透明度测试(alpha test)之后,深度测试(depth test)之前。如果模板测试通过,则相应的像素点更新,否则不更新。
模板测试可以定义不同的比较方式:
还可以定义更新缓冲值的方式:
总的来说模板测试就是对当前模板缓冲值(stencil Buffer Value)和模板参考值(reference Value)使用特定的比较操作进行比较来决定是否渲染该像素,模板测试后可以根据测试结果,按照特定方式更新模板缓冲区的值。
模板测试可以与其他测试或图形算法结合实现许多效果,比如:描边、多边形填充、反射区域控制等。
3 深度测试(Z Test)
深度测试发生在模板测试之后,透明度混合之前。所谓深度测试,就是针对当前对象在屏幕上(更准确的说是frame buffer)对应的像素点,将对象自身的深度值与当前该像素点缓存的深度值进行比较,如果通过了,本对象在该像素点才会将颜色写入颜色缓冲区,否则否则不会写入颜色缓冲。
深度缓冲(Z-Buffer)就像颜色缓冲(储存所有的片段颜色)一样,在每个片段中储存了信息,并且(通常)和颜色缓冲有着一样的宽度和高度。深度缓冲是由窗口系统自动创建的,它会以 16、24 或 32 位 float 的形式储存它的深度值。在大部分的系统中,深度缓冲的精度都是 24 位的。一般来说,深度缓冲区中存储的深度值为 0 到 1 范围的浮点值,且为非线性。深度值在各个空间的变化如下图:
深度写入(Z-Write)包括两种状态:ZWrite On 与 ZWrite Off。当我们开启深度写入的时候,通过深度测试则将新的深度值写入深度缓存;反之,如果关闭深度写入,那么深度就不会写入深度缓冲区。
深度测试对深度缓冲区和颜色缓冲区的写入情况有以下四种:
- 深度测试通过,深度写入开启:写入深度缓冲区,写入颜色缓冲区;
- 深度测试通过,深度写入关闭:不写深度缓冲区,写入颜色缓冲区;
- 深度测试失败,深度写入开启:不写深度缓冲区,不写颜色缓冲区;
- 深度测试失败,深度写入关闭:不写深度缓冲区,不写颜色缓冲区;
一般来说,深度测试可以自定义比较方式,默认为小于等于,即深度小于等于缓冲区中的深度时则通过测试,并且深度写入默认开启。深度测试的流程图如下:
深度测试不仅可以解决遮挡问题,还可以应用于很多效果,比如:阴影贴图、透明渲染、粒子渲染、切边效果、X光等。
4 Early-Z 和 Z-Prepass
4.1 Early-Z
在正常的渲染管线中,深度测试在所有测试完成后才进行,片元着色器计算的所有片元经过深度测试后会有一大部分被舍弃,这相当于进行了很多无用的计算,而实际上在进入片元着色器之前我们就已经知道了所有顶点的深度,因此完全可以提前进行深度测试,将深度大的片元提前舍弃,不去计算,这样就可以节省很多的计算量,这就是 Early-Z 的思想。上面的渲染管线图中给出了 Early-Z 在整个管线中的位置。
但是有一些情况下,Early-Z 会失效或使用 Early-Z 会造成错误:
- 开启了透明度测试(Alpha Test ),这时如果提前进行深度测试,可能导致透明物体后的片元没有通过深度测试而渲染不出来
- 进行手动剔除(discard)操作,这时提前通过深度测试筛选出来的片元也可能会被手动剔除而造成错误
- 片元着色器中手动修改 GPU 插值得到的深度,这时提前通过深度剔除片元很大可能会造成错误
- 开启了透明度混合(Alpha Blend),开启透明度混合一般会关闭深度写入,所以 Early-Z 不生效
- 关闭深度测试时 Early-Z 自然也不生效
Early-Z 进行的操作和原本逐像素处理阶段的 Z-Test(为了 Early-Z 区别,这个阶段也会被称为 Late-Z)操作完全一样,现代的 GPU 已经都开始包含这样的硬件设计。但是 Early-Z 有以下两个主要的缺点:
- 一旦进行了手动写入深度值、开启 alpha test 或者丢弃像素等上述操作,那么 GPU 就会关闭 Early-Z 直到下次清空 Z-Buffer 后才会重新开启(不过现在的 GPU 也在逐渐优化,使其更智能的开关 Early-Z)。之所以 GPU 会选择关闭 Early-Z 是因为上述那些操作可能会在片元着色器与 Late-Z 阶段之间修改深度缓存中的深度值,导致提前的 Early-Z 结果不正确。我们也可以在 fragment shader 中使用
layout(early_fragment_tests)
来强制打开 Early-Z。 - Early-Z 的优化效果并不稳定,最理想条件下所有绘制顺序都是由近及远,那么 Early-Z 可以完全避免过度绘制。但是相反的状态下,由远及近绘制物体, Early-Z 则会起不到任何效果。所以有些时候为了完全发挥 Early-Z 的功效,我们会在每帧绘制时对场景的物体按照到摄像机的距离由远及近进行排序。这个操作会在 CPU 端进行,当场景复杂到一定程度,频繁的排序将会占用 CPU 的大量计算资源。
4.2 Z-Prepass
Z-Prepass 是一种软件技术。它主要是配合 Early-Z 使用,来减少上面提到的 Early-Z 的第二个缺点——效果不稳定。Z-Prepass 的做法是将场景做两个 pass 的绘制。第一个 pass 仅写入深度,不做任何复杂的片元计算,不输出任何颜色。第二个 pass 关闭深度写入,并将深度比较函数设为“相等”。
本节一开始就提到, Early-Z 的出现是因为经过大量运算的片元,很大概率会在之后被丢弃掉。那么对于第一个 pass 由于只写入深度,不在片元做任何计算,所以即便之后会被丢弃,也并不可惜。也就是说无论场景中的物体以怎样的顺序绘制,我们都可以以很小的代价提前绘制好当前场景的深度缓存。那么在第二个 pass 时,Early-Z 就可以用这个深度缓存中的值和当前深度值进行比较,只绘制深度相等的片元,任何其他的片元都可以直接丢弃,因此第二个 pass 要把深度比较函数设为“相等”。同时当前的深度缓存已经是完全正确的结果了,因此第二个 pass 也不需要对深度缓存做任何更新,便可以关闭深度写入。
Z-Prepass 必须配合 Early-Z 才能发挥效果,如果没有 Early-Z 的话,第二个 pass 的深度测试依旧在片元着色器之后,因此所有片元都会在片元阶段进行复杂计算。Z-Prepass 的思想和延迟渲染管线(defered render pipeline,之后会专门总结)有些相似,差别在于:
- 第一,Z-Prepass 的第一个 pass 只计算深度,并且结果直接存储在深度缓存。而延迟渲染会同时计算更多其他的屏幕空间数据,并将这些数据存储在额外的 frame buffer 中,需要更大的缓存(也就是G-Buffer)。
- 第二,Z-Prepass 的第二个 pass 依旧需要对全场景的各个物体进行绘制(至少顶点阶段是如此),而延迟渲染的第二个 pass 类似于后处理,本质上只绘制了一个屏幕大小的矩形。