JKになりたい

何か書きたいことを書きます。主にWeb方面の技術系記事が多いかも。

Shaderやっていく〜ランバート反射〜

完成図

f:id:sakata_harumi:20190517020535p:plain:w400

左がランバート反射のシェーダー。右がStandardシェーダー(Metallic、Smoothness共に0)になります。

光の当たっている方向は同じですが、影のつき方が全然違うことがわかります。

ランバート反射とは?

Unity4まで標準としてランバート反射に基づく拡散ライティングモデルが使われていました。

Wikipedia曰く、ランバート反射は「拡散反射表面を理想的に扱った反射モデル」になります。
拡散反射表面というと、入射光が様々な角度で反射しているかのように見えるもので、ざらざらした木の表面などが相当します。
逆に、金属面などの鏡面反射を伴うものの表面は表現することはできません。

ランバート反射では、ポリゴン表面が反射する光量は入射光と表面法線との角度に依存します。

光が90度の角度で表面に当たると、全ての光が反射して戻ってきますし、その角度が小さいほど反射される光が少なくなります。

f:id:sakata_harumi:20190517002526p:plain:w500

これら2つのベクトルは単位ベクトルとして入力されますので、それらの内積を取れば良いです。
内積が0に等しいときは、2つのベクトルは直交しておりなす角が90度であることがわかりますし、1(or -1)であるときそれらは互いに平行であることがわかります。

内積が負の場合は、光がポリゴンの反対側からきていることになります。そのため、内積が負の場合は反射しないものとして0を代入しておく場合もあります。
今回のような不透明ジオメトリの場合は、カメラの前に面していないポリゴンはカリングされレンダリングされないので、この問題は発生しません。

実装

以前の記事では組み込みのランバート反射に基づくライティングモデルを使用していましたが、今回は自分で実装してみましょう。

サーフェースシェーダーにおいて、カスタム反射モデルを使用する場合はhalf4 Lighting<Name> (SurfaceOutput s, half3 lightDir, half atten);といったインターフェースに実装します。

上の例はビュー方向に依存していないライティングモデルのインターフェースですが、他に,ビュー方向に依存している場合の
half4 Lighting<Name> (SurfaceOutput s, half3 lightDir, half3 viewDir, half atten);
や、遅延ライティングパスに使用する
half4 Lighting<Name>_PrePass (SurfaceOutput s, half4 light); といったものもあります。

<Name>の部分を任意に変更して、関数の名前をコンパイラディレクティブとして宣言しておきます。
#pragma surface ... 命令は#pragma surface surfaceFunction lightModel [optionalparams]といった形でしたね。
ここのlightModelを独自に定義した関数名にしてあげるとカスタムライティングモデルが使用できます。

実装全体はこちらです。

Shader "Custom/SimpleLambert"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200

        CGPROGRAM
        #pragma surface surf SimpleLambert

        #pragma target 3.0

        sampler2D _MainTex;

        struct Input
        {
            float2 uv_MainTex;
        };

        void surf (Input IN, inout SurfaceOutput o)
        {
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex);
            o.Albedo = c.rgb;
        }
        half4 LightingSimpleLambert(SurfaceOutput s, half3 lightDir, half atten)
        {
            half NdotL = dot(s.Normal, lightDir);
            half4 color;
            color.rgb = s.Albedo * _LightColor0.rgb * (NdotL * atten);
            color.a = s.Alpha;
            return color;
        }
        ENDCG
    }
    FallBack "Diffuse"
}

SurfaceFunctionはテクスチャを貼ってるだけなので割愛します。

メインのカスタムライティングモデルを定義している部分をみていきます。

half4 LightingSimpleLambert(SurfaceOutput s, half3 lightDir, half atten)
{
    half NdotL = dot(s.Normal, lightDir);
    half4 color;
    color.rgb = s.Albedo * _LightColor0.rgb * (NdotL * atten);
    color.a = s.Alpha;
    return color;
}

見ていきます・・といっても非常にシンプルですね。

half NdotL = dot(s.Normal, lightDir)内積を計算しています。先程も書きましたが0だと直交(=光は反射されない)、1だと平行(=光を全て反射)でした。

color.rgb =...の部分で表面の色を決定しています。_LightColor0がライトのカラー情報を保持しており、attenが光の減衰率を表しています。
つまり、「SurfaceのAlbedoカラー」「ライトのカラー」「(内積 * 光の減衰率)」で最終的に描画するカラーを決定しています。

おわりに

今回は拡散反射モデルを実装したので、次は鏡面反射モデルを見ていきたいと思います。