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

その50 やれば簡単マルチパスレンダリング


 マルチパスレンダリングというのは、複数の経路がある描画方法の事です。普段バックバッファのみに描画している方はシングルパスレンダリングをしている状態です。そうではなくて、1つの画面を作るために複数のサーフェイスに描画する技術を言います。

 本章ではマルチパスレンダリングをさらに狭義の意味で扱います。マルチパスレンダリングには「一度に複数枚のサーフェイスに描画してしまう技術」という意味合いもあります。例えば、Tex1とTex2というサーフェイスを使ってバックバッファに描画する場合、Tex1生成、Tex2生成、バックバッファへの描画と描画が3回(3パス)必要になります。マルチパスレンダリングを使うと、Tex1の生成とTex2の生成を1パスで一気に行うことが出来ます。描画回数が2回となるために大幅なパフォーマンスの向上に繋がります。大変に魅力的な技術なんです。

 この章ではそんな魅力たっぷりのマルチパスレンダリングについてビシッとまとめていこうと思います。



@ まずはレンダリングターゲット

 レンダリングについて少しおさらいします。私たちが普段何気なく描画しているバックバッファ。何となく「描画=バックバッファ」と思いがちになりますが、実はこれはちょっと端折っています。描画デバイスは、デフォルトで「レンダリングターゲット0番」に描画するように設定されています。そして、バックバッファはデフォルトでレンダリングターゲット0番に設定されているんです。つまり、描画デバイスとバックバッファの間には「レンダリングターゲット」というサーフェイスストッカーが挟まっているわけです:


これをしっかり意識するとマルチパスレンダリングがわかりやすくなります。



A IDirect3DDevice9::SetRenderTargetメソッド

 上図のようにバックバッファはデフォルトでRT0に設定されています。しかしそこはバックバッファの占有席ではありません。IDirect3DDevice9::SetRenderTargetメソッドを用いると自前で作成したサーフェイスをRT0やRT1などに設定する事ができます:

IDirect3Device9::SetRenderTargetメソッド
HRESULT SetRenderTarget(
   DWORD RenderTargetIndex,
   IDirect3DSurface9 *pRenderTarget
);

RenderTargetIndexにはレンダリングターゲットの番号を指定します。
pRenderTargetにはサーフェイスを渡します。

 気を付けたいのが、SetRenderTargetメソッドを用いると前に設定されていたサーフェイスへのポインタが失われてしまいます。例えばTR0に自前のサーフェイスを設定するとバックバッファに戻す事ができません。ですから通常は前に設定されていたサーフェイスはIDirect3DDevice9::GetRenderTargetで予め保持しておいて、それを入れ替えます。



B 指定のレンダリングターゲットに描画する

 RT0以外のサーフェイスへの描画は固定機能パイプラインでは残念ですができません(たぶんです)。これを可能にするのはプログラマブルシェーダです。

 シェーダ(HLSL)を使っている方は、ピクセルシェーダで次のような記述をしていると思います:

float4 Test_PS( float2 texCoord : TEXCOORD0 ) : COLOR0
{
   float outColor
   // 色々と記述して
   return outColor;
}

赤文字に注目です。ここにあるCOLOR0というのは、実はレンダリングターゲット0番に描画しなさいよ〜という意味のセマンティクスなんです。COLORの後ろの数字が実は書き込み先のターゲット番号だったというわけです。ですから、

float4 Test_PS( float2 texCoord : TEXCOORD0 ) : COLOR1
{
   float outColor
   // 色々と記述して
   return outColor;
}

とすればレンダリングターゲット1番に描きこまれます。「これはシングルパスレンダリングじゃん。じゃぁ、0番と1番に同時に描くにはどうしたらいいの?」と思われるかもしれません。これは上手い方法がありまして、戻り値を構造体にしてしまいます:

struct OUTPUT_PS {
   float4 color0 : COLOR0;
   float4 color1 : COLOR1;
};

OutPS Test_PS( float2 texCoord : TEXCOORD0 )
{
   OutPS outColor
   // 色々と記述して
   return outColor;
}

