Unityにおける文字の描画と比較検証

はじめに

こんにちは、サイバーエージェントのゲーム事業部で、新規開発プロジェクトのUnityエンジニアをしている杉浦です。

Unityにおいて、文字を描画するだけでもいくつかの方法が挙げられます。

本記事では、文字の各描画方法の説明とそれらの比較検証を行った結果を示し、Unityにおいて文字の描画をする場合に、さまざまなシチュエーションで最適な方法を選択できることを目指します。

また、今回の検証は次のような環境で行いました。

  • Unity 2018.3.6f1
  • TextMeshPro 1.4.0

※本記事は2019年4月14日に開催された技術書典6にて販売した「UniTips Vol.3」からの転載となります。

ビットマップフォントとダイナミックフォント

まずは、文字を描画するために必要なデータであるフォントについて説明します。

Unityにおいてフォントは、ビットマップフォントとダイナミックフォントの2種類があります。

ビットマップフォント

ビットマップフォントは、描画したい文字をあらかじめテクスチャに書き込んでおき、実際に文字を描画する際にはそのテクスチャを参照することで描画を行います。ビットマップフォントによって文字を描画する場合のフローをより詳細に表したものが以下の図です。

上の図のとおり、基本的には各文字に対して板ポリゴンを作成し、そこに描画したい文字のテクスチャを貼ることで文字を表示します。

メリット

  • フォントファイル(ttf/otf)をアプリに含める必要がなくなる
  • あらかじめテクスチャを用意するので動作は速い
  • テクスチャさえ用意すれば凝った文字表現ができる

デメリット

  • あらかじめ用意した文字以外を描画することができない
  • 文字の種類が多く必要な場合はテクスチャの容量が大きくなりメモリを圧迫する
  • 拡大に弱い(後述するSDFを用いた場合は除く)

ダイナミックフォント

ダイナミックフォントは、文字を描画したいタイミングでttfファイルやotfファイルから文字データを取得し、描画したい文字を動的にテクスチャに書き込んでから、 そのテクスチャを参照することで文字の描画を行います。ダイナミックフォントによって文字を描画する場合のフローをより詳細に表したものが以下の図です。

上の図のとおり、ビットマップフォントの場合はあらかじめ文字データをテクスチャに書き込んでいたところが、ダイナミックフォントの場合はフォントデータからデータを取得し、動的にテクスチャへと書き込んでいます。

メリット

  • フォントデータ(ttf/otf)に入っている文字ならば描画できる
  • 描画したい文字の種類が多い場合も常に文字のための巨大なテクスチャをメモリに置いておかなくて済む
  • あらかじめテクスチャを書き出す手間がない
  • フォントデータから描画するのでさまざまな大きさの文字描画がきれいにできる

デメリット

  • 実行時にテクスチャに文字を書き込む分の時間がかかる
  • フォントデータ分は常にメモリを圧迫する

uGUI TextとTextMeshPro

次に、Unityにおいて文字を描画する際に使われるコンポーネントについて説明します。

これらのコンポーネントは、さきほど説明したビットマップフォントやダイナミックフォントを用いることで文字の描画を行いますが、データの扱いや実装の違いがあります。

uGUI Text

uGUI Textは、Unity5よりも以前からある、文字を描画するためのUnity標準のコンポーネントです。任意のフォントで文字を描画するための手順は次のようになります。

  1. Assets配下に文字の描画に使用したいフォントファイル(ttf/otf)をインポート
  2. Hierarchy上で右クリックし、UI > Textを選択しTextコンポーネントのアタッチされたオブジェクト作成
  3. 1でインポートしたフォントファイルをInspector上のFontに入れる

uGUI Textの特徴

知っておくべきuGUI Textの特徴としては、ダイナミックフォントとして使用した際にテクスチャに書き出される文字がサイズやスタイルによって別でテクスチャに書き出されるということが挙げられます。

以下の図のように、Fontサイズが小さいものと大きいものや、スタイルが通常のものとイタリック体のものではそれぞれ別の文字としてテクスチャに書き出されています。そのため使っている文字の種類数が少ない場合でも、スタイルやサイズが異なっていれば想像以上にフォントテクスチャが大きくなってしまうため注意が必要です。

