ホーム < ゲームつくろー! < DirectX技術編 < 高速フォント表示


その5 高速フォント表示

この章のサンプルプログラムはこちら


 Direct3Dはフォントの扱いが苦手です。一応画面に簡単に文字を出すためのID3DXFontインターフェイスがありますが、背後でWindows APIを用いてフォント形状を取得しているので、描画が異常なほど遅いのです。あまりに遅くて、特にノベルス系のように画面にフォント文字が沢山出て来るゲームではまともに使えません。

 ID3DXFontを介して描画するのがなぜ遅いのか?それは、1フレームごとにフォントを一から作成して描画しているためです。フォントの情報を得て、それを点の情報に変換して画面に打つ。これでは、どれだけ頑張ったって速度は出ません。ところで、DirectXは「絵」の描画については異常な程高速です(それがDirectX最大の売りですよね)。ということは、フォントを毎回「打つ」のではなく、フォントの絵を「描画」すれば、高速なフォント描画があっさりと実現できるわけです。

 フォントの絵を用意する手法は、ゲームのスコアのような特定の数字フォントしか扱わない場合に良く使われるます。しかし、ノベルス系のゲームには数万文字の文章があり、数千のフォントが使用されます。それ全て絵として用意するわけにはいきません。ですから、動的にフォントを作成し、それを絵に変換する作業がどうしても必要になります。

 DirectXで絵と言うとサーフェイスかテクスチャです。Direct7の時には2Dの矩形領域であるサーフェイスの扱いは結構楽だったのですが、Direct8になってDirect Graphicsとして2D処理がまとめられた際、いきなり扱いにくくなってしまいました。また、サーフェイスは回転したり、拡大縮小したりという事が本来苦手です。一方、テクスチャは板ポリゴンに貼ってしまえば何でもできますし、取替えが非常に簡単です。よって、ここではテクスチャを絵として採用することにします。




↑テクスチャ+板ポリなら、こういう事が自由にできます


 フォントをテクスチャに書き込む場合、@テクスチャのピクセルを直接書き換える、AテクスチャからIDirect3DSurface9オブジェクトへのポインタを取り出してサーフェースに書き込む、という方法が考えられるのですが、今回は@の方法を採用します。Aの方法を用いると、IDirect3DSurface9オブジェクトから「デバイスコンテキストハンドル」を取得できるので、Windows API関数を使ってフォントをとても簡単に描画できます。当初私も「これは便利だ」と思ったのですが、サーフェイスのデバイスコンテキストハンドルを正しく取得するためには、α情報の無いテクスチャを作成しなければならない事がわかり、事情が変わってきました。背景付きフォントを使いたい人はいないわけでして、背景を取り去って正しくフォントを描画するまでには、α無しテクスチャに描画→αありテクスチャにコピー→α情報の書き直し→板ポリゴンに貼り付けというかなり面倒な作業を要してしまうのです(この方法で実現は可能です)。α情報の書き直しはテクスチャのピクセルを直接書き換えるので、結局は@と変わらないわけです。



@ テクスチャのサーフェイスにフォントの点を直接書き込む
   (GetGlyphOutline関数の利用)

 テクスチャサーフェイスのピクセルを直接変更する方法はIDirect3DTexture9::LockRect関数を使えば可能です。また、フォントのピクセル情報はGetGlyphOutlineというWindows API関数からアンチエイリアス付きのビットマップとして取得する事が出来ます。これをテクスチャサーフェイスに直接書き込むわけです。ただし、GetGlyphOutline関数はフォントのサイズぎりぎりでしか取得してくれません。ですから、フォントの描画位置は自前で調整する必要があります。ここが結構面倒なところなのです。
 GetGlyphOutLine関数と位置決めについては「Windows API TIPs編」の「GetGlyphOutline関数のフォント位置」にまとめましたのでご参照ください。ここでは、フォントのビットマップの幅と高さ(iBmp_w, iBmp_h)、及び余白を加えたフォント領域(iFnt_w, iFnt_h)そしてフォントの書き出し位置(iOfs_x, iOfs_y)が得られているとして話を続けます。
 まず、フォント領域と同じ縦横の比率を持つ板ポリゴンを新規に作成します。また、フォント領域と同じ大きさのテクスチャもIDirect3DDevice9::CreateTexture関数から作ります。この時作るテクスチャは当然ながらα情報を持ったD3DFMT_A8R8G8B8形式で作成します。また、ピクセルへの書き込みを可能にするため、D3DUSAGE_DYNAMIC及びD3DPOOL_DEFAULTで作成します(詳しくは「テクスチャ作成あれこれ参照」)。

