知識0からのUnityShader勉強

知識0からのUnityShader勉強

UnityのShaderをメインとして、0から学んでいくブログです。

【UnityShader】ライティングの基礎シェーダー #115

はじめに

今回は複数ライトに対応した基本的なシェーダーについて勉強します。

シェーダー

Shader "Day3/Practice1"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _SpecularPow ("Speclar Pow", float) = 5
    }

    SubShader
    {
        Tags { "RenderType"="Opaque"}
        LOD 100

        CGINCLUDE

        sampler2D _MainTex;
        float4 _MainTex_ST;
        // ライトの色を取得する
        half4 _LightColor0;

        #pragma vertex vert
        #pragma fragment frag

        #include "UnityCG.cginc"

        struct appdata
        {
            float4 vertex : POSITION;
            float2 uv : TEXCOORD0;
            half3 normal : NORMAL;
        };

        ENDCG

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

            CGPROGRAM

            struct v2f
            {
                float2 uv : TEXCOORD0;
                half3 normal : NORMAL;
                float3 viewDir : TEXCOORD1;
                float3 lightDir : TEXCOORD2;
                float4 vertex : SV_POSITION;
            };

            half _SpecularPow;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                float4 worldPos = mul(unity_ObjectToWorld, v.vertex);
                o.viewDir = normalize(_WorldSpaceCameraPos - worldPos.xyz);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                float isDirectional = step(1, _WorldSpaceLightPos0.w);
                o.lightDir = normalize(_WorldSpaceLightPos0.xyz - (worldPos.xyz * isDirectional));

                o.normal = UnityObjectToWorldNormal(v.normal);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 texCol = tex2D(_MainTex, i.uv);

                // 拡散反射
                float3 diffuse = saturate(dot(i.normal, i.lightDir)) * _LightColor0;

                // 鏡面反射
                float3 reflectVec = reflect(-i.lightDir, i.normal);
                float specular = pow(saturate(dot(reflectVec, i.viewDir)), _SpecularPow);

                // 環境光
                float3 ambient = ShadeSH9(float4(i.normal, 1));

                return fixed4(ambient, 1);

                fixed4 col = fixed4(texCol.rgb * (ambient + diffuse) + specular, texCol.a);

                return col;
            }
            ENDCG
        }

        Pass
        {
            Tags { "LightMode"="ForwardAdd" }
            Blend One One

            CGPROGRAM

            struct v2f
            {
                float2 uv : TEXCOORD0;
                half3 normal : NORMAL;
                float3 lightDir : TEXCOORD1;
                float4 vertex : SV_POSITION;
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                float4 worldPos = mul(unity_ObjectToWorld, v.vertex);
                float isDirectional = step(1, _WorldSpaceLightPos0.w);
                o.lightDir = normalize(_WorldSpaceLightPos0.xyz - (worldPos.xyz * isDirectional));

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

                o.normal = UnityObjectToWorldNormal(v.normal);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 texCol = tex2D(_MainTex, i.uv);
                float3 lightCol = saturate(dot(i.normal, i.lightDir)) * _LightColor0;

                return fixed4(texCol.rgb * lightCol, 1);
            }
            ENDCG
        }
    }
}

拡散反射光

非金属表面付近で起きる光の反射のうち、界面で発生する鏡面反射を除いた成分のことである

拡散反射 - Wikipedia:より引用

つまり、光があたっている部分を明るく、あたっていない部分は暗くなるものになります。

以下の画像のdiffuse reflectionがそれにあたります。

https://upload.wikimedia.org/wikipedia/commons/thumb/b/bd/Lambert2.gif/440px-Lambert2.gif

拡散反射 - Wikipedia:より引用

求め方

光が一番当たる部分は、光の向きとオブジェクトの法線が逆ベクトルに
当たらない部分は垂直、もしくはそれ以上と考えれば求められそうです。

以下の画像のようにイメージして頂ければわかりやすいかと思います。

https://cdn-ak.f.st-hatena.com/images/fotolife/n/nn_hokuson/20161101/20161101195523.jpg

【Unityシェーダ入門】ランバート拡散照明モデルを試す - おもちゃラボ:より引用

つまり、光の向きと法線の内積を求めることで光の強さを計算することができます。
それをコードに起こしたものが以下になります。

// vertex
float isDirectional = step(1, _WorldSpaceLightPos0.w);
o.lightDir = normalize(_WorldSpaceLightPos0.xyz - (worldPos.xyz * isDirectional));


// fragment
float3 diffuse = saturate(dot(i.normal, i.lightDir)) * _LightColor0;

_WorldSpaceLightPos0

ディレクショナルライトとその他のライトで取得できるものが異なります。

