Windows と C++

DirectX プログラミング用の最新 C++ ライブラリ

Kenny Kerr

コード サンプルのダウンロード

私は、多くの DirectX コードを記述してきました。DirectX に関する記事も幅広く執筆してきました。DirectX のオンライン トレーニング コースを作成したこともあります。DirectX は、一部の開発者が考えているほど難しいものではありません。確かに、学習曲線というものはありますが、一度乗り越えてしまえば、DirectX が機能する理由と方法を理解するのは難しくありません。しかし、DirectX ファミリの API には、より簡単に使用する余地があることは認めます。

数日前の晩、私はこの問題を解決しようと決意しました。私は徹夜で小さなヘッダー ファイルを作成しました。数日後には、コードは 5,000 行に達しました。目標は、Direct2D を使用したアプリケーション作成をより簡単にするために利用できるツールを提供することであり、現在大きく広まっている "C++ は難しい"、"DirectX は難しい" などの意見すべてに挑戦することでした。DirectX に新しい面倒なラッパーを作成するつもりはありませんでした。代わりに C++11 を利用して、中核となる DirectX API で領域と時間のオーバーヘッドを引き起こすことなくより単純な DirectX 用 API を作成することにしました。この私が開発したライブラリについては、dx.codeplex.com(英語) を参照してください。

ライブラリ自体は、dx.h という単一のヘッダー ファイルのみで構成されています。CodePlex で公開している他のソース コードは dx.h の使用例です。

今回はこのライブラリを使用して、DirectX に関連したさまざまな一般的な作業をより簡単に実行する方法について紹介します。また、C++11 を活用して Microsoft .NET Framework のような影響の大きいラッパーを利用することなく従来の COM API をさらに快適にする方法を理解していただけるよう、このライブラリの設計についても説明します。

もちろん、今回の話題の中心は Direct2D です。依然として Direct2D は、アプリケーションとゲームの幅広いクラスで DirectX を利用するための最も簡単で効率的な方法です。多くの開発者は、2 つの正反対のグループに分類されるようです。1 つは、さまざまなバージョンの DirectX API の経験がある筋金入りの DirectX 開発者たちです。このような開発者は長年の DirectX の進化によってかたくなになっており、入会条件が厳しい排他的なクラブにいることを幸せに感じています。このクラブには一握りの開発者しか入ることができません。もう 1 つのグループは、DirectX は難しいという話を聞いて、DirectX には近寄りたくないと考えている開発者たちです。この開発者たちは、当然ながら、C++ も拒否する傾向もあります。

私はこのどちらのグループにも属していません。C++ と DirectX は難しくある必要はないと考えています。先月のコラム (msdn.microsoft.com/magazine/dn198239、英語) では、Direct2D 1.1 について紹介し、デバイスを作成してスワップ チェーンを管理するための、前提条件となる Direct3D と DirectX Graphics Infrastructure (DXGI) コードについて紹介しました。GPU または CPU のレンダリングに適した D3D11CreateDevice 関数で Direct3D デバイスを作成するコードは、約 35 行です。しかし、私が作成した小さなヘッダー ファイルを使用すると、その長さは次のようになります。

auto device = CreateDevice();

CreateDevice 関数は、Device1 オブジェクトを返します。すべての Direct3D 定義は Direct3D 名前空間に含まれているので、より明示的に次のように記述することもできます。

Direct3D::Device1 device = Direct3D::CreateDevice();

Device1 オブジェクトは、DirectX 11.1 リリースで導入された Direct3D デバイス インターフェイスである ID3D11Device1 COM インターフェイス ポインターの、単なるラッパーです。Device1 クラスは、Device クラス (元の ID3D11Device インターフェイスのラッパー) から派生しています。このクラスは、1 つの参照を表しており、インターフェイス ポインターそのものを保持しているだけの場合よりも多くのオーベーヘッドが発生することはありません。Device1 とその親クラスの Device は、インターフェイスと言うよりも標準的な C++ クラスであることに注意してください。これらをスマート ポインターと考えることもできますが、さすがにそれは単純化しすぎでしょう。確かに、これらのクラスでは参照カウントを扱い、選択したメソッドを直接呼び出す "->" 演算子を提供しますが、その真価が発揮されるのは dx.h ライブラリから提供される多くの非仮想メソッドを使用するときです。

