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

その9 マルチレンダーターゲットで異なる情報を一度に描いてみる


 通常ピクセルシェーダは設定されている1枚のレンダーターゲットに対してポリゴンを塗りつぶす点を穿っていきます。これは言ってみれば「シングルレンダーターゲット描画」と言えます。しかしDirect3Dは一つのピクセルシェーダから複数枚のレンダーターゲットに一度に描画するという「マルチレンダーターゲット描画」をサポートしています。これはDirectX9でも出来ますし、もちろんDirect3D10でも可能です。

 「マルチレンダーターゲットなんて何に使うか良くわからんなぁ」と思う方はごもっともw。確かに普通ではまず使いません。でも世の中に「ディファードレンダリング(Defferd Rendering:遅延シェーディング)」なる凄い手法が出てきて、その状況は一変しました。今商用ゲームで物凄くたくさんのライティングが施されていますが、これ実はディファードレンダリング(ディファードライティング)の成す技なのです。そして、ディファードレンダリングはマルチレンダーターゲット描画が根幹になっているんです。逆に言えばマルチレンダーターゲットを知れば世界を光で溢れさせることが出来る…ほら、何となくワクワクしてきませんか(^-^)

 この章ではそんなマルチレンダーターゲットの方法について見ていく事にしましょう。



@ レンダーターゲットを複数作成する

 「まるちれんだーたーげっと」というのはちょっと冗長なので、以後これを「MRT」と略称します。冒頭でも述べているように、MRT描画とは複数のレンダーターゲットに一度に描画する手法の事です。

 MRTを行うにあたり、DirectX9の時代にはかなりな制約がありましたが、Direct3D10になってからは随分とその制約から解放されました。書き込み先のレンダーターゲットのフォーマットは異なっていても構いません。レンダーターゲットの大きさも異なっていても描画されます。ただし描画範囲はビューポートに縛られてしまうため、通常は同じサイズのレンダーターゲットを使用します。

 これらを踏まえた上で、複数のレンダーターゲットを作成してみます。

 Direct3D10でのレンダーターゲットの作り方についてはDirectX10技術編その7「Direct3D10でのテクスチャの作り方」で説明してあるように、D3D10_TEXTURE2D_DESC構造体でレンダーターゲットである事を宣言して、ID3D10Device::CreateTextureメソッドに渡し専用のテクスチャを作成します。そのテクスチャをシェーダの出力であるレンダーターゲットビューに登録すればOKです:

レンダーターゲット&レンダーターゲットビュー作成
// レンダーターゲットを作成しレンダーターゲットビューに登録
ID3D10Texture2D *renderTex = 0;
ID3D10RenderTargetView *renderTargetView = 0;
D3D10_TEXTURE2D_DESC rtDesc;

memset( &rtDesc, 0, sizeof( rtDesc ) );
rtDesc.Width = 1024;
rtDesc.Height = 1024;
rtDesc.MipLevels = 1;
rtDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
rtDesc.SampleDesc.Count = 1;
rtDesc.Usage = D3D10_USAGE_DEFAULT;
rtDesc.ArraySize = 1;
rtDesc.BindFlags = D3D10_BIND_RENDER_TARGET | D3D10_BIND_SHADER_RESOURCE;
rtDesc.CPUAccessFlags = 0;

cpDevice_->CreateTexture2D( &rtDesc, 0, &renderTex );
cpDevice_->CreateRenderTargetView( renderTex, 0, &renderTargetView );

 マルチレンダーターゲットも作り方は全く一緒です。全てのレンダーターゲットが同じフォーマットで良いなら次のようにD3D10_TEXTURE2D_DESC構造体を使いまわしてOKです:

マルチレンダーターゲットを作成
const int mrtNum = 4;
ID3D10Texture2D *renderTexes[ mrtNum ] = {};
ID3D10RenderTargetView *renderTargetViews[ mrtNum ] = {};
for ( int i = 0; i < mrtNum; ++i ) {
    cpDevice_->CreateTexture2D( &rtDesc, 0, &renderTexes[ i ] );
    cpDevice_->CreateRenderTargetView( renderTexes[ i ], 0, &renderTargetViews[ i ] );
}

 これで描き込み先が用意出来ました。



