【笔记】【百人计划】图形3.1 深度与模板测试

图形3.1 深度与模板测试

一、模板测试Stencil Test

image-20220816182543699

1.1 渲染管线中的逐片元操作

  • 逐片元操作(可配置,不可编程)
    • 像素权限测试Pixel Ownership Test(屏幕窗口使用权限,如scene和game窗口区分)
    • 裁剪测试Scissor Test(可以对渲染部分区域进行控制)
    • 透明度测试Alpha Test(片元透明度大于阈值,则通过测试,否则剔除——只能实现不透明和全透明)
    • 模板测试Stencil Test
    • 深度测试Depth Test
    • 混合Blending
    • Dithering
    • Logic Op

1.2 模板测试理解

  • 通过一定条件来判断是对该片元抛弃还是保留
  • 模板缓冲区
    • 和颜色缓冲区、深度缓冲区类似
    • 为屏幕上每个像素点保存一个uint8
    • 可以用这个值与预先设定的参考值比较,根据结果来决定是否更新对应像素的颜色。
    • 这个比较的过程被称为模板测试
  • 发生在透明度测试后,深度测试前
  • 通过测试,则更新像素点,否则不更新语法表示
1
2
3
4
5
6
7
8
9
stencil{
Ref referenceValue
ReadMask readMask
WriteMask writeMask
Comp comparisonFunction
Pass stencilOperation
Fail stencilOperation
ZFail stencilOperation//模板测试通过了,但是深度测试没通过
}
1
2
3
4
if(referenceValue&readMask comparisonFunction stencilBufferValue&readMask)
pass
else
discard
1.2.1 比较函数Comparison Function
Greater GEqual Less LEqual Equal NotEqual Always Never
$>$ $\geq$ $<$ $\leq$ $=$ $\neq$ 总是通过 总是失败
1.2.2 模板(更新)操作stencilOperation
属性 含义
Keep 保留当前缓冲内容,即stencilBufferValue不变
Zero 将0写入缓冲,即stencilBufferValue变为0
Replace 将参考值写入缓冲,即将stencilBufferValue赋值为referenceValue
IncrSat stencilBufferValue加1,如果超过255,则保留为255
DecrSat stencilBufferValue减1,如果小于0,则保留为0
Invert 将stencilBufferValue按位取反
IncrWarp stencilBufferValue加1,如果超过255,则变成0(然后继续自增)
DecrWarp stencilBufferValue减1,如果小于0,则变成255(然后继续自减)

1.3 总结

  • 使用模板缓冲区最重要的两个值:当前模板缓冲值stencilBufferValue和模板参考值referenceValue

  • 模板测试主要就是对这两个值使用特定的比较操作

  • 模板测试之后要对模板缓冲区的值进行更新操作,Keep,Zero,Replace等

  • 模板测试之后可以根据结果对模板缓冲区做不同的更新操作,比如测试成功操作pass,测试失败操作fail,深度测试失败Zfail,还有正对正面和背面精确更新操作PassBack,PassFront,FailBack等

  • 应用

    • 描边
      • image-20220816205437129
    • 多边形填充
      • image-20220816205429461
    • 反射区域控制
      • image-20220816205345560
    • Shadow Volume

二、深度测试Depth Test

  • 一些特性

image-20220816205656742

在这里面值得注意的是最后一张,greater的部分,绿色的部分消失了。这是因为蓝色外面的部分还没有渲染,是无穷远,greater的比较无法通过,就不被渲染。像这种涉及无穷远比较的,比如天空盒,要多注意深度测试的比较方式。

2.1 渲染管线中的深度测试

Early-Z

现在大部分的GPU都提供一个叫做提前深度测试(Early Depth Testing)的硬件特性。提前深度测试允许深度测试在片段着色器之前运行。只要我们清楚一个片段永远不会是可见的(它在其他物体之后),我们就能提前丢弃这个片段。

片段着色器通常开销都是很大的,所以我们应该尽可能避免运行它们。当使用提前深度测试时,片段着色器的一个限制是你不能写入片段的深度值。如果一个片段着色器对它的深度值进行了写入,提前深度测试是不可能的。OpenGL不能提前知道深度值。