例を挙げて説明しましょう。他のメソッドや関数に渡す Direct3D デバイスの DXGI インターフェイスが必要になる場合はよくあります。この DXGI インターフェイスは、次のように地道に記述できます。

auto device = Direct3D::CreateDevice();
 wrl::ComPtr dxdevice;
 HR(device->QueryInterface(dxdevice.GetAddressOf()));

これは確かに機能しますが、DXGI デバイス インターフェイスを直接扱う必要も発生しています。また、IDXGIDevice2 インターフェイスが、DirectX 11.1 バージョンの DXGI デバイス インターフェイスであることを思い出す必要があります。代わりに、単に AsDxgi メソッドを呼び出すだけで済みます。

auto device = Direct3D::CreateDevice();
 auto dxdevice = device.AsDxgi();

作成される Device2 オブジェクト (この場合は Dxgi 名前空間で定義) では、IDXGIDevice2 COM インターフェイス ポインターをラップし、固有の非仮想メソッドを提供します。別の例では、DirectX "オブジェクト モデル" を使用して DXGI ファクトリを利用する場合が考えられます。

auto device   = Direct3D::CreateDevice();
 auto dxdevice = device.AsDxgi();
 auto adapter  = dxdevice.GetAdapter();
 auto factory  = adapter.GetParent();

もちろん、これは Direct3D Device クラスが便利なショートカットとして GetDxgiFactory メソッドを提供する、非常に一般的なパターンです。

auto d3device = Direct3D::CreateDevice();
 auto dxfactory = d3device.GetDxgiFactory();

したがって、GetDxgiFactory などのいくつかの便利なメソッドや関数を除けば、非仮想メソッドは基盤となる DirectX インターフェイスのメソッドや関数に 1 対 1 でマップされます。これは十分とは思えないかもしれませんが、いくつもの手法の組み合わせによって非常に便利で生産性の高い DirectX のプログラミング モデルが形成されています。1 つ目の手法はスコープ列挙の使用です。DirectX ファミリの API では、数多くの定数を定義しています。その多くは、従来の列挙、フラグ、または定数です。このような定数は厳密に型指定されておらず、見つけにくく、Visual Studio IntelliSense が適切に機能しません。ファクトリ オプションについては後で説明することにして、Direct2D ファクトリの作成に必要なコードを示しましょう。

wrl::ComPtr factory;
 HR(D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED,
                      factory.GetAddressOf()));

D2D1CreateFactory 関数の最初のパラメーターは列挙ですが、スコープ列挙ではないので、Visual Studio IntelliSense での検出が困難です。この旧式の列挙では、多少のタイプ セーフが提供されますが、完全ではありません。せいぜい、E_INVALIDARG 結果コードを実行時に取得する程度です。皆さんはどうされているかわかりませんが、私はこのようなエラーをコンパイル時にキャッチするか、さらに良い方法として、まとめて回避します。

auto factory = CreateFactory(FactoryType::MultiThreaded);

ここでも、呼び出される Direct2D ファクトリ インターフェイスの最新バージョンを知るための調査に時間を費やす必要はありません。間違いなく、この手法における最大のメリットは生産性です。もちろん、DirectX API は COM インターフェイス メソッドを作成して呼び出すだけのものではありません。多くの Plain Old Data 構造を使用して、さまざまなプロパティやパラメーターをまとめています。スワップ チェーンの記述が良い例です。紛らわしいメンバーがあるために、私はプラットフォームの特性はもちろんこの構造体の準備方法を思い出すたびに苦労します。ここでも、厄介な DXGI_SWAP_CHAIN_DESC1 構造体の代替機能が用意されているので、dx.h ライブラリが役立ちます。

SwapChainDescription1 description;

この場合、DirectX で同じ型が認識されるようバイナリ互換の代替機能が提供されていますが、もう少し実用的な機能も使用できます。これは、Microsoft .NET Framework による P/Invoke ラッパーの提供対象とよく似ています。既定のコンストラクターでは、ほとんどのデスクトップ アプリケーションと Windows ストア アプリに適した既定値を利用できます。たとえば、デスクトップ アプリケーションで、サイズ変更時のレンダリングがスムーズになるようにこの既定値をオーバーライドする必要がある場合は、次のようになります。

