【連載】Unity時代の3D入門 – 第5回「拡散反射ライティング」

こんにちは、クライアントサイドエンジニアの矢野です。

第4回では、シェーダとは何かを学び、実際に簡単なシェーダを書きました。
第5回では、物体に色と陰をつけるライティングである拡散反射ライティングについて説明します。

「見える」ということ

人間が物体を見るとき、実際には物体に反射した光を見ています。
そのため、物体を照らす光が赤ければ照らされた物体も赤く見えるし、照らす光の存在しない真っ暗な場所ではどんな物体も見えません。

また、太陽の方向を向いている面はより多くの光が差し込むので明るくなり、太陽に背を向けている面は光が当たらないので暗くなります。
これにより明暗ができて、物体は立体的に見えます。

3Dの世界では、このような物体と光の性質をシェーダでシミュレートすることによって表現します。
また、ライトの影響を計算して描画に反映する処理をライティングといいます。

 

拡散反射とは

光が物体に当たると、光の一部が表面で反射し、残りは屈折しながら物体の中に入ります。

表面で反射した光はハイライトを作ったり周囲の景色を映したりしますが、今回は物体の中に入る屈折光の方を見ていきます。

屈折光は、物体内の粒子と衝突することによって複雑に軌道を変えます。
この現象を散乱といいます。

散乱した光の一部は、度重なる散乱の結果として再び物体の表面から出てきます。
このように、一度物体の中に入ってから出てきた光を拡散反射光といいます。

こうして出てくる拡散反射光が多い場所ほど明るくなり、少ない場所ほど暗くなります。
光源に背をむけている面はそもそも当たっている光が少ないため、拡散反射光も弱くなり、結果的に暗く見えるということです。

拡散反射光はディフューズ(diffuse)とも呼ばれます。

 

[原理] 反射の強さを表現する

拡散反射をシェーダで表現するために、まず反射の強さ、つまり物体の明るさをどう表現するか考えます。
物体に光がある一定の方向からのみ当たるとき、物体の明るさは次のようになります。


これを数値で表すため、一番明るい部分を1、一番暗い部分を0とします。

最終的にこのような値を計算で出すことができれば、明るさを表現できたということになります。

さて、ここで一度ベクトルの内積という計算が出てきますので説明します。
内積は2つのベクトルを使う演算であり、単位ベクトル同士の内積は、同じ方向を向いていれば1、逆を向いていれば-1、90度角度がついているときに0になるという性質があります。

ちなみに計算式としては、ベクトル (Ax, Ay, Az) と ベクトル (Bx, By, Bz) の内積は Ax * Bx + Ay * By + Az * Bz となります。
これを反射の強さの計算に応用します。

まず、ある頂点における法線と、頂点からライトに向けたベクトルの単位ベクトルとの内積をとります。
さらに内積の結果、負になった値を全て0にすると、下図のようになります。

これの内積の値は、上述の、数値で表した物体の明るさと等しくなります。
このように、ある頂点における拡散反射の強さを頂点の法線と正規化したライト方向のベクトルとの内積を使ってモデル化できます。

 

[シェーダ] 反射の強さを表現する

この計算をシェーダで記述します。

Shader "Custom/DiffuseIntensity"
{
    SubShader
    {
        Pass
        {
            // ライト情報を使用するときに記述
            Tags { "LightMode"="ForwardBase" }

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            // Unityで定義されている変数や関数をインクルード
            #include "UnityCG.cginc"

            struct appdata
            {
                half4 vertex : POSITION;
                // 頂点の法線情報
                half3 normal : NORMAL;
            };

            struct v2f
            {
                half4 pos : SV_POSITION;
                half3 normal: TEXCOORD1;
            };
            
            v2f vert (appdata v)
            {
                v2f o = (v2f)0;
                o.pos = UnityObjectToClipPos(v.vertex);
                // ワールド座標系に変換
                o.normal = UnityObjectToWorldNormal(v.normal);
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                i.normal = normalize(i.normal);

                fixed4 col;
                // 拡散反射光の計算
                col.rgb = max(0, dot(i.normal, _WorldSpaceLightPos0.xyz));
                return col;
            }
            ENDCG
        }
    }
}

上記ではまず、Tag を定義しています。

Tags { "LightMode"="ForwardBase" }

シェーダ内でUnityのライトの情報(位置や色など)を取得したいときにはこの記述が必要となります。
正確には、このパスが Forward Rendering のベースパスであることを定義するものですが、現段階では上記の理解で問題ありません。

次に UnityCG.cginc をインクルードしています。
これにより、Unityであらかじめ定義されているシェーダ用の変数やヘルパー関数を使用することができます。

