Stencil Shader with Depth

Hi, I’m building a unity level that takes advantage of Stencil Buffer shaders to mask objects. I have a shader that is puts a value in the stencil buffer, and another shader that only renders when that value is in the stencil buffer. This part is all working.

My problems arise when using multiple masks with differing values. If I have a mask that sets the value to 1 (Mask 1) and a mask that sets the value to 2 (Mask 2), and Mask 2 precedes Mask 1 in the Rendering Order (they use the same shader, so are in the same queue) then any areas of overlap on screen will set the Buffer to 2, even if 1 is in front. This is in part because the stencil buffer shader does not write to the z buffer (which it kinda can’t so the the correct geometry can render behind it later). I’m also trying to prevent geometry that only renders when the buffer is set to #value from rendering when the mask that sets the buffer to #value is behind the object. I have two rough ideas for solutions, but lack the knowledge of shader coding to implement them / know if their stupid ideas.

  • Change the render order of the masks so that the furthest mask from the camera renders first, and the render order moves forward. This would make the overwriting work in my favor. I believe at the moment the render order (within the same point in the queue) is defined by the order of mesh creation? This solution would break down if the masks ever intersection since it would be a per object or per fragment operation but this case won’t arise in my project.

  • Recycle the depth buffer. Have an early pass than runs on all my masks and all my geometry and writes to the depth buffer, then run all my Stencil buffer masks. Then (somehow?) clear the depth buffer, then render all my geometry. This would also mean objects in front of masks would prevent those areas from being effected by the mask. This sounds like it might be very inefficient, and I have no idea how to clear the depth buffer at a specific point in the queue (or at all).

Do either of these ideas sound viable? and can anyone help me out with understanding the pieces of shader code I’m missing? Or am I doing this all wrong and you’ve got a totally better way of doing it.

Objects in render queue 2500 or less render front to back (i.e.: closest to the camera first). Mesh creation order is (sort of) irrelevant, just where the center of the mesh is relative to the camera location. Batching can screw with this order as both dynamic and static batching works by merging multiple meshes with the same material, and it doesn’t always merge them in an order that’s consistent. Static batching generally merges in the order the meshes appear in the hierarchy, so mesh creation order can matter here, but dynamic batching doesn’t seem as predictable.

Now, for your first idea, this is easier than you might think, though not in the way you might be thinking about it. Just because a shader specifies a queue doesn’t mean that’s the one the material has to use. You can override the queue on each material using a custom render queue.

In Unity 5.4 and earlier this was accessible via script, and somewhat hidden in the editor via the debug inspector, but in 5.5 they actual expose this in the inspector as an option to choose the queue from the shader or use a custom one! So, if you want absolute control you can just programmatically set the queues to whatever you want at runtime. This will also prevent dynamic batching, which in this case is a good thing.

Alternatively you could modify the renderer components’ sorting order.

This is exposed for sprite renderers in the editor, but all renderer components have this property and you can use it to define the sorting order explicitly. I believe dynamic batching even respects this property, even with multiple materials, though I haven’t tested that theory extensively. Do know that if you set this value via script in the editor it doesn’t get saved on anything other than sprite renderer components. I don’t get the impression this is an issue for you, but it’s good to be aware of.

Lastly your idea of writing to the depth with your stencil shaders and then clearing the depth buffer isn’t as nutty as it sounds. You might be able to use GL.Clear() for that.

However to use that function you would have to manually render your stencil quads first with something like Graphics.DrawMesh() so you could do the clear function before rendering the rest of the scene, at which point you’d be manually controlling the draw order anyway. That clear might also clear the stencil though, which would make it especially useless for you.

Alternatively you could use a shader that writes out to depth to clear it as well. You just need to have a shader that use SV_Depth (search these forums) on a sphere or plane attached to the camera with a render queue after your stencils but before everything else. If you write out a depth of 1.0 and use ColorMask 0 it’ll render only to the depth and be essentially the same as a clear. Be mindful that in Unity 5.5 the depth buffer is sometimes inverted, so it might need to write 0.0 instead of 1.0. There’s an #ifdef you can use to check for that.

3 Likes

This reply was perfect. SV_Depth was exactly what I needed.

I’ve implemented the second option (with a depth clear in the middle) and it’s almost perfect. I’m using 4 different shaders.

WriteDepthOnly
This shader renders nothing (ColorMask 0) and writes to the Z buffer (ZWrite on) and is queued at “Geometry-100” so it render first. It is on all objects and masks.

WriteDepthOnly build a set of ZBufferValues to correctly build stencil buffer values with.

WriteDepthOnly.shader