SwapChainDescription1 description;
 description.SwapEffect = SwapEffect::Discard;

ところで、このスワップ効果は、Windows Phone 8 を対象とする場合にも必要になりますが、Windows ストア アプリでは許可されていません。不思議なことです。

多くのすばらしいライブラリを利用すると、稼動するソリューションをすばやく簡単に作成できます。具体的な例を考えてみましょう。Direct2D では、線形グラデーション ブラシが提供されます。このようなブラシを作成するには、グラデーション境界の定義、グラデーション境界コレクションの作成、およびそのコレクションが指定された線型グラデーション ブラシの作成という 3 つの手順が必要です。Direct2D API を直接使用すると、図 1のようになります。

図 1 線形グラデーション ブラシの地道な作成

D2D1_GRADIENT_STOP stops[] =
 {
   { 0.0f, COLOR_WHITE },
   { 1.0f, COLOR_BLUE },
 };
 wrl::ComPtr collection;
 HR(target->CreateGradientStopCollection(stops,
    _countof(stops),
    collection.GetAddressOf()));
 wrl::ComPtr brush;
 HR(target->CreateLinearGradientBrush(D2D1_LINEAR_GRADIENT_BRUSH_PROPERTIES(),
    collection.Get(),
    brush.GetAddressOf()));

dx.h を利用すると、さらにわかりやすくなります。

GradientStop stops[] =
 {
   GradientStop(0.0f, COLOR_WHITE),
   GradientStop(1.0f, COLOR_BLUE),
 };
 auto collection = target.CreateGradientStopCollection(stops);
 auto brush = target.CreateLinearGradientBrush(collection);

これは、図 1のコードと比べて大幅に短いわけではありませんが、明らかに簡単に記述でき、初めての場合 (特に IntelliSense を利用している場合) に間違えることはまずありません。より快適なプログラミング モデルを作成するために、ライブラリではさまざまな手法を使用しています。この例では、コンパイル時に GradientStop 配列のサイズを推測する関数テンプレートで CreateGradientStopCollection メソッドがオーバーロードされているため、_countof マクロを使用する必要はありません。

エラー処理はどうでしょうか。このように簡潔なプログラミング モデルを作成する前提条件の 1 つは、エラー伝達の例外を採用することです。先ほどの Direct3D Device AsDxgi メソッドの定義を考えてみましょう。

inline auto Device::AsDxgi() const -> Dxgi::Device2
 {
   Dxgi::Device2 result;
   HR(m_ptr.CopyTo(result.GetAddressOf()));
   return result;
 }

これは、ライブラリで非常によく見かけるパターンです。まず、メソッドが const であることに注意してください。基盤となる ComPtr だけがデータ メンバーであり、このデータ メンバーを修正する必要がないので、ライブラリに含まれている実質的にすべてのメソッドは const です。メソッドの本体を見ると、結果の Device オブジェクトの作成方法がわかります。すべてのライブラリ クラスでムーブ セマンティクスが提供されているので、何度もコピー (および、推定ですが何組もの AddRef/Release のペア) を実行しているように見えても、実際は実行時には何も実行されません。途中で式をラップしている HR は、結果が S_OK ではない場合に例外をスローするインライン関数です。最後に、ライブラリでは、呼び出し元が新たに QueryInterface を呼び出して追加の機能を公開しないように、最も具体的なクラスを返そうとします。

前の例では、ComPtr の CopyTo メソッドを使用しました。このメソッドは事実上 QueryInterface を呼び出しているだけです。Direct2D から、もう 1 つの例を示します。

inline auto BitmapBrush::GetBitmap() const -> Bitmap
 {
   Bitmap result;
   (*this)->GetBitmap(result.GetAddressOf());
   return result;
 }

こちらの例は、基盤となる COM インターフェイスのメソッドを直接呼び出す点で少し異なります。このパターンは、実はライブラリのコードの大部分を占めています。ここでは、ブラシの描画に使用するビットマップを返しています。多くの Direct2D メソッドでは、この例のように void が返されるため、結果を確認する HR 関数は必要ありません。しかし、GetBitmap メソッドに間接的にアクセスする方法は、あまりわかりやすくない場合があります。

