土屋つかさの技術ブログは今か無しか

土屋つかさが主にプログラミングについて語るブログです。

#unity TextMeshPro内の低レイヤ処理クラスFontEngineの隠しメソッドにリフレクションでアクセスする(TextMeshPro Deep Dive #3)

 この記事はUnityアドベントカレンダー2024(シリーズ3)12日目の記事です。空いてたので埋めさせていただきました。

qiita.com

 今回の記事はこちらのエントリの内容をベースにした物になります。

zenn.dev

TextMeshProの内部処理

 TextMeshPro(以下TMP)は、Unityの標準文字描画ライブラリです。以前は独立したパッケージでしたが、Unity2023.2からuguiパッケージの一部になりました。また将来的にはUnity本体のTextCoreに取り込まれる予定のようです。詳しくは下記を参照。

コラム:TextMeshProの落とし穴

madnesslabo.net

 さて、TMPは大きくわけて以下の2つの機能から構成されています。

  • A:フォントファイル(.ttf)を元にTMP_FontAssetファイルを生成する
  • B:TMP_FontAssetファイルを読み込んで任意の文字を画面に描画する

 基本的に、Aは開発時、Bはゲーム実行時の処理になります*1

 今回はAについての話です。

FontEngineクラス

 TMP_FontAssetは、収録した文字についての情報と、その文字を描画したテクスチャを保持するアセットです。このアセットを作成するには当然フォントファイル(.ttf)を解析する必要があり、その処理はFontEngineというUnityネイティブクラスで行われています。

docs.unity3d.com

 FontEngineはUnityEngine.TextCore.LowLevel名前空間に配置されている、文字通り低レベル層での文字処理機能を提供するstaticクラスです。フォント情報(FontFace)や個々の文字情報(Glyph)の取得だけでなく、文字をテクスチャ(Texture2D)に描画する機能もあります。TMPはFontEngineを介してフォントの情報を抽出し、TMP_FontAssetを作成しています。

 FontEngineクラスは公式ドキュメント上は12個のメソッドが提供されています。しかし、それらのメソッドだけではTMP_FontAssetの生成に必要な機能が足りません。ではTMPがどうしているかというと、ドキュメントに記載されていない複数のメソッドにアクセスしているのです。これらを便宜上「隠しメソッド」と呼ぶ事にします。

 これらの隠しメソッドはinternal指定で実装されています。TMPが含まれるアセンブリはFontEngineへのinteralメソッドへの参照が許可されています*2。しかし、そうでない外部アセンブリからは隠しメソッドに直接アクセス出来ないため、リフレクションを使う必要があります。

 今回、TMP_FontAssetCreatorWindowクラスの機能を独自実装しようとしてこの問題にひっかかりまして、冒頭の記事のおかげで必要なプロクシクラスを実装して解決できました(ありがとうございます)。

FontEngineProxyクラス

ソースコード

 今回土屋が作成したプロクシクラスはこちらになります。

FontEngineProxy.cs · GitHub

