前計算イラディアンス


~ Precomputed Irradiance ~






■はじめに

さて、最近、周りを見渡すと、そこかしこでSH(球面調和関数)、SHという声が聞こえます。
「ここでも早く紹介しなきゃなぁ~」と思っているのですが、やろうとすると結構面倒なので、2の足を踏んでいます。
ということで、その下準備として、モデルの各点に入射するイラディアンスを計算してみました。

で、いつものようにプログラムです。

ソースには、いつものように適当にファイルが入っています。
今回は、PS_2_0とMRT(Multi rendering target)を使うので、ほとんどRADEON9***専用です。

main.hアプリケーションのヘッダ
main.cppアプリケーションのソース
hlsl.fxシェーダプログラム

カーソルキーで、カメラが回って、zやxでズームが変えられるのは今までのとおりですが、 今回は、aキーで「イラディアンスのみ」、「デカールのみ」、「デカール*イラディアンス」の切り替えができます。

なお、実行すると、Athron 2000+ と RADEON 9700 Proの環境で下の様な画像で1分半ぐらい待たされると思います。これが前計算のイラディアンスを計算してる部分です。
実際のアプリケーションを組む場合には、あらかじめ処理しておいてテクスチャとして持つのがよいでしょう。

■概要

さて、今回の方法はどのようなプロセスを経ているのでしょうか?
ちょっとややこしいので、下の絵を見てください。

最初にメッシュに張られたテクスチャ座標をレンダリング先の座標として、法線マップと位置座標のマップを作成します。ただし、このときの法線マップは普通の接空間を基本とした法線マップではなくて、ローカル座標系での法線マップにしました。
次は、作成した法線マップと位置座標のマップから、テクセルの各点においてメッシュに張られた位置から見たイラディアンスを計算します。 求め方は、各テクセルから3次元の位置を計算して、そこから法線方向に見た画像をレンダリングします。ただし、レンダリングする座標変換は真横がレンダリングターゲットの端になるような曲面の座標変換で、空が見えれば白、地面が覆われるなら黒でレンダリングします。その後は、ミップマップの要領で画像を1テクセルまで平均化しテクセルの色を算出します。
後は、地面が平らだったときに真っ白になるように色の強さを調整して完成です。

■法線マップ、位置座標マップの作成

最初のステップは、法線マップと位置座標のマップの作成です。
2つのマップを同時に作成するので、ピクセルシェーダの出力先となる構造体を定義します。

hlsl.fx
0103: // ------------------------------------------------------------
0104: // ------------------------------------------------------------
0105: // 位置、法線マップの作成
0106: // ------------------------------------------------------------
0107: // ------------------------------------------------------------
0108: struct VS_OUTPUT_MAP
0109: {
0110:     float4 Pos          : POSITION;
0111:     float4 Position     : TEXCOORD0;
0112:     float4 Normal       : TEXCOORD1;
0113: };
0114: struct PS_OUTPUT_MAP
0115: {
0116:     float4 Position     : COLOR0;
0117:     float4 Normal       : COLOR1;
0118: };

頂点シェーダでは、テクスチャ座標を元に位置座標を計算し((0,0)-(1,1)の範囲を(-1,1)-(1,-1)にする(微調整つき))、あとは出力する2つの値をピクセルシェーダに適当に放り込みます。

hlsl.fx
0120: // ------------------------------------------------------------
0121: // 頂点シェーダ
0122: // ------------------------------------------------------------
0123: VS_OUTPUT_MAP VS_Map (
0124:       float4 Pos    : POSITION           // モデルの頂点
0125:     , float4 Normal : NORMAL             // 法線ベクトル
0126:     , float4 Tex0   : TEXCOORD0          // テクスチャ座標
0127: ){
0128:     float MAP_SIZE = 256;
0129:     VS_OUTPUT_MAP Out = (VS_OUTPUT_MAP)0;        // 出力データ
0130:     
0131:     float4 pos = mul( Pos, mWVP );
0132:     
0133:     // 位置座標
0134:     Out.Pos.x =  2.0 * (Tex0.x*(MAP_SIZE+1)/MAP_SIZE - 1/MAP_SIZE) - 1.0;
0135:     Out.Pos.y = -2.0 * (Tex0.y*(MAP_SIZE+1)/MAP_SIZE - 1/MAP_SIZE) + 1.0;
0136:     Out.Pos.z = 0.5;
0137:     Out.Pos.w = 1;
0138:     
0139:     // 色
0140:     Out.Position = Pos;
0141:     Out.Normal   = Normal;
0142: 
0143:     return Out;
0144: }

ピクセルシェーダでは、頂点シェーダから受け取った値を無難に出力します。

hlsl.fx
0147: // ------------------------------------------------------------
0148: // ピクセルシェーダ
0149: // ------------------------------------------------------------
0150: PS_OUTPUT_MAP PS_Map (VS_OUTPUT_MAP In)
0151: {
0152:     PS_OUTPUT_MAP Out;
0153:     
0154:     Out.Position = In.Position;
0155:     Out.Normal   = In.Normal;
0156: 
0157:     return Out;
0158: }

