ボリューム霧


~ Volume fog ~






■はじめに

さて、浮動小数点数影では無理してIEEEフォーマットのテクスチャを使って何も成果が出せず、残念だったのですが、 もう少し恩恵を受けることをやってみましょう。
今回は、ボリューム霧に挑戦してみます。
上の画像はボクセル表現された UFO が宙を浮いています。
ボリューム霧は、すでにCEDEC2002でも紹介されて、やり方はすでに白日の下にさらされている訳ですが、 DirectX9 世代に入って、より現実的なエフェクトになってきました。
今回は、IEEE フォーマットを使っただけでなく、MRT(Multi rendering target:複数のレンダリングターゲットへ同時にレンダリングする)を使ってパス数を減らすことも行っています。

まぁ、いつものように適当にファイルが入っています。
APP WIZARD から出力されるフレームワークのファイルは紹介を省かせていただきます。

hlsl.fxシェーダの入ったエフェクトファイル
main.hアプリケーションのヘッダ
main.cppアプリケーションのソース

あと、実行ファイル、表示用モデル及び、プロジェクトファイルが入っています。

■ボリューム霧の考え方

ボリューム霧ですが、考え方はシンプルです。
そもそも、普段用いている霧は、現在自分がいる点から物体が描かれる点まで間の不純物による光の散乱をシミュレートしています。
不純物粒子が特定の間にしか存在しない状態がボリューム霧です。
ボリューム霧を再現するには、ボリュームの奥までの霧の濃度から、手前の濃度までの差を計算すればよいでしょう。

さて、ここで注意する点は、霧の中に不透明な物体があったときです。
この時、不透明な物体の先にある不純物は見えませんから、不透明物体を霧の一番先の場所とみなして計算します。

今回の描画の手順ですが、実は4パスです(しかもMRTを使ってさえも…)。
1パス目で、MRTを使って、ボリューム霧の無い状態の色と深度をレンダリングします。
今回、深度をR23Fフォーマットでレンダリングしましたが、ここは別にR8G8B8A8でもいいと思います。
2パス目と3パス目で、フォグの厚みをレンダリングします。
2パス目で表側の深度を加算で、3パス目で裏側の深度を減算でレンダリングします。
最後に4パス目で、計算されたフォグの厚みから、ポスト処理でフォグの計算をしてボリュームレンダリングする物体を浮かび上がらせます。

フォグの厚みを計るバッファは、浮動小数点数バッファなのですが、浮動小数点数バッファはアルファ合成が使えないので、 現在書き込んでいるバッファをテクスチャとしてレンダリングしています。
この方法は禁じ手の1つです。このレンダリングをするために、1つのオブジェクト(のサブセット)をレンダリングするたびに、 BeginScene/EndScene で描画の終了を待ちました。普通のフォーマットだと、半透明合成が使えるのでこの状況は楽なのですが、 今度は精度が足りなくなるので一長一短です。
いい忘れましたが、今回の方法は「表-裏」を計算するので、「閉じた」モデルで無いとだめです。
また、凸なモデルでないと、レンダリングパイプラインの問題で正常に計算できないので、 モデルは凸のサブセットに分けて置いてください。

■パス1:霧なしシーンのレンダリング

今回は、パスごとに見ていきましょう。
最初のレンダリングは、色と深度の描画です。
この2つのシーンに関しては、同じ方向からの描画なので、MRTを使って1度に処理ができます。
MRTを使うための条件は、D3DCAPS9 に、NumSimultaneousRTs という同時にレンダリングできる枚数を格納した変数があるので、この値を確認します。

main.cpp
0128: //-------------------------------------------------------------
0129: // Name: ConfirmDevice()
0130: // Desc: 初期化の時に呼ばれます。必要な能力をチェックします。
0131: //-------------------------------------------------------------
0132: HRESULT CMyD3DApplication::ConfirmDevice( D3DCAPS9* pCaps
0133:                         , DWORD dwBehavior, D3DFORMAT Format )
0134: {
0139:     // シェーダのチェック
0140:     if( pCaps->VertexShaderVersion < D3DVS_VERSION(1,1) &&
0141:       !(dwBehavior & D3DCREATE_SOFTWARE_VERTEXPROCESSING ) )
0142:         return E_FAIL;  // 頂点シェーダ
0143:     
0144:     if( pCaps->PixelShaderVersion < D3DPS_VERSION(2,0))
0145:         return E_FAIL;  // ピクセルシェーダ
0146: 
0147:     // MRT を2枚使う
0148:     if(pCaps->NumSimultaneousRTs < 2) return E_FAIL;
0149: 
0150:     return S_OK;
0151: }

