久しぶりに DirectX11 のコードを書いていました。初期化から画面のクリア処理まで圧倒的に短く100行くらいで終わりました。近年流行の低レイヤーグラフィックスAPIとは全然違いますね。 昔は DirectX11 の初期化コードでも長い!と思っていた頃がありましたが、今回このくらいの長さだと何も見ないでも記述できたこともあり手軽だなと思うようになりました。
Direct2D と DirectWrite
DirectX11 で文字を出すのに一苦労する話題はよく聞きます。
今回 DirectX11 のバックバッファ部分に文字を描く話を紹介します。昔からある方法なのでやっている人も多いと思いますが、Direct2D と DirectWrite を使います。
Direct2D
Direct2D とは、名前の通り2Dグラフィックス専用のAPIです。レイヤーとしては DirectX11 よりもアプリケーション側になります。
DirectWrite
DirectWrite とは、テキストを描画するためのAPIです。これもまたレイヤーとしては DirectX11 よりもアプリケーションよりで、 Direct2D の上に乗るものとなります。
実装方法
DirectX11 も登場してから長く、今となっては簡単な方法が多くの環境で使えることと思います。そのため、同期処理を自分で記述しないで DXGI にお任せする簡単な方法を紹介します。
初期化手順
以下の手順で、Direct2D に必要なオブジェクトの準備を行います。
- DXGIデバイス(IDXGIDevice)を DirectX11 デバイスから取得
- ID2D1Factory を生成し、Direct2D デバイスを生成
- Direct2D デバイスコンテキストを生成
- スワップチェインクラスのバックバッファから DXGISurface を取得して、Direct2D描画先を取得
DirectX11 のデバイスとスワップチェインが準備できていれば、これらの手順は簡単です。QueryInterface で ID3D11Device から IDXGIDevice は取得できるので、 WRL を用いている環境ではさらに簡単です。これらの手順をコードで示したものが以下の通りです。
D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, __uuidof(ID2D1Factory3), &factoryOptions, &factory);
ComPtr<IDXGIDevice> dxgiDevice;
m_d3d11Device->QueryInterface(IID_PPV_ARGS(&dxgiDevice));
factory.As(&m_d2dFactory);
m_d2dFactory->CreateDevice(dxgiDevice.Get(), &m_d2dDevice);
m_d2dDevice->CreateDeviceContext(D2D1_DEVICE_CONTEXT_OPTIONS_NONE, &m_d2dDeviceContext);
ComPtr<ID3D11Texture2D> backBuffer;
m_swapChain->GetBuffer(0, IID_PPV_ARGS(&backBuffer));
backBuffer.As(&m_dxgiSurface);
// 描画先 Bitmap 取得.
D2D1_BITMAP_PROPERTIES1 bitmapProps = D2D1::BitmapProperties1(
D2D1_BITMAP_OPTIONS_TARGET | D2D1_BITMAP_OPTIONS_CANNOT_DRAW,
D2D1::PixelFormat(DXGI_FORMAT_UNKNOWN, D2D1_ALPHA_MODE_PREMULTIPLIED),
float(dpi), float(dpi)
);
m_d2dDeviceContext->CreateBitmapFromDxgiSurface(m_dxgiSurface.Get(), &bitmapProps, &m_d2dBitmap);
2D描画用のブラシやフォントに関して準備
GDIでの描画経験があれば、ブラシを生成して描画していたことを思い出すでしょう。Direct2Dも同じようにブラシ(ID2D1SolidColorBrush) を用意します。またテキストの描画では IDWriteTextFormat を生成します。
m_d2dDeviceContext->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::WhiteSmoke), &m_brushWhite);
hr = m_dwriteFactory->CreateTextFormat(
L"メイリオ", NULL, DWRITE_FONT_WEIGHT_NORMAL,
DWRITE_FONT_STYLE_NORMAL, DWRITE_FONT_STRETCH_NORMAL,
44, // サイズ
L"ja-JP", &m_textFormat
);
描画手順
DirectX11 の描画処理が終わった後、スワップチェインの Present を実行する手前までの間で Direct2D+DirectWrite で描画処理を実装します。
- Direct2D デバイスコンテキストで BeginDraw / EndDraw を実行し描画範囲を囲む
- 描画先を SetTarget で設定
- テキストや2Dオブジェクトを描画する
このような感じになります。コードで示すと以下の通りです。
m_d2dDeviceContext->BeginDraw();
m_d2dDeviceContext->SetTarget(m_d2dBitmap.Get());
D2D1_ROUNDED_RECT roundRect{ ... (省略) };
m_d2dDeviceContext->DrawRoundedRectangle(&roundRect, m_brushBlue.Get(), 8.0f, m_stroke.Get());
std::wstring text = L"Hello, Direct2D & DirectWrite";
D2D1_RECT_F textRect;
textRect = roundRect.rect;
m_d2dDeviceContext->DrawTextW(
text.c_str(), text.size(),
m_textFormat.Get(), textRect, m_brushWhite.Get());
m_d2dDeviceContext->EndDraw();
描画結果
面白みは無いですが、描画結果としては以下の通りです。
まとめ
今回は DirectX11 アプリでのテキストや図形を描画する1つの方法を紹介しました。外部ライブラリを使わないでも文字列や図形を Direct2D, DirectWrite という DirectX ファミリの力を借りることで実装出来ます。
以前の DXGI 世代が古い頃、DirectX11.0 が出始めの頃は DirectX10とDirect2D らの共存のみが考慮されていて、 IDXGIKeyedMutex という同期オブジェクトを用いて、同期を取りながら描画処理を実装する必要がありました。それに比べると今は簡単に処理できるようになりました。
コメント
描画手順に示されているコードを、以下のように書き換えると書式付きで描画できます。
それはC++20で採用された std::format_toを使います。
std::wstring text = L”Hello, Direct2D & DirectWrite”;
を以下のように書き換えます。
std::wstring text;
std::format_to(std::back_inserter(text), L”Hello, Direct2D & DirectWrite”);
ちなみにというヘッダーファイルになっています。
Visual Studio 2019および2022で利用が可能となっています。
Visual Studio 2017ではこのヘッダーファイルが利用できないのでgithubで公開されている{fmt}を使えば同等の処理をしてくれます。