ホーム < ゲームつくろー! < DirectX技術編 < 高速フォント表示(アンチエイリアス無しバージョン)

その35 高速フォント表示(アンチエイリアス無しバージョン)


 DirectX技術編その5でアンチエイリアスがかかった透過度付きフォントを高速に描画する方法を紹介しました。これはWindows API関数のGetGlyphOutline関数を駆使してフォント情報をビットマップとして取得し、テクスチャに1ピクセルずつ書き込む作業を行いました。書き込みは1回のみで、あとはDirectXの持つテクスチャ描画に任せているので高速にフォントが表示できる仕組みになっています。

 その5で使ったGetGlyphOutline関数は、アンチエイリアスが「かかっていない」ビットマップも取る事ができます。やる事はこの関数の第3引数にGGO_BITMAPというフラグを設定するだけなのですが、取得できるビットマップが「モノトーンビットマップ」である点がその5とは違います。モノトーンビットマップとは1ピクセルを「1ビット」で表現しているビットマップです(その5では1ピクセル1バイトでした)。よって、テクスチャへピクセルを書き込む際の処理が異なってしまいます。

 ここではアンチエイリアスがかかっていないモノトーンビットマップからピクセル情報を読み込んで、テクスチャに書き込む高速フォント表示アンチエイリアス無しバージョンを紹介します。



@ モノトーンビットマップとは?

 GetGlyphOutline関数の第3引数にGGO_BITMAPフラグを指定して取得できるモノトーンビットマップというのは、1ピクセルの情報を1ビットで表現しています。と言ってもピンと来ないかもしれませんので、実際にGetGlyphOutline関数から取られるモノトーンビットマップの情報と、それをプロットした図を以下に示します。


お…意外とでかい…(汗)

 これはMS明朝でフォントの大きさを32として「あ」を描画した場合です。右に並ぶ数字(16進数)がGetGlyphOutline関数から得られるモノトーンビットマップのデータです。一番上の列で説明します。最初は「01」ですが、これは「0000 0001」というビットの並びを意味します。次の「80」は「1000 0000」ですね。薄黄色を付けた部分が、このゼロイチに対応しています。同様に全部プロットすると「あ」が出現します。

 つまり、モノトーンビットマップをテクスチャにプロットするには、右列のバイトデータからビットを読み取って、テクスチャのピクセルフォーマットに変換すれば良いわけです。



A ビット→アルファ情報への変換手順

 C言語の基本単位は「バイト」です。ポインタ演算もバイト単位が最小となります。ですから「0001 1100」というビットの並びを順番に取るというのは、実はC言語は苦手なんです。モノトーンビットマップはその苦手なビットを順番に取り出す作業をしなければなりません。

 GetGlyphOutline関数から取得できるフォントサイズ情報を格納したGLYPHMETRICS構造体には、フォントを囲うぎりぎりのサイズが取られます。横幅が13ピクセルであればしっかりと「13」と格納されるわけです。そのぎりぎりのフォントが収まる「位置(オフセット)」というのは、例えば上の「あ」のように決まっています。この位置合わせについてはDirectX技術編その5およびProgramming TIPs編その1『GetGlyphOutline関数のフォント位置』で決定版を説明しておりますので、ここではそれについては触れません。それらの情報がもう取られていると言う前提で話を進めます。

 以上を踏まえてビットをアルファ情報(色情報)へ変換してみましょう。

 まず、*ptrというポインタの先にモノトーンビットマップの情報がバイト単位で格納されているとします。上の例であれば、

ptr ptr+1 ptr+2 ptr+3 ptr+4 ptr+5 ptr+6 ・・・
01 80 00 00 00 c0 00 ・・・

です。そして、以下の各変数に必要情報が格納されているとします。

int iOfs_x = GM.gmptGlyphOrigin.x;                      // テクスチャ書き込み位置のオフセット横位置
int iOfs_y = TM.tmAscent - GM.gmptGlyphOrigin.y;        // テクスチャ書き込み位置のオフセット横位置
int iBmp_w = GM.gmBlackBoxX                             // 取得したBMPの横ピクセル数
int iBmp_h = GM.gmBlackBoxY;                            // BMPの縦ピクセル数
int iUseBYTEparLine = (1 + (iBmp_w / 32))*4;            // 1行に使用しているBYTE数(4バイト境界あり)
int x, y;                                               // テクスチャの書き込み位置

iOfs_xにはテクスチャに書き込むためのオフセット横位置が入ります。先程の「あ」の例ですと、右に4つずれていますから、iOfs_x=4です。
iOfs_yは同様にオフセット縦位置です。計算方法は上の通りにすれば間違いありません。
iBmp_wには取得したビットマップ(ぎりぎりの範囲)の横ピクセル数を格納します。
iBmp_hは縦ピクセル数です。
iUseBYTEparLineにはモノトーンビットマップの1行を格納するのに必要なバイト数が入ります。これは4バイト境界で計算されます。例えばiBmp_wが68ピクセルであった場合、これを32で割ると2、それに1を足すと3(ダブルワード)が出てきます。それを4倍するとバイト単位(12バイト)が算出されます。こういうのは慣れですね。
最後のx,yはテクスチャの書き込み位置です。

 その他の変数についてはサンプルプログラムにも全く同じ箇所がありますので説明は省略致します。

 テクスチャへの書き込み位置は、オフセットがありますので横方向はx=iOfs_xからx=iOfs_x+iBmp_w、縦方向はy=iOfs_yからy=iOfs_y+iBmp_hまでとなります。この間でループを回します。ループの中ではC言語が苦手とするビットを抽出する作業をこなします。これはソースを先に見た方が良いかもしれませんね。プログラムは次のようになります。