アプリケーションプログラムでは、レンダリングターゲットの2つのテクスチャに結びつけられたサーフェスを設定して、以上のシェーダを使って描画します。
なお、深度テストとカリングを切っておくと、向きなどを気にしなくてよく穴が開かないので、何も考えずに幸せになれるでしょう。

main.cpp
0381:         //-------------------------------------------------
0382:         // レンダリングターゲットの変更
0383:         //-------------------------------------------------
0384:         m_pd3dDevice->SetRenderTarget(0, m_pPosSurf);
0385:         m_pd3dDevice->SetRenderTarget(1, m_pNormalSurf);
0386:         m_pd3dDevice->SetDepthStencilSurface(NULL);
0387:         // ビューポートの変更
0388:         D3DVIEWPORT9 viewport_height = {0,0      // 左上の座標
0389:                         , MAP_SIZE  // 幅
0390:                         , MAP_SIZE // 高さ
0391:                         , 0.0f,1.0f};     // 前面、後面
0392:         m_pd3dDevice->SetViewport(&viewport_height);
0393: 
0394:         //-------------------------------------------------
0395:         // フレームバッファのクリア
0396:         //-------------------------------------------------
0397:         m_pd3dDevice->Clear(0L, NULL
0398:                         , D3DCLEAR_TARGET
0399:                         , 0x00000000, 1.0f, 0L);
0400: 
0401:         if( m_pEffect != NULL ) 
0402:         {
0403:             //-------------------------------------------------
0404:             // シェーダの設定
0405:             //-------------------------------------------------
0406:             m_pEffect->SetTechnique( m_hTechnique );
0407:             m_pEffect->Begin( NULL, 0 );
0408:             m_pEffect->Pass( 0 );
0409: 
0410:             RS( D3DRS_ZENABLE, FALSE );
0411:             RS( D3DRS_CULLMODE, D3DCULL_NONE );
0412: 
0413:             //-------------------------------------------------
0414:             // 背景の描画
0415:             //-------------------------------------------------
0416:             m_pMeshBg->Render(m_pd3dDevice);
0417: 
0418:             RS( D3DRS_ZENABLE, TRUE );
0419:             RS( D3DRS_CULLMODE, D3DCULL_CCW );
0420: 
0421:             m_pEffect->End();
0422:         }

■レンダリングターゲットのテクセル値を拾う

さて、レンダリングした情報を元にラディアンスを計算するのですが、レンダリングした結果を拾ってこなくてはなりません(VS_3_0世代になったら全てGPUでできそうですが…)。
そのための手順はちょっと面倒です。普段レンダリングターゲットとして使うテクスチャは「D3DPOOL_DEFAULT」で作成すると思うのですが、CPUから参照するためのテクスチャは「D3DPOOL_SYSTEMMEM」で作らないといけません。
ということで、法線マップ、位置マップ共に、同じサイズとフォーマットの別のテクスチャを用意して、そこに一度データをコピーすることによってCPUでレンダリングしたテクスチャを参照します。
コピー先のテクスチャは次のようにレンダリングターゲットと同じタイミングで作成します。

main.cpp
0290:     // レンダリングターゲットの生成
0294:     // 位置マップ
0295:     if (FAILED(m_pd3dDevice->CreateTexture(MAP_SIZE, MAP_SIZE, 1, 
0296:         D3DUSAGE_RENDERTARGET, D3DFMT_A32B32G32R32F, D3DPOOL_DEFAULT, &m_pPosTex, NULL)))
0297:         return E_FAIL;
0298:     if (FAILED(m_pPosTex->GetSurfaceLevel(0, &m_pPosSurf)))
0299:         return E_FAIL;
0300:     if (FAILED(m_pd3dDevice->CreateTexture(MAP_SIZE, MAP_SIZE, 1, 
0301:         0, D3DFMT_A32B32G32R32F, D3DPOOL_SYSTEMMEM , &m_pPosLockTex, NULL)))
0302:         return E_FAIL;
0303:     if (FAILED(m_pPosLockTex->GetSurfaceLevel(0, &m_pPosLockSurf)))
0304:         return E_FAIL;
0305:     // 法線マップ
0306:     if (FAILED(m_pd3dDevice->CreateTexture(MAP_SIZE, MAP_SIZE, 1, 
0307:         D3DUSAGE_RENDERTARGET, D3DFMT_A32B32G32R32F, D3DPOOL_DEFAULT, &m_pNormalTex, NULL)))
0308:         return E_FAIL;
0309:     if (FAILED(m_pNormalTex->GetSurfaceLevel(0, &m_pNormalSurf)))
0310:         return E_FAIL;
0311:     if (FAILED(m_pd3dDevice->CreateTexture(MAP_SIZE, MAP_SIZE, 1, 
0312:         0, D3DFMT_A32B32G32R32F, D3DPOOL_SYSTEMMEM , &m_pNormalLockTex, NULL)))
0313:         return E_FAIL;
0314:     if (FAILED(m_pNormalLockTex->GetSurfaceLevel(0, &m_pNormalLockSurf)))
0315:         return E_FAIL;

すると、「GetRenderTargetData」を使えばサーフェスとしてコピーできるので、後はロックして読み込みます。
配列pos[x][y][3]、normal[x][y][3]にデータを読み込みました。
今回は、浮動小数点フォーマットを使って、そのまま浮動小数点数として読み込みました(halfはこれができないから、パフォーマンスが悪くても32Fの浮動小数点数を使わざるを得ませんでした…)。

