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

その66 Windows APIな力でアウトラインフォントを描画する:サンプルプログラム



 DirectX技術編その66「Windows APIな力でアウトラインフォントを描画する」で説明した内容を踏まえたサンプルプログラムです。実行すると様々なアウトラインフォントが描画されます。


サンプルスクリーンショット。見た目が大切なので100%サイズです。

 DirectX9のテクスチャにアウトラインフォントを直接穿っているため、毎フレーム作成するのではなくて、必要になった時に作成して後は使いまわすという使い方をします。ゲーム開始前にある程度作っておくとか、ひらがなは先に作っておいてmapに格納しておくなど、パフォーマンスを上げる工夫をすると良いかもです。


 サンプルを動かすために必要なファイルはありません。空のプロジェクトを立ち上げて下のコードを全部コピペすれば動きます。うまく動かない場合は掲示板にご連絡下さい。

○ 工夫の仕方

 createOutlineFontTexture関数がすべてのメンドクサイ作業をしてアウトラインフォントテクスチャを作成してくれています。ただ、このサンプルの関数の場合は文字ごとにコンパチブルなデバイスコンテキストを作成したりDIBSectionを作成してメモリを確保してもらうなど無駄が結構多いです。アウトラインフォントテクスチャを返してくれるクラスを作成してこれらリソースを予め作って使い回せばパフォーマンスが上がります。

// アウトラインフォント作成テスト

#pragma comment(lib, "dxguid.lib")
#pragma comment(lib, "d3d9.lib")
#pragma comment(lib, "d3dx9.lib")

#include <windows.h>
#include <tchar.h>
#include <d3d9.h>
#include <d3dx9.h>

TCHAR gName[100] = _T("アウトラインフォント作成テスト");


// アウトラインフォントテクスチャ作成
//  dev       : 描画デバイス
//  hWnd      : ウィンドウハンドル(NULLだとデスクトップのハンドルになります)
//  faceName  : TrueTypeフォント名
//  charSet   : フォントのキャラクタセット(LOGFONT構造体のリファレンスを参照下さい)
//  str       : 作成したい文字
//  fontHeight: フォント高
//  weight    : フォントの太さ(0〜1000)
//  penSize   : アウトラインの太さ(ピクセル)
//  edgeColor : アウトラインの色
//  fillColor : 塗りつぶし色
//  quality   : アンチエイリアス精度(1以上の整数)
//  info(出力): フォントの位置情報
//               left, top: (0,0)からのオフセット位置
//               right    : フォント全幅(次の文字の横位置になります)
//               bottom   : フォント全高

