Underlay in top of characters

I think it would be great to have an options in Underlay section of a SDF shader like ‘draw on top of the character’.
The reason is I have a lot of text using inner shadow effect, and to achieve that I should do copy of tmpro text as a child of ‘main’ text gameobject and I wrote a component that duplicates text and all parameters from parent component to this child. It is not critical for now, but this approach doubles overdraw, increase material presets and in some cases leads to children order issues.
Also it would be ultra great to have an opportunity to have an ‘inners shadows’ and ‘outter(regular) shadows’ simultaneously (within one quad ;)). Like the photoshop layer styles does.

P.S. May be I miss something but I can’t understand why is the ‘inner shadow’ goes under the character, so it leads to no effect (hidden under char) by default. From my pov it’s strange behavior and may be there is technical reasouns for that?

1 Like

Making the face of the text semi transparent should provide better results.

Alternatively, see the following post.

Thank you for the link!
I did it in my own way, to keep original functionality. If someone need this, here is the code.

Modified SDF mobile shader

// Simplified SDF shader:
// - No Shading Option (bevel / bump / env map)
// - No Glow Option
// - Softness is applied on both side of the outline

Shader "TextMeshPro/Mobile/Distance Field" {

Properties {
    _FaceColor            ("Face Color", Color) = (1,1,1,1)
    _FaceDilate            ("Face Dilate", Range(-1,1)) = 0

    _OutlineColor        ("Outline Color", Color) = (0,0,0,1)
    _OutlineWidth        ("Outline Thickness", Range(0,1)) = 0
    _OutlineSoftness    ("Outline Softness", Range(0,1)) = 0

    _UnderlayColor        ("Border Color", Color) = (0,0,0,.5)
    _UnderlayOffsetX     ("Border OffsetX", Range(-1,1)) = 0
    _UnderlayOffsetY     ("Border OffsetY", Range(-1,1)) = 0
    _UnderlayDilate        ("Border Dilate", Range(-1,1)) = 0
    _UnderlaySoftness     ("Border Softness", Range(0,1)) = 0

    _WeightNormal        ("Weight Normal", float) = 0
    _WeightBold            ("Weight Bold", float) = .5

    _ShaderFlags        ("Flags", float) = 0
    _ScaleRatioA        ("Scale RatioA", float) = 1
    _ScaleRatioB        ("Scale RatioB", float) = 1
    _ScaleRatioC        ("Scale RatioC", float) = 1

    _MainTex            ("Font Atlas", 2D) = "white" {}
    _TextureWidth        ("Texture Width", float) = 512
    _TextureHeight        ("Texture Height", float) = 512
    _GradientScale        ("Gradient Scale", float) = 5
    _ScaleX                ("Scale X", float) = 1
    _ScaleY                ("Scale Y", float) = 1
    _PerspectiveFilter    ("Perspective Correction", Range(0, 1)) = 0.875

    _VertexOffsetX        ("Vertex OffsetX", float) = 0
    _VertexOffsetY        ("Vertex OffsetY", float) = 0

    _ClipRect            ("Clip Rect", vector) = (-32767, -32767, 32767, 32767)
    _MaskSoftnessX        ("Mask SoftnessX", float) = 0
    _MaskSoftnessY        ("Mask SoftnessY", float) = 0
  
    _StencilComp        ("Stencil Comparison", Float) = 8
    _Stencil            ("Stencil ID", Float) = 0
    _StencilOp            ("Stencil Operation", Float) = 0
    _StencilWriteMask    ("Stencil Write Mask", Float) = 255
    _StencilReadMask    ("Stencil Read Mask", Float) = 255
  
    _ColorMask            ("Color Mask", Float) = 15
}

SubShader {
    Tags
    {
        "Queue"="Transparent"
        "IgnoreProjector"="True"
        "RenderType"="Transparent"
    }


    Stencil
    {
        Ref [_Stencil]
        Comp [_StencilComp]
        Pass [_StencilOp]
        ReadMask [_StencilReadMask]
        WriteMask [_StencilWriteMask]
    }

    Cull [_CullMode]
    ZWrite Off
    Lighting Off
    Fog { Mode Off }
    ZTest [unity_GUIZTestMode]
    Blend One OneMinusSrcAlpha
    ColorMask [_ColorMask]

    Pass {
        CGPROGRAM
        #pragma vertex VertShader
        #pragma fragment PixShader
        #pragma shader_feature __ OUTLINE_ON
        #pragma shader_feature __ UNDERLAY_ON UNDERLAY_INNER
        #pragma shader_feature __ UNDERLAY_ON_TOP

        #pragma multi_compile __ UNITY_UI_CLIP_RECT
        #pragma multi_compile __ UNITY_UI_ALPHACLIP

        #include "UnityCG.cginc"
        #include "UnityUI.cginc"
        #include "TMPro_Properties.cginc"

        struct vertex_t {
            float4    vertex            : POSITION;
            float3    normal            : NORMAL;
            fixed4    color            : COLOR;
            float2    texcoord0        : TEXCOORD0;
            float2    texcoord1        : TEXCOORD1;
        };

        struct pixel_t {
            float4    vertex            : SV_POSITION;
            fixed4    faceColor        : COLOR;
            fixed4    outlineColor    : COLOR1;
            float4    texcoord0        : TEXCOORD0;            // Texture UV, Mask UV
            half4    param            : TEXCOORD1;            // Scale(x), BiasIn(y), BiasOut(z), Bias(w)
            half4    mask            : TEXCOORD2;            // Position in clip space(xy), Softness(zw)
        #if (UNDERLAY_ON | UNDERLAY_INNER)
            float4    texcoord1        : TEXCOORD3;            // Texture UV, alpha, reserved
            half2    underlayParam    : TEXCOORD4;            // Scale(x), Bias(y)
        #endif
        };


        pixel_t VertShader(vertex_t input)
        {
            float bold = step(input.texcoord1.y, 0);

            float4 vert = input.vertex;
            vert.x += _VertexOffsetX;
            vert.y += _VertexOffsetY;
            float4 vPosition = UnityObjectToClipPos(vert);

            float2 pixelSize = vPosition.w;
            pixelSize /= float2(_ScaleX, _ScaleY) * abs(mul((float2x2)UNITY_MATRIX_P, _ScreenParams.xy));
          
            float scale = rsqrt(dot(pixelSize, pixelSize));
            scale *= abs(input.texcoord1.y) * _GradientScale * 1.5;
            if(UNITY_MATRIX_P[3][3] == 0) scale = lerp(abs(scale) * (1 - _PerspectiveFilter), scale, abs(dot(UnityObjectToWorldNormal(input.normal.xyz), normalize(WorldSpaceViewDir(vert)))));

            float weight = lerp(_WeightNormal, _WeightBold, bold) / 4.0;
            weight = (weight + _FaceDilate) * _ScaleRatioA * 0.5;

            float layerScale = scale;

            scale /= 1 + (_OutlineSoftness * _ScaleRatioA * scale);
            float bias = (0.5 - weight) * scale - 0.5;
            float outline = _OutlineWidth * _ScaleRatioA * 0.5 * scale;

            float opacity = input.color.a;
        #if (UNDERLAY_ON | UNDERLAY_INNER)
                opacity = 1.0;
        #endif

            fixed4 faceColor = fixed4(input.color.rgb, opacity) * _FaceColor;
            faceColor.rgb *= faceColor.a;

            fixed4 outlineColor = _OutlineColor;
            outlineColor.a *= opacity;
            outlineColor.rgb *= outlineColor.a;
            outlineColor = lerp(faceColor, outlineColor, sqrt(min(1.0, (outline * 2))));

        #if (UNDERLAY_ON | UNDERLAY_INNER)

            layerScale /= 1 + ((_UnderlaySoftness * _ScaleRatioC) * layerScale);
            float layerBias = (.5 - weight) * layerScale - .5 - ((_UnderlayDilate * _ScaleRatioC) * .5 * layerScale);

            float x = -(_UnderlayOffsetX * _ScaleRatioC) * _GradientScale / _TextureWidth;
            float y = -(_UnderlayOffsetY * _ScaleRatioC) * _GradientScale / _TextureHeight;
            float2 layerOffset = float2(x, y);
        #endif

            // Generate UV for the Masking Texture
            float4 clampedRect = clamp(_ClipRect, -2e10, 2e10);
            float2 maskUV = (vert.xy - clampedRect.xy) / (clampedRect.zw - clampedRect.xy);

            // Structure for pixel shader
            pixel_t output = {
                vPosition,
                faceColor,
                outlineColor,
                float4(input.texcoord0.x, input.texcoord0.y, maskUV.x, maskUV.y),
                half4(scale, bias - outline, bias + outline, bias),
                half4(vert.xy * 2 - clampedRect.xy - clampedRect.zw, 0.25 / (0.25 * half2(_MaskSoftnessX, _MaskSoftnessY) + pixelSize.xy)),
            #if (UNDERLAY_ON | UNDERLAY_INNER)
                float4(input.texcoord0 + layerOffset, input.color.a, 0),
                half2(layerScale, layerBias),
            #endif
            };

            return output;
        }


        // PIXEL SHADER
        fixed4 PixShader(pixel_t input) : SV_Target
        {
            half d = tex2D(_MainTex, input.texcoord0.xy).a * input.param.x;
            half4 c = input.faceColor * saturate(d - input.param.w);

        #ifdef OUTLINE_ON
            c = lerp(input.outlineColor, input.faceColor, saturate(d - input.param.z));
            c *= saturate(d - input.param.y);
        #endif

        #if UNDERLAY_ON
            d = tex2D(_MainTex, input.texcoord1.xy).a * input.underlayParam.x;
            #if UNDERLAY_ON_TOP
                // SHADOW IN TOP OF CHAR
                half4 shadowColor = float4(_UnderlayColor.rgb * _UnderlayColor.a, _UnderlayColor.a) * saturate(d - input.underlayParam.y);
                c.rgb = shadowColor.rgb + c.rgb * (1 - shadowColor.a);
                c.a = max(c.a, shadowColor.a);
            #else
                // ORIGINAL IMPLEMENTATION
                c += float4(_UnderlayColor.rgb * _UnderlayColor.a, _UnderlayColor.a) * saturate(d - input.underlayParam.y) * (1 - c.a);
            #endif
        #endif

        #if UNDERLAY_INNER
            half sd = saturate(d - input.param.z);
            d = tex2D(_MainTex, input.texcoord1.xy).a * input.underlayParam.x;

            #if UNDERLAY_ON_TOP
                // INNER SHADOW IN TOP OF CHAR
                half innerAlpha = _UnderlayColor.a * (1 - saturate(d - input.underlayParam.y));
                c.rgb = lerp(c.rgb, _UnderlayColor.rgb, innerAlpha);
                c.a = max(c.a, innerAlpha * sd);
                c.rgb *= c.a;
            #else
                // ORIGINAL IMPLEMENTATION
                c += float4(_UnderlayColor.rgb * _UnderlayColor.a, _UnderlayColor.a) * (1 - saturate(d - input.underlayParam.y)) * sd * (1 - c.a);
            #endif
        #endif

        // Alternative implementation to UnityGet2DClipping with support for softness.
        #if UNITY_UI_CLIP_RECT
            half2 m = saturate((_ClipRect.zw - _ClipRect.xy - abs(input.mask.xy)) * input.mask.zw);
            c *= m.x * m.y;
        #endif

        #if (UNDERLAY_ON | UNDERLAY_INNER)
            c *= input.texcoord1.z;
        #endif

        #if UNITY_UI_ALPHACLIP
            clip(c.a - 0.001);
        #endif

            return c;
        }
        ENDCG
    }
}

CustomEditor "TMPro.EditorUtilities.TMP_SDFShaderGUI"
}