では、シェーダを見てみましょう。
このレンダリングでは、いつものレンダリングの他に深度を書き込みますので、 深度を(テクスチャ座標として)いつもより多めに出力します。

hlsh.fx
0048: // -------------------------------------------------------------
0049: // 頂点シェーダプログラム
0050: // -------------------------------------------------------------
0051: VS_OUTPUT VS (
0052:       float4 Pos    : POSITION          // 頂点位置
0053:     , float4 Normal : NORMAL            // 法線ベクトル
0054:     , float4 Tex    : TEXCOORD0         // テクスチャ座標
0055: ){
0056:     VS_OUTPUT Out = (VS_OUTPUT)0;        // 出力データ
0057:     
0058:     // 座標変換
0059:     float4 pos = mul( Pos, mWVP );
0060:     
0061:     // 位置座標
0062:     Out.Pos = pos;
0063:     
0064:     // 照明計算
0065:     Out.Col = vCol * max( dot(vLightDir, Normal), 0);
0066:     
0067:     // テクスチャ座標
0068:     Out.Tex = Tex;
0069:     
0070:     // 深度
0071:     Out.Depth = pos.z / pos.w;
0072:     
0073:     return Out;
0074: }

今回は、MRTを使うので、ピクセルシェーダの出力がいつもの単純な色のベクトルではなく、構造体になります。
2つの色の出力に、深度といつものレンダリング結果を出力します。

hlsh.fx
0075: // -------------------------------------------------------------
0076: // ピクセルシェーダ出力データ
0077: // -------------------------------------------------------------
0078: struct PS_OUTPUT {
0079:     float4 Color : COLOR0;
0080:     float4 Depth : COLOR1;
0081: };
0082: // -------------------------------------------------------------
0083: // ピクセルシェーダプログラム
0084: // -------------------------------------------------------------
0085: PS_OUTPUT PS ( VS_OUTPUT In ) {
0086:     
0087:     PS_OUTPUT Out = ( PS_OUTPUT ) 0;
0088:     
0089:     // 通常色
0090:     Out.Color = In.Col * tex2D( DecaleMapSamp, In.Tex );
0091:     
0092:     // 深度
0093:     Out.Depth.x = In.Depth;
0094: 
0095:     return Out;
0096: }

では、シェーダを呼び出すアプリケーション側のプログラムです。
MRTの設定は簡単です。SetRenderTarget の1つめの引数を変えて、2回呼び出すだけです。
後は、いつもどおりです。シェーダの切り替えをして、変数をシェーダに設定してから描画します。

