半リアルタイム確率拡散光/鏡面光マップ


~ Semi-realtime stochastic diffuse/specular map ~






■最初に追記事項

Beeさんから、助言をいただきました。
07:14から、19:40ごろの更新の違いは、

半リアルタイムにIrradiance Mapの生成
等方的にサンプリング
解説の一部変更

です。

■はじめに

今回は、キューブマップの応用例として、拡散光マップや鏡面光マップを作成しました。
拡散光マップは最近よく見ますが、どうやって皆さん用意していますか?
ひょっとして、著作権無視してパクってきていませんか?
作れるなら自分で作りましょう。
今回は、半リアルタイムに放射マップを作成します。

まぁ、いつものように適当にファイルが入っています。
今回は、天球を半球にしてみました。

diffuse.vsh拡散光マップ作成の頂点シェーダー。
diffuse.psh拡散光マップ作成のピクセルシェーダー。
vs.vsh深度描画の頂点シェーダー。
ps.psh深度描画のピクセルシェーダー。
draw.cppメインの描画部分。
draw.h描画の各関数の定義。
bg.cpp背景の描画。
main.h基本的な定数など。
main.cpp描画に関係しないシステム的な部分。
load.cppロード。
load.hロードのインターフェイス。
tile.bmp (床デカール)
sky.bmp (空デカール)

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

■やっていること

拡散光マップとは、環境マップとして得られた放射照度のテクスチャをぼかすことによって、 各法線ベクトルに関する入射光の強さを前計算し、テクスチャによって拡散光を表現しようとする方法です。

基本的には、キューブマップにレンダリングした画像に関して、 光源ベクトルについてLambertの余弦則に応じてた明るさで合成します。
1度のサンプリングでは、キューブマップの1点に関しての合成しかできませんので、 何度もサンプリングして、最終的なキューブマップを作成します。
下の図では、L=(0,1,0)のy軸を向いた光源ベクトルに関した拡散光が1回のサンプリング例としてかかれています。

さて、サンプリングの方法ですが、方向ベクトルをランダムに選んでサンプリングします。
元のキューブテクスチャの各点は確率的にサンプリングされ、最終的な色はアンサンブル平均(乱数が違う状態での多くのサンプルの平均)として決定されます。
確率的なサンプリングでは、サンプリング数を多くしたときに正確な答えに近づきます。今回は、4つのテクスチャを256回レンダリングして、なおかつ反対向きのベクトルに関して同時にサンプリングを行うので、 計2048個の平行光線が周りから照射されたのと同じ効果が得られます。
結果は合っていると思うのですが、これがどのくらいよくできているのかわかりません・・・。

さらに、Beeさんに教えていただいたのですが、 等方的なサンプリングを行うために、立方体の中で一様に選ばれた乱数の中から、 単位球の中に収まるものだけを光源ベクトルに使うことによって、等方的なサンプリングを実現しています。

さて、256回レンダリングというわざとらしい回数ですが、 これは、特殊な合成法の限界のところからきています。
今回は、プログラムの合成に線形合成を使っています。
サンプリングのときに行いたい合成は、

      1      1            1
col = - a1 + - a2 + ・・・ + - an
      n      n            n

なので、加算合成すればよいかと思いますが、フレームバッファの値も利用して、

          1          1      n-1        1
an = (1 - - ) an-1 + - bn = --- an-1 + - bn
          n          n       n         n

の合成をします。この結果は、

     n-1        1      n-1  n-2        1          1      1      1  n 
an = --- an-1 + - bn = --- (--- an-2+ --- bn-1) + - bn = - a1 + -  Σ bi
      n         n       n   n-1       n-1         n      n      n i=2

になるので、a1に1回目のレンダリング結果を代入するならば、最終的に求めたい答えと同じものが得られます。 ただし、現在が8bitsフォーマットで計算をしているので、1/256以上は0になってしまうので、256枚以上は合成できません。また、後の合成になればなるほど係数の誤差が大きくなるので、注意が必要です。

■テクスチャの作成

さて、テクスチャを作りましょう。
最初にキューブマップの環境マップを作成します。
最初にキューブテクスチャを用意します。

draw.cpp
0251:     // 環境キューブマップの作成
0252:     hr = lpD3DDev->CreateCubeTexture( 512, 1, D3DUSAGE_RENDERTARGET, D3DFMT_A8R8G8B8,
0253:                                 D3DPOOL_DEFAULT , &pCubeMap );
0254:     if ( FAILED(hr) ) return hr;