——LearnOpengl

  • (Early-Z)

  • 片元着色器

  • 逐片元操作(可配置,不可编程)

    • 像素权限测试Pixel Ownership Test(屏幕窗口使用权限,如scene和game窗口区分)
    • 裁剪测试Scissor Test(可以对渲染部分区域进行控制)
    • 透明度测试Alpha Test(片元透明度大于阈值,则通过测试,否则剔除——只能实现不透明和全透明)
    • 模板测试Stencil Test
    • 深度测试Depth Test
    • 混合Blending
    • Dithering
    • Logic Op

2.2 深度测试理解

深度测试就是当前对象在屏幕(Frame Buffer)对应的像素点,将对象自身的深度值与该当前该像素点缓存的深度值进行比较,如果通过,本对象在该像素点才会将颜色写入颜色缓冲,否则不会写入。

1
2
3
4
5
//深度缓冲区
if(ZWrite On && (currentDepthValue ComparisonFunction DepthBufferValue))
写入深度
else
忽略深度
1
2
3
4
5
//颜色缓冲区
if(currentDepthValue Comparison Function DepthBufferValue)
写入颜色缓冲
else
不写入颜色缓冲
  • 从发展角度理解
    • 控制渲染顺序
      • 画家算法
      • Z-Buffer算法
    • 控制Z-Buffer对深度的储存
      • Z-Test
      • Z-Write
    • 控制不同类型物体渲染顺序
      • 透明物体
      • 不透明物体
      • 渲染队列
    • 减少overdraw
      • Early-Z
      • Z-Cull
      • Z-check
2.2.1 深度缓冲区Z-Buffer

深度缓冲就像颜色缓冲,在每个片段中储存了信息,并且和颜色缓冲一样拥有宽度和高度,深度缓冲是由窗口系统自动创建的,他会以16、24、32位float的形式储存深度值。在大部分系统中深度缓冲精度都是24位。(Z-Buffer中储存的是当前深度信息,对于每个像素储存一个深度值)

通过Z-Write和Z-Test来调用Z-Buffer,实现想要的渲染结果

2.2.2 深度写入Z-Write

深度写入包括两种状态:Z-Write On 与Z-Write Off

当我们开启深度写入时,物体被渲染时,对应每个像素的深度都写入到深度缓冲区。反之,不会写入。但是物体写入深度,除了需要深度写入开启,还需要通过深度测试。

Z-Test分为通过和不通过两种情况,Z-Write分为开启和关闭两种情况,一共就是四种情况

深度测试 深度写入 深度缓冲区 颜色缓冲区
通过 开启 写入 写入
通过 关闭 不写入 写入
失败 开启 不写入 不写入
失败 关闭 不写入 不写入

这里顺便附上之前用Opengl做天空盒时对深度测试的理解。(现在看来glDepthMask就是开启深度写入,glDepthFunc就是比较函数,只有开启深度测试,深度写入才有用)可以根据上面的表格来更深入地理解下面的描述。

此外,还额外进行了天空盒的绘制,天空盒其实也就是先绘制一个立方体,然后将其z坐标取为w,使得在坐标归一化除以w时,z=1位于无限远的位置,这样能够使得天空的面永远在无限远处。

期间还需要对深度缓存进行一些操作。以下是我的理解。

//在不进行天空盒z=w操作的基础上,我们先渲染一遍天空盒,并且在此期间关闭深度遮罩不写入深度缓存,渲染完毕后开启深度遮罩,渲染其他物体,此时,其他物体覆盖在天空盒上,正常渲染

//(是否写入深度缓存结果上也等同于是否进行深度测试,即可以关闭深度遮罩,也可以关闭深度测试)

//如果不关闭深度遮罩,天空盒深度将写入深度缓存,如果物体在天空盒后面,则被遮挡。

//理解测试:那么在不进行z=w的时候,先绘制天空盒,不关闭深度遮罩,但是在绘制完天空盒之后,清除深度缓存,能正常绘制其他物体。测试正确。

//现在进行z=w的操作,并且先渲染天空盒,采用关闭深度测试的方式,即便z=1.0,也能正常渲染(未写入深度缓冲)。但是如果把天空盒放在最后渲染,天空盒将覆盖所有物体。

//如果是采用关闭深度遮罩的方式呢?只是不写入深度缓存,但实际上还是会进行深度测试,z=1.0无法渲染天空盒,因为原本就是1,但如果用LEQUAL,则可以渲染天空盒(默认为LESS)

