Text Outline

From Unify Community Wiki
Jump to: navigation, search

Author: Serge Billault

Contents

Description

Text outlining with independantly selectable interior and exterior. The pixel being rendered sample its surrounding according to outline _Thickness (in pixels) and deduce the outline alpha from the distance with the nearest outline inducing texture texel.

Preview

Text outline.jpg


Understanding the human nature behind the impossibility to get Texts local UVs

A shader can only access individual vertices and has no infos on the vertices of a triangle other than the one being processed. It is therefore impossible to get some informations such as local UVs if you dont pass these informations in some way or another. The only thing that stands between you and being free of all the... "funy" things you have to do to perform simple operations is the fact that despite Unity knowing how to insert informations it need for itself into the geometry, when it comes to the users needs they suddenly dont know any more how to do it. As result you get this kind of thread on the forums (notice the date: 2020, we have been dealing with Unity decision for a decade):


Should they one day decide to include local UVs infos for everything quad based like texts and images, then

  • night becomes day.
  • Text packages are no more necessary for 90% of us.
  • fonts, no matter which, do not need any padding any more.


I mentioned padding because that is actually what prevent you from adding arbitrary outlines of any size. In their Atlas, glyphs are so tightly packed that you often end up sampling part of glyphs that are not the one being rendered (bleeding effect). If you have access to local UVs you overcome it directly in the shader by scaling the quad and the UVs (so that the glyph size remain unchanged but the area we can draw to, increases). When sampling you then return fixed4(0,0,0,0) for everything outside of the glyph clip rectangle, but draw outlines using the whole quad.


But it has never been deemed usefull to service the users by passing local UVs as informations in spite of all the users over the world asking for it for 10 years. So we keep writing our own Text components, rewriting mesh populatiing / drawing functions or buying packages from wich we use only 1% of the features.


Below: the expected vertices order by the shader in this page. We can see that Unity find it funy to not only not pass the local UVs requested by thousand of users since a decade but that they also enjoy passing quads with varying vertices orders. Because. The result is that you must not rely on the local UVs in this shader for anything other than bounds checks.

UnityInconsistencies.jpg

Code ( UI_TextOutline.shader )

