前言

Unity关于模板缓冲区和模板测试与dx12类似,但也有不同,本文将基于dx12 来总结Unity中的模板缓冲区及模板测试的含义

模板缓冲区

什么是模板缓冲区?

  • 我们知道深度缓冲区存储每个像素的深度值z,而模板缓冲区是一个额外的buffer,它的分辨率大小和深度缓冲区相同,但模板缓冲区的一个像素点占8bit,且格式为UINT(格式有两种,都为UINT,但一种仅仅8位bit;另一种虽然占32位,但有用的只有前8位,剩下的24位仅用于填充),而这8bit的作用之一是控制颜色缓冲区和z缓冲区的渲染,比如在一个像素的模板缓冲区中存放1,表示该像素对应的空间点处于阴影体中

    注意:模板缓冲区需要搭配深度缓冲区一起工作,所以接下来会经常看到深度缓冲区的身影

为什么需要模板缓冲区?

  • 模板缓冲区主要用于实现特效、草这样的面片
  • 举个例子,若在模板缓冲区中绘制了一个空心矩形,模板缓冲中的值最开始时会被默认为0,之后在模板缓冲区中使用1填充了一个空心矩形,场景中的片段将会只在片段的模板值为1的时候会被渲染

如何使用模板缓冲区?

  • 在DX12中,需要填写模板缓冲区的描述符(D3D12_DEPTH_STENCIL_DESC)并将该描述符填入渲染管线对象(D3D12_GRAPHICS_PIPELINE_STATE_DESC)

  • 但在Unity中,为了提升开发者的开发效率,对模板缓冲区的使用方式进行了封装

  • 使用框架如下
    声明 Stencil,然后在关键字后填写需要的内容即可

    Stencil
    {
      Ref <ref>
      ReadMask <readMask>
      WriteMask <writeMask>
      Comp <comparisonOperation>
      Pass <passOperation>
      Fail <failOperation>
      ZFail <zFailOperation>
      CompBack <comparisonOperationBack>
      PassBack <passOperationBack>
      FailBack <failOperationBack>
      ZFailBack <zFailOperationBack>
      CompFront <comparisonOperationFront>
      PassFront <passOperationFront>
      FailFront <failOperationFront>
      ZFailFront <zFailOperationFront>
    }
    

    关于这些关键词的含义在下面的模板测试会讲到

模板测试

定义

允许开发者在渲染片元时将模板缓冲区中的值设定为一个特定的值,通过在渲染时修改模板缓冲的内容,写入模板缓冲。在接下来的渲染迭代中,我们可以读取这些值,来决定丢弃/保留某个片段

执行阶段

输出合并阶段。当片元着色器处理完一个片段之后,模板测试(Stencil Test)会开始执行

大致流程

  1. 启用模板缓冲的写入
  2. 渲染物体,更新模板缓冲区

  3. 禁用模板缓冲的写入

  4. 渲染(其它)物体,这次根据模板缓冲的内容丢弃/保留特定的片段

丢弃/保留特定的片段的计算原理

if( StencilRef & StencilReadMask \unlhd Value & StencilReadMask )
accept pixel
else
reject pixel

  • ​ StencilRef (对应Unity的Ref):模板参考值。UINT,范围[0, 255],默认0

  • ​ StencilReadMask(对应Unity的ReadMask):在模板测试时使用掩码值。UINT,范围[0, 255],默认255(0xff)

  • \unlhd(对应Unity的Comp):比较函数,用于比较左右两边的值,若为true,则通过模板测试;否则,失败

  • ​ Comp

    • 在DX12中,通过使用enum D3D12_COMPARISON_FUNC中的特定值来确定比较函数
    //以下比较运算符返回true则通过测试
    typedef enum D3D12_COMPARISON_FUNC {
      D3D12_COMPARISON_FUNC_NEVER = 1,  //只返回false.永远不通过深度测试
      D3D12_COMPARISON_FUNC_LESS = 2,   //"<".片段深度值小于缓冲区的深度值
      D3D12_COMPARISON_FUNC_EQUAL = 3,  //"==".片段深度值等于缓冲区的深度值
      D3D12_COMPARISON_FUNC_LESS_EQUAL = 4, //"≤".片段深度值小于等于缓冲区的深度值
      D3D12_COMPARISON_FUNC_GREATER = 5,    //">".片段深度值大于缓冲区的深度值
      D3D12_COMPARISON_FUNC_NOT_EQUAL = 6,  //"!=".片段深度值不等于缓冲区的深度值
      D3D12_COMPARISON_FUNC_GREATER_EQUAL = 7,  //"≥".片段深度值大于等于缓冲区的深度值
      D3D12_COMPARISON_FUNC_ALWAYS = 8  //只返回true.永远通过深度测试
    } ;
    
    • 在Unity中,对比较函数进行了简化更加直观
    enum CompareFunction
    {
        Never = 1,
        Less = 2,
        Equal = 3,
        LEqual = 4,
        Greater = 5,
        NotEqual = 6,
        GEqual = 7,
        Always = 8
    };
    