レンダリングするときは、GetCubeMapSurfaceでレンダリングする面を6面の中から指定してサーフェスを確保します。確保したサーフェスは、SetRenderTarget でレンダリングターゲットに指定してレンダリングします。
このとき、ビュー行列を適当なものを指定する必要があります。 この行列は、ヘルプにも書いてあるので、そのまま使いましょう。

draw.cpp
0408: //-----------------------------------------------------------------------------
0409: // Name: MakeCubeMap()
0410: // Desc: 環境マップを作成する
0411: //-----------------------------------------------------------------------------
0412: VOID MakeCubeMap(LPDIRECT3DDEVICE8 lpD3DDev )
0413: {
0414:     // デバイスのトランスフォーム行列を保存する。
0415:     D3DXMATRIX matProjSave, matViewSave;
0416:     lpD3DDev->GetTransform( D3DTS_VIEW,       &matViewSave );
0417:     lpD3DDev->GetTransform( D3DTS_PROJECTION, &matProjSave );
0418: 
0419:     // 現在のバック バッファと z バッファを格納する。
0420:     LPDIRECT3DSURFACE8 pBackBuffer, pZBuffer;
0421:     lpD3DDev->GetRenderTarget( &pBackBuffer );
0422:     lpD3DDev->GetDepthStencilSurface( &pZBuffer );
0423: 
0424:     // 90°の視野を射影に使用する。
0425:     D3DXMATRIX matProj;
0426:     D3DXMatrixPerspectiveFovLH( &matProj, D3DX_PI/2, 1.0f, 0.5f, 1000.0f );
0427:     lpD3DDev->SetTransform( D3DTS_PROJECTION, &matProj );
0428: 
0429:     // キューブ マップの 6 つのサーフェスに対してループ処理を実行する。
0430:     for( DWORD i=0 ; i<6 ; i++ ) {
0431:         D3DXVECTOR3 vEnvEyePt = D3DXVECTOR3( 0.0f, 10.0f, 0.0f );
0432:         D3DXVECTOR3 vLookatPt, vUpVec;
0433: 
0434:         switch( i ){
0435:         case D3DCUBEMAP_FACE_POSITIVE_X:
0436:             vLookatPt = D3DXVECTOR3( 1.0f, 0.0f, 0.0f )+vEnvEyePt;
0437:             vUpVec    = D3DXVECTOR3( 0.0f, 1.0f, 0.0f );
0438:             break;
0439:         case D3DCUBEMAP_FACE_NEGATIVE_X:
0440:             vLookatPt = D3DXVECTOR3(-1.0f, 0.0f, 0.0f )+vEnvEyePt;
0441:             vUpVec    = D3DXVECTOR3( 0.0f, 1.0f, 0.0f );
0442:             break;
0443:         case D3DCUBEMAP_FACE_POSITIVE_Y:
0444:             vLookatPt = D3DXVECTOR3( 0.0f, 1.0f, 0.0f )+vEnvEyePt;
0445:             vUpVec    = D3DXVECTOR3( 0.0f, 0.0f,-1.0f );
0446:             break;
0447:         case D3DCUBEMAP_FACE_NEGATIVE_Y:
0448:             vLookatPt = D3DXVECTOR3( 0.0f,-1.0f, 0.0f )+vEnvEyePt;
0449:             vUpVec    = D3DXVECTOR3( 0.0f, 0.0f, 1.0f );
0450:             break;
0451:         case D3DCUBEMAP_FACE_POSITIVE_Z:
0452:             vLookatPt = D3DXVECTOR3( 0.0f, 0.0f, 1.0f )+vEnvEyePt;
0453:             vUpVec    = D3DXVECTOR3( 0.0f, 1.0f, 0.0f );
0454:             break;
0455:         case D3DCUBEMAP_FACE_NEGATIVE_Z:
0456:             vLookatPt = D3DXVECTOR3( 0.0f, 0.0f,-1.0f )+vEnvEyePt;
0457:             vUpVec    = D3DXVECTOR3( 0.0f, 1.0f, 0.0f );
0458:             break;
0459:         }
0460: 
0461:         D3DXMATRIX matView;
0462:         D3DXMatrixLookAtLH( &matView, &vEnvEyePt, &vLookatPt, &vUpVec );
0463:         lpD3DDev->SetTransform( D3DTS_VIEW, &matView );
0464: 
0465:         //レンダリングするサーフェスへのポインタを取得する。
0466:         LPDIRECT3DSURFACE8 pFace;
0467:         pCubeMap->GetCubeMapSurface( (D3DCUBEMAP_FACES)i, 0, &pFace );
0468:         lpD3DDev->SetRenderTarget ( pFace , pZBuffer );
0469:         pFace->Release();
0470: 
0471:         lpD3DDev->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, 0x000000, 1.0f, 0L);
0472:         lpD3DDev->BeginScene();
0473:         // ここで、シーンをレンダリングする。
0474:         DrawBg(lpD3DDev);
0475:         lpD3DDev->EndScene();
0476:     }
0477: 
0478:     // レンダリング ターゲットをメイン バック バッファに戻す。
0479:     lpD3DDev->SetRenderTarget( pBackBuffer, pZBuffer );
0480:     pBackBuffer->Release();
0481:     pZBuffer->Release();
0482: 
0483:     // 元のトランスフォーム行列をリストアする。
0484:     lpD3DDev->SetTransform( D3DTS_VIEW,       &matViewSave );
0485:     lpD3DDev->SetTransform( D3DTS_PROJECTION, &matProjSave );
0486: }

