だんごのUnity開発メモ

Unityでの開発や勉強内容をまとめています!

【Unity SRP】ランバート拡散反射モデルの実装

はじめに

前回実装した不透明オブジェクトに陰影をつけていきます。
s-dango.hatenablog.jp

最終的な表示画面
f:id:s-dango:20211212181118p:plain:h300

頻出する語句

ベクトル・・・・・・・向きと大きさを持つ量(速度や力など)
ベクトルの内積・・・・2つのベクトルにの計算によって得られる値
ベクトルの正規化・・・ベクトルの方向を維持しつつ大きさを1にすること
法線・・・・・・・・・3Dモデルにおいて、表面の一点に対して垂直なベクトル
陰影・・・・・・・・・光が当たらずにできる暗い部分(物体の裏側など、光が十分に届かずにできる陰と、他の物体に遮られてできる影がある)

目次

反射モデルとは

3Dグラフィックスにおいて、現実世界の光の挙動を表現するための計算式のことです。
モデリングされた物体のサーフェス(表面)の一点に陰影つけるために使用します。

拡散反射光

物体の陰を表現することができます。

特徴
  • 光源から向きが離れていくほど暗くなっていく
  • 視点によって色や大きさは変化しない
ランバート反射モデル

拡散反射光の挙動を表現するための計算モデルの一つです。
サーフェスの「法線ベクトル」と「光源方向ベクトル」との内積で算出することができます。

ランバート反射モデルの式

N・・・正規化法線ベクトル
L・・・光源ベクトル
C・・・拡散反射光の色
I_l・・・入射光の輝度


\begin{align*}
&N \cdot L = |N||L|cos\alpha \\
&I = \bigl( N \cdot L \bigr) \times C \times I_l
\end{align*}

最終的に正規化法線ベクトルと光源ベクトルのなす角θが算出され、この値がサーフェスの輝度となります。
\theta が 0.0 になればなるほど、サーフェスの輝度が 1.0 に近づいていきます。
f:id:s-dango:20211212235740j:plain:h300

実行サンプル

左がライティング無し、右がランバート反射を適用したモデルです。
f:id:s-dango:20211212235959p:plain:h300

ライトの実装

ランバート拡散反射を実装するためには、まずライトの方向ベクトルを取得する必要があります。
以下の実装では、Hierarchy内で一番明るいディレクショナルライトをメインライトとして、方向ベクトルをシェーダー側に渡しています。

前回のC#コードに追記する形になります。
※コードはURPの実装を参考にしています。

void RenderSingleCamera(ScriptableRenderContext context, Camera camera)
{
    ScriptableCullingParameters cullParams;
    if (camera.TryGetCullingParameters(out cullParams) == false)
    {
        return;
    }

    CommandBuffer cmd = CommandBufferPool.Get();

    ProfilingSampler sampler = new ProfilingSampler($"{nameof(BasicRenderPipeline)}.{nameof(RenderSingleCamera)}.{camera.name}");
    using (new ProfilingScope(cmd, sampler))
    {
        context.ExecuteCommandBuffer(cmd);
        cmd.Clear();

        // カメラプロパティ設定
        context.SetupCameraProperties(camera, false);

        // カリング設定
        CullingResults cullResults = context.Cull(ref cullParams);

        // ライト情報の設定 <- ここでライトのセットアップを行っています
        SetupLights(context, cullResults);

        // 不透明オブジェクト描画
        DrawObjectsOpaque(context, camera, cullResults);

        // Skybox描画
        context.DrawSkybox(camera);
    }

    context.ExecuteCommandBuffer(cmd);
    CommandBufferPool.Release(cmd);

    context.Submit();
}
void SetupLights(ScriptableRenderContext context, CullingResults cullResults)
{
    CommandBuffer cmd = CommandBufferPool.Get();

    ProfilingSampler sampler = new ProfilingSampler("Setup Light Contents");
    using (new ProfilingScope(cmd, sampler))
    {
        NativeArray<VisibleLight> visibleLights = cullResults.visibleLights;

        // メインライト検索
        int lightIndex = -1;
        float brightestLightIntensity = 0f;
        for (int i = 0; i < visibleLights.Length; i++)
        {
            VisibleLight visibleLight = visibleLights[i];
            Light light = visibleLight.light;

            if (light == null)
            {
                break;
            }

            if (visibleLight.lightType == LightType.Directional)
            {
                // 最も明るいディレクショナルライトを使用
                if (light.intensity > brightestLightIntensity)
                {
                    brightestLightIntensity = light.intensity;
                    lightIndex = i;
                }
            }
        }

        // シェーダーに渡すライトデータの作成
        if (lightIndex < 0)
        {
            return;
        }

        VisibleLight mainVisibleLight = visibleLights[lightIndex];
        Vector4 dir = -mainVisibleLight.localToWorldMatrix.GetColumn(2);
        Vector4 lightPos = new Vector4(dir.x, dir.y, dir.z, 0);

        cmd.SetGlobalVector("_MainLightPos", lightPos);
    }

    context.ExecuteCommandBuffer(cmd);
    CommandBufferPool.Release(cmd);
}

