How to make stencil mask for a stencil mask

I have made a stencil shader mask and object. The stencil object behind the stencil mask can only be seen when looking through the stencil mask. (code for the stencil object and mask below)

Does anyone know how I can make a stencil mask for my current stencil mask?

Shader "Custom/Stencil/UI/Default-Mask"
{
    Properties
    {
        [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
        _Color ("Tint", Color) = (1,1,1,1)
    
        _StencilComp ("Stencil Comparison", Float) = 8
        _Stencil ("Stencil ID", Float) = 1
        _StencilOp ("Stencil Operation", Float) = 0
        _StencilWriteMask ("Stencil Write Mask", Float) = 255
        _StencilReadMask ("Stencil Read Mask", Float) = 255

        _ColorMask ("Color Mask", Float) = 15

        [Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0
    }

    SubShader
    {
        Tags
        {
            //"Queue"="Transparent"
            "Queue" = "Geometry-100"
            "IgnoreProjector"="True"
            "RenderType"="Transparent"
            "PreviewType"="Plane"
            "CanUseSpriteAtlas"="True"
        }
    
        Stencil
        {
            Ref [_Stencil]
            Comp [_StencilComp]
            Pass replace
            ReadMask [_StencilReadMask]
            WriteMask [_StencilWriteMask]
        }
Shader "Custom/Stencil/UI/Default-Object"
{
    Properties
    {
        [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
        _Color ("Tint", Color) = (1,1,1,1)
     
        _StencilComp ("Stencil Comparison", Float) = 8
        _Stencil ("Stencil ID", Float) = 1
        _StencilOp ("Stencil Operation", Float) = 0
        _StencilWriteMask ("Stencil Write Mask", Float) = 255
        _StencilReadMask ("Stencil Read Mask", Float) = 255

        _ColorMask ("Color Mask", Float) = 15

        [Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0
    }

    SubShader
    {
        Tags
        {
            "Queue"="Transparent"
            "IgnoreProjector"="True"
            "RenderType"="Transparent"
            "PreviewType"="Plane"
            "CanUseSpriteAtlas"="True"
        }
     
        Stencil
        {
            Ref [_Stencil]
            Comp equal
            Pass [_StencilOp]
            ReadMask [_StencilReadMask]
            WriteMask [_StencilWriteMask]
        }

Like I have said more often on the forum here. Don’t put your reference to 1 and your masks to 255. If you want to only use one bit, set your masks to 1 too.

It’s quite possible to mask the mask by using a second bit in the stencil buffer.

Step 1: (Write to the first bit)
Ref 1
Comp always
Pass replace
WriteMask 1

Step 2: (If the first bit is set, write to the second bit)
Ref 3
Comp equals
Pass replace
ReadMask 1
WriteMask 2

Step 3: (If the second bit is set, render)
Ref 2
Comp equals
ReadMask 2

Another way would be to just write to bits 1 and 2 in the first two steps and directly compare with both in the third step.

2 Likes

I am really new to shader, there for sorry for the stupid questions that now follows:

“How should I implement this shader? And does this work for masks in masks in masks?”

As far as I understand, I need do the following three steps:
Step 1 - Create a shader file for a stencil object
Step 2 - Implement my code for a stencil object, but change the (read/write) masks to 1?
Step 3 - Create a shader file for a stencil mask
Step 4 - Implement your code for a shader mask (that can also see read other shader masks) within the “Stencil{…}” of my code?

Or do I need to create a new shader file specificly for a shader mask mask?

Also, where would I use my “Queue” = “Geometry-100”?

Again, so sorry for my lack of knowledge. :frowning:

The way those shaders are set up, you don’t need any extra shader. It can be controlled from the settings in the material. My first 2 steps can be done with the first shader and the last step with the second shader.

The render order is important, but you can control that through the material too. (The queue in the shader is just the default value for the material.)

1 Like

Where can I define the if statement?

You said:

Step 2: (If the first bit is set, write to the second bit)
Step 3: (If the second bit is set, render)

Because I suppose this doesn’t work:

    SubShader
    {
        Tags
        {
            //"Queue"="Transparent"
            "Queue" = "Geometry-100"
            "IgnoreProjector"="True"
            "RenderType"="Transparent"
            "PreviewType"="Plane"
            "CanUseSpriteAtlas"="True"
        }
        Stencil
        {
            // old code:
            //Ref [_Stencil]
            //Comp [_StencilComp]
            //Pass replace
            //ReadMask [_StencilReadMask]
            //WriteMask [_StencilWriteMask]
         
           //Step 1: (Write to the first bit)
           Ref 1
           Comp always
           Pass replace
           WriteMask 1

           //Step 2: (If the first bit is set, write to the second bit)
           Ref 3
           Comp equals
           Pass replace
           ReadMask 1
           WriteMask 2

        }

Well, that’s almost it. But the two different steps should be two different shaders/passes. How would you mask your mask in the same step?

1 Like

I like your way of explaining, but I am not sure where I made the mistake in the code, which applies (as far as I understood) your feedback.

Step 1 and 2 are added as subshaders, to the “mask” shader.
Step 3 is for the “object/non-mask” shader.

But it seems something isn’t going as I planned, now all are invisible.
A short overview of the changes are demonstrated in the code below:

Stencil mask

Shader "Custom/Stencil/UI/Default-Mask"
{
    // Subshader to define the base shader for the object
    SubShader
        {
            Tags
            {
                "Queue" = "Geometry-100"
                "IgnoreProjector" = "True"
                "RenderType" = "Transparent"
                "PreviewType" = "Plane"
                "CanUseSpriteAtlas" = "True"
            }

            //Step 1: (Write to the first bit)
            Stencil
            {
                Ref 1
                Comp always
                Pass replace
                ReadMask 1
                WriteMask 1
            }

        } // End subshader stencil for masking mask

        // Subshader to define the base shader for the object
        SubShader
        {
            Tags
            {
                "Queue" = "Geometry-100"
                "IgnoreProjector" = "True"
                "RenderType" = "Transparent"
                "PreviewType" = "Plane"
                "CanUseSpriteAtlas" = "True"
            }

            //Step 2: (If the first bit is set, write to the second bit)
            Stencil
            {
                Ref 3
                Comp equal
                Pass replace
                ReadMask 1
                WriteMask 2
            }

    } // End subshader stencil for masking mask
}

Stencil object

Shader "Custom/Stencil/UI/Default-Object"
{
    SubShader
    {
        Tags {
            "Queue" = "Transparent"
            "IgnoreProjector" = "True"
            "RenderType" = "Transparent"
            "PreviewType" = "Plane"
            "CanUseSpriteAtlas" = "True"
        }

        //Step 3: (If the second bit is set, render)
        Stencil {

        Ref 2
        Comp equal
        ReadMask 2


        }
    } // End subshader
}

I really wan’t to make this work. :frowning:

It may be better of sending the whole code, so anyone can have a closer look.
There for, I have put a test project and have added it to this reply.
The current code works for Meshes and UI, it is a little more advance, but works as told above.

Also here is a image that helps explaining the situation:
3122078--236349--Stencil_Node_View_Simple_Example.png

3122078–236351–Unity_StencilTest_Project.zip (979 KB)

Don’t know if you’re still working on this, but you’re setting the wrong ref values. The ref value is the one used for the comparison, so now you’re setting it to 1, then checking if it’s equal to 3, then checking if it’s equal to 2. Of course they are all invisible, the comparison is always false. Try using Ref 1 for all of them, for example.

Also, it seems you can do two stencils in the same pass by doing two separate Stencil { }.

Big thanks to jvo3dc for the explanation, it was very helpful.

I don’t think that’s going to work. The hardware can only do one at a time, so with a double block it probably just does one.

I deleted that because I was not sure, but I am currently using this in a shader and it’s working:

            Stencil
        {
            Ref[_DrawInIndex]
            Comp Equal
            ReadMask 1
        }

            Stencil
        {
            Ref[_MaskIndex]
            Comp NotEqual
            Pass replace
            //WriteMask 255
        }

Basically I am using a masked object to mask another object, using two different ref values for two different writemasks.

Right, well, it’s probably working by accident. What are the _DrawInIndex and _MaskIndex set to?

Yes, that’s very much the case. I think it works because the bitwise operations just happen to work out (those indices were 100 and 101, respectively). However, that means it may interfere with other stencil masks, so even if it works I scrapped that approach, and ended up using a single pass with Pass IncrSat. That way I can feed the increased value to the second masked object.

Maybe other people needing to mask a mask can use the same approach.

Well, that’s kind of why I went for this:

Ref 3
Comp equals
Pass replace
ReadMask 1
WriteMask 2

So, if the first bit is set, then set the second bit. So this mask is writing in the second bit, but it itself is masked by the first bit.

Ref is 3 in this case, but could be anything with the last two bits sets. So 7, 15 or 255 would do exactly the same. It’s the masks that really control it here.

Hey, so I’m trying to do something similar with nested masks, but in my case I have multiple potential masks inside my outer mask that each could be showing a different underlying image/scene. Trying to understand what’s being suggested above and whether that could apply to this situation? Or perhaps this is the same exact situation and I’m not realizing it?

Any guidance would be appreciated!

Think I figured it out. Might be a better way to do this, but here’s my setup:

I use 3 shaders. One for the Outer Stencil, one for Inner Stencils (looks at first 4 bits), and one for Geometry:

Outer Stencil:

Stencil
{
Ref[_StencilMask]
Comp always
Pass replace
}

Inner Stencil:

Stencil
{
Ref[_StencilMask]
Comp equal
ReadMask 240
Pass replace
Fail keep
}

Geometry:

Stencil
{
Ref[_StencilMask]
Comp equal
}

So an example setup would be the outer stencil ref id = 16 / inner stencil and geo #1 ref id = 17 / inner stencil and geo #2 ref id = 18

Just for completeness: You don’t need to hardcode the stencil in the shader. You can pass them as properties as shown here: Stencil · supyrb/ConfigurableShaders Wiki · GitHub
This makes it way easier to iterate and experiment with stencil shaders :slight_smile:

Hey noethis, don’t know if you’re still interested, but I ended up figuring out a setup with the new Sprite Mask component. If you play with the custom range you can selectively mask only certain layers or sortingOrder intervals in the same layer. You could use an outer stencil that applies to everything, and inner stencils that only mask a given sortingOrder. I found it’s way easier to maintain than using multiple shaders and materials.

I’m interested! How do custom ranges change the stencil behavior? does it change the ref or what?

I don’t know how Unity handles it behind the scenes. You don’t need any custom shader code nor to modify the stencil buffer yourself, you just set it in the editor component and it works with the default sprite shader.

Oh, but I’m looking into using it with tilemap, which does not work with the editor component as it’s meant for the sprite renderer.
So I can’t use the editor component.