【MMEの作り方】シェーダーエフェクト編02/照明の影響で明暗をつける
▼今回の記事ページで使うサンプルファイル
今回の記事ぺージの解説で使うサンプルとなるfxファイルは以下のリンクからダウンロードできます。
ただし、ファイルをDL・使用した際にトラブル・不利益が生じたとしても筆者は責任を負わないものとします。自己責任で御利用ください。
▼前回の単色シェーダーエフェクトの改良
前回の記事では3Dオブジェクトを単色に塗りつぶすシェーダーエフェクトを作りました。
しかし、記事内でも言及した通り内容がシンプル過ぎて実用性は皆無です。
少なくとも、のっぺりした単色に対してライトの影響を受けての明暗くらいは実装したい。
なので今回の記事ではそれを実現するための方法を解説していきます。
▼光の方向をパラメータで指定するコード
まずは以下の図のように、単色オブジェクトに明暗が付いているものを作っていきます。

まずはコードをご覧ください。
(※サンプルファイル『1-2-1_Shading.fx』)
////////////////////////////////////////////////////////////////
// 1-2-1_ライティング/光の方向をfxファイル内のパラメータで指定
////////////////////////////////////////////////////////////////
// -------------------------------------------------------------
// パラメータ宣言関連
// -------------------------------------------------------------
// 変数
float4 DrawColor = float4(1, 0.5, 0.3, 1); // ベース色 (RGBA)
float3 LightDir = float3(-0.5, -0.9, 0.5); // 光の方向(ワールド空間)
// 座標変換行列
float4x4 World : WORLD;
float4x4 WorldViewProj : WORLDVIEWPROJECTION;
//---------------------------------------------
// 頂点シェーダ関連
//---------------------------------------------
struct VS_OUTPUT {
float4 Pos : POSITION;
float3 Normal : TEXCOORD0; // ワールド空間法線
};
// 頂点シェーダ
VS_OUTPUT Basic_VS(float4 Pos : POSITION, float3 Normal: NORMAL)
{
VS_OUTPUT Out;
// 頂点位置を射影変換
Out.Pos = mul(Pos, WorldViewProj);
// 法線をワールド空間へ変換
float3x3 World3x3 = (float3x3)World;
Out.Normal = normalize(mul(Normal, World3x3));
return Out;
}
//---------------------------------------------
// ピクセルシェーダ関連
//---------------------------------------------
float4 Basic_PS(float3 Normal : TEXCOORD0) : COLOR0
{
// 光の方向を正規化(逆方向にして「当たる側」が明るくなるように)
float3 L = normalize(-LightDir);
// 拡散反射(Lambert)計算
float NdotL = saturate(dot(Normal, L));
// アンビエント(環境光)と拡散反射を合成
float3 Ambient = 0.2 * DrawColor.rgb;
float3 Diffuse = NdotL * DrawColor.rgb;
float3 finalColor = Ambient + Diffuse;
// 最終的なRGBA
float4 ResultColor = float4(finalColor, DrawColor.a);
return ResultColor;
}
//---------------------------------------------
// テクニック定義
//---------------------------------------------
technique MainTec < string MMDPass = "object"; >
{
pass ResultColor
{
VertexShader = compile vs_3_0 Basic_VS();
PixelShader = compile ps_3_0 Basic_PS();
}
}
technique MainTec_ss < string MMDPass = "object_ss"; >
{
pass ResultColor
{
VertexShader = compile vs_3_0 Basic_VS();
PixelShader = compile ps_3_0 Basic_PS();
}
}
technique EdgeTec < string MMDPass = "edge"; >
{
}今回は色の指定に加えて、ひとまずライトベクトル(光が当たる方向)もfloat3の値で指定しています。