シェーダーの実装

シェーダー側では、C#コード側で登録した _MainLightsPos プロパティを使用して、ランバート拡散反射を実装しています。
前回のシェーダーコードに追記する形になります。
※コードはURPの実装を参考にしています。

Shader "Custom/Basic"
{
    Properties
    {
        [MainTexture] _BaseMap("Base Map", 2D) = "white" {}
        [MainColor] _BaseColor("Base Color", Color) = (1, 1, 1, 1)
        _Cutoff("Alpha Cutout", Range(0.0, 1.0)) = 0.5

        // ObsoleteProperties
        [HideInInspector] _MainTex("Texture", 2D) = "white" {}
        [HideInInspector] _Color("Color", Color) = (1, 1, 1, 1)
    }
    SubShader
    {
        Tags { "RenderType" = "Opaque" }
        LOD 100

        Pass
        {
            Name "BasicPass"
            Tags { "LightMode" = "BasicPass" }

            HLSLPROGRAM
            #pragma exclude_renderers gles gles3 glcore
            #pragma target 4.5

            #pragma vertex vert
            #pragma fragment frag

            #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"

            struct Attributes
            {
                float4 positionOS : POSITION;
                float2 uv : TEXCOORD0;
                float3 normal : NORMAL;
            };

            struct Varyings
            {
                float4 vertex : SV_POSITION;
                float2 uv : TEXCOORD0;
                float3 normal : TEXCOORD1;
            };

            TEXTURE2D(_BaseMap);
            SAMPLER(sampler_BaseMap);
            float4 _BaseMap_ST;
            float4 _BaseColor;
            float _Cutoff;

            float4x4 unity_ObjectToWorld;
            float4x4 unity_MatrixVP;

            float4 _MainLightPos;   // C#側で登録したプロパティを使用しています

            float3 TransformObjectToWorld(float3 positionOS)
            {
                return mul(unity_ObjectToWorld, float4(positionOS, 1.0)).xyz;
            }

            float4 TransformWorldToHClip(float3 positionWS)
            {
                return mul(unity_MatrixVP, float4(positionWS, 1.0));
            }

            float3 TransformObjectToWorldNormal(float3 normalOS) 
            {
                return mul(normalOS, (float3x3)unity_ObjectToWorld);
            }

            Varyings vert(Attributes i)
            {
                Varyings o = (Varyings)0;

                float3 positionWS = TransformObjectToWorld(i.positionOS.xyz);
                float4 positionCS = TransformWorldToHClip(positionWS);
                o.vertex = positionCS;
                o.uv = TRANSFORM_TEX(i.uv, _BaseMap);
                o.normal = TransformObjectToWorldNormal(i.normal);

                return o;
            }

            half4 frag(Varyings i) : SV_Target
            {
                float2 uv = i.uv;
                float4 texColor = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, uv);
                float3 color = texColor.rgb * _BaseColor.rgb;
                half alpha = texColor.a * _BaseColor.a;
                
                // 正規化法線ベクトル・ライト方向ベクトルを使用して、ランバート拡散反射を実装しています
                half3 lightDir = normalize(_MainLightPos.xyz);
                half3 normal = normalize(i.normal);
                half dotNL = max(0, dot(normal, lightDir));
                color.rgb *= dotNL;

                return half4(color, alpha);
            }
            ENDHLSL
        }
    }
}

正規化法線ベクトル・ライト方向ベクトルを内積して、その算出結果(0.0~1.0)をサーフェスの輝度としています。

half3 lightDir = normalize(_MainLightPos.xyz);
half3 normal = normalize(i.normal);
half dotNL = max(0, dot(normal, lightDir));
color.rgb *= dotNL;

終結

シェーダーを設定したマテリアルをオブジェクトに適用すると、以下のような表示になると思います。
f:id:s-dango:20211212181118p:plain:h300

おわりに

今回は反射モデルの基本である、ランバート拡散反射モデルをSRPで実装しました。
ここまでみていただいてありがとうございます。
なにか間違い等あればご指摘いただけると嬉しいです。