main.cpp
0446: //-------------------------------------------------------------
0447: // Name: FrameMoveCreateMap()
0448: // Desc: 位置、法線マップを作成する。
0449: //-------------------------------------------------------------
0450: static int ix=0;
0451: static int iy=0;
0452: static float pos[MAP_SIZE][MAP_SIZE][3];
0453: static float normal[MAP_SIZE][MAP_SIZE][3];
0454: int CMyD3DApplication::FrameMoveFinalGathering()
0455: {
0456:     ix = this->m_iCount % MAP_SIZE;
0457:     iy = this->m_iCount / MAP_SIZE;
0458:     
0459:     if(0==this->m_iCount){
0460:         D3DLOCKED_RECT d3dlr;
0461:         float *p;
0462:         m_pd3dDevice->GetRenderTargetData(m_pPosSurf,m_pPosLockSurf);
0463:         m_pPosLockSurf->LockRect(&d3dlr,NULL,D3DLOCK_READONLY); //サーフェイス上の矩形をロック    
0464:         p = (float *)d3dlr.pBits;
0465:         for(int y=0;y<MAP_SIZE;y++){
0466:         for(int x=0;x<MAP_SIZE;x++){
0467:             pos[x][y][0]=p[0];
0468:             pos[x][y][1]=p[1];
0469:             pos[x][y][2]=p[2];
0470:             p+=4;
0471:         }
0472:         }
0473:         m_pPosLockSurf->UnlockRect();
0474: 
0475:         m_pd3dDevice->GetRenderTargetData(m_pNormalSurf,m_pNormalLockSurf);
0476:         m_pNormalLockSurf->LockRect(&d3dlr,NULL,D3DLOCK_READONLY); //サーフェイス上の矩形をロック    
0477:         p = (float *)d3dlr.pBits;        
0478:         for(int y=0;y<MAP_SIZE;y++){
0479:         for(int x=0;x<MAP_SIZE;x++){
0480:             normal[x][y][0]=p[0];
0481:             normal[x][y][1]=p[1];
0482:             normal[x][y][2]=p[2];
0483:             p+=4;
0484:         }
0485:         }
0486:         m_pNormalLockSurf->UnlockRect();
0487:     }
0488: 
0489:     // 終了条件
0490:     if(MAP_SIZE-1==ix && MAP_SIZE-1==iy) return 1;
0491: 
0492:     return 0;
0493: }

■マスク

それが終わったら、ラディアンスを計算するのですが、その際にも少し手順があります。
最初に光が届かない、法線ベクトルと反対を向いている光の成分を0に落とします。これは、レンダリングターゲットを最初に適当な値でマスクすることによって実現します。つまり、最初に白丸を描いて円の外側の寄与が0になるようにしておいてから周りの背景を描画します。

これを行うには、丸いテクスチャを張り付けるだけです。
ただし、今回は色成分だけでなく、アルファ成分にも同じ値を書き込みます。

main.cpp
0527:         //-------------------------------------------------
0528:         // レンダリングターゲットの変更
0529:         //-------------------------------------------------
0530:         m_pd3dDevice->SetRenderTarget(0, m_pReductionSurf[0]);
0531:         m_pd3dDevice->SetDepthStencilSurface(NULL);
0532:         // ビューポートの変更
0533:         m_pReductionSurf[0]->GetDesc(&d3dsd);
0534:         viewport.Height = d3dsd.Width;
0535:         viewport.Width  = d3dsd.Height;
0536:         m_pd3dDevice->SetViewport(&viewport);
0537: 
0538:         //-------------------------------------------------
0539:         // 重みを持つ半球の部分だけ描画
0540:         //-------------------------------------------------
0541:         {
0542:         m_pd3dDevice->SetTextureStageState(0,D3DTSS_COLOROP,    D3DTOP_SELECTARG1);
0543:         m_pd3dDevice->SetTextureStageState(0,D3DTSS_COLORARG1,  D3DTA_TEXTURE);
0544:         m_pd3dDevice->SetTextureStageState(1,D3DTSS_COLOROP,    D3DTOP_DISABLE);
0545:         m_pd3dDevice->SetTextureStageState(0,D3DTSS_ALPHAOP,    D3DTOP_SELECTARG1);
0546:         m_pd3dDevice->SetTextureStageState(0,D3DTSS_ALPHAARG1,  D3DTA_TEXTURE);
0547:         m_pd3dDevice->SetTextureStageState(1,D3DTSS_ALPHAOP,    D3DTOP_DISABLE);
0548:         m_pd3dDevice->SetVertexShader(NULL);
0549:         m_pd3dDevice->SetPixelShader(0);
0550:         m_pd3dDevice->SetFVF( D3DFVF_XYZRHW | D3DFVF_TEX1 );
0551:         TVERTEX Vertex[4] = {
0552:             // x  y  z rhw tu tv
0553:             {          0,           0,0, 1, 0, 0,},
0554:             {d3dsd.Width,           0,0, 1, 1, 0,},
0555:             {d3dsd.Width,d3dsd.Height,0, 1, 1, 1,},
0556:             {          0,d3dsd.Height,0, 1, 0, 1,},
0557:         };
0558:         m_pd3dDevice->SetTexture( 0, m_pMaskTex );
0559:         m_pd3dDevice->DrawPrimitiveUP( D3DPT_TRIANGLEFAN, 2, Vertex, sizeof( TVERTEX ) );
0560:         }