私はライブラリの初期バージョンのプロトタイプを作成していたときに、コンパイラで複雑な技法を実行するか、COM で複雑な技法を実行するかを選択する必要がありました。初期の試みでは、テンプレート、特に型の特徴を使用するだけでなくコンパイラの型の特徴 (別名、組み込み型の特徴) も使用して、C++ で複雑な技法を実行しました。これは、最初は面白かったのですが、すぐに、自分の仕事を増やしていることが判明しました。

ライブラリでは、具体的なクラスとして COM インターフェイス間の "is-a" 関係をモデル化しています。COM インターフェイスが直接継承できるのは、他の 1 つのインターフェイスだけです。IUnknown 自体は例外として、各 COM インターフェイスは、他の 1 つのインターフェイスから直接継承する必要があります。この結果、最終的に、IUnknown を頂点とする型階層ができあがります。私は、まず各 COM インターフェイスのクラスを定義しました。RenderTarget クラスには、ID2D1RenderTarget インターフェイス ポインターが含まれていました。DeviceContext クラスには、ID2D1DeviceContext インターフェイス ポインターが含まれていました。これは、DeviceContext を RenderTarget として扱う限り、十分合理的でしょう。何しろ、ID2D1DeviceContext インターフェイスは、ID2D1RenderTarget インターフェイスから派生しているのです。DeviceContext が存在する場合に、RenderTarget を参照パラメーターとして想定しているメソッドにその DeviceContext を渡そうと考えるのはきわめて合理的です。

残念ながら、C++ の型システムではこのように事は運びません。このアプローチを使用すると、DeviceContext は実際に RenderTarget から派生することも、2 つの参照を保持することもできません。私は、ムーブ セマンティクスと組み込み型の特徴を組み合わせて、必要に応じて適切に参照を移動するようにしました。これはほぼ機能しましたが、余分な AddRef/Release のペアが導入される場合がありました。最終的に、このアプローチはあまりに複雑だったので、もっと単純なソリューションが必要になりました。

C++ とは異なり、COM には非常に明確に定義されたバイナリ コントラクトがあります。結局、これこそが COM というものです。取り決めた規則を守る限り、開発者が COM に失望することはないでしょう。言ってしまえば、COM を利用すると複雑な技法を実行できます。C++ は戦う相手ではなく、便利に利用できる手段です。つまり、各 C++ クラスには、厳密に型指定された COM インターフェイス ポインターではなく、IUnknown への汎用的な参照が含まれているだけです。C++ を使用するとタイプ セーフが強化され、C++ のクラス継承の規則 (最近ではムーブ セマンティクスも) を使用すると、これらの COM "オブジェクト" を C++ クラスとして再び扱えるようになります。概念的には、最初は次のような状態でした。

class RenderTarget { ComPtr ptr; };
 class DeviceContext { ComPtr ptr; };

そして、最終的には次のような状態になりました。

class Object { ComPtr ptr; };
 class RenderTarget : public Object {};
 class DeviceContext : public RenderTarget {};

COM インターフェイスとその関係によって暗黙的に示されている論理階層が C++ オブジェクト モデルで実現しているので、プログラミング モデル全体が非常に自然で使用しやすくなっています。この記事で説明できなかった要素は多数あるので、ぜひソース コードを詳しく調べてください。このコラムの執筆時点では、dx.h ライブラリは、Direct2D と Windows アニメーション マネージャーのほぼすべてに加え、DirectWrite、Windows Imaging Component (WIC)、Direct3D、および DXGI の便利な機能を対象としています。また、今後も機能を追加する予定なので、定期的にご確認ください。それではお楽しみください。

Kenny Kerr は、カナダを拠点とするコンピューター プログラマであり、Pluralsight の執筆者です。また、Microsoft MVP でもあります。彼のブログは kennykerr.ca(英語) で、Twitter は twitter.com/kennykerr(英語) でフォローできます。

この記事のレビューに協力してくれた技術スタッフの Worachai Chaoweeraprasit (マイクロソフト) に心より感謝いたします。
Worachai Chaoweeraprasit (マイクロソフト)、wchao@microsoft.com(英語のみ)

Worachai Chaoweeraprasit は、Direct2D と DirectWrite の開発担当者です。彼は、2D ベクター グラフィックスのスピードと品質に加えて、テキストの画面における可読性に夢中になっています。余暇には、自宅で 2 人の子供に追い詰められることを楽しんでいます。