また、uGUI Textには標準でアウトラインやシャドウをつけるためのコンポーネントも用意されています。これらはBaseMeshEffectというクラスを継承し、ModifyMeshというメソッドをオーバーライドし、文字描画に使用するメッシュを直接操作する実装になっています。

実際のShadowコンポーネントやOutlineコンポーネントの実装は、Unityの[Bitbucketリポジトリ]にて公開されているため詳細なコードは省きますが、大まかな実装方法を説明するとShadowコンポーネントの場合、

  1. 文字の頂点すべてを複製する
  2. 複製した頂点の位置を少しずつずらす
  3. ずらした頂点色を半透明にする

上記のような流れでShadowを実現しています。そのため、Shadowをつけた一文字を描画するために2回の描画を行う必要があります。OutlineもShadowとほぼ同様に各方向に描画をずらすことで実現しており、こちらは一文字を描画するために5回の描画を行っています。

それぞれShadowとOutlineの描画とそのWireframe表示を行ったものが以下の図です。

Wireframeの図を見ると、さきほど説明したとおり、ひとつの文字に対して複数の板ポリゴンが使用されていることが分かると思います。

uGUI Textにおけるビットマップフォントとダイナミックフォント

uGUI Textはデフォルトの設定ではダイナミックフォントを使用するようになっていますが、ビットマップフォントを使用することもできます。ビットマップフォントを使用したい場合はフォントデータを選択し、以下の図のCharacterの部分がDynamicになっているものを使用したい文字に合わせて変更します。

次の図は、CharacterをASCII default setに設定した場合に書き出されたテクスチャです。

Fontファイルの子にあるFontTextureに、ASCII文字がテクスチャとして書き出されていることがわかります。同様に、CharacterをUnicodeに設定した場合にはフォントデータに存在するUnicodeの文字がテクスチャに書き出されます。

TextMeshPro

TextMeshProは昔からよく使用されている文字を描画するための外部アセットで、Unity2017の頃に有償だったものが無償となり、Unity2018.2で標準でUnityプロジェクト内に含まれるようになりました。Unity2018.2以降では、TextMeshProはPackageとして追加されているので、UpdateなどはPackageManagerから行うことができます。

TextMeshProで任意のフォントで文字描画するための手順は次のようになります。

  1. Window > TextMeshPro > Font Asset Creatorを開く
  2. FontAssetを作成したいFontを選択する
  3. ChracterSetなどの設定をし、GenerateFontAtlasで書き出す
  4. TextMeshProのコンポーネントに書き出したFontAssetを設定する

実際のFontAssetCreatorのウィンドウが、次の図です。

大まかに設定値の説明をすると、

SourceFontFile

FontAssetを作成するためのフォントデータを指定する

SamplingPointSize

1文字のサイズ

Padding

文字と文字の間の余白で、小さすぎるとアウトラインなどがうまく出せなくなるので注意

PackingMethod

FontAssetを作成する際のパッキング設定で、基本はFastで製品版のときのみOptimumでよい

Atlas Resolution

作成するテクスチャのサイズで、入れたい文字が多く、SamplingPointSizeが大きいほど大きめのサイズが必要

Character Set

テクスチャに含めておく文字のセットで、基本的にはここにある文字しか描画できない

Render Mode

文字をレンダリングする方法

Get Kerning Pairs

フォントデータにカーニングの情報が含まれていたら取得するかどうか

上記で設定した値を元にFontAssetを作成し、TextMeshProではそれを利用して文字の描画を行います。

TextMeshProの特徴

TextMeshProの特徴としてもっとも重要なのが、Signed Distance Fieldと呼ばれる手法を用いて文字をテクスチャに書き出していることです。これは、文字の中心からの距離をテクスチャに書き込みます。

TextMeshProを用いて文字をテクスチャに書き出したものが次の図です。全体的に文字の境界がぼやけており、文字の中心から距離が離れるにつれて色が黒になっていることが分かると思います。

TextMeshProのシェーダー側で、このテクスチャを元に描画したい文字の距離の境界値を決めて、内側なら色を塗る、外側ならピクセルを破棄することで文字の描画を行います。