次に、頂点シェーダーについてを見ていきましょう。
前回は3Dモデルの頂点をスクリーン座標に置き換える処理でしたが、今回は『法線』なるものに関連した項目が書かれています。
法線とは、平たくいうと頂点や面の向きを決定する要素をいいます。
面が正面に向いていて正面から光が当たっていればその面は明るくなります。
面が真横を向いているのに光が横っ面から当たれば面は暗くなります。
これら光の影響を与える要素が法線です。
英語で法線は『Normal(ノーマル)』といいます。
今回の頂点シェーダーでは、モデルに対して法線をどのように取り扱うかも計算しています。
そして計算した法線についての情報をOUTPUTしてピクセルシェーダー側に渡しているのです。

頂点シェーダーにより3Dモデルが法線を使って光の向きの影響を受けられるようにできました。
次はピクセルシェーダー内で実際にモデルに明暗を付ける処理を行います。
それが以下の部分です。

ネックとなるのは『dot』なる演算子です。
これはベクトルの内積を取るための演算子になります。
そうです、ここで高校数学で習う概念が突如現れます。
内積の公式についておさらいすると以下のようになります。

数学記号を見ただけで頭の痛くなる人もいるかもですね。
一応『何か知らんけど法線ベクトルとライトベクトルの内積を取ればオブジェクトに明暗をつけられる』くらいの理解でプログラムとしては動きます。
とはいえ、それだと味気ないので補足的に説明も書いておきます。
内積の公式ではベクトルの絶対値を乗算して、それにベクトル同士の角度のコサインを更に乗算しています。
ここで大事なのはCosθ(ここでは法線ベクトルとライトベクトルの角度のコサイン)です。
Cosθの値は定義(要するに数学って学問での取り決め)により、
・0度なら1
・90度なら0
という値になることになっています。
つまり、法線ベクトル(つまり面)と光の向きに関して、
・同じ角度を向いているなら2つのベクトルの間の角度は0度なので1(つまり100%)
・横っ面に光が当たっているなら2つのベクトルの角度は90度なので0(つまり0%)
の光の影響を受けることになります。
そして、その間の角度ならそれに応じたパーセンテージになります。
要するに内積を使うと、面の向きと光の方向に対しての光の当たり具合を検出できるわけです。
で、生成した光の当たり具合(つまりは明暗)をベースとなる色に対して乗算して完成となります。
▼MMD内のライトの方向を反映させるコード
さて上述のシェーダーエフェクトでの光の方向はあくまでパラメータ内の数値として設定しました。
しかし、現実的に使うとしたらMMD内のライトの方向の影響を与えたいところです。
そこで、先ほどのライト方向に関するコードを以下のように書き換えます。
(サンプルファイルの『1-2-2_Shading_mmdLight.fx』をご覧ください)
// MMD内の照明操作でのライト方向を取得する
float3 LightDirection : DIRECTION < string Object = "Light"; > ;これを先ほどのライトベクトルに置き換えるとMMD内のライト方向の影響を受けるシェーダーエフェクトになります。

▼スペキュラー反射
次にサンプルファイル『1-2-3_Specular.fx』をご覧ください。
スペキュラー反射とは、イラストの文脈でいうところのハイライトのことです。

スペキュラー反射は、法線ベクトルとハーフベクトルの内積を取ることで実現できます。
ここでいうハーフベクトルとは、視線ベクトルとライトベクトルのちょうど中間を指すベクトルをいいます。
ここでいきなり『視線ベクトル』なる言葉が出て来ましたが、『視線ベクトル』とは要はオブジェクトを映すカメラがどちらに向いているかのことです。
視線ベクトルの取り扱いに関するコードやデータの流れは以下の通りです。

で、視線ベクトルを取ってきて前述の通りハーフベクトル化して法線との内積をとってスペキュラーを算出しているわけです。
これをfxファイルとして実装している部分が以下の部分になります。

▼リムライト
最後にサンプルファイル『1-2-4_Rimlihgt.fx』をご覧ください。
リムライトとは、オブジェクトの輪郭に光を当ててオブジェクトと背景の境目をくっきりさせるためのライトです。

現実世界でリムライトは人物の背後から照明を当てて実現します。しかし3DCGではオブジェクトの輪郭に明るい色を足すことで疑似的に実現することが可能です。
3DCGでの疑似的なリムライトは、法線ベクトルと視線ベクトルの内積を取ると実現できます。

