【躬行】-深度缓冲和模板缓冲是怎么存储的?

发布时间 2023-11-06 22:04:28作者: bzyzhang

概述

最近在工作中需要实现一个功能,用到了模板测试。但奇怪的是,模板测试竟然不起作用!在解决问题的过程中,发现了一些有趣的知识点。通过本文,可以了解在unity中,深度缓冲和模板缓冲到底是怎么存储的。

测试环境的搭建

Unity版本:2021.3.16f1

URP版本:12.1.8

RenderDoc:1.29

需要注意的是,URP的版本迭代,代码改动较大,最好与上面的版本一致。否则,可能会因为版本不同,产生无谓的麻烦。

后面的实验需要使用到RenderDoc。关于怎么在Unity中使用RenderDoc,可以查看最后的参考文献部分。

  1. 由于后续需要修改URP的源码进行测试,所以需要移动URP源码的路径。新建URP项目,源码的路径是类似这种:xxx\Library\PackageCache(xxx是URP项目的文件夹名)。需要将以下两个URP源码文件夹移动到xxx\Packages文件夹下:

    移动后,Packages文件夹类似这样:

  2. 实现一个基础的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
                
            }
        }
    }
    
    
  3. 需要设置一下测试的场景环境。使用上面的Shader新建两个材质球:Far和Near,如下设置:

    far材质球

    Far材质球,设置为总是通过模板测试,替换模板值3,Render Queue设为2000。

    near材质球

    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;
    
    1. 场景效果

    2. Frame Debugger

    3. RenderDoc

      分析:从场景效果看,只渲染了天空盒,没有显示出Far或Near。但从Frame Debugger上看,流程并没有改变,还是先渲染Far,再渲染Near,接着再渲染天空盒。只是天空盒将Far和Near都覆盖了。从RenderDoc看,只有颜色纹理RT0。从这些内容分析以下,应该是因为没有了深度缓冲和模板缓冲,导致深度测试和模板测试不起作用了。

实验二 设为16

colorDescriptor.depthBufferBits = (useDepthRenderBuffer) ? 16 : 0;
  1. 场景效果

  2. Frame Debugger 与上面相同,略

  3. RenderDoc

    分析:从场景效果看,显示出Far和Near,但是模板测试并没有起作用,因为完整的渲染出了Near。从Frame Debugger上看,流程并没有改变。从RenderDoc看,除了颜色纹理RT0,还多渲染了一张纹理DS(从名字看,应该是Depth Stencil)。在RT0中右键选中Far范围内的一点,再切换到DS,可以在RenderDoc的底部看到选中点的深度、模板信息。从上图可以看出,DS纹理的格式是R16,后面的值是选中点的深度缓冲值。这样,可以推测,有了深度缓冲,深度测试应该是起作用了,但是模板缓冲还是没有起作用,因为没有模板缓冲。

实验三 设为24

colorDescriptor.depthBufferBits = (useDepthRenderBuffer) ? 24 : 0;
  1. 场景效果

  2. Frame Debugger 与上面相同,略

  3. RenderDoc

    分析:从场景效果和Frame Debugger上看,效果和流程与开始实验前完全一样。从RenderDoc看,与设为16时一样,都有RT0和DS两张纹理。但DS纹理的格式和内容是不同的,在上图底部可以发现,DS的格式是D32S8,后面还有深度缓冲值和模板缓冲值。与设为16时相比,DS纹理的格式不同,纹理的信息中,还多了模板缓冲值。这样,可以推测,深度缓冲和模板缓冲都有了,深度测试和模板测试也都起作用了。

实验四 设为32

colorDescriptor.depthBufferBits = (useDepthRenderBuffer) ? 32 : 0;
  1. 场景效果与上面相同,略

  2. Frame Debugger 与上面相同,略

  3. RenderDoc与上面相同,略

可以发现,设为32时,与设为24时的效果完全相同。这是为什么呢?

colorDescriptor.depthBufferBits的源码如下:

    public int depthBufferBits
    {
      get => GraphicsFormatUtility.GetDepthBits(this.depthStencilFormat);
      set => this.depthStencilFormat = RenderTexture.GetDepthStencilFormatLegacy(value, this.graphicsFormat);
    }

设为24时,单步调试的结果如下:

9、bit为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纹理的内存。

参考