pD3dDev9->CreateTexture(
iFnt_w, iFnt_h,
1,
D3DUSAGE_DYNAMIC,
D3DFMT_A8R8G8B8,
D3DPOOL_DEFAULT,
&pTexture,
NULL
);

 次に書き込み先ポインタを取得するため、テクスチャのサーフェイスをIDirect3DTexture9::LockRect関数でロックします。

HRESULT LockRect(
UINT Level,
D3DLOCKED_RECT* pLockedRect,
CONST RECT* pRect,
DWORD Flags
);

Levelはミップマップのレベルの事で、今は1つしかありませんから、ここには0が入ります。
pLockedRectはD3DLOCKED構造体へのポインタを渡します。ロックが成功した場合、この構造体の中にサーフェイスメモリへのポインタ(D3DLOCKED::pBits)が渡されます。また、1行のサイズ(D3DLOCKED::Pitch)がバイト単位で格納されます。この値は後で大切になってきます。
pRectはロックする矩形領域を指定します。今はテクスチャ全部を指定したいのでNULLとします。
Flagsはロック時のオプションフラグです。D3DLOCK列挙型から選択します。D3DLOCK_DISCARDにすると書き込み専用となります。

サーフェイスのロックが完了したら、GetGlyphOutline関数で得たフォントのピクセル情報を書き込みます。ただし、フォントのビットマップはアンチエイリアスのレベルによってその数値の幅が異なります。テクスチャに描く時にこれらの幅を0〜255にスケール変換しないと、透明過ぎて薄〜いフォントになってしまいます。

 GetGlyphOutline関数によるアンチエイリアスレベルは3段階あります。

アンチエイリアスフラグ 段階(最大値)
GGO_GRAY2_BITMAP 5
GGO_GRAY4_BITMAP 17
GGO_GRAY8_BITMAP 65


GetGlyphOutline関数で得たフォントビットマップのピクセル値をValとすると、スケール変換式は次のようにするとうまくいきます。

Trans = (255 * Val) / (段階-1)

 ここからが最後の詰めです。GetGlyphOutline関数で得られるフォントの横幅(gmBlackBoxX)とビットマップの横幅とには違いが起こる事があります。これはビットマップの1行のピクセル数は4の倍数でなければならないという決まりがあるからです(いわゆる4バイト境界です)。例えばgmBlackBoxX=13だとしたら、ビットマップの横幅iBmp_wは16で確保されています。確保されたビットマップの横幅を得るには、

iBmp_w = gmBlackBoxX + (4-(gmBlackBoxX%4))%4

と計算します。これは、この手のビットマップを扱う時の常套手段です。

 これで書き込みの準備が終わりました。テクスチャサーフェイスの1ピクセルが32ビット(ARGB)なのに対し、取得したビットマップは8bitである事に注意してテクスチャに書き込みます。pBmpがフォントビットマップへのポインタとすると、

// サーフェイスロック
D3DLOCKED_RECT lockRect;
pTexture->LockRect(0, &lockRect, NULL, D3DLOCK_DISCARD);

// テクスチャサーフェイスの初期化
FillMemory(lockRect.pBits, lockRect.Pitch * iFnt_h, 0);