模板测试后的逻辑

不管该片元的模板测试是否通过,都需要由开发者来决定其去留

  • 在DX12中,填写描述符D3D12_DEPTH_STENCILOP_DESC来决定
    typedef struct D3D12_DEPTH_STENCILOP_DESC {
    D3D12_STENCIL_OP      StencilFailOp;    //描述当片元在模板测试失败时,应如何更新模板缓冲区
    D3D12_STENCIL_OP      StencilDepthFailOp;   //描述当片元通过模板测试,但在深度测试失败时,应如何更新模板缓冲区
    D3D12_STENCIL_OP      StencilPassOp;    //描述当片元通过模板测试、深度测试时,应如何更新模板缓冲区
    D3D12_COMPARISON_FUNC StencilFunc;  //模板测试中所用的比较函数
    } D3D12_DEPTH_STENCILOP_DESC;
    
    //指定在深度/模板测试期间被执行的模板运算符
    typedef enum D3D12_STENCIL_OP {
    D3D12_STENCIL_OP_KEEP = 1,  // 不修改模板缓冲区
    D3D12_STENCIL_OP_ZERO = 2,  // 将模板缓冲区中的元素置为0
    D3D12_STENCIL_OP_REPLACE = 3,   // 将模板缓冲区中的元素替换为用于模板测试的模板参考值(StencilRef).只有当深度/模板缓冲区状态块绑定至管线时,才能设定该值
    D3D12_STENCIL_OP_INCR_SAT = 4,// 对模板缓冲区中的元素进行递增.若超出范围会进行钳制
    D3D12_STENCIL_OP_DECR_SAT = 5,// 对模板缓冲区中的元素进行递减.若超出范围会进行钳制
    D3D12_STENCIL_OP_INVERT = 6,    // 对模板缓冲区中的元素按二进制位进行反转
    D3D12_STENCIL_OP_INCR = 7,  // 对模板缓冲区中的元素进行递增.若超出范围会回到最小值0
    D3D12_STENCIL_OP_DECR = 8       // 对模板缓冲区中的元素进行递增.若超出范围会回到最大值255
    } ;
    
  • 在Unity中,对其进行了简化
    enum StencilOp
    {
    Keep = 0,       // 不修改模板缓冲区
    Zero = 1,       // 将模板缓冲区中的元素置为0
    Replace = 2,    // 将模板缓冲区中的元素替换为模板参考值(Ref)
    IncrSat = 3,    // 对模板缓冲区中的元素进行递增.若超出范围会clamp
    DecrSat = 4,    // 对模板缓冲区中的元素进行递减.若超出范围会进行clamp
    Invert = 5,     // 对模板缓冲区中的元素按二进制位进行反转
    IncrWrap = 6,   // 对模板缓冲区中的元素进行递增.若超出范围会回到最小值0
    DecrWrap = 7    // 对模板缓冲区中的元素进行递增.若超出范围会回到最大值255
    }
    

定义深度/模板状态

最后,需要定义深度/模板缓冲区状态:包括是否启用深度/模板测试,比较函数,是否进行深度/模板写入,对模型的正面和背面做什么模板运算

  • 在DX12中,需要填写描述符D3D12_DEPTH_STENCIL_DESC
    typedef struct D3D12_DEPTH_STENCIL_DESC {
    BOOL                       DepthEnable; // 是否开启深度测试,TRUE则开启.默认TRUE
    D3D12_DEPTH_WRITE_MASK     DepthWriteMask;  // 是否禁止深度写入,若为D3D12_DEPTH_WRITE_MASK_ZERO则不可进行深度写入,若为D3D12_DEPTH_WRITE_MASK_ALL则可以.默认D3D12_DEPTH_WRITE_MASK_ALL
    D3D12_COMPARISON_FUNC      DepthFunc;       // 指定比较函数
    BOOL                       StencilEnable;   // 是否开启模板测试,若为true则开启.默认FALSE
    UINT8                      StencilReadMask; // 掩码值,用于模板测试
    UINT8                      StencilWriteMask;    // 掩码值,用于屏蔽对应位的写入操作
    D3D12_DEPTH_STENCILOP_DESC FrontFace;       //指示根据测试和深度测试的结果,对正面朝向的三角形进行指定的模板运算
    D3D12_DEPTH_STENCILOP_DESC BackFace;        //指示根据测试和深度测试的结果,对背面朝向的三角形进行指定的模板运算
    } D3D12_DEPTH_STENCIL_DESC;
    
    //是否禁止深度写入
    typedef enum D3D12_DEPTH_WRITE_MASK {
    D3D12_DEPTH_WRITE_MASK_ZERO = 0,
    D3D12_DEPTH_WRITE_MASK_ALL = 1
    } ;
    
    //用于模板测试的掩码值的默认值——不会屏蔽任何一位模板值
    #define D3D12_DEFAULT_STENCIL_READ_MASK (0xff)
    
    //写掩码值的默认值——不会屏蔽任何一位模板值
    #define D3D12_DEFAULT_STENCIL_WRITE_MASK (0xff)
    
  • 在Unity中,对于深度测试/写入和模板测试/写入的处理是分开的,因此这里只提及模板
    Stencil
    {
    Pass <passOperation>    // 通过模板、深度测试后,如何更新模板缓冲区(双面)
      Fail <failOperation>  // 未通过模板测试后,如何更新模板缓冲区(双面)
      ZFail <zFailOperation>    // 通过模板测试,但未通过深度测试,如何更新模板缓冲区(双面)
      CompBack <comparisonOperationBack>    // 为模型的背面定义的比较函数
      PassBack <passOperationBack>  // 通过模板、深度测试后,如何更新模板缓冲区(背面)
      FailBack <failOperationBack>  // 未通过模板测试后,如何更新模板缓冲区(背面)
      ZFailBack <zFailOperationBack>    // 通过模板测试,但未通过深度测试,如何更新模板缓冲区(背面)
      CompFront <comparisonOperationFront>  // 为模型的正面定义的比较函数
      PassFront <passOperationFront>    // 通过模板、深度测试后,如何更新模板缓冲区(正面)
      FailFront <failOperationFront>    // 未通过模板测试后,如何更新模板缓冲区(正面)
      ZFailFront <zFailOperationFront>  // 通过模板测试,但未通过深度测试,如何更新模板缓冲区(正面)
    }
    

总结

  • 不难看出,模板与深度相差无几,只是模板测试的运算逻辑更加复杂,但本质和深度缓冲区一样,都是进行测试留下开发者想要的部分

  • 最后,总结一下Unity中Stencil的使用方法和各参数的含义

    Stencil
    {
      Ref <ref>                         // 参考值,比较函数比较的对象.UINT,范围[0, 255],默认0
      ReadMask <readMask>                   // 在模板测试时,使用此值作为遮罩.UINT,范围[0, 255],默认255
      WriteMask <writeMask>             // 在写入模板缓冲区,使用此值作为遮罩.UINT,范围[0, 255],默认255
      Comp <comparisonOperation>            // 比较函数
      Pass <passOperation>              // 通过模板、深度测试后,如何更新模板缓冲区(双面)
      Fail <failOperation>              // 未通过模板测试后,如何更新模板缓冲区(双面)
      ZFail <zFailOperation>                // 通过模板测试,但未通过深度测试,如何更新模板缓冲区(双面)
      CompBack <comparisonOperationBack>    // 为模型的背面定义的比较函数
      PassBack <passOperationBack>      // 通过模板、深度测试后,如何更新模板缓冲区(背面)
      FailBack <failOperationBack>      // 未通过模板测试后,如何更新模板缓冲区(背面)
      ZFailBack <zFailOperationBack>        // 通过模板测试,但未通过深度测试,如何更新模板缓冲区(背面)
      CompFront <comparisonOperationFront>// 为模型的正面定义的比较函数
      PassFront <passOperationFront>        // 通过模板、深度测试后,如何更新模板缓冲区(正面)
      FailFront <failOperationFront>        // 未通过模板测试后,如何更新模板缓冲区(正面)
      ZFailFront <zFailOperationFront>  // 通过模板测试,但未通过深度测试,如何更新模板缓冲区(正面)
    }
    

reference

ShaderLab 命令:模板

Directx12 3D 游戏开发实战

https://github.com/QianMo/Game-Programmer-Study-Notes


他们曾如此骄傲的活过,贯彻始终