書き込むテクスチャは、手続き的に作成します。
中心とテクセルの位置を計算して、その距離がテクスチャの大きさの半分よりも小さければ白に、大きければ黒く出力します。
なお、実際にはちょっと余裕をとって、2テクセルほど小さくしています。

main.cpp
0222: //-------------------------------------------------------------
0223: // 真ん中が白い絵を作る
0224: //-------------------------------------------------------------
0225: VOID WINAPI FillTex (D3DXVECTOR4* pOut, CONST D3DXVECTOR2* pTexCoord, 
0226: CONST D3DXVECTOR2* pTexelSize, LPVOID pData)
0227: {
0228:     FLOAT x = 2.0f*(pTexCoord->x-0.5f);
0229:     FLOAT y = 2.0f*(pTexCoord->y-0.5f);
0230:     FLOAT col = (x*x+y*y<(1.0f-2.0f/DIFFUSE_SIZE)*(1.0f-2.0f/DIFFUSE_SIZE))
0231:                 ? 1.0f : 0.0f;
0232:     
0233:     pOut->x = pOut->y = pOut->z = pOut->w = col;
0234: }

0338:     // マスク用のテクスチャの生成
0339:     if( FAILED(m_pd3dDevice->CreateTexture(DIFFUSE_SIZE, DIFFUSE_SIZE, 1
0340:                           , D3DUSAGE_RENDERTARGET, D3DFMT_A8R8G8B8
0341:                           , D3DPOOL_DEFAULT, &m_pMaskTex, NULL)))
0342:         return E_FAIL;
0343:     if( FAILED(D3DXFillTexture(m_pMaskTex, FillTex, NULL)))
0344:         return E_FAIL;

■ラディアンスの計算

いよいよ入射するラディアンスを計算します。
自分自身を魚眼レンズのようにレンダリングして遮蔽項を計算します。
どのように座標変換すればよいでしょうか?ここでランバートの余弦則を思い出しましょう。入射する光は、法線ベクトルの向きとの余弦の強さでラディアンスに寄与します。ということは、レンダリングされる面積が法線ベクトルの向きの余弦に比例すればよいということになります。ということで、レンダリングターゲットからの距離rが法線ベクトルと位置ベクトルのなす角の正弦になるようにレンダリングをします。

で、まぁどうするかということですが、3次元の位置座標を半径1の単位球に射影してそのx,y座標を(適用にスケーリングして)スクリーンの座標にします。これは、単位球に射影した点を平行投影したことになります。深度値は、単位円に射影した場所でのz値を用いれば正しい結果が得られます(今回は前後関係は使わないので関係ないのですが…)。

シェーダプログラムは次のようになります。
頂点シェーダでは、ビュー空間に射影した後に単位球に落として座標を計算します(mWVPなんて行列を使っていますが、これはビュー座標系への変換行列だけになっています)。

hlsl.fx
0160: // ------------------------------------------------------------
0161: // ------------------------------------------------------------
0162: // ラディアンスの計算
0163: // ------------------------------------------------------------
0164: // ------------------------------------------------------------
0165: struct VS_OUTPUT_RADIANCE
0166: {
0167:     float4 Pos          : POSITION;
0168:     float4 Color        : COLOR0;
0169:     float4 Depth        : TEXCOORD0;
0170: };
0171: 
0172: // ------------------------------------------------------------
0173: // 頂点シェーダプログラム
0174: // ------------------------------------------------------------
0175: VS_OUTPUT_RADIANCE VS_Radiance (
0176:       float4 Pos    : POSITION           // モデルの頂点
0177:      ,float4 Normal : NORMAL             // 法線ベクトル
0178:      ,float4 Tex0   : TEXCOORD0          // テクスチャ座標
0179: ){
0180:     VS_OUTPUT_RADIANCE Out = (VS_OUTPUT_RADIANCE)0;        // 出力データ
0181:     
0182:     float4 pos = mul( Pos, mWVP );
0183:     
0184:     // 位置座標
0185:     float rlen = rsqrt(dot(pos.xyz, pos.xyz));
0186:     pos *= rlen;
0187:     Out.Pos = pos;
0188:     Out.Pos.w = 1;
0189:     
0190:     Out.Color = 0;
0191:     
0192:     Out.Depth = pos.z;
0193:     
0194:     return Out;
0195: }
0196: // ------------------------------------------------------------
0197: // ピクセルシェーダプログラム
0198: // ------------------------------------------------------------
0199: float4 PS_Radiance (VS_OUTPUT_RADIANCE In) : COLOR
0200: {
0201:     // (深度を見て)裏面を消去する
0202:     clip(In.Depth);
0203:     
0204:     return In.Color;
0205: }

ピクセルシェーダでは、z値が負の場所は描画しないようにclip命令で削除します。こうすると後ろに回りこんだ不適切なピクセルの描画を抑止することができます。