Shader "Stencils/WriteDepthOnly"
{  
    SubShader
    {
        Tags
        {
            "RenderType" = "Opaque"
            "Queue" = "Geometry-100"
        }
        ColorMask 0
        ZWrite on
      
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            struct appdata
            {
                float4 vertex : POSITION;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
            };

            v2f vert(appdata v)
            {
                v2f o;
                o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
                return o;
            }

            half4 frag(v2f i) : COLOR
            {
                return half4(1,1,0,1);
            }
            ENDCG
        }
      
    }
}

StencilSetValue
This shader renders nothing (ColorMask 0) and does not write to the Z buffer (ZWrite off) and is queued at “Geometry-50” so it render second. If it passes the ZTest it sets the stencil buffer to the value of its property _StencilMask. It is on all masks.

using the Z buffer values build by WriteDepthOnly, if the ZTest passes, set your value in the Stencil Buffer so that the appropriate objects can be rendered behind the mask.

StencilSetValue.shader

Shader "Stencils/StencilSetValue"
{
    Properties
    {
        _StencilMask("Stencil Mask", Int) = 0
    }
    SubShader
    {
        Tags
        {
            "RenderType" = "Opaque"
            "Queue" = "Geometry-50"
        }
        ColorMask 0
        ZWrite Off
        Stencil
        {
            Ref[_StencilMask]
            Comp always
            Pass replace
            //ZFail replace
        }

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            struct appdata
            {
                float4 vertex : POSITION;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
            };

            v2f vert(appdata v)
            {
                v2f o;
                o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
                return o;
            }

            half4 frag(v2f i) : COLOR
            {
                return half4(1,1,0,1);
            }
            ENDCG
        }
    }
}

ClearDepthBuffer
This shader renders nothing (ColorMask 0) and writes to the Z buffer (ZWrite on) and is queued at “Geometry-25” so it render third. It is on one quad in front of the camera.

ClearDepthBuffer sets the depth buffer value to 0, leaving it empty before normal rendering begins. this prevents the masks depth buffer entries from preventing the objects behind them from being rendered.

ClearDepthBuffer.shader

Shader "Stencils/ClearDepthBuffer"
{
    //from http://wiki.popcornfx.com/index.php/PostFx_Workaround_(Unity_Plugin)
    Properties
    {
        _MainTex("Texture", 2D) = "white" {}
    }
    SubShader
    {
        // No culling or depth
        ZTest Always
        ZWrite On

        // Writes to a single-component texture (TextureFormat.Depth)
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                fixed2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D    _MainTex;

            v2f vert(appdata v)
            {
                v2f o;
                o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
                o.uv = v.uv;
                return o;
            }

            #if !defined(SHADER_API_D3D9) && !defined(SHADER_API_D3D11_9X)
            fixed frag(v2f i) : SV_Depth
            {
                fixed4 col = tex2D(_MainTex, i.uv);
                return 0;
            }
            #else
            void frag(v2f i, out float4 dummycol:COLOR, out float depth : DEPTH)
            {
                fixed4 col = tex2D(_MainTex, i.uv);
                dummycol = col;

                depth = 0;
            }
            #endif
            ENDCG
        }
    }
}

StencilRevealByReference
This is a surface shader that renders only if the value in the stencil buffer matches its _StencilMask property.
It is on all objects that should be masked.

ClearDepthBuffer.shader

Shader "Stencil/StencilRevealByReference"
{
    Properties{
        _StencilMask("Stencil Mask", Int) = 0

        _Color("Color", Color) = (1,1,1,1)
        _MainTex("Albedo (RGB)", 2D) = "white" {}
    _Glossiness("Smoothness", Range(0,1)) = 0.5
        _Metallic("Metallic", Range(0,1)) = 0.0
    }
        SubShader{
        Tags
        { 
            "RenderType" = "Opaque"
            "Queue" = "Geometry"
        }
        LOD 200
        Stencil{
        Ref[_StencilMask]
        Comp equal
        Pass keep
    }

        CGPROGRAM
        // Physically based Standard lighting model, and enable shadows on all light types
#pragma surface surf Standard fullforwardshadows

        // Use shader model 3.0 target, to get nicer looking lighting
#pragma target 3.0

        sampler2D _MainTex;

    struct Input {
        float2 uv_MainTex;
    };

    half _Glossiness;
    half _Metallic;
    fixed4 _Color;

    void surf(Input IN, inout SurfaceOutputStandard o) {
        // Albedo comes from a texture tinted by color
        fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
        o.Albedo = c.rgb;
        // Metallic and smoothness come from slider variables
        o.Metallic = _Metallic;
        o.Smoothness = _Glossiness;
        o.Alpha = c.a;
    }
    ENDCG
    }
        FallBack "Diffuse"
}

