ホーム < ゲームつくろー! < プログラマブルシェーダ編


シェーダ編

その4 ディフューズマップで表面に色を付ける


 シェーダの仕事の1つはポリゴンモデルの表面に綺麗なテクスチャを張ることです。DirectX9の固定機能を使う場合はIDirect3DDevice9::SetTextureメソッドを用いましたが、シェーダを使う場合はまったく別の手段を取ります。

 モデルの表面にはさまざまなタイプのテクスチャを貼り付けることができますが、一番簡単なのがディフューズマップ(ディフューズテクスチャ)です。この章ではその方法について見ていく事にしましょう。


@ 入射光と反射光の色

 私たちは普段「反射した光の色」を見ています。例えば樹木の緑。緑色に見えるのは葉っぱが緑色の光を反射して私たちの目に届くからです。逆を言えば、葉っぱは緑色の光以外を吸収してしまいます。緑色の成分を取り除いた光を葉に当てると、反射できる光が無いので理屈の上では黒色に見えます。実際に試すのはちょっと難しいのですが、ドローソフト(Photoshopなど)で2つの画像を乗算するテストを行うと簡単に体験できます。

 私たちは「葉っぱは緑色だ」と考えて緑色のテクスチャを作ります。しかし、これは暗黙的に「白色光」を当てた時に見える色を想定しています。しかし先ほども例に挙げたように、カラー光を当てると物は別の色に見える事があります。ピクセルシェーダでライティングを考える時、この入射光の色と跳ね返って目に届く反射光の色の関係をしっかりと捉えるのがとても大切になります。


A ディフューズとは?

 ディフューズ(Diffuse)を辞書で引くと「拡散反射」と出てきます。拡散反射というのはあらゆる方向に等しく跳ね返る光の事です。その光の強さ(明るさ)はライトと表面の角度で決まります。例えば、目の前に真っ直ぐ立っている壁に向けて真っ直ぐにライトを当てると、一番強い光が跳ね返ります。しかし斜めから当てると、多くの光は飛散してしまって、弱い光がディフューズとして目に届きます。表面は特定の色を吸収して、残りを反射します。それが私たちには「色」として認識できます。つまりディフューズには「強さ」と「色」があるわけです。

 ポリゴンがすべて同じ色を反射するとしても良いのですが、やっぱり物の表面は色々な色を持っています。ディフューズマップ(ディフューズテクスチャ)はモデルの表面が反射する色をテクスチャにしたものです。私たちが普段言う「テクスチャ」の殆どはこのテクスチャです。



B ライトと法線の角度と光の強さ

 太陽の光のようなディレクショナルライトは、光の進行方向、つまり光が差し込む方向が必ずあります。差し込んだ光はポリゴン表面に当たる事で反射します。このポリゴンの向きを表すのが「法線」です。

 さて、法線はモデル空間(ローカル空間)の座標で定義されています。一方ディレクショナルライトは「ワールド空間」で定義するのが普通です。両者はその向きを定義する空間が違うため、ディレクショナルライトがX軸方向を向いていて、ポリゴンの法線が-X軸方向に向いているからといって、そのポリゴンが光をいっぱいに浴びる…という事にはなりません。

 ポリゴンが光をいっぱいに浴びるのか、それとも裏を向いて真っ黒になるのかは、ポリゴンをワールドに置いた時に初めてわかります。つまり、「法線をワールド空間に変換する」ことで、初めてライトとポリゴンの面の角度関係を考える事ができるわけです。

 法線をワールド空間に置くには、法線にワールド変換行列を掛け算すれば良いだけです。これはDirectX9だと次のようにプログラムできます:

法線をワールドに置く
D3DXMATRIX WorldMat;   // ワールド変換行列
D3DXVECTOR3 Normal;   // 法線

D3DXVec3TransformNormal( &Normal, &Normal, &WorldMat );

 D3DXVec3TransformNormal関数は法線のような向きを表すベクトルを指定の行列で変換してくれます。

 ワールドに置いた法線と、同じくワールドで定義したディレクショナルライトの方向があると、光が面にどの位強く当たっているかを計算できます。ワールド空間にある法線の向きをN、ディレクショナルライトの向きをDとすると、その強さは内積を用いて次のように算出されます:

 Dがマイナスなのは、ライトの方向を逆にするためです。法線は面から飛び出ています。一方ライトは面に向かいますね。お互いが正反対を向いている時に面は最も強く光を受ける(つまり最も強く光を跳ね返す)わけですが、そのまま内積を取ると答えは「-1」と一番小さくなってしまます。そこでライトの方向を逆にすると符号が逆転して感覚とPowerの値が合います。



