ホーム < ゲームつくろー! < DirectX技術編

その66 WindowsAPIな力でアウトラインフォントを描画する


 DirectX技術編その5「高速フォント表示」ではTrueTypeフォントをテクスチャに描き込んで描画する方法を紹介しました。フォントテクスチャを一度だけ作成し、あとはそれを並べるだけでフォント文字が描画できるため、高速に描画ができるというわけです。

 この記事を挙げた所掲示板にて「アウトラインフォント(袋文字、縁取り文字)は描画できないか?」というご質問を頂きました。確かに縁取り文字が描画できれば表現の幅が広がります。しかしどうやれば良いのか…。テクスチャを重ねて云々ではちょっと太刀打ちできそうにありません。そこで色々あれこれと調べるうち、WindowsAPIの力をドカッと使うとなんと可能である事がわかりましたので、ここでご報告です(^-^)



@ 決め手その1「CreateDIBSection API」

 アウトラインフォントを描画する指針は「高速フォント表示」と同じで、一度システムメモリ上にフォントの絵を書き出し、それをテクスチャにコピーして使用します。つまり、メモリ上に絵を描ければ勝ちというわけです。絵を描くにはメモリ上に直接数値を置いていくのが原始的な方法ですが、それじゃ埒が明きません。出来ればWindows APIのGDI系関数を使ってメモリに描画したいんです。それを可能にするのがCreateDIBSection APIです。

 CreateDIBSection APIはDIB(デバイス独立BMP)を作成するAPIです。この関数、作成したいBMPを指定すると、何とそのメモリブロック位置を教えてくれます。作成されたBMP(HBITMAP)はデバイスコンテキストに渡せばそこに描画してくれますから、WIndows APIでの描画結果がメモリ上に刻まれてしまうというカラクリです!やった、これで描き放題です(^-^)

 CreateDIBSection関数でDIBを作成するには次のようにパラメータを与えます:

CreateDIBSection関数でDIBを作成
// BMP情報作成
BITMAPINFO bmpInfo = {};
bmpInfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bmpInfo.bmiHeader.biWidth = 256;
bmpInfo.bmiHeader.biHeight = -256;
bmpInfo.bmiHeader.biPlanes = 1;
bmpInfo.bmiHeader.biBitCount = 24;

// DIBを作成しメモリブロックを貰う
unsigned char *p = 0;
HBITMAP hBitMap = CreateDIBSection(0, &bmpInfo, DIB_RGB_COLORS, (void**)&p, 0, 0);

作成するBMPの高さをマイナス値で指定しているのに注目です。BMPは通常はメモリの若い方に画像の下側の色情報法を格納します。上下が逆さまに入るわけです。そこで高さにマイナス値を入れると上下を反転したBMPを作成してくれます。色深度は24bitです。

 DIBセクションに上記パラメータを与えると、256×256×24bitなBMPをシステムメモリ上に確保してくれ、pにその先頭ポインタを返してくれます。このpは読み込みはもちろんの事書き込みもできますので、BMPにプログラム上から数値的に絵を描く事もできちゃいます。ただし、作成されるのはBMPなので「4バイトアライン」が当然発生します。ちょっと注意です。

 作成したBMPに描画するには、それをデバイスコンテキストに設定する必要があります。しかし、DirectXの描画先に指定しているウィンドウが持っているデバイスコンテキストを指定すると、描いた結果が直接描画されてしまいます。これはまずい。そこで、互換性のあるメモリデバイスコンテキスト(コンパチブルデバイスコンテキスト)を作成します:

メモリデバイスコンテキストを作成
// コンパチブルDC作成
HDC hDC = GetDC(NULL);
HDC memDC = CreateCompatibleDC(hDC);
ReleaseDC(NULL, hDC);

このmemDCに作成したDIBを設定し、フォントを描画します。



A 決め手その2「StrokeAndFillPath」

 描くべきキャンパスはできました。ここにフォント文字をWindows APIで描画すれば、その結果もメモリに直ちに刻まれます。ただ、今回描きたいのはアウトラインフォントです。Windows APIには直接アウトラインフォントを描画するメソッドはありませんが、StrokeAndFillPath APIを用いるとGDI系で描画されたパスを指定のペンで描画し、その内部を指定のブラシで塗ってくれます。フォントもパス描画なのでこのAPIの恩恵によってアウトラインフォントになってしまうんです。

 StrokeAndFillPath APIの使い方は次の通り:

メモリデバイスコンテキストを作成
SetBkMode(memDC, TRANSPARENT);
BeginPath(memDC);

TextOutA(memDC, x, y, str, len);

EndPath(memDC);
StrokeAndFillPath(memDC);