これを初めて知った時は「おお、なるほど」目から鱗でした。1つのピクセルシェーダから2つの情報を出すには、確かに構造体しかありませんね。



C MRTのサポートに注意

 マルチレンダリングターゲットはわりと最近は使えるようになっているのですが、ちょっと古めのビデオカードだとサポートしていない事もあります。マルチレンダリングターゲットとして持てるサーフェイスの最大数は、IDirect3DDevice9::GetDeviceCapsメソッドで取得できるD3DCAPS9構造体のNumSimultaneousRTsの数でチェックします:

マルチレンダリングターゲットの最大数
D3DCAPS9 Caps;
pDev->GetDeviceCaps( &Caps );
DWORD MaxRT = Caps.NumSimultaneousRTs;

これが「1」の場合、残念ながらマルチパスレンダリングを行うことができません。私の環境で試してみますと確か4年程前に買ったELSA GLADIAC FX736Ultra DDR3(GeForce FX 5700 Ultra)では「1」、ノートパソコン(FMV-BIBLO MG70H 2004年春モデルIntel(R) 82852/82855 GM/GME Graphics Controller)でも「1」でした。とある関係で譲り受けたNVIDIA GeForce6600に差し替えてみるとこれが「4」になっていました。大雑把ですが2005年以降くらいのビデオカードからは使えるようになっているのかなと予想します。

 プログラム上でこれを確認するのが面倒という方は、DirectXのユーティリティの中にある「DirectX Caps Viewer(DXCapsViewer.exe)」を試してみてください。これはビデオカードの能力を列挙してくれるアプリケーションです。ご自身のビデオカードのフォルダ下にある[D3D Device Types]→[HAL]→[Caps]の中に上のフラグがあります。



D マルチパスレンダリングで何をするのか?

 マルチパスレンダリングをすることにより何が出来るのか?これは一概にこれと言えるものではありませんが、昨今の複雑で何枚ものテクスチャを重ね張りする描画表現には必須の技術である事は確かです。特に「動的にテクスチャを作る」エフェクトで効果的です。動的に作るというと、例えばポストエフェクト(描画した絵にぼかしやセピア化などの加工を加える後処理の事)、キューブマップ作成、鏡の効果などなど様々あります。

 マルチパスレンダリングで密かになるほどなと思ったのは、頂点シェーダとの兼ね合いです。頂点シェーダでは非常に複雑なボーン操作、ピクセルシェーダでのライト処理のための法線の設定、頂点変換など面倒な事を色々と行います。シングルパスレンダリングの場合、毎回この作業を繰り返さなければなりません。一方マルチパスレンダリングであれば、頂点シェーダの作業は1回のみで済む事が殆どです。これはパフォーマンスの向上に繋がります。

 マルチパスレンダリングで何が出来るかは、シェーダプログラムを学んでいくとおのずと見えてきますが、以下では練習のためにRGBカラーとアルファ情報を2つのサーフェイス(一方はバックバッファ)に同時にレンダリングする例を示します。



