DirectX デバイス リソースの操作
Windows ストア DirectX ゲームの DirectX Graphic Infrastructure (DXGI) の役割について説明します。 DXGI は、低レベルのグラフィックスとグラフィックス アダプター リソースを構成および管理するために使用される一連の API です。 それなしでは、ゲームのグラフィックスをウィンドウに描画できません。
DXGI についてこのように考えてみましょう。GPU に直接アクセスしてそのリソースを管理するには、それをアプリに説明する方法が必要です。 GPU に関して必要な最も重要な情報は、それらのピクセルを画面に送信できるようにピクセルを描画する場所です。 通常、これは "バック バッファー" と呼ばれる GPU メモリ内の場所で、ピクセルを描画し、"反転" または "スワップ" して、更新信号で画面に送信できます。 DXGI を使用すると、その場所およびそのバッファーを使用する手段 (スワップ可能なバッファーのチェーンであるため、"スワップ チェーン" と呼ばれ、複数のバッファリング戦略が可能になります) を取得できます。
これを行うには、スワップ チェーンに書き込むためのアクセス権と、スワップ チェーンの現在のバック バッファーを表示するウィンドウへのハンドルが必要です。 また、バック バッファーの内容でウィンドウを更新するようにオペレーティング システムに要求したときに、それが実行されるようにその 2 つを接続する必要もあります。
画面に描画するための全体的なプロセスは次のとおりです。
- アプリの CoreWindow を取得します。
- Direct3D デバイスとコンテキストのインターフェイスを取得します。
- レンダリングされるイメージを CoreWindow に表示するスワップ チェーンを作成します。
- 描画用のレンダー ターゲットを作成し、それにピクセルを設定します。
- スワップ チェーンを提示します。
アプリのウィンドウを作成する
最初に行う必要があるのは、ウィンドウの作成です。 まず、WNDCLASS のインスタンスを設定してウィンドウ クラスを作成し、RegisterClass を使用してそれを登録します。 ウィンドウ クラスには、使用するアイコン、静的メッセージ処理関数 (詳細については後述)、ウィンドウ クラスの一意の名前などのウィンドウの重要なプロパティが含まれています。
if(m_hInstance == NULL)
m_hInstance = (HINSTANCE)GetModuleHandle(NULL);
HICON hIcon = NULL;
WCHAR szExePath[MAX_PATH];
GetModuleFileName(NULL, szExePath, MAX_PATH);
// If the icon is NULL, then use the first one found in the exe
if(hIcon == NULL)
hIcon = ExtractIcon(m_hInstance, szExePath, 0);
// Register the windows class
WNDCLASS wndClass;
wndClass.style = CS_DBLCLKS;
wndClass.lpfnWndProc = MainClass::StaticWindowProc;
wndClass.cbClsExtra = 0;
wndClass.cbWndExtra = 0;
wndClass.hInstance = m_hInstance;
wndClass.hIcon = hIcon;
wndClass.hCursor = LoadCursor(NULL, IDC_ARROW);
wndClass.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
wndClass.lpszMenuName = NULL;
wndClass.lpszClassName = m_windowClassName.c_str();
if(!RegisterClass(&wndClass))
{
DWORD dwError = GetLastError();
if(dwError != ERROR_CLASS_ALREADY_EXISTS)
return HRESULT_FROM_WIN32(dwError);
}
次に、ウィンドウを作成します。 ウィンドウのサイズ情報と、作成したばかりのウィンドウ クラスの名前も指定する必要があります。 CreateWindow を呼び出すと、HWND と呼ばれるウィンドウへの不透明なポインターが返されます。HWND ポインターを保持して、ウィンドウを参照する必要があるとき (破棄または再作成を含む) にいつでも、また、ウィンドウに描画するために使用する DXGI スワップ チェーンを作成するときにも (特に重要) 使用する必要があります。
m_rc;
int x = CW_USEDEFAULT;
int y = CW_USEDEFAULT;
// No menu in this example.
m_hMenu = NULL;
// This example uses a non-resizable 640 by 480 viewport for simplicity.
int nDefaultWidth = 640;
int nDefaultHeight = 480;
SetRect(&m_rc, 0, 0, nDefaultWidth, nDefaultHeight);
AdjustWindowRect(
&m_rc,
WS_OVERLAPPEDWINDOW,
(m_hMenu != NULL) ? true : false
);
// Create the window for our viewport.
m_hWnd = CreateWindow(
m_windowClassName.c_str(),
L"Cube11",
WS_OVERLAPPEDWINDOW,
x, y,
(m_rc.right-m_rc.left), (m_rc.bottom-m_rc.top),
0,
m_hMenu,
m_hInstance,
0
);
if(m_hWnd == NULL)
{
DWORD dwError = GetLastError();
return HRESULT_FROM_WIN32(dwError);
}
Windows デスクトップ アプリ モデルには、Windows メッセージ ループへのフックが含まれています。 ウィンドウ イベントを処理する "StaticWindowProc" 関数を記述することで、このフックからメイン プログラムのループを基本的にオフにする必要があります。 これは、Windows がクラス インスタンスのコンテキストの外部で呼び出すため、静的関数である必要があります。 静的メッセージ処理関数の非常に単純な例を次に示します。
LRESULT CALLBACK MainClass::StaticWindowProc(
HWND hWnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam
)
{
switch(uMsg)
{
case WM_CLOSE:
{
HMENU hMenu;
hMenu = GetMenu(hWnd);
if (hMenu != NULL)
{
DestroyMenu(hMenu);
}
DestroyWindow(hWnd);
UnregisterClass(
m_windowClassName.c_str(),
m_hInstance
);
return 0;
}
case WM_DESTROY:
PostQuitMessage(0);
break;
}
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
この単純な例は、プログラムの終了条件 WM_CLOStandard Edition (ウィンドウを閉じるように要求されたときに送信される) と WM_DESTROY (ウィンドウが実際に画面から削除された後に送信される) を調べるのみです。 完全な運用アプリは、他のウィンドウ イベントも処理する必要があります。ウィンドウ イベントの完全な一覧については、「ウィンドウ通知」を参照してください。
Windows が静的メッセージ プロセスを実行できるようにすることで、メイン プログラムのループ自体が、Windows メッセージを確認する必要があります。 動作をフォークすることで、プログラムが効率的に実行されるようにします。各イテレーションは、新しい Windows メッセージが使用可能な場合はその処理を選択する必要があり、キューにメッセージがない場合は新しいフレームをレンダリングする必要があります。 非常に単純な例を次に示します。
bool bGotMsg;
MSG msg;
msg.message = WM_NULL;
PeekMessage(&msg, NULL, 0U, 0U, PM_NOREMOVE);
while (WM_QUIT != msg.message)
{
// Process window events.
// Use PeekMessage() so we can use idle time to render the scene.
bGotMsg = (PeekMessage(&msg, NULL, 0U, 0U, PM_REMOVE) != 0);
if (bGotMsg)
{
// Translate and dispatch the message
TranslateMessage(&msg);
DispatchMessage(&msg);
}
else
{
// Update the scene.
renderer->Update();
// Render frames during idle time (when no messages are waiting).
renderer->Render();
// Present the frame to the screen.
deviceResources->Present();
}
}
Direct3D デバイスとコンテキストのインターフェイスを取得する
Direct3D を使用する最初の手順は、ID3D11Device と ID3D11DeviceContext のインスタンスとして表される Direct3D ハードウェア (GPU) のインターフェイスを取得することです。 前者は GPU リソースの仮想表現であり、後者はレンダリング パイプラインとプロセスの、デバイスに依存しない抽象化です。 次のように簡単に考えてみましょう。ID3D11Device には、ピクセルの描画を開始するために必要な一連のリソースを取得して構成するために、通常はレンダリングが発生する前に低い頻度で呼び出すグラフィックス メソッドが含まれています。 一方、ID3D11DeviceContext には、すべてのフレーム (バッファーやビューなどのリソースの読み込み、出力マージャーとラスタライザーの状態の変更、シェーダーの管理、それらのリソースを状態とシェーダーに通過させた結果の描画) で呼び出すメソッドが含まれています。
このプロセスには、機能レベルの設定という非常に重要な部分があります。 この機能レベルは、アプリがサポートするハードウェアの最小レベルを、D3D_FEATURE_LEVEL_9_1 が最も低い機能セットで、D3D_FEATURE_LEVEL_11_1 が現在の最高レベルであると DirectX に通知します。 可能な限り幅広い対象ユーザーにリーチする場合は、少なくとも 9_1 をサポートする必要があります。 時間を割いて Direct3D の機能レベルをご覧いただき、ゲームでサポートする最小および最大の機能レベルを自分で評価し、選択した機能の影響を把握してください。
Direct3D デバイスとデバイス コンテキストの両方への参照 (ポインター) を取得し、それらを DeviceResources インスタンスのクラス レベルの変数 (ComPtr スマート ポインター) として格納します。 Direct3D デバイスまたはデバイス コンテキストにアクセスする必要があるときは常に、これらの参照を使用します。
D3D_FEATURE_LEVEL levels[] = {
D3D_FEATURE_LEVEL_11_1
D3D_FEATURE_LEVEL_11_0,
D3D_FEATURE_LEVEL_10_1,
D3D_FEATURE_LEVEL_10_0,
D3D_FEATURE_LEVEL_9_3,
D3D_FEATURE_LEVEL_9_2,
D3D_FEATURE_LEVEL_9_1,
};
// This flag adds support for surfaces with a color-channel ordering different
// from the API default. It is required for compatibility with Direct2D.
UINT deviceFlags = D3D11_CREATE_DEVICE_BGRA_SUPPORT;
#if defined(DEBUG) || defined(_DEBUG)
deviceFlags |= D3D11_CREATE_DEVICE_DEBUG;
#endif
// Create the Direct3D 11 API device object and a corresponding context.
Microsoft::WRL::ComPtr<ID3D11Device> device;
Microsoft::WRL::ComPtr<ID3D11DeviceContext> context;
hr = D3D11CreateDevice(
nullptr, // Specify nullptr to use the default adapter.
D3D_DRIVER_TYPE_HARDWARE, // Create a device using the hardware graphics driver.
0, // Should be 0 unless the driver is D3D_DRIVER_TYPE_SOFTWARE.
deviceFlags, // Set debug and Direct2D compatibility flags.
levels, // List of feature levels this app can support.
ARRAYSIZE(levels), // Size of the list above.
D3D11_SDK_VERSION, // Always set this to D3D11_SDK_VERSION for Windows Store apps.
&device, // Returns the Direct3D device created.
&m_featureLevel, // Returns feature level of device created.
&context // Returns the device immediate context.
);
if (FAILED(hr))
{
// Handle device interface creation failure if it occurs.
// For example, reduce the feature level requirement, or fail over
// to WARP rendering.
}
// Store pointers to the Direct3D 11.1 API device and immediate context.
device.As(&m_pd3dDevice);
context.As(&m_pd3dDeviceContext);
スワップ チェーンを作成する
これで、描画するウィンドウができ、データを送信して GPU にコマンドを渡すインターフェイスができました。 ここで、それらを結び付ける方法を見てみましょう。
まず、スワップ チェーンのプロパティに使用する値を DXGI に通知します。 これを行うには、DXGI_SWAP_CHAIN_DESC 構造体を使用します。 デスクトップ アプリには、次の 6 つのフィールドが特に重要です。
- Windowed: スワップ チェーンが全画面表示であるか、ウィンドウに合わせて切り取られているかを示します。 前に作成したウィンドウにスワップ チェーンを配置するには、これを TRUE に設定します。
- BufferUsage: これは DXGI_USAGE_RENDER_TARGET_OUTPUT に設定します。 これは、スワップ チェーンが描画画面になり、Direct3D レンダー ターゲットとして使用できるようになることを示します。
- SwapEffect: これは DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL に設定します。
- Format: DXGI_FORMAT_B8G8R8A8_UNORM 形式は、32 ビットの色 (3 つの RGB カラー チャネルそれぞれに 8 ビットと、アルファ チャネルに 8 ビット) を指定します。
- BufferCount: 従来のダブルバッファー動作の場合は、テアリングを避けるために、これを 2 に設定します。 グラフィックス コンテンツが 1 つのフレームをレンダリングするのに 1 モニター更新サイクルより長くかかる場合 (たとえば、60 Hz で 16 ミリ秒を超えるしきい値にしている場合) は、バッファー数を 3 に設定します。
- SampleDesc: このフィールドはマルチサンプリングを制御します。 フリップ モデル スワップ チェーンの場合は、Count を 1、Quality を 0 に設定します。 (フリップ モデルのスワップ チェーンでマルチサンプリングを使用するには、別のマルチサンプリング レンダー ターゲット上に描画し、そのターゲットを提示直前のスワップ チェーンに映し出します。コード例は、「Windows ストア アプリのマルチサンプリング」に提供されています)。
スワップ チェーンの構成を指定した後、スワップ チェーンを作成するために、Direct3D デバイス (およびデバイス コンテキスト) を作成したのと同じ DXGI ファクトリを使用する必要があります。
短い形式:
前に作成した ID3D11Device 参照を取得します。 それを IDXGIDevice3 にアップキャストし (まだしていない場合)、IDXGIDevice::GetAdapter を呼び出して DXGI アダプターを取得します。 IDXGIAdapter::GetParent (IDXGIAdapter は IDXGIObject から継承) を呼び出して、そのアダプターの親ファクトリを取得します。これで、そのファクトリを使用し、CreateSwapChainForHwnd を呼び出すことで、スワップ チェーンを作成できます。これを次のコード サンプルに示します。
DXGI_SWAP_CHAIN_DESC desc;
ZeroMemory(&desc, sizeof(DXGI_SWAP_CHAIN_DESC));
desc.Windowed = TRUE; // Sets the initial state of full-screen mode.
desc.BufferCount = 2;
desc.BufferDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
desc.SampleDesc.Count = 1; //multisampling setting
desc.SampleDesc.Quality = 0; //vendor-specific flag
desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL;
desc.OutputWindow = hWnd;
// Create the DXGI device object to use in other factories, such as Direct2D.
Microsoft::WRL::ComPtr<IDXGIDevice3> dxgiDevice;
m_pd3dDevice.As(&dxgiDevice);
// Create swap chain.
Microsoft::WRL::ComPtr<IDXGIAdapter> adapter;
Microsoft::WRL::ComPtr<IDXGIFactory> factory;
hr = dxgiDevice->GetAdapter(&adapter);
if (SUCCEEDED(hr))
{
adapter->GetParent(IID_PPV_ARGS(&factory));
hr = factory->CreateSwapChain(
m_pd3dDevice.Get(),
&desc,
&m_pDXGISwapChain
);
}
始めたばかりの場合は、ここに示した構成を使用することをお勧めします。 ここで、DirectX の前のバージョンに既に慣れている場合は、これらのクラスすべてに後戻りする代わりに、デバイスとスワップ チェーンを同時に作成しなかったのはなぜなのか、疑問を持つかもしれません。答えは効率性です。スワップ チェーンは Direct3D デバイス リソースであり、デバイス リソースは、それらを作成した特定の Direct3D デバイスに関連付けられています。 新しいスワップ チェーンを使用して新しいデバイスを作成する場合は、新しい Direct3D デバイスを使用してすべてのデバイス リソースを再作成する必要があります。 つまり、(上記のように) 同じファクトリを使用してスワップ チェーンを作成すると、スワップ チェーンを再作成して、既に読み込んだ Direct3D デバイス リソースを引き続き使用できます。
これで、オペレーティング システムのウィンドウ、GPU とそのリソースにアクセスする方法、レンダリング結果を表示するためのスワップ チェーンが完成しました。 残っているのは、全体を結び付けることのみです。
描画用のレンダー ターゲットを作成する
シェーダー パイプラインには、ピクセルを描画するためのリソースが必要です。 このリソースを作成する最も簡単な方法は、描画するピクセル シェーダーのバック バッファーとして ID3D11Texture2D リソースを定義し、そのテクスチャをスワップ チェーンに読み込むことです。
これを行うには、レンダー ターゲット "ビュー" を作成します。 Direct3D では、ビューとは特定のリソースにアクセスする方法です。 この場合、ビューを使用すると、ピクセルの操作が完了するたびに、ピクセル シェーダーのテクスチャへの書き込みが可能になります。
このコードを見てみましょう。 スワップ チェーンで DXGI_USAGE_RENDER_TARGET_OUTPUT を設定すると、基になる Direct3D リソースを描画画面として使用できるようになります。 このため、レンダー ターゲット ビューを取得するために必要なことは、スワップ チェーンからのバック バッファーの取得と、バック バッファー リソースにバインドされたレンダー ターゲット ビューの作成のみになります。
hr = m_pDXGISwapChain->GetBuffer(
0,
__uuidof(ID3D11Texture2D),
(void**) &m_pBackBuffer);
hr = m_pd3dDevice->CreateRenderTargetView(
m_pBackBuffer.Get(),
nullptr,
m_pRenderTarget.GetAddressOf()
);
m_pBackBuffer->GetDesc(&m_bbDesc);
"深度ステンシル バッファー" も作成します。 深度ステンシル バッファーは、単に ID3D11Texture2D リソースの特定の形式です。これは通常、ラスタライズ中に、カメラからシーン内のオブジェクトへの距離に基づいて描画優先度を持つピクセルを決定するために使用されます。 深度ステンシル バッファーは、ラスタライズ中に特定のピクセルが破棄または無視されるステンシル効果にも使用できます。 このバッファーはレンダー ターゲットと同じサイズにする必要があります。 フレーム バッファーの深度ステンシル テクスチャは、最終的なラスタライズの前とその最中にシェーダー パイプラインによって排他的に使用されるため、それに対する読み取りや、レンダリングはできません。
深度ステンシル バッファーのビューも、ID3D11DepthStencilView として作成します。 このビューは、基になる ID3D11Texture2D リソースを解釈する方法をシェーダー パイプラインに通知します。そのため、このビューを指定しない場合、ピクセル単位の深度テストは実行されず、シーン内のオブジェクトが控えめに言っても少し裏返しになる可能性があります。
CD3D11_TEXTURE2D_DESC depthStencilDesc(
DXGI_FORMAT_D24_UNORM_S8_UINT,
static_cast<UINT> (m_bbDesc.Width),
static_cast<UINT> (m_bbDesc.Height),
1, // This depth stencil view has only one texture.
1, // Use a single mipmap level.
D3D11_BIND_DEPTH_STENCIL
);
m_pd3dDevice->CreateTexture2D(
&depthStencilDesc,
nullptr,
&m_pDepthStencil
);
CD3D11_DEPTH_STENCIL_VIEW_DESC depthStencilViewDesc(D3D11_DSV_DIMENSION_TEXTURE2D);
m_pd3dDevice->CreateDepthStencilView(
m_pDepthStencil.Get(),
&depthStencilViewDesc,
&m_pDepthStencilView
);
最後の手順は、ビューポートを作成することです。 これにより、画面に表示されるバック バッファーの可視部分の四角形が定義されます。ビューポートのパラメーターを変更することで、画面に表示されるバッファー部分を変更できます。 このコードは、全画面表示のスワップ チェーンの場合、ウィンドウ全体のサイズ (または画面の解像度) を対象にします。 試しに指定された座標値を変更し、結果を観察してみましょう。
ZeroMemory(&m_viewport, sizeof(D3D11_VIEWPORT));
m_viewport.Height = (float) m_bbDesc.Height;
m_viewport.Width = (float) m_bbDesc.Width;
m_viewport.MinDepth = 0;
m_viewport.MaxDepth = 1;
m_pd3dDeviceContext->RSSetViewports(
1,
&m_viewport
);
このようにして、何もないところからウィンドウへのピクセルの描画に進みます。 作業の開始時には、DirectX が、ピクセルの描画を開始するために必要なコア リソースを DXGI を使用して管理する方法についての理解を深めることをお勧めします。
次に、グラフィックス パイプラインの構造を確認します。DirectX アプリ テンプレートのレンダリング パイプラインを理解することに関する記事を参照してください。
関連トピック