//实际上,天空盒深度值已经是1.0的情况,开启深度遮罩没有任何意义(开启深度遮罩只是为了避免天空盒深度小于其他物体的情况,1.0已经最大),渲染天空盒和物体的顺序也不会有影响

//但是,后渲染天空盒,保留深度测试,可以利用深度测试提升性能

2.2.3 比较函数Comparison Function
Greater GEqual Less LEqual Equal NotEqual Always Never
$>$ $\geq$ $<$ $\leq$ $=$ $\neq$ 总是通过 总是失败
2.2.4 默认状态

Z-Write On,Z-Test Lequal,深度缓存一开始为无穷大。(这些和Opengl是不一样的)

2.3 渲染队列

2.3.1 Unity中的渲染队列

Unity中设置队列

image-20220816220946497

1
Tags {"Queue" = "Transparent"}//默认是Geometry
渲染队列
Background(1000) 最早被渲染的物体的队列
Geometry(2000) 不透明物体的渲染队列。大多数物体都应该是用该队列进行渲染(默认渲染队列)
AlphaTest(2450) 有透明通道,需要进行AlphaTest的物体的队列,比在Geometry中更有效
Transparent(3000) 半透明物体。一般是不写入深度的物体,Alpha Blend等的在该队列渲染
Overlay(4000) 最后被渲染的物体。一般是覆盖效果,如镜头光晕,屏幕贴片。
  • 渲染队列的物体排序:根据深度排序,深度小的在最前,深度大的在最后
  • 不透明物体的渲染顺序:从前往后
  • 透明物体的渲染顺序:从后往前(OverDraw)
2.3.2 Tips

多pass shader中,unity会选择所有pass里队列最靠前的作为物体的队列,然后根据pass的编写顺序逐pass执行

2.4 Early-Z

传统的渲染管线中,Z-Test是在Blending阶段,这时的深度测试,所有对象都计算过了片元着色器。大量的计算是无用的。

现代GPU运用了Early-Z技术,在Vs和Fs之间,进行一次深度测试。如果深度测试失败,就不必进行片元着色器的计算。但是最终的Z-Test仍需进行,保证最终遮挡结果正确。前面一次主要是Z-Cull为了裁剪达到优化目的。后一次是Z-Check,为了检查。

image-20220816221825445

2.5 深度值

image-20220816222256273

这个非线性深度也能够回答GAMES101中闫老师在投影矩阵那部分,讲frustum中间的点在投影矩阵之后是变远了还是变近了的问题。

要想有正确的投影性质,需要使用一个非线性的深度方程,它是与 1/z 成正比的。它做的就是在z值很小的时候提供非常高的精度,而在z值很远的时候提供更少的精度。花时间想想这个:我们真的需要对1000单位远的深度值和只有1单位远的充满细节的物体使用相同的精度吗?线性方程并不会考虑这一点。

由于非线性方程与 1/z 成正比,在1.0和2.0之间的z值将会变换至1.0到0.5之间的深度值,这就是一个float提供给我们的一半精度了,这在z值很小的情况下提供了非常大的精度。在50.0和100.0之间的z值将会只占2%的float精度,这正是我们所需要的。这样的一个考虑了远近距离的方程是这样的:
$$
F_{depth}=\frac{1/z-1/near}{1/far-1/near}
$$

img

2.5.1 深度冲突Z-fighting

一个很常见的视觉错误会在两个平面或者三角形非常紧密地平行排列在一起时会发生,深度缓冲没有足够的精度来决定两个形状哪个在前面。结果就是这两个形状不断地在切换前后顺序,这会导致很奇怪的花纹。这个现象叫做深度冲突(Z-fighting),因为它看起来像是这两个形状在争夺(Fight)谁该处于顶端。

所以实际上z-fighting的问题还是精度的问题。

一般解决方法就是稍微移动一点,不要重合。Learnopengl里还基于深度储存的精度剔除,可以把近平面设置远一点。或者是用更大的精度来储存。(实际上永远不会有足够的精度来保证完全重合)

2.6 总结

  • 是用深度缓冲区最重要的两个值:当前深度缓冲currentDepthValue和深度缓冲值zbufferValue,并通过比较操作获取理想渲染结果
  • Unity中的渲染顺序:先渲染不透明物体(从前往后),再渲染透明物体(从后往前)
  • 通过深度写入和深度测试组合控制半透明物体的渲染
  • 引入Early-z技术后的深度测试渲染流程
  • 深度缓冲区储存0-1的浮点值(非线性深度)
  • 应用
    • 基于深度的着色(湖水)
      • (摄像机到地面距离和湖水深度作比较,以差值比例控制着色)
    • ShadowMap
    • 透明物体、粒子渲染
    • 透视X-ray效果
    • 切边效果

