Search Unity

  1. Unity 6 Preview is now available. To find out what's new, have a look at our Unity 6 Preview blog post.
    Dismiss Notice
  2. Unity is excited to announce that we will be collaborating with TheXPlace for a summer game jam from June 13 - June 19. Learn more.
    Dismiss Notice

TextMesh Pro Underlay in top of characters

Discussion in 'UGUI & TextMesh Pro' started by keni4, Mar 20, 2019.

  1. keni4

    keni4

    Joined:
    Nov 30, 2014
    Posts:
    31
    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?
     
  2. Stephan_B

    Stephan_B

    Joined:
    Feb 26, 2017
    Posts:
    6,596
    Making the face of the text semi transparent should provide better results.

    Alternatively, see the following post.
     
  3. keni4

    keni4

    Joined:
    Nov 30, 2014
    Posts:
    31
    Thank you for the link!
    I did it in my own way, to keep original functionality. If someone need this, here is the code.

    Code (CSharp):
    1. // Simplified SDF shader:
    2. // - No Shading Option (bevel / bump / env map)
    3. // - No Glow Option
    4. // - Softness is applied on both side of the outline
    5.  
    6. Shader "TextMeshPro/Mobile/Distance Field" {
    7.  
    8. Properties {
    9.     _FaceColor            ("Face Color", Color) = (1,1,1,1)
    10.     _FaceDilate            ("Face Dilate", Range(-1,1)) = 0
    11.  
    12.     _OutlineColor        ("Outline Color", Color) = (0,0,0,1)
    13.     _OutlineWidth        ("Outline Thickness", Range(0,1)) = 0
    14.     _OutlineSoftness    ("Outline Softness", Range(0,1)) = 0
    15.  
    16.     _UnderlayColor        ("Border Color", Color) = (0,0,0,.5)
    17.     _UnderlayOffsetX     ("Border OffsetX", Range(-1,1)) = 0
    18.     _UnderlayOffsetY     ("Border OffsetY", Range(-1,1)) = 0
    19.     _UnderlayDilate        ("Border Dilate", Range(-1,1)) = 0
    20.     _UnderlaySoftness     ("Border Softness", Range(0,1)) = 0
    21.  
    22.     _WeightNormal        ("Weight Normal", float) = 0
    23.     _WeightBold            ("Weight Bold", float) = .5
    24.  
    25.     _ShaderFlags        ("Flags", float) = 0
    26.     _ScaleRatioA        ("Scale RatioA", float) = 1
    27.     _ScaleRatioB        ("Scale RatioB", float) = 1
    28.     _ScaleRatioC        ("Scale RatioC", float) = 1
    29.  
    30.     _MainTex            ("Font Atlas", 2D) = "white" {}
    31.     _TextureWidth        ("Texture Width", float) = 512
    32.     _TextureHeight        ("Texture Height", float) = 512
    33.     _GradientScale        ("Gradient Scale", float) = 5
    34.     _ScaleX                ("Scale X", float) = 1
    35.     _ScaleY                ("Scale Y", float) = 1
    36.     _PerspectiveFilter    ("Perspective Correction", Range(0, 1)) = 0.875
    37.  
    38.     _VertexOffsetX        ("Vertex OffsetX", float) = 0
    39.     _VertexOffsetY        ("Vertex OffsetY", float) = 0
    40.  
    41.     _ClipRect            ("Clip Rect", vector) = (-32767, -32767, 32767, 32767)
    42.     _MaskSoftnessX        ("Mask SoftnessX", float) = 0
    43.     _MaskSoftnessY        ("Mask SoftnessY", float) = 0
    44.  
    45.     _StencilComp        ("Stencil Comparison", Float) = 8
    46.     _Stencil            ("Stencil ID", Float) = 0
    47.     _StencilOp            ("Stencil Operation", Float) = 0
    48.     _StencilWriteMask    ("Stencil Write Mask", Float) = 255
    49.     _StencilReadMask    ("Stencil Read Mask", Float) = 255
    50.  
    51.     _ColorMask            ("Color Mask", Float) = 15
    52. }
    53.  
    54. SubShader {
    55.     Tags
    56.     {
    57.         "Queue"="Transparent"
    58.         "IgnoreProjector"="True"
    59.         "RenderType"="Transparent"
    60.     }
    61.  
    62.  
    63.     Stencil
    64.     {
    65.         Ref [_Stencil]
    66.         Comp [_StencilComp]
    67.         Pass [_StencilOp]
    68.         ReadMask [_StencilReadMask]
    69.         WriteMask [_StencilWriteMask]
    70.     }
    71.  
    72.     Cull [_CullMode]
    73.     ZWrite Off
    74.     Lighting Off
    75.     Fog { Mode Off }
    76.     ZTest [unity_GUIZTestMode]
    77.     Blend One OneMinusSrcAlpha
    78.     ColorMask [_ColorMask]
    79.  
    80.     Pass {
    81.         CGPROGRAM
    82.         #pragma vertex VertShader
    83.         #pragma fragment PixShader
    84.         #pragma shader_feature __ OUTLINE_ON
    85.         #pragma shader_feature __ UNDERLAY_ON UNDERLAY_INNER
    86.         #pragma shader_feature __ UNDERLAY_ON_TOP
    87.  
    88.         #pragma multi_compile __ UNITY_UI_CLIP_RECT
    89.         #pragma multi_compile __ UNITY_UI_ALPHACLIP
    90.  
    91.         #include "UnityCG.cginc"
    92.         #include "UnityUI.cginc"
    93.         #include "TMPro_Properties.cginc"
    94.  
    95.         struct vertex_t {
    96.             float4    vertex            : POSITION;
    97.             float3    normal            : NORMAL;
    98.             fixed4    color            : COLOR;
    99.             float2    texcoord0        : TEXCOORD0;
    100.             float2    texcoord1        : TEXCOORD1;
    101.         };
    102.  
    103.         struct pixel_t {
    104.             float4    vertex            : SV_POSITION;
    105.             fixed4    faceColor        : COLOR;
    106.             fixed4    outlineColor    : COLOR1;
    107.             float4    texcoord0        : TEXCOORD0;            // Texture UV, Mask UV
    108.             half4    param            : TEXCOORD1;            // Scale(x), BiasIn(y), BiasOut(z), Bias(w)
    109.             half4    mask            : TEXCOORD2;            // Position in clip space(xy), Softness(zw)
    110.         #if (UNDERLAY_ON | UNDERLAY_INNER)
    111.             float4    texcoord1        : TEXCOORD3;            // Texture UV, alpha, reserved
    112.             half2    underlayParam    : TEXCOORD4;            // Scale(x), Bias(y)
    113.         #endif
    114.         };
    115.  
    116.  
    117.         pixel_t VertShader(vertex_t input)
    118.         {
    119.             float bold = step(input.texcoord1.y, 0);
    120.  
    121.             float4 vert = input.vertex;
    122.             vert.x += _VertexOffsetX;
    123.             vert.y += _VertexOffsetY;
    124.             float4 vPosition = UnityObjectToClipPos(vert);
    125.  
    126.             float2 pixelSize = vPosition.w;
    127.             pixelSize /= float2(_ScaleX, _ScaleY) * abs(mul((float2x2)UNITY_MATRIX_P, _ScreenParams.xy));
    128.          
    129.             float scale = rsqrt(dot(pixelSize, pixelSize));
    130.             scale *= abs(input.texcoord1.y) * _GradientScale * 1.5;
    131.             if(UNITY_MATRIX_P[3][3] == 0) scale = lerp(abs(scale) * (1 - _PerspectiveFilter), scale, abs(dot(UnityObjectToWorldNormal(input.normal.xyz), normalize(WorldSpaceViewDir(vert)))));
    132.  
    133.             float weight = lerp(_WeightNormal, _WeightBold, bold) / 4.0;
    134.             weight = (weight + _FaceDilate) * _ScaleRatioA * 0.5;
    135.  
    136.             float layerScale = scale;
    137.  
    138.             scale /= 1 + (_OutlineSoftness * _ScaleRatioA * scale);
    139.             float bias = (0.5 - weight) * scale - 0.5;
    140.             float outline = _OutlineWidth * _ScaleRatioA * 0.5 * scale;
    141.  
    142.             float opacity = input.color.a;
    143.         #if (UNDERLAY_ON | UNDERLAY_INNER)
    144.                 opacity = 1.0;
    145.         #endif
    146.  
    147.             fixed4 faceColor = fixed4(input.color.rgb, opacity) * _FaceColor;
    148.             faceColor.rgb *= faceColor.a;
    149.  
    150.             fixed4 outlineColor = _OutlineColor;
    151.             outlineColor.a *= opacity;
    152.             outlineColor.rgb *= outlineColor.a;
    153.             outlineColor = lerp(faceColor, outlineColor, sqrt(min(1.0, (outline * 2))));
    154.  
    155.         #if (UNDERLAY_ON | UNDERLAY_INNER)
    156.  
    157.             layerScale /= 1 + ((_UnderlaySoftness * _ScaleRatioC) * layerScale);
    158.             float layerBias = (.5 - weight) * layerScale - .5 - ((_UnderlayDilate * _ScaleRatioC) * .5 * layerScale);
    159.  
    160.             float x = -(_UnderlayOffsetX * _ScaleRatioC) * _GradientScale / _TextureWidth;
    161.             float y = -(_UnderlayOffsetY * _ScaleRatioC) * _GradientScale / _TextureHeight;
    162.             float2 layerOffset = float2(x, y);
    163.         #endif
    164.  
    165.             // Generate UV for the Masking Texture
    166.             float4 clampedRect = clamp(_ClipRect, -2e10, 2e10);
    167.             float2 maskUV = (vert.xy - clampedRect.xy) / (clampedRect.zw - clampedRect.xy);
    168.  
    169.             // Structure for pixel shader
    170.             pixel_t output = {
    171.                 vPosition,
    172.                 faceColor,
    173.                 outlineColor,
    174.                 float4(input.texcoord0.x, input.texcoord0.y, maskUV.x, maskUV.y),
    175.                 half4(scale, bias - outline, bias + outline, bias),
    176.                 half4(vert.xy * 2 - clampedRect.xy - clampedRect.zw, 0.25 / (0.25 * half2(_MaskSoftnessX, _MaskSoftnessY) + pixelSize.xy)),
    177.             #if (UNDERLAY_ON | UNDERLAY_INNER)
    178.                 float4(input.texcoord0 + layerOffset, input.color.a, 0),
    179.                 half2(layerScale, layerBias),
    180.             #endif
    181.             };
    182.  
    183.             return output;
    184.         }
    185.  
    186.  
    187.         // PIXEL SHADER
    188.         fixed4 PixShader(pixel_t input) : SV_Target
    189.         {
    190.             half d = tex2D(_MainTex, input.texcoord0.xy).a * input.param.x;
    191.             half4 c = input.faceColor * saturate(d - input.param.w);
    192.  
    193.         #ifdef OUTLINE_ON
    194.             c = lerp(input.outlineColor, input.faceColor, saturate(d - input.param.z));
    195.             c *= saturate(d - input.param.y);
    196.         #endif
    197.  
    198.         #if UNDERLAY_ON
    199.             d = tex2D(_MainTex, input.texcoord1.xy).a * input.underlayParam.x;
    200.             #if UNDERLAY_ON_TOP
    201.                 // SHADOW IN TOP OF CHAR
    202.                 half4 shadowColor = float4(_UnderlayColor.rgb * _UnderlayColor.a, _UnderlayColor.a) * saturate(d - input.underlayParam.y);
    203.                 c.rgb = shadowColor.rgb + c.rgb * (1 - shadowColor.a);
    204.                 c.a = max(c.a, shadowColor.a);
    205.             #else
    206.                 // ORIGINAL IMPLEMENTATION
    207.                 c += float4(_UnderlayColor.rgb * _UnderlayColor.a, _UnderlayColor.a) * saturate(d - input.underlayParam.y) * (1 - c.a);
    208.             #endif
    209.         #endif
    210.  
    211.         #if UNDERLAY_INNER
    212.             half sd = saturate(d - input.param.z);
    213.             d = tex2D(_MainTex, input.texcoord1.xy).a * input.underlayParam.x;
    214.  
    215.             #if UNDERLAY_ON_TOP
    216.                 // INNER SHADOW IN TOP OF CHAR
    217.                 half innerAlpha = _UnderlayColor.a * (1 - saturate(d - input.underlayParam.y));
    218.                 c.rgb = lerp(c.rgb, _UnderlayColor.rgb, innerAlpha);
    219.                 c.a = max(c.a, innerAlpha * sd);
    220.                 c.rgb *= c.a;
    221.             #else
    222.                 // ORIGINAL IMPLEMENTATION
    223.                 c += float4(_UnderlayColor.rgb * _UnderlayColor.a, _UnderlayColor.a) * (1 - saturate(d - input.underlayParam.y)) * sd * (1 - c.a);
    224.             #endif
    225.         #endif
    226.  
    227.         // Alternative implementation to UnityGet2DClipping with support for softness.
    228.         #if UNITY_UI_CLIP_RECT
    229.             half2 m = saturate((_ClipRect.zw - _ClipRect.xy - abs(input.mask.xy)) * input.mask.zw);
    230.             c *= m.x * m.y;
    231.         #endif
    232.  
    233.         #if (UNDERLAY_ON | UNDERLAY_INNER)
    234.             c *= input.texcoord1.z;
    235.         #endif
    236.  
    237.         #if UNITY_UI_ALPHACLIP
    238.             clip(c.a - 0.001);
    239.         #endif
    240.  
    241.             return c;
    242.         }
    243.         ENDCG
    244.     }
    245. }
    246.  
    247. CustomEditor "TMPro.EditorUtilities.TMP_SDFShaderGUI"
    248. }
    249.  

    Code (CSharp):
    1.  
    2. #if UNITY_EDITOR
    3. using UnityEditor;
    4. using UnityEngine;
    5.  
    6. namespace TMPro.EditorUtilities {
    7.     public class TMProInnerOnTopFeature {
    8.         const string MENU_PATH = "CONTEXT/Material/Swap Underlay Order";
    9.         const string SHADER_PATH = "TextMeshPro/Mobile/Distance Field";
    10.         const string KEYWORD = "UNDERLAY_ON_TOP";
    11.  
    12.  
    13.         [MenuItem(MENU_PATH, priority = 150)]
    14.         public static void SwitchFeature (MenuCommand menu) {
    15.             var mat = menu.context as Material;
    16.             if (!mat) {
    17.                 Debug.LogError("Hey! This is not a material!");
    18.                 return;
    19.             }
    20.  
    21.             if (mat.IsKeywordEnabled(KEYWORD)) {
    22.                 mat.DisableKeyword(KEYWORD);
    23.                 Debug.Log($"Material {mat.name} [{KEYWORD}] => set to DISABLED", mat);
    24.             }
    25.             else {
    26.                 mat.EnableKeyword(KEYWORD);
    27.                 Debug.Log($"Material {mat.name} [{KEYWORD}] => set to ENABLED", mat);
    28.             }
    29.         }
    30.  
    31.  
    32.         [MenuItem(MENU_PATH, true)]
    33.         public static bool Validate (MenuCommand menu) {
    34.             var mat = menu.context as Material;
    35.             return mat && mat.shader.name == SHADER_PATH;
    36.         }
    37.     }
    38. }
    39. #endif
    40.  
     
    Rayeloy and yyylny like this.
  4. Rayeloy

    Rayeloy

    Joined:
    Feb 12, 2017
    Posts:
    45
    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
     
  5. keni4

    keni4

    Joined:
    Nov 30, 2014
    Posts:
    31
    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?
     
    Last edited: Apr 21, 2021
    Rayeloy likes this.
  6. Rayeloy

    Rayeloy

    Joined:
    Feb 12, 2017
    Posts:
    45
    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?
    upload_2021-4-21_13-25-30.png

    Thank you!!
     
  7. Rayeloy

    Rayeloy

    Joined:
    Feb 12, 2017
    Posts:
    45
    Just to clarify, it should look like this:
    upload_2021-4-21_13-33-46.png
    Not like this:
    upload_2021-4-21_13-34-44.png
     

    Attached Files:

  8. keni4

    keni4

    Joined:
    Nov 30, 2014
    Posts:
    31
    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.
    upload_2021-4-21_16-22-26.png

    Hope this will help @Rayeloy
     
    Rayeloy likes this.
  9. Rayeloy

    Rayeloy

    Joined:
    Feb 12, 2017
    Posts:
    45
    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: upload_2021-4-21_16-44-29.png
     
    keni4 likes this.
  10. Stephan_B

    Stephan_B

    Joined:
    Feb 26, 2017
    Posts:
    6,596
    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.
     
    Rayeloy likes this.
  11. huulong

    huulong

    Joined:
    Jul 1, 2013
    Posts:
    225
    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:

    Title logo 1200w.png

    TMP Underlay (outline per character):

    upload_2022-10-1_14-46-0.png

    Trying to space characters (breaks the handwriting feeling):

    upload_2022-10-1_14-46-33.png

    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?
     
  12. Stephan_B

    Stephan_B

    Joined:
    Feb 26, 2017
    Posts:
    6,596
    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.
     
  13. theGiallo

    theGiallo

    Joined:
    Jan 25, 2014
    Posts:
    5
    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.
     
    Last edited: Jan 16, 2024