How is TilemapRenderer's "Order in Layer" & layer messing with Stencil Buffer?

So I’m done trying to figure this out since there seems to be zero information about this anywhere on the web.

Does anyone know why setting a TilemapRenderer’s sorting layer to anything past default (or having it at default with an ‘Order in Layer’ greater than 0) just completely disables writing to the stencil buffer?

(if it’s not disabling it, it’s doing something weird to max out the Stencil ref values past 255, it would seem (because checking if the ‘Ref’ value is ‘Greater’ than 255 will make the pixels show)).

Edit with solution in quote (see below for shaders used):

  • To get the effect working have some planes that cover all your tiles. (MAKE SURE THE PLANES’ LAYERS ARE SET TO ‘Ignore Raycast’)
  • Apply the “Sprite Stencil Write” shader to your tilemap
  • Set the stencil values on the tilemap material to | Stencil Ref = Anything above 0 (this should be unique per plane covering each tilemap) | Stencil Comparison = 8 (‘Always’ succeed) | Stencil Operation = 2 (‘Replace’ the value in the stencil buffer)
  • Apply the “Unlit Color Stencil Read” shader to your planes
  • Set the stencil values on your places to | Stencil Ref = The ‘Stencil Ref’ value on the material of the tilemap this plane is covering | Stencil Comparison = 3 (only draw the planes where the stencil buffer value is ‘Equal’ to the ‘Stencil Ref’ value on this material
  • Finally if you want the fade effect just modify the alpha value (by changing the color) in a coroutine
IEnumerator ToggleFadeOverlay_cr(bool show, MeshRenderer planeMeshRenderer, float duration)
    {
        Color c = planeMeshRenderer.material.GetColor("_Color");
        float time = show ? c.a : 1 - c.a;
        while (time < 1)
        {
            time = Mathf.Clamp01(time + Time.deltaTime / duration);

            planeMeshRenderer.material.SetColor("_Color", new Color(c.r, c.b, c.g, show ? Mathf.Lerp(0, 1, time) : Mathf.Lerp(1, 0, time)));

            yield return null;
        }

        //Set the coroutine member variable to null, here, if you're tracking whether the coroutine is running not
    }
  • If you’re having issue with rendering order, drop the MeshRendererEditorOverride script from the solution quote in a folder called ‘Editor’ in your ‘Assets’ folder and set the “Order in Layer” value to something higher than what it is

Original Post
Some more info:
My shaders:
Writing to the stencil buffer -

Shader "Custom/Stencil/Sprite Stencil Write"
{
    Properties
    {
        [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
        _Color ("Tint", Color) = (1,1,1,1)
        [MaterialToggle] PixelSnap ("Pixel snap", Float) = 0
        [HideInInspector] _RendererColor ("RendererColor", Color) = (1,1,1,1)
        [HideInInspector] _Flip ("Flip", Vector) = (1,1,1,1)
        [PerRendererData] _AlphaTex ("External Alpha", 2D) = "white" {}
        [PerRendererData] _EnableExternalAlpha ("Enable External Alpha", Float) = 0
        [IntSlider] _StencilRef ("Stencil Ref", Range(0,255)) = 0
        [IntSlider] _StencilComparison ("Stencil Comparison", Range(1, 8)) = 3 //1 Never, 2 Less, 3 Equal, 4 LEqual, 5 Greater, 6 NotEqual, 7 GEqual, 8 Always
        [IntSlider] _StencilOperation("Stencil Operation", Range(0,7)) = 0 //0 Keep, 1 Zero, 2 Replace, 3 IncrSar, 4 DecrSat, 5 Invert, 6 IncrWrap, 7 DecrWrap
    }

    SubShader
    {
        Tags
        {
            "Queue"="Transparent"
            "IgnoreProjector"="True"
            "RenderType"="Transparent"
            "PreviewType"="Plane"
            "CanUseSpriteAtlas"="True"
        }

        Cull Off
        Lighting Off
        ZWrite Off
        Blend One OneMinusSrcAlpha

        Pass
        {
            Stencil
            {
                Ref [_StencilRef]
                Comp [_StencilComparison]
                Pass [_StencilOperation]
            }

            CGPROGRAM
            #pragma vertex SpriteVert
            #pragma fragment frag
            #pragma target 2.0
            #pragma multi_compile_instancing
            #pragma multi_compile _ PIXELSNAP_ON
            #pragma multi_compile _ ETC1_EXTERNAL_ALPHA
            #include "UnitySprites.cginc"

            fixed4 frag(v2f IN) : SV_Target
            {
                fixed4 c = SampleSpriteTexture(IN.texcoord) * IN.color;
                if (c.a < 0.00001) //This is here to prevent transparent pixels from writing to the stencil buffer
                    discard;
                c.rgb *= c.a;
                return c;
            }
            ENDCG
        }
    }
}

Reading from the stencil buffer -

Shader "Custom/Stencil/Unlit Color Stencil Read"
{
    Properties
    {
        _Color("Color", Color) = (1,1,1,1)
        [IntSlider] _StencilRef ("Stencil Ref", Range(0,255)) = 0
        [IntSlider] _StencilComparison("Stencil Comparison", Range(1, 8)) = 3 //1 Never, 2 Less, 3 Equal, 4 LEqual, 5 Greater, 6 NotEqual, 7 GEqual, 8 Always
    }
    SubShader
    {
        Tags
        {
            "Queue" = "Transparent+10"
            "IgnoreProjector" = "True"
            "RenderType" = "Transparent"
            "PreviewType" = "Plane"
            "CanUseSpriteAtlas" = "True"
        }
        LOD 100

        Stencil
        {
            Ref[_StencilRef]
            Comp[_StencilComparison]
        }

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
            };

            struct v2f
            {
                UNITY_FOG_COORDS(1)
                float4 vertex : SV_POSITION;
            };

            fixed4 _Color;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = _Color;
                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }
            ENDCG
        }
    }
}
  • Have some tiles in a tilemap.

  • Place the first shader as the tilemap material (in the TilemapRenderer on your tilemap gameobject)

  • Put the second shader on the plane that’s between the camera and the tilemap (facing the camera) and make sure the plane has all the tiles behind it.

  • Create a sorting layer below (lower down in the sorting layer list) ‘Default’ and assign this to your TilemapRenderer (or just set the “Order in Layer” on the TilemapRenderer to 1).

  • Now you’ll no longer be able to get the color from the second shader that’s on the plane to show up in front of the tiles; no matter what you change in the Stencil Buffers.

