图形 3.5 Early-z和Z-prepass
Early-z和Z-prepass
B站视频:图形 3.5 Early-z和Z-prepass
文章目录
- Early-z和Z-prepass
- 深度测试(Depth Test)
- 传统渲染管线中的深度测试
- 逐片元测试
- 解决物体可见遮挡性的问题
- 流程图
- 代码逻辑
- 带来的问题
- 提前深度测试(Early-Z)
- 传统渲染管线中的提前深度测试
- Early-Z失效
- 高效利用Early-Z
- Z-Prepass
- 方法一:双Pass法
- 动态批处理的问题
- 方法二:提前分离的PrePass
- 使用URP下的RendererFeature
- 透明渲染的解决方案
- Z-Prepass的计算消耗
- Early-Z **和** Z-Prepass的实例应用
- 面片叠加的头发渲染
- 最初方案
- 性能改善
- 优化的方案
深度测试(Depth Test)
传统渲染管线中的深度测试
逐片元测试
解决物体可见遮挡性的问题
例如需渲染一紫一橙两个三角形(如下图):
- 紫色三角形的深度值为5,与深度缓冲区中的默认值 ∞ \infty ∞进行比对,5小于 ∞ \infty ∞,故写入深度缓冲区中;
- 黄色三角形在渲染时,与深度缓冲区中的值进行比对,小于缓冲区的值则进行替换,最终结果如右图。
流程图
代码逻辑
//对每一个三角形
foreach(triangle T){
//对每一个三角形中的片元
foreach(fragment(x,y,z) in T){
//深度测试:如果片元深度小于ZBuffer深度
if(fragment.z < ZBuffer[x,y]){
//更新颜色
FrameBuffer[x,y] = fragment.rgb;
//更新深度
ZBuffer[x,y] = fragment.z;
}
//深度测试失败
else{
//什么都不做,片元数据被丢弃
}
}
}
带来的问题
未通过测试的片元将会被丢弃,导致片元的计算就白白浪费了,造成一些性能的问题。
提前深度测试(Early-Z)
传统渲染管线中的提前深度测试
提前深度测试(Early-Z)发生在光栅化之后,片元着色器之前。
还是上面的例子:
注:提前深度测试阶段仍然可以搭配模板测试(stencil test)使用
Early-Z失效
在以下几种情况下,Early-Z会失效:
-
开启Alpha Test 或 clip/discard 等手动丢弃片元操作;
还是以上图为例子,若物体1在shader中被手动discard了,物体1不会被渲染,但是其深度还在,Early-Z阶段会认为物体1仍然挡住了后续的物体,导致后续的物体2不能被正确的渲染出来。
-
手动修改GPU插值得到的深度;
-
开启Alpha Blend;
进行透明度混合的物体,一般不会开启深度写入(ZWrite Off)。
-
关闭深度测试Depth Test。
一旦进行了以上操作,那么GPU就会关闭early-z直到下次clear z-buffer后才会重新开启(不过现在的gpu也在逐渐优化,使其更智能开关early-z)。之所以gpu会选择关闭early-z是因为上述那些操作可能会在片元阶段与late-z阶段之间修改深度缓存中的深度值,导致提前的early-z的结果并不正确。我们也可以在fragment shader中使用layout(early_fragment_tests)来强制打开early-z。
高效利用Early-Z
如果将不透明物体从远至近进行渲染,Early-Z将不会带来任何优化效果。
此外,如果通过CPU将不透明物体从近至远排序,在场景十分复杂的情况下,将会带来额外的性能损耗,且可能导致合批操作无法正常进行。
Z-Prepass
方法一:双Pass法
- 在第一个Pass即Z-Prepass中仅仅只写入深度,不计算输出任何颜色;
- 在第二个Pass中关闭深度写入,并且将深度比较函数设置为相等(Equal)。
Shader "Custom/EarlyZPresentation"{
Properties{
//.......
}
SubShader{
Tags { "RenderType"="Opaque" }
//Z-Prepass部分
Pass
{
//开启深度写入
ZWrite On
//关闭颜色输出
ColorMask 0
CGPROGRAM
//...这里省略了单纯地顶点变换计算部分
ENDCG
}
//正常地计算输出颜色的Pass
Pass
{
//关闭深度写入
Zwrite Off
//深度相等为通过
ZTest Equal
CGPROGRAM
//...这里省略了顶点变换和颜色等计算部分
ENDCG
}
}
}
动态批处理的问题
一个物体的材质球shader若是多Pass,则无法参与合批,带来Drawcall问题。
方法二:提前分离的PrePass
仍然使用两个Pass,但:
- 将原先第一个Pass(即Z-Prepass)单独分离出来为单独一个Shader,并先使用这个Shader将整个场景的Opaque的物体渲染一遍;
- 而原先材质只剩下原先的第二个Pass,仍然关闭深度写入,并且将深度比较函数设置为相等。
使用URP下的RendererFeature
透明渲染的解决方案
Z-Prepass也是透明渲染的一种解决方案,可防止复杂的半透明物体内部深度穿插的问题。
Z-Prepass的计算消耗
引用国外论坛的帖子。
条件 | 性能 |
---|---|
没有Z-PrePass + 简单的几何变换光栅Pass | 0ms + 几何变换光栅Pass ~2.7ms = ~2.7ms |
有Z-PrePass + 简单的几何变换光栅Pass | Z-PrePass ~2.0ms + 几何变换光栅Pass ~2.4ms = ~4.4ms |
因此是否使用Z-Prepass因根据项目实际情况去使用,例如在一个场景中有非常多的OverDraw,且不能很好的将不透明物体从前往后进行排序,那么使用Z-PrePass的计算消耗要远小于这些OverDraw。
Early-Z 和 Z-Prepass的实例应用
面片叠加的头发渲染
如下图,该头发效果有多个半透明面片构成,需要从后往前进行排序渲染才能得到正确的透明度混合结果。
最初方案
有三个Pass:
- 第一个Pass处理不透明的部分。
- 开启透明度测试,仅通过不透明的像素;
- 关闭背面剔除;
- 开启深度写入,并将深度测试通过条件为小于(Less);
- 第二个Pass处理半透明的背面部分。
- 开启透明度测试,仅通过半透明的像素;
- 剔除正面部分;
- 关闭深度写入,并将深度测试通过条件为小于(Less);
- 第二个Pass处理半透明的正面部分。
- 开启透明度测试,仅通过半透明的像素;
- 剔除背面部分;
- 关闭深度写入,并将深度测试通过条件为小于(Less);
性能改善
- 广泛使用Early-Z,以避免计算消耗巨大的片元着色器;
- 通常一半的头发都藏在头的后面;
- 先将头部渲染出来
- 使用透明度测试时,无法使用Early-Z;
- 解决方案:使用一个简单的shader进行透明度测试,形成一个Z-Buffer(即为Z-PrePass)
- 在后续测试中使用深度测试,而不是透明度测试,以实现相同的效果
- Early-Z节省相当多的开销。
优化的方案
有四个Pass:
- 第一个Pass写入深度。
- 开启透明度测试,仅通过不透明的像素;
- 关闭背面剔除;
- 开启深度写入,并将深度测试通过条件为小于(Less);
- 不写入任何颜色;
- 使用简单的片元着色器,仅返回透明度;
- 第二个Pass处理不透明的部分。
- 关闭背面剔除;
- 关闭深度写入,并将深度测试通过条件为等于(Equal);
- 第三个Pass处理半透明的背面部分。
- 剔除正面部分;
- 关闭深度写入,并将深度测试通过条件为小于(Less);
- 第四个Pass处理半透明的正面部分。
- 剔除背面部分;
- 关闭深度写入,并将深度测试通过条件为小于(Less);