#include "UnityCG.cginc"

次に、appdata で NORMAL セマンティクスを付けた変数を定義して頂点の法線情報を取得します。

half3 normal : NORMAL;

また、この法線情報はモデル座標系における情報です。
モデル座標系とは各モデル毎に原点と軸が決められたものであり、今回はこれをUnityの原点と軸に合わせたワールド座標系に変換する必要があります。

この変換は、UnityCG.cgincで定義された関数 UnityObjectToWorldNormal() で行っています。

o.normal = UnityObjectToWorldNormal(v.normal);

最後に、フラグメントシェーダで拡散反射光の計算をしています。

col.rgb = max(0, dot(i.normal, _WorldSpaceLightPos0.xyz));

上記ではまず _WorldSpaceLightPos0.xyz で、シーン上に置かれた Directional Light の位置を取得しています。
この変数はUnityの定義済み変数です。
自動的にインクルードされる UnityShaderVariables.cginc の中で定義されていて、ディレクショナルライトの場合はライトの方向が、その他のライトの場合はライトの位置情報が入っています。

これを用いて、dot(i.normal, _WorldSpaceLightPos0.xyz) で内積を求め、それに対してmax関数を用いることで0未満の値を0に変換しています。
すなわちこれが拡散反射光の強さとなるため、この値をrgb値に代入しています。

このシェーダを適当なオブジェクトに適用します。
Directional Lightの情報を取得するため、Hierarchy に Directional Light が置かれていることを確認してください。

効果がわかりやすいように Directional Light を回転させています。
ライトの向きと法線の向きにより拡散反射光の強さが計算されていることがわかります。

 

[原理] 物体の色を表現する

次に物体の色について考えます。
物体内に入った光は、拡散反射として出てくるまでの間に一部の色が吸収されます。

例えば白色 = RGB(1, 1, 1) の光が差し込んでいて、そのうちの緑(G)と青(B)が吸収されると、出てくる光は赤色 = RGB(1, 0, 0) になります。

計算上は、白色を物体に当てたときに出てくる色を物体の色として定義します。
つまり上記の例だと赤色 = RGB(1, 0, 0) です。

これに先ほど求めたライトの強さの値を乗算すれば、一番明るい部分(ライトの強さ = 1)はRGB(1, 0, 0)、一番暗い部分(ライトの強さ = 0)はRGB(0, 0, 0)、となり、ライティングされた物体の色が求められます。

 

[シェーダ] 物体の色を表現する

シェーダは以下のように変更します。

Shader "Custom/DiffuseObjectColor"
{
    Properties
    {
        // 物体の色をプロパティで定義
        _Color ("Color (RGB)", Color) = (1, 1, 1, 1)
    }
    SubShader
    {
        Pass
        {
            Tags { "LightMode"="ForwardBase" }

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            
            #include "UnityCG.cginc"

            struct appdata
            {
                half4 vertex : POSITION;
                half3 normal : NORMAL;
            };

            struct v2f
            {
                half4 pos : SV_POSITION;
                half3 normal: TEXCOORD1;
            };

            half4 _Color;

            v2f vert (appdata v)
            {
                v2f o = (v2f)0;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.normal = UnityObjectToWorldNormal(v.normal);
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                i.normal = normalize(i.normal);

                fixed4 col;
                // 物体の色を反映
                col.rgb = max(0, dot(i.normal, _WorldSpaceLightPos0.xyz)) * _Color.rgb;
                return col;
            }
            ENDCG
        }
    }
}

まず、物体の色をプロパティとして定義できるようにします。
プロパティはUnityの機能であり、以下のように記述した変数の値をインスペクタから設定できるようになります。

Properties
{
     // 物体の色をプロパティで定義
    _Color ("Color (RGB)", Color) = (1, 1, 1, 1)
}

この色をフラグメントシェーダでライトの強さの値に乗算することにより、物体の色を反映させます。

col.rgb = max(0, dot(i.normal, _WorldSpaceLightPos0.xyz)) * _Color.rgb;

物体の色を赤に設定した場合、レンダリング結果は下図のようになります。

 

[原理] ライトの色を表現する

物体に差し込む光にも色はあるので、次にこの色を反映します。

例えば、物体の白色の部分 = RGB(1, 1, 1) に赤いライト = RGB(1, 0, 0) を反映すると赤 = RGB(1, 0, 0) になります。

このようにライトの色は、先ほど求めた物体の色に乗算することで適用できます。

 

[シェーダ] ライトの色を表現する

シェーダは以下のように変更します。