Editor’s material context menu for toggling functionality

#if UNITY_EDITOR
using UnityEditor;
using UnityEngine;

namespace TMPro.EditorUtilities {
    public class TMProInnerOnTopFeature {
        const string MENU_PATH = "CONTEXT/Material/Swap Underlay Order";
        const string SHADER_PATH = "TextMeshPro/Mobile/Distance Field";
        const string KEYWORD = "UNDERLAY_ON_TOP";


        [MenuItem(MENU_PATH, priority = 150)]
        public static void SwitchFeature (MenuCommand menu) {
            var mat = menu.context as Material;
            if (!mat) {
                Debug.LogError("Hey! This is not a material!");
                return;
            }

            if (mat.IsKeywordEnabled(KEYWORD)) {
                mat.DisableKeyword(KEYWORD);
                Debug.Log($"Material {mat.name} [{KEYWORD}] => set to DISABLED", mat);
            }
            else {
                mat.EnableKeyword(KEYWORD);
                Debug.Log($"Material {mat.name} [{KEYWORD}] => set to ENABLED", mat);
            }
        }


        [MenuItem(MENU_PATH, true)]
        public static bool Validate (MenuCommand menu) {
            var mat = menu.context as Material;
            return mat && mat.shader.name == SHADER_PATH;
        }
    }
}
#endif
2 Likes