main.cpp
0382:             //-------------------------------------------------
0383:             // レンダリングターゲットの保存
0384:             //-------------------------------------------------
0385:             m_pd3dDevice->GetRenderTarget(0, &pOldBackBuffer);
0386:             m_pd3dDevice->GetDepthStencilSurface(&pOldZBuffer);
0387:             m_pd3dDevice->GetViewport(&oldViewport);
0388: 
0389:             //-------------------------------------------------
0390:             // レンダリングターゲットの変更
0391:             //-------------------------------------------------
0392:             m_pd3dDevice->SetRenderTarget(0, m_pColorMapSurf);
0393:             m_pd3dDevice->SetRenderTarget(1, m_pDepthMapSurf);
0394:             m_pd3dDevice->SetDepthStencilSurface(m_pMapZ);
0395:             m_pd3dDevice->SetViewport(&viewport);
0396: 
0397:             // レンダリングターゲットのクリア
0398:             m_pd3dDevice->Clear(0L, NULL,
0399:                             D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER,
0400:                             0xffffff, 1.0f, 0L);
0401: 
0402:             //-------------------------------------------------
0403:             // シェーダの設定
0404:             //-------------------------------------------------
0405:             hTechnique = m_pEffect->GetTechniqueByName( "TShader" );
0406:             m_pEffect->SetTechnique( hTechnique );
0407:             m_pEffect->Begin( NULL, 0 );
0408:             m_pEffect->Pass( 0 );
0409:             
0410:             //-------------------------------------------------
0411:             // シェーダ定数の設定
0412:             //-------------------------------------------------
0413:             // 座標変換
0414:             m = m_mWorld * m_mView * m_mProj;
0415:             m_pEffect->SetMatrix( m_hmWVP, &m );
0416: 
0417:             // ライト
0418:             D3DXMatrixInverse( &m, NULL, &m_mWorld);
0419:             D3DXVec4Transform( &v, &m_LightDir, &m );
0420:             D3DXVec4Normalize( &v, &v );v.w = 0.3f;
0421:             m_pEffect->SetVector( m_hvDir, &v );
0422: 
0423:             //-------------------------------------------------
0424:             // 描画
0425:             //-------------------------------------------------
0426:             D3DMATERIAL9 *pMtrl = m_pMeshBg->m_pMaterials;
0427:             for( i=0; i<m_pMeshBg->m_dwNumMaterials; i++ ) {
0428:                 // メッシュの色
0429:                 v.x = pMtrl->Diffuse.r;
0430:                 v.y = pMtrl->Diffuse.g;
0431:                 v.z = pMtrl->Diffuse.b;
0432:                 v.w = pMtrl->Diffuse.a;
0433:                 m_pEffect->SetVector( m_hvCol, &v );
0434:                 // テクスチャ
0435:                 m_pEffect->SetTexture("DecaleMap"
0436:                                 , m_pMeshBg->m_pTextures[i]);
0437: 
0438:                 m_pMeshBg->m_pLocalMesh->DrawSubset( i ); // 描画
0439:                 pMtrl++;
0440:             }
0441: 
0442:             m_pEffect->End();

■パス2、3:霧ボリュームのレンダリング

さて、霧のボリュームを計算します。
2パス目と3パス目ではやっていることはほとんど変わりません。
同じ頂点シェーダを使います。

hlsh.fx
0155: // -------------------------------------------------------------
0156: // 頂点シェーダプログラム
0157: // -------------------------------------------------------------
0158: VS_OUTPUT_VOLUME VS_VOLUME (
0159:       float4 Pos    : POSITION          // 頂点位置
0160:     , float4 Tex    : TEXCOORD0         // テクスチャ座標
0161: ){
0162:     VS_OUTPUT_VOLUME Out = (VS_OUTPUT_VOLUME)0;        // 出力データ
0163:     
0164:     // 座標変換
0165:     float4 pos = mul( Pos, mWVP );
0166:     
0167:     // 位置座標
0168:     Out.Pos = pos;
0169:     
0170:     // 深度
0171:     Out.Depth = pos.z / pos.w;
0172:     
0173:     // テクスチャ座標
0174:     Out.Tex = mul(Pos, mWVPT);
0175:     
0176:     return Out;
0177: }

2パス目と3パス目の違いは、表面と裏面を切り替えて、深度をレンダリングすることです。
今回は、1つのテクスチャを使ってレンダリングします。
最初に、表面を加算でレンダリングします。この時、分かりやすいようにすでに描かれた遮蔽物の深度からの差(を適当なスケーリングしたもの)を描画します。
深度の奥にある部分は影響されないはずなので、その時には値を0にして、最終的な影響が出ないように修正します。
3パス目では、裏面を減算で同様にレンダリングします。

hlsh.fx
0178: // -------------------------------------------------------------
0179: // ピクセルシェーダプログラム
0180: // -------------------------------------------------------------
0181: float4 PS_VOLUME1( VS_OUTPUT_VOLUME In) : COLOR
0182: {
0183:     float depth_map    = tex2Dproj(    DepthMapSamp, In.Tex ).x;
0184:     float frame_buffer = tex2Dproj( FrameBufferSamp, In.Tex ).x;
0185:     float diff = 20.0f*(In.Depth.x - depth_map);
0186:     
0187:     return frame_buffer - ((0 < diff) ? 0 : diff);
0188: }
0189: 
0190: // -------------------------------------------------------------
0191: // ピクセルシェーダプログラム
0192: // -------------------------------------------------------------
0193: float4 PS_VOLUME2 ( VS_OUTPUT_VOLUME In) : COLOR
0194: {
0195:     float depth_map    = tex2Dproj(    DepthMapSamp, In.Tex ).x;
0196:     float frame_buffer = tex2Dproj( FrameBufferSamp, In.Tex ).x;
0197:     float diff = 20.0f*(In.Depth.x - depth_map);
0198:     
0199:     return frame_buffer + ((0 < diff) ? 0 : diff);
0200: }