その後に、ディフューズマップを作ります。同じようにキューブマップを確保します。

draw.cpp
0255:     // ディフューズマップの作成
0256:     hr = lpD3DDev->CreateCubeTexture( 512, 1, D3DUSAGE_RENDERTARGET, D3DFMT_A8R8G8B8,
0257:                                 D3DPOOL_DEFAULT , &pDiffuseMap );
0258:     if ( FAILED(hr) ) return hr;

いよいよ拡散光マップの作成です。
環境マップと同じようにレンダリングの環境を整えたます。
違うのは、レンダリングするポリゴンです。
レンダリングするポリゴンは、画面全面に張る分割したポリゴンです。
同時に、頂点シェーダの定数レジスタ32~35に規格化した光源ベクトルを代入します。
具体的には、次の方法で4つの光源ベクトルを作ります。
中身は、単位球の中に入るベクトルが得られるまで、何度もベクトルを作る方法です。

0585:         // 等方的サンプリング (Thanks Bee さん)
0586:         D3DXVECTOR4 l[4];
0587:         for(int k=0;k<4;k++){
0588:             float d;
0589:             l[k].w = 0;
0590:             do{
0591:                 l[k].x = 2.0f*frand()-1.0f; 
0592:                 l[k].y = 2.0f*frand()-1.0f; 
0593:                 l[k].z = 2.0f*frand()-1.0f;
0594:                 d = D3DXVec4LengthSq(&l[k]);
0595:             }while(1.0f<d);
0596:         }
0597:         D3DXVec4Normalize(&l[0], &l[0]);
0598:         D3DXVec4Normalize(&l[1], &l[1]);
0599:         D3DXVec4Normalize(&l[2], &l[2]);
0600:         D3DXVec4Normalize(&l[3], &l[3]);

また、ピクセルシェーダの定数レジスタに1/nの定数を入力します。