There are 2 main bugs I’m dealing with now.

  • There is Z fighting between the masks entries into the zBuffer and its ZTest. This is causing the patterns on these cubes. I feel like there might be some clever way to offset the ZBuffer entry of the masks. Or I can hack it together by putting the WriteDepthOnly shader on a different mesh with a small offset from the mask. The masks are all quads. Is there a difference between 1 mesh being rendered twice with 2 materials and 2 meshes being rendered once?2932514--216891--Capture.PNG
  • If an object that is meant to be masked, sits in front of the mask object, then it blocks the mask from setting the stencil buffer values around it, preventing it from being rendered, as desired. However, this also prevents any objects which are behind the forward object and the mask from being rendered, essentially turning any object in front of its mask into a sort of ‘null mask’. This problem seems systemic and would require a different technique entirely to fix. I can manage it in this project with careful level design.

Why do you have the ZWrite and StencilSetValue passes separate? Just remove the ZWrite Off line from the StencilSetValue and get rid of the WriteDepthOnly passes entirely and you should get the same results you want with out the z fighting.

Resolved it perfectly. I’m very new to shaders and was overthinking it. Thanks.

I’m new to using the stencil buffer, and I’m wondering whether there’s any way to get the value in a fragment shader and display a color based on it, or whether it can only be used as a mask for ignoring pixels entirely?

Nope.

Yep.

2 Likes

Would it be possible to write to the depth buffer so it can be read by another shader, without causing problems for other things? I’m trying to pass information that can be read and displayed by another shader. I’ve written shaders that read from the depth buffer but only to determine actual depth; so I’m not sure if that method would work to pass other information or whether it would cause problems.

Unity’s depth texture gets used for rendering the main directional shadows, and is rendered using the same shader pass as is used to render the shadow maps. It’s certainly possible to render special data to it, but not recommended.

For what you’re looking to do you’ll want to research replacement shaders and render textures.

I might be able to get it to work with the z-buffer except for some reason my attempts to write to the z-buffer aren’t working. I’m using a shader with a grabpass to create something that is nominally transparent but uses the Opaque rendertype (tags are: “Queue” = “Transparent” “RenderType”=“Opaque”). I’m trying to output a number to o.depth (defined as “float depth : DEPTH;” in the frag output struct), and I’m using ZWrite On; but nothing is being output to the depth buffer because the object doesn’t show up along with other objects when I use another shader to display the depth buffer (i.e. another shader on an object in front of the other objects so that other objects show up with various colors depending on their depth, but the object with the grabpass shader doesn’t show up at all). I don’t know what I’m doing wrong.

The _CameraDepthTexture is not the current z-buffer. It’s the z-buffer from a previous render pass of the entire scene using the shadowcaster pass.

Short version of the forward rendering path:

  • Render the camera view using each shaders’ shadowcaster pass for any material with a queue of 2499 or less. The resulting z-buffer / depth buffer gets saved to a texture, this is the _CameraDepthTexture.
  • Render the shadow maps for all lights, which are also depth buffers rendered using the shadowcaster passes and saved to textures.
  • Render the screen space shadows by projecting the main directional light’s shadow map onto the _CameraDepthTexture, save the results to a texture.
  • Render the scene again using the always, forwardbase, and forwardadd shader passes. The z-buffer during this part and the _CameraDepthTexture may have no correlation to eachother, like transparent queues can Zwrite, but they will not appear in the _CameraDepthTexture, or you can have problems like using shaders with alphatest but will be fully solid in the _CameraDepthTexture if it’s missing a matching shadowcaster pass.

You can modify the shadow caster pass, and put the material / shader in the opaque queue, but like I said it’ll cause issues with the directional shadows. Anything you do to the z buffer during the “main” shader pass(es) cannot be accessed, apart from using it to reject pixels, just like the stencil.

For the grab pass there’s some fun problems with render pass order that may be biting you, but with out seeing the shader I couldn’t say for sure. Additionally the grab pass just grabs the color, not the depth.

1 Like

Thank you for your thorough explanation. Is it possible to use the stencil buffer to send information based on a rim calculation, like in a rim shader that determines the edge of an object ? I’ve been using the stencil buffer but would like to indicate the edges of the object so the shader that renders the stencil information can smoothly blend it with the background rather than just having a hard-edged stencil effect.

If you want any kind of smooth transition, then stencils are not what you want. They’re for defining hard edged regions on the screen.

What you’re describing sounds like you should be looking into replacement shaders and render textures, or potentially destination alpha.

1 Like