【Unity ShaderLab】NPRなライティングを実装してみる

2022/03/12に公開

はじめに

UnityのShaderLabにて、NPR(Non Photorealtstic Rendering)を実装してみました。


今回実装したものは以下の3つです。

  • 環境光 (Ambient)
  • 拡散反射光 (Diffuse; Phong 反射)
  • 鏡面反射光 (Specular; Blinn-Phong 鏡面反射)

複数Passによるライティング実装

Point Light や Directional Light を使ったライティングを行うには、ライトごとに対応するPassを実装する必要があります。

LightMode 用途
ForwardBase Directional Light や Ambient Light
ForwardAdd Point Light や Spot Light

これらのライティングは、加算合成します。

Passのブレンディング

シェーダー上に Blend を指定することで、Passの出力色と、フレームバッファをどのように合成されるかを指定できます。

Blend SrcFactor DstFactor

Passの最終出力色 \blue{finalColor} は、
シェーダー出力色 \green{SrcValue} と フレームバッファの色 \green{DstValue} を用いて以下のように計算されます。

\blue{finalColor} = \green{SrcValue} * \red{SrcFactor} + \green{DstValue} * \red{DstFactor}

上書きブレンド

以下のように指定した場合、フレームバッファをシェーダー出力色で上書きします。

Blend One Zero 
\blue{finalColor} = \green{SrcValue} * 1 + \green{DstValue} * 0 = \green{SrcValue}
シェーダー例

以下のようなシェーダーを書いた場合、
1Pass目の出力 fixed4(1, 0, 0, 0)がフレームバッファに書きこまれた後、
2Pass目の出力 fixed4(0, 1, 0, 0)がフレームバッファを上書きするため、
最終的にfixed4(0, 1, 0, 0) が画面に表示されます。

Pass
{
    Blend One Zero // 上書きブレンド

    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag

    fixed4 frag(v2f i) : SV_Target
    {
        return fixed4(1, 0, 0, 0);
    }
    ENDCG
}

Pass
{
    Blend One Zero // 上書きブレンド
    
    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag

    fixed4 frag(v2f i) : SV_Target
    {
        return fixed4(0, 1, 0, 0);
    }
    ENDCG
}

加算ブレンド

以下のように指定した場合、SrcValue(シェーダー出力色)とDstValue (フレームバッファ)の色を加算合成します。

Blend One One 
\blue{finalColor} = \green{SrcValue} * 1 + \green{DstValue} * 1 = \green{SrcValue} + \green{DstValue}
シェーダー例

以下のようなシェーダーを書いた場合、
1Pass目の出力 fixed4(1, 0, 0, 0)がフレームバッファに書きこまれた後、
2Pass目の出力 fixed4(0, 1, 0, 0)がフレームバッファに加算されるので、
最終的にfixed4(1, 1, 0, 0) が画面に表示されます。

Pass
{
    Blend One Zero // 上書きブレンド

    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag

    fixed4 frag(v2f i) : SV_Target
    {
        return fixed4(1, 0, 0, 0);
    }
    ENDCG
}

Pass
{
    Blend One One // 加算ブレンド
    
    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag

    fixed4 frag(v2f i) : SV_Target
    {
        return fixed4(0, 1, 0, 0);
    }
    ENDCG
}

複数ライトのブレンディング

拡散反射光、鏡面反射光、環境光といったライティングを個別のPassで計算し、これらを加算でブレンドするようにします。

SubShader 
{
    Pass
    {
        Tags { "LightMode"="ForwardBase" } 
        Blend One Zero
        
        // Directional Light や Ambient Light のライティング処理をここに書く
    }
    
    Pass
    {
        Tags { "LightMode"="ForwardAdd" }
        Blend One One // 加算ブレンド
        
        // Point Light のライティング処理をここに書く
    }
}

実装1: Ambientライト + Directionalライト

疑似コード実装(GLSL)

ShaderLabを使わずに実装した場合、以下のようになります。

vec3 directionalLightNPR(vec3 viewDir, vec3 normal, vec3 light)
{
    // 拡散反射光
    float diffuse = dot(light, normal); // ライトベクトルと法線の内積
    diffuse = saturate(diffuse);

    // 鏡面反射光
    vec3 refl = reflect(-LIGHT, normal); // 反射ベクトル
    float specular = saturate(dot(viewDir, refl)); // 視線ベクトルと反射ベクトルの内積
    specular = pow(specular, 80.0);

    // 光の計算 (拡散反射光 + 鏡面反射光 + 環境光)
    return diffuse * DIFFUSE_COLOR + specular * SPECULAR_COLOR + AMBIENT_COLOR;
}

ShaderLabによる実装 (Directional Light)