draw.cpp
0487: //-----------------------------------------------------------------------------
0488: // Name: MakeDiffuseMap()
0489: // Desc: 拡散マップを作成する
0490: //-----------------------------------------------------------------------------
0491: VOID MakeDiffuseMap(LPDIRECT3DDEVICE8 lpD3DDev
0492:                     , LPDIRECT3DCUBETEXTURE8 pDiffuseMap
0493:                     , LPDIRECT3DCUBETEXTURE8 pEnvMap )
0494: {
0495:      static int n = 0;
0496:     
0497:     if(256<=n)return;
0498:     n++;
0499: 
0500:     lpD3DDev->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_DIFFUSE );
0501:     lpD3DDev->SetTextureStageState(0, D3DTSS_COLOROP,   D3DTOP_SELECTARG1);
0502:     lpD3DDev->SetTextureStageState(1, D3DTSS_COLORARG1, D3DTA_TEXTURE );
0503:     lpD3DDev->SetTextureStageState(1, D3DTSS_COLOROP,   D3DTOP_SELECTARG1);
0504:     lpD3DDev->SetTextureStageState(2, D3DTSS_COLORARG1, D3DTA_TEXTURE );
0505:     lpD3DDev->SetTextureStageState(2, D3DTSS_COLOROP,   D3DTOP_SELECTARG1);
0506:     lpD3DDev->SetTextureStageState(3, D3DTSS_COLORARG1, D3DTA_TEXTURE );
0507:     lpD3DDev->SetTextureStageState(3, D3DTSS_COLOROP,   D3DTOP_SELECTARG1);
0508: 
0509:     lpD3DDev->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE);
0510:     lpD3DDev->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
0511:     lpD3DDev->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
0512:     lpD3DDev->SetVertexShader( hDiffuseVertexShader );
0513:     lpD3DDev->SetPixelShader ( hDiffusePixelShader );
0514:     lpD3DDev->SetTexture( 0, pEnvMap );
0515:     lpD3DDev->SetTexture( 1, pEnvMap );
0516:     lpD3DDev->SetTexture( 2, pEnvMap );
0517:     lpD3DDev->SetTexture( 3, pEnvMap );
0518: 
0519:     // デバイスのトランスフォーム行列を保存する。
0520:     D3DXMATRIX matProjSave, matViewSave;
0521:     lpD3DDev->GetTransform( D3DTS_VIEW,       &matViewSave );
0522:     lpD3DDev->GetTransform( D3DTS_PROJECTION, &matProjSave );
0523: 
0524:     // 現在のバック バッファと z バッファを格納する。
0525:     LPDIRECT3DSURFACE8 pBackBuffer, pZBuffer;
0526:     lpD3DDev->GetRenderTarget( &pBackBuffer );
0527:     lpD3DDev->GetDepthStencilSurface( &pZBuffer );
0528: 
0529:     // 90°の視野を射影に使用する。
0530:     D3DXMATRIX matProj;
0531:     D3DXMatrixPerspectiveFovLH( &matProj, D3DX_PI/2, 1.0f, 0.5f, 1000.0f );
0532:     lpD3DDev->SetTransform( D3DTS_PROJECTION, &matProj );
0533: 
0534:     // キューブ マップの 6 つのサーフェスに対してループ処理を実行する。
0535:     for( DWORD i=0 ; i<6 ; i++ ) {
0536:         D3DXVECTOR3 vEnvEyePt = D3DXVECTOR3( 0.0f, 10.0f, 0.0f );
0537:         D3DXVECTOR3 vLookatPt, vUpVec;
0538: 
0539:         switch( i ){
0540:         case D3DCUBEMAP_FACE_POSITIVE_X:
0541:             vLookatPt = D3DXVECTOR3( 1.0f, 0.0f, 0.0f )+vEnvEyePt;
0542:             vUpVec    = D3DXVECTOR3( 0.0f, 1.0f, 0.0f );
0543:             break;
0544:         case D3DCUBEMAP_FACE_NEGATIVE_X:
0545:             vLookatPt = D3DXVECTOR3(-1.0f, 0.0f, 0.0f )+vEnvEyePt;
0546:             vUpVec    = D3DXVECTOR3( 0.0f, 1.0f, 0.0f );
0547:             break;
0548:         case D3DCUBEMAP_FACE_POSITIVE_Y:
0549:             vLookatPt = D3DXVECTOR3( 0.0f, 1.0f, 0.0f )+vEnvEyePt;
0550:             vUpVec    = D3DXVECTOR3( 0.0f, 0.0f,-1.0f );
0551:             break;
0552:         case D3DCUBEMAP_FACE_NEGATIVE_Y:
0553:             vLookatPt = D3DXVECTOR3( 0.0f,-1.0f, 0.0f )+vEnvEyePt;
0554:             vUpVec    = D3DXVECTOR3( 0.0f, 0.0f, 1.0f );
0555:             break;
0556:         case D3DCUBEMAP_FACE_POSITIVE_Z:
0557:             vLookatPt = D3DXVECTOR3( 0.0f, 0.0f, 1.0f )+vEnvEyePt;
0558:             vUpVec    = D3DXVECTOR3( 0.0f, 1.0f, 0.0f );
0559:             break;
0560:         case D3DCUBEMAP_FACE_NEGATIVE_Z:
0561:             vLookatPt = D3DXVECTOR3( 0.0f, 0.0f,-1.0f )+vEnvEyePt;
0562:             vUpVec    = D3DXVECTOR3( 0.0f, 1.0f, 0.0f );
0563:             break;
0564:         }
0565: 
0566:         D3DXMATRIX matView;
0567:         D3DXMatrixLookAtLH( &matView, &vEnvEyePt, &vLookatPt, &vUpVec );
0568:         lpD3DDev->SetTransform( D3DTS_VIEW, &matView );
0569: 
0570:         D3DXMATRIX m = matView * matProj;
0571:         D3DXMatrixInverse( &m,  NULL, &m);
0572:         lpD3DDev->SetVertexShaderConstant( 8, D3DXMatrixTranspose(&m,&m), 4);
0573: 
0574:         //レンダリングするサーフェスへのポインタを取得する。
0575:         LPDIRECT3DSURFACE8 pFace;
0576:         pDiffuseMap->GetCubeMapSurface( (D3DCUBEMAP_FACES)i, 0, &pFace );
0577:         lpD3DDev->SetRenderTarget ( pFace , pZBuffer );
0578:         pFace->Release();
0579: 
0580:         if(1==n){
0581:             lpD3DDev->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, 0x000000, 1.0f, 0L);
0582:         }
0583:         lpD3DDev->BeginScene();
0584:         
0585:         // 等方的サンプリング (Thanks Bee さん)
0586:         D3DXVECTOR4 l[4];
0587:         for(int k=0;k<4;k++){
0588:             float d;
0589:             l[k].w = 0;
0590:             do{
0591:                 l[k].x = 2.0f*frand()-1.0f; 
0592:                 l[k].y = 2.0f*frand()-1.0f; 
0593:                 l[k].z = 2.0f*frand()-1.0f;
0594:                 d = D3DXVec4LengthSq(&l[k]);
0595:             }while(1.0f<d);
0596:         }
0597:         D3DXVec4Normalize(&l[0], &l[0]);
0598:         D3DXVec4Normalize(&l[1], &l[1]);
0599:         D3DXVec4Normalize(&l[2], &l[2]);
0600:         D3DXVec4Normalize(&l[3], &l[3]);
0601:         lpD3DDev->SetVertexShaderConstant(32, l, 4);
0602:         lpD3DDev->SetPixelShaderConstant(1, &D3DXVECTOR4(0,0,0,1.0f/n), 1);
0603: 
0604:         lpD3DDev->SetStreamSource( 0, pBlurVB, sizeof(D3D_BLUR_VERTEX) );
0605:         lpD3DDev->SetIndices(pBlurIB,0);
0606:         lpD3DDev->DrawIndexedPrimitive( D3DPT_TRIANGLELIST, 0,
0607:                             (BLUR_GRID+1)*(BLUR_GRID+1), 0, 2*BLUR_GRID*BLUR_GRID );
0608: 
0609:         lpD3DDev->EndScene();
0610:     }
0611:     lpD3DDev->SetTexture( 0, NULL );
0612:     lpD3DDev->SetTexture( 1, NULL );
0613:     lpD3DDev->SetTexture( 2, NULL );
0614:     lpD3DDev->SetTexture( 3, NULL );
0615: 
0616:     // レンダリング ターゲットをメイン バック バッファに戻す。
0617:     lpD3DDev->SetRenderTarget( pBackBuffer, pZBuffer );
0618:     pBackBuffer->Release();
0619:     pZBuffer->Release();
0620: 
0621:     // 元のトランスフォーム行列をリストアする。
0622:     lpD3DDev->SetTransform( D3DTS_VIEW,       &matViewSave );
0623:     lpD3DDev->SetTransform( D3DTS_PROJECTION, &matProjSave );
0624:     lpD3DDev->SetRenderState(D3DRS_ALPHABLENDENABLE, FALSE);
0625: }