なお、今回はオブジェクトを黒く描画しましたが、ここに色をつけると、より高次のラディアンスの効果を取り入れることができます。

さて、撮影するビュー座標系ですが、カメラの位置をメッシュの表面にして、カメラの向きを法線方向にします。
これは、最初に求めた法線ベクトルと位置座標マップを利用して、テクセルの位置座標マップの点から見て、見る先を法線マップによる法線ベクトルから求めてビュー行列を作ります。

なお、実際にはカメラの位置は位置座標マップの値から法線ベクトルの方向に「少し浮かせて」います。

main.cpp
0562:         if( m_pEffect != NULL ) 
0563:         {
0564:             //-------------------------------------------------
0565:             // シェーダの設定
0566:             //-------------------------------------------------
0567:             m_pEffect->SetTechnique( m_hTechnique );
0568:             m_pEffect->Begin( NULL, 0 );
0569:             m_pEffect->Pass( 1 );
0570:             
0571:             // アルファは書き込まない
0572:             RS(D3DRS_COLORWRITEENABLE, D3DCOLORWRITEENABLE_RED |D3DCOLORWRITEENABLE_GREEN |D3DCOLORWRITEENABLE_BLUE );
0573: 
0574:             //-------------------------------------------------
0575:             // 背景の描画
0576:             //-------------------------------------------------
0577:             float x[3] = {pos[ix][iy][0], pos[ix][iy][1], pos[ix][iy][2]};
0578:             float n[3] = {normal[ix][iy][0], normal[ix][iy][1], normal[ix][iy][2]};
0579:             D3DXVECTOR3 vFromPt   = D3DXVECTOR3( x[0], x[1], x[2] ) + 0.05f*D3DXVECTOR3( n[0], n[1], n[2] );
0580:             D3DXVECTOR3 vLookatPt = D3DXVECTOR3( n[0], n[1], n[2] ) + vFromPt;
0581:             D3DXVECTOR3 vUpVec    = D3DXVECTOR3( 0.0f, 1.0f, 0.0f );
0582:             D3DXMatrixLookAtLH( &mView, &vFromPt, &vLookatPt, &vUpVec );
0583: 
0584:             m_pEffect->SetMatrix( m_hmWVP, &mView );
0585: 
0586:             m_pMeshBg->Render(m_pd3dDevice);
0587: 
0588: 
0589:             RS(D3DRS_COLORWRITEENABLE, 0xf );
0590:             m_pEffect->End();
0591:         }

なお、今回の方法では、下の図のオレンジ色の部分のように、一部塗りつぶされない部分が出てきます。

これは、一部の頂点が裏側に回りこむことによって、本来描画されるべき(下の図で灰色の)部分が描画されないからです。(結果敵に赤色の部分だけが描画されます)。 とりあえず、今回は目立たなかったので、そのままにしておきましたが、本当の結果よりも明るくなっているはずなので対策が必要です。 なお、前述したカメラを地表から離す距離も、この欠ける面積の広さに依存して調整しています。

■この問題の解決法(追加 2003 Dec. 26)

以上の問題を解決する方法を思いつきました。
シャドウボリュームの作成法において、縮退四角形の変わりにトライアングルファンを用いて、その一頂点は光源の位置にし、さらにw=0にする方法が知られています。
この方法を利用して、裏側に回りこむ頂点はw=0にします。こうするとw=0にした頂点の分は射影座標系の外側に広がっていくので、隙間の無い画像が得られます。

hlsl.fx
0172: // ------------------------------------------------------------
0173: // 頂点シェーダプログラム
0174: // ------------------------------------------------------------
0175: VS_OUTPUT_RADIANCE VS_Radiance (
0176:       float4 Pos    : POSITION           // モデルの頂点
0177:      ,float4 Normal : NORMAL             // 法線ベクトル
0178:      ,float4 Tex0   : TEXCOORD0          // テクスチャ座標
0179: ){
0180:     VS_OUTPUT_RADIANCE Out = (VS_OUTPUT_RADIANCE)0;        // 出力データ
0181:     
0182:     float4 pos = mul( Pos, mWVP );
0183:     
0184:     // 位置座標
0185:     float rlen = rsqrt(dot(pos.xyz, pos.xyz));
0186:     pos *= rlen;
0187:     Out.Pos = pos;
0188:     Out.Pos.w = 1;
0189:     if(pos.z<0){
0190:         Out.Pos.w = 0;
0191:     }
0192:     
0193:     Out.Color = 0;
0194:     
0197:     return Out;
0198: }

■ラディアンスからイラディアンスへ

次に、レンダリングした結果を平均して1つの色に落としこんでイラディアンスとします。
平均するのは、64ボックスサンプリングしました。1パスで16テクセルサンプリングすることによって、8x8の64の領域を平均化します。

やっていることは、頂点シェーダからピクセルシェーダへ出力できる8つのテクスチャ座標を利用してピクセルシェーダで一様にずらすことによって、16テクセルのサンプリングをし、最終的に16で割って平均化します。

