在渲染管线中, 深度测试是一个很关键的阶段, 它是后续阶段的基础, 也是影响视觉的重要阶段.
深度测试发生在片元着色器处理之后(准确的说的透明度测试, 模板测试之后), 透明度混合之前.
在片元着色器对所有的片元着色之后, GPU会对每个片元执行深度测试来决定遮挡关系.
我们知道渲染的任务是将三维的模型世界转换到二维图像, 最终会生成为一幅图像, 存放到帧缓存中, 缓存中的每一个位置存储着颜色值.
在这个过程中, 由于是三维空间到二维图像的转换, 二维图像的每个位置可能会被多次映射, 即被多个片元覆盖, 那么到底使用哪个片元的颜色会决定最后的结果是否正确. 这个决定使用哪个片元的颜色来存储的任务就是深度测试和其对应的深度写入来完成的.
GPU中会有帧缓存对应的深度缓存(Z-Buffer), 深度缓存中的位置与帧缓存的位置一一对应, 唯一的差别是帧缓存中存储的是片元的颜色值, 而深度缓存中存储的是片元的深度值.
我们一般可以简单理解深度缓存中的值的大小表示距离摄像机的远近, 值越小表示距离越近, 当然这个值不是坐标的Z值, 是投影后的值.
深度测试的原理很简单: 比较源深度值(就是要用来覆盖的值)与目标深度值(就是深度缓存中已经存在的值), 按照某种规则来决定是否通过测试, 我们可以通过给深度测试不同的参数来执行规定不同的规则. 如果通过测试了, 帧缓存中的颜色值会被源片元(源深度值所在片元)的颜色覆盖, 但是深度缓存中的深度值是否被覆盖, 还要看深度写入是否开启.
简单说就是, 深度测试的通过与否, 决定者帧缓存的颜色值是否更新, 深度测试通过后, 深度值是否更新还要受深度写入的开关来控制.
换句话说, 我们可以只更新颜色而不更新深度缓存, 后续的透明度混合阶段就是利用了这个功能.
通过给测试传递不同的参数来决定通过测试的规则, 常用的有以下值:
Greater
: 当源深度值比目标深度值大时通过测试. 即 D e p t h s > D e p t h d Depth_s > Depth_d Depths>DepthdLess
: 当源深度值比目标深度值小时通过测试. 即 D e p t h s < D e p t h d Depth_s < Depth_d Depths<DepthdGEqual
: 大于等于LEqual
: 小于等于Equal
: 等于NotEqual
: 不等于Always
: 总是通过Never
: 总是不通过Off
: 关闭测试, 等于Always默认的参数是LEqual
, 即源深度值小于等于目标深度值时通过测试.
深度缓存中默认存放的值是无限大的值, 如果使用大于或者等于相关的参数需要注意, 不然会出现渲染不出任何颜色的问题.
图1中的的绿色盒子使用的参数是Greater
, 由于深度缓存中默认存放的是无限大的值, 所以永远无法通过测试, 导致所有的片元都被抛弃, 无法渲染出颜色.
图2中的蓝色盒子使用的是LEqual
, 所以任意的值都能通过深度测试并刷新了深度缓存中的值, 然后绿色盒子在进行深度测试时, 就可以与深度缓存中的值作比较了, 因为绿色盒子深度值更大, 所以所以遮挡的部分都会使用绿色盒子的片元颜色, 没有遮挡的部分则无法通过测试.
在ShaderLab中使用ZTest param
来设定深度测试的参数, 可以给SubShader, 也可以给Pass设置, 使用ZWrite Off/On
来开关深度写入.
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
ZTest Always
Pass
{
ZTest Greater
ZWrite On
}
}
默认情况下, 只需要根据深度测试就可以得出正确的遮挡关系而不需要关心渲染顺序. 但是在渲染半透明物体时, 我们需要注意渲染的顺序.
因为我们想要透过半透明物体看到后面的物体, 如果先绘制半透明物体, 再绘制不透明物体, 深度测试无法通过, 被半透明物体遮挡的部分就会抛弃(因为半透明物体更近). 当然, 如果先绘制不透明物体, 再绘制半透明物体则没有这个问题.
大部分引擎会先把所有的不透明物体按照某种渲染顺序, 从远到近, 从近到远都可以, 因为深度测试可以检测出来.
两者的区别可能就是性能会有所有差异, 比如由近到远的绘制, 因为已经记录更近的物体的深度, 在绘制远处物体时, 无法通过测试就能减少很多片源的后续处理.
Unity会在渲染之前作模型的合并, 可以提前将被遮挡的部分抛弃, 在所有不透明物体渲染之后, 将所有的半透明物体按照从远到近来渲染, 这样能够最大化的降低因为渲染顺序而导致的不正确效果.
如果半透明物体之间没有交叉, 这个渲染是没有问题的, 但是大部分绘制都是基于对象的, 两个物体完全有可能在中心点上是一远一近, 但是其它部分可能是一近一远, 如果不做特殊处理, 就会出现近的物体的一部分被遮挡.
还比如我们在Unity默认的渲染顺序下基本上不会出现什么问题, 但是如果修改了渲染顺序, 比如我强制将某个距离更近的半透明物体先进行绘制, 然后再进行其它物体的绘制, 这时就会出现问题, 因为深度缓存中已经记录了最先绘制的半透明物体的深度值, 再绘制其它更远的物体时无法通过深度测试, 所以会被抛弃掉遮挡的部分.
所以一般情况下会关闭半透明物体的深度写入(ZWrite Off
), 这样, 先绘制更近的半透物体只会更新颜色缓存而不会更新深度缓存, 继续绘制其它物体时因为深度缓存的值还是原来的值, 就相当于没有绘制过该半透物体一样, 那么其它物体就能正确进行深度测试.
我们经常会在渲染半透明物体的shader中看到以下代码:
ZWrite Off
就是这个原因.
如果渲染不透明物体顺序正确的情况下, 且物体之间互相分离, 关不关闭深度写入其实都能得到正确的结果. 否则关闭深度写入能够最大限度的保证结果正确.
虽然依然不能从根本上解决问题, 但是在大部分情况下问题已经不是那么明显了.
关闭深度写入只是一个表面看上去稍微可以折中解决问题而且代价不高的方案.
总而言之, 之所以会出现不透明物体的渲染问题, 归根结底是因为大部分引擎使用的技术是基于对象来排序的, 无法处理像素级别的排序, 现在有专门的技术: 顺序无关的透明度(order independent transparency, OIT)来解决.
我们这里只是简单讲解一下原理, 未来有机会再给大家做一些实际的测试来论证.
上面介绍了, 深度测试是发生在片元着色器处理之后, 就是说在所有的片元都处理完了之后, 在通过深度测试来抛弃那些被遮挡的片元.
那么既然要抛弃, 为什么不在着色之前抛弃呢? 这样还少处理很多的片元.
因为我们要处理的片元可能存在半透明的问题, 就是说, 虽然片元被遮挡了, 但是不一定会被抛弃, 如果遮挡这个片元的片元是半透明的, 那么这个片元还是要渲染的.
那么如果我们在着色之前就能确定是否存在半透遮挡, 那么不就可以提前进行深度测试了?
是的, 这个技术就叫做Early-Z.
当然, 我们在着色之后还是要进行深度测试的, 因为在着色的过程中可能产生半透遮挡, 不过这可能已经减少了大量的片元了.
好了, 今天只是对深度测试做简单介绍, 更复杂的内容, 比如Z冲突, 深度值的非线性特性等内容, 将来有机会再给大家分享, 希望对大家有所帮助.