for(y=iOfs_y; y<iOfs_y+iBmp_h; y++)
{
   for(x=iOfs_x; x<iOfs_x+iBmp_w; x++){
   {
      DWORD num = (x-iOfs_x) / 8;       // @ 現在のxが1行の何BYTE目かを算出
      BYTE bit = (x-iOfs_x) % 8;        // A 現在のxが1バイト内の何ビット目かを算出
      BYTE mask = ((BYTE)1)<<(7-bit);   // B 現在のビット数のマスク作成(マスクは逆さまになる)
      BYTE Cur = *((BYTE*)ptr + iUseBYTEparLine*(y-iOfs_y)+num);     // C BMPのバイト値を取得
      Cur &= mask;                      // D 作成したマスクと現在のバイト位置とで論理積演算
      Alpha = (Cur >> (7-bit)) * 255;   // E Curに立ったビットフラグを最下位ビットまでシフトしてアルファ値に変換
      Color = 0x00ffffff | (Alpha<<24); // F アルファ値を登録

      // G テクスチャに書き込み
      memcpy((BYTE*)LockedRect.pBits + LockedRect.Pitch*y + 4*x, &Color, sizeof(DWORD));
   }
}

 わかりやすくするために最適化はしていません。

 まず@ではテクスチャの横位置xが取得したビットマップの何バイト目に所属しているかを計算しています。(x - iOfs_x)とするとテクスチャの位置からビットマップのピクセル位置に変換できます。モノトーンビットマップの場合「1バイトに8ピクセル分の情報がある」ので8で割っているわけです。

 Aではxが1バイトの中の何ビット目に位置しているかを計算しています。例えば(x - iOfs_x)=3だとするとbit=3と格納されます。尚、ここでは「最上位ビットを0、最下位ビットを7」としています。

byte(0x75) 7 5
bit番号 0 1 2 3 4 5 6 7
値(ピクセル) 0 1 1 1 0 1 0 1
大丈夫ですよね(^-^;

 BではAで求めた現在のbit数からマスクを作成しています。これが無いとある1つのビットだけを抽出するのは面倒になります。例えばbit=3だとすると、Bの計算では「0001 0000」というマスクが作成されます。

 Cでは現在のビットマップの値をバイト単位で取得しています。これはポインタ演算です。ptrがビットマップの先頭で、それにiUseBYTEparLine(1行のバイト数)×高さで行レベルの移動、さらにnum(現在の横方向のバイト数)を足し算して、適切な箇所を抽出しています。ここは、色々な値を入れてみれば、正しく計算がされている事が分かると思います。

 DではBで作成したマスクと、Cで取られた現在のバイト数の論理積を取っています。上の表の例で行きますと、

現在のbyte(0x75) 7 5
bit番号 0 1 2 3 4 5 6 7
値(ピクセル) 0 1 1 1 0 1 0 1
マスク(bit=3) 0 0 0 1 0 0 0 0
&(論理積) 0 0 0 1 0 0 0 0
大丈夫…ですよね(^-^;

となるわけです。3bit目に色があるか否かが論理積のビットに抽出されていますね。

 Eでは抽出されたビットを一度最下位ビットに戻し( Cur >> (7-bit) )、それに255を掛けています。これは、要はアルファ値(0xff)を求めたいわけです。もし論理積の結果Curにビットが1つも立っていなければ、この掛け算の結果も0(0x00)になるわけです。条件分岐をしない分ちょっとだけパフォーマンスが速いかもしれません(本当にちょっとだけだと思いますが)。

 Fで求めたAlpha値を左に24bitシフトしてアルファ値の位置に持って行きます。ここで色情報が完全に確定します。

 Gはテクスチャのピクセル(ARGBフォーマットDWORD単位)に求めたColorを書き込んでいます。memcpyではなくて[ ]演算子で代入してもかまいません(ポインタをDWORD*に変換する必要はあります)。

以上の作業をビットマップが持つピクセル数分だけ繰り返すと、アンチエイリアスのかかっていない透過度付きのフォントテクスチャが完成します。やれやれです(^-^;


 以上のプロセスを加味したサンプルプログラムを用意しておりますので、コピペしてお使い下さい。デバッグしてメモリの動きなどを見れば参考になるかと思います。アンチエイリアスのかかっていないフォントを作成する他の手段は幾つかあると思います(アンチエリアスフォントの透過度を閾値で区切る等)。それらを統合してクラス化してみると楽しいかもしれませんね。



B 謝辞

 この記事を書くきっかけを与えて下さいましたらでおんさんに感謝申し上げます。