Directional Light のライティングは、ForwardBaseパスで実装します。
ForwardBaseパスでは、Directional Lightの色を _LightColor0 で取得でき、
Directional Lightのライトベクトルを _WorldSpaceLightPos0.xyz で取得できます。

Pass
{
    Tags { "LightMode" = "ForwardBase" }
    Blend One Zero

    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag

    #include "UnityCG.cginc"
    #include "Lighting.cginc"

    // CPUからGPUに渡すデータ
    struct appdata
    {
        float4 vertex : POSITION;
        float2 uv : TEXCOORD0;
        float3 normal : NORMAL;
    };

    // vertからfragに渡すデータ
    struct v2f
    {
        float2 uv : TEXCOORD0;
        float4 vertex : SV_POSITION;
        float3 normal : TEXCOORD1;
        float3 viewDir : TEXCOORD2;
    };

    sampler2D _MainTex;
    float4 _MainTex_ST;
    float _SpecularGloss; // 鏡面反射 光沢度
    half4 _AmbientColor; // 環境光 カラー

    v2f vert (appdata v)
    {
        v2f o;

        // MVP座標変換
        o.vertex = UnityObjectToClipPos(v.vertex);

        // UV座標のタイリング・オフセット計算
        o.uv = TRANSFORM_TEX(v.uv, _MainTex);

        // ワールド空間のNormal
        o.normal = UnityObjectToWorldNormal(v.normal);

        // ワールド空間のViewベクトル(サーフェスからみたカメラ位置)
        o.viewDir = normalize(_WorldSpaceCameraPos - mul(unity_ObjectToWorld, v.vertex));

        return o;
    }

    float4 frag (v2f i) : SV_Target
    {
        // ライトベクトル
        #define L _WorldSpaceLightPos0.xyz

        // 反射ベクトル
        #define R reflect(-L, i.normal)

        fixed4 texColor = tex2D(_MainTex, i.uv);

        // 拡散反射光
        float diffuse = saturate(dot(i.normal, L));

        // 鏡面反射光
        float specular = pow(saturate(dot(i.viewDir, R)), _SpecularGloss);

        // ディレクショナルライト + 環境光
        return ((diffuse + specular) * _LightColor0 + _AmbientColor) * texColor;
    }
    ENDCG
}

実装2 : Point Light

疑似コード実装(GLSL)

ポイントライトによるライティングを、ShaderLabを使わずに実装した場合、以下のようになります。

vec3 pointLightNPR(vec3 position, vec3 viewDir, vec3 normal, vec3 worldLightPos) 
{
    // サーフェスを基準としたライトの位置
    vec3 lightPos = worldLightPos - position;

    // ライトベクトル
    vec3 light = normalize(lightPos);

    // 光の減衰
    float atten = pow(length(lightPos) + 1, -2.0); 

    // 拡散反射光
    float diffuse = dot(light, normal) * atten;
    diffuse = saturate(diffuse);

    // 鏡面反射光
    vec3 refl = reflect(-light, normal); // 反射ベクトル
    float specular = saturate(dot(viewDir, refl)); // 視線ベクトルと反射ベクトルの内積
    specular = pow(specular, 80.0) * atten;

    // 光の計算 (拡散反射光 + 鏡面反射光 + 環境光)
    return diffuse * DIFFUSE_COLOR + specular * SPECULAR_COLOR + AMBIENT_COLOR;
}

Point Light と Directional Light の違い

Point Light のライティングでは、光の距離減衰attenを計算に組み込んでいるところがDirectinal Lightとは異なる点です。
Point Lightは、光源からサーフェスまでの距離に応じて光が減衰します。

    // 拡散反射光
-   float diffuse = dot(light, normal);
+   float diffuse = dot(light, normal) * atten;
    diffuse = saturate(diffuse);

    // 鏡面反射光
    vec3 refl = reflect(-light, normal); // 反射ベクトル
    float specular = saturate(dot(viewDir, refl)); // 視線ベクトルと反射ベクトルの内積
-   specular = pow(specular, 80.0);
+   specular = pow(specular, 80.0) * atten;

    // ライティングの合成
    return diffuse * DIFFUSE_COLOR + specular * SPECULAR_COLOR + AMBIENT_COLOR;

ShaderLabによる実装 (Point Light)

光の距離減衰

ポイントライトをシーンに配置すると、光源から遠い部分ほど光が弱くなります。
UNITY_LIGHT_ATTENUATION を使う方法や、光源とサーフェス間の距離を利用して自前計算する方法があります。

光の減衰 結果
UNITY_LIGHT_ATTENUATION
距離の逆二乗 \dfrac{1}{(1+D)^2}
指数関数 \exp(-D)

ShaderLabによる実装

