前言

已经不止一次看到有文章说:不管有没有Early-Z最后的Late-Z一定会执行了,仔细想一下其实是不合理的:

  • Early-Z相当于把Late-Z提前,一样会有逐片元的深度测试和深度写入,如果Early-Z和Late-Z是共存的,那么就有两次Z-Buffer的读取和写入,造成带宽浪费
  • Early-Z因为种种原因失效了,执行Late-Z无可非议,但如果Early-Z没有失效,我们都在Early-Z处理好了,为什么还需要在Late-Z处理一次?

这篇文章就把深度缓冲区的所有操作都整理起来,并且还会包含一些引申出来的知识点,给每位看官进行一条龙服务。

正文

名词规范

国际惯例了属于是,为了避免歧义,本文中所有用到的名词,英语词汇都将在此处列出,希望看官们能把此处列出的名词和释义代入文章中,而不是自己脑海里的,这样你好我也好:

  • PS:片元着色器
  • Z-Buffer:深度缓冲区
  • Z-Test:深度测试
  • Z-Write:深度写入
  • Early-Z:提前Z-Test和Z-Write,位于光栅化阶段之后,PS阶段之前,以pixel quad为单位(既以4个像素为一组,因为深度缓存内的数据是按Z字形排列的)逐个像素进行比较
  • Late-Z:传统的Z-Test和Z-Write,位于PS阶段之后
  • Z-Culling:比Early-Z更粗粒度的深度剔除,以Tile(比如16*16像素)为单位进行整体比较,但是它不会对Z-Buffer直接进行读取和写入,而是只读取存储在On-Chip深度缓冲区(TBR和TBDR架构特有)中的数据,特点是容量小但是速度快
  • Hi-Z:一种软件技术,全名Hierarchical-Z,Hi-Z原理很简单,就是根据物体的包围盒所在的屏幕坐标与深度图比较深度 如果被挡住就不提交数据给gpu渲染,一般是采用和上一帧深度图对比 避免重新绘制一遍场景深度。逻辑上是 包围盒覆盖的像素点 挨个都挡住物体,就裁剪物体。但这样需要对比很多个像素性能很差,所以提出了Hi-Z概念。就是把深度图创建出多个mipmaps,mip0 就是 原始深度图信息,mip1 就是1/4 mip0面积大小,4个mip0像素 取最远离相机的那个值写入一个mip1像素,mip2同理不断创建更低精度的图。这样一个物体包围盒 如果是在mip0图上 占据16x16像素。就不用对比256次了,只需要 找到mip4 上一个像素就可以了,因为这一个像素记录的是这16x16像素最远离相机的深度 如果它都挡住了物体那么 那么其他的像素更靠近相机 肯定就能确定整个物体都被挡住了,可以进行剔除了
  • Z-PrePass:一种软件技术,顾名思义,即一个渲染Pass,以渲染一个场景为例,会有两个Pass,第一个Pass是Z-PrePass,只计算场景深度值得到Z-Buffer,第二个Pass关闭深度写入,将深度比较函数设置为相等,同Z-PrePass得出的Z-Buffer进行对比,只有相等的才会被绘制。由此可见Z-PrePass必须配合Early-Z,因为如果不用Early-Z,这一系列对比操作会在Late-Z进行,并那这个优化就并没有什么卵用了
  • IMR,TBR,TBDR对应三种GPU渲染架构,TBR/TBDR相对于IMR,利用OnChipBuffer高速缓存进行带宽优化,但相比于IMR,其引入了多处latecy(例如顶点转tile,内存->onChip的数据传递,HSR/FPK/LRZ等),降低了整体管线的性能峰值
  • HSR/FPK/LRZ:分别对应PowerVR,Arm Mali,Adreno的硬件级OverDraw优化,详细参见:老生新谈 TBDR

Early-Z

Early-Z虽好,但有使用限制:要求PS中不能对深度进行修改

That way, we notice all the rejected pixels early, without wasting a lot of computation on them. However, we can’t always do this: the pixel shader may ignore the interpolated depth value, and instead provide its own depth to be written to the Z-buffer (e.g. depth sprites); or it might use discard, alpha test, or alpha-to-coverage, all of which “kill” pixels/samples during pixel shader execution and mean that we can’t update the Z-buffer or stencil buffer early because we might be updating depth values for samples that later get discarded in the shader!

举个最常见的例子,如果我们对开启Alpha Test的Shader进行Early-Z操作,假设有一个片元A,其深度测试是通过的,并且将会在PS中会因为Alpha Test不通过而被丢弃,如果Early-Z正常运作(执行深度测试和深度写入),那么就会造成片元A的深度已经覆写进Z Buffer里,但是其片元是被丢弃的,也就造成了透明物体遮挡住了不透明物体的渲染错误现象

image-20210923143418670

总结一下,以下情况会导致Early-Z失效:

  • 开启Alpha Test:由于Alpha Test需要在像素着色器后面的Alpha Test阶段比较,所以无法在像素着色器之前就决定该像素是否被剔除。
  • 开启Alpha Blend:启用了Alpha混合的像素很多需要与frame buffer做混合,无法执行深度测试,也就无法利用Early-Z技术。
  • 开启Tex Kill:即在shader代码中有像素摒弃指令(DX的discard,OpenGL的clip)。
  • 关闭深度测试。Early-Z是建立在深度测试看开启的条件下,如果关闭了深度测试,也就无法启用Early-Z技术。
  • 开启Multi-Sampling:多采样会影响周边像素,而Early-Z阶段无法得知周边像素是否被裁剪,故无法提前剔除。
  • 以及其它任何导致需要混合后面颜色的操作。

随着硬件的演化,Early-Z的失效也只影响当前Drawcall的优化,后续Drawcall在情况允许的情况下依旧可以享受Early-Z

那么回到我们一开始的问题:不管有没有Early-Z最后的Late-Z一定会执行 ?

先借用 Dx11 performancereloaded 两张图来说明

image-20210907005949956

我们可以看到第一张流程图是完全舍弃了Late-Z阶段的,但是需要注意的是它的Depth Write是关闭的,如果Depth Write是开启的,情况就会有所不同了

Early-Z技术会导致一个问题:深度数据冲突(depth data hazard)。

img

例子要结合上图,假设数值深度值5已经经过Early-Z即将写入Frame Buffer,而深度值10刚好处于Early-Z阶段,读取并对比当前缓存的深度值15,结果就是10通过了Early-Z测试,会覆盖掉比自己小的深度值5,最终frame buffer的深度值是错误的结果。

避免深度数据冲突的方法之一是在写入深度值之前,再次与frame buffer的值进行对比:

img

总结一下,Early-Z执行的情况下,如果不开启Depth Write,Late-Z将不会执行,否则将会执行Late-Z来避免渲染错误

引用

图形学|shader|用一篇文章理解半透明渲染、透明度测试和混合、提前深度测试并彻底理清渲染顺序。

渲染杂谈:early-z、z-culling、hi-z、z-perpass到底是什么?

IMR, TBR, TBDR 还有GPU架构方面的一些理解

移动设备GPU架构知识汇总

Compute Shader 进阶应用:结合Hi-Z 剔除海量草渲染

Z/Stencil processing, 3 different ways.

Dx11 performancereloaded

深入GPU硬件架构及运行机制

老生新谈 TBDR