【Unity】【シェーダ】Forward Renderingで複数のライトを取り扱う

f:id:halya_11:20180711135016p:plain

ForwardRenderingで複数ライトを取り扱うときの挙動やシェーダの書き方をまとめました。

ライトの情報を取得できる2つのパス

Forward Renderingでライトの影響を受けるシェーダを書くとき、パスに次のようなTagを書きます。

Tags { "LightMode"="ForwardBase" }

これを書くことにより、Unityがマテリアルにライトの情報を渡してくれてそれをシェーダで取り扱えるようになります。

単純にディレクショナルライトの影響を受けるだけならLightModeがForwardBaseなパスのみを記述すれば十分ですが、
シーンに二つ以上のライトがある場合は2パス目として下記のTagを記述したパスを追加する必要があります。

Tags { "LightMode"="ForwardAdd" }

Unityで使われるライトの情報はこの二つのパスのいずれかに受け渡されます。

ライトの計算方法の種類

前節のようにして受け渡されたライトの情報はシェーダ内で計算されますが、
ライトの種類や重要度に応じて下記のいずれかの計算方法が選択されます。

  1. ForwardBaseパスのフラグメントシェーダで計算
  2. ForwardAddパスで計算
  3. ForwardBaseの頂点シェーダで計算
  4. ForwardBaseの頂点シェーダで球面調和を使って計算

振り分けられ方としては、まずシーンにあるライトのうち一番重要度の大きいディレクショナルライトが1.で計算されます。
それ以外のライトについては、重要度が高い順に前節に2、3、4に振り分けられていきます。

この振り分けられ方は若干複雑ですが、マニュアルに詳しく書かれています。

https://docs.unity3d.com/jp/460/Manual/RenderTech-ForwardRendering.html

ライトの重要度

前節でライトの重要度という言葉を使っていますが、これは下記の要因によって決まります。

  • LightのRender Mode
  • その地点における物体を照らす明るさ

LightのRender Modeについては、Importantにすると2個目以降のライトは前節の2の計算方法で処理されます。
つまりライトを増やせば増やすほどフラグメントシェーダが高価になっていきます。

Render ModeをNot Importantにすると前節の3か4の計算方法で処理されます。
そのうえで、明るさに応じて重要度が高いものを3で処理し、低いものは4で処理します。

Render ModeがAutoの場合は明るさに応じて重要度を自動的に判断して計算方法を振り分けます。

複数ライトを取り扱うシェーダを書く

それでは複数ライトを取り扱うシェーダを書いてみます。

Shader "MultiLight"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        Pass
        {
            Tags { "LightMode"="ForwardBase" }

            CGPROGRAM
           #pragma vertex vert
           #pragma fragment frag
            // VERTEXLIGHT_ONなどが定義されたバリアントが生成される
           #pragma multi_compile_fwdbase
            
           #include "UnityCG.cginc"
           #include "AutoLight.cginc"

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

            struct v2f
            {
                half4 pos : SV_POSITION;
                half2 uv : TEXCOORD0;
                half3 normal: TEXCOORD1;
                half3 ambient: TEXCOORD2;
                half3 worldPos: TEXCOORD3;
                LIGHTING_COORDS(4, 5)
            };

            sampler2D _MainTex;
            half4 _MainTex_ST;
            half4 _LightColor0;
            
            v2f vert (appdata v)
            {
                v2f o = (v2f)0;

                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
                o.normal = UnityObjectToWorldNormal(v.normal);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex);

                // UnityStandardCoreの処理を参考に
               #if UNITY_SHOULD_SAMPLE_SH

                   #if defined(VERTEXLIGHT_ON)

                        o.ambient = Shade4PointLights(
                            unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,
                            unity_LightColor[0].rgb, unity_LightColor[1].rgb,
                            unity_LightColor[2].rgb, unity_LightColor[3].rgb,
                            unity_4LightAtten0, o.worldPos, o.normal
                        );

                   #endif

                    o.ambient += max(0, ShadeSH9(float4(o.normal, 1)));
               #else

                o.ambient = 0;

               #endif
                
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                half4 col = tex2D(_MainTex, i.uv);

                // AutoLightに定義されているマクロで減衰を計算する
                UNITY_LIGHT_ATTENUATION(attenuation, i, i.normal);
                half3 diff = max(0, dot(i.normal, _WorldSpaceLightPos0.xyz)) * _LightColor0 * attenuation;
                col.rgb *= diff + i.ambient;
                return col;
            }
            ENDCG
        }

        Pass {

            Tags { "LightMode"="ForwardAdd" }

            Blend One One
            ZWrite Off
            
            CGPROGRAM
           #pragma vertex vert
           #pragma fragment frag
           #pragma multi_compile_fwdadd

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

            struct appdata 
            {
                half4 vertex : POSITION;
                half3 normal : NORMAL;
                half2 texcoord : TEXCOORD0;
            };
            
            struct v2f
            {
                half4 pos : SV_POSITION;
                half2 uv : TEXCOORD0;
                half3 normal: TEXCOORD1;
                half3 ambient: TEXCOORD2;
                half3 worldPos: TEXCOORD3;
                LIGHTING_COORDS(4, 5)
            };
            
            sampler2D _MainTex;
            half4 _MainTex_ST;
            half4 _LightColor0;

            v2f vert (appdata v) 
            {
                v2f o = (v2f)0;

                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
                o.normal = UnityObjectToWorldNormal(v.normal);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex);
                
                return o;
            }

            half4 frag(v2f i) : COLOR 
            {
                half4 col = tex2D(_MainTex, i.uv);

                // _WorldSpaceLightPos0.wはディレクショナルライトだったら0、それ以外は1となる
                half3 lightDir;
                if (_WorldSpaceLightPos0.w > 0) {
                    lightDir = _WorldSpaceLightPos0.xyz - i.worldPos.xyz;
                } else {
                    lightDir = _WorldSpaceLightPos0.xyz;
                }
                lightDir = normalize(lightDir);

                UNITY_LIGHT_ATTENUATION(attenuation, i, i.normal);
                half3 diff = max(0, dot(i.normal, lightDir)) * _LightColor0 * attenuation;
                col.rgb *= diff;
                return col;
            }
            ENDCG
        }
    }
}

