ディスプレースメントマップ


~ Displacement mapping ~






■はじめに

さて、皆さんが一番期待していて、実装が一番期待できないのがディスプレースメントマップです。
NVIDIA はやる気ありませんし(あくまでもDirectX9の方法に関してですが)、ATIも実装されていません (今回のボードはRADEON9700proですが、タイトル画像が50fps なのは、タイミング的に表示されてしまったもので、こんなには出ません)。
唯一まともに動くのが Matrox の Parhelia だけですが、持っている人もほとんどいないでしょう。

かわいそうだから、やってあげましょう。
ハードウェア的にサポートさてていないグラフィックボードをお使いの方は、リファレンスラスタライザで実行してください。

まぁ、いつものように適当にファイルが入っています。
ほとんどが APP WIZARD から出力されるファイルで、もう飽きたのでその辺のファイルはもう無視します。

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

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

■ディスプレースメントマップとは

ディスプレースメントマップとは、バンプマップのすんごいのです。
といっても、バンプマップを知らない方には何も意味をなさないので、もう少しまじめに説明すると、 テクスチャの色に応じて、モデルをでこぼこさせる方法のことです。
例えば、下のような、中心が白で、周りが黒の板にディスプレースメントマップを施すと、 色の強い真ん中の部分が盛り上がります。

使用前 使用後

今の場合には、(16x16の四角形に分割した)板にディスプレースメントマップを施した場合ですが、 一般のモデルに関しては、法線ベクトルの方向に頂点を施します。
例えば、いつものトラ君が

太ったり

やせたりします。

■方法

では、ディスプレースメントマップはどのようにして実現されているのでしょうか。
それを見るには、頂点シェーダを見るのが早いと思います。
頂点シェーダプログラムは、次のようになっています。

hlsl.fx
0007: // -------------------------------------------------------------
0008: // 頂点シェーダプログラム
0009: //  c0 - c3 - 座標変換
0010: //  c4.x    - ディスプレースする量
0011: //  c12     - (0.0, 0.5, 1.0, 2.0)
0012: //  c13     - 光源の方向
0013: // -------------------------------------------------------------
0014: VertexShader VS = asm
0015: {
0016:     vs_1_1
0017:     
0018:     dcl_position0 v0
0019:     dcl_normal0   v1
0020:     dcl_texcoord0 v2
0021:     dcl_sample0   v3
0022:     
0023:     mul r0, v1, c4.x    // 法線にディスプレースの大きさを掛ける
0024:     mul r0, r0, v3.x    // ★さらにマップの値を掛ける
0025:     add r0, v0, r0      // 変位を頂点に足す
0026:     mov r0.w, v0.w      // w成分は1
0027:     m4x4 oPos, r0, c0   // 座標変換
0028:     
0029:     dp3 r0, v1, c13     // ランバート拡散
0030:     max r0, r0, c12.x
0031:     add oD0, r0, c13.w
0032:     
0033:     mov oT0, v2         // テクスチャ座標
0034: };

今回は、HLSL ではありません。これは、肝心かなめの、 ディスプレースメントマップのテクスチャを頂点シェーダで読むための定数が、定義されていないからです。
先頭の頂点宣言を見ると、見慣れない dcl_sample0 があるが気づくと思います (そもそも頂点宣言の説明をしていませんでしたね。DirectX9 では、頂点シェーダの最初の部分に、 使用するレジスタの宣言をします)。
これが、ディスプレースメントマップのテクスチャの輝度値になります。
ここに頂点のUV値でサンプリングされたテクスチャの値が0から1の範囲で入ってきます
(UV値は頂点データとして与えられます)。
あとは、適当な定数と法線ベクトルを掛けて位置座標に足しこんでやればディスプレースメントの完了です。
光源計算やデカールのテクスチャを張る部分はいつもと同じです。