ライト
ディレクショナルライト float4(ワールド空間の向き, 0)
その他のライト float4(ワールド空間の位置, 1)

その他のライトの場合は、位置が取得できるので向きベクトルへと変換しています。

鏡面反射光

二物質の界面において発生する反射である。鏡面反射では反射の法則が成り立ち、入射角と反射角を等しくする。

鏡面反射 - Wikipedia:より引用

つまり、どの程度光を反射させるかを示すものになります。

求め方

まずは、光が入ってくるベクトルの反射ベクトルを求めます。
そのベクトルと視線のベクトルの内積を求めることで、反射の強さを求めることができそうです。

以下の画像のようにイメージして頂ければわかりやすいかと思います。

https://cdn-ak.f.st-hatena.com/images/fotolife/n/nn_hokuson/20161102/20161102205834.png

【Unityシェーダ入門】フォン鏡面反射で金属っぽくしてみる - おもちゃラボ:より引用

コードに起こしたものが以下になります。

// vertex
o.viewDir = normalize(_WorldSpaceCameraPos - worldPos.xyz);

// fragment
float3 reflectVec = reflect(-i.lightDir, i.normal);
float specular = pow(saturate(dot(reflectVec, i.viewDir)), _SpecularPow);

reflectはhlslに定義済の反射ベクトルを返すものになります。
また、計算結果にpowをしているのは物体によって反射率が異なるからになります。

環境光

物体間や物体内部における光の反射が生み出す間接光を簡易的に表現したい場合に用いる。

アンビエント | CG用語辞典 | CGWORLD Entry.jp:より引用

とのこと。

求め方

0.3f辺りを加算する手法もありますが、今回はShadeSH9を使用します。

コードに起こしたものが以下になります。

float3 ambient = ShadeSH9(float4(i.normal, 1));

ShadeSH9

引数に法線を入れることで、環境光を取得するものになります。
wは1.0とコメントに記載してあります。

定義は以下となります。

// normal should be normalized, w=1.0
// output in active color space
half3 ShadeSH9 (half4 normal)
{
    // Linear + constant polynomial terms
    half3 res = SHEvalLinearL0L1 (normal);

    // Quadratic polynomials
    res += SHEvalLinearL2 (normal);

#   ifdef UNITY_COLORSPACE_GAMMA
        res = LinearToGammaSpace (res);
#   endif

    return res;
}

深堀り

ShadeSH9の中身を深堀りしてみましたが、結論から述べると理解できませんでした。

// normal should be normalized, w=1.0
half3 SHEvalLinearL0L1 (half4 normal)
{
    half3 x;

    // Linear (L1) + constant (L0) polynomial terms
    x.r = dot(unity_SHAr,normal);
    x.g = dot(unity_SHAg,normal);
    x.b = dot(unity_SHAb,normal);

    return x;
}

// normal should be normalized, w=1.0
half3 SHEvalLinearL2 (half4 normal)
{
    half3 x1, x2;
    // 4 of the quadratic (L2) polynomials
    half4 vB = normal.xyzz * normal.yzzx;
    x1.r = dot(unity_SHBr,vB);
    x1.g = dot(unity_SHBg,vB);
    x1.b = dot(unity_SHBb,vB);

    // Final (5th) quadratic (L2) polynomial
    half vC = normal.x*normal.x - normal.y*normal.y;
    x2 = unity_SHC.rgb * vC;

    return x1 + x2;
}

unity_SH系は、球面調和関数によって求められたGIのようで、それと法線情報をごにょごにょして環境光を求めているっぽいです。

zhuanlan.zhihu.com

使わなかったもの

UNITY_LIGHTMODEL_AMBIENTは公式にLegacy variableとあるので、使用しませんでした。

UNITY_LIGHTMODEL_AMBIENTをそのまま返したものは以下になります。

f:id:soramamenatan:20220324182413p:plain

各光の描画結果

拡散反射光

f:id:soramamenatan:20220324185641p:plain

鏡面反射光

f:id:soramamenatan:20220324185657p:plain

環境光

f:id:soramamenatan:20220324185713p:plain

上記3つを合わせたもの

f:id:soramamenatan:20220324185352p:plain

他のライトの実装

基本的には、拡散反射光と同様に法線とライトの向きの内積によって求めます。

float3 lightCol = saturate(dot(i.normal, i.lightDir)) * _LightColor0;

また、Unityにライトのパスを追加していることを伝えるためにForwardAddをTagsに追加します。
今回は、色を追加するのでBlendはOne Oneにしています。

Tags { "LightMode"="ForwardAdd" }
Blend One One

他のライトを追加した描画結果

ポイントライトを追加しました。

f:id:soramamenatan:20220324190931p:plain

参考サイト様

qiita.com

qiita.com