Shader "Custom/DiffuseLightColor"
{
    Properties
    {
        _Color ("Color (RGB)", Color) = (1, 1, 1, 1)
    }
    SubShader
    {
        Pass
        {
            Tags { "LightMode"="ForwardBase" }

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            
            #include "UnityCG.cginc"

            struct appdata
            {
                half4 vertex : POSITION;
                half3 normal : NORMAL;
            };

            struct v2f
            {
                half4 pos : SV_POSITION;
                half3 normal: TEXCOORD1;
            };

            half4 _Color;
            // ライトの色を取得するための変数を定義する
            half4 _LightColor0;

            v2f vert (appdata v)
            {
                v2f o = (v2f)0;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.normal = UnityObjectToWorldNormal(v.normal);
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                i.normal = normalize(i.normal);

                fixed4 col;
                // ライトの色を反映
                col.rgb = max(0, dot(i.normal, _WorldSpaceLightPos0.xyz)) * _Color.rgb * _LightColor0.rgb;
                return col;
            }
            ENDCG
        }
    }
}

まず、ライトの色を取得するための変数を定義します。

half4 _LightColor0;

この変数は _WorldSpaceLightPos0 と同じくUnityの定義済み変数です。
Lighting.cginc をインクルードすることでも使用できますが、今回のように変数を定義すればこれにライトの色が入ってきます。

そしてこのライトの色を乗算することにより反映します。

col.rgb = max(0, dot(i.normal, _WorldSpaceLightPos0.xyz)) * _Color.rgb * _LightColor0.rgb;

例えば物体の色を白にして、Directional Light の赤に設定した場合、レンダリング結果は下図のようになります。

 

テクスチャを貼る

ここまでで物体に拡散反射光を適用できましたが、物体の色が単色では面白みがないので、テクスチャを貼ってみましょう。

テクスチャを貼るにはシェーダを次のように変更します。

Shader "Custom/DiffuseTexture"
{
    Properties
    {
        // テクスチャのプロパティ
        _MainTex ("Main Texture (RGB)", 2D) = "white" {}
        _Color ("Color (RGB)", Color) = (1, 1, 1, 1)
    }
    SubShader
    {
        Pass
        {
            Tags { "LightMode"="ForwardBase" }

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                half4 vertex : POSITION;
                // uv座標
                half2 uv : TEXCOORD0;
                half3 normal : NORMAL;
            };

            struct v2f
            {
                half4 pos : SV_POSITION;
                // uv座標
                half2 uv : TEXCOORD0;
                half3 normal: TEXCOORD1;
            };

            // テクスチャ情報を格納する変数
            sampler2D _MainTex;
            half4 _MainTex_ST;

            half4 _Color;
            half4 _LightColor0;

            v2f vert (appdata v)
            {
                v2f o = (v2f)0;
                o.pos = UnityObjectToClipPos(v.vertex);
                // uv値をフラグメントシェーダに渡す
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.normal = UnityObjectToWorldNormal(v.normal);
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                i.normal = normalize(i.normal);

                fixed4 col;
                // テクスチャの色を取得
                half4 tex = tex2D(_MainTex, i.uv);
                // テクスチャの色を反映
                col.rgb = max(0, dot(i.normal, _WorldSpaceLightPos0.xyz)) * _Color.rgb * _LightColor0.rgb * tex.rgb;
                return col;
            }
            ENDCG
        }
    }
}

まず、プロパティでテクスチャを設定できるようにします。

 _MainTex ("Main Texture (RGB)", 2D) = "white" {}

次に、appdata と v2f 構造体それぞれにuv座標を定義します。
appdata では TEXCOORD0 セマンティクスを定義することでuv座標を取得できます。

// appdata
half2 uv : TEXCOORD0;

// v2f
half2 uv : TEXCOORD0;

また、テクスチャ情報を格納するための変数を2つ定義しています。

// テクスチャ情報を格納する変数
sampler2D _MainTex;
half4 _MainTex_ST;

_MainTex にはテクスチャの色情報が入ってきます。
_MainTex_ST のように、[テクスチャのプロパティ名] + _ST という変数を定義すると、テクスチャの Tiling と Offset の情報を取得できます。

頂点シェーダではuv値を求めます。
TRANSFORM_TEX はUnityCG.cginc に定義されているマクロで、元のuv値と_MainTex、_MainTex_ST を用いてタイリングとオフセットを考慮したuv値を計算しています。

o.uv = TRANSFORM_TEX(v.uv, _MainTex);

フラグメントシェーダではまず、tex2D 関数にテクスチャとuv値を渡すことで、このピクセルにおけるテクスチャの色を取得しています。

half4 tex = tex2D(_MainTex, i.uv);

さらにこれを乗算することで、テクスチャの色を反映します。