Point Light のライティングは、ForwardAdd パスにて実装します。
ForwardAddパスでは、Point Lightの色を _LightColor0 で取得でき、
Point Lightのワールド座標を _WorldSpaceLightPos0.xyz で取得できます。

光の減衰は UNITY_LIGHT_ATTENUATION マクロで取得できます。

Pass
{
    Tags { "LightMode" = "ForwardAdd" }
    
    Blend One One // 加算ブレンド

    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag
    #pragma multi_compile_fwdadd_fullshadows

    #include "UnityCG.cginc"
    #include "Lighting.cginc"
    #include "AutoLight.cginc"

    // CPUからGPUに渡すデータ
    struct appdata
    {
        float4 vertex : POSITION;
        float2 uv : TEXCOORD0;
        float3 normal : NORMAL;
    };

    // vertからfragに渡すデータ
    struct v2f
    {
        float2 uv : TEXCOORD0;
        float4 vertex : SV_POSITION;
        float3 normal : TEXCOORD1;
        float3 viewDir : TEXCOORD2;
        float3 worldPos : TEXCOORD3;
    };

    sampler2D _MainTex;
    float4 _MainTex_ST;
    float _SpecularGloss; // 鏡面反射 光沢度
    half4 _AmbientColor; // 環境光 カラー

    v2f vert (appdata v)
    {
        v2f o;

        // MVP座標変換
        o.vertex = UnityObjectToClipPos(v.vertex);

        // UV座標のタイリング・オフセット計算
        o.uv = TRANSFORM_TEX(v.uv, _MainTex);

        // ワールド空間のNormal
        o.normal = UnityObjectToWorldNormal(v.normal);

        // ワールド空間のViewベクトル(サーフェスからみたカメラ位置)
        o.viewDir = normalize(_WorldSpaceCameraPos - mul(unity_ObjectToWorld, v.vertex));

        // ワールド空間の座標
        o.worldPos = mul(unity_ObjectToWorld, v.vertex);

        return o;
    }

    float4 frag (v2f i) : SV_Target
    {
        UNITY_LIGHT_ATTENUATION(attenuation, i, i.worldPos);
        
        // ライトベクトル
        #define L normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz)

        // 反射ベクトル
        #define R reflect(-L, i.normal)

        fixed4 texColor = tex2D(_MainTex, i.uv);

        // 拡散反射光
        float diffuse = saturate(dot(i.normal, L));

        // 鏡面反射光
        float specular = pow(saturate(dot(i.viewDir, R)), _SpecularGloss);

        return (diffuse + specular) * texColor * _LightColor0 * attenuation;
    }
    ENDCG
}

光の減衰の取得

UNITY_LIGHT_ATTENUATION マクロを利用することで、光の減衰を取得できます。

UNITY_LIGHT_ATTENUATION(attenuation, i, i.worldPos);

UNITY_LIGHT_ATTENUATION マクロを利用するためには、以下の一行を記述する必要があります。

    #pragma multi_compile_fwdadd_fullshadows

AutoLight.cginc をインクルードする必要もあります。

Point Light と Directional Light の実装の違い

Pass
{
-   Tags { "LightMode" = "ForwardBase" }
+   Tags { "LightMode" = "ForwardAdd" }
    
    Blend One One // 加算ブレンド

    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag
+   #pragma multi_compile_fwdadd_fullshadows

    #include "UnityCG.cginc"
    #include "Lighting.cginc"
+   #include "AutoLight.cginc"

    // CPUからGPUに渡すデータ
    struct appdata
    {
        float4 vertex : POSITION;
        float2 uv : TEXCOORD0;
        float3 normal : NORMAL;
    };

    // vertからfragに渡すデータ
    struct v2f
    {
        float2 uv : TEXCOORD0;
        float4 vertex : SV_POSITION;
        float3 normal : TEXCOORD1;
        float3 viewDir : TEXCOORD2;
+       float3 worldPos : TEXCOORD3;
    };

    sampler2D _MainTex;
    float4 _MainTex_ST;
    float _SpecularGloss; // 鏡面反射 光沢度
    half4 _AmbientColor; // 環境光 カラー

    v2f vert (appdata v)
    {
        v2f o;

        // MVP座標変換
        o.vertex = UnityObjectToClipPos(v.vertex);

        // UV座標のタイリング・オフセット計算
        o.uv = TRANSFORM_TEX(v.uv, _MainTex);

        // ワールド空間のNormal
        o.normal = UnityObjectToWorldNormal(v.normal);

        // ワールド空間のViewベクトル(サーフェスからみたカメラ位置)
        o.viewDir = normalize(_WorldSpaceCameraPos - mul(unity_ObjectToWorld, v.vertex));

+       // ワールド空間の座標
+       o.worldPos = mul(unity_ObjectToWorld, v.vertex);

        return o;
    }

    float4 frag (v2f i) : SV_Target
    {
+       UNITY_LIGHT_ATTENUATION(attenuation, i, i.worldPos);
        
        // ライトベクトル
-       #define L _WorldSpaceLightPos0.xyz
+       #define L normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz)

        // 反射ベクトル
        #define R reflect(-L, i.normal)

        fixed4 texColor = tex2D(_MainTex, i.uv);

        // 拡散反射光
        float diffuse = saturate(dot(i.normal, L));

        // 鏡面反射光
        float specular = pow(saturate(dot(i.viewDir, R)), _SpecularGloss);

-       return ((diffuse + specular) * _LightColor0 + _AmbientColor) * texColor;
+       return (diffuse + specular) * texColor * _LightColor0 * attenuation;
    }
    ENDCG
}

