总结

  1. Unity 中的渲染顺序

    • 按 shader 指定的渲染队列顺序:Background—>Geometry (默认队列)—>AlphaTest—>Transparent—>Overlay
    • 不透明物体队列内的渲染顺序:从前往后
    • 透明物体队列内的渲染顺序:从后往前
  2. Unity 中的默认条件:

    • ZWrite:On
    • Z-Test:LessEqual
    • 渲染队列:Geometry(2000)
  3. 通过对 ZWrite 和 Z-Test 的相互组合配置来控制半透明物体的渲染(关闭深度写入,开启深度测试,透明度混合)

  4. 深度缓冲区中存储的深度值为 $[0,1]$ 的非线性值,所以在 Fragment Shader 中操作 Depth Buffer 的时候,比如制作雾效、深度可视化等,一定先将 z 变换到线性空间。[[03 深度法线纹理#非线性转线性]]

1 深度和 ZBuffer

概念

  • 首先先了解一下模型在渲染管线中的几次空间变换
    Pasted image 20221031205336
  • 模型一开始所在的模型空间:无深度。
  • 通过 M 矩阵变换到世界空间,此时模型坐标已经变换到了齐次坐标(x,y,z,w):深度存在 z 分量
  • 通过 V 矩阵变换到观察空间(摄像机空间):深度存在 z 分量(线性)
  • 通过 P 矩阵变换到裁剪空间:深度缓冲中此空间的 z/w 中(已经变成了非线性的深度)
  • 最后通过一些投影映射变换到屏幕空间

深度缓冲区存储非线性的深度

Pasted image 20230708104649

不同引擎和平台标准不同,这里想说的是深度值根据 near 和 far 决定!

深度缓冲区存储的深度值是一个 $[0,1]$ 范围的值,Z-Test 时需要将空间中的 Z 值变换到该范围。有两种变换方式:

  1. 线性变换:1000 单位远的深度值和只有 1 单位远的充满细节的物体使用相同的精度,造成性能浪费。
  2. 非线性变换:非线性深度方程是和 1/z 成正比的。这样一来会有如下效果:在 Z 很近的时候有高精度,Z 很远的时候低精度

所以深度缓冲区中的存储的值是非线性的 $[0,1]$ 范围的值!

所以在 Fragment Shader 中操作 Depth Buffer 的时候,比如制作雾效、深度可视化等,一定先将 z 变换到线性空间

![[LearnOpenGL#深度值精度]]

  • 平截头体:又称视景体、视锥,是三维世界中在屏幕上可见的区域,即虚拟摄像机的视野
  • 下图中红框的位置是平截头体,就是摄像机拍摄的范围。
    Pasted image 20221031211925

Z-Fight 深度冲突

由于采用了非线性变换,当物体距离摄像机较远的时候,Z 值相近的物体由于深度缓冲精度不足,GPU 无法判断物体的前后顺序,出现交替闪烁的线性
深度冲突不能够被完全避免,但一般会有一些技巧有助于在你的场景中减轻或者完全避免深度冲突、防止深度冲突

  1. 不要把多个物体摆得太靠近,略微在场景中移动物体坐标,错开那些靠的很近的物体,其实基本上都能解决问题。

  2. 尽可能将近平面设置远一些。在前面我们提到了精度在靠近平面时是非常高的,所以如果我们将平面远离观察者,我们将会对整个平截头体有着更大的精度。然而,将近平面设置太远将会导致近处的物体被裁剪掉,所以这通常需要实验和微调来决定最适合你的场景的平面距离。

  3. 牺牲一些性能,使用更高精度的深度缓冲。大部分深度缓冲的精度都是 24 位的,但现在大部分的显卡都支持 32 位的深度缓冲,这将会极大地提高精度。

  4. DirectX 11,DirectX12,PS4,Xbox One,和 Metal 中采用了更为先进的 Reveresed Direction Z(反向 Z) 方法,Untiy 的实现如下 UNITY_REVERSED_Z: ^9bb785

    1
    2
    3
    4
    5
    6
    7
    8
    #if UNITY_REVERSED_Z
    // 具有 REVERSED_Z 的平台(如 D3D)的情况。
    // UNITY_NEAR_CLIP_VALUE定义为近剪裁平面的值。 Direct3D为1.0,OpenGL为–1.0。
    positionCS.z = min(positionCS.z, UNITY_NEAR_CLIP_VALUE);
    #else
    // 没有 REVERSED_Z 的平台(如 OpenGL)的情况。
    positionCS.z = max(positionCS.z, UNITY_NEAR_CLIP_VALUE);
    #endif

    【Unity】对于 DirectX 11,DirectX12,PS4,Xbox One,和 Metal,现在使用的都是新的方法 Reversed direction。即 NDC 的取值范围是 $[1,0]$。对于其他的图形接口,保持传统的取值范围。即我们前文提到的,OpenGL 是 $[-1,1]$,老版本 DirectX 是 $[0,1]$ 。

Reversed direction 技术浮点深度缓冲区相结合,可显著提高相对于传统方向的深度缓冲区精度。这样做的优点是环节 Z-fighting 并改善阴影,特别是在使用小的近平面和大的远平面时。

为什么精度提高?

我们之前认为在距离 d 上数值的分布应该是等距的,但其实这不对,因为计算机存储浮点数是按照尾数幂指数的方式存储的,数值是离散的而且不等距:
v2-305978cf70035c41ad60e9383d957df8_1440w

左(不考虑浮点数存储格式),右(考虑浮点数存储格式)

注意看 y 轴上点的分布,这也导致了在实际情况下,精度的衰减其实更快,在稍微远离近平面不远的地方或许精度就不够用了。但是如果我们把 y 轴范围反一下,即 Reversed direction,就会变成这样:
v2-f16b18c05e59f7c93aed342ffa3de19e_1440w|500
精度衰减的没那么快了,而且基本上是相同精度。因此新的接口都使用 Reversed direction 的方法,不过可惜 OpenGL ES 的取值仍然是传统取值。

1 深度测试

Pasted image 20221031204347

从渲染管线理解

  • 59e936613d6e957a313031be895df47d_MD5 1
  • 深度测试同样位于像素处理阶段的合并阶段中,在模板测试之后透明度混合之前

从逻辑上理解

深度缓冲:
Pasted image 20221031203329
颜色缓冲:
Pasted image 20221031203332

  • **理解:和模板测试差不多,都是通过一个比较来判断一系列操作
  • 图 1:开启深度写入,当前深度值和深度缓冲区的值作比较,如果通过就写入深度,不通过就忽略深度
  • 图 2:当前深度值和深度缓冲区中的值做比较,如果通过就写入颜色缓冲区,不通过就不写入颜色缓冲区

从书面概念上理解

  • 深度测试的概念

  • 就是针对当前屏幕上(更准确的说是FrameBuffer(帧缓冲区))对应的像素点,将对象自身的深度值与当前深度缓冲区的深度值做比较,如果通过了,这个对象在该像素点才会将颜色写入颜色缓冲区。

从发展上看

[!NOTE]
OverDraw: 在一帧中重复渲染同一个像素,造成性能浪费

58a9ae0fb86496f5c18efc1ebd30bc99_MD5 1

  1. 控制渲染顺序

    • 画家算法:指油画的画法,也就是画一幅油画,是从远处开始画,然后近处的东西一点点叠加在上面(GAMES 系列的课提到过多次)
    • 存在的问题:例如一列物体,最前面的物体最大,站在正前面看只能看到最前面的物体,这样一来后边的就不用画了,不然就是 OverDraw。
    • Z-Buffer 算法:通过深度缓冲区来控制渲染顺序
  2. 控制Z-buffer 对深度的存储:使用 Z-Test 和 ZWrite 来进行控制

  3. 控制不同类型物体的渲染顺序

    • 透明物体
    • 不透明物体
    • 渲染队列(很有用的概念,后边会讲)
  4. **减少 OverDraw

    • Early-Z

2 基本原理和使用方法

##Z-buffer(深度缓冲区)
在实时渲染中,深度缓冲区(depth buffer,也称Z-buffer)是用于解决可见性问题的,它可以决定那个物体的哪些部分会被渲染在前面,哪些部分会被遮挡。
基本思想: 根据深度缓存中的值来判断该片元距离摄像机的距离,当渲染一个片元时,需要把它的深度值和已经存在于深度缓冲中的值进行比较 (只有开启了深度测试 Z-Test 才会进行比较

  • 如果它的值距离摄像机更远(判断值的大小时,要注意摄像机 forward 方向是 +Z 还是-Z ,比如 Unity 是+Z 方向,那么距离越远 Z 越大),那么说明这个片元不应该被渲染到屏幕上(有物体挡住了它);

  • 否则,这个片元应该覆盖掉此时颜色缓冲中的像素值,并把它的深度值更新到深度缓冲中(只有开启了深度写入 ZWrite,才会覆盖缓冲区中的值)。

  • Z-Buffer 中存储的是当前的深度信息,对于每个像素存储一个深度值,初始值为无穷大。

  • 和颜色缓冲区一样,在每个片段中存储了信息,并且通常和颜色缓冲有着一样的宽度和高度。深度缓冲是由窗口系统自动创建的,它会以 16、24、32 位 float 形式存储深度值。大部分系统中深度值是 24 位的,另外 8 位存模板缓冲。

[!NOTE] 颜色缓冲区
就是最终在显示屏硬件上显示颜色的 GPU 显存区域了,这个缓冲区储存了每帧更新后的最终颜色值,图形流水线经过一系列测试,包括片段丢弃、颜色混合等,最终生成的像素颜色值就储存在这里,然后提交给显示硬件显示。

Z Writer(深度写入)

深度写入包括两种状态:ZWrite On 、 ZWrite Off

  • 当我们开启深度写入,物体被渲染时针对物体在屏幕(FrameBuffer)上每个像素的深度都写入到深度缓冲区。
  • 关闭深度写入状态,物体的深度就不会写入深度缓冲区。
  • 如果 Z-Test 没通过,肯定不会写入深度。也就是说,只有 Z-Test 和 ZWrite 都可行的情况下才写入深度缓冲区

综上,ZWrite 有 On、Off 两种情况;Z-Test 有通过、不通过两种情况,两者结合的四种情况如下:
Pasted image 20221031204208

Z-Test 的比较操作

Pasted image 20221031204303

默认情况:

  • Z Write:On
  • Z Test:LEqual

Unity 的渲染队列

  1. Unity 内置的几种渲染队列:
    Pasted image 20221031204519
    按照渲染顺序从先到后排序,队列数越小,越先渲染;反之同理。

  2. UnityShader 中设置渲染队列:

  • 语法:Tags { “Queue” = “渲染队列名”}
  • 默认是 Geometry
  1. Unity 中不透明物体队列内的渲染顺序:从前往后,也就是说深度小的先渲染,其次再渲染深度大的

  2. Unity 中透明物体队列内的渲染顺序:从后往前(类似画家算法,会造成 OverDraw)

3 渲染顺序解析

Pasted image 20221031223142

  • 场景中有三个正方体,并赋予了不同的颜色。正常的情况应该是从前到后依次为蓝、绿、红

图 1 正常渲染顺序

  • Pasted image 20221031223159
  1. 没渲染时,此时 Unity 的深度缓冲区默认值为无穷大

  2. 渲染蓝色正方体,相对于默认深度缓冲区的无穷大,肯定是小于等于,所以测试通过
    Pasted image 20221031223209

  3. 渲染绿色正方体,此时蓝色物体位置的深度缓冲区的值已经不是无穷大了,其它位置还是

    • 绿色正方体进行深度测试,深度测试同样是 LessEqual,并且绿色的深度值比蓝色正方体的大。
    • 结果就是:两个正方体重叠部分是大于深度缓冲区的,也就是测试不通过,所以重叠部分没有写入绿色,还是蓝色的
    • 没有重叠部分,深度当然比无穷大小,所以写入,渲染出来了绿色正方体未重叠的部分。
  4. 红色同理。

图 2 关闭前排正方体的深度写入

b86429618c9afad29395410cb346a3f1_MD5 1

  1. 将蓝色正方体的深度写入 ZWrite 关闭;
  2. 第一个蓝色正方体的渲染时,测试通过,但是并没有写入深度。
  3. 也就是说,渲染完蓝色正方体时,深度缓冲区的值还是无穷大。
  4. 这就是蓝绿重叠部分,显示绿色的原因。

图 3

  • 相较于图 2,只是把绿色正方体的 Z-Test 改为了 always
  • 无论是 LessEqual 还是 always,测试都通过,所以效果和图2一样

图 4 改变 Z-Test 条件

Pasted image 20221031223413

  • 将红色正方体的 Z-Test 也改为了 always,这样一来红色正方体的深度测试也是一直通过,并且写入。
  • 因为是从前往后渲染的,所有依次为蓝、绿、红,深度缓冲区中的值也是后边渲染的
  • 可以理解为后边遮住前边的效果。

图 5 改变渲染队列

Pasted image 20221031223509

  • 相对于图 4,改变了绿色正方体的渲染队列为 Geometry+1,此时的帧缓冲区面板如下
    Pasted image 20221031223515
  • 尽管场景中绿色正方体在红色正方体前面,但是因为队列+1,它的渲染顺序变为了红色正方体后
  • 也就是说,渲染队列优先级 > 透明物体的渲染顺序(从前到后)

图 6 再次理解 Z-Test 条件

Pasted image 20221031223523

  • 相对于图 1,将绿色正方体的 Z-Test 改为了 Greater,
  • 也就是说蓝色正方体和绿色正方体重叠部分,大于模板缓冲区的部分通过测试,写入模板缓冲区
  • 结果就是重叠部分为绿色,而未重叠部分的深度当然小于无穷大,所以没通过测试,自然也就不渲染。
  • 红色部分正常。

shader 自定义面板

Pasted image 20221031223721

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Properties
{
_Color ("Color", Color) = (1,1,1,1)
[Enum(Off, 0, On, 1)]_ZWriteMode("Zwrite Mode",Float) = 1
[Enum(UnityEngine.Rendering.CompareFunction)] _ZComp("Z-Test Comp",Float) = 4 //第四个选项
}
SubShader
{
Tags { "RenderType"="Opaque" "Queue" = "Geometry"}
ZWrite [_ZWriteMode]
Z-Test [_ZComp]
Cull Off

Pass
{
......
}

4 粒子系统中的深度测试

  • 创建一个粒子系统 ParticleSystem,可以看到默认的是透明的

Pasted image 20221031232902

  • 为了加深理解,我们自己来复刻一下这个粒子系统的效果

  • 我们自己创建一个材质,给到粒子上

  • 此时粒子系统变成了这样

Pasted image 20221031232935

  • 创建一个 shader(Unlit),把粒子的贴图选上,附到材质上,效果如下(是不透明的),这显然不是我们要的效果

Pasted image 20221031232947

  • 打开 shader 修改代码

  • 首先回顾前边说的:Unity 中默认的 ZWrite On、Z-Test 是 LessEqual、渲染队列是 Geometry

  • 我们想要让粒子透明,就需要做如下配置

  • 渲染队列改为透明物体的渲染队列:Transparent

  • ZWrite Off,对于透明物体,是有相互叠加关系的,所以关掉写入

  • Z-Test 默认(LessEqual),对于透明物体是这样的:如果透明物体前有不透明物体,此时透明物体看不到;如果透明物体后面有不透明物体,此时透明物体可以看到。

  • 要渲染半透明物体,还要进行 Blend 操作:Blend One One(加法混合,叠加效果的显示)

  • 修改完成后效果如下:(正是我们想要的效果)
    Pasted image 20221031233100

七、扩展

Pasted image 20221031233631
参考资料
参考一下作者作业部分:3.1 模板测试和深度测试 (yuque.com)

其他:

5 优化技术

Early-Z 技术

直接修改传统渲染管线,在光栅化阶段(三角形遍历阶段)之后、像素处理阶段之前,加入一个 Early-Z 阶段。

这个阶段进行的操作和原本像素处理阶段的 Z-Test(为了与 Early-Z 区别,这个阶段也会被称为为 Late-Z)操作完全一样,现代的 gpu 已经都开始包含这样的硬件设计。

深度测试带来的问题

OverDraw!

Pasted image 20221208224714

  • 简单地概括就是
  • 传统的渲染管线中,Z-Test 发生在像素处理阶段的合并阶段,这时进行深度测试的话,所有对象的像素着色器都会计算一遍,有一些通过不了深度测试将会被直接抛弃(图中的红色片元),那么之前做得计算就都是无效计算了。
  • Z-Test 之前把该片元丢弃,就能节省大量计算,这就是 Early-Z 的由来。

Early-Z 步骤

Early-Z 技术分为两步:

  1. Z-Cull深度剔除,即提前深度测试
  2. Z-Check,即正常的深度测试(Late-Z),来保证正确的遮挡关系

Pasted image 20221208224906
Pasted image 20221208224921

如图:片元 1 写入深度后,在渲染片元 2、3 的时候,会进行深度剔除(z-cull),因为没有通过,所以这两个片元不会被计算

Early-Z 有两个主要的缺点:

  • 一旦进行了手动写入深度值、开启 alpha 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 可以完全避免 overdraw。但是相反的状态下,则会起不到任何效果。所以有些时候为了完全发挥 Early-Z 的功效,我们会在每帧绘制时对场景的物体按照到摄像机的距离由远及近进行排序。这个操作会在 cpu 端进行,当场景复杂到一定程度,频繁的排序将会占用 cpu 的大量计算资源

Early-Z 失效的情况

  1. 一旦进行了手动写入深度值、开启 Alpha Testclip/discard 等手动丢弃片元的操作,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 不仅会进行深度测试,还要进行深度写入
    • 例如:如果经过 AlphaTest,前面渲染的片元被丢弃了(但写入了深度),那么后续的像素都将无法正常渲染。
  2. 手动修改 GPU 插值得到的深度
    • 类似上述情况
  3. 开启 Alpha Blend
    • 开启了透明度混合不会开启深度写入,也就不符合 Early-Z 了
  4. 关闭深度测试
    • 都关了还测试啥

Early-Z 优化效果不稳定

Early-Z 的优化效果并不稳定,最理想条件下所有绘制顺序都是由近及远,那么 Early-Z 可以完全避免 overdraw。
但是相反的状态下,不透明物体由远及近渲染,则会起不到任何效果。

所以有些时候为了完全发挥 Early-Z 的功效,我们会在每帧绘制时对场景的物体按照到摄像机的距离由远及近进行排序。这个操作会在 cpu 端进行。

这样做会有两个问题:

  1. 复杂的场景,cpu 性能消耗很大
  2. 严格按照由近到远的顺序渲染,将不能同时搭配批处理优化手段。

有没有其他方法?搭配使用 [[06 深度测试#Z-Perpass]]

Z-Culling

Z-Culling 和 Early-Z 一样都是 gpu 硬件层面的优化,所以之前我一直混淆两者是同一种东西。

[!NOTE] Title
pixel quad: 以 $4$ 个像素为一组,因为深度缓存内的数据是按 Z 字形排列的
tile:(比如 $16*16$ 像素)

两者最明显的区别:

  • Early-Z 以 pixel quad 为单位逐个像素进行比较,
  • Z-Culling 以 tile 为单位进行整体比较。

Z-Culling 只应用于使用 TBR 架构的移动端 gpu 中。其主要方式取得当前 tile 所对应的的深度缓冲区中的 Zmax 和 Zmin,如果该 tile 当前深度的最小值<Zmax,则说明整个tile都不可见将整个tile全部丢弃。如果该tile当前深度的最大值>Znim,则说明整个 tile 都处于最前面,保留整个 tile,并因此可以省去该 tile 对应片元在 Late-Z 阶段对深度缓冲区的读取操作,直接写入就可以。对于其它情况,则交给后续的深度处理进行更细致的判断。

由于 Z-Culling 通常用于 TBR 架构 gpu,所以它也和 TPR 架构一样保持了对 gpu 带宽的敏感性。因此不同于 Early-Z,Z-Culling 并不会对深度缓存进行写入,也不会对深度缓存进行直接读取。它所需要的比对数据(Zmax 和 Zmin)都会储存在 on-chip 缓存中的某个固定区域,特点即是容量小但速度快。

由于 Z-Culling 对深度缓存是只读的,因此不会因为手动写入深度值、开启 alpha test 或者丢弃像素等操作对其有影响,这刚好解决了 Early-Z 的第一个缺点。

总结来说,Z-Culling 利用 TBR 架构进行了非常粗粒度的提前深度测试,但不会带来额外的对于深度缓存进行读写消耗,因此也比 z-early 具有刚多的适用范围。

这里有一个疑问,为什么 Early-Z 不像 Z-Culling 一样,对深度缓存只读,来避免收到手动写入深度值、开启 alpha test 或者丢弃像素等操作的影响?
其中一个解释是,在 Z-Culling 阶段后,那些没有被优化的片元在 Late-Z 阶段会读取深度缓存进行细粒度的测试,完成后再更新写入新的深度缓存。同时也会更新 Z-Culling 会访问的 on-chip 缓存。由于 Z-Culling 访问的是 on-chip 的所以不会带来额外开销,所以整体上只有对深度缓存进行一次读一次写。而对于 Early-Z 来说,如果在 Early-Z 阶段只读取深度缓存而不写入的话,那么在 Late-Z 阶段就需要重新读取然后写入,以更新深度缓存。这就相当于两次读一次写,带来了额外的开销。不过也看到有人说 Late-Z 阶段对深度缓存的读写是无论如何都会进行的,所以此处存疑。
还需要说明的是,Z-Culling 和 Early-Z 都可以不依赖于对方单独存在,当然两者也可以共存。当两者共存的时候,会先进行 Z-Culling 做粗粒度的筛选,再进行 Early-Z 做细粒度的排除。在有些资料中也会把 Z-Culling 成为 HiZ(没错,就是最后要讲的 Hi-Z),这要是不弄混就怪了。

Z-Perpass(Pre-Z)

[[2 URP#^4wccdt|URP的Pre-Z功能]]

和上面两种技术不同,Z-Perpass 是一种软件技术它主要是配合 Early-Z 使用,来减少开始提到的 early 的第二个缺点——效果不稳定
其做法是将场景做两个 pass 的绘制。第一个 pass 仅写入深度,不做任何复杂的片元计算,不输出任何颜色。第二个 pass关闭深度写入,并将深度比较函数设为“相等”。我在开篇有提到过度绘制的主要矛盾——经过大量运算的片元,很大概率会在之后被丢弃掉。那么对于第一个 pass 由于只写入深度,不在片元做任何计算,所以即便之后会被丢弃,也并不可惜。也就是说无论场景中的物体以怎样的顺序绘制,我们都可以以很小的代价提前绘制好当前场景的深度缓存。那么在第二个 pass 时,Early-Z 就可以用这个深度缓存中的值和当前深度值进行比较,只绘制深度相等的片元,任何其他的片元都可以直接丢弃,因此第二个 pass 要把深度比较函数设为“相等”。同时当前的深度缓存已经是完全正确的结果了,因此第二个 pass 也不需要对深度缓存做任何更新,便可以关闭深度写入。
Z-Perpass 必须配合 Early-Z 才能发挥效果,如果没有 Early-Z 的话,第二个 pass 的深度测试依旧在片元后,因此所有片元都会在片元阶段进行复杂计算。Z-Perpass 的思想和延迟渲染管线(defered render pipeline,下面也会提到)有些相似,差别在于:第一,Z-Perpass 的第一个 pass 只计算深度,并且结果直接存储在深度缓存。而延迟渲染会同时计算更多其他的屏幕空间数据,并将这些数据存储在额外的 framebuffer 中,需要更大的缓存(也就是 GBuffer)。第二,Z-Perpass 的第二个 pass 依旧需要对全场景的各个物体进行绘制(至少顶点阶段是如此),而延迟渲染的第二个 pass 类似于后处理本质上只绘制了一个屏幕大小的矩形。

1. 方式 1:双 pass

其实早就写过了,就是开启深度写入的透明度混合
[[08 透明#2. 开启深度写入的半透明效果]]

使用两个 pass

  • pass1:Z-prepass 中仅仅写入深度,不计算输出任何颜色。目的只是为了深度值写入缓冲区

  • pass2:关闭深度写入,将深度比较函数改为相等,进行正常的透明度混合(AlphaBlend)

  • 效果:

  • 每个物体都会渲染两个 pass,且所有物体的 z-prepass 的结果就自动形成了一个最小深度值的缓冲区 Z-buffer,无需 cpu 进行排序

  • 代码:

  • Pasted image 20221208225600

  • 问题 1:动态批处理

  • 多 pass shader 无法进行动态批处理 —> Draw Call 问题

  • 问题 2:Draw Call

  • 使用 z-prepass shader 的物体,draw call 会多一倍

  • Pasted image 20221208225622

2. 方式 2:提前分离的 Prepass

用于解决 DrawCall 问题

  • 内容

  • 仍然使用两个 pass

  • 将 pass1 的 z-prepass 单独分离出一个 shader,并用这个 shader 将场景的不透明物体先渲染一遍

  • 原来 shader 中的 pass,仍然关闭深度写入,深度比较函数仍然为相等,进行正常的透明度混合

补充

3. Pre-Z 也是透明渲染的一种解决方案

Pasted image 20221208225657

  • 这样会存在一个问题:无法看到透明物体的背面

  • 解决方法:透明物体的双面渲染

  • 核心思路:将渲染分为正面背面两部分

  • pass1:

  • 只渲染背面(cull front)

  • pass2

  • 只渲染正面(cull back)

  • 由于 Unity 会顺序执行 Subshader 中的各个 Pass,所以我们可以保证背面总是在正面被渲染之前渲染,来得到正确的深度渲染关系

四、Z-prepass 的其他问题

1. Z-prepass 的性能消耗是否能被忽视

  • 国外论坛一位名为 lipsryme 的老哥做了一项实验:
    Pasted image 20221208225723

  • 可以看到,Z-prepass 的消耗为 2.0ms,而带来的优化只减少了 0.3ms(2.7-2.4)

  • 后续讨论中,发现 Z-prepass 是需要根据项目的实际情况来决定是否采用的。

  • 总结有以下建议

  • 当一个有非常多 OverDraw 的场景,且不能很好的将不透明物体从前往后进行排序时,可以考虑使用 PreZ 进行优化

  • 注意,PreZ 会增加 DrawCall,如果用错了可能是负优化

五、Early-Z 和 Z-prepass 的实例应用

1. 面片叠加的头发渲染

  • 对于半透明的面片来说,需要从后往前进行排序渲染才能得到正确的透明度混合结果
  • Pasted image 20221208230553
    Pasted image 20221208230548

2. 排序后的头发渲染

Pasted image 20221208230615

  • 分为 3 个 pass

  • pass1

  • 处理不透明部分,开启 Alpha test 透明度测试,仅通过不透明的像素,

  • 关闭背面剔除

  • 开启深度写入

  • pass2

  • 剔除正面,渲染背面

  • pass3

  • 剔除背面,渲染正面

  • 问题:会带来非常多 OverDraw 的问题

3. 性能改善

Pasted image 20221208230629

  • 使用 Early-Z 剔除

  • 透明度测试开启时 Early-Z 无法使用的解决方案:    

  • 使用一个简单的 shader 进行透明度测试形成 Z-buffer,(就是我们上边说的提前分离的 z-prepass)

4. 改善的渲染方案

Pasted image 20221208230643

  • pass1:准备 Z-buffer

  • 开启透明度测试

  • 关闭背面剔除

  • 开启深度写入,深度测试设置为 less

  • 关闭颜色缓冲区写入

  • 用于一个简单的片元着色器来返回透明度值

  • pass2、pass3、pass4 参考之前排序后的头发渲染部分,同理

六、其他参考资料

Hi-Z

Hi-Z 全名 Hierarchical Z,和Z-Perpass 一样也是一种软件技术,据说这项技术最早是在《刺客信条:大革命》中使用的。
其核心原理是利用上一帧的深度图和摄像机矩阵,来对当前帧的场景做剔除,对于剔除后的物体进行绘制新的深度图和 GBuffer,然后再用新的深度图和当前摄像机矩阵再对当前帧的场景做剔除,对剔除后的物体进行绘制更新刚刚的深度图和 GBuffer。之所这种看起来十分复杂的方法能提高效率,是因为每一帧的绘制都已上一帧的绘制结果为基础。我们假设相邻两针差距不会特别大,那么以上一帧的深度图作为结果来对当前帧可见的物体进行筛选,可以得到绝大部分。而对于少量两针不一样的物体,进行第二次深度绘制,由于第二次绘制的少量不一样的物体所带来的的计算量很小,因此可以带来性能上的提升。这种基于前一帧的迭代式的对场景物体进行剔除,便可以在一定程度上减少过度绘制。不过由于我也没有实现过这种算法,所以对这种算法实际带来的效果存疑。

6 效果实现

Pasted image 20221031231326

实现思路

  • 分为三部分:前边的墙、被墙挡住的 X-Ray 效果部分、高出墙部分的物体
  • 回想一下前边 6 张图,哪张图是前边渲染完,后边渲染显示在先渲染完前边的? —>图 6
  • 也就是说,X-Ray 效果部分我们使用到了 Z-Test :Greater,深度写入关闭
  • 高出墙体部分是默认的渲染:LessEqual、ZWrite On
  • 两个 passd 对物体绘制两次

shader

Pasted image 20221031231520

Pasted image 20221031231522

Pasted image 20221031231524

Pasted image 20221031231526

Pasted image 20221031231528

Pasted image 20221031231531

  • 代码理解

  • 写 CGINCLUDE 的好处:将顶点和片元着色器写在里边,在多 passshade 的时候,直接调用就可以了。(跟 C++头文件类似)

  • X-Ray 绘制部分

  • 和之前实现思路相同,ZWrite Off,Z-Test Greater

  • Cull back 是剔除背面,为了优化

  • Blend     SrcAlpha One :由于有一个透明的效果,除了上边的,还需要一步 Blend,来做透明度混合

  • 渲染类型和渲染队列为 Transparent

  • 正常绘制部分略

用 RenderFeature 实现

[[3 Renderer Feature#透视效果 X-Ray]]