最初にフォントを描画する際に背景が塗り潰されるのを防ぐためにSetBkModeを透過(TRANSPARENT)にします。次にBeginPath関数で「これからパスを描きますよ〜」と宣言します。こうするとTexiOut関数はパス描画に変わり、直接は描画されなくなります。描画し終わったらEndPath関数でパス描画の終了宣言をして、最後にStrokeAndFillPath関数を呼び出して現在保持しているペンとブラシでパスを描画してもらいます。この時描画先のDCを指定します。

 実際に適当なペンとブラシとフォントを指定して通常のウィンドウに描画してみましょう:

ごきげ〜ん(^-^)/。先のメモリ上にあるDIBをデバイスコンテキストに持たせれば、この色情報をメモリから直接読み込めます。では、それぞれの文字をきっちり納めるサイズのテクスチャを次に作ることにしましょう。



B 文字をきっちり納める幅高を求める

 フォント文字をきっちり納める矩形サイズはGetGlyphOutline APIで算出できます。DirectX技術編その5「高速フォント表示」ではこの関数から直接フォントBMPをもらっていましたが、この関数に文字だけ与えると、その文字を納める情報だけを返してくれます。ただそれだけではちょっと情報不足でして、GetTextMetrics関数でフォント自体の情報も一緒に得ます:

GetGlyphOutline関数からフォント幅高情報を得る
chat *str = "ま";

int len = IsDBCSLeadByte(str[0]) ? 2 : 1;
UINT code = (len == 2 ? (unsigned char)str[0] << 8 | (unsigned char)str[1] : (unsigned char)str[0]);

GLYPHMETRICS gm;
MAT2 mat ={{0,1}, {0,0}, {0,0}, {0,1}};
GetGlyphOutline(memDC, code, GGO_METRICS, &gm, 0, 0, &mat);

TEXTMETRICA tm;
GetTextMetricsA(memDC, &tm);