表面と裏面の切り替えはエフェクトファイル内でできます。
それぞれのパスで、D3DRS_を取っ払った変数で値を設定すると、レンダリングステートの設定ができます。

hlsh.fx
0202: // -------------------------------------------------------------
0203: // テクニック
0204: // -------------------------------------------------------------
0205: technique TVolume
0206: {
0207:     pass P0
0208:     {
0209:         Sampler[0] = (DepthMapSamp);
0210:         Sampler[1] = (FrameBufferSamp);
0211:         
0212:         // レンダリングステート
0213:         CullMode = CCW;
0214:         Zenable = False;
0215:         
0216:         // シェーダ
0217:         VertexShader = compile vs_1_1 VS_VOLUME ();
0218:         PixelShader  = compile ps_2_0 PS_VOLUME1();
0219: 
0220:     }
0221:     pass P1
0222:     {
0223:         Sampler[0] = (DepthMapSamp);
0224:         Sampler[1] = (FrameBufferSamp);
0225:         
0226:         // レンダリングステート
0227:         CullMode = CW;
0228:         Zenable = False;
0229:         
0230:         // シェーダ
0231:         VertexShader = compile vs_1_1 VS_VOLUME ();
0232:         PixelShader  = compile ps_2_0 PS_VOLUME2();
0233:     }
0234: }

呼び出しプログラムですが、そんなに面白いものではありません。
SetRenderTarget(1, NULL)で、MRTを切るのを忘れないようにしましょう。
後は、レンダリングターゲットをテクスチャに使うという危険な技を使うために、EndSceneで描画を区切る点が特別でしょうか。

main.cpp
0450:             //-------------------------------------------------
0451:             // レンダリングターゲットの変更
0452:             //-------------------------------------------------
0453:             m_pd3dDevice->SetRenderTarget(0, m_pFogMapSurf);
0454:             m_pd3dDevice->SetRenderTarget(1, NULL);
0455:             m_pd3dDevice->SetViewport(&viewport);
0456:             // レンダリングターゲットのクリア
0457:             m_pd3dDevice->Clear(0L, NULL,
0458:                             D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER,
0459:                             0x0, 1.0f, 0L);
0460: 
0461:             //-------------------------------------------------
0462:             // シェーダの設定
0463:             //-------------------------------------------------
0464:             hTechnique = m_pEffect->GetTechniqueByName( "TVolume" );
0465:             m_pEffect->SetTechnique( hTechnique );
0466:             m_pEffect->SetTexture("DepthMap", m_pDepthMap);
0467:             m_pEffect->SetTexture("FrameBuffer", m_pFogMap);
0468: 
0469:             D3DXMatrixTranslation ( &mL, 0,1.0f,0 );
0470:             m = mL*m_mWorld * m_mView * m_mProj;
0471:             m_pEffect->SetMatrix( m_hmWVP, &m );
0472: 
0473:             mWT = m * mT;
0474:             m_pEffect->SetMatrix( m_hmWVPT, &mWT );
0475:             
0476:             for( i=0 ; i<2 ; i++ ){
0477:                 for( DWORD j=0; j<m_pMesh->m_dwNumMaterials; j++ ){
0478:                     // 一度描画を終わらせて再スタート
0479:                     m_pd3dDevice->EndScene();
0480:                     m_pd3dDevice->BeginScene();
0481:                     m_pEffect->Begin( NULL, 0 );
0482:                     m_pEffect->Pass( i );
0483: 
0484:                     m_pMesh->m_pLocalMesh->DrawSubset( j );
0485:                     m_pEffect->End();
0486:                 }
0487:             }

■パス4:ボリューム霧の合成

いよいよ最後のパスです。
基本的には、Gバッファ(幾何バッファ:あらかじめ2次元テクスチャにレンダリングに必要な変数を落とし込んでおいて、 物体の裏に隠れる部分の処理を完全に省く)の考え方を踏襲して、2次元の全画面ポリゴンを1枚張るだけです。
頂点シェーダはそれらを何もせずにピクセルシェーダに送ります。