hlsl.fx
0216: // ------------------------------------------------------------
0217: // グローバル変数
0218: // ------------------------------------------------------------
0219: float MAP_WIDTH;
0220: float MAP_HEIGHT;
0221: 
0222: // ------------------------------------------------------------
0223: // テクスチャ
0224: // ------------------------------------------------------------
0225: texture ReductionMap;
0226: sampler ReductionSamp = sampler_state
0227: {
0228:     Texture = <ReductionMap>;
0229:     MinFilter = LINEAR;
0230:     MagFilter = LINEAR;
0231:     MipFilter = NONE;
0232: 
0233:     AddressU = Clamp;
0234:     AddressV = Clamp;
0235: };
0236: // ------------------------------------------------------------
0237: // 頂点シェーダからピクセルシェーダに渡すデータ
0238: // ------------------------------------------------------------
0239: struct VS_OUTPUT_REDUCTION
0240: {
0241:     float4 Pos          : POSITION;
0242:     float2 Tex0         : TEXCOORD0;
0243:     float2 Tex1         : TEXCOORD1;
0244:     float2 Tex2         : TEXCOORD2;
0245:     float2 Tex3         : TEXCOORD3;
0246:     float2 Tex4         : TEXCOORD4;
0247:     float2 Tex5         : TEXCOORD5;
0248:     float2 Tex6         : TEXCOORD6;
0249:     float2 Tex7         : TEXCOORD7;
0250: };
0251: 
0252: // ------------------------------------------------------------
0253: // 頂点シェーダプログラム
0254: // ------------------------------------------------------------
0255: VS_OUTPUT_REDUCTION VS_Reduction (
0256:       float4 Pos    : POSITION           // モデルの頂点
0257:      ,float4 Tex    : TEXCOORD0          // テクスチャ座標
0258: ){
0259:     VS_OUTPUT_REDUCTION Out = (VS_OUTPUT_REDUCTION)0;        // 出力データ
0260:     
0261:     // 位置座標
0262:     Out.Pos = Pos;
0263:     
0264:     Out.Tex0 = Tex + float2(3.0f/MAP_WIDTH, 1.0f/MAP_HEIGHT);
0265:     Out.Tex1 = Tex + float2(3.0f/MAP_WIDTH, 3.0f/MAP_HEIGHT);
0266:     Out.Tex2 = Tex + float2(3.0f/MAP_WIDTH, 5.0f/MAP_HEIGHT);
0267:     Out.Tex3 = Tex + float2(3.0f/MAP_WIDTH, 7.0f/MAP_HEIGHT);
0268:     Out.Tex4 = Tex + float2(1.0f/MAP_WIDTH, 1.0f/MAP_HEIGHT);
0269:     Out.Tex5 = Tex + float2(1.0f/MAP_WIDTH, 3.0f/MAP_HEIGHT);
0270:     Out.Tex6 = Tex + float2(1.0f/MAP_WIDTH, 5.0f/MAP_HEIGHT);
0271:     Out.Tex7 = Tex + float2(1.0f/MAP_WIDTH, 7.0f/MAP_HEIGHT);
0272:     
0273:     return Out;
0274: }
0275: 
0276: // ------------------------------------------------------------
0277: // ピクセルシェーダプログラム
0278: // ------------------------------------------------------------
0279: float4 PS_Reduction ( VS_OUTPUT_REDUCTION In ) : COLOR0
0280: {
0281:     float4 t0 = tex2D(ReductionSamp, In.Tex0);
0282:     float4 t1 = tex2D(ReductionSamp, In.Tex1);
0283:     float4 t2 = tex2D(ReductionSamp, In.Tex2);
0284:     float4 t3 = tex2D(ReductionSamp, In.Tex3);
0285:     
0286:     float4 t4 = tex2D(ReductionSamp, In.Tex4);
0287:     float4 t5 = tex2D(ReductionSamp, In.Tex5);
0288:     float4 t6 = tex2D(ReductionSamp, In.Tex6);
0289:     float4 t7 = tex2D(ReductionSamp, In.Tex7);
0290:     
0291:     float4 t8 = tex2D(ReductionSamp, In.Tex0 + float2(+4.0f/MAP_WIDTH, 0));
0292:     float4 t9 = tex2D(ReductionSamp, In.Tex1 + float2(+4.0f/MAP_WIDTH, 0));
0293:     float4 ta = tex2D(ReductionSamp, In.Tex2 + float2(+4.0f/MAP_WIDTH, 0));
0294:     float4 tb = tex2D(ReductionSamp, In.Tex3 + float2(+4.0f/MAP_WIDTH, 0));
0295:     
0296:     float4 tc = tex2D(ReductionSamp, In.Tex4 + float2(+4.0f/MAP_WIDTH, 0));
0297:     float4 td = tex2D(ReductionSamp, In.Tex5 + float2(+4.0f/MAP_WIDTH, 0));
0298:     float4 te = tex2D(ReductionSamp, In.Tex6 + float2(+4.0f/MAP_WIDTH, 0));
0299:     float4 tf = tex2D(ReductionSamp, In.Tex7 + float2(+4.0f/MAP_WIDTH, 0));
0300:     
0301:     return ((t0+t1+t2+t3)
0302:            +(t4+t5+t6+t7)
0303:            +(t8+t9+ta+tb)
0304:            +(tc+td+te+tf))/16;
0305: }