まず幅高を知りたい一文字を定義します。上では"ま"を例にしています。次に文字が2バイト文字か否かをIsDBCSLeadByte関数に教えてもらいます。続いて文字をUINT型にキャストします。これはGetGlyphOutline関数が要求する為です。この文字コードとフォントを設定したデバイスコンテキスト、そしてGLYPHMETRICS構造体をGetGlyphOutline関数に渡すと、その文字を納める情報をgmに返してくれます。さらにフォント自身の情報をGetTexMetrics関数を通しTEXTMETRIC構造体に返してもらいます。

 GLYPHMETRICS構造体のgmBlackBoxXとgmBlackBoxYがフォント"ま"をきっちり納めるサイズです。gmCellIncXには左右の余白を含めたフォント全幅が格納されています。しかしgmCellIncYには値が格納されて来ません。gmptGlyphOrigin.xとgmptGlyphOrigin.yにはフォントをぴったりと納める枠の左上座標が格納されます。この原点は「X軸がベースライン、Y軸がフォント全幅左端」になっています…え〜と、下の図の通りです(^-^;:

フォントを(0, 0)に描いた時、左上は(gm.gmptGlyphOrigin.x, tm.tmAscent - gmptGlyphOrigin.y)、幅高が(gm.gmBlackBox.x, gm.gmBkackBoxY)という事です。では、これに従ってAのアウトラインフォント"ま"をぴったり囲う矩形を描画してみましょう:


300%拡大中

はい、ピッタ…ん〜?はみ出してね?そうなんです、上は太さ5のペンで描いたため、2.5ピクセルほど縁取り分はみ出してしまうんです。そこでペンの太さの半分だけ上下左右に矩形を拡大します:

今度こそピッタリです。アウトラインフォントをぴったり囲う矩形サイズは結局こうなります:

ぴったりサイズ
RECT r = {
    gm.gmptGlyphOrigin.x - penSize / 2,                  // 左
    tm.tmAscent - gm.gmptGlyphOrigin.y - penSize / 2,    // 上
    gm.gmBlackBoxX + penSize,                            // 幅
    gm.gmBlackBoxY + penSize                             // 高さ
};

Rectangle(hDC, r.left, r.top, r.left + r.right, r.top + r.bottom);

 さて、上の絵をDirectXのテクスチャに転写するとどうなるでしょうか?当然ですが「白い背景の"ま"」になります。白い部分は透明にしないといけないわけです。結局、1ピクセルずつ色味を見て、白かったら透過度を100%にしてテクスチャに穿って行かないといけない事がわかります。しかしです、フォントの中身を白色に塗りたい時はどうなるでしょうか?背景の白と中身の白とが区別できず、塗部分も透明になってしまいます。この問題、どう解決したら良いでしょうか?



C 塗りとパスと透過度を色成分に分ける!

 上の図を見ると、背景、縁(パス)、そして塗りの3つの部分に分けられる事がわかります。と言うことは、例えばR成分を塗り、G成分を縁、そしてB成分を透過度と分けてフォントを描画し、DirectXのテクスチャに書き込む際にそれを色や透過度情報に変換していけば、問題がうまく解決できる事がわかります!!実際、そういうフォントを描くとこんな感じになります:

この色分けした情報を元に、DirectXのテクスチャ側で背景(B)を透明に、緑を縁に、そして赤を塗りとして色を穿って行きます。

 ちょっと情報を整理しましょう。色の情報はすでにDIBSectionを通してメモリブロックにあります。"ま"を(0, 0)に描いた時の"ま"をぴったり納める矩形の左上regionLTは、

  regionLT = ( gm.gmptGlyphOrigin.x - penSize / 2, tm.tmAscent - gm.gmptGlyphOrigin.y - penSize / 2);

で与えられます。その矩形の幅と高さは、

  regionWH = ( gm.gmBlackBoxX + penSize, gm.gmBlackBoxY + penSize );

となります。ペンの太さを考慮するのがポイントです。この幅高でIDirect3DTexture9を生成しておきます。完全に透明な部分は青色、縁取りは緑、塗りは赤色で塗り分け、テクスチャに点を穿つ時の情報とします。

 ところで、上の図のようにフォントを(0, 0)基準で描くとテクスチャに点を打つ時の座標計算で大混乱してしまいます。そこで、矩形の左上がぴったり(0, 0)位置になるように、フォントを描画する時にTextOut関数で予めオフセットしてしまいましょう:

オフセットして"ま"をメモリデバイスコンテキストに描画
SetBkMode(memDC, TRANSPARENT);
BeginPath(memDC);

TextOutA(memDC, -regionLT.x, -regionLT.y, str, len);

EndPath(memDC);
StrokeAndFillPath(memDC);

すると、下の図のような描かれ方になります:

ぴったりです。こうしておけば、(0, 0)の位置から幅高分だけ切り取って、テクスチャの同じ座標に点を穿って行けば良いことになります。

 さてテクスチャに対するその点の穿ち方ですが、上の絵は24bitのメモリBMPです。一方DirectXの透過テクスチャは32bitが普通です。そのため、一点ずつ24bitを32bitに変換して点を打っていかなければなりません。ただ、ここでハマるのが「4バイトアライン」です。BMPは横幅バイト(ピッチサイズ:Pitch Size)が4バイトの倍数で確保されます。上の"ま"の横幅が例えば193ピクセルだとして、それとぴったり同じDIBSectionを作ったとすると、DIBSectionのピッチサイズは193×3バイト=579バイトではなくて、4で割り切れる580バイトになります。このアラインサイズ(ピッチサイズ)を得るには色々な方法がありますが、例えば次の式で計算ができます:

DIBのピッチサイズの計算方法
int pixelNum = 193;  // 1ラインピクセル数
int pixelSize = 3;  // 3バイト(24bit)
int align = 4;      // 4バイトアライン

int pitchSize = (pixelNum * pixelSize + align - 1) / align * align;

(193 * 3 + 4 - 1) / 4 * 4 = (582) / 4 * 4 = 145 * 4 = 580バイト、です。

 ということで、テクスチャへ点を打つのは以下のようになります:

メモリBMPの色情報をDirectXのテクスチャに転写
char *p;   // DIBSectionの先頭ポインタ
int pichSize = /*上の*/;
unsigned edgeColor;    // アウトラインの色
unsigned paintColor;    // 塗りつぶし色

D3DLOCKED_RECT lockR;
tex->LockRect(0, &lockR, 0, 0);   // テクスチャロック

for (unsigned y = 0; y < regionWH.y; y++) {
    for (unsigned x = 0; x < regionWH.x; x++) {
        char *src = p + y * pitchSize + x * 3;
        unsigned *dst = (unsigned*)lockR.pBit + y * lockR.Pitch + x;

        // 色の並びはBGR
        char B = src[0];
        char G = src[1];
        if (B)
            *dest = 0;   // 完全透明
        else if (G) {
            *dest = edgeColor;   // アウトライン色
        else
            *dest = paintColor;  // 塗りつぶし色
    }
}

tex->UnlockRect(0);

これでフォントサイズぴったりの透過処理が入った32bitカラーなフォントテクスチャができたので、板ポリに貼るなど自由にフォントを並べる事ができます。試しにID3DXSpriteに貼ってみました:


"ま"(^-^)/

DirectX上なので背景に3Dな球を散りばめてみました。ちゃんとアウトラインフォントが描けています。

 ところで、この"ま"。確かに描けていますがジャギがきちゃないです。これは元のDIBに刻印されたフォント自体がジャギっているからです。これを解消するには、例えば倍のサイズのフォントをDIBに描き、テクスチャに転写する段階で平均化します。その作業は…んむぅ〜〜説明するにもうキャパシティオーバー!という事で、サンプルを作成しました。結果を一先ずどうぞ:

左がジャギ処理なし、右が元のフォントの4倍サイズのDIBから16ピクセル平均を取って点を穿った豪華なアンチエイリアスが入った"ま"です。もう、段違いですよね(^-^)

 こういうアウトラインが入ったフォントテクスチャを作成できる関数をサンプルに公開致しましたので、自由にお使い下さい!



D 謝辞

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