テキストのアウトラインを何とかする
Unity で文字を表示するとき,これに装飾を加えたいと思う場合があります。その中でもアウトライン(縁取り・輪郭線)はよく使われる表現です。特にコントラストの高い背景に重ねて表示する場合,文字が単色だと部分的に背景に溶けてしまう場合があり,それを避ける方法としてアウトラインは手頃な方法です。
https://gyazo.com/949b41d5c62376fc504d2cdf662f167a
しかし,任意の日本語テキストにアウトラインを付けようと思うと案外うまく行かない場合があります。この記事ではその解決方法のひとつとして,専用のカスタムシェーダーを使う方法を紹介します。
従来の方法
Unity で文字にアウトラインを付ける方法は数々あります。Unity 標準で(追加コストなしで)実現できる方法としては以下のようなものが考えられるでしょう。
あらかじめテクスチャを用意
NGUI(フリー版)/uGUIのアウトライン機能を使う
TextMeshProを使う
しかしそれぞれ一長一短があります。
テクスチャで用意
表示すべき文字が最初から決まっている場合,文字組みごと画像で用意しておけば簡単です。アウトラインに限らず,どんなリッチな修飾でもできます。
反面,表示すべき文字があらかじめ決まっていない場合には使えません。また,表示すべき文字が多い場合,テクスチャの容量が増えてメモリ・ストレージを圧迫するという問題もあります。i18nを考えてデータを管理するとなるとその煩雑さも課題です。
NGUI/uGUI のアウトライン機能を使う
Unity で GUI を簡単に組み立てることのできるアセットとして古くから圧倒的な支持を集めてきた NGUI と,それを基本として Unity に内蔵された uGUI には,Unity のダイナミックフォントアセットにアウトラインを修飾して描画する機能があります。任意のフォントに含まれる任意の文字の動的な表示に対応し,メモリ・ストレージのフットプリントも最小。一見,これで何の問題も無いように思われます。
しかし,これには1つ重大な問題点があります。それはアウトライン描画が「汚い」そして「重い」という問題です。その原因はアウトラインの描画方法にあります。
https://gyazo.com/91eb5224cdfe03c837282d81c63cd92c
NGUI(uGUI)によるOutline
「汚い』件は上の画像を見てもらえれば分かる通り,文字の角のアウトラインが不自然に途切れるものです。文字の線の幅が1ドット程度の,ごく小さく描画する場合には気にならない場合もあるのですが,少しでも大きく表示しようとすると気になります。この現象の原因はアウトラインの描画の仕組みにあります。
https://gyazo.com/82d7c5e81c358964333ae3602f897e9c
NGUI(uGUI)の描画メッシュ
NGUI(uGUI) のアウトラインがどのように描画されているかはメッシュを表示してみれば一目瞭然です。上下左右にオフセットした文字を,影の色で重ね書きしているのです。これでは「ずらしていない方向」のアウトラインが描画されず途切れるのも当然です。また,このように透明度付きのテクスチャを何度も同じ領域に重ね書きするのは,GPU のピクセル打ち込みレートを無駄遣いする「重い」処理であるのは賢明な読者諸氏にとっては常識であろうと思います。
また,uGUI のアウトラインコンポーネントは CanvasRenderer を使うUIコンポーネントにしか使えません。3Dに混在させるには TextMesh コンポーネントを使用する必要がありますが,これにはアウトラインコンポーネントは適用できないことも,頭の痛い問題です。
TextMeshProを使う
比較的貧弱なNGUI/uGUIのテキスト描画機能を補うものとして,近年急速に注目を集め,Unityに統合されることが決定している TextMeshPro を使う方法があります。
TextMeshPro は Unity の標準フォントアセットに頼らず,独自の事前計算SDF(Signed Distance Field)テクスチャベースの描画機構を持ち,シャープなエッジを保ったままスムーズに拡大表示できます。また,アウトラインに留まらず非常に多彩な修飾を高い効率で描画できます。MeshRenderer / CanvasRenderer 両方に対応しているのも便利です。 良いこと尽くめに見えるTextMeshPro。実際多くの場面ではこれで充分であると考えられます。しかし,これにも1つ重大な弱点があります。それは我々日本人が属するCJK文字文化圏にとっては避けられない課題,肥大化するアトラスです。
先にも述べましたがTextMeshProはその原理上,SDFテクスチャを要します。そしてこのテクスチャは動的生成に対応していません。SDFテクスチャの生成処理は計算量が多く,将来的にも動的生成は望み薄でしょう。その結果として,日本語などの文字種の多いフォントの運用は大変困難なのです。
このあたりの事情は,ダイナミックフォントがまだ実装されていなかった昔の Unity を知っている読者氏には良くご理解いただけることと思います。具体的には,標準的な日本語フォントに含まれる約6,000~8,000種の文字を実用的な解像度で納めようとすると2048✕2048ドットのテクスチャには収まりません。対象の文字種を,常用漢字程度(約3,000種)に絞れば,2048✕2048ドットに何とか収まりますが拡大には耐えませんし画数の多い漢字は細部が潰れたりします。また何より文字種を絞ると運用が難しくなりますし表現の幅を狭めます。4096✕4096ドットのテクスチャを難なく扱えるプラットフォーム(PCや据え置きコンソール)では問題になりにくいのですが,モバイル機器がターゲットだと躊躇されます。
アウトラインシェーダー
鑑みるに,世間の方向性としては TextMeshProを使え で固まっていると言えます。ラテン文字圏では何ら問題ありませんし,肥大化するアトラスも技術の進歩により徐々に解決していくでしょう。しかし,どうしても貧弱なプラットフォームに対応しなければらならいといった課題がある場合,現時点の既存技術の範囲では手に余ります。前述の通りラテン文字圏では問題が存在しないため,欧米の開発者に期待することはできません。我々CJK圏の者が何とかする必要があるのです。
何とかする方法はまあ幾つか考えられるのですが,そのひとつ,比較的簡単に実装できるものとしてフォントの画面への描画時に同時にアウトラインも描画するシェーダーを紹介しようと思います。
https://gyazo.com/18fac5c5b649db268694330a9de64f45
このシェーダーによるアウトライン
uGUIのものよりは綺麗に描画されていると思います。
https://gyazo.com/ed146c69ecf81e56974a11ed2e7564a8
一発描きなのでオーバードローも最小
以下がそのシェーダーコードです。
Unity内蔵の GUI/Text Shader シェーダー(DefaultResources/Font.shader)を一部改造したものです。
解説のため,オリジナルのシェーダーバリアント(VR対応とか)を削っています。必要な方は適宜元に戻してください。
code:GUI-Text-Outline.shader(c)
Shader "GUI/Text-Outline"
{
Properties {
_MainTex ("Font Texture", 2D) = "white" {}
HDR _Color ("Text Color", Color) = (1,1,1,1) HDR _OutlineColor ("Outline Color", Color) = (0,0,0,1) _OutlineSpread ("Outline Spread", Range(0.1, 10)) = 1
}
SubShader {
Tags {
"Queue" = "Transparent"
"IgnoreProjector" = "True"
"RenderType" = "Transparent"
"PreviewType" = "Plane"
}
Lighting Off Cull Off ZTest Always ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
Pass {
CGPROGRAM
#pragma shader_feature AUTO_OUTLINE_COLOR struct appdata_t {
float4 vertex : POSITION;
half4 color : COLOR;
float2 uv : TEXCOORD0;
};
struct v2f {
float4 vertex : SV_POSITION;
half4 color : COLOR;
float2 uv : TEXCOORD0;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float4 _MainTex_TexelSize;
half4 _Color;
half4 _OutlineColor;
half _OutlineSpread;
v2f vert (appdata_t v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.color = v.color * _Color;
o.uv = TRANSFORM_TEX(v.uv,_MainTex);
return o;
}
half4 frag (v2f i) : SV_Target
{
half4 col = i.color;
half4 outc = abs(col - half4(1,1,1,0));
half4 outc = _OutlineColor;
half a0 = tex2D(_MainTex, i.uv).a;
col = lerp(outc, col, a0);
float4 delta = float4(1, 1, 0,-1) * _MainTex_TexelSize.xyxy * _OutlineSpread;
half a1 = max(max(tex2D(_MainTex, i.uv + delta.xz).a,
tex2D(_MainTex, i.uv - delta.xz).a),
max(tex2D(_MainTex, i.uv + delta.zy).a,
tex2D(_MainTex, i.uv - delta.zy).a));
delta *= 0.7071;
half a2 = max(max(tex2D(_MainTex, i.uv + delta.xy).a,
tex2D(_MainTex, i.uv - delta.xy).a),
max(tex2D(_MainTex, i.uv + delta.xw).a,
tex2D(_MainTex, i.uv - delta.xw).a));
half aa = max(a0, max(a1, a2));
col.a *= aa;
return col;
}
ENDCG
}
}
}
やっていることは至極単純で,下図の通り周囲のテクセルをサンプリングし,目的のピクセルがアウトライン領域であるかどうかを判別して塗り分けているだけです。
https://gyazo.com/ed373aaa9d15bcb9f32e2cc3ac4bf37f
後はこれを UI.Text なり TextMesh なりのマテリアルに設定すればOKです…と言いたいところなのですが…実は問題があり,それだけでは使えません。
キャラクタパディングの設定
上記のシェーダーをそのまま適用すると,下図のようになってしまいます。
https://gyazo.com/a5c081ef343d57d5d4cd3c520ed7b8cc
あれれ,文字の境界部分が何かヘンです。
これはUnityが生成するフォントテクスチャ(ダイナミックフォントのキャッシュ)にアウトライン分の余裕が無く隣の文字が見切れてしまっていて,なおかつ文字を描画しているメッシュ自体もアウトラインの幅を考慮しないギリギリの大きさしかないためです。
ではどうすれば良いでしょうか。
解決方法は2通りあります。ひとつはアウトラインを文字の外側に描画するのをあきらめて,内側に描画するという手法です。しかしこれに適したフォントを用意するのが容易ではなく,また文字自体の輪郭がぼやけてしまうという弱点があります。
もうひとつの解決方法は,Unityのフォントテクスチャにアウトライン分の余裕を持たせる方法です。これには UnityEngine.Fontクラスの非公開プロパティ characterPadding を調整します。非公開プロパティを変更する方法は,正攻法であればカスタムインスペクタもしくはカスタムインポーターを書くのですが,ここではもっと簡便な方法を紹介します。それはフォントアセットのmetaファイルを直接編集することです。
code:myfont.otf.meta(yaml)
fileFormatVersion: 2
guid: xx8eeb07be4c1dd4daf3bf0c2026fbbb
timeCreated: 1517964811
licenseType: Pro
TrueTypeFontImporter:
externalObjects: {}
serializedVersion: 4
fontSize: 48
forceTextureCase: -2
characterSpacing: 0
characterPadding: 4 ←ここを変更!
includeFontData: 1
fontName: MyFont
fontNames:
- MyFont
fallbackFontReferences: []
customCharacters:
fontRenderingMode: 0
ascentCalculationMode: 1
useLegacyBoundsCalculation: 0
userData:
assetBundleName:
assetBundleVariant:
こうすればフォントテクスチャ上で文字の周囲に空白が入り,当初の目的を果たすことができます。ぜひお試しください。
ドロップシャドウ
本稿を応用すると,文字の特定の方向のみに影を描画するドロップシャドウを実現することができます。
これは一見簡単に思えますが多少の工夫が必要になります。そのあたりを次回解説します。
2018/4/24