気づいた方も折られると思いますが、変位は別に法線方向だけとは限りません。
接ベクトルや別の定数ベクトルなどを用いればさまざまなバリエーションが可能です。
あまり考えられませんが、フォグの変位もディスプレースメントマップでできるのではないでしょうか (それ以外はピクセルシェーダでやればいいですね)。

■オブジェクト

さて、ディスプレースメントマップを実装する手順を追ってみましょう。
ディスプレースメントマップには、いつものメッシュの取り扱いのほかにも、 ディスプレースメントマップのテクスチャオブジェクトが必要になります。
さらに、DirectX9で導入された頂点宣言のオブジェクト LPDIRECT3DVERTEXDECLARATION9 もきちんと設定しなくてはなりません。

main.h
0038: class CMyD3DApplication : public CD3DApplication
0039: {
0040:     BOOL                            m_bDMap;    // ディスプレースメントマップをサポートしているか
0041:     FLOAT                           m_degree;   // 変化させる量
0042:     CD3DMesh                        *m_pMesh;   // 表示するメッシュ
0043:     LPDIRECT3DVERTEXDECLARATION9    m_pDecl;    // 頂点宣言
0044:     LPDIRECT3DTEXTURE9              m_pDispMap; // ディスプレースメントマップのオブジェクト
0045:     LPD3DXEFFECT                    m_pEffect;  // シェーダが書かれたエフェクト
0046: 

■能力チェック

さて、必ず必要なことに、グラフィックカードの能力を調べなくてはなりません。
ところが、IDirect3DDevice9 オブジェクトを生成した後にしか調べられない能力もあるので、 まず、シェーダが使えるかを先にしておきましょう。

main.cpp
0147: HRESULT CMyD3DApplication::ConfirmDevice( D3DCAPS9* pCaps, DWORD dwBehavior,
0148:                                           D3DFORMAT Format )
0149: {
0150:     UNREFERENCED_PARAMETER( Format );
0151:     UNREFERENCED_PARAMETER( dwBehavior );
0152:     UNREFERENCED_PARAMETER( pCaps );
0153:     
0154:     BOOL bCapsAcceptable;
0155: 
0156:     // TODO: Perform checks to see if these display caps are acceptable.
0157:     bCapsAcceptable = TRUE;
0158:     
0159:     // シェーダのチェック
0160:     bCapsAcceptable &= (D3DPS_VERSION(1,1) <= pCaps->PixelShaderVersion);
0161:     bCapsAcceptable &= (D3DVS_VERSION(1,1) <= pCaps->VertexShaderVersion);
0162:     
0163:     if( bCapsAcceptable )         
0164:         return S_OK;
0165:     else
0166:         return E_FAIL;
0167: }

生成したら、頂点計算をハードウェアで行えるか、 また、m_d3dCaps.DevCaps2 を見て、N-パッチがサポートされているかどうか調べます。
N-パッチとはポリゴンを分割する方法で、3角形を相似な4つのテクスチャに分割し、分割されて新たにできた頂点の位置は法線ベクトルの方向に内挿します。 実際には、これはN-パッチとの1回(セグメント)の分割で、できた3角形に同様の分割を行うことによって、再帰的にポリゴンを分割することができます。
ディスプレースメントマップは、N-パッチの拡張として実装されているようなので、とりあえず、N-パッチが使えなければ話になりません。
個人的には、頂点座標を動かせればよくて頂点分割はいらないので、N-パッチを使わない方法にしてほしいと思います。
さらに、ディスプレースメントマップのテクスチャがサポートされているか調べます。
これら一連のテストをクリアしたら、ディスプレースメントマップを使うことができます。

main.cpp
0180: HRESULT CMyD3DApplication::InitDeviceObjects()
0181: {
中略
0193:     
0194:     // ディスプレースメントマップのできる条件
0195:     D3DFORMAT adapterFormat = m_d3dSettings.DisplayMode().Format;
0196:     m_bDMap = ( (m_pd3dDevice->GetSoftwareVertexProcessing() == FALSE) && 
0197:                 (m_d3dCaps.DevCaps2 & D3DDEVCAPS2_DMAPNPATCH ) &&
0198:                  SUCCEEDED( m_pD3D->CheckDeviceFormat( m_d3dCaps.AdapterOrdinal,
0199:                                                        m_d3dCaps.DeviceType,
0200:                                                        adapterFormat,
0201:                                                        D3DUSAGE_DMAP,
0202:                                                        D3DRTYPE_TEXTURE,
0203:                                                        D3DFMT_L8 ) ) );
0204: 

■初期化

それぞれのオブジェクトを初期化していきましょう。
まず、シェーダはいつもと同じように読み込みます。

main.cpp
0205:     // シェーダの読み込み
0206:     if( FAILED( D3DXCreateEffectFromFile( m_pd3dDevice, "hlsl.fx", NULL, NULL, 
0207:                                         0, NULL, &m_pEffect, NULL ) ) ) return E_FAIL;

他には、頂点宣言のオブジェクトを生成します。
頂点宣言のオブジェクトは、レンダラに流し込むデータの構造を設定します。 高度な FVF (柔軟なファイルフォーマット)と思ってください。
今回のように特別な頂点宣言を使うならば、必ず必要です。 ほんとは、いつもしたほうがいいです。

さて、頂点宣言のデータの4つめのデータがディスプレースメントマップのデータです。
頂点宣言のデータの2つめの引数が、データの構造体の先頭からのオフセットですが、3番目と4番目のデータは同じオフセット、つまり、同じデータを使います。
言い換えれば、今回は、デカールのテクスチャとディスプレースメントマップのUV値に同じテクスチャ座標を使います。
一般に、デカールのテクスチャとディスプレースメントマップは違うテクスチャが使えます。もちろん、それぞれのテクスチャ座標に違うものを使うこともできます。
ディスプレースメントマップの頂点宣言のデータの3つめの引数は、D3DDECLMETHOD_LOOKUP を指定します。 これが、テクスチャからのサンプリング結果を頂点シェーダの入力レジスタに入れる設定です。
次の、D3DDECLUSAGE_SAMPLE(と、さらに次のインデックスの0) が、シェーダの頂点宣言の dcl_sample0 に結果が入れられることを表現しています。
頂点宣言のデータの5番目の引数の usage の名前を見れば、どのような用途に用いるデータか判断できると思います。

main.cpp
0035: // 頂点宣言
0036: D3DVERTEXELEMENT9 decl[] =
0037: {
0038:     {0,  0, D3DDECLTYPE_FLOAT3,   D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_POSITION, 0},//  位置
0039:     {0, 12, D3DDECLTYPE_FLOAT3,   D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_NORMAL,   0},//  法線
0040:     {0, 24, D3DDECLTYPE_FLOAT2,   D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 0},//  テクスチャ座標
0041:     {0, 24, D3DDECLTYPE_FLOAT2,   D3DDECLMETHOD_LOOKUP,  D3DDECLUSAGE_SAMPLE,   0},//  ディスプレースメントマップ
0042:     D3DDECL_END()
0043: };

0209:     if(m_bDMap){
0210:         // 頂点宣言のオブジェクトの生成
0211:         if( FAILED( hr = m_pd3dDevice->CreateVertexDeclaration( decl, &m_pDecl ) ) )
0212:             return DXTRACE_ERR( "CreateVertexDeclaration", hr );

後は、テクスチャの読み込みです。
今回は、R8B8G8 のテクスチャを読み込んだ後に、D3DFMT_L8 の輝度データへ変換してディスプレースメントマップに使用します。
このフォーマットの変換は、D3DXCreateTextureFromFileEx を使えば自動的に行えます。
このフォーマットでレンダリングターゲットに指定できるのか不明ですが、指定できれば動的なディスプレースメントマップ制御が可能になりますね。

main.cpp
0214:         // ディスプレイスメントマップの読み込み
0215:         if(FAILED(hr = D3DXCreateTextureFromFileEx( m_pd3dDevice, "tiger_disp.bmp"
0216:                                 , D3DX_DEFAULT      //  幅(ファイルから)
0217:                                 , D3DX_DEFAULT      //  高さ(ファイルから)
0218:                                 , D3DX_DEFAULT      // ミップ レベル
0219:                                 , D3DUSAGE_DMAP     // 
0220:                                 , D3DFMT_L8         // 
0221:                                 , D3DPOOL_MANAGED   // テクスチャの配置先となるメモリ クラス
0222:                                 , D3DX_DEFAULT      // フィルタリングする方法
0223:                                 , D3DX_DEFAULT      // フィルタリングする方法
0224:                                 , 0                 // 透明となる D3DCOLOR の値。
0225:                                 , NULL              // ソース イメージ ファイル内のデータの記述
0226:                                 , NULL              // 256 色パレット
0227:                                 , &m_pDispMap
0228:                                 )))
0229:             return DXTRACE_ERR( "Load Displacement Texture", hr );

さて、後は、サンプラーステートでも指定しておきましょう。
サンプラーステートを指定するには、サンプリングの段階に D3DDMAPSAMPLER を指定します。
後は、他のテクスチャのサンプラーステートと同じように設定できます。

main.cpp
0230:         // サンプラーステートの設定
0231:         m_pd3dDevice->SetSamplerState( D3DDMAPSAMPLER, D3DSAMP_ADDRESSU,  D3DTADDRESS_CLAMP );
0232:         m_pd3dDevice->SetSamplerState( D3DDMAPSAMPLER, D3DSAMP_ADDRESSV,  D3DTADDRESS_CLAMP );
0233:         m_pd3dDevice->SetSamplerState( D3DDMAPSAMPLER, D3DSAMP_MIPFILTER, D3DTEXF_NONE );
0234:         m_pd3dDevice->SetSamplerState( D3DDMAPSAMPLER, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR );
0235:         m_pd3dDevice->SetSamplerState( D3DDMAPSAMPLER, D3DSAMP_MINFILTER, D3DTEXF_LINEAR );
0236:     }

あと、デバイスがリセットされたときに呼ばれる RestoreDeviceObjects で、自分でメッシュを再構成しなくてはなりません。
普段は FVF を用いて CD3DMesh クラスが自動で行ってくれるのですが、メッシュがN-パッチに対応するように作り直さなくてはならないので、 CloneMesh で自分で作り直します。
D3DXMESH_NPATCHES を忘れないようにしてくださいね。

main.cpp
0305:     if(m_bDMap){
0306:         //-------------------------------------------------------------------------
0307:         // ディスプレースメントマップのときは、自分でRestoreDeviceObjects
0308:         //-------------------------------------------------------------------------
0309:         if( m_pMesh->m_pSysMemMesh ){
0310:             if( FAILED( m_pMesh->m_pSysMemMesh->CloneMesh( 0L|D3DXMESH_NPATCHES, decl,
0311:                                                 m_pd3dDevice, &m_pMesh->m_pLocalMesh ) ) )
0312:             return E_FAIL;
0313:             D3DXComputeNormals( m_pMesh->m_pLocalMesh, NULL );
0314:         }
0317:     }

■描画

さて、描画ですが、エフェクトファイルで描画する部分は他と同じです。

main.cpp
0398: HRESULT CMyD3DApplication::Render()
0399: {
0400:     // Clear the viewport
0401:     m_pd3dDevice->Clear( 0L, NULL, D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER,
0402:                          0x000000ff, 1.0f, 0L );
0403: 
0404:     // Begin the scene
0405:     if( SUCCEEDED( m_pd3dDevice->BeginScene() ) )
0406:     {
0407:         if(m_bDMap){
0408:             if( m_pEffect != NULL ) 
0409:             {
0410:                 //-------------------------------------------------------------------------
0411:                 // シェーダの設定
0412:                 //-------------------------------------------------------------------------
0413:             
0414:                 D3DXHANDLE hTechnique = m_pEffect->GetTechniqueByName( "TShader" );
0415:                 m_pEffect->SetTechnique( hTechnique );
0416:                 m_pEffect->Begin( NULL, 0 );
0417:                 m_pEffect->Pass( 0 );
描画
0457:                 m_pEffect->End();
0458:             }
0462:         }
0463: 
0467:         // End the scene.
0468:         m_pd3dDevice->EndScene();
0469:     }
0470: 
0471:     return S_OK;
0472: }

肝心な描画の内部です。
頂点シェーダの定数レジスタを設定する部分は今までとほとんど同じです(型情報を示す「F」が今までの定数レジスタの設定に付きました)。
デカールのテクスチャの設定もいつもどおりです。

main.cpp
0419:                 // シェーダ定数の設定
0420:                 // 座標変換
0421:                 D3DXMATRIX m = m_mWorld * m_mView * m_mProj;
0422:                 D3DXMatrixTranspose( &m, &m );
0423:                 m_pd3dDevice->SetVertexShaderConstantF( 0,(float*)&m,4 );
0424:                 // 変位の大きさ
0425:                 D3DXVECTOR4 displacement = D3DXVECTOR4(m_degree,0.0f,0.0f,0.0f);
0426:                 m_pd3dDevice->SetVertexShaderConstantF( 4,(float*)&displacement,1 );
0427:                 // 適当な定数
0428:                 D3DXVECTOR4 consts = D3DXVECTOR4(0.0f,0.5f,1.0f,2.0f);
0429:                 m_pd3dDevice->SetVertexShaderConstantF(12,(float*)&consts,1 );
0430:                 // ライト
0431:                 D3DXVECTOR4 v;
0432:                 D3DXMatrixInverse( &m, NULL, &m_mWorld);
0433:                 D3DXVec4Transform( &v, &m_LighPos, &m );
0434:                 D3DXVec4Normalize( &v, &v );v.w = 0.3f;
0435:                 m_pd3dDevice->SetVertexShaderConstantF(13,(float*)&v,1 );
0436: 
0437:                 // テクスチャの設定
0438:                 m_pd3dDevice->SetTexture(0, m_pMesh->m_pTextures[0]);

いよいよディスプレースメントマップの設定です。
やはり、D3DDMAPSAMPLER の段階のテクスチャにディスプレースメントマップを設定します。
他に、N-パッチのセグメント(詳細レベル)を設定します。ここはどのくらい分割するかどうかなので、見た目で決めましょう。
後は、頂点宣言を設定して描画すればディスプレースメントマップを使った表示ができます。

main.cpp
0439: 
0440:                 //-------------------------------------------------------------------------
0441:                 // ディスプレースメントマップの設定
0442:                 //-------------------------------------------------------------------------
0443:                 m_pd3dDevice->SetTexture(D3DDMAPSAMPLER, m_pDispMap);
0444:                 m_pd3dDevice->SetNPatchMode(4);// Nパッチのモードで動かす必要がある
0445:                 m_pd3dDevice->SetVertexDeclaration( m_pDecl );
0446: 
0447:                 //-------------------------------------------------------------------------
0448:                 // 描画
0449:                 //-------------------------------------------------------------------------
0450:                 m_pMesh->Render( m_pd3dDevice );
0451: 
0452:                 //-------------------------------------------------------------------------
0453:                 // 描画終了
0454:                 //-------------------------------------------------------------------------
0455:                 m_pd3dDevice->SetNPatchMode(0);
0456:                 m_pd3dDevice->SetVertexShader( NULL );

終わったら、N-パッチの使用をやめたり、固定機能シェーダに戻します。

■最後に

使ってみると、やっぱり使えないのですが、面白いフィーチャーですね。
「一応作りました」的なノリが見えるのですが、DirectX9.1ぐらい(気が早いなぁ)には洗練されてくるのではないでしょうか。





もどる

imagire@gmail.com