B 光の強さと目に届く色

 ワールドにあるライトの方向と法線の方向から面が光をどれだけの強さで反射するかがわかりました。続いてライトが発射する色と、表面が反射する色の関係を見ていきましょう。ライトは特定の色の光を面に向かって放射します。それをLC(Light Color)としましょう。一方表面が反射する色をRC(Reflection Color)とします。@でお話したように、物は入射した光の一部を吸収して残りを反射します。これをうまく表現するには、入射色と反射色を掛け算します:

 入射も反射も1以上にはなれないので、SurfaceColorは常に入射色よりも暗くなります。

 上の計算で求めたSurfaceColorは光が真っ直ぐ面に当たった時の最大反射色です。光が面に斜めに当たるとそのぶんディフューズの強さは失われます。最終的に目に届く光の色は上のSurface Colorに拡散光の強さであるPowerを掛け算することで求まります:

 この面がもしカメラから見えているならば、DiffuseColorの色として見えます。拡散反射をするため、どの角度から見ても同じ光の強さで目に届きます。



C ディフューズをシェーダで描画する

 ここまで見てきた原理を用いて、さっそくシェーダでディフューズを再現してみましょう。

 まず必要な物を整理します:

・ ディレクショナルライトの方向  (Light Direction)
・ ディレクショナルライトの色  (Light Color)
・ ローカル座標定義の法線  (Local Normal)
・ ワールド変換行列  (World Matrix)
・ 反射色 (Reflection Color) = ディフューズマップ

 反射色はディフューズカラー、つまりディフューズマップから得ます。この他にも頂点をスクリーンにまで持っていくのでビュー変換と射影変換行列も使います。まずはこれらをシェーダプログラムのグローバル変数として定義します:

シェーダのグローバル変数
float3 g_LD;            // ライトの方向
float3 g_LC;            // ライトカラー(入射色)
float4x4 g_worldMat;    // ワールド変換行列
texture g_diffuseMap;   // ディフューズマップ

float4x4 g_viewMat;     // ビュー行列
float4x4 g_projMat;     // 射影変換行列

sampler diffuseSampler = sampler_state {
   texture = <g_diffuseMap>;
};

 頂点シェーダですべき事は、法線をワールド空間に変換し、それをピクセルシェーダに渡す事です:

頂点シェーダ
struct VS_OUT {
   float4 pos    : POSITION;    // 変換後位置
   float4 uv     : TEXCOORD0;   // UV座標
   float3 normal : TEXCOORD1;   // ワールド変換した法線
};

VS_OUT Diffuse_VS( float4 inLocalPos : POSITION, float3 inLocalNormal : NORMAL, float4 inUV : TEXCOORD0 )
{
   VS_OUT Out = (VS_OUT)0;

   // ローカル頂点をスクリーンへ
   Out.pos = mul( inLocalPos, worldMat );
   Out.pos = mul( Out.pos, viewMat );
   Out.pos = mul( Out.pos, projdMat );

   // 法線をワールド空間へ
   Out.normal = mul( inLocalNormal, worldMat );

   // UV座標を登録
   Out.uv = inUV;

   return Out;
}

この頂点シェーダでは2つの事をしています。1つはローカル座標の頂点をスクリーンへ変換しています。もう1つが法線をライトと同じワールド空間に変換する計算です。計算結果はOut.normalに格納されています。Out.normalはTEXCOORD1(テクスチャ座標)なのでピクセルシェーダに渡るとポリゴン表面の1点の法線として線形補間されます。これによって平らなポリゴン面上を滑らかに変化する法線が扱えるわけです。

 ピクセルシェーダではディフューズカラーを算出します。計算は先の原理の通りで、ワールド空間にあるライトの方向と法線の向きとの内積から光の強さ、入射色と反射色の掛け算から目に届く色味をそれぞれ算出します:

ピクセルシェーダ
float4 Diffuse_PS( float3 inWorldNormal : TEXCOORD1, float4 inUV : TEXCOORD0 )
{
   // 反射する光の強さを算出
   float Power = dot( normalize(inWorldNormal), -normalize(g_LD) );
   Power = clamp( Power, 0.0f, 1.0f );

   // 拡散反射する光の色を算出
   float4 RC = tex2D( diffuseSampler, inUV );   // 反射色
   float4 SurfaceColor = RC * g_LC;             // 表面の色が決定

   return Power * SurfaceColor;   // ディフューズカラー算出
}

 内積を計算するときには、ワールド空間にある法線とライト方向の両方を正規化する必要があります。テクスチャからピックアップした反射色にライトからの入射色を掛け算すると表面の色が算出できるので、後は光の強さを掛け算すればディフューズカラーが決定します。



 これで、モデルにディフューズテクスチャを貼る事ができました。ライトの方向の反対側には陰もちゃんとできます。これは光の強さによる副産物です。割と簡単な計算で色と陰が付くのはうれしいものです。本章の内容を踏まえたモデルにディフューズマップを貼り付けるサンプルを挙げましたので、興味のある方はご参照ください。