アプリケーションプログラムでは、この平均操作を2回行い、64x64の画像を1つの色に集約します。
なお、2回目の平均では、大きさを1テクセルにできるので、レンダリング先を元のテクスチャの対応する位置にしています。

1回目のぼかしのプログラム

main.cpp
0592:         //-------------------------------------------------
0593:         //-------------------------------------------------
0594:         // ラディアンスをミップマップの要領で小さくする
0595:         //-------------------------------------------------
0596:         //-------------------------------------------------
0597:         m_pd3dDevice->SetRenderTarget(0, m_pReductionSurf[1]);
0598:         m_pd3dDevice->SetDepthStencilSurface(NULL);
0599:         // ビューポートの変更
0600:         m_pReductionSurf[1]->GetDesc(&d3dsd);
0601:         viewport.Height = d3dsd.Width;
0602:         viewport.Width  = d3dsd.Height;
0603:         m_pd3dDevice->SetViewport(&viewport);
0604: 
0605:         TSS(0,D3DTSS_COLOROP,   D3DTOP_SELECTARG1);
0606:         TSS(0,D3DTSS_COLORARG1, D3DTA_TEXTURE);
0607:         TSS(1,D3DTSS_COLOROP,   D3DTOP_DISABLE);
0608:         
0609:         if( m_pEffect != NULL ) {
0610:             //-------------------------------------------------
0611:             // シェーダの設定
0612:             //-------------------------------------------------
0613:             m_pEffect->SetTechnique( m_hTechnique );
0614:             m_pEffect->Begin( NULL, 0 );
0615:             m_pEffect->Pass( 2 );
0616: 
0617:             m_pEffect->SetFloat("MAP_WIDTH",  DIFFUSE_SIZE);
0618:             m_pEffect->SetFloat("MAP_HEIGHT", DIFFUSE_SIZE);
0619: 
0620:             //-------------------------------------------------
0621:             // フィルタリング
0622:             //-------------------------------------------------
0623:             TVERTEX Vertex1[4] = {
0624:                 //   x    y     z    tu tv
0625:                 {-1.0f, +1.0f, 0.1f,  0, 0},
0626:                 {+1.0f, +1.0f, 0.1f,  1, 0},
0627:                 {+1.0f, -1.0f, 0.1f,  1, 1},
0628:                 {-1.0f, -1.0f, 0.1f,  0, 1},
0629:             };
0630:             m_pd3dDevice->SetFVF( D3DFVF_XYZ | D3DFVF_TEX1 );
0631:             m_pEffect->SetTexture("ReductionMap", m_pReductionTex[0]);
0632:             m_pd3dDevice->DrawPrimitiveUP( D3DPT_TRIANGLEFAN
0633:                             , 2, Vertex1, sizeof( TVERTEX ) );
0634: 
0635:             m_pEffect->End();
0636:         }

2回目のぼかしのプログラム

main.cpp
0638:         //-------------------------------------------------
0639:         //-------------------------------------------------
0640:         // ラディアンスをテクスチャの対応する位置に張る
0641:         //-------------------------------------------------
0642:         //-------------------------------------------------
0643:         m_pd3dDevice->SetRenderTarget(0, m_pDiffuseSurf);
0644:         m_pd3dDevice->SetDepthStencilSurface(NULL);
0645:         // ビューポートの変更
0646:         m_pDiffuseSurf->GetDesc(&d3dsd);
0647:         viewport.Height = d3dsd.Width;
0648:         viewport.Width  = d3dsd.Height;
0649:         m_pd3dDevice->SetViewport(&viewport);
0650:         
0651:         // 確認しやすくするために、最初は赤く塗りつぶす
0652:         if(0==ix&&0==iy)m_pd3dDevice->Clear(0L, NULL, D3DCLEAR_TARGET, 0x00800000, 1.0f, 0L);
0653: 
0654:         if( m_pEffect != NULL ) {
0655:             //-------------------------------------------------
0656:             // シェーダの設定
0657:             //-------------------------------------------------
0658:             m_pEffect->SetTechnique( m_hTechnique );
0659:             m_pEffect->Begin( NULL, 0 );
0660:             m_pEffect->Pass( 2 );
0661: 
0662:             m_pEffect->SetFloat("MAP_WIDTH",  8);
0663:             m_pEffect->SetFloat("MAP_HEIGHT", 8);
0664: 
0665:             //-------------------------------------------------
0666:             // フィルタリング
0667:             //-------------------------------------------------
0668:             float x =  2.0f*((float)ix/(float)MAP_SIZE) - 1.0f;
0669:             float y = -2.0f*((float)iy/(float)MAP_SIZE) + 1.0f;
0670:             TVERTEX Vertex1[4] = {
0671:                 //   x    y     z    tu tv
0672:                 {x,                      y,                     0.1f,  0, 0},
0673:                 {x+2.0f/(float)MAP_SIZE, y,                     0.1f,  1, 0},
0674:                 {x+2.0f/(float)MAP_SIZE, y-2.0f/(float)MAP_SIZE,0.1f,  1, 1},
0675:                 {x,                      y-2.0f/(float)MAP_SIZE,0.1f,  0, 1},
0676:             };
0677:             m_pd3dDevice->SetFVF( D3DFVF_XYZ | D3DFVF_TEX1 );
0678:             m_pEffect->SetTexture("ReductionMap", m_pReductionTex[1]);
0679:             m_pd3dDevice->DrawPrimitiveUP( D3DPT_TRIANGLEFAN
0680:                             , 2, Vertex1, sizeof( TVERTEX ) );
0681: 
0682:             m_pEffect->End();
0683:         }