さて、作成するシェーダプログラムです。
座標は、入力された座標をそのまま出力します。
法線ベクトルは、射影空間の位置座標をワールド座標に直して、規格化することによって算出します。 後は、内積を計算して、Lambert 余弦則の係数cosθを求めつつ、その値の正負によって、 光源に対して表と裏のどちらのベクトルを使うのか選択します。

cosθは、絶対値をmax命令で求めておいて、色の出力として出力します。

0001: ; diffuse.vsh
0003: ; c8-11  -- 逆(ビュー + 透視変換)行列
0004: ; c12    -- {0.0, 0.5, 1.0, 2.0}
****: ; c32-35 -- 規格化された光源ベクトル
0005: ;
0006: ; v0    頂点の座標値
0008: 
0009: vs.1.1
0010: 
0011: ;座標変換
0012: mov oPos,  v0
0013: 
0014: m4x4 r3, v0, c8
0015: // 正規化
0016: dp3 r3.w, r3, r3
0017: rsq r3.w, r3.w
0018: mul r3, r3, r3.w
0019: 
0020: // oT0 = (0<=(N・L1))?L1:-L1
0021: dp3 r0,   r3, c32           ; (N・L1)
0022: sge   r1,  r0,  c12.x
0023: add   r2,  c32, c32
0024: mad  oT0,  r1,  r2, -c32
0025: 
0026: // oT1 = (0<=(N・L2))?L2:-L2
0027: dp3 r0.a,  r3, c33          ; (N・L2)
0028: sge   r1,  r0.a,  c12.x
0029: add   r2,  c33, c33
0030: mad  oT1,  r1,  r2, -c33
0031: 
0032: // (|N・L1|, |N・L1|, |N・L1|, |N・L2|)
0033: max oD0, r0, -r0
0034: 
0035: 
0036: // oT2 = (0<=(N・L3))?L3:-L3
0037: dp3 r0,   r3, c34           ; (N・L3)
0038: sge   r1,  r0,  c12.x
0039: add   r2,  c34, c34
0040: mad  oT2,  r1,  r2, -c34
0041: 
0042: // oT3 = (0<=(N・L4))?L4:-L4
0043: dp3 r0.a,  r3, c35          ; (N・L4)
0044: sge   r1,  r0.a,  c12.x
0045: add   r2,  c35, c35
0046: mad  oT3,  r1,  r2, -c35
0047: 
0048: // (|N・L3|, |N・L3|, |N・L3|, |N・L4|)
0049: max oD1, r0, -r0

