最初はこの仕組みで満足していたんだけど、マルチスレッド対応とちゃんとしたダブルバッファリングの仕組みに変えてから、11on12を捨てたいと思うようになった。
マルチスレッドとダブルバッファリング
マルチスレッド&ダブルバッファリングで完全にCPUとGPUを並列に処理できるようになった時、Direct2D用の描画が気になるようになってきた。
描画コマンドはすべて別スレッドでコマンドリストに書き込むようになっており、メインスレッドの利用を極力抑えるようにしているが、Direct2D部分はマルチスレッド対応以前のメインスレッドが描画コマンドを発行する仕組みのまま。
いまはFPSぐらいしか表示してないので特に影響はなさそうだけど、大量の線、矩形、文字列を描画するような時に問題になってくると思われる。
フォントテクスチャ
というわけでDirect12だけにして、11on12はやめることにした。
といっても、文字描画の仕組みがなくなってしまうので裏でDirect2D/DirectWriteの仕組みで文字列を描画して、結果のビットマップをテクスチャにして表示してみようと思う。
DirectWrite版フォントテクスチャ
Direct2DのID2D1RenderTargetクラスにCreateWicBitmapRenderTargetという関数があったのでこれを使って文字描画したビットマップを取得してみる。
途中ハマったことは、IWICBitmap::CreateBitmapのピクセルフォーマットの指定。
最終的には、GUID_WICPixelFormat32bppBGRを指定してうまく行ったけど、アルファ付きに出来ない。アンチエイリアス部分が背景色に影響して結果が変わってくる。
下記は取得したビットマップの背景色を透過させてPNG保存した画像。
背景色が黒だとアンチエイリアス部分に水色や茶色などが使われている模様。
![]() |
| グレースケール |
SetTextAntialiasModeをD2D1_TEXT_ANTIALIAS_MODE_GRAYSCALEにすると想像していた結果になった。D2D1_TEXT_ANTIALIAS_MODE_CLEARTYPEの方が見た目がいいんだろうけど、加工する場合はこっちのほうが使いやすい。
![]() |
| 背景緑 |
背景を真緑にして、透過色として識別しようと思ったらこんな結果に。
透過を意識したデータを作れないので、これを使う場合はグレースケールになるかな。
GetGlyphOutline版フォントテクスチャ
昔、DirectX9とか?でID3DXFontが遅かった時代に使われていた手法で、GetGlyphOutlineを使ってフォントテクスチャを作って文字を描いていたらしく、この方法でも試してみた。
取得できる値は指定したクオリティのグレースケールのビットマップ。
これをアルファ値として別途真っ白のビットマップとして保存してみた。
![]() |
| GGO_GRAY2_BITMAP |
![]() |
| GGO_GRAY4_BITMAP |
![]() |
| GGO_GRAY8_BITMAP |
それぞれの処理時間の違いは、ほとんどなさそう。0.2~0.9msとぶれ幅が大きいけど、大体が0.3ms前後だった。
これなら常にGGO_GRAY8_BITMAPで良さそう。
フォント作成時、CLEARTYPE_QUALITY、CLEARTYPE_NATURAL_QUALITYを指定するとClearTypeも使えるっぽい。ただ、CLEARTYPE_QUALITYはほとんど処理速度は変わらなかったけど、CLEARTYPE_NATURAL_QUALITYにすると約2倍になった。
比較
フォント品質
Direct2D版の方が見た目がきれい。アンチエイリアス部分に別の色が入るとキリッとする。ただ、取得したデータをキャッシュして色をシェーダ側でつけるとなると、グレースケールにすることになるので差はほとんど無くなる。GetGlyphOutlineは純粋に白のデータにアルファ値だけのテクスチャが作れるため、こちらの方が使い勝手がいい。
処理速度
2文字の場合、Direct2Dが1.5ms(1.2ms)に対して、GetGlyphOutlineは0.3ms。
20文字の場合、Direct2Dが1.5ms(1.2ms)に対して、GetGlyphOutlineは1.0ms。
※Direct2Dのカッコ内はグレースケールの処理時間
Direct2Dの方はこの程度の文字数の差によって速度は殆ど変わらなかった。
GetGlyphOutlineは文字が増えると処理時間も線形に延びていく。
どちらの仕組みでも、毎フレームのテクスチャ生成は現実的ではなさそう。
数字、アルファベット等よく使う字は予め生成し、ほかは逐次キャッシュしていく仕組みにしたり、ゲーム内で使う文字列をローカライズファイルに纏めておいて、そこから使ってる文字のテクスチャを事前生成するなど工夫すれば速度に関しては問題なさそう。
フォントテクスチャ作成
文字のビットマップを取得する
TEXTMETRIC oTM ;
::GetTextMetrics( hDC, &oTM ) ;
GLYPHMETRICS oGM ;
const MAT2 oMat = {{0,1},{0,0},{0,0},{0,1}} ;
tWC c = L'漢' ;
auto nSize = ::GetGlyphOutline( hDC, c, nGradFlag, &oGM, 0, nullptr, &oMat ) ;
tBin oData ;
oData->Resize( nSize ) ;
::GetGlyphOutline( hDC, c, GGO_GRAY8_BITMAP, &oGM, oData.Count(), oData.Val(), &oMat ) ;
TEXTMETRICとGLYPHMETRICを使って、テクスチャに書き込みと描画する際の頂点データ用の値を準備する。
フォントのサイズについて
![]() |
| TEXTMETRICとGLYPHMETRICの関係図 |
一般的なフォントサイズといえばtmHeight。
それが、青い線(ベースライン)を挟んで、tmAscentとtmDescentに分かれる。
ビットマップの画像はgmBlackBoxX、gmBlackBoxYのサイズで返却される。
ただし、返却されたメモリの横幅は4バイトアラインになっているため、4の倍数に補正する必要がある。
(例えばgmBlackBoxX=7の時、メモリの横幅は8となる)
auto w = oGM.gmBlackBoxX ; auto nMask = a-1 ; w = ( w + nMask ) & ~nMask ;
テクスチャの書き込み座標
最初の文字のX座標は0。次の文字はgmBlackBoxXをインクリメントしていく。
テクスチャの横幅が残りtmMaxCharWidth分ない場合は次の行に改行して0にリセット。
Y座標は、行数にtmHeightを掛けたものが基準。
その基準にtmAscent - gmptGlyphOrigin.yを足した位置が書き込み位置となる。
頂点座標
配置位置(=基準)にgmptGlyphOrigin.xを足す。注意点は、例えば一番最初の文字がjの場合、上図のようにgmptGlyphOrigin.xがマイナスになっている(前の文字に重なる)
必要に応じて位置を補正する
テクスチャの横幅はgmBlackBoxXなので、その分の幅を取る。
次の文字の開始位置は基準からgmCellIncX行ったところとなる。
高さは常にtmHeightで処理する。
パフォーマンス
横400文字、縦100行、計4万文字を表示してみたところ、FPSが20ぐらいになった。
試しに同じ条件でDirect2Dで描画すると60FPSで表示される。
文字列は1行0~9を40回繰り返した文字列を100行分表示している。
おそらく、100行分同じ文字列なのでうまく使いまわしているのかもしれない。
そこで全行ランダムにして表示するようにしたらDirect2DでもFPSが30まで落ちた。
パフォーマンスチューニング1
頂点バッファをx,y,u,v,r,g,b,aで作って1文字4頂点1セットでバッファを毎フレーム書き込んでいる。インデックスバッファは適当な文字数分予め用意しておき、足りなくなったらまたある程度余裕を持たせて作成するようにしていた。
1文字のデータはfloat(4byte)✕8✕4=128byteとなっているため、これを減らそうと考えた。
頂点バッファを渡すのをやめて、構造化バッファを渡して頂点シェーダ内で位置、UVを作ることにした。色に関しては一旦保留。
構造化バッファで渡す値は、x,y,w,h,u,v,uw,vhで、1文字のデータはfloat(4byte)✕8=32byteとなり1/4に削減した。
結果、FPSは25に上昇したけどまだDirect2Dに負けている。これなら新しい仕組みにする意味がない。
パフォーマンスチューニング2
渡しているx,yはディスプレイの座標なので、unsigned shortで問題ない。
また、w,u,vはフォントのマスタ情報を別途構造化バッファで作って、そのインデックスを指定するようにすれば、1つのunsigned shortで表現できる。
パフォーマンスチューニング1では、色を一旦保留にしたが、色マスタ情報作って同様の考え方にすれば1つのunsigned shortで表現できる。
というわけで、x, y, no, colとなり、1文字のデータはunsigned short(2byte)✕4=8byteで更に1/4となり、色情報も追加できた。
結果、FPSは30に上昇し、ほぼDirect2Dと同じになった。
追加の検証で、1行の文字列をまとめて描画ではなく1文字づつ描画するように変えてみた。
Direct2Dは3FPSとなり、1/10に落ちた。
新しい仕組みの方は12FPSでDirect2Dの4倍で動いてる。
Releaseビルドならこの状態でも60FPSでてる。
限界を探るため、今度は色をすべての文字でランダムにすると25FPSとなった。
色マスタ情報が大きくなり毎フレームの更新に負荷がかかるのだろう。
色数が多すぎる場合は、使う色を限定化してマスタの更新をなくせばこの状態でも問題なく動きそう。
結果的にドロ-コールを100回呼び出しで比較するとほぼ同じ速度で、4万回呼び出す場合は、マルチスレッド対応されている新方式の方が速くなった。
サロゲートペア対応
実装している最中気になっていたのが、サロゲートペアについて。
GetGlyphOutline関数に渡す値が1文字分のコードなので、サロゲートペアの場合だめだろうと思っていた。
サロゲートペアといえば「𩸽(ほっけ)」なので文字列に指定してみる。
Direct2Dでは問題なく表示されるが、GetGlyphOutlineでは中点が2つが表示される。
GetGlyphOutlineのコードを渡す型が32bitなので、まとめて渡してみてもだめ。
調べてみると、GetCharacterPlacement関数でGlyphIndexというものを取得して、GlyphIndex指定でGetGlyphOutlineを呼び出せば取得できるらしい。
tStr s = L"𩸽" ;
tWC nBuf[2] ;
GCP_RESULTS oGR = {} ;
oGR.lStructSize = sizeof(oGR) ;
oGR.lpGlyphs = nBuf ;
oGR.nGlyphs = 2 ;
::GetCharacterPlacement( hDC, s, oGR.nGlyphs, 0, &oGR, GCP_GLYPHSHAPE ) ;
関数がうまくいくと、oGR.nGlyphsは1になり、nBuf[0]にGlyphIndexが入っている。
GetGlyphOutline関数に渡していたGGO_GRAY8_BITMAPをGGO_GRAY8_BITMAP | GGO_GLYPH_INDEXに変えて、文字コードを渡していたところにGlyphIndexを指定するとサロゲートペアのビットマップが返ってくるようになった。
なんと読むのかもわからない文字だけど混在していても表示できている。







0 件のコメント:
コメントを投稿