// フォント情報の書き込み
for(int y=iOfs_y; y<iOfs_y+iBmp_h; y++)
for(int x=iOfs_x; x<iOfs_x+iBmp_w; x++){
    DWORD Trans = (255 * pBmp[x-iOfs_x + iBmp_w*(y-iOfs_y)]) / (Level-1);
    DWORD color = 0x00ffffff | (Trans<<24);
   memcpy((BYTE*)lockRect.pBits + lockRect.Pitch*y + 4*x ), &color, sizeof(DWORD));
}

// サーフェイスアンロック
pTexture->UnlockRect(0);

サーフェイスの左上からオフセット(iOfs_x, iOfs_y)させて、ビットマップの幅と高さだけループを回しています。α情報の付加(colorの部分)は単純なビット演算ですね。メモリコピーはDWORD単位ですからポインタ演算を4倍して(4*xの部分)、4BYTEずつ書き込み位置をずらす必要があります。

これでα情報+アンチエイリアス付きのテクスチャが出来ました。後は、これを板ポリゴンいっぱいに貼り付ければ、1文字完成です。



A 板ポリゴンにテクスチャ貼り付け

 テクスチャの貼り付けは難しくありませんが、α情報を用いるためにいくつか設定が必要になります。デバイスに対して次のような設定をするとOKです。

// テクスチャセット
g_pD3DDev->SetTexture(0, pTexture);
g_pD3DDev->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE );
g_pD3DDev->SetTextureStageState(0, D3DTSS_COLOROP , D3DTOP_MODULATE );
g_pD3DDev->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_DIFFUSE );
g_pD3DDev->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TEXTURE );
g_pD3DDev->SetTextureStageState(0, D3DTSS_ALPHAOP , D3DTOP_MODULATE );
g_pD3DDev->SetTextureStageState(0, D3DTSS_ALPHAARG2, D3DTA_DIFFUSE ); // 板ポリのα値を利用

// レンダリングステート
g_pD3DDev->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE);
g_pD3DDev->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
g_pD3DDev->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);

 あとはデバイスにポリゴンを指定して描画すれば、美しい透過処理のかかった高速フォント描画が完了です。実際に作ってみましたが、ちゃんとアンチエイリアスのかかったフォントが描画できました。


640×480(Full Screen) MSゴシック 200pixcel 10文字 半透明処理・回転中

 ここまでの作業は大変面倒なのですが、一度テクスチャサーフェイスへの書き込みをしてしまえば、後はポリゴンの描画だけになるので非常に高速です。また、こうする事によって、板ポリゴンを動かせばフォントも自由に動かす事が出来ます。もちろん、回転や拡大縮小、半透明化、色の変化なども通常の絵と同じように扱えるのです。板ポリゴンのメッシュを増やせば、曲げたりもできるでしょうね。この自由度の高さがこの方法の最大の魅力です。

 ここまで読んで頂いてお分かりの通り、この方法は板ポリゴンを1文字単位で作成しています。これには訳があって、ノベルス系のゲームの場合「文字送り」が必要になるからです。文字が1文字ずつ出てくるあれを行うには、1文字ずつ板ポリゴン文字を作り、それを整列させる必要があります。板ポリゴンをフォント領域(空白を含めた大きさ)にしたのは、その整列を簡単に行えるよう考慮したためです。特にフォントを固定ピッチにすると、単純に並べるだけで済んでしまいます。
 テクスチャステージステートでは、実は板ポリゴンのα値を利用できるようにしています。これは、フォント文字を半透明にする時に非常に重宝します。これをうまく使うと、単なる文字送りではなくて、透明から半透明に文字がす〜っと出てくるような美しい効果も簡単に作ることが出来ます。そういうエフェクトは、ノベルスの雰囲気アップにもつながりますね。

 最後にクラス化の話です。上の方法を整理すれば、1文字板ポリゴンを生成するクラスは割と簡単に出来そうです。さらに、文字を並べて文字列にするクラスがあれば、従来の文字列を扱うのとほぼ同様の手軽さが手に入ります。それをノベルス系の文字送りを行うクラスなどに派生することも可能でしょう。