報告。`Dy`専用のSDFFontGeneratorツールを作りました。
報告
Dy
ゲームフレームワークのフォントレンダリングに必要するテクスチャをバッチで生成してくれるツールを作りましたことをご報告申し上げます。
Dy
ゲームフレームワークはOpenGLをグラフィックAPIとして動かしておりDirectXなどで基本的に支援するフォントレンダリングが出来ません。そのため、遠回りとしてOpenGLでフォントレンダリングを支援できるようにするFreetypeを使うのが普通です。
Freetypeライブラリは文字ファイルを読み込んで各文字に該当する上の情報を返還することが出来ます。しかしFreetypeだけを使用して実時間でテクスチャを生成するのは非効率であり、性能も落ちる可能性があります。
なぜなら英語の文字またはASCIIだけなら問題ではなくなりますが、UTF-8の文字を一つ一つ生成するならテクスチャが1000個以上になるかねません。
それによって文字を画面に書き詰めることだけにしてもGPU側が大変な文字一つのテクスチャをバインディングして描画手順を踏まなければなりません。大きなロスが出てしまいます。
そして私がDy
に実装しようとしたのはただの一般テクスチャではなく、SDF(Signed Distance Field)を使用してどんなに文字が大きくなっても普通のようにぼけない文字レンダリングをしたかったです。
上の画像のように普通のテクスチャを使うとサイズより大きくレンダリングするときには外がぼけてしまいます。しかしSDFを使うと右の画像のように大きく拡大してもボケません。
そして私は開発をより速く進めるためにSDFが実装されているライブラリーを使うことにしました。
msdfgenライブラリはFreetypeライブラリをベースとしてSDFのテクスチャを実装するようにします。そしてより発展したアルゴリズムであるMSDFでレンダリングするようにもします。
しかし問題はmsdfgenは1文字ずつテクスチャを生成してくれません。そして日本語の出力に必要な必須文字であるおよそ2300文字文のファイルを生成してバッチで読ませるのは一般的な考えなら無理になります。
そしてSDFの1文字生成にかかる時間は普通にFreetypeを使ってテクスチャを作るのより5倍くらいの時間が掛かっってしまいます。
この時点で私はDy
でリアルタイムで文字のSDFテクスチャを生成したいことをやめて、バッチで大きなテクスチャに文字テクスチャを埋め込み、Text Atlasを作ってDy
からは情報やイメージバッファを読み込みすれば良いと思いました。そしてこれをやってくれるツールも作らないとならないと思いました。
なぜならずっと私一つでDy
を開発するならただのCUIでも構いませんがCUIが操作できないデザイナーがいるときを想定するならGUIじゃないとツールが役に立たないと思いましたからです。
それで私はグラフィックライブラリとしてQt
を使うことにしました。Qt
はC++言語で書かれたGUIライブラリとして数多くの企業やプロジェクトで使っています。これを用いてGUIのインタフェースとロジックを便利に実装することが出来ます。
そしてQt
はOpenGLを支援するのでOpenGLを使ってレンダリングを行ったりすることが出来ます。
実装過程
フォントレンダリングのためのバッチテクスチャを実装するにはmsdfgenライブラリを使い、各文字のテクスチャを一つにテクスチャにレンダリングするロジックとその文字に関しての情報を取得して他のコンテナに入れ込むロジックを実装することが必要でした。
そして今のバージョンではASCII、韓国語、日本語ひらがなとカタカナ、そしてCJKに対応する漢字に対応しています。そうするために各マップにUTF-16コードを入れてテクスチャを生成するときに該当するUTF-16の文字に対しデータを作るようにしました。
文字を一つずつ生成するたびに、あとでDy
で使う情報は取り出してjsonというデータ記述言語をバインディングするインスタンスに書き込みます。
そしてmsdfgenから取り出したバッファデータは一文字が入れるイメージインスタンスに書き込みます。(Qt
ではQImage
という、バッファのデータが入れやすくするコンテナがあります。)最終的にそのQImage
インスタンスはフォントの実質な文字テクスチャに書き込むようになります。
ここで重要なのは、レンダリングをしてアトラスを作る際に文字が入れるバッチテクスチャ(Batch Texture)から決められた一部レンダリング空間に一つずつだけ入るのではありません。
SDFで表現した字は一つのチャンネルだけ使えることが出来ます。これを利用してRGBAのべ4チャンネルに4文字ずつ入れるようにしました。
しかしレンダリングはとにかく、SDFのアルゴリズムを用いて情報を取り出すのに時間が相当掛かります。200文字くらいに対するデータを生成するにはSDFアルゴリズムの速度が遅いとしても差し支えがほぼありません。
しかし、ハングルかそれとも漢字のように最小2,3000文字が必要となれば処理を完了するのに30分くらいかかる恐れがあります。
SDFアルゴリズムのコードをいじって処理時間をへらすわけにはいきません。しかし処理時間は減らしたいです。この時点で私はマルチスレッドを活用して時間を減らせたいと思いました。
// Issue task to parallel thread. for (auto j{0u}; j < concurrentThreadNumber; ++j) { auto result = std::async( std::launch::async, CreateGlyphInformation, std::cref(sFtFunctions), targetCharMap[i + j], sFtFaceList[j], charCount + j ); threadResultList.emplace_back(std::move(result)); }
手順は以下となります。
- ハードウェアから支援するスレッドの数を得る。これによって動かすスレッドの数が違われます。
- 一回にスレッドの数分だけ
std::async
とstd::launch::async
で別のスレッドからタスクを行うようにします。タスクは以下になります。- それぞれのスレッドから初期化したFreetypeインスタンスから与えられた文字コードを入れて情報を取り出す。
- SDFアルゴリズムを行って文字のバッファを取り、QImageに入れてレンダリングできるインスタンスを作る。
std::async
が返すstd::future
からそれぞれのスレッドからのデータを取ります。- 情報データはフォントのJsonインスタンスに書き入れて、QImageはフォントのバッチテクスチャの指定した空間にレンダリングします。
- まだ残っている文字の情報があるなら1に戻り作業を続けます。
- 作業が終わってからのフォント情報インスタンスとバッチテクスチャはファイル化します。
こうやってReleaseモードで10000文字に18分掛かることを5分で処理できるようになりました。期待したのよりはちょっと遅めですが、OpenGLのレンダリング書き込みをするために処理を待つ時間があるためだと思います。
最後にzlib
を使用してJson情報インスタンスを圧縮します。こうするには2つの理由があります。
- 多い情報を入れ込むことによって情報だけでも10MB以上の大容量になってしまうことを防止。
- 外部から情報を操作することによって発生できるフォントレンダリングの誤動作(バグ)を防止。
そしてDy
側からファイルを読み込み、シェーダーを装着してレンダリングすればランタイムにSDFテクスチャを生成することなくフォントをレンダリングするようになりました。
改善点&反省点
- SDF自体の問題でもありますが、SDFは尖った部分を丸くして描画してしまいます。それを防ぐために新たに開発したアルゴリズムがMSDF(Multi-channel Signed Distance Field)です。
しかしこのアルゴリズムはR, G, Bの3チャンネルを使うため今の実装仕様では1024x1024に256文字しか入れない問題があります。
テクスチャを小さくして描画すればと言われそうですが、英語ならできますが漢字、ハングル、そして日本語の場合には少なくとも48x48じゃないとレンダリングが化けてしまいます。
テクスチャサイズを抑えながらMSDFが実装できる方法を探るのが今後の課題になりそうです。 - 上のレンダリング画面を見ていますと縁の部分の太さがそれぞれ違うことがわかります。これはツールでSDFバッファを生成するときにテクスチャのサイズによって自動スケールするようにしたからです。
即ち、フォントが同じ大きさでも「。」のような小さい文字は大きくなってテクスチャして、「覇」のような文字は大きい分小さくなります。
これを直すには自動スケールを消して情報入れのロジックを少し修正しなければなりません。 - 上のSDFテクスチャを見るとそれぞれの文字の間に空白があることがわかります。今は
1024x1024
テクスチャに一文字に64x64
ずつ空間を割り当ててレンダリングしているからです。
改善できれば空間を埋めながらSDFテクスチャを入れたいと思います。(ずいぶん長くなりそうですが)
結論
実は11月中旬としてツールは完成しましたが、報告文書は書いておらず今まで引き釣りしました。言い訳は言いません。すみませんでした🙏
追記
- 2018-12-03
従来複数のファイルで出力されたことを、一つのファイルとして出力するようにしました。フォントの情報とフォントのレンダリングに活用するイメージのバッファを圧縮してヘッダーと一緒に埋め込みました。
まだDy
でのインポート作業は実装していないんですが、近日にやるつもりです。