シェーダー全体

Shader "Day3/Pracitce01"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _SpecularGloss ("Specular Gloss", Float) = 50.0
        _AmbientColor ("Ambient Color", Color) = (0.3, 0.3, 0.3, 1) // 環境 カラー
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100
        
        Pass
        {
            Tags { "LightMode" = "ForwardBase" }
            Blend One Zero

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
            #include "Lighting.cginc"

            // CPUからGPUに渡すデータ
            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                float3 normal : NORMAL;
            };

            // vertからfragに渡すデータ
            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
                float3 normal : TEXCOORD1;
                float3 viewDir : TEXCOORD2;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float _SpecularGloss; // 鏡面反射 光沢度
            half4 _AmbientColor; // 環境光 カラー

            v2f vert (appdata v)
            {
                v2f o;

                // MVP座標変換
                o.vertex = UnityObjectToClipPos(v.vertex);

                // UV座標のタイリング・オフセット計算
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);

                // ワールド空間のNormal
                o.normal = UnityObjectToWorldNormal(v.normal);

                // ワールド空間のViewベクトル(サーフェスからみたカメラ位置)
                o.viewDir = normalize(_WorldSpaceCameraPos - mul(unity_ObjectToWorld, v.vertex));

                return o;
            }

            float4 frag (v2f i) : SV_Target
            {
                // ライトベクトル
                #define L _WorldSpaceLightPos0.xyz

                // 反射ベクトル
                #define R reflect(-L, i.normal)

                fixed4 texColor = tex2D(_MainTex, i.uv);

                // 拡散反射光
                float diffuse = saturate(dot(i.normal, L));

                // 鏡面反射光
                float specular = pow(saturate(dot(i.viewDir, R)), _SpecularGloss);

                return ((diffuse + specular) * _LightColor0 + _AmbientColor) * texColor;
            }
            ENDCG
        }
        
        Pass
        {
            Tags { "LightMode" = "ForwardAdd" }
            
            Blend One One // 加算ブレンド

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_fwdadd_fullshadows

            #include "UnityCG.cginc"
            #include "Lighting.cginc"
            #include "AutoLight.cginc"

            // CPUからGPUに渡すデータ
            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                float3 normal : NORMAL;
            };

            // vertからfragに渡すデータ
            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
                float3 normal : TEXCOORD1;
                float3 viewDir : TEXCOORD2;
                float3 worldPos : TEXCOORD3;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float _SpecularGloss; // 鏡面反射 光沢度
            half4 _AmbientColor; // 環境光 カラー

            v2f vert (appdata v)
            {
                v2f o;

                // MVP座標変換
                o.vertex = UnityObjectToClipPos(v.vertex);

                // UV座標のタイリング・オフセット計算
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);

                // ワールド空間のNormal
                o.normal = UnityObjectToWorldNormal(v.normal);

                // ワールド空間のViewベクトル(サーフェスからみたカメラ位置)
                o.viewDir = normalize(_WorldSpaceCameraPos - mul(unity_ObjectToWorld, v.vertex));

                // ワールド空間の座標
                o.worldPos = mul(unity_ObjectToWorld, v.vertex);

                return o;
            }

            float4 frag (v2f i) : SV_Target
            {
                UNITY_LIGHT_ATTENUATION(attenuation, i, i.worldPos);
                
                // ライトベクトル
                #define L normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz)

                // 反射ベクトル
                #define R reflect(-L, i.normal)

                fixed4 texColor = tex2D(_MainTex, i.uv);

                // 拡散反射光
                float diffuse = saturate(dot(i.normal, L));

                // 鏡面反射光
                float specular = pow(saturate(dot(i.viewDir, R)), _SpecularGloss);

                return (diffuse + specular) * texColor * _LightColor0 * attenuation;
            }
            ENDCG
        }
    }
}

Discussion