IDirect3DTexture9* createOutlineFontTexture(
        IDirect3DDevice9 *dev,
        HWND hWnd = NULL,
        const char *faceName = "HGP創英角ポップ体",
        unsigned char charSet = SHIFTJIS_CHARSET,
        const char *str = "ま",
        unsigned fontHeight = 450,
        unsigned weight = 0,
        int penSize = 5,
        unsigned edgeColor = 0xffE2B27A,
        unsigned fillColor = 0xff7FDBEB,
        int quality = 3,
        RECT *info = 0
) {
        struct Color {
                unsigned char A, R, G, B;
                unsigned color;
                Color(unsigned color) : color(color), B(color & 0xff), G((color >> 8) & 0xff), R((color >> 16) & 0xff), A((color >> 24) & 0xff) {}
        };
        Color cEdgeColor(edgeColor), cFillColor(fillColor);

        penSize *= quality;
        IDirect3DTexture9 *tex = 0;

        // デバイスコンテキスト作成
        HDC hDC = GetDC(hWnd);
        HDC memDC = CreateCompatibleDC(hDC);
        ReleaseDC(hWnd, hDC);   // 解放していいよ

        // フォント作成
        LOGFONTA lf  = {};
        lf.lfHeight  = fontHeight * quality;    // 拡大サイズで
        lf.lfCharSet = charSet;
        lf.lfWeight  = weight;
        lf.lfQuality = ANTIALIASED_QUALITY;
        memcpy(lf.lfFaceName, faceName, strlen(faceName));
        HFONT hFont = CreateFontIndirectA(&lf);
        HFONT oldFont  = (HFONT)SelectObject(memDC, hFont);     // フォントを設定しないとエラーになります

        // でっかい"ま"を描画するBMPを作成
        TEXTMETRICA tm;
        GLYPHMETRICS gm;
        MAT2 mat ={{0,1}, {0,0}, {0,0}, {0,1}};
        int len = IsDBCSLeadByte(str[0]) ? 2 : 1;
        UINT code = (len == 2 ? (unsigned char)str[0] << 8 | (unsigned char)str[1] : (unsigned char)str[0]);
        GetTextMetricsA(memDC, &tm);
        GetGlyphOutlineA(memDC, code, GGO_METRICS, &gm, 0, 0, &mat);

        RECT charRegion = {     // LT - RB
                gm.gmptGlyphOrigin.x - penSize / 2,
                tm.tmAscent - gm.gmptGlyphOrigin.y - penSize / 2,
                gm.gmptGlyphOrigin.x + gm.gmBlackBoxX + penSize / 2,
                tm.tmAscent - gm.gmptGlyphOrigin.y + gm.gmBlackBoxY + penSize / 2
        };

        RECT charWH = {0, 0, (gm.gmBlackBoxX + penSize + quality - 1) / quality * quality, (gm.gmBlackBoxY + penSize + quality - 1) / quality * quality};
        int bmpW = charWH.right;
        int bmpH = charWH.bottom;
        BITMAPINFO bmpInfo = {};
        bmpInfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
        bmpInfo.bmiHeader.biWidth = bmpW;
        bmpInfo.bmiHeader.biHeight = -bmpH;
        bmpInfo.bmiHeader.biPlanes = 1;
        bmpInfo.bmiHeader.biBitCount = 24;
        unsigned char *p = 0;
        HBITMAP hBitMap = CreateDIBSection(0, &bmpInfo, DIB_RGB_COLORS, (void**)&p, 0, 0);
        HBITMAP oldBMP = (HBITMAP)SelectObject(memDC, hBitMap);

        // BMP背景を青色で初期化
        HBRUSH bgBrush = (HBRUSH)CreateSolidBrush(RGB(0, 0, 255));
        FillRect(memDC, &charWH, bgBrush);
        DeleteObject(bgBrush);

        // でっかい"ま"のパスを描く
        // パス色は緑、塗は赤
        HPEN   hPen     = (HPEN)CreatePen(PS_SOLID, penSize, RGB(0, 255, 0));
        HBRUSH hBrush   = (HBRUSH)CreateSolidBrush(RGB(255, 0, 0));
        HPEN   oldPen   = (HPEN)SelectObject(memDC, hPen);
        HBRUSH oldBrush = (HBRUSH)SelectObject(memDC, hBrush);

        SetBkMode(memDC, TRANSPARENT);
        BeginPath(memDC);
        TextOutA(memDC, -charRegion.left, -charRegion.top, str, len);
        EndPath(memDC);
        StrokeAndFillPath(memDC);

        SelectObject(memDC, oldPen);
        SelectObject(memDC, oldBrush);
        SelectObject(memDC, oldFont);
        SelectObject(memDC, oldBMP);
        DeleteObject(hBrush);
        DeleteObject(hPen);
        DeleteObject(hFont);

        // DirectXのテクスチャにBMPの色分けを使いフォントを穿つ
        int texW = charWH.right / quality;
        int texH = charWH.bottom / quality;
        int q2 = quality * quality;
        dev->CreateTexture(texW, texH, 1, 0, D3DFMT_A8R8G8B8, D3DPOOL_MANAGED, &tex, 0);
        D3DLOCKED_RECT lockR;
        if (SUCCEEDED(tex->LockRect(0, &lockR, 0, 0))) {
                char *d = (char*)lockR.pBits;
                unsigned BMPPitch = (charWH.right * 3 + 3) / 4 * 4;
                for (int y = 0; y < texH; y++) {
                        for (int x = 0; x < texW; x++) {
                                unsigned &v = *((unsigned*)d + x + y * texW);   // テクスチャのピクセル位置
                                unsigned alph = 0;
                                unsigned edge = 0;
                                unsigned fill = 0;
                                // quality倍率分点を平均化
                                for (int i = 0; i < quality; i++) {
                                        for (int j = 0; j < quality; j++) {
                                                alph += p[(y * quality + i) * BMPPitch + (x * quality + j) * 3 + 0];
                                                edge += p[(y * quality + i) * BMPPitch + (x * quality + j) * 3 + 1];
                                                fill += p[(y * quality + i) * BMPPitch + (x * quality + j) * 3 + 2];
                                        }
                                }
                                alph /= q2;
                                edge /= q2;
                                fill /= q2;

                                // 透過度がある場合はエッジ色を採用
                                // 不透明の場合はブレンド色を採用
                                unsigned a = 0xff - alph;
                                if (a < 0xff) {
                                        // 半透明
                                        unsigned r = cEdgeColor.R;
                                        unsigned g = cEdgeColor.G;
                                        unsigned b = cEdgeColor.B;
                                        a = (a * cEdgeColor.A) >> 8;
                                        v = a << 24 | r << 16 | g << 8 | b;
                                } else {
                                        // 不透明
                                        unsigned r = ((cEdgeColor.R * edge) >> 8) + ((cFillColor.R * fill) >> 8);
                                        unsigned g = ((cEdgeColor.G * edge) >> 8) + ((cFillColor.G * fill) >> 8);
                                        unsigned b = ((cEdgeColor.B * edge) >> 8) + ((cFillColor.B * fill) >> 8);
                                        a = ((cEdgeColor.A * edge) >> 8) + ((cFillColor.A * fill) >> 8);
                                        v = a << 24 | r << 16 | g << 8 | b;
                                }
                        }
                }
                tex->UnlockRect(0);
        }

        DeleteObject(hBitMap);
        DeleteDC(hWnd, memDC);

        if (info) {
                info->left = charRegion.left / quality;
                info->top  = charRegion.top / quality;
                info->right = (gm.gmCellIncX + penSize) / quality;
                info->bottom = (tm.tmHeight + penSize) / quality;
        }
        return tex;
}