説明が必要そうな部分はコメントを書いていますが、下記でいくつか補足します。

まずForwardBaseパスの#if UNITY_SHOULD_SAMPLE_SH~#endifは頂点ライティング・球面調和ライティングをしている部分です。
重要度の低いライトの情報がここに入ってきます。
処理内容はUnityStandardCoreを参考にしつつ、ShadeSHPerVertexだけUnityStandardUtilsに依存してしまうのでShadeSH9に置き換えました。

また、ForwardAddパスでライトの情報をとってきていますが、ディレクショナルライトとその他のライトでライトベクトルのとり方を変える必要があります。
ディレクショナルライトであるかどうかは_WorldSpaceLightPos0.wに0が入っているかどうかで判定できる決まりがあるので、これを使って判定しています。

docs.unity3d.com

通化

一部をcgincludeファイルに書き出すことで2パスの処理を共通化できます。
まずcgincludeファイルは次のように書きます。

MultiLight.cginc

#if !defined(MULTILIGHT_INCLUDED)
#define MULTILIGHT_INCLUDED
#endif

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

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

struct v2f
{
    half4 pos : SV_POSITION;
    half2 uv : TEXCOORD0;
    half3 normal: TEXCOORD1;
    half3 ambient: TEXCOORD2;
    half3 worldPos: TEXCOORD3;
    LIGHTING_COORDS(4, 5)
};


sampler2D _MainTex;
half4 _MainTex_ST;
half4 _LightColor0;

v2f vert (appdata v)
{
    v2f o = (v2f)0;

    o.pos = UnityObjectToClipPos(v.vertex);
    o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
    o.normal = UnityObjectToWorldNormal(v.normal);
    o.worldPos = mul(unity_ObjectToWorld, v.vertex);

   #if UNITY_SHOULD_SAMPLE_SH

       #if defined(VERTEXLIGHT_ON)

            o.ambient = Shade4PointLights(
                unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,
                unity_LightColor[0].rgb, unity_LightColor[1].rgb,
                unity_LightColor[2].rgb, unity_LightColor[3].rgb,
                unity_4LightAtten0, o.worldPos, o.normal
            );

       #endif

        o.ambient += max(0, ShadeSH9(float4(o.normal, 1)));
   #else

    o.ambient = 0;

   #endif
    
    return o;
}

half4 frag(v2f i) : COLOR 
{
    half4 col = tex2D(_MainTex, i.uv);

    half3 lightDir;
    if (_WorldSpaceLightPos0.w > 0) {
        lightDir = _WorldSpaceLightPos0.xyz - i.worldPos.xyz;
    } else {
        lightDir = _WorldSpaceLightPos0.xyz;
    }
    lightDir = normalize(lightDir);

    UNITY_LIGHT_ATTENUATION(attenuation, 0, i.normal);
    half3 diff = max(0, dot(i.normal, lightDir)) * _LightColor0 * attenuation;
    col.rgb *= diff + i.ambient;

    return col;
}

次にシェーダを次のように変更します。

MultiLight.shader

Shader "MultiLight"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        Pass
        {
            Tags { "LightMode"="ForwardBase" }

            CGPROGRAM
           #pragma vertex vert
           #pragma fragment frag
           #pragma multi_compile_fwdbase

           #include "MultiLight.cginc"
            ENDCG
        }

        Pass {

            Tags { "LightMode"="ForwardAdd" }

            Blend One One
            ZWrite Off
            
            CGPROGRAM
           #pragma vertex vert
           #pragma fragment frag
           #pragma multi_compile_fwdadd

           #include "MultiLight.cginc"
            ENDCG
        }
    }
}

これで共通化されました。

結果

レンダリング結果は次のようになります。

f:id:halya_11:20180711135016p:plain

複数のライトが無事反映されました。

参考

docs.unity3d.com

docs.unity3d.com

qiita.com

esprog.hatenablog.com