I desperately need the same thing but to be able to switch the outter shadow so that it stays behind the character. I don’t know how to writes shader code, and I tried messing up with your code without any result, could you help me with this ? @keni4

Hi @Rayeloy Just to clarify, you need both inner and outer shadows are on, and inner should be on top of a character with outer shadow lying under a character?

2 Likes

I need just the outer shadow BEHIND the characters. Also, some weird spikes are forming in the underlay instead of being a smooth shape, would you know why that happens?
7061989--838909--upload_2021-4-21_13-25-30.png

Thank you!!

Just to clarify, it should look like this:
7062025--838918--upload_2021-4-21_13-33-46.png
Not like this:
7062025--838921--upload_2021-4-21_13-34-44.png

7062025--838915--upload_2021-4-21_13-33-27.png

Got it. This happens because outline/shadow effects are rendered per character, not per word nor per whole text. There is nothing you can do with that. I had the same problem some time ago. Try to play with char spacing, outline width, char sorting order. Yeah, I know this will not looks exactly like meant by designer, but very close to one.

About jugged edges - try to increase char Sampling Point Size and Padding proportionally, and char atlas resolution if needed.
7062373--838957--upload_2021-4-21_16-22-26.png

Hope this will help @Rayeloy

1 Like

It did help a lot, thank you so much. It is a shame that functionality cannot be created though. You saved my day! @keni4