■明度の調整

さて、最後です。モデルが平面だったらイラディアンスの強さが白になるように調整します。
つまり遮蔽されなかったときの強さで今までに求められた色を割ればいいです。
このときのために仕込んでいたものがあります。アルファ成分はマスクのテクスチャをレンダリングするときには書きこきましたが、それ以降は書き込んでいません。つまり、アルファ成分に遮蔽されなかったときの強さが格納されているので、その値で割るのです。アルファ成分に関しても平均操作は行われているので、適切にスケーリングされます。

hlsl.fx
0310: // ------------------------------------------------------------
0311: // アルファ成分で色成分を割る
0312: // ------------------------------------------------------------
0313: float4 PS_Div ( float4 Tex0 : TEXCOORD0 ) : COLOR0
0314: {
0315:     float4 samp = tex2D(SrcSamp, Tex0);
0316:     
0317:     if(samp.w != 0) samp /= samp.w;
0318:     
0319:     return samp;
0320: }

まぁ、アプリケーション側では、シェーダを指定して全画面にポリゴンを張るだけです。

main.cpp
0685:         //-----------------------------------------------------
0686:         // 最終フレームでは、アルファ成分の値で結果を割る(正規化)
0687:         //-----------------------------------------------------
0688:         if(MAP_SIZE-1==ix && MAP_SIZE-1==iy){
0689:             m_pd3dDevice->SetRenderTarget(0, m_pFinalSurf);
0690:             m_pd3dDevice->SetDepthStencilSurface(NULL);
0691:             // ビューポートの変更
0692:             m_pFinalSurf->GetDesc(&d3dsd);
0693:             viewport.Height = d3dsd.Width;
0694:             viewport.Width  = d3dsd.Height;
0695:             m_pd3dDevice->SetViewport(&viewport);
0696: 
0697:             if( m_pEffect != NULL ) {
0698:                 m_pEffect->SetTechnique( m_hTechnique );
0699:                 m_pEffect->Begin( NULL, 0 );
0700:                 m_pEffect->Pass( 3 );
0701: 
0702:                 TVERTEX Vertex[4] = {
0703:                     // x  y  z rhw tu tv
0704:                     {       0,       0,0, 1, 0+0.5f/(FLOAT)MAP_SIZE, 0+0.5f/(FLOAT)MAP_SIZE,},
0705:                     {MAP_SIZE,       0,0, 1, 1+0.5f/(FLOAT)MAP_SIZE, 0+0.5f/(FLOAT)MAP_SIZE,},
0706:                     {MAP_SIZE,MAP_SIZE,0, 1, 1+0.5f/(FLOAT)MAP_SIZE, 1+0.5f/(FLOAT)MAP_SIZE,},
0707:                     {       0,MAP_SIZE,0, 1, 0+0.5f/(FLOAT)MAP_SIZE, 1+0.5f/(FLOAT)MAP_SIZE,},
0708:                 };
0709:                 m_pd3dDevice->SetFVF( D3DFVF_XYZRHW | D3DFVF_TEX1 );
0710:                 m_pEffect->SetTexture(m_htSrcTex, m_pDiffuseTex);
0711:                 m_pd3dDevice->DrawPrimitiveUP( D3DPT_TRIANGLEFAN
0712:                                 , 2, Vertex, sizeof( TVERTEX ) );
0713: 
0714:                 m_pEffect->End();
0715:             }

■で、どうつかうの

さて、こうやって計算したイラディアンスですが、どうして使いましょうか?
そもそもイラディアンスは、各点へ入射する光の総量を計算したものです。
今回の計算では、さえぎられなかった部分を白に、さえぎられた部分を黒にする遮蔽項の計算をしているのですが、これは、2次反射が非常に弱い環境下での、天球が一様な強さの白色で覆われた場合のライティングの結果になっています。
ということで、求めたイラディアンスにデカールのテクスチャを乗算した結果は、この状況下でのライティングになります。

この精度までくると、デカールのテクスチャをリアルなものにしないと、結果がちゃちに見えてしまいますね。
まぁ、でも、洋ゲーっぽい雰囲気といえるでしょうか(逆に言えば、洋ゲーでのテクスチャの書き込み方がイラディアンスを意識したものであったということが理解できたということでしょう)。

■最後に

今回のプログラムを応用すれば、非リアルタイムでのいろいろなことができるのではないでしょうか。
気付かれている方は気付かれていると思いますが、今回の計算は定数の係数を除いては、球面調和関数展開の最低次の計算になっています(要するにアンビエント)。マスクのためのテクスチャを変更すれば、他の球面調和関数展開の計算も可能になります。
SHかぁ~。この計算を何回もするのかと思うと気が重くなりますね。高速化も考えないと…





もどる

imagire@gmail.com