TextMeshProでは、SDFテクスチャを用いることで文字の拡大縮小に強く、さまざまなスタイルの描画にも対応できるように考えられています。たとえば、文字のOutlineを取りたい場合は、SDFテクスチャで距離が一定以上離れている点のみ色を塗ることで実現が可能です。

また、文字をBold体にしたい場合は通常の文字よりも遠い距離まで色を塗ることで実現が可能です。このように、uGUI Textに比べて、SDFテクスチャとシェーダ側の処理でさまざまな表現を実現できることがTextMeshProの強みです。

また、TextMeshProコンポーネント自体の機能も充実しており、テキストのカーニングの設定や文字のページ送りなども簡単に実装できます。

TextMeshProにおけるビットマップフォントとダイナミックフォント

Unity2018.3のTextMeshPro 1.4.0より前までは、TextMeshProはビットマップフォントにしか対応していませんでした。

これは、SDFテクスチャの作成にある程度時間がかかってしまうことが原因でしたが、TextMeshPro 1.4.0からはSDF AAというRenderModeが追加され、このRenderModeであれば動的にSDFテクスチャを作成しても十分な速度が出るということで、ダイナミックフォントも利用できるようになりました。

TextMeshProでダイナミックフォントを利用するには、次のような手順で設定を行います。

  1. さきほど説明した方法でFontAssetを作成する
  2. 作られたFontAssetを選択し、Generation Settingsにダイナミックフォントとして利用したいFontを設定
  3. Atlas Population ModeをDynamicに設定

実際に上記の設定を行ったものが以下の図です。

これにより、TextMeshProにおいてもダイナミックフォントを利用することができます。

補足ですが、FontAssetを作成する際に設定したCharacterSetに基づいてテクスチャに書き出された文字に関しては、Dynamicを選択してその設定を適用された時点でテクスチャはリセットされるため、FontAssetのCharacterSetはどれを選んでいても構いません。

また、TextMeshProでは、Fallback FontAssetと呼ばれるものを設定することができます。

Fallback FontAssetは、あるFontAssetに存在しない文字が指定された場合に、代わりに別のFontAssetから描画すべき文字を探して表示を行うことができます。

実際にFallback FontAssetを用いる例としては、次の図のようにASCII文字のみを書き出したFontAssetとダイナミックフォントとして設定を行ったFontAssetがあるとします。

そのとき、ASCII文字のみを書き出したFontAssetのFallback FontAssetに、ダイナミックフォントとして設定を行ったFontAssetを設定すれば、ASCII文字ならすでに書き出されているテクスチャから、それ以外の文字ならダイナミックフォントとして動的に描画といったことが可能となります。

uGUI TextとTextMeshProの比較検証

描画できる最大文字数

uGUI Textコンポーネントは、ひとつのコンポーネントで描画できる最大文字数に制限があります。UnityにおいてひとつのMeshが持てる最大頂点数が65,000と決まっているため、Textコンポーネントもその制限を受けます。そのため、通常4頂点で1文字を描画しているTextコンポーネントにおいて描画できる最大文字数は16,250文字となります。

また、さきほど説明をしたOutlineコンポーネントやShadowコンポーネントをアタッチする場合は、一文字あたりの描画に使う頂点数が増えるため、描画できる文字数はさらに減ります。実際に動作を確かめるため、以下のように任意の文字数の文字列を生成するスクリプトを作成しました。

using System.Text;
using UnityEngine;
using UnityEngine.UI;
using Random = UnityEngine.Random;

public class FontTextureTest : MonoBehaviour
{
    private static readonly int kGenerateTextSize = 16250;
    [SerializeField]
    private Text _text;
    
    private void Start()
    {
        string data = "abcdefghijklmnopqrstuvwxyz";
        
        StringBuilder builder = new StringBuilder();
        
        for (int i = 0; i < kGenerateTextSize; ++i)
        {
            builder.Append(data[Random.Range(0, data.Length)]);
        }

        _text.text = builder.ToString();
    }

}

実行した結果が以下です。

