DirectXの話 第105回
DirectX11はじめました
Windows7をインストールし、RadeonHD 5870も手に入ったので久しぶりに更新してみます。
今回はDX11はじめてということで、DX11の初期化から描画までの基本的な流れを解説します。
DX10をすでにいじっている人には新鮮さのない内容となっていますが、DX10をスキップした人(私のように)向けということで。
では、早速初期化部分をみていきます。
ウィンドウを作成する部分はDX9と変わりませんが、デバイス作成はだいぶ変化があります。
まず、CAPSがなくなりました。その代わりにFeatureLevelが登場しています。
CAPSはディスプレイアダプタが持っている機能を記述したものです。
使用可能なテクスチャフォーマット、サイズ、ShaderModel、定数レジスタの最大数などなどです。
DX5くらいの頃は非常に重要で、フレームバッファのフォーマットが16ビットカラーでも、片や565フォーマット、片や5551フォーマットなんてこともありましたし。
しかし、最近はDX9対応が当たり前になってきて(WindowsVistaはDX9必須のはず)細かな機能差がなくなってきました。
そこで登場FeatureLevel。CAPSを大まかに分けたものと考えるといいでしょう。
つまり、DX11レベルになるのに必要な機能は決まっていて、そのうち1つでも機能が入っていないとレベルが下がるわけです。
もう1つ、これはDX10かららしいのですが、デバイスがいくつかのオブジェクトに分かれました。
初期化時に作成すべきなのはデバイス、コンテキスト、スワップチェインの3つ、加えてレンダリングターゲットです。
早速デバイス作成部分を見ていきます。"dx11test.cpp" 内の CreateDeviceAndSwapChain() 関数がそれです。
// デバイスの各種設定
・
・(略)
・
// スワップチェインの設定
・
・(略)
・
// デバイスとスワップチェインを作成する
HRESULT hr = D3D11CreateDeviceAndSwapChain(
adapter,
dtype,
NULL,
flags,
featureLevels,
numFeatureLevels,
sdkVersion,
&scDesc,
&g_pSwapChain,
&g_pDevice,
&validFeatureLevel,
&g_pContext );
D3D11CreateDeviceAndSwapChain() 関数でデバイスとスワップチェインを同時に作成することができます。
デバイスのみを先に作成し、後でスワップチェインを作成することも可能です。
第1引数のadapterはディスプレイアダプタですが、デフォルトを使用する場合はNULLでも問題ありません。
列挙したい場合は EnumAdapters() 関数を参照してください。今回はアダプタの名前を出力しているだけです。
第5引数がチェックしたいFeatureLevelの配列です。
先頭からチェックし、有効なFeatureLevelが見つかったところで終了します。
なので配列の若い方に高いFeatureLevelを配置する必要があります。
有効なFeatureLevelは第11引数に出力されます。
この関数で作成されるオブジェクトはデバイス、コンテキスト、スワップチェインの3つです。
DX11のデバイスオブジェクトは主にバッファやテクスチャの作成を行います。描画そのものは行いません。
描画を行うのはコンテキストです。上記関数の最終引数がこれです。
このコンテキストは即時コンテキストで、これによって詰まれたコマンドは逐次実行されます。
これとは別に遅延コンテキストもあります。こちらはコマンドを溜めておき、必要な場所で実行させることができるもののようです。
主にマルチスレッドアプリケーションで複数のスレッドがコマンドを作成する際に有効です。
スワップチェインはバックバッファとフレームバッファのスワップに関する処理を行います。
これはDX9にもあったのですが、デバイス内に必ず1つ存在するという仕組みでした。
これだけではバックバッファのクリアすらできません。クリアをするにはレンダリングターゲットを作成する必要があります。
CreateRenderTarget() でレンダリングターゲットを、CreateDepthStencil() で深度バッファを作成しています。
bool CreateRenderTarget()
{
// レンダリングターゲットを作成する
ID3D11Texture2D* pBackBuffer;
HRESULT hr = g_pSwapChain->GetBuffer( 0, __uuidof(ID3D11Texture2D), reinterpret_cast<void**>(&pBackBuffer) );
if( FAILED(hr) ) { return false; }
hr = g_pDevice->CreateRenderTargetView( pBackBuffer, NULL, &g_pRenderTargetView );
pBackBuffer->Release();
if( FAILED(hr) ) { return false; }
return true;
}
bool CreateDepthStencil()
{
// 深度バッファテクスチャを作成する
D3D11_TEXTURE2D_DESC depthDesc;
・
・(略)
・
HRESULT hr = g_pDevice->CreateTexture2D( &depthDesc, NULL, &g_pDepthStencil );
if( FAILED(hr) ) { return false; }
// 深度バッファターゲットを作成する
D3D11_DEPTH_STENCIL_VIEW_DESC dsvDesc;
・
・(略)
・
hr = g_pDevice->CreateDepthStencilView( g_pDepthStencil, &dsvDesc, &g_pDepthStencilView );
if( FAILED(hr) ) { return false; }
return true;
}
レンダリングターゲットはスワップチェインから取得したテクスチャを利用して作成されます。
しかし、深度バッファはスワップチェインにはないので、テクスチャを作成してからターゲットを作成します。
作成したターゲットはコンテキストにアタッチします。
レンダリングターゲットは複数アタッチできます。
第1引数がレンダリングターゲットの数、第2引数がレンダリングターゲットの配列、第3引数が深度バッファターゲットです。
次にシェーダを作成します。CreateShader() 関数を見てください。
コンパイルから作成の流れはDX9とほとんど変わりませんので割愛します。
問題は入力レイアウトです。
DX9ではシェーダに流される頂点のフォーマットは頂点宣言を用いて設定していました。
これは各頂点バッファに対応したものが作られ、描画の直前に自動的にシェーダ入力と頂点データのバインドが行われていました。
これは非効率です。特に入力される頂点フォーマットが固定であれば無駄もいいところです。
また、DX10から頂点入力のセマンティックが自由に設定できるようになりました。位置はPOSITIONでなくてもよく、MONSHOとつけても問題ないのです。
その関係もあってDX10では頂点入力のフォーマットをあらかじめシェーダにバインドしておくようになりました。
しかし問題もあります。同じシェーダを利用する異なる頂点フォーマットがある場合です。
例えば、あるモデルは位置、法線、カラーを属性として持っています。
通常そのモデルはライティング計算とカラーの合成を行うので3つの属性が必要になります(マテリアルA)。
ただし、ある魔法を使うとライティングされずにカラーだけを出力するようになります(マテリアルB)。
マテリアルAのシェーダは問題ありませんが、マテリアルBのシェーダでは法線が邪魔です。
また、マテリアルBは他のモデルでも使用され、こちらは元から位置とカラーだけしか持っていません。
となるとマテリアルBは最低でも2種類のレイアウトが必要になるというわけです。
一般的な運用方法は、やはり描画前に頂点バッファに対応したレイアウトを生成する方法じゃないかと思います。
キャッシュを用いる方法もありだと思いますが、どちらにしろ動的に生成できるようにしなければなりません。
少なくとも、自分ならそうするな、という程度の話です。他にいい運用方法があるなら教えてください。
g_pContext->OMSetRenderTargets( 1, &g_pRenderTargetView, g_pDepthStencilView );
// 入力頂点属性を作成する
const D3D11_INPUT_ELEMENT_DESC layout[] =
{
{ "IN_POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "IN_COLOR", 0, DXGI_FORMAT_B8G8R8A8_UNORM, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};
hr = g_pDevice->CreateInputLayout( layout, ARRAYSIZE(layout), pVSBuffer->GetBufferPointer(), pVSBuffer->GetBufferSize(), &g_pLayout );
if( FAILED(hr) ) { return false; }
上記がレイアウトの生成手法です。
生成の際、コンパイルした頂点シェーダのバッファが必要になります。
サンプルではこの後破棄していますが、前述のように動的にレイアウトを作成する場合は破棄してはいけないかと思います。
次に必要なバッファを作成します。CreateBuffer() 関数を見てください
頂点バッファとインデックスバッファについては解説を省きます。
DX11ではバッファ生成時に初期値を入力することが可能です。動的に変更しないデータはこれを利用するといいでしょう。
動的に変更する場合は Lock()、Unlock() の代わりに Map()、Unmap() を利用します。
注意してほしいのは、Map()、Unmap() はコンテキストの機能である点です。
サンプルでは頂点バッファを初期値入力、インデックスバッファを動的変更で設定しています。
今までになかったのはコンスタントバッファです。これは、シェーダのコンスタントレジスタに入力する値のバッファです。
シェーダプログラムを見てもらうとわかるのですが、コンスタントレジスタが cbuffer という構造体のようになっています。
いちいちレジスタの名前解決をしなくてすみますし、変化のサイクルによってバッファを分けておくと非常に便利です。
例えば、カメラに関する情報はシーン1つについてほぼ同一のものが使用されますので、1回設定すればOKです。
逆にモデルの情報はモデル1つにつき1つ存在します。複数モデルを描画するなら複数必要です。
これらを分けておくことで簡単なキャッシュも可能です。
ここまでで描画に必要な初期化を済ませました。ついに描画です。
といっても、やることはほとんど同じです。クリアして、描画して、スワップするだけです。
クリアはレンダリングターゲットに対して行うようになりましたが、機能自体はコンテキストが持っています。
コンテキストにレンダリングターゲットとして設定されているレンダリングターゲット以外もクリアが可能です。
スワップはスワップチェインの機能になりました。Present() メソッドなのは変わりません。
描画も必要なものを設定して行うだけですが、プリミティブは描画命令で指定するのではなく、事前に設定するようになりました。
g_pContext->IASetPrimitiveTopology( D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP );
g_pContext->DrawIndexed( 4, 0, 0 );
なお、今回のサンプルでは行っていませんが、各種レンダリングステートの設定もオブジェクトになっています。
いくつかの機能で1くくりになっていて、やはりこれも高速化の一環でしょう。
ちょっと解説が足らない気もしますが、以上で今回の解説は終了です。
おまけとして、DX9とDX11の簡単な速度比較を行ってみたので掲載しておきます。
1.描画を行わずにクリアとスワップのみ100万回
2.クリア、四角形描画、スワップを100万回
3.クリア、100万回の四角形描画、スワップを1回
で計測しています。結果の単位はmsです。
かなりおおざっぱな計測なのであまり参考にはならないかもしれません。
計測結果を見ると1と3がDX11の方が圧倒的に速いですが、なぜか2が遅いです。
描画セッティングが重いのかと思ったのですが、どうもそうではないようです。
何が問題なのかはもっときちんと計測してやる必要がありそうですね。
今回のサンプルはWindows7 x64で動作を確認しています。
プロジェクトはVisualStudio2010 ベータ2を用いています。
次回予定は未定ですが、やはりDX11の調査を行い、その解説を上げることになると思います。