マルチチャンネルディスタンスフィールドを使ったフォント描画をwebglでやってみた
はじめに
OpenGLなどでフォントや色々なシェイプの描画に使えるマルチチャンネルディスタンスフィールドを使った方法を試してみました。
実験用のコードとgithub pagesのデモページです。
https://github.com/bigsleep/msdf-experiment
https://bigsleep.github.io/msdf-experiment/index.html
フォント描画方法について
OpenGLなどでフォント描画するには
- フォントのベジェ曲線情報をラスタライズ
- 生成したフォントの二次元画像からテクスチャ作成
- 頂点バッファを作成してテクスチャを貼り付ける
のようなやり方がよくある方法と思います。
この方法だとフォントが拡大表示されたときに解像度が低くギザギザの目立つ状態になってしまいます。 高解像度にしようとすると画像データが大きくなり事前にラスタライズしておくような方法もやりづらくなります。
ディスタンスフィールドを使った方法では低解像度でもかなり鮮明に描画できるようです。 ディスタンスフィールドについてはこちらのページなどが参考になりました。
https://thebookofshaders.com/07/?lan=jp
ただディスタンスフィールドでの描画だとフォントのエッジが丸くなってしまったりする問題があります。 マルチチャンネルディスタンスフィールドを使った描画方法では フォントの隣り合う辺を別々のチャンネルのディスタンスフィールドで表現してエッジが丸くならないようにしているようです。
msdfgen
マルチチャンネルディスタンスフィールドを自前で生成するのはなかなか難しそうですが msdfgenというプログラムが公開されていてこれを利用して生成できました。
https://github.com/Chlumsky/msdfgen
msdfgen作者の方の論文も公開されているようでした。
https://dspace.cvut.cz/bitstream/handle/10467/62770/F8-DP-2015-Chlumsky-Viktor-thesis.pdf
自分の環境 (Ubuntu 17.10) でもビルドして動かすことができました。確かfreetypeだけaptでインストールしました。 使い方はほぼREADMEに書いてありますが、以下は自分で動かしたときのメモになります。
mode
モードはsdf, psdf, msdfがあります。マルチチャンネルディスタンスフィールドはmsdfを使います。 sdfは通常のディスタンスフィールド、psdfは疑似ディスタンスフィールドです。 psdfもエッジがまるくならないようにディスタンスフィールドを1チャンネルのまま変形させたものらしいです。 この他にmetricsモードで文字のバウンディングボックスや幅を表示することができます。
-font
フォント指定のオプションです。 -font <フォントファイル名> <文字> のように指定します。対象の文字はコードポイントの十進数か十六進数またはクォートした文字で指定します。 自分の環境ではなぜか十進数のコードポイント指定しかうまく使えませんでした。
-scale
スケールは1シェイプユニットがディスタンスフィールドのピクセル何個に当たるかというパラメーターです。 シェイプユニットはmsdfgenで使われている距離の単位でフォントの1emあたりのユニットを64で割った値になっているようです。 1emあたりのユニットは2048が多くのフォントで使われていて、この場合は1emは32シェイプユニットに等しくなると思います。 いくつか文字を出力してみたところはscaleは2ぐらいで問題なさそうでした。 scaleが2で1emあたりのユニットが2048のときは、ディスタンスフィールドは1emあたり64ピクセルのサイズになります。
-range
ディスタンスフィールドの幅に当たるパラメーターのようです。 出力画像のサイズはバウンディングボックスにこのレンジを考慮して決めました。 rangeの単位はシェイプユニットです。
-pxrange
pxrangeはrangeをディスタンスフィールドのピクセルで表したパラメータです。 pxrange = range * scale になるようです。
フォントの情報のメモ
em
1emは文字の高さの基準となる大きさです。cssなどでもサイズの単位に使われています。 フォントのデータには1emあたりのユニットというのが定義されています。 fontforgeなどのプログラムで調べることができます。 1emあたりのユニットは2048や1024がよく使われるようです。
バウンディングボックス
バウンディングボックスは文字のうち印字される領域の四角形です。 ディスタンスフィールドは印字される部分をカバーしていればいいので バウンディングボックスから出力画像のサイズを決められます。
アドバンス
文字の送り幅の情報です。 今回生成したフォントでは半角文字のアドバンスは0.5emでした。 プロポーショナルフォントでは文字ごとにアドバンスが変わってきます。
ベースライン
ベースラインは1emの文字高さのうちy座標が0となるラインを表しています。 これもfontforgeで調べられました。
フォント描画の流れ
リポジトリに事前準備に使ったスクリプトも含めてありますがわかりにくいためやったことを簡単に書いておきます。
スクリプトの部分はbashで書いていたんですがrubyやpythonなどで書いたほうがやりやすかったかもしれません。 msdfgenはC++ライブラリとしても使えるようなので大量に生成する場合はC++から使ったほうが速く生成できると思います。 コマンドでは毎回フォントファイルのパースが入るので時間かかっていると思います。
- 事前準備
- msdfgenで文字のmetrics (バウンディングボックス, アドバンスなど) を取得
- 文字のマルチチャンネルディスタンスフィールドのサイズと位置を決定
- msdfgenでマルチチャンネルディスタンスフィールドを生成
- 文字の位置、幅、スケールを同時に出力しておく
- 生成したマルチチャンネルディスタンスフィールド画像をパッキングして一枚の画像にする
- プログラム中
- 先に生成したマルチチャンネルディスタンスフィールド画像を読み込みテクスチャを生成する
- 頂点バッファを生成してテクスチャを貼り付ける
- マルチチャンネルディスタンスフィールド用のシェーダープログラムで描画
three.jsメモ
BufferGeometry
https://threejs.org/docs/#api/core/BufferGeometry
three.jsで頂点バッファを使う場合はBufferGeometryを使うようです。 BoxGeometoryなどの内部でもBufferGeometryが使われていました。 インデックスをセットした場合はdrawElementsを使って描画され、 インデックスをセットしていない場合はdrawTrianglesが使われるようです。 今回はpositionとuvのattributeだけを使いました。 positionは位置、uvはテクスチャ座標です。 Float32Arrayなどを使ってBufferAttributeを作成しBufferGeometryにaddAttributeメソッドでセットします。
ShaderMaterial
https://threejs.org/docs/#api/materials/ShaderMaterial
独自のシェーダーを使う場合はShaderMaterialを使います。 three.js内部でシェーダーの頭に組み込みのuniformやattributeが挿入されるようです。 positionなどいくつかのattributeは先に使われているので自分で定義しようとするとコンパイルエラーになります。 RawShaderMaterialもあってこちらはthree.js組み込みのuniformやattributeが挿入されません。 カメラやコントロールとの連携を使うには組み込みのuniformを使う必要があると思います。
OrbitControls
https://threejs.org/docs/#examples/controls/OrbitControls
OrbitControlsをつけるだけでマウスで視点を変えることができました。 デモやデバッグ用途などでも便利そうです。 この他にFlyControlsやFirstPersonControlsなどもあるようでした。
TextGeometry
https://threejs.org/docs/#api/geometries/TextGeometry
three.jsにもフォント描画用の機能がありました。 この機能では文字のベジェ曲線を頂点メッシュで表現して描画しているようでした。 用途によってはこれも使えると思います。
まとめ
色々時間がかかりましたがマルチチャンネルディスタンスフィールドでのフォント描画ができました。 OpenGLを使ったゲームなどでも使えるんじゃないかと思います。 機会があれば使ってみたいと思います。