だんごのUnity開発メモ

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

【Unity SRP】鏡面反射の実装

はじめに

前回は不透明オブジェクトに拡散反射を実装しました。
今回は前回のモデルに鏡面反射を実装していきます。

s-dango.hatenablog.jp

目次

反射モデルとは

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

鏡面反射光

物体の光沢やハイライトを表現することができます。

特徴

  • 物体の色の影響を受けず、光源と同じ色のまま反射する
  • 視点によって見える位置や大きさが変化する
  • 正反射方向に近づけば近づくほど、光が強くなる

フォン反射モデル

鏡面反射の光の挙動を表現するためのモデルです。
「光源の正反射ベクトル」と「視点ベクトル」との内積で算出することができます。

フォン反射モデルの式

N・・・正規化法線ベクトル
L・・・光源ベクトル
R・・・正反射ベクトル
V・・・視点ベクトル
k_s・・・鏡面反射係数(鏡面反射の反射率)
i_s・・・鏡面反射成分(光源の明るさ)
a・・・粗さ係数(物体の光沢度)


\begin{align*}
&R = 2 \times N \times \bigl( N \cdot L \bigr) - L \\
&I = k_s \times i_s \times \bigl( R \cdot V \bigr) ^ 2
\end{align*}

この視点方向と正反射方向が一致するときに明るさが最大になります。
光沢度を表す$a$の値が大きくなるにつれて、光沢の明るさも減少していきます。
f:id:s-dango:20220122194631j:plain:h300

実行サンプル

左:鏡面反射なし、右:フォン反射モデル f:id:s-dango:20220122194751p:plain:h300

カメラ情報の設定

今回はシェーダー側でカメラの方向ベクトルを使用するため、C#側からシェーダーへカメラベクトルを渡す必要があります。

前回のコードにシェーダー側へ渡すための処理を追加していきます。
※コードは URP の実装を参考にしています。

/// <summary>
/// 単一カメラごとのレンダリング
/// </summary>
/// <param name="context"></param>
/// <param name="camera"></param>
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);

        // カメラの情報をシェーダー側に渡す
        SetPerCameraShaderVariables(cmd, camera);

        // ライト情報の設定
        SetupLights(context, cullResults);

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

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

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

    context.Submit();
}

/// <summary>
/// シェーダーに渡すカメラ情報の設定
/// </summary>
/// <param name="camera"></param>
void SetPerCameraShaderVariables(CommandBuffer cmd, Camera camera)
{
    cmd.SetGlobalVector("_WorldSpaceCameraPos", camera.transform.position);
}

SetPerCameraShaderVariables でカメラデータを設定しています。
今回はカメラの位置情報のみシェーダー側に渡しています。

cmd.SetGlobalVector("_WorldSpaceCameraPos", camera.transform.position);

シェーダーの実装

シェーダーではC#側で設定した _WorldSpaceCameraPos を使用して鏡面反射を実装しています。

前回のコードに鏡面反射の処理を追加します。
※コードは URP の実装を参考にしています。

Shader "Custom/Demo02_Specular_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
        _SpecularBrightness("Specular Brightness", Float) = 2.0

        // 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;
                float3 viewDir : TEXCOORD2;
            };

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

            float4x4 unity_ObjectToWorld;
            float4x4 unity_MatrixVP;

            float4 _MainLightPos;
            half4 _MainLightColor;

            float4 _WorldSpaceCameraPos;

            float _SpecularBrightness;

            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);
            }

            float3 GetWorldSpaceViewDir(float3 positionWS) 
            {
                return _WorldSpaceCameraPos.xyz - positionWS;
            }

            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);
                o.viewDir = GetWorldSpaceViewDir(positionWS);

                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 = saturate(dot(normal, lightDir));

                // 鏡面反射(フォン反射モデル)
                half3 viewDir = normalize(i.viewDir);
                half3 r = 2.0 * normal * dot(normal, lightDir) - lightDir;
                half specular = pow(saturate(dot(r, viewDir)), _SpecularBrightness);
                color.rgb *= dotNL + specular;

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

光源方向の正反射ベクトルと視点ベクトルの内積を算出したのち、光沢度を表す _SpecularBrightness でべき乗することで、鏡面反射を表現しています。
そして、最終的に拡散反射光と合わせることで最終的なサーフェスの色としています。

// 鏡面反射(フォン反射モデル)
half3 viewDir = normalize(i.viewDir);
half3 r = 2.0 * normal * dot(normal, lightDir) - lightDir;
half specular = pow(saturate(dot(r, viewDir)), _SpecularBrightness);
color.rgb *= dotNL + specular;

ブリン-フォン反射モデル

フォン反射モデルは正反射ベクトルを計算するのに複数の内積や乗算を行うため、処理負荷を減らすために考え出されたモデルです。
「光源ベクトル」と「視点ベクトル」の中間ベクトルである「ハーフベクトル」と「法線ベクトル」との内積で算出することができます。

ブリン-フォン反射モデルの式

N・・・正規化法線ベクトル
L・・・光源ベクトル
V・・・視点ベクトル
H・・・ハーフベクトル
a・・・粗さ係数(物体の光沢度)


\begin{align*}
&H=\frac{L+V}{|L+V|}\\
&I=\bigl( N \cdot H \bigr)^a
\end{align*}

正反射ベクトルを求めるフォン反射モデルとは違い、精度は落ちてしまいますが、その代わりに処理負荷が軽減されます。
f:id:s-dango:20220122195752j:plain:h300

以下は実装部分になります。
フォン反射モデルのコードがブリン-フォン反射モデルに置き換わっています。

// 鏡面反射(ブリン・フォン反射モデル)
half3 viewDir = normalize(i.viewDir);
half3 halfLV = normalize(lightDir + viewDir);
half specular = pow(saturate(dot(normal, halfLV)), _SpecularBrightness);
color.rgb *= dotNL + specular;
実行サンプル

左:鏡面反射無し 中央:ブリン-フォン反射モデル 右:フォン反射モデル
f:id:s-dango:20220122195846p:plain:h300

おわりに

今回は反射モデルの基本である、鏡面反射モデルをSRPで実装しました。
ここまでみていただいてありがとうございます。