前提知识
深度测试
对某个片元进行深度测试: 比较该片元已经存在于深度缓冲区中的深度值,如果当前片元的深度值比深度缓存中的深度值大(距离摄像机远),说明该片元是被遮挡的,无需参与合并,也就是说这个片元没有通过深度测试。
什么是合并: 合并决定了通过深度测试的片元的颜色如何写入到颜色缓存中,如果是不透明物体,直接覆盖掉原来的颜色即可,如果是透明物体,就要和原本颜色缓存中的颜色进行混合后写入。
Unity 的渲染流水线中,采用了 Early-Z 技术,即在片元着色器之前进行深度测试,提前知道那些片元是舍弃的,对于舍弃的片元,无需再使用片元着色器进行着色。
深度写入
一个物体通过了深度测试,如果开启了深度写入的话,就会把当前片元的深度值写入到深度缓存中。深度写入的开启与否是可配置的,一般来说,不透明物体都会开启深度写入,以使得屏幕总是显示距离摄像机最近的片元。
渲染顺序
不透明物体需要在透明物体之前渲染。
一般来说,渲染半透明物体会关闭深度写入。
对于下面这种情况:A、B是不透明物体,C是半透明物体,如果我们先渲染A、再渲染C,最后渲染B,那么A的颜色会最先写入到颜色缓存中,渲染C时,会将C与A的颜色进行混合,但并不会更新深度缓存中的深度值,最后渲染B的时候,B发现自己的深度小于深度缓存中的值,于是就会覆盖掉深度缓存中的值以及颜色缓存中的值,导致C也被覆盖。
所以,正确的做法应该是先处理所有的不透明物体,得到一个颜色,然后在这个颜色上进行混合。
对于透明物体来说,渲染顺序也是有讲究的,我们需要先渲染后面的透明物体,再渲染前面的,这样才能得到正确的混合结果。
透明物体渲染
Unity中,通常使用两种方法实现透明效果,其一是透明度测试,其二是透明度混合。
透明度测试
这种方法其实并不能得到半透明效果,透明度测试是一种极端的做法,如果片元通过了透明度测试,则视作完全透明(不处理),否则视作不透明片元处理。通常来说,透明度测试的依据是片元的透明度是否小于某个阈值。
if(texColor.a - _Cutoff < 0.0){
discard;
}
透明度混合
通常来说,透明度混合并不需要开启深度写入,例如,Unity中 Standard Shader 中的透明效果就没有开启。
混合是不可编程的,但是是高度可配置的。简单来说,混合就是颜色缓冲中的颜色乘以一个权重与当前颜色乘以一个权重相加。编写Shader时,我们可以配置这两个权重。
O
r
b
g
=
S
r
c
F
a
c
t
o
r
×
S
r
g
b
+
D
s
t
F
a
c
t
o
r
×
D
r
g
b
O_{rbg}=SrcFactor \times S_{rgb}+DstFactor\times D_{rgb}
Orbg=SrcFactor×Srgb+DstFactor×Drgb
O
a
=
S
r
c
F
a
c
t
o
r
A
×
S
a
+
D
s
t
F
a
c
t
o
r
×
D
a
O_{a} =SrcFactorA\times S_{a}+DstFactor\times D_{a}
Oa=SrcFactorA×Sa+DstFactor×Da
一个问题: 对于一些物体来说,它们的前后顺序并不能绝对地定义,例如下面这种相互交叉的情况。
这三个半透明物体使用的是Unity内置的Standard Shader,位置前后相互交叉,在上面的交叉点,实际上绿色物体是靠近摄像机的,但镜头轻微挪动,却会得到两种渲染效果。
这里的渲染瑕疵看起来并算不上严重,但对某些内部网格多次交叉的物体来说,会渲染出非常奇怪的效果。
一种解决方案是:额外增加一个Pass,在这个Pass中不输出任何颜色信息,仅开启深度写入。
Pass{
ZWrite On
ColorMask 0
}
引起上述瑕疵是由于片元着色器先渲染了前面的片元,再去渲染后面的片元,导致看起来后面的物体在前面,开启深度写入后,渲染前面的片元时会写入深度值,使得后面的片元不能通过深度测试,这样就不会引起视觉错误,代价是完全丢弃了后面的片元。
参考资料
《Unity Shader入门精要》- 冯乐乐