NEUROMANTIC

自分でC/C++/UE4/Graphics/ゲームなどをやったことをメモするブログ

報告。`Dy`専用のSDFFontGeneratorツールを作りました。

報告

f:id:neuliliilli:20181201202946p:plain
ツールのウィンドウ画面

Dyゲームフレームワークのフォントレンダリングに必要するテクスチャをバッチで生成してくれるツールを作りましたことをご報告申し上げます。

DyゲームフレームワークはOpenGLをグラフィックAPIとして動かしておりDirectXなどで基本的に支援するフォントレンダリングが出来ません。そのため、遠回りとしてOpenGLでフォントレンダリングを支援できるようにするFreetypeを使うのが普通です。

https://www.freetype.org/freetype2/docs/glyphs/metrics.png

Freetypeライブラリは文字ファイルを読み込んで各文字に該当する上の情報を返還することが出来ます。しかしFreetypeだけを使用して実時間でテクスチャを生成するのは非効率であり、性能も落ちる可能性があります。
なぜなら英語の文字またはASCIIだけなら問題ではなくなりますが、UTF-8の文字を一つ一つ生成するならテクスチャが1000個以上になるかねません。
それによって文字を画面に書き詰めることだけにしてもGPU側が大変な文字一つのテクスチャをバインディングして描画手順を踏まなければなりません。大きなロスが出てしまいます。

https://edge.alluremedia.com.au/m/l/2014/11/sdf.png

そして私がDyに実装しようとしたのはただの一般テクスチャではなく、SDF(Signed Distance Field)を使用してどんなに文字が大きくなっても普通のようにぼけない文字レンダリングをしたかったです。
上の画像のように普通のテクスチャを使うとサイズより大きくレンダリングするときには外がぼけてしまいます。しかしSDFを使うと右の画像のように大きく拡大してもボケません。

そして私は開発をより速く進めるためにSDFが実装されているライブラリーを使うことにしました。

github.com

msdfgenライブラリはFreetypeライブラリをベースとしてSDFのテクスチャを実装するようにします。そしてより発展したアルゴリズムであるMSDFでレンダリングするようにもします。
しかし問題はmsdfgenは1文字ずつテクスチャを生成してくれません。そして日本語の出力に必要な必須文字であるおよそ2300文字文のファイルを生成してバッチで読ませるのは一般的な考えなら無理になります。
そしてSDFの1文字生成にかかる時間は普通にFreetypeを使ってテクスチャを作るのより5倍くらいの時間が掛かっってしまいます。

この時点で私はDyでリアルタイムで文字のSDFテクスチャを生成したいことをやめて、バッチで大きなテクスチャに文字テクスチャを埋め込み、Text Atlasを作ってDyからは情報やイメージバッファを読み込みすれば良いと思いました。そしてこれをやってくれるツールも作らないとならないと思いました。
なぜならずっと私一つでDyを開発するならただのCUIでも構いませんがCUIが操作できないデザイナーがいるときを想定するならGUIじゃないとツールが役に立たないと思いましたからです。

https://upload.wikimedia.org/wikipedia/commons/thumb/0/0b/Qt_logo_2016.svg/1200px-Qt_logo_2016.svg.png

それで私はグラフィックライブラリとしてQtを使うことにしました。QtはC++言語で書かれたGUIライブラリとして数多くの企業やプロジェクトで使っています。これを用いてGUIのインタフェースとロジックを便利に実装することが出来ます。
そしてQtはOpenGLを支援するのでOpenGLを使ってレンダリングを行ったりすることが出来ます。

実装過程

フォントレンダリングのためのバッチテクスチャを実装するにはmsdfgenライブラリを使い、各文字のテクスチャを一つにテクスチャにレンダリングするロジックとその文字に関しての情報を取得して他のコンテナに入れ込むロジックを実装することが必要でした。

f:id:neuliliilli:20181201230117p:plain
現在バージョン(v0)のキャラクタマップの仕入れと生成の仕組み

そして今のバージョンではASCII、韓国語、日本語ひらがなとカタカナ、そしてCJKに対応する漢字に対応しています。そうするために各マップにUTF-16コードを入れてテクスチャを生成するときに該当するUTF-16の文字に対しデータを作るようにしました。