ピクセルシェーダプログラムでは、環境マップから色をサンプリングしておいて、 それらをcosθの係数で掛けて、平均を取って出力します。
ただし、実際に平均をとってみたら、結果が暗かったので、 2倍の明るさに強めて平均をとりました。
わざとらしくコメントアウトしてある部分のコメントをはずして、下の2行をコメントアウトすると、平均化した結果が得られます。 この辺は、テクスチャにもよると思いますので、適当に係数付けてください。

α成分には、合成の強さ1/nを入れます。

0001: ; diffuse.psh
0002: 
0003: ps.1.1
0004: 
0005: def c0, 0.5f, 0.5f, 0.5f, 0.5f
0006: 
0007: tex t0
0008: tex t1
0009: tex t2
0010: tex t3
0011: 
0012: mul t0, t0, v0          ; (N・L1)*tex(L1)
0013: mul t1, t1, v0.a        ; (N・L2)*tex(L2)
0014: mul t2, t2, v1          ; (N・L3)*tex(L3)
0015: mul t3, t3, v1.a        ; (N・L4)*tex(L4)
0016: 
0017: ;lrp t0, c0, t0, t1     
0018: ;lrp t2, c0, t2, t3     
0019: 
0020: add t0, t0, t1
0021: add t2, t2, t3
0022: 
0023: lrp r0.rgb, c0, t0, t2  ; r0.rgb = ((N・L1)*tex(L1) + (N・L2)*tex(L2)
0024:                         ;          +(N・L3)*tex(L3) + (N・L4)*tex(L4))/4
0025: +mov r0.a, c1.a         ; r0.a = c1.a = 1/n
0026: 

■マッピング

実際の環境マップを張るのは非常に簡単です。
頂点シェーダでは、テクスチャ座標にワールド座標の位置座標を出力します。

vs.vsh
0001: ; c0-3   -- world + ビュー + 透視変換行列
0002: ; c4-7   -- world
****: ; c14    -- メッシュの色
0004: ;
0005: ; v0    頂点の座標値
0006: ; v3    法線ベクトル (w成分は1.0f)
0007: 
0008: vs.1.0
0009: 
0010: ;座標変換
0011: m4x4 oPos,  v0,   c0
0035: 
0036: mov oD0, c14
0037: 
0038: ; ワールド座標での法線ベクトル
0039: m3x3 oT0,  v3,   c4

ピクセルシェーダでは、重み付けしてから、メッシュの色に積算して拡散光成分を求めます。