// ウィンドウプロシージャ
LRESULT CALLBACK WndProc( HWND hWnd, UINT mes, WPARAM wParam, LPARAM lParam ){
        if( mes == WM_DESTROY || mes == WM_CLOSE ) { PostQuitMessage( 0 ); return 0; }
        return DefWindowProc( hWnd, mes, wParam, lParam );
}


int APIENTRY _tWinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow )
{
        // アプリケーションの初期化
        MSG msg; HWND hWnd;
        WNDCLASSEX wcex = { sizeof(WNDCLASSEX), CS_HREDRAW | CS_VREDRAW, WndProc, 0, 0, hInstance, NULL, NULL, (HBRUSH)(COLOR_WINDOW+1), NULL, (TCHAR*)gName, NULL};
        if( !RegisterClassEx(&wcex) )
                return 0;

        RECT R = { 0, 0, 640, 480 };
        AdjustWindowRect( &R, WS_OVERLAPPEDWINDOW, FALSE );
        if( !( hWnd = CreateWindow( gName, gName, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, R.right - R.left, R.bottom - R.top, NULL, NULL, hInstance, NULL ) ) )
                return 0;

        // Direct3Dの初期化
        LPDIRECT3D9 g_pD3D;
        LPDIRECT3DDEVICE9 g_pD3DDev;
        if( !( g_pD3D = Direct3DCreate9( D3D_SDK_VERSION ) ) ) return 0;

        D3DPRESENT_PARAMETERS d3dpp = { 640, 480, D3DFMT_UNKNOWN, 0, D3DMULTISAMPLE_NONE, 0, D3DSWAPEFFECT_DISCARD, NULL, TRUE, TRUE, D3DFMT_D24S8, 0, D3DPRESENT_RATE_DEFAULT, D3DPRESENT_INTERVAL_DEFAULT }; 

        if( FAILED( g_pD3D->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd, D3DCREATE_HARDWARE_VERTEXPROCESSING, &d3dpp, &g_pD3DDev ) ) )
        if( FAILED( g_pD3D->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd, D3DCREATE_SOFTWARE_VERTEXPROCESSING, &d3dpp, &g_pD3DDev ) ) )
        {
        g_pD3D->Release();
        return 0;
        }

        // アウトラインフォント文字テクスチャ作成
        IDirect3DTexture9 *tex_ma = createOutlineFontTexture(g_pD3DDev, hWnd, "HGP創英角ポップ体", SHIFTJIS_CHARSET, "ま", 240, 500,  9, 0xffCC6677, 0x00CC6677, 4);
        IDirect3DTexture9 *tex_ru = createOutlineFontTexture(g_pD3DDev, hWnd, "MS 明朝"       , SHIFTJIS_CHARSET, "る", 300, 1000, 3, 0xff336677, 0x90ccaa66, 4);
        IDirect3DTexture9 *tex_pe = createOutlineFontTexture(g_pD3DDev, hWnd, "MS ゴシック"   , SHIFTJIS_CHARSET, "ぺ", 180, 500,  2, 0xff000000, 0xff789c33, 4);
        IDirect3DTexture9 *tex_ke = createOutlineFontTexture(g_pD3DDev, hWnd, "HG行書体"        , SHIFTJIS_CHARSET, "け", 300, 500,  5, 0x50336677, 0xdd336677, 4);

        // 文字列にしてみよう
        const char* str = "まるぺけつくろ〜";
        const int strNum = 8;
        IDirect3DTexture9 *tex_str[strNum];
        RECT strInfo[8];
        for (unsigned i = 0; i < strNum; i++)
                tex_str[i] = createOutlineFontTexture(g_pD3DDev, hWnd, "HGP創英角ポップ体", SHIFTJIS_CHARSET, str + i * 2, 70, 300, 2, 0xff503020, 0xffcc9080, 4, strInfo + i);

        ShowWindow(hWnd, nCmdShow);

        ID3DXSprite *sprite = 0;
        D3DXCreateSprite(g_pD3DDev, &sprite);

        // DirectXなので3Dモデルを
        struct Rand {float operator()(int v) {return (float)(rand() % v);}} r;
        const int meshNum = 500;
        ID3DXMesh *meshs[meshNum];
        D3DXMATRIX worlds[meshNum];
        for (unsigned i = 0; i < meshNum; i++) {
                D3DXCreateSphere(g_pD3DDev, 10.0f, 30, 30, &meshs[i], 0);
                D3DXMatrixTranslation(&worlds[i], r(200), r(200), r(200));
                D3DXMATRIX rot;
                D3DXMatrixRotationYawPitchRoll(&rot, r(100), r(150), r(200));
                worlds[i] *= rot;
        }

        D3DXMATRIX view, proj;
        D3DXMatrixLookAtLH(&view, &D3DXVECTOR3(0.0f, 0.0f, -400.0f), &D3DXVECTOR3(0.0f, 0.0f, 0.0f), &D3DXVECTOR3(0.0f, 1.0f, 0.0f));
        D3DXMatrixPerspectiveFovLH(&proj, D3DXToRadian(30.0f), 1.33333f, 1.0f, 3000.0f);
        g_pD3DDev->SetTransform(D3DTS_VIEW, &view);
        g_pD3DDev->SetTransform(D3DTS_PROJECTION, &proj);

        // ライト
        D3DLIGHT9 light;
        ZeroMemory(&light, sizeof(D3DLIGHT9));
        light.Direction = D3DXVECTOR3(-1, -2, 1);
        light.Type = D3DLIGHT_DIRECTIONAL; 
        light.Diffuse.r = 1.0f;
        light.Diffuse.g = 1.0f;
        light.Diffuse.b = 1.0f;
        g_pD3DDev->SetLight( 0, &light );
        g_pD3DDev->LightEnable( 0, true );
        g_pD3DDev->SetRenderState( D3DRS_LIGHTING, TRUE );

        D3DMATERIAL9 mat = {{1.0f, 1.0f, 1.0f, 1.0f}};
        g_pD3DDev->SetMaterial(&mat);

        // メッセージ ループ
        do{
                if( PeekMessage(&msg, NULL, 0, 0, PM_REMOVE) ){ DispatchMessage( &msg ); }

                g_pD3DDev->Clear( 0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(255,255,255), 1.0f, 0 );
                // 描画
                g_pD3DDev->BeginScene();

                for (unsigned i = 0; i < meshNum; i++) {
                        g_pD3DDev->SetTransform(D3DTS_WORLD, worlds + i);
                        meshs[i]->DrawSubset(0);
                }

                sprite->Begin(D3DXSPRITE_ALPHABLEND);
                sprite->Draw(tex_ma, 0, 0, &D3DXVECTOR3( 20.0f,  20.0f, 0.0f), 0xffffffff);
                sprite->Draw(tex_ru, 0, 0, &D3DXVECTOR3(180.0f, 165.0f, 0.0f), 0xffffffff);
                sprite->Draw(tex_pe, 0, 0, &D3DXVECTOR3(330.0f, 120.0f, 0.0f), 0xffffffff);
                sprite->Draw(tex_ke, 0, 0, &D3DXVECTOR3(420.0f, 170.0f, 0.0f), 0xffffffff);
                int pos = 0;
                for (unsigned i = 0; i < strNum; i++) {
                        sprite->Draw(tex_str[i], 0, 0, &D3DXVECTOR3(10.0f + pos, 480.0f - strInfo[i].bottom + strInfo[i].top, 0.0f), 0xffffffff);
                        pos += strInfo[i].right;
                }
                sprite->End();

                g_pD3DDev->EndScene();
                g_pD3DDev->Present( NULL, NULL, NULL, NULL );

        }while( msg.message != WM_QUIT );

        tex_ma->Release();
        tex_ru->Release();
        tex_pe->Release();
        tex_ke->Release();
        for (unsigned i = 0; i < strNum; i++)
                tex_str[i]->Release();

        g_pD3DDev->Release();
        g_pD3D->Release();
        return 0;
}