col.rgb = max(0, dot(i.normal, _WorldSpaceLightPos0.xyz)) * _Color.rgb * _LightColor0.rgb * tex.rgb;

テクスチャを反映すると以下のようなレンダリング結果が得られます。

 

環境光を適用する

さて、ここまでで拡散反射光をシェーダで表現することができました。
最後に一点、環境光というものを追加してこの回を終わろうと思います。

ここまでで作成した物体は、ライトの向きを表とすると、裏側が完全に黒色になっています。
もちろんこれは光が当たっていないためですが、現実的にはこの光が地面や壁などに反射してから再び物体に差し込むため、物体の裏側も完全な黒にはなりません。

このような間接光を環境光といいます。
環境光は、拡散反射のライトの成分に環境光の色を足し合わせることで表現します。

環境光は現実では非常に複雑なものですが、今回は単色であると近似してシェーダに適用します。

Shader "Custom/DiffuseTexture"
{
    Properties
    {
        _MainTex ("Main Texture (RGB)", 2D) = "white" {}
        _Color ("Color (RGB)", Color) = (1, 1, 1, 1)
        // 環境光の色
        _Ambient ("Ambient Color (RGB)", Color) = (0, 0, 0, 0)
    }
    SubShader
    {
        Pass
        {
            Tags { "LightMode"="ForwardBase" }

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                half4 vertex : POSITION;
                half2 uv : TEXCOORD0;
                half3 normal : NORMAL;
            };

            struct v2f
            {
                half4 pos : SV_POSITION;
                half2 uv : TEXCOORD0;
                half3 normal: TEXCOORD1;
            };

            sampler2D _MainTex;
            half4 _MainTex_ST;
            half4 _Color;
            half4 _LightColor0;
            // 環境光の色
            half4 _Ambient;

            v2f vert (appdata v)
            {
                v2f o = (v2f)0;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.normal = UnityObjectToWorldNormal(v.normal);
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                i.normal = normalize(i.normal);

                fixed4 col;
                half4 tex = tex2D(_MainTex, i.uv);
                // 環境光の色を反映
                col.rgb = tex.rgb * _Color.rgb * (max(0, dot(i.normal, _WorldSpaceLightPos0.xyz)) * _LightColor0.rgb + _Ambient.rgb);
                return col;
            }
            ENDCG
        }
    }
}

まず _Color と同じように _Ambient プロパティと変数を定義します。
フラグメントシェーダでは、拡散反射光の色と環境光の色を加算してから、それを物体の色に乗算しています。

col.rgb = tex.rgb * _Color.rgb * (max(0, dot(i.normal, _WorldSpaceLightPos0.xyz)) * _LightColor0.rgb + _Ambient.rgb);

このシェーダを適用すると、下図のようになります。

左が環境光を適用したもの、右が環境光を適用していないものです。
環境光を適用することにより、陰の部分が明るくみえていることがわかります。

なお、Unityでは Window > Lighting  > Settings の Scene タブから環境光を設定し、これをシェーダで受け取ることができます。
今回は詳しくは説明しませんが、興味のある方はチャレンジしてみてください。

 

補足(発展)

今回、説明の便宜上、正確性を犠牲にした部分があるため数点補足をします。

まず「光」という表現についてです。
ここで使っている光とは、ヒトの目に見える可視光線のことを指しています。
電磁波という波である光には波長があり、ヒトが知覚できるある範囲内の波長の光を可視光線と呼びます。

また、可視光線の中でも波長の違いが存在し、それがすなわち「色」の違いとなります。
赤い色の物質とは、ヒトが赤い色として知覚する波長以外の波長を吸収し、赤い色の波長を拡散反射する物質であるといえます。

ちなみに、吸収された光エネルギーは消えて無くなるわけではなく、熱エネルギーや化学変化のエネルギーに変換されます。

次に透過光についてです。
物体に入った光は吸収されるか反射されるという説明をしましたが、実際には物体を通り抜ける透過光も存在します。

水のような透明な物体は光が透過してその先にある物体の色を反射します。

最後に拡散反射の強さの計算について、今回行った計算は拡散反射光の全てが光の入射位置と全く同じ位置から射出されるものとして近似したものとなります。
これは物理的には正確性を欠きますが、簡単な計算式で表せるというメリットがあります。

この近似モデルをランバート反射モデルといいます。
もし正確に拡散反射光を計算するならば、射出される位置は入射位置を中心として放射状に広がりを見せるはずです。

 

まとめ

今回は以下のことを行いました。

  • 拡散反射光の概念と計算
  • テクスチャの取り扱い
  • 環境光

次回は、ハイライトを作るライティング、鏡面反射ライティングを説明します。

 

バックナンバー