ホーム < ゲームつくろー! < プログラマブルシェーダ編
シェーダ編
その6 0から学ぶ環境マップ
約2年ぶりのシェーダの記事となりました(^-^;。たまたま機会があったためまとめておこうと思った今回のテーマは環境マップです。環境マップとはモデルの表面に周囲の背景等が映り込む様子を再現した物で、その手法その物やそれに必要なテクスチャの両方を指すようです。鏡や磨かれた柱、金属製の時計などなど、私たちの周りには鏡面性が高いために周りの景色を反射している物が沢山存在しています。私たちは知らず知らずそれらを見て「磨かれた金属っぽい」などの質感を感じています。これが画面にあると強烈な現実味を醸しだすようになります:
上は今回用意したサンプルの実行結果です。これをどのように実現するのか?実は結構簡単なんです。じっくり見ていきましょう。
@ 目→モデル表面反射→背景のどこか
今、私たちの目線から物の表面のある一点へ視線を飛ばすとします。その視線は面に入射し、反射します。反射した後の私たちの視線は、きっと遥かかなたに飛んで行って、いずれ何かに衝突します。そこには少なくとも何らかの色があるはずです。今度は逆にその当たった点から物の反射点までレイを飛ばします。レイは表面で反射して、きっと私たちの目に飛び込んできます。これにより私たちは物の表面に「色」を感じるわけです:
では、これをシェーダで考えてみます。
頂点シェーダにはローカル空間での頂点座標が引数として入ってきます。これにワールド変換行列を掛けるとワールド空間に移動します。今カメラのワールド位置があるとして、その位置からワールド空間頂点に対してレイを飛ばします。このレイの方向が「視線ベクトル」となります。頂点にはもう一つ「法線」の情報もあります。法線は面の向きを表しますね。と言う事は、視線ベクトルが頂点に当たった後「どちらに反射するか(反射ベクトル)」を簡単に計算できてしまいます。後はその反射した先にある景色から色をもらえばいいんです。
「まてまて、『反射した先にある景色の色』って、シェーダ内で周囲のモデルの情報は取れないでしょ」とお気付きの方はすばらです。その通りでして、シェーダ内ではワールドにある物の情報は普通取れません。シェーダで取得可能な色情報は頂点カラーと「貼り付けるテクスチャ」のみです。そこでこう考えます、「世界は大きな立方体だ!」と。非常に大胆です。しかしそうすると、世界(背景)の色情報は6面のテクスチャにすべて表現されます:
シェーダ内で立方体を想定すれば、具体的なモデルの情報をシェーダに渡す必要はありません。後は、視線の先にあるだろうでっかいテクスチャのUVを直接指定して、その色を出力すれば、モデルの表面に周囲の色が反射したように見えます。
この立方体の内側に貼り付いているテクスチャこそ「環境マップ」です。
A IDirect3DCubeTexture + reflect関数を使えば簡単!
上のようなテクスチャは特に「キューブマップ」と呼ばれています。こういう6面のテクスチャをIDirect3DTexture9を6つ使って表現することもできますが、DirectXにはキューブマップ専用のIDirect3DCubeTextureというインターフェイスが用意されています。これを使うと非常に簡単に環境マップ(キューブ環境マップ)を実現出来ます。
キューブマップは幾つかの方法で作成できるのですが、ファイルから作成するにはD3DXCreateCubeTextureFromFileメソッドを使うと簡単です:
HRESULT D3DXCreateCubeTextureFromFile(
LPDIRECT3DDEVICE9 pDevice,
LPCTSTR pSrcFile,
LPDIRECT3DCUBETEXTURE9 *ppCubeTexture
);
pDeviceは描画デバイスです。
pSrcFileにはキューブマップの画が入った画像ファイル名を指定します。ただし、ファイルは.ddsフォーマットである必要があります。
ppCubeTextureにキューブマップオブジェクトへのポインタが返ります。
この関数で扱えるファイルは.ddsフォーマットファイルです。これはDirectXに付属しているDirectX Texture Tool(Dxtex.exe)で他の画像ファイルから作ることができます。このツールでキューブマップテクスチャも作成可能で、上の図にあるような6面の「べき乗サイズの」背景画を用意しておいてTool内に連続で読み込ませると作れます(詳しくはツールを使ってみて下さい)。
さてそうして作成したキューブマップテクスチャをシェーダに渡します。今回のサンプル内のシェーダを見てみましょう:
// 環境マップ用頂点シェーダ
const char *vertexShaderStr =
float4x4 view : register(c0);
float4x4 proj : register(c4);
float4x4 world : register(c8);
float3 cameraPosW : register(c12); // カメラのワールド位置
struct VS_IN {
float3 pos : POSITION;
float3 normal: NORMAL;
};
struct VS_OUT {
float4 pos : POSITION;
float3 normalW: TEXCOORD0; // ワールド空間の法線
float3 viewVecW: TEXCOORD1; // ワールド空間での視線ベクトル
};
VS_OUT main( VS_IN In ) {
VS_OUT Out;
Out.pos = mul( float4(In.pos, 1.0f), world );
Out.viewVecW = Out.pos.xyz - cameraPosW;
Out.pos = mul( Out.pos, view );
Out.pos = mul( Out.pos, proj );
Out.normalW = mul( float4(In.normal, 0.0f), world );
return Out;
}
// 環境マップ用ピクセルシェーダ
const char *pixelShaderStr =
textureCUBE cubeTex;
samplerCUBE cubeTexSampler =
sampler_state {
Texture = <cubeTex>;
MinFilter = LINEAR;
MagFilter = LINEAR;
MipFilter = LINEAR;
};
struct VS_OUT {
float3 normalW: TEXCOORD0;
float3 viewVecW: TEXCOORD1;
};
float4 main( VS_OUT In ) : COLOR {
float3 vReflect = reflect( In.viewVecW, In.normalW );
return texCUBE(cubeTexSampler, vReflect);
}
サンプルの殆どは変数やサンプラなどの宣言で、実際のシェーダコードは極めて短いです!特にピクセルシェーダを御覧下さい。2行です(笑)。これで冒頭の環境マップが実現出来てしまいます。
これはシェーダ自体がキューブテクスチャからのサンプリングをサポートしてくれているためです。まずキューブマップはtextureCUBEというテクスチャ型で与えられます。このテクスチャ専用のサンプラがsamplerCUBEです。そしてtexCUBE関数を通してsamplerCUBEサンプラに対して「反射ベクトル」を与えると、そのベクトルの先にあるキューブマップの色をサクっと取ってきてくれるんです。これにより、反射ベクトルと立方体の交点を求めて、それをUVに変換して云々…という面倒な計算が全部省略できてしまいます。
では、その反射ベクトルを求めるにはどうするか?実はこれも簡単で「reflect関数」を使います。この関数は、視線ベクトルと面の法線ベクトルから、視線が面に反射した後の進行方向である反射ベクトルをサクっと求めてくれます。と言うことで、ピクセルシェーダはとっても簡単なんです。
視線ベクトルはカメラの位置とカメラが見ている「点」がわかれば求められます。これは頂点シェーダで求めます。そのため、上の例では頂点シェーダにカメラのワールド位置を与えています。頂点をワールド空間に移動した後、「頂点位置-カメラ位置」で視線ベクトルを作っています。それをTEXCOORD1としてピクセルシェーダに引渡します。ついでに法線もピクセルシェーダに渡します。
B プログラム側
プログラム側ではIDirect3DDevice9::SetTextureメソッドにキューブマップテクスチャを渡し、頂点シェーダとピクセルシェーダをそれぞれ描画デバイスに設定して、描画するだけです:
// カメラ位置更新
D3DXVECTOR3 cameraPos(350.0f * cos(a*0.5f), 450.0f * sin(0.2f*a), 350.0f * sin(a*0.5f));
g_pD3DDev->SetTexture(0, cubeTex); // キューブテクスチャセット
g_pD3DDev->SetVertexShader(vertexShader);
g_pD3DDev->SetPixelShader(pixelShader);
g_pD3DDev->SetVertexShaderConstantF(0, (float*)&view, 4);
g_pD3DDev->SetVertexShaderConstantF(4, (float*)&proj, 4);
g_pD3DDev->SetVertexShaderConstantF(8, (float*)&world, 4);
g_pD3DDev->SetVertexShaderConstantF(12, (float*)&cameraPos, 4);
cube->DrawSubset(0);
当初色々面倒なのかなと思っていた環境マップは、キューブマップさえ用意できれば驚くほど簡単な実装で実現できることがわかりました。ゲームのリアリティをこんなにサクっと高められるのであれば、積極的に使っていきたいもんです(^-^)。