作业

根据课程内容,使用深度测试与模板测试做一些有意思的效果

模板测试-描边

正好撞见Unity的描边,我们就来试试模板测试的描边

image-20220819163419228

我们来拆解一下这个效果

  • 2pass
    • 一个pass绘制物体
      • 写入一个模板值
    • 一个pass绘制物体法线外扩
      • 模板比较

现在问题就在于,如何进行法线外扩。

https://www.laowangomg.com/?p=712

这篇文章总结的非常详细。

其实最重要的问题是,我们在什么时候进行法线外扩?从模型空间到齐次裁剪空间过程中,在哪一步?

问题在于,我们应该让法线如何外扩?对于模型上的所有顶点,沿法线方向移动距离s,这是模型空间、世界空间、相机空间的做法。这样做的问题在于,我们可以把这个距离看作都是世界空间的距离,在经过投影变换后,不同位置的尺度会发生变化,也就是说,导致描边不均匀。

上面的博客给出的办法是——在齐次裁剪空间进行法线外扩,并且我们可以忽略z轴上的变化。只考虑屏幕的xy方向。

但是在我的实践中发现了一个问题,就是其实描边的宽度还是会出现不一致。并且这和渲染窗口分辨率有关。

image-20220819220901399

于是我们就知道是怎么回事了:

齐次裁剪空间时宽度一致的外扩尺度,经过视口变换后不再一致,但是我们肯定没有办法在视口变换的阶段处理顶点的位置,因此我们仍然需要进行改变。

我们要解决的核心问题是,让它在屏幕上能保持尺度的一致,其实,就是==观察空间==保持一致。

之前说在MVP空间的外扩都会导致投影后尺度变化。事实的确如此,在观察空间也会出现这个问题。

那么,在观察空间如何解决这一问题呢?我们可以借用裁剪空间的方法,只考虑屏幕上xy的变化,而忽略z的尺度变化。

1
2
o.pos.xy += normalize(vNormal.xy) * _OutLine;
//o.pos.xyz += normalize(vNormal.xyz) * _OutLine;

我们可以看出使用XYZ和xy的细微差别,这也很好理解。

1660921557672

这里我还做了一项处理就是让描边的粗细不受距离的影响,或者说,距离增加,描边的粗细也应该增加。不然就会像这样

image-20220819222542483

这应该如何处理呢?

image-20220819232155564

我们的h1/z1是由参数控制的,不妨令z1为1,则h1就等于

1
normalize(o.pos.xy)*_OutLine

那么要令距离z2的点在投影上和h1具有相同的高度,h2就很好求了。

这里还要注意,将z值取绝对值,我们只需要正值的计算。这样再调一调粗细的参数,效果和unity的描边效果就是基本一致的。

但是这种做法的局限性在于顶点法线,如果比较尖锐的顶点不是插值的法线的话,就会出现面片的分离。这就需要对模型法线进行处理,涉及到相应的工具制作。这也就是之后下一步工作的伏笔了。

1660923128047

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Pass{

Stencil{
Ref 0
Comp Equal
}
Cull front
CGPROGRAM

float _OutLine;
float _Attenuation;

v2f vert(a2v v) {
v2f o;
o.pos = mul(UNITY_MATRIX_MV,v.vertex);
float3 vNormal = mul((float3x3) UNITY_MATRIX_V, UnityObjectToWorldNormal(v.normal));
o.pos.xy += normalize(vNormal.xy) * _OutLine * abs(o.pos.z);
o.pos = mul(UNITY_MATRIX_P,o.pos);
return o;
};
fixed4 frag(v2f i) : SV_Target {
fixed4 color =fixed4(1.0,0.0,0.0,1.0);
return fixed4(color);
};
#pragma vertex vert
#pragma fragment frag

ENDCG
}

我们为了防止描边被遮挡,也可做如下处理

1
2
3
4
5
6
7
8
9
10
11
12
13
Stencil{
Ref 1
Comp Always
Pass replace
ZFail replace
}//物体pass