f:id:neuliliilli:20181201234236p:plain
文字情報生成の全体的なフロー。バッチテクスチャの解像度は1024x1024です。

文字を一つずつ生成するたびに、あとでDyで使う情報は取り出してjsonというデータ記述言語をバインディングするインスタンスに書き込みます。
そしてmsdfgenから取り出したバッファデータは一文字が入れるイメージインスタンスに書き込みます。(QtではQImageという、バッファのデータが入れやすくするコンテナがあります。)最終的にそのQImageインスタンスはフォントの実質な文字テクスチャに書き込むようになります。

f:id:neuliliilli:20181202000737p:plain
一つの空間に4文字が入れます。

ここで重要なのは、レンダリングをしてアトラスを作る際に文字が入れるバッチテクスチャ(Batch Texture)から決められた一部レンダリング空間に一つずつだけ入るのではありません。
SDFで表現した字は一つのチャンネルだけ使えることが出来ます。これを利用してRGBAのべ4チャンネルに4文字ずつ入れるようにしました。

しかしレンダリングはとにかく、SDFのアルゴリズムを用いて情報を取り出すのに時間が相当掛かります。200文字くらいに対するデータを生成するにはSDFアルゴリズムの速度が遅いとしても差し支えがほぼありません。
しかし、ハングルかそれとも漢字のように最小2,3000文字が必要となれば処理を完了するのに30分くらいかかる恐れがあります。

SDFアルゴリズムのコードをいじって処理時間をへらすわけにはいきません。しかし処理時間は減らしたいです。この時点で私はマルチスレッドを活用して時間を減らせたいと思いました。

f:id:neuliliilli:20181202003808p:plain
現在バージョン(v0)のマルチスレッド実装ロジック構造

// 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));
}

手順は以下となります。

  1. ハードウェアから支援するスレッドの数を得る。これによって動かすスレッドの数が違われます。
  2. 一回にスレッドの数分だけstd::asyncstd::launch::asyncで別のスレッドからタスクを行うようにします。タスクは以下になります。
    1. それぞれのスレッドから初期化したFreetypeインスタンスから与えられた文字コードを入れて情報を取り出す。
    2. SDFアルゴリズムを行って文字のバッファを取り、QImageに入れてレンダリングできるインスタンスを作る。
  3. std::asyncが返すstd::futureからそれぞれのスレッドからのデータを取ります。
  4. 情報データはフォントのJsonインスタンスに書き入れて、QImageはフォントのバッチテクスチャの指定した空間にレンダリングします。
  5. まだ残っている文字の情報があるなら1に戻り作業を続けます。
  6. 作業が終わってからのフォント情報インスタンスとバッチテクスチャはファイル化します。

f:id:neuliliilli:20181202010927p:plain
現在バージョン(v0)のバッチファイル出力結果
f:id:neuliliilli:20181202011002j:plain
ハングル10000文字をバッチテクスチャでイメージした結果

こうやってReleaseモードで10000文字に18分掛かることを5分で処理できるようになりました。期待したのよりはちょっと遅めですが、OpenGLのレンダリング書き込みをするために処理を待つ時間があるためだと思います。

最後にzlibを使用してJson情報インスタンスを圧縮します。こうするには2つの理由があります。

  1. 多い情報を入れ込むことによって情報だけでも10MB以上の大容量になってしまうことを防止。
  2. 外部から情報を操作することによって発生できるフォントレンダリングの誤動作(バグ)を防止。

そしてDy側からファイルを読み込み、シェーダーを装着してレンダリングすればランタイムにSDFテクスチャを生成することなくフォントをレンダリングするようになりました。

f:id:neuliliilli:20181030205230p:plain

改善点&反省点

  • 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

f:id:neuliliilli:20181203141917p:plain
(v2)複数のファイルが一つに埋め込めるように実装しました。

従来複数のファイルで出力されたことを、一つのファイルとして出力するようにしました。フォントの情報とフォントのレンダリングに活用するイメージのバッファを圧縮してヘッダーと一緒に埋め込みました。
まだDyでのインポート作業は実装していないんですが、近日にやるつもりです。