Could you share more details of how and when the Plane with second shader is rendered? You would likely need to change the sorting details for that to match the TilemapRenderer.

The Frame Debugger would likely detect the issues regarding this, so sharing that would help too!

The plane is just using the second shader so queue=3010
7624861--948508--PlaneRenderDetails.png

But like I said in my original post, this works fine with all TilemapRenderers that are on a sorting layer below default or are on the default sorting layer but have their ‘order in layer’ set to 0.

Here is the frame debugger. I’ve selected the object/line that corresponds to the renderer details posted above
7624861--948511--FrameDebuger.png

Up until this very moment I had no idea Unity had this Frame Debugger feature XD Is it relatively new?

Anyways it seems the issue is that the selected object (Shop Fade Overlay) is getting rendered as “Event #6” while the tiles it’s supposed to be drawing in front of are the last “Draw Dynamic” (Event #13) in that list.

Why is it getting rendered before a material that has its shader queue at 3000 while it’s rendering at 3010?

Does the Frame Debugger show any differences when you change the Order in Layer value?

WOAH :hushed:

When I change the “Order in Layer” of the TilemapRenderer to 0 both the ‘Factory Fade Overlay’ and ‘Shop Fade Overlay’ jump to their correct rendering positions (those two objects are using the same shader so neither of them should be where they are in that image (rendering before all the tiles at shader-queue 3000)
7624876--948514--FrameDebuger2.png

…though it seems now instead of rendering the tiles in large groups it’s now choosing to render them 1-4 tiles at a time (though this could just be frame dependent)

…and now the tilemap is also set to an “order in layer” that I don’t want it on. Something of note, those two nested ‘Draw Dynamic’ calls are it rendering the entire 2 floor tilemaps in 2 calls; but these are set to sorting layers above default on the sorting layer list.

For better clarity here’s a video run-through of the two instances of Frame Debugger

https://www.youtube.com/watch?v=i3LnT-DyIu8

I can’t really tell from your screenshots if any of the Renderers with your Stencil Write shader is rendered after the Renderer with the Stencil Read shader, which would likely cause the issue.

Have you tried adjusting the Sorting details of the Renderer with the Stencil Read shader? The Order in Layer will likely not show up in the Inspector, which looks like a MeshRenderer, so you will need to write a script to change it or expose it. Would it be possible to see if it works if the Sorting values matches the ones you have set in the TilemapRenderer.

Also, you can take a look at how sorting for 2D Renderers work at Unity - Manual: 2D Sorting, which may be helpful. The Order in Layer takes precedence over the Render Queue which should show up in the Frame Debugger.

YES! That fixed it. Thanks so much. I never would have considered there’s a hidden variable that affects rendering, on a component who’s sole purpose is to render.

Here’s a gif of the final effect I used it for (in case anyone is curious)…

7627024--948982--TransitionEffect_Optimized.gif

…and if anyone needs it, here is a script that overrides the MeshRenderer inspector to expose the ‘sortingLayerName’ (as a selectable enum) and the ‘sortingOrder’ variables

using UnityEngine;
using UnityEditor;
using UnityEditorInternal;

[CustomEditor(typeof(MeshRenderer), true), CanEditMultipleObjects]
public class MeshRendererEditorOverride : Editor
{
    MeshRenderer script;
    string[] sortingLayerNames;

    private void OnEnable()
    {
        script = (MeshRenderer)target;
    }

    public string[] GetSortingLayerNames()
    {
        System.Type internalEditorUtilityType = typeof(InternalEditorUtility);
        System.Reflection.PropertyInfo sortingLayersProperty = internalEditorUtilityType.GetProperty("sortingLayerNames", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic);
        return (string[])sortingLayersProperty.GetValue(null, new object[0]);
    }

    public override void OnInspectorGUI()
    {
        sortingLayerNames = GetSortingLayerNames();
        string sortingLayer = script.sortingLayerName;
        int sortingOrder = script.sortingOrder;
        int indexOf;
        for (indexOf = 0; indexOf < sortingLayerNames.Length; ++indexOf)
            if (sortingLayerNames[indexOf] == sortingLayer)
                break;

        DrawDefaultInspector();
        script.sortingLayerName = sortingLayerNames[EditorGUILayout.Popup("Sorting Layer", indexOf, sortingLayerNames)];
        script.sortingOrder = EditorGUILayout.IntField("Order In Layer", script.sortingOrder);
        if (sortingOrder != script.sortingOrder || sortingLayer != script.sortingLayerName)
            Undo.RecordObject(script, "'" + script.name + "' (Mesh Renderer) Changed");
    }
}