Stencil{
Ref 0
Comp Equal
}
Cull front
ZTest always//描边pass

深度测试-遮挡透视

首先当然是做一个遮挡扫描效果。

我们来拆解下这个效果的组成

  • 遮挡物
    • 正常的
  • 被遮挡物
    • 多pass
      • 以正常部分的队列为物体渲染队列从前往后
    • 正常部分
      • Geometry
      • 开启深度测试LESS,开启深度写入
    • 遮挡部分
      • Transparent
      • 开启深度测试Greater,不写入深度

我们首先来写一下遮挡部分

在透明的处理上,顺便看了下3.2部分的混合,用正常的透明混合方法Blend SrcAlpha OneMinusSrcAlpha

当然也可以选择其他的(感觉差不太多)

image-20220817153223447
大问题

出现了一个大问题。为遮挡物赋予一些材质时,会导致渲染顺序出错。

正常来说,遮挡物和被遮挡物都会处于Geometry的渲染队列,并且遮挡物先渲染,再渲透明Pass,最后是非透明Pass这是非常合理的.

image-20220817211331779

赋予某些材质后。

有些时候是正常的,有些时候,透明pass会在遮挡物之前渲染,导致透明物体被遮挡。

image-20220817211349707

image-20220817211648337

这还不是最麻烦的。在这个状态下,如果后退摄像机一点,这里两个透明物体中的其中一个被渲染出来了。

image-20220817211717544

如果再后退一点——透明物体都正常绘制了。

image-20220817211752817

**但是,这依然不是最麻烦的。**如果这个时候,再把摄像机后退一些会发生什么呢?

image-20220817211856345

这就是我所感觉到的绝望。

解决方法我想到了很多。

  1. 被遮挡物体物体两个Pass都放在了Geometry渲染。如果把两个Pass放在Geometry之后的其他Pass渲染,就没问题了。但要注意修改Tags的pass时,最好是subshader的pass,这样会比较稳定,如果只改两个Pass的tags,是不稳定的。
  2. 和上面的方法基本思想一样,做法刚好相反。就是一定要保证遮挡物在被遮挡物两个pass之前渲染,把遮挡物的队列设成Geometry-1(或者更前面)就好了。
  3. 换一个遮挡物的shader和材质。这个是比较麻烦的。因为,即便是一个原本能正常绘制的shader,在我完全复制所有代码以后,重新生成的材质,依然出现了上述问题。这就比较玄学了,这也是最头疼的部分。。。
  4. https://zhuanlan.zhihu.com/p/468122471用这篇文章的处理方法,把两个pass分开到两个shader,用UsePass的方法,这样各自的subshader可以设置各自的Tag。结果也是正常的。

但是,虽然这些方法能解决问题,我还是不知道为什么会出现这样的问题,以及,其实没有解决问题的根本——为什么在这些shader中渲染顺序会出错,不应该都是按深度排序的吗?

首先会考虑是否是z精度的问题?但是又怎么会随距离周期变化呢?

发现其实DepthPass顺序就错了,也就是说直接原因还是深度。但是为什么某些材质就没有这个问题了呢?并且SubShader和Pass的Tag也有影响。。。

image-20220817213753109

又找到了一个绝妙的解决方法

我直接重新做一个shader,然后用两次UsePass。。。当然,缺少原来CGINCLUDE的参数和属性,这一部分直接原样复制过去就行了。

果然能解决问题。

。。。但是令人崩溃的又来了。。。我继续把两个UsePass分别替换回原来的两个Pass的代码。。。竟然也是对的。。。问题是这样和原来的代码有啥区别。。。shader的名字的区别。。。为什么。。。已经麻了,就当是bug吧。

感觉跟unity的资源读取和shader编译的过程有一定关系。

https://www.cnblogs.com/zhlabcd/p/11767018.html这篇文章出现了一样的现象,但是看起来原因不太一样,不过可能有一定共性。

背后的真正原因,只能有缘再解决了

参考资料

[1] https://www.bilibili.com/video/BV1Tb4y1C7Qa

【技术美术百人计划】图形 3.1 深度与模板测试 传送门效果示例

[2] https://learnopengl-cn.github.io/04%20Advanced%20OpenGL/01%20Depth%20testing/

[3] https://zhuanlan.zhihu.com/p/427742656

[4] https://www.laowangomg.com/?p=712

[5] https://zhuanlan.zhihu.com/p/468122471