A ID3D10Device::OMSetRenderTargetsメソッドに設定

 @で作成した複数のレンダーターゲットビューを描画時にID3D10Device::OMSetRenderTargetsメソッドに設定します。このメソッド、その名前「OMSetRenderTargets」から分かるように、元から複数のレンダーターゲットを設定する前提になっているんですよね(^-^;。ちょっとこのメソッドの宣言を見てみましょう:

マルチレンダーターゲットを作成
virtual void STDMETHODCALLTYPE OMSetRenderTargets(
    UINT  NumViews,
    ID3D10RenderTargetView  *const *ppRenderTargetViews,
    ID3D10DepthStencilView  *pDepthStencilView
);

NumViewsはレンダーターゲットビューの枚数です。最大枚数はD3D10_SIMULTANEOUS_RENDER_TARGET_COUNTマクロで決められていますが8枚です。
ppRenderTargetViewsは設定するレンダーターゲットビューの配列へのポインタを渡します。
pDepthStencilViewには深度ステンシルビューを渡します。これは1枚だけです。


【トピック】 中途半端な所にあるconstの解釈方法

 第2引数のppRenderTargeetViewsは型が「ID3D10RenderTargetView *const *」となっています。こういうの「うっ」てなりますよねw。下記の例をご覧ください:

 上のようなconstは「その後に続く参照先を保護する」と見ると読みやすくなります。型にあるconstの先は「*ppAry」ですよね。これは「ppAryが保持するポインタが指す先」ですから、図の赤い四角で囲んだ実体になります。ここが保護対象(書き込み不可)になっているという訳です。つまり「配列を指しているポインタを内部では変更しません」と宣言している事になります。気を付けたいのが、実際の配列内の実体にはconstが付いていないので変更できる(const付きでは無いメソッドを呼べる)という事です。


 @で作成したレンダーターゲットビューを描画デバイスにセットするには次のようにするだけです:

マルチレンダーターゲットビューを描画デバイスにセット
cpDevice_->OMSetRenderTargets( mrtNum, renderTargetViews, &dsView );

これでOK…と言いたいのですが、案外忘れがちなビューポートの設定があります。



B レンダーターゲット用のビューポートを設定

 レンダーターゲットの画像サイズは必ずしもバックバッファのそれと同じとは限りません。もし画像サイズがバックバッファと異なるのであれば、描画の範囲であるビューポートをレンダーターゲット用に設定し直す必要があります。そうしないとバックバッファのサイズでレンダリングが切り取られてしまいます。設定自体は至極単純で、ビューポートの構造体(D3D10_VIEWPORT)を作り、ID3D10Device::RSSetViewportsメソッドに渡すだけです:

レンダーターゲットのビューポートを作成&設定
// レンダーターゲット毎のビューポート設定
D3D10_VIEWPORT vpForRT;
vpForRT[ i ].TopLeftX = 0;
vpForRT[ i ].TopLeftY = 0;
vpForRT[ i ].Width = 1024;
vpForRT[ i ].Height = 1024;
vpForRT[ i ].MinDepth = 0.0f;
vpForRT[ i ].MaxDepth = 1.0f;

cpDevice_->RSSetViewports( 1, &vpForRT );

 RSSetViewportsメソッドもビューポートを配列で渡す形式ですが、ここに異なるビューポートを渡してもジオメトリシェーダで特殊な記述をしない限り2つ目以降の意味はありません。マルチレンダーターゲットにしても、デフォルトでは使われるビューポートの設定は配列の一番最初の物だけなのでご注意下さい。

 これで呼び出し側の設定は終わりです。続いて書き込み側であるシェーダの記述を見てみましょう。



C シェーダのマルチレンダリング出力

 シェーダでマルチに出力するにはピクセルシェーダの戻り値を構造体にして、そこにSV_TARGET*セマンティクスを並べてあげます:

ピクセルシェーダで複数の出力
struct PS_OUTPUT {
    float4 target0 : SV_TARGET0;   // レンダーターゲット0番への出力カラー
    float4 target1 : SV_TARGET1;   // レンダーターゲット1番への出力カラー
    float4 target2 : SV_TARGET2;   // レンダーターゲット2番への出力カラー
    float4 target3 : SV_TARGET3;   // レンダーターゲット3番への出力カラー
};

PS_OUTPUT psMain( PS_INPUT In ) {
    PS_OUTPUT Out;
    Out.target0 = float4( 0.3, 0.3, 0.3, 1.0 );
    Out.target1 = float4( 0.8, 0.5, 0.7, 1.0 );
    Out.target2 = float4( 0.3, 0.1, 0.6, 1.0 );
    Out.target3 = float4( 0.2, 0.7, 0.3, 1.0 );
    return Out;
}

 SV_TARGET*はピクセルシェーダからの出力カラーを受け取る特別な変数です。番号とレンダーターゲット配列の要素番号とがそれぞれ対応しています。上の例は、一つのピクセルシェーダから4つのレンダーターゲットにそれぞれ違う色を出力しています。このシェーダだと各レンダーターゲットにはそれぞれのモデルが各出力カラーのシルエットで描画される事になります。

 一つ大切な事として、各レンダーターゲットへのブレンドステートをオープンにしてあげる必要があります。以下の設定をご覧ください:

ブレンドステート
BlendState bs {
    AlphaToCoverageEnable = FALSE;
    BlendEnable[ 0 ] = TRUE;
    BlendEnable[ 1 ] = TRUE;
    BlendEnable[ 2 ] = TRUE;
    BlendEnable[ 3 ] = TRUE;
    SrcBlend = ONE;
    DestBlend = ZERO;
    BlendOp = REV_SUBTRACT;
    SrcBlendAlpha = ONE;
    DestBlendAlpha = ZERO;
    BlendOpAlpha = ADD;
    RenderTargetWriteMask[ 0 ] = 0x0F;
    RenderTargetWriteMask[ 1 ] = 0x0F;
    RenderTargetWriteMask[ 2 ] = 0x0F;
    RenderTargetWriteMask[ 3 ] = 0x0F;
};

各レンダーターゲットの番号に対してBlendEnable(ブレンドを有効にする)をTRUEに、合成時のマスクもそれぞれ設定しています。MRTでは同じレンダーステートのみ適用できます。



 この章ではDirect3D10によるマルチレンダーターゲット描画の方法について見てきました。特に特別な設定をするわけではないのが分かりますよね。使い方は簡単、ですから後はこれを使って「何をするか」です。本章のサンプルプログラムでは簡単な例にしたいため「ブルーム効果」をMRTで実現してみました。眩しく光るモデルをご覧ください(^-^)