vs.vsh
0001: ; ps.psh
0002: 
0003: ps.1.1
0004: 
0020: def c0, 1.00f, 1.00f, 1.00f, 1.0f   ; 拡散反射係数
0021: def c1, 0.04f, 0.04f, 0.04f, 1.0f   ; 環境反射係数
0022: 
0023: tex t0  ; diffuse マップ
0024: 
0025: mul t0, t0, c0
0026: add t0, t0, c1
0027: mul r0, t0, v0

■鏡面反射光マップ

では、次は、鏡面反射光です。
画面は jpeg で圧縮してるので汚いですね。
環境マップのときに気が付いているかもしれませんが、環境マップは遠くの景色がはっきりと移りこみます。
ですが、実際には、移り込みは鏡面反射の結果であるので、 表面のざらつき具合に応じてぼやけるはずです。
これをマップとして表現したものが鏡面反射光マップです。 鏡面反射光マップは環境マップの進化系といってよいでしょう。

specular.vsh鏡面反射光マップ作成の頂点シェーダー。
specular.psh鏡面反射光マップ作成のピクセルシェーダー。
vs.vsh深度描画の頂点シェーダー。
ps.psh深度描画のピクセルシェーダー。
draw.cppメインの描画部分。
draw.h描画の各関数の定義。
bg.cpp背景の描画。
main.h基本的な定数など。
main.cpp描画に関係しないシステム的な部分。
load.cppロード。
load.hロードのインターフェイス。
tile.bmp (床デカール)
sky.bmp (空デカール)

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

■やってること

鏡面反射光マップが拡散光マップと違うところは、 環境マップをサンプリングするときの法則がLambertではなくて、Phong のシェーディングであることです。
拡散光マップよりも、局所的に色をサンプリングします。

頂点シェーダのプログラムの出力する係数を8乗して出力します。

0001: ; specular.vsh
0002: ; c0-3   -- world + ビュー + 透視変換行列
0003: ; c8-11  -- 逆(ビュー + 透視変換)行列
0004: ; c12    -- {0.0, 0.5, 1.0, 2.0}
0005: ;
0006: ; v0    頂点の座標値
0007: ; v3    法線ベクトル (w成分は1.0f)
0008: 
0009: vs.1.1
0010: 
0011: ;座標変換
0012: mov oPos,  v0
0013: 
0014: m4x4 r3, v0, c8
0015: // 正規化
0016: dp3 r3.w, r3, r3
0017: rsq r3.w, r3.w
0018: mul r3, r3, r3.w
0019: 
0020: // oT0 = (0<=(N・L1))?L1:-L1
0021: dp3 r0,   r3, c32           ; (N・L1)
0022: sge   r1,  r0,  c12.x
0023: add   r2,  c32, c32
0024: mad  oT0,  r1,  r2, -c32
0025: 
0026: // oT1 = (0<=(N・L2))?L2:-L2
0027: dp3 r0.a,  r3, c33          ; (N・L2)
0028: sge   r1,  r0.a,  c12.x
0029: add   r2,  c33, c33
0030: mad  oT1,  r1,  r2, -c33
0031: 
0032: // (|N・L1|^8, |N・L1|^8, |N・L1|^8, |N・L2|^8)
0033: max r0, r0, -r0
0034: mul r0, r0, r0
0035: mul r0, r0, r0
0036: mul oD0, r0, r0
0037: 
0038: // oT2 = (0<=(N・L3))?L3:-L3
0039: dp3 r0,   r3, c34           ; (N・L3)
0040: sge   r1,  r0,  c12.x
0041: add   r2,  c34, c34
0042: mad  oT2,  r1,  r2, -c34
0043: 
0044: // oT3 = (0<=(N・L4))?L4:-L4
0045: dp3 r0.a,  r3, c35          ; (N・L4)
0046: sge   r1,  r0.a,  c12.x
0047: add   r2,  c35, c35
0048: mad  oT3,  r1,  r2, -c35
0049: 
0050: // (|N・L3|^8, |N・L3|^8, |N・L3|^8, |N・L4|^8)
0051: max r0, r0, -r0
0052: mul r0, r0, r0
0053: mul r0, r0, r0
0054: mul oD1, r0, r0

また、実際に計算してみたら、色が弱かったので、ピクセルシェーダを加算合成することによって、色を強めて出力しました。