hlsh.fx
0276: // -------------------------------------------------------------
0277: // 頂点シェーダプログラム
0278: // -------------------------------------------------------------
0279: VS_OUTPUT_FINAL VS_FINAL (
0280:       float4 Pos    : POSITION          // 頂点位置
0281:     , float2 Tex    : TEXCOORD0         // テクスチャ座標
0282: ){
0283:     VS_OUTPUT_FINAL Out;        // 出力データ
0284:     
0285:     // 位置座標
0286:     Out.Pos = Pos;
0287:     
0288:     // テクスチャ座標
0289:     Out.Tex = Tex;
0290:     
0291:     return Out;
0292: }

ピクセルシェーダでは、フォグの体積をレンダリングした「fog_map」を合成パラメータとして、1パス目でレンダリングした結果とフォグの色を合成します。
計算自体は単純な線形フォグです。
好きな方は、お好きなように合成方法を変えてください。

hlsh.fx
0293: // -------------------------------------------------------------
0294: // ピクセルシェーダプログラム
0295: // -------------------------------------------------------------
0296: float4 PS_FINAL ( VS_OUTPUT_FINAL In) : COLOR
0297: {
0298:     float  fog_map = tex2D( FogMapSamp,   In.Tex ).x;
0299:     float4 col_map = tex2D( ColorMapSamp, In.Tex );
0300:     float4 fog_color = {0.84f, 0.88f, 1.0f, 1.0f};
0301:     
0302:     return lerp(col_map, fog_color, fog_map);

アプリケーション側もそんなにたいしたことはありません。
しいて言えば、D3DFVF_XYZRHW がDirectX8と違って正しく処理できるようになっているので、オールドタイプの方は気をつけてください。

main.cpp
0495:             //-------------------------------------------------
0496:             // レンダリングターゲットを元に戻す
0497:             //-------------------------------------------------
0498:             m_pd3dDevice->SetRenderTarget(0, pOldBackBuffer);
0499:             m_pd3dDevice->SetDepthStencilSurface(pOldZBuffer);
0500:             m_pd3dDevice->SetViewport(&oldViewport);
0501:             pOldBackBuffer->Release();
0502:             pOldZBuffer->Release();
0503: 
0504:             //-------------------------------------------------
0505:             // シェーダの設定
0506:             //-------------------------------------------------
0507:             hTechnique = m_pEffect->GetTechniqueByName( "TFinal" );
0508:             m_pEffect->SetTechnique( hTechnique );
0509:             m_pEffect->Begin( NULL, 0 );
0510:             m_pEffect->Pass( 0 );
0511:             m_pEffect->SetTexture( "FogMap",   m_pFogMap );
0512:             m_pEffect->SetTexture( "ColorMap", m_pColorMap );
0513: 
0514:             // バッファのクリア
0515:             m_pd3dDevice->Clear( 0L, NULL,
0516:                             D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER,
0517:                             0x00404080, 1.0f, 0L );
0518: 
0519:             m_pd3dDevice->SetFVF( D3DFVF_XYZRHW | D3DFVF_TEX1 );
0520:             typedef struct {FLOAT p[4]; FLOAT tu, tv;} TVERTEX;
0521:             TVERTEX Vertex[4] = {
0522:                 //         x               y     z rhw tu  tv
0523:                 {             0,              0, 0, 1, ds, dt},
0524:                 {(FLOAT)m_Width,              0, 0, 1,  s, dt},
0525:                 {(FLOAT)m_Width,(FLOAT)m_Height, 0, 1,  s,  t},
0526:                 {             0,(FLOAT)m_Height, 0, 1, ds,  t},
0527:             };
0528:             m_pd3dDevice->DrawPrimitiveUP( D3DPT_TRIANGLEFAN
0529:                             , 2, Vertex, sizeof( TVERTEX ) );
0530: 
0531:             m_pEffect->End();

ds、dt は、テクセル中心をずらすための「0.5/テクスチャサイズ」です。
今回、テクスチャのサイズをビューポートの大きさ以上の2nの大きさにしました (GeForce3のような2n以外のサイズのレンダリングターゲットを作れない場合もありますからね)。
テクスチャやビューポートの計算が少し面倒くさくなっているので、ソースを見る方は注意してください。

■最後に

むむむ、浮動小数点数バッファはアルファ合成ができないので、意外と不便でした。
ただ、負の値を保存できて値の範囲が大きくなると、このフォーマットが有利なのであるにこしたことは無いです。





もどる

imagire@gmail.com