Unity 高速化Tips 軽量なGUIテキストのアウトライン処理

# Unity標準のGUI向けText表示のOutlineは遅いです

UnityのText表示はお手軽で簡単、1文字が2ポリゴンになり1文が一つのMeshになるからそこそこ高速です。
ところが、標準のテキストOutlineエフェクトは、手軽に使える反面、速度が遅く、クオリティも今一つと感じている方も多いかと思います。
アセットストアには軽量で綺麗なアウトラインエフェクトのアセットがありますが、私がゲーム中に使っている手軽で高速なアウトライン表示のシェーダーを紹介したいと思います。

---

# Unity標準のOutline

標準のOutlineは、同じ文字を黒色で上下左右に4回書いて、その上から通常の色でテキストを書いています。

<img width="48%" alt="Outline_00.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51521/564dadec-20e8-efbd-678c-1568bdd1e23d.png">
<img width="48%" alt="Outline_01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51521/c7976d58-3173-2f9b-19bc-32d40bf8f45b.png">

左がOutline無し、右がUnity標準のOutlineになります。ポリゴンの数が、2.9Kから14.3Kと5倍に増えていることが分かると思います。拡大してみると、アウトラインの部分に欠けている部分があり、あまり綺麗ではありません。

レンダリングコストが5倍になったうえ、あまり綺麗でもないので使う気が無くなります。ゲームではパフォーマンスは大切ですから、ポリゴンが5倍になるというのは許容し難いです。

---

# 軽量版Outlineシェーダー

今回紹介する軽量版Outlineシェーダーの原理は簡単です。テキスト表示を行うとき、輪郭を滑らかに見せるために、ピクセルを半透明でレンダリングするアンチエイリアス処理がされます。上の左のOutline無しの画像では輪郭がぼやけて見えると思います。この、半透明の部分をアウトライン色に置き換えることで、文字の輪郭に別の色を付けることができるのです。

<img width="48%" alt="Outline_02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51521/16068169-a5db-eae2-e365-3b5212b16773.png">
<img width="43%" alt="OutlineDemo.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51521/75dfb80c-28b7-1c87-9857-40ebb1a89413.gif">

ポリゴン数は2.9Kのまま。レンダリングのコストをほとんど変えずに、黒い輪郭を描くことができました。しかも、標準のOutline処理よりも、多少滑らかに縁取りされていることが分かると思います。

ただし、欠点もあります。半透明の「ボケ」を利用している原理上、フォントのサイズや倍率に影響を受けてしまうため、右の動画のようにCutOffの値を見栄えが良くなるよう調整する必要があります。少々面倒ですが、ゲームの場合は見栄えを細かく調整する作業が多いので、それほど苦にはなりませんでした。

また、元のフォントが詳細すぎると半透明の部分が少なくなり、アウトライン部分が目立ちません。粗目の小さいフォントサイズ指定し、それを拡大して表示したほうが綺麗にアウトラインがかかります。いままで見た目を気にして大きめのフォントを使う事がありましたが、小さいフォントサイズでも見た目が良好になり、レンダリングも高速になるので一石二鳥です。

# シェーダーコード

シェーダ―のソースコードを公開します。これを、Materialにアタッチして、TextコンポーネントのMaterialにセットすれば完了です。
是非お試しください。

```HLSL: 軽量アウトラインシェーダー
Shader "FS/FSOutlineText" {
	Properties {
		[PerRendererData] _MainTex("Sprite Texture", 2D) = "white" {}
		_Cutoff("Alpha Cutoff", Range(0, 1)) = 0.5
		_BlackCut("Outline Cutoff", Range(0, 1)) = 0.0
		_OutlineColor("Outline Color", Color) = (0,0,0,1)
		[Enum(Off, 0, On, 1)] _ZWrite("ZWrite", Float) = 1
	}

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

		ZWrite[_ZWrite]
		Blend SrcAlpha OneMinusSrcAlpha

		Pass {
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#pragma target 3.0

			#include "UnityCG.cginc"

			struct appdata_t {
				float4 vertex : POSITION;
				float2 texcoord : TEXCOORD0;
				fixed4 color : COLOR;
				UNITY_VERTEX_INPUT_INSTANCE_ID
			};

			struct v2f {
				float4 vertex : SV_POSITION;
				half2 texcoord : TEXCOORD0;
				fixed4 color : COLOR;
				UNITY_VERTEX_OUTPUT_STEREO
			};

			sampler2D _MainTex;
			float4 _MainTex_ST;
			half _Cutoff;
			half _BlackCut;
			fixed4 _OutlineColor;

			v2f vert(appdata_t v)
			{
				v2f o;
				UNITY_SETUP_INSTANCE_ID(v);
				UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);

				o.vertex = UnityObjectToClipPos(v.vertex);
				o.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);
				o.color = v.color;
				return o;
			}

			fixed4 frag(v2f i) : SV_Target
			{
				half alpha = tex2D(_MainTex, i.texcoord).a;
                clip(alpha - _Cutoff);
				half inner = step(_BlackCut, alpha);
				return fixed4(i.color.rgb * inner + 
							  _OutlineColor.rgb * (1-inner), 
 						      i.color.a);
			}

			ENDCG
		}
	}
}
```