This post will be in English because, unlike other posts, this one may contain useful thingy’s.
At the end of the post we will be able to replicate the effect seen on a movie. For this tutorial I will use schoolbell font.
Step 1. Export the font.
After importing the *.ttf file into Unity, set the correct size (usually really big, 150px or more) and create the editable copy, as shown in the following screenshot. If you can not be bothered with tracing that many letters for tutorial purpose, limit your options to only upper or lower case. I did that.
Step 2. Trace the letters in gimp/photoshop
Open the exported file in gimp, and then trace the files with gradient ranging from red to black. Do it as if you were writing those letters by hand. The more red parts will be rendered first, the more black ones second. In the end you should end up with something like this:
Step 3. Part of the shader
First of all, we need to find the shader unity uses for UI fonts. After getting the build-in shaders from unity website, we see that UI/Unlit/Text shader falls back on UI/Default Font shader which then falls back on UI/Default one. Let’s just copy-paste this one and add the possibility to multiply the alpha by a value from an external map.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 |
Shader "Custom/Unlit/HandwritingTextShaderv1" { Properties { [PerRendererData] _MainTex ("Font Texture", 2D) = "white" {} //Red - black mask font texture _NewTex("Mask Font Texture", 2D) = "white" {} _Color ("Tint", Color) = (1,1,1,1) //Cutoff 0 - we show thew whole letter //Cutoff 1 - we show nothing _Cutoff("Alpha cutoff", Range(0.0, 1.0))=0.5 _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 [Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0 } 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] Blend SrcAlpha OneMinusSrcAlpha ColorMask [_ColorMask] Pass { Name "Default" CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma target 2.0 #include "UnityCG.cginc" #include "UnityUI.cginc" #pragma multi_compile __ UNITY_UI_ALPHACLIP struct appdata_t { float4 vertex : POSITION; float4 color : COLOR; float2 texcoord : TEXCOORD0; UNITY_VERTEX_INPUT_INSTANCE_ID }; struct v2f { float4 vertex : SV_POSITION; fixed4 color : COLOR; float2 texcoord : TEXCOORD0; float4 worldPosition : TEXCOORD1; float3 worldSpaceViewDir : NORMAL; UNITY_VERTEX_OUTPUT_STEREO }; fixed4 _Color; fixed4 _TextureSampleAdd; float4 _ClipRect; v2f vert(appdata_t IN) { v2f OUT; UNITY_SETUP_INSTANCE_ID(IN); UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT); OUT.worldPosition = IN.vertex; OUT.vertex = UnityObjectToClipPos(OUT.worldPosition); OUT.texcoord = IN.texcoord; OUT.worldSpaceViewDir = UnityObjectToViewPos(OUT.worldPosition); OUT.color = IN.color * _Color; return OUT; } sampler2D _MainTex; sampler2D _NewTex; float _Cutoff; fixed4 frag(v2f IN) : SV_Target { half4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color; color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect); //Here we multiply alpha by the valu from color.a*=(step(_Cutoff, tex2D(_NewTex, IN.texcoord).r)); return color; } ENDCG } } } |
Step 4. Keeping track of the current letter
After the following steps, we are able to get this:
The letters write themselves just fine, but there is no way yet to tell the shader which letter should be animated, and which one is already written.
My first idea was to pass to the shader the coordinates of the current letter in world space view. That proven unsuccessful, as the letters may overlap.
Another idea is to use color variable associated with each vertex of the final text mesh. Normally, each vertex gets the same color as the color of the text. The code below colors the vertices of a text mesh accordingly, so that only the letter with index lettersShown
is colored.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
public void ColorVertices (int lettersShown) { //we color the vertices that do not belong to the current displayed letter blue; UIVertex tempVertex; for (int i = Mathf.Max ((lettersShown - 1) * 4, 0); i < lettersShown * 4; i++) { tempVertex = text.cachedTextGenerator.verts [i]; tempVertex.color = Color.blue; text.cachedTextGenerator.verts [i] = tempVertex; } //we color the vertices that belong to the current displayed letter green for (int i = lettersShown * 4; i < lettersShown * 4 + 4; i++) { tempVertex = text.cachedTextGenerator.verts [i]; tempVertex.color = Color.green; text.cachedTextGenerator.verts [i] = tempVertex; } //we color the vertices that come after the current displayed letter red for (int i = (lettersShown + 1) * 4; i < text.cachedTextGenerator.verts.Count; i++) { tempVertex = text.cachedTextGenerator.verts [i]; tempVertex.color = Color.red; text.cachedTextGenerator.verts [i] = tempVertex; } //needed to pass this information on text.SetVerticesDirty (); } |
Step 5. Final modifications of the shader.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
Shader "Custom/Unlit/HandwritingTextShaderv2" { Properties { [PerRendererData] _MainTex ("Font Texture", 2D) = "white" {} _NewTex("Mask Font Texture", 2D) = "white" {} _Color ("Tint", Color) = (1,1,1,1) _Cutoff("Alpha cutoff", Range(0.0, 1.0))=0.5 _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 //If we do not do vertex coloring, we stick to previous version of the shader [MaterialToggle] _IsAnimating("Is animating", Float) = 0 [Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0 } 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] Blend SrcAlpha OneMinusSrcAlpha ColorMask [_ColorMask] Pass { Name "Default" CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma target 2.0 #include "UnityCG.cginc" #include "UnityUI.cginc" #pragma multi_compile __ UNITY_UI_ALPHACLIP struct appdata_t { float4 vertex : POSITION; float4 color : COLOR; float2 texcoord : TEXCOORD0; UNITY_VERTEX_INPUT_INSTANCE_ID }; struct v2f { float4 vertex : SV_POSITION; fixed4 color : COLOR; float2 texcoord : TEXCOORD0; float4 worldPosition : TEXCOORD1; float3 worldSpaceViewDir : NORMAL; UNITY_VERTEX_OUTPUT_STEREO }; fixed4 _Color; fixed4 _TextureSampleAdd; float4 _ClipRect; v2f vert(appdata_t IN) { v2f OUT; UNITY_SETUP_INSTANCE_ID(IN); UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT); OUT.worldPosition = IN.vertex; OUT.vertex = UnityObjectToClipPos(OUT.worldPosition); OUT.texcoord = IN.texcoord; OUT.worldSpaceViewDir = UnityObjectToViewPos(OUT.worldPosition); //Now _Color variable carries the actual color of a text, IN.color OUT.color = IN.color;// * _Color; return OUT; } sampler2D _MainTex; sampler2D _NewTex; float _Cutoff; float _IsAnimating; fixed4 frag(v2f IN) : SV_Target { half4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * _Color; color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect); if(_IsAnimating>0){ color.a*=(1-IN.color.r); color.a*=(step(_Cutoff, tex2D(_NewTex, IN.texcoord).r)*IN.color.g+IN.color.b); } else{ color*=IN.color; color.a*=(step(_Cutoff, tex2D(_NewTex, IN.texcoord).r)); } return color; } ENDCG } } } |
Step 6. Animation coroutine
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
IEnumerator WriteTextOut () { //We put color of a text on the material, for further use; text.material.SetColor ("_Color", text.color); yield return new WaitForSeconds (0.5f); int lettersShown = 0; text.material.SetFloat ("_IsAnimating", 1); while (lettersShown < text.text.Length) { ColorVertices (lettersShown); //We do not want to waste time on spaces if (!char.IsWhiteSpace (text.text [lettersShown])) { float cutoff = 1; while (cutoff > 0) { cutoff = Mathf.Max (0, cutoff - Time.deltaTime * speed); text.material.SetFloat ("_Cutoff", cutoff); yield return new WaitForEndOfFrame (); } } lettersShown++; } text.material.SetFloat ("_IsAnimating", 0); } |