解説

 ベタ書きで解説も無く恐縮ですが、コードサンプルとしてお使いください。なお、作業に必要な分のみ実装していて、FontEngineの隠しメソッドを網羅しているわけではありません。

 試しにFontEngine.RenderGlyphToTexture()にアクセスするコードは以下になります。

    //↓このintenrnal隠しメソッドにアクセスする。
    //internal static FontEngineError RenderGlyphToTexture(Glyph glyph, int padding, GlyphRenderMode renderMode, Texture2D texture)

    //隠しメソッドへの参照を格納するデリゲートを宣言
    private static Func<Glyph, int, GlyphRenderMode, Texture2D, FontEngineError> dRenderGlyphToTexture;

    //プロクシメソッド本体。引数は隠しメソッドと統一
    public static FontEngineError RenderGlyphToTexture(
        Glyph glyph,
        int padding,
        GlyphRenderMode renderMode,
        Texture2D texture)
    {
        //初アクセスの場合はデリゲートを初期化する。
        if (dRenderGlyphToTexture == null)
        {
            //メソッドへの参照を取得
            var method =
                typeof(FontEngine).GetMethod("RenderGlyphToTexture", BindingFlags.Static | BindingFlags.NonPublic);
            //デリゲートを初期化
            dRenderGlyphToTexture =
                (Func<Glyph, int, GlyphRenderMode, Texture2D, FontEngineError>)Delegate.CreateDelegate(
                    typeof(Func<List<Glyph>, int, GlyphRenderMode, Texture2D, FontEngineError>),
                    null, method);
        }
        //隠しメソッドを呼び出す
        return dRenderGlyphToTexture.Invoke(glyph, padding, renderMode, texture);
    }

 使い方は簡単でFontEngine.RenderGlyphsToTexture()にアクセスしたい場合にFontEngineProxy.RenderGlyphsToTexture()を呼ぶだけです。

 コメントアウトしてある3箇所について簡単に説明します。

 RenderGlyphsToTexture()は引数4個版と6個版が多重定義されてまして、結果的に4個版が土屋が必要なコードフローを通らず検証出来なかったのでコメントアウトしました(多分動きません)。多重定義されたメソッドをリフレクションで呼び出す場合には引数の型指定が余分に必要でして、コメントアウトしてない方のRenderGlyphsToTexture()でその指定が入っています。

 GetLigatureSubstitutionRecords()GetMarkToBaseAdjustmentRecords()については、メソッドだけでなく戻り値もFontEngine内のinternalな構造体だったため、実装方法がわからず諦めました。これって、構造体のメンバ1つずつを愚直にリフレクションで取得するしか無いのでは?(そんな面倒なことやりたくない)。まあこの辺はいいかな……。

おわりに

 ことあるごとに書いていますがTextMeshProは元々個人プロジェクトだった事もあってなのか、コードの可読性が極めて低く、クラス設計にも問題があります。また、今回のようにネイティブクラスとの強い癒着もあって拡張性にも難があります。

 そのため、土屋はTMPのコードを使わない*3、独自のUnity用文字描画フレームワークの構築を目指していて、今回はその作業の一環になります。いつかお見せできたらいいなーと思いつつ今回はここまで。

参考リンク

TextMeshProドキュメント

madnesslabo.net

 「宴」の作者様が作成したTextMeshProの素晴らしいドキュメント。TMPは日本語ドキュメントが壊滅的な中、非常に丁寧かつ実挙動を確認した上での記述で実用性も抜群。2024年土屋つかさUnityドキュメント賞を勝手に進呈したいくらい。お見事です!><

TextMeshPro Deep Dive 連載

#unity 深掘りTextMeshPro:SDFフォントデータによる文字描画実装(第1回:SDFの仕組み) - 土屋つかさの技術ブログは今か無しか

#unity SDFフォントデータによる文字描画実装(TextMeshPro Deep Dive #2) - 土屋つかさの技術ブログは今か無しか

#unity TextMeshPro内の低レイヤ処理クラスFontEngineの隠しメソッドにリフレクションでアクセスする(TextMeshPro Deep Dive #3) - 土屋つかさの技術ブログは今か無しか

#unity UIから「Save As...」ボタンを外したらUnityエディタがクラッシュするようになった話(TextMeshPro Deep Dive #4) - 土屋つかさの技術ブログは今か無しか

宣伝「Unityシェーダープログラミングの教科書シリーズ」

 土屋は同人誌「Unityシェーダープログラミングの教科書」シリーズを書いています。現在以下の5冊が出ています。

 これらの同人誌は、現在BOOTHの下記ストアにてPDF版を有料頒布しています。

s-games.booth.pm

 Unityのシェーダープログラミング関連について、世界で最も詳細な本だと自負しています。リンク先にサンプルページがあるので、是非御覧になってください。

*1:色々例外はあるけどここでは触れない

*2:多分。FontEngine側からInternalsVisibleToが通してある

*3:互換性も無い