概述
最近在工作中需要实现一个功能,用到了模板测试。但奇怪的是,模板测试竟然不起作用!在解决问题的过程中,发现了一些有趣的知识点。通过本文,可以了解在unity中,深度缓冲和模板缓冲到底是怎么存储的。
测试环境的搭建
Unity版本:2021.3.16f1
URP版本:12.1.8
RenderDoc:1.29
需要注意的是,URP的版本迭代,代码改动较大,最好与上面的版本一致。否则,可能会因为版本不同,产生无谓的麻烦。
后面的实验需要使用到RenderDoc。关于怎么在Unity中使用RenderDoc,可以查看最后的参考文献部分。
-
由于后续需要修改URP的源码进行测试,所以需要移动URP源码的路径。新建URP项目,源码的路径是类似这种:xxx\Library\PackageCache(xxx是URP项目的文件夹名)。需要将以下两个URP源码文件夹移动到xxx\Packages文件夹下:
移动后,Packages文件夹类似这样:
-
实现一个基础的Shader,包含了深度测试和模板测试。代码很简单,就不赘述了。如下所示:
Shader "Test/Hello World" { Properties { _Color ("Main Color", Color) = (1,1,1,1) [Header(Stencil)] [Enum(UnityEngine.Rendering.CompareFunction)]_StencilComp ("Stencil Comparison", Float) = 8 [IntRange]_Stencil ("Stencil ID", Range(0,255)) = 0 [Enum(UnityEngine.Rendering.StencilOp)]_StencilPass ("Stencil Pass", Float) = 0 } SubShader { Tags { "Queue" = "Geometry" "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" } Pass { Tags { "LightMode" = "UniversalForward" } Cull Off ZTest LEqual ZWrite On Stencil { Ref [_Stencil] Comp [_StencilComp] Pass [_StencilPass] } HLSLPROGRAM #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" #pragma vertex vert #pragma fragment frag struct Attributes { float4 positionOS: POSITION; }; struct Varyings { float4 vertex: SV_POSITION; }; half4 _Color; Varyings vert(Attributes input) { Varyings output = (Varyings)0; output.vertex = TransformObjectToHClip(input.positionOS.xyz); return output; } half4 frag(Varyings input): SV_Target { return _Color; } ENDHLSL } } }
-
需要设置一下测试的场景环境。使用上面的Shader新建两个材质球:Far和Near,如下设置:
Far材质球,设置为总是通过模板测试,替换模板值3,Render Queue设为2000。
Near材质球,设置模板缓冲值为3时才通过,保留模板缓冲值,Render Queue设为2010。
通过上面的设置,会先渲染Far材质球,写入模板缓冲3。然后再渲染Near材质球,只有模板缓冲中值为3的区域才会渲染。
使用Frame Debugger查看渲染流程,可以发现,确实是先渲染Far,再渲染Near。整体的渲染流程如下:
注意上图中红框中的部分,是颜色缓冲纹理的名称。在代码中使用全局搜索,可以找到如下部分:
通过观察分析,可以发现,深度缓冲和模板缓冲,主要是受到下面代码的影响:
colorDescriptor.depthBufferBits的代码注释如下:
这个值代表渲染纹理的深度缓冲精度比特值,支持0,16,24,32这四个值。
下面,分别把colorDescriptor.depthBufferBits设为上面的四个值,查看效果。
实验
实验一 设为0
colorDescriptor.depthBufferBits = (useDepthRenderBuffer) ? 0 : 0;
-
场景效果
-
Frame Debugger
-
RenderDoc
分析:从场景效果看,只渲染了天空盒,没有显示出Far或Near。但从Frame Debugger上看,流程并没有改变,还是先渲染Far,再渲染Near,接着再渲染天空盒。只是天空盒将Far和Near都覆盖了。从RenderDoc看,只有颜色纹理RT0。从这些内容分析以下,应该是因为没有了深度缓冲和模板缓冲,导致深度测试和模板测试不起作用了。
-
实验二 设为16
colorDescriptor.depthBufferBits = (useDepthRenderBuffer) ? 16 : 0;
-
场景效果
-
Frame Debugger 与上面相同,略
-
RenderDoc
分析:从场景效果看,显示出Far和Near,但是模板测试并没有起作用,因为完整的渲染出了Near。从Frame Debugger上看,流程并没有改变。从RenderDoc看,除了颜色纹理RT0,还多渲染了一张纹理DS(从名字看,应该是Depth Stencil)。在RT0中右键选中Far范围内的一点,再切换到DS,可以在RenderDoc的底部看到选中点的深度、模板信息。从上图可以看出,DS纹理的格式是R16,后面的值是选中点的深度缓冲值。这样,可以推测,有了深度缓冲,深度测试应该是起作用了,但是模板缓冲还是没有起作用,因为没有模板缓冲。
实验三 设为24
colorDescriptor.depthBufferBits = (useDepthRenderBuffer) ? 24 : 0;
-
场景效果
-
Frame Debugger 与上面相同,略
-
RenderDoc
分析:从场景效果和Frame Debugger上看,效果和流程与开始实验前完全一样。从RenderDoc看,与设为16时一样,都有RT0和DS两张纹理。但DS纹理的格式和内容是不同的,在上图底部可以发现,DS的格式是D32S8,后面还有深度缓冲值和模板缓冲值。与设为16时相比,DS纹理的格式不同,纹理的信息中,还多了模板缓冲值。这样,可以推测,深度缓冲和模板缓冲都有了,深度测试和模板测试也都起作用了。
实验四 设为32
colorDescriptor.depthBufferBits = (useDepthRenderBuffer) ? 32 : 0;
-
场景效果与上面相同,略
-
Frame Debugger 与上面相同,略
-
RenderDoc与上面相同,略
可以发现,设为32时,与设为24时的效果完全相同。这是为什么呢?
colorDescriptor.depthBufferBits的源码如下:
public int depthBufferBits
{
get => GraphicsFormatUtility.GetDepthBits(this.depthStencilFormat);
set => this.depthStencilFormat = RenderTexture.GetDepthStencilFormatLegacy(value, this.graphicsFormat);
}
设为24时,单步调试的结果如下:
设为32时,单步调试的结果如下:
从上面可以发现,设置depthBufferBits的int值,并不会向一般的属性那样直接存储int值。而是经过计算之后,存储到GraphicsFormat类型的变量中。而当设置的值是24和32时,保存的GraphicsFormat类型的变量都是D32_SFloat_S8_UInt。这也就解释了为什么设为24和32时,RenderDoc中完全一致的问题。
实验结论
上面的实验结果,可以用下面的图表简洁表达:
DS纹理 | 深度测试 | 模板测试 | |
---|---|---|---|
0 | ✖ | ✖ | ✖ |
16 | ✔ | ✔ | ✖ |
24 | ✔ | ✔ | ✔ |
32 | ✔ | ✔ | ✔ |
回到最初遇到的问题:模板测试不起作用。根据上面的表格,在项目中查了一下,是因为depthBufferBits设为了0,导致深度测试和模板测试都不起作用了。思路延伸一下:从性能优化的角度考虑,如果某种情况下不需要深度测试或模板测试,可以赋予depthBufferBits一个比较低的值,这样,DS纹理占用的内存会比较小,甚至不需要申请DS纹理的内存。