Shader "Unlit/UITextOutline"
{
    Properties
    {
        /*                 */                 _MainTex     ( " Texture",       2D                 ) = "white" {}
        /*[PerRendererData]*/                 _OutlineColor( " Outline Color", Color              ) = ( 0.0, 0.0, 0.0, 1.0 )
        /*[PerRendererData]*/                 _Thickness   ( " Thickness",     Range( 1.0, 16.0 ) ) = 2.1
        /*[PerRendererData]*/[MaterialToggle] _Exterior    ( " Exterior", Float ) = 1.0
        /*[PerRendererData]*/[MaterialToggle] _Interior    ( " Interior", Float ) = 0.0
        /*[PerRendererData]*/                 _Sampling_UV_MIN_X   ( " Sampling UV Min X", Range( 0.0, 1.0 ) ) = 0.0
        /*[PerRendererData]*/                 _Sampling_UV_MAX_X   ( " Sampling UV Max X", Range( 0.0, 1.0 ) ) = 1.0
        /*[PerRendererData]*/                 _Sampling_UV_MIN_Y   ( " Sampling UV Min Y", Range( 0.0, 1.0 ) ) = 0.0
        /*[PerRendererData]*/                 _Sampling_UV_MAX_Y   ( " Sampling UV Max Y", Range( 0.0, 1.0 ) ) = 1.0
 
         [HideInInspector]_StencilComp     ( "Stencil Comparison", Float ) = 8
         [HideInInspector]_Stencil         ( "Stencil ID",         Float ) = 0
         [HideInInspector]_StencilOp       ( "Stencil Operation",  Float ) = 0
         [HideInInspector]_StencilWriteMask( "Stencil Write Mask", Float ) = 255
         [HideInInspector]_StencilReadMask ( "Stencil Read Mask",  Float ) = 255
         [HideInInspector]_ColorMask       ( "Color Mask",         Float ) = 15
    }
 
    SubShader
    {
        //****************************************************************************************************
        //
        //****************************************************************************************************
 
        Tags 
        { 
            "Queue"             = "Transparent"
            "IgnoreProjector"   = "True"
            "RenderType"        = "Transparent"
            "PreviewType"       = "Plane"
            "CanUseSpriteAtlas" = "True"
        }
 
        //****************************************************************************************************
        //
        //****************************************************************************************************
 
        Stencil
        {
            Ref       [_Stencil]
            Comp      [_StencilComp]
            Pass      [_StencilOp]
            ReadMask  [_StencilReadMask]
            WriteMask [_StencilWriteMask]
        }
 
        //****************************************************************************************************
        //
        //****************************************************************************************************
 
        Cull          Off
        Lighting      Off
        ZWrite        Off
        ZTest         [unity_GUIZTestMode]
        ColorMask     [_ColorMask]
        Blend SrcAlpha OneMinusSrcAlpha
 
        //****************************************************************************************************
        //
        //****************************************************************************************************
 
        Pass
        {
            CGPROGRAM
 
                //********************************************************************************************
                //
                //********************************************************************************************
 
                #pragma vertex   vert
                #pragma fragment frag
 
                #include "UnityCG.cginc"
 
                //********************************************************************************************
                //
                //********************************************************************************************
 
                struct appdata
                {
                    float4 vertex   : POSITION;
                    float2 uv       : TEXCOORD0;
                    float4 color    : COLOR;
                };
 
                struct v2f
                {
                    float4 vertex   : SV_POSITION;
                    float2 uv       : TEXCOORD0;
                    float2 uv_local : TEXCOORD1;
                    float4 color    : COLOR;
                }; 
 
                static const fixed  alpha_threshold = 0.25f;
                static const fixed2 quads_uvs[ 4 ]  = { fixed2( 0.0f, 1.0f ), fixed2( 1.0f, 1.0f ), fixed2( 1.0f, 0.0f ), fixed2( 0.0f, 0.0f ) };
 
                //********************************************************************************************
                //
                //********************************************************************************************
 
                sampler2D _MainTex;
                fixed4    _MainTex_TexelSize;
                fixed4    _MainTex_ST;
                fixed4    _OutlineColor;
                fixed     _Thickness;
                fixed     _Exterior;
                fixed     _Interior;
                fixed     _Sampling_UV_MIN_X;
                fixed     _Sampling_UV_MAX_X;
                fixed     _Sampling_UV_MIN_Y;
                fixed     _Sampling_UV_MAX_Y;
 
                //********************************************************************************************
                //
                //********************************************************************************************
 
                fixed4 Sample( sampler2D smplr, fixed2 uv, int lod )
                {
                    if( lod < 0 )
                    {
                        return tex2D( smplr, uv );
                    }
 
                    return tex2Dlod( smplr, fixed4( uv.x, uv.y, 0, lod ) );
                }
 
                //********************************************************************************************
                //
                //********************************************************************************************
 
                fixed GetOutlineAlpha( v2f i, fixed a, bool exterior )
                {
                    fixed alpha = -1.0f;
 
                    bool valid_context = exterior ? ( a < alpha_threshold ) : ( a > alpha_threshold );
 
                    if ( valid_context )
                    {
                        bool valid_region = ( i.uv_local.x >= _Sampling_UV_MIN_X ) && 
                                            ( i.uv_local.x <= _Sampling_UV_MAX_X ) &&
                                            ( i.uv_local.y >= _Sampling_UV_MIN_Y ) &&
                                            ( i.uv_local.y <= _Sampling_UV_MAX_Y );
 
                        if( valid_region )
                        {
                            fixed  texel_w = _MainTex_TexelSize.x;
                            fixed  texel_h = _MainTex_TexelSize.y; 
                            fixed  min_dst = 1.0e38f;
 
                            int x_s = ( i.uv_local.x <= _Sampling_UV_MIN_X ) ? 0 : -_Thickness;
                            int x_e = ( i.uv_local.x >= _Sampling_UV_MAX_X ) ? 0 :  _Thickness;
                            int y_s = ( i.uv_local.y <= _Sampling_UV_MIN_Y ) ? 0 : -_Thickness;
                            int y_e = ( i.uv_local.y >= _Sampling_UV_MAX_Y ) ? 0 :  _Thickness;
 
                            for( int x = x_s; x <= x_e; ++x )
                            {
                                for( int y = y_s; y <= y_e; ++y )
                                {
                                    fixed2 offset   = fixed2( x * texel_w, y * texel_h );
 
                                    fixed4 sampling = Sample( _MainTex, i.uv + offset, 0 );
 
                                    bool   outline  = exterior ? ( sampling.a >= alpha_threshold ) : ( sampling.a <= alpha_threshold );
 
                                    if( outline )
                                    {
                                        fixed dst = ( x * x ) + ( y * y );
                                        if( min_dst > dst ) min_dst = dst;
                                    }
                                }
                            }
 
                            if( min_dst < 1.0e38f )
                            {
                                alpha = 1.0f - ( clamp( ( min_dst ) / ( _Thickness * _Thickness ), 0.0f, 1.0f ) );
                            }
                        }
                    }
 
                    return alpha;
                }
 
                //********************************************************************************************
                //
                //********************************************************************************************
 
                v2f vert( appdata v, uint id : SV_VertexID )
                {
                    v2f o;
 
                    o.vertex   = UnityObjectToClipPos( v.vertex );
                    o.uv       = TRANSFORM_TEX( v.uv, _MainTex );
                    o.color    = v.color;
                    o.uv_local = quads_uvs[ id & 3 ];
 
                    return o;
                }
 
                //********************************************************************************************
                //
                //********************************************************************************************
 
                fixed4 frag( v2f i ) : SV_Target
                {
                    fixed4 output = Sample( _MainTex, i.uv, 0 );
 
                    if( _Exterior > 0.0f )
                    {
                        fixed outline_alpha = GetOutlineAlpha( i, output.a, true  );
                        if( outline_alpha > 0.0f ) 
                        {
                            return fixed4( _OutlineColor.rgb, _OutlineColor.a * outline_alpha * i.color.a );
                        }
                    }
 
                    if( _Interior > 0.0f )
                    {
                        fixed outline_alpha = GetOutlineAlpha( i, output.a, false );
                        if( outline_alpha > 0.0f )
                        {
                            fixed4 material_color = fixed4( i.color.rgb, 1.0f );
                            fixed4 result         = lerp( material_color, _OutlineColor, outline_alpha );
                            return fixed4( result.rgb, result.a * i.color.a );
                        }
                    }
 
                    output = fixed4( i.color.rgb, output.a * i.color.a );
                    return output;
                }
 
            ENDCG
        }
    }
 
    Fallback "UI/Default"
}

License

Well, it's one of my first shaders, so... . Planet free.

Personal tools
Namespaces

Variants
Actions
Navigation
Extras
Toolbox