E マルチパスレンダリング例その1:色とアルファ値を同時描画

 この例はDirectX付属の飛行機モデルを描画します。この時RGBカラーとアルファ値を分離して別々のテクスチャに一度に描画してみます。そして、その結果を交互に画面に描画します。これが何に使えるかというのは、とりあえず考えないで下さい(^-^;。この簡単な例だけでも、マルチパスレンダリングの本質は理解できると思います。ちゃちゃっと作るつもりだったのですが・・・いざ取り掛かってみると意外と色々勘所があるようです。

○ シェーダプログラム

 まずはシェーダプログラムから作ります。目標はピクセルシェーダで描画情報をRGBとアルファに分離する事です:

シェーダプログラム(RGBAndAlphaMPR.fx)
float4x4 WVP; // WorldViewProjection Matrix
texture Tex; // Texture
sampler2D TexSampler = sampler_state { Texture = (Tex); };

struct OUTPUT_VS
{
   float4 pos : POSITION;
   float2 texCoord : TEXCOORD0;
};

struct OUTPUT_PS
{
   float4 color : COLOR0;
   float4 alpha : COLOR1;
};

OUTPUT_VS SimpleVS( float4 inPos : POSITION, float2 inTexCoord : TEXCOORD0 )
{
   OUTPUT_VS outVS = (OUTPUT_VS)0;
   outVS.pos = mul( inPos, WVP );
   outVS.texCoord = inTexCoord;
   return outVS;
}

OUTPUT_PS RGBAndAlphaMPR_PS( float2 texCoord : TEXCOORD0 )
{
   OUTPUT_PS PSout = (OUTPUT_PS)0;
   PSout.color = tex2D( TexSampler, texCoord );
   PSout.alpha = PSout.color.a;
   PSout.alpha.a = 1.0f;
   return PSout;
}

 じっくり説明します。まずワールドビュープロジェクション行列としてWVPを宣言しています。これはお決まりですね。続くTexというのはモデルに貼り付けるテクスチャです。これはピクセルシェーダで使います。samplerはテクスチャから色をとってくれる人ですが、今回はTexから取ってもらうように設定しています。本当はサンプリングの方法などを定義するのですが、今回は簡単のために省いています。

 続くOUTPUT_VSは頂点シェーダの戻り値となる構造体です。今回は変換後の頂点位置とテクスチャ座標(UV座標)が必要なのでそう設定しています。その次にあるOUTPUT_PSがこのシェーダプログラムの肝です。ここには2つのCOLORが設定されています。OUTPUT_PS::alphaにアルファ情報が格納され、それがレンダリングターゲット1番に出力される運びとなります。

 SimpleVS頂点シェーダは特に何をしているというわけでもありません。入力されたローカル頂点をWVP行列でスクリーン座標に変換します。UV座標も構造体にコピーして戻り値に返しています。

 ピクセルシェーダでは入力されてきたUV座標に該当するテクセル(テクスチャ座標上のピクセル)をtex2D関数で取得しています。これはピクセルシェーダの超基本の1つです。取得した色からアルファ値をPSout.alphaに代入しています。最後にreturnで構造体を返しています。これで2つのレンダリングターゲットに一度に描画されます。


○ テクニック

 今回はアルファの入ったテクスチャを飛行機に貼り付けます。そこで透明度を有効にする処理をテクニックに記述します:

テクニック(RGBAndAlphaMPR.fx)
technique RGBAndAlphaMPR
{
   pass p0
   {
      // レンダリングステート設定
      AlphaBlendEnable = TRUE;
      SrcBlend = SRCALPHA;
      DestBlend = INVSRCALPHA;
      ColorOp[0] = SELECTARG1;
      ColorArg1[0] = TEXTURE;
      ColorArg2[0] = DIFFUSE;
      AlphaOp[0] = SELECTARG1;
      AlphaArg1[0] = TEXTURE;
      AlphaArg2[0] = DIFFUSE;
      ColorOp[1] = DISABLE;
      AlphaOp[1] = DISABLE;

      // シェーダ
      VertexShader = compile vs_2_0 SimpleVS();
      PixelShader = compile ps_2_0 RGBAndAlphaMPR_PS();
   }
}

アルファブレンドを有効にするのでAlphaBlendEnableをTRUEに、以下アルファブレンドを実現するスタンダードな設定です。テクニック内にレンダリングターゲットの記述をしておくと、プログラム側で凄い楽をできます(^-^)


○ プログラム

 先のシェダーを作ってしまえば、通常の描画にちょっと手を加えただけでマルチパスレンダリングができてしまいます。ここでは少し長めですが全ソースを掲載致します。大切な所を赤色で示しています。また、以下のソースはコピペして直ぐに使えます:

メインプログラム(main.cpp)
// マルチレンダリングターゲットテスト

#pragma comment(lib, "dxguid.lib")
#pragma comment(lib, "d3d9.lib")
#pragma comment(lib, "d3dx9.lib")

#include <windows.h>
#include <tchar.h>
#include <d3d9.h>
#include <d3dx9.h>
#include <math.h>

TCHAR gName[100] = _T("マルチレンダリングターゲットテスト");

#define SAFERELEASE(x) if(x){x->Release();}
#define FULLRELEASE \
   SAFERELEASE(pAlphaSurf); \
   SAFERELEASE(pBackBuffer); \
   SAFERELEASE(pEffect); \
   SAFERELEASE(pErrorBuffer); \
   SAFERELEASE(pAirPlaneTex); \
   SAFERELEASE(pAirPlane); \
   SAFERELEASE(pAlphaTex); \
   SAFERELEASE(g_pD3DDev); \
   SAFERELEASE(g_pD3D);

#define FAILEDCHECK(x) \
   if(FAILED(x)) { \
      FULLRELEASE; \
   return 0; \
}

// ウィンドウプロシージャ
LRESULT CALLBACK WndProc(HWND hWnd, UINT mes, WPARAM wParam, LPARAM lParam){
   if(mes == WM_DESTROY || mes == WM_CLOSE ) {PostQuitMessage(0); return 0;}
   return DefWindowProc(hWnd, mes, wParam, lParam);
}


int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)
{
   // アプリケーションの初期化
   MSG msg; HWND hWnd;
   WNDCLASSEX wcex ={sizeof(WNDCLASSEX), CS_HREDRAW | CS_VREDRAW, WndProc, 0, 0, hInstance, NULL, NULL,
   (HBRUSH)(COLOR_WINDOW+1), NULL, (TCHAR*)gName, NULL};
   if(!RegisterClassEx(&wcex))
      return 0;

   if(!(hWnd = CreateWindow(gName, gName, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, 640, 480,
      NULL, NULL, hInstance, NULL)))
      return 0;

   // Direct3Dの初期化
   LPDIRECT3D9 g_pD3D;
   LPDIRECT3DDEVICE9 g_pD3DDev;
   if( !(g_pD3D = Direct3DCreate9( D3D_SDK_VERSION )) ) return 0;

   D3DPRESENT_PARAMETERS d3dpp = {640,480,D3DFMT_UNKNOWN,0,D3DMULTISAMPLE_NONE,0,
            D3DSWAPEFFECT_DISCARD,NULL,TRUE,TRUE,D3DFMT_D24S8,0,D3DPRESENT_RATE_DEFAULT,D3DPRESENT_INTERVAL_DEFAULT};

   if( FAILED( g_pD3D->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd, D3DCREATE_HARDWARE_VERTEXPROCESSING, &d3dpp, &g_pD3DDev ) ) )
   if( FAILED( g_pD3D->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd, D3DCREATE_SOFTWARE_VERTEXPROCESSING, &d3dpp, &g_pD3DDev ) ) )
   {
      g_pD3D->Release();
      return 0;
   }

   D3DCAPS9 Caps;
   g_pD3DDev->GetDeviceCaps( &Caps );
   DWORD RT = Caps.NumSimultaneousRTs;
   if ( RT <= 1 ) {
      MessageBox( hWnd, _T("マルチレンダリングターゲットがサポートされていません。終了します。"), _T("サポート外エラー"),0);
      g_pD3DDev->Release();
      g_pD3D->Release();
      return 0;
   }

   // 変数定義
   ID3DXMesh *pAirPlane = 0;
   IDirect3DTexture9 *pAirPlaneTex = 0;
   IDirect3DTexture9 *pAlphaTex = 0;
   ID3DXEffect *pEffect = 0;
   ID3DXBuffer *pErrorBuffer = 0;
   IDirect3DSurface9 *pAlphaSurf = 0;
   IDirect3DSurface9 *pBackBuffer = 0;

   // 飛行機オブジェクトの作成
   DWORD NumMaterials;
   FAILEDCHECK( D3DXLoadMeshFromX( _T("airplane 2.x"), D3DXMESH_MANAGED, g_pD3DDev, NULL, NULL, NULL, &NumMaterials, &pAirPlane) );
   FAILEDCHECK( D3DXCreateTextureFromFile( g_pD3DDev, _T("AirPlaneTex.png"), &pAirPlaneTex ) );

   // エフェクトの生成
   FAILEDCHECK( D3DXCreateEffectFromFile( g_pD3DDev, _T("RGBAndAlphaMPR.fx"), 0, 0, 0, 0, &pEffect, &pErrorBuffer) );

   // アルファ用テクスチャの作成
   FAILEDCHECK( g_pD3DDev->CreateTexture( 640, 480, 1, D3DUSAGE_RENDERTARGET, D3DFMT_A8R8G8B8, D3DPOOL_DEFAULT, &pAlphaTex, 0 ) );

   // レンダリングターゲットセット
   pAlphaTex->GetSurfaceLevel( 0, &pAlphaSurf );
   g_pD3DDev->SetRenderTarget( 1, pAlphaSurf );
   g_pD3DDev->GetRenderTarget( 0, &pBackBuffer );


   // 各行列生成
   D3DXMATRIX WorldMat, ViewMat, ProjMat, WVP;
   D3DXMatrixIdentity( &WorldMat );
   D3DXMatrixPerspectiveFovLH( &ProjMat, D3DXToRadian(45), 640.0f/480.0f, 0.1f, 100.0f);

   ShowWindow(hWnd, nCmdShow);

   // メッセージ ループ
   int count = 0;
   double angle = 0.0;
   do{
      Sleep(1);
      if( PeekMessage(&msg, NULL, 0, 0, PM_REMOVE) ){ DispatchMessage(&msg);}

      g_pD3DDev->Clear( 0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(40,40,80), 1.0f, 0 );
      g_pD3DDev->BeginScene();

      // ビュー回転
      angle += 0.01f;
      D3DXMatrixLookAtLH(
         &ViewMat,
         &D3DXVECTOR3((float)(10.0f*cos(angle)),5,(float)(10.0f*sin(angle))),
         &D3DXVECTOR3(0,0,0),
         &D3DXVECTOR3(0,1,0)
         );
      WVP = WorldMat * ViewMat * ProjMat;

      // レンダリングターゲットを交換
      if ( count++ % 60 == 0 ) {
         IDirect3DSurface9 *pTmp = pAlphaSurf;
         pAlphaSurf = pBackBuffer;
         pBackBuffer = pTmp;
         g_pD3DDev->SetRenderTarget( 1, NULL ); // これをしないと文句言われます(^-^;
         g_pD3DDev->SetRenderTarget( 0, pBackBuffer );
         g_pD3DDev->SetRenderTarget( 1, pAlphaSurf );
      }

      // 描画デバイスにエフェクトセット
      pEffect->SetMatrix("WVP", &WVP );
      pEffect->SetTexture("Tex", pAirPlaneTex );
      pEffect->SetTechnique( "RGBAndAlphaMPR" );
      unsigned int numPass;
      pEffect->Begin( &numPass, 0 );
      pEffect->BeginPass( 0 );
      unsigned int i;
      for( i = 0; i < NumMaterials; i++ )
         pAirPlane->DrawSubset( i );
      pEffect->EndPass();

      g_pD3DDev->EndScene();
      g_pD3DDev->Present( NULL, NULL, NULL, NULL );
   }while(msg.message != WM_QUIT);

   FULLRELEASE;

   return 0;
}


赤文字はマルチパスレンダリングに必要な部分です。IDirect3DDevice9::SetRenderTargetメソッドで作成したアルファ用テクスチャ(サーフェイス)をRT1番に登録しています。描画部分では先に用意したシェーダ内で定義した「WVP」と「Tex」に行列とモデルに貼り付けるテクスチャを渡しています。後はパスを呼び出すだけです。この辺りについては本サイトのプログラマブルシェーダ編をご覧下さい。

 このプログラムを実行した結果は次のようになります。



 昨今のハイスペックなPC環境で、マルチパスレンダリングは当たり前の技術になってきています。今回ご紹介したようにその方法は意外と簡単です。マルチパスレンダリングをサポートしていない環境に対する配慮なども必要になりますが、このレンダリング方法を知っておけば複雑なエフェクトも効率よく作る事ができます。ガンガン導入してみましょう〜。

 尚、今回の例はファイルとして公開いたしますのでお試し下さい。