end result: 7062610--839017--upload_2021-4-21_16-44-29.png

1 Like

The visual artifacts you reference are due to the use of SDFAA which is very fast but less accurate then the super slow but accurate SDF16 mode.

See the following thread / post about these render modes.

“Although the SDFAA modes are less accurate and visible in the Editor when zoomed in on the text, this is not visible in the game view unless the text is larger than 90 point size as at lower point size, there is not enough pixels to represents those inaccuracies. This would most likely only impact text like Titles.”

Note: For all the known text used in the project, that is the text contained in your menus, titles, dialogues, etc. I recommend using a static font asset where using SDF16 will give you the best possible quality. Then for text coming from user input / unknown at build time, to use a dynamic font asset using SDFAA assigned as local fallback to the static primary font asset.

Make sure the sampling point size to padding ratio is the same between the primary and fallback font asset.

1 Like

So apparently the thread started with a request for underlay over characters, then continued with underlay below neighboring characters… Anyway, I’m in the second situation, and I cannot just space out my characters more because I’m using a handwriting font where characters are meant to be connected to each other.

But I understand it’s not easy, as I had the same issue with my graphics editor (Krita): setting vector outline will do it per character and cause overlap, while setting layer outline will apply outline to the whole layer, union of all shapes, and give the wanted result.

It would be nice to have the same with Unity TMP, possibly using some shader that applies to the whole text graphics?

Wanted result, using Krita’s Layer Outline:

TMP Underlay (outline per character):

Trying to space characters (breaks the handwriting feeling):

EDIT: since the thread’s title is kinda the opposite on the second request, maybe we should open a separate thread specifically for Underlay below neighboring characters or Final text Underlay?

1 Like

Please test the latest shader included in version 3.2.0-pre.3. This new shader is a 2 pass shader which should provide some benefits with cursive type text.

Hi, there, sorry for necroposting. I’m on 2023.2.5 and Underlay is no more done in 2 passes. In 2023.1.15 it was so. I have the mobile version with a variant that’s 2 passes, but it doesn’t use the texture, so I can’t use that.

Edit: it wasn’t double pass. I’ve ended up modifying the mobile 2 passes to make it use a texture, like the single pass non mobile does.