一方で、TextMeshProの場合は、ひとつのMeshが持ちうる最大の頂点数を超えそうになったタイミングで別のMeshを生成し、そちらに続けて文字描画を行ってくれるため最大数を気にすることなく文字描画を行うことができます。以下の図がTextMeshProを用いて30000文字を描画したときの状態をFrameDebuggerで確認したものです。説明したとおり、最大頂点数を超えそうになったところで、次のMeshを生成していることがわかります。

FontTextureがいっぱいになったとき

ダイナミックフォントでたくさんの種類の文字を使用していると、あらかじめ用意されていたFontTextureのサイズでは文字を書き込むスペースが足りなくなってしまうことがあります。

uGUI Textの場合、基本的には勝手にリビルドされてテクスチャサイズを大きくした別のテクスチャを用意されます。その際に、既存の使われている文字は新しいテクスチャの方に勝手に書き込んでくれるため、あまり用意されているFontTextureのサイズ自体を気にすることはないかもしれません。

ただし、別のテクスチャを用意してそちらに既存の文字すべてを再度書き込むという処理は負荷が大きく、すでに大きなサイズのFontTextureを使用していた場合にはスパイクが発生する場合もあります。

そのため、むやみやたらにいろんな種類の文字を使わずに、また使用する場合にはテクスチャがリビルドされるタイミングを意識した実装を行う必要があります。たとえば、Fontクラスに用意されているFont.RequestCharactersInTextureというメソッドは、FontTextureに文字を追加するリクエストを行うことができるため、使用する文字はロードのタイミングであらかじめFontTextureに書き込んでおくということもできます。

以下のスクリプトは、Textで文字描画を順次行い、その際のFontTextureのリビルドタイミングを調べるスクリプトです。

using System;
using System.Text;
using UnityEngine;
using UnityEngine.UI;

public class FontTextureTest : MonoBehaviour
{
    [SerializeField]
    private Text _text;

    private static readonly int kBaseUnicode = 32;
    private static readonly int kMaxLoopCount = 10000;

    private void Awake()
    {
        Font.textureRebuilt += Rebuild;
    }
    
    private void Start()
    {
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < kMaxLoopCount; ++i)
        {
            builder.Append(Convert.ToChar(kBaseUnicode + i));
            _text.text = builder.ToString();
        }
    }

    private void Rebuild(Font font)
    {
        Debug.LogWarning(
            $"FontTexture width: {font.material.mainTexture.width},
             height: {font.material.mainTexture.height}");
    }
}

これを実行すると、あらかじめ用意されたFontTextureから文字が入り切らなくなったときには、以下の図のように新たなFontTexutreが作成されていることがわかります。

uGUI Textの場合にも、FontTextureはどこまでも大きくなるわけではなく、最大サイズが4096×4096と決められています。そのため、FontTextureが4096×4096でも入り切らなくなってしまった場合には、以下の図のようなエラーが発生してしまうため注意が必要です。

TextMeshProの場合、uGUI Textとは違いFontTextureのサイズを動的に拡大するようなことはしません。そのため、FontTextureに入り切らなくなってしまった文字に関しては、フォントデータが見つからなかったときと同様に四角形が表示されてしまいます。対策としては、FontTextureのサイズをあらかじめ非常に大きくとっておくことが挙げられますが、こちらは常にメモリを専有するため注意が必要です。

そのため、基本的にはビットマップフォントでよく使う文字を書き出しておき、さきほど紹介したFontAssetのFallback設定にFontTextureのサイズを控えめに取ったダイナミックフォントを設定したほうが、メモリにも優しく、文字が描画できないリスクも軽減できます。

おわりに

この章で説明したとおり、Unityにおいては文字を描画するだけでも気にしたほうがよいことはたくさんあります。特に、フォントファイルやFontTextureのサイズはアプリのサイズにも直結するので注意が必要です。

余談ですが、UnityのTextコンポーネントは将来的には非推奨になるようなので、今のうちからTextMeshProに慣れておくことをお勧めします。また、TextMeshProのShaderによる文字表現は面白いので、興味がある方はぜひそちらも調べてみてください!

【そのほかの記事をみる】

おすすめ記事