0001: ; specular.psh
0002: 
0003: ps.1.1
0004: 
0005: def c0, 0.5f, 0.5f, 0.5f, 0.5f
0006: 
0007: tex t0
0008: tex t1
0009: tex t2
0010: tex t3
0011: 
0012: mul t0, t0, v0          ; (N・L1)^8*tex(L1)
0013: mul t1, t1, v0.a        ; (N・L2)^8*tex(L2)
0014: mul t2, t2, v1          ; (N・L3)^8*tex(L3)
0015: mul t3, t3, v1.a        ; (N・L4)^8*tex(L4)
0016: 
0017: add t0, t0, t1
0018: add t2, t2, t3
0019: 
0020: add r0.rgb, t0, t2      ; r0.rgb = ((N・L1)^8*tex(L1) + (N・L2)^8*tex(L2)
0021:                         ;          +(N・L3)^8*tex(L3) + (N・L4)^8*tex(L4))/4
0022: +mov r0.a, c1.a         ; r0.a = c1.a = 1/n

後は、レンダリング時のマッピングは環境マップと同じように、反射ベクトルでマッピングします。

0001: ; c0-3   -- world + ビュー + 透視変換行列
0002: ; c4-7   -- world
0003: ; c12    -- {0.0, 0.5, 1.0, 2.0}
0004: ;
0005: ; v0    頂点の座標値
0006: ; v3    法線ベクトル (w成分は1.0f)
0007: 
0008: vs.1.0
0009: 
0010: ;座標変換
0011: m4x4 oPos,  v0,   c0
0012: 
0013: ; Lambert 拡散
0014: dp4 r0, v3, c13
0015: max r0, r0, c13.w
0016: mul oD0, r0, c14
0017: 
0018: ; ワールド座標での法線ベクトル
0019: m3x3 r1,  v3,   c4
0020: mov oT1, r1
0021: 
0022: ; 視線ベクトル
0023: m4x4 r0,  v0,   c4
0024: add r0, c15, -r0
0025: mov oT2, r0
0026: 
0027: dp3 r0.w, r0, r0    ; 正規化
0028: rsq r0.w, r0.w
0029: mul r0, r0, r0.w
0030: 
0031: dp3 r0.w, r0, r1
0032: mul r0.w, r0.w, c12.w
0033: mad oT0.xyz, r0.w, r1, -r0  ; r=2(N・E)N-E

ピクセルシェーダでは、フレネル項を追加しました。

0001: ; ps.psh
0002: 
0003: ps.1.1
0004: 
0005: def c0, 1.00f, 1.00f, 1.00f, 1.0f   ; フレネル スケール
0006: def c1, 0.30f, 0.30f, 0.30f, 1.0f   ; フレネル バイアス
0007: 
0008: tex t0  ; 環境マップ
0009: tex t1  ; normal
0010: tex t2  ; eye
0011: 
0012: dp3_sat r0, t1_bx2, t2_bx2
0013: mul r0, 1-r0, 1-r0
0014: mul r0, r0, r0      ; (1-N・E)^4
0015: mul r0, r0, c0
0016: add r0, r0, c1      ; r = s*(1-N・E)^4 + b
0017: 
0018: lrp r0, r0, t0, v0  ; 線形補間で環境マップを張り詰ける

■最後に

今回は、拡散光のマップに関しては、テクスチャを一部だけがオレンジの夕闇のようなテクスチャにして見ました。
青空にしてしまうと、ほとんど明るくなってしまうので、陰になる部分がわからなくなってしまいます。
ということで、閉鎖空間で一部が明るくないと、効果的ではありませんね。

鏡面反射光の場合には、光の鋭さが見られません。
もっと冪をあげればよいのでしょうけれど、それならば、 ピクセルシェーダで計算すべきでしょう。 といっても、命令数が残ってないので、DirectX9を待たなければなりませんが。
あと、立体角単位の確率分布で考えていないので、キューブマップの各面の隅の部分が 光が強くなってしまっていますね。これは修正すべきですね。修正済み

今回は、最初のフレームでテクスチャを作成していますが、 待たせたくないようならば、各フレームに計算を分離することができます。 30フレーム/秒の画面でも、256回ならば8秒程度で完成しますし、 数フレーム計算しただけで、それなりの結果が出ると思うので、 半リアルタイムに作成できると思います。対応済み





もどる

imagire@gmail.com