Text Outline
(correct interior outline color computing + more crisp outlines) |
m (faster clip region (for what it's worth, given that we cant relay rely on UVs)) |
||
(13 intermediate revisions by one user not shown) | |||
Line 11: | Line 11: | ||
+ | == 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): | ||
+ | * https://forum.unity.com/threads/shader-graph-getting-local-sprite-uv-from-sprite-sheet.865834/ | ||
+ | |||
+ | |||
+ | 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. | ||
+ | |||
+ | [[File:UnityInconsistencies.jpg]] | ||
== Code ( UI_TextOutline.shader ) == | == Code ( UI_TextOutline.shader ) == | ||
Line 23: | Line 49: | ||
/*[PerRendererData]*/[MaterialToggle] _Exterior ( " Exterior", Float ) = 1.0 | /*[PerRendererData]*/[MaterialToggle] _Exterior ( " Exterior", Float ) = 1.0 | ||
/*[PerRendererData]*/[MaterialToggle] _Interior ( " Interior", Float ) = 0.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 | SubShader | ||
{ | { | ||
+ | //**************************************************************************************************** | ||
+ | // | ||
+ | //**************************************************************************************************** | ||
+ | |||
Tags | Tags | ||
{ | { | ||
− | "Queue" | + | "Queue" = "Transparent" |
− | "RenderType" = " | + | "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 | Blend SrcAlpha OneMinusSrcAlpha | ||
+ | |||
+ | //**************************************************************************************************** | ||
+ | // | ||
+ | //**************************************************************************************************** | ||
Pass | Pass | ||
Line 56: | Line 124: | ||
struct appdata | struct appdata | ||
{ | { | ||
− | float4 vertex : POSITION; | + | float4 vertex : POSITION; |
− | float2 uv | + | float2 uv : TEXCOORD0; |
− | float4 color | + | float4 color : COLOR; |
}; | }; | ||
struct v2f | struct v2f | ||
{ | { | ||
− | float4 vertex : SV_POSITION; | + | float4 vertex : SV_POSITION; |
− | float2 uv | + | float2 uv : TEXCOORD0; |
− | float4 color | + | float2 uv_local : TEXCOORD1; |
− | }; | + | float4 color : COLOR; |
+ | }; | ||
− | static const fixed alpha_threshold = 0.25f; | + | 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 ) }; | ||
//******************************************************************************************** | //******************************************************************************************** | ||
Line 81: | Line 151: | ||
fixed _Exterior; | fixed _Exterior; | ||
fixed _Interior; | fixed _Interior; | ||
+ | fixed _Sampling_UV_MIN_X; | ||
+ | fixed _Sampling_UV_MAX_X; | ||
+ | fixed _Sampling_UV_MIN_Y; | ||
+ | fixed _Sampling_UV_MAX_Y; | ||
//******************************************************************************************** | //******************************************************************************************** | ||
Line 86: | Line 160: | ||
//******************************************************************************************** | //******************************************************************************************** | ||
− | + | 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 ); | bool valid_context = exterior ? ( a < alpha_threshold ) : ( a > alpha_threshold ); | ||
+ | |||
if ( valid_context ) | 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 ) | ||
{ | { | ||
− | dst = ( x * x ) + ( y * 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 | + | return alpha; |
} | } | ||
Line 130: | Line 230: | ||
//******************************************************************************************** | //******************************************************************************************** | ||
− | v2f vert( appdata v ) | + | v2f vert( appdata v, uint id : SV_VertexID ) |
{ | { | ||
v2f o; | v2f o; | ||
− | o.vertex = UnityObjectToClipPos( v.vertex ); | + | o.vertex = UnityObjectToClipPos( v.vertex ); |
− | o.uv | + | o.uv = TRANSFORM_TEX( v.uv, _MainTex ); |
− | o.color | + | o.color = v.color; |
+ | o.uv_local = quads_uvs[ id & 3 ]; | ||
+ | |||
return o; | return o; | ||
} | } | ||
Line 146: | Line 248: | ||
fixed4 frag( v2f i ) : SV_Target | fixed4 frag( v2f i ) : SV_Target | ||
{ | { | ||
− | fixed4 output = | + | fixed4 output = Sample( _MainTex, i.uv, 0 ); |
− | + | ||
− | + | ||
if( _Exterior > 0.0f ) | if( _Exterior > 0.0f ) | ||
{ | { | ||
− | fixed outline_alpha = | + | fixed outline_alpha = GetOutlineAlpha( i, output.a, true ); |
− | if( outline_alpha > 0.0f ) return fixed4( _OutlineColor. | + | if( outline_alpha > 0.0f ) |
+ | { | ||
+ | return fixed4( _OutlineColor.rgb, _OutlineColor.a * outline_alpha * i.color.a ); | ||
+ | } | ||
} | } | ||
if( _Interior > 0.0f ) | if( _Interior > 0.0f ) | ||
{ | { | ||
− | fixed outline_alpha = | + | fixed outline_alpha = GetOutlineAlpha( i, output.a, false ); |
if( outline_alpha > 0.0f ) | if( outline_alpha > 0.0f ) | ||
{ | { | ||
− | fixed4 material_color = fixed4( i.color. | + | fixed4 material_color = fixed4( i.color.rgb, 1.0f ); |
fixed4 result = lerp( material_color, _OutlineColor, outline_alpha ); | fixed4 result = lerp( material_color, _OutlineColor, outline_alpha ); | ||
− | return fixed4( result. | + | return fixed4( result.rgb, result.a * i.color.a ); |
} | } | ||
} | } | ||
− | + | ||
+ | output = fixed4( i.color.rgb, output.a * i.color.a ); | ||
return output; | return output; | ||
} | } | ||
Line 173: | Line 277: | ||
} | } | ||
} | } | ||
+ | |||
+ | Fallback "UI/Default" | ||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> |
Latest revision as of 08:36, 3 November 2020
Author: Serge Billault
Contents |
[edit] 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.
[edit] Preview
[edit] 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.
[edit] 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" }
[edit] License
Well, it's one of my first shaders, so... . Planet free.