Stencil 介绍

Stencil 的概念和 Stencil Buffer 联系在一起。

我们知道,屏幕有深度缓冲,在默认情况下,当正在渲染的点的距离大于深度缓冲里的值时,就会直接丢弃此时Pass。

Stencil Buffer 也是一个 Buffer,不同于深度缓冲有默认的大小对比行为,Stencil Buffer 需要我们手动约定处理方式。

官方12有一些介绍,不过可能稍微有点抽象不好理解,这里我们从一个例子出发,来解释下可以做到什么。

Stencil Test

假如本来我们有一个Cube和一个正方形面片Quad,当QuadCube的前面的时候,Cube会被遮住一些部分。

origin_scene_cube_and_quad

现在我们将Quad替换为下面的 Shader,这个 Shader 做了下列几件事:

  • Blend Zero One 表示保持原有的颜色,这意味着渲染的物体将是透明的。
  • ZWrite Off 表示不改变深度缓冲
  • Stencil 内的内容就是模板测试,具体内容后面会再解释,这里只需要知道Comp Always表示永远通过,Pass Replace表示Pass后将 Stencil 缓冲区的值替换为Ref所表示的值。
Shader "Custom/Stencil/StencilFilter"
{
    Properties
    {
        [IntRange] _StencilID ("Stencil ID", Range(0, 255)) = 1
    }

    SubShader
    {
        Tags {
            "RenderType" = "Opaque"
            "RenderPipeline" = "UniversalPipeline"
            "Queue" = "Geometry"
        }
        
        Blend Zero One
        ZWrite Off
            
        Stencil
        {
          Ref [_StencilID]
          Comp Always
          Pass Replace
        }
        
        Pass
        {
        }
    }
}

现在Quad将是透明的,同时,它也改写了整个区域的Stencil Buffer的值。

origin_scene_cube_and_filter_quad

然后,我们将CubeShader也进行替换:

  • 我们只需要关注Stencil内的内容,这里Comp Equal的意思是,如果Ref的值等于Stencil Buffer就通过测试,继续后面的Pass,否则停止渲染。
Shader "Custom/Stencil/StencilTest"
{
    Properties
    {
        [IntRange] _StencilID ("Stencil ID", Range(0, 255)) = 0
        [MainColor] _BaseColor("Base Color", Color) = (1, 1, 1, 1)
        [MainTexture] _BaseMap("Base Map", 2D) = "white"
    }

    SubShader
    {
        Tags {
            "RenderType" = "Opaque"
            "RenderPipeline" = "UniversalPipeline"
            "Queue" = "Geometry"
        }
        
        Stencil
        {
            Ref [_StencilID]
            Comp Equal
        }

        Pass
        {
            HLSLPROGRAM

            #pragma vertex vert
            #pragma fragment frag

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            struct Attributes
            {
                float4 positionOS : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct Varyings
            {
                float4 positionHCS : SV_POSITION;
                float2 uv : TEXCOORD0;
            };

            TEXTURE2D(_BaseMap);
            SAMPLER(sampler_BaseMap);

            CBUFFER_START(UnityPerMaterial)
                half4 _BaseColor;
                float4 _BaseMap_ST;
            CBUFFER_END

            Varyings vert(Attributes IN)
            {
                Varyings OUT;
                OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);
                OUT.uv = TRANSFORM_TEX(IN.uv, _BaseMap);
                return OUT;
            }

            half4 frag(Varyings IN) : SV_Target
            {
                half4 color = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, IN.uv) * _BaseColor;
                return color;
            }
            ENDHLSL
        }
    }
}

QuadCube_StencilID的值都改为相同的值,比如1

然后拖动Quad,会看到非常有意思的一幕。只有被Quad遮挡的区域,才会渲染Cube

Stencil Test

如果我们有2个不同_StencilIDCube,和2个不同_StencilIDQuad,它们可以各自显示符合自己ID的Cube,但是不会显示另一个Cube

Stencil Test to Cube

这里稍微解释下发生了什么:

  • 由于整个屏幕每次渲染默认的Stencil Buffer的值是0,所以两个CubeQuad没有覆盖到的地方都不会显示。
  • Quad渲染后改变了当前像素的Stencil Buffer的值,而Cube设置了Comp Equal,所以可以显示在改变了Stencil Buffer的区域。

渲染顺序

在上面的探讨中,可以意识到一个问题:Quad只有在Cube之前渲染,才能够先改变Stencil Buffer,然后渲染Cube

如果它们的渲染顺序也就是Queue都是相等的,那么可能会发生闪烁。

StencilTestBlink

可以将QuadQueue设置为比Cube更小的值,此时就可以确保QuadCube之前进行渲染。

render queue

Cube不再会发生闪烁:

Stencil Test Stable

Stencil Test 语法介绍

可以参考官方文档2或者这篇中文文章3

核心语法如下:

Stencil
{
    Ref <ref>
    ReadMask <readMask>
    WriteMask <writeMask>
    Comp <comparisonOperation>
    Pass <passOperation>
    Fail <failOperation>
    ZFail <zFailOperation>
}

其中<ref>是参考值,它如何与Stencil Buffer对比取决于Comp

ReadMask 是对比的时候的遮罩,同理 WriteMask 是写入的时候的遮罩。

Comp定义RefStencil Buffer Value如何对比,默认是Always也就是总是PassEqual表示相等时Pass,还有Less/LEqual/Grater/NotEqual/GEual等等。

Pass定义的是Comp通过后对Stencil Buffer Value的操作,比如Keep表示保持Stencil Buffer的值,Replace表示用Ref的值替换掉Stencil Buffer的值,还有Zero/IncrSat/DecrSat/Invert/IncrWrap/DecrWrap

Fail定义的是Comp失败后的操作,内容和Pass一样。

ZFail定义的是Comp通过,但是深度缓冲测试未通过的情况,内容还是和Pass一样。

还可以单独定义正面和反面的Pass/Fail/ZFail,这里可以查看官方文档去了解。

一个最简单的修改Stencil Buffer的代码:

Stencil
{
     Ref 1
     Comp Always
     Pass Replace
}

一个最简单的测试Stencil Buffer的代码:

Stencil
{
    Ref 1
    Comp Equal
}

Renderer Objects feature

如果每增加一个Shader,我们就需要添加Stencil Buffer,然后设置_StencilID,那无疑会是一个非常麻烦的事情,这意味着我们需要修改很多材质。

你也可以参考 impossible-geom-stencils4 了解如何使用 Renderer Objects feature 来设置Stencil

如图,场景中有3个Cube和2个Quad,其中紫色的Cube是半透明材质。

origin scene 2

先将2个Quad替换为前面的StencilFilter

创建2个新的Layer,分别命名Stencil Object 1Stencil Object 2,给3个Cube分配上这2个Layer中的任意1个。

然后找到Universal Renderer Data设置,取消掉这两层的渲染。

Universal Renderer Data filter setup

添加一个Render Objects FeatureFilters中的Layer Mask选择Stencil Object 1/2,这里一共需要添加4个Render Objects Feature,对应2种Queue和2个Layer Mask

重点是在Override中,勾选Stencil,这样可以覆盖掉原有的Stencil设置,比如这里改为Equal同时设置Value为对应的值。

还有一点就是Event插入时机,如果是Opaque应该对应选择After Rendering OpaquesTransparent要对应选择After Rendering Transparents

render object feature

看一下效果:

scene 2

如果修改一下场景布局,取消相机渲染天空盒,给StencilFilter加上颜色,就可以有如下的Magic Cube

Magic Cube