Unity Shaderにおける高光反射とBlinn-Phongモデルの実装

高光反射の基本原理

高光反射(スペキュラ)は、光源からの直射光が物体表面で鏡面的に反射され、視点方向に強く輝く現象をモデル化する。一般的な計算式は以下の通りである:

Specular = LightColor × pow(max(dot(ReflectDir, ViewDir), 0), Gloss)

ここで、ReflectDirは光の反射方向、ViewDirは視点方向、Glossは光沢度(シャープさ)を制御するパラメータである。

頂点シェーダによる高光反射の実装

まず、頂点シェーダで高光を計算する例を示す。この実装では、拡散反射(ディフューズ)、環境光(アンビエント)、および高光反射を組み合わせる。

Shader "Custom/SpecularVertex"
{
    Properties
    {
        _Diffuse ("Diffuse Color", Color) = (1,1,1,1)
        _Specular ("Specular Color", Color) = (1,1,1,1)
        _Gloss ("Glossiness", Range(8, 200)) = 50
    }
    SubShader
    {
        Pass
        {
            Tags { "LightMode" = "ForwardBase" }
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "Lighting.cginc"

            fixed4 _Diffuse;
            fixed4 _Specular;
            half _Gloss;

            struct a2v {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

            struct v2f {
                float4 pos : SV_POSITION;
                fixed3 color : COLOR;
            };

            v2f vert(a2v v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);

                // 法線をワールド空間に変換
                fixed3 worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));
                fixed3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
                fixed3 viewDir = normalize(_WorldSpaceCameraPos - mul(v.vertex, unity_WorldToObject).xyz);
                fixed3 reflectDir = normalize(reflect(-lightDir, worldNormal));

                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb;
                fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(dot(worldNormal, lightDir), 0);
                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(dot(reflectDir, viewDir), 0), _Gloss);

                o.color = ambient + diffuse + specular;
                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                return fixed4(i.color, 1.0);
            }
            ENDCG
        }
    }
    FallBack "VertexLit"
}

フラグメントシェーダによる高精度な高光計算

頂点シェーダでの計算は補間により精度が低下するため、高品質な表現にはフラグメントシェーダで計算を行うのが望ましい。

struct v2f {
    float4 pos : SV_POSITION;
    float3 worldNormal : TEXCOORD0;
    float3 worldPos : TEXCOORD1;
};

v2f vert(a2v v)
{
    v2f o;
    o.pos = UnityObjectToClipPos(v.vertex);
    o.worldNormal = UnityObjectToWorldNormal(v.normal);
    o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
    return o;
}

fixed4 frag(v2f i) : SV_Target
{
    fixed3 worldNormal = normalize(i.worldNormal);
    fixed3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
    fixed3 viewDir = normalize(_WorldSpaceCameraPos - i.worldPos);
    fixed3 reflectDir = normalize(reflect(-lightDir, worldNormal));

    fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb;
    fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(dot(worldNormal, lightDir), 0);
    fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(dot(reflectDir, viewDir), 0), _Gloss);

    return fixed4(ambient + diffuse + specular, 1.0);
}

Blinn-Phongモデルの導入

Blinn-Phongモデルは、視線方向と光源方向の中間ベクトル(ハーフベクトル)と法線の内積を用いることで、より安定した高光を実現する。

fixed3 halfDir = normalize(lightDir + viewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(dot(worldNormal, halfDir), 0), _Gloss);

この手法はPhongモデルと比べて計算コストが低く、かつリアルな外観を得られるため、ゲーム開発で広く使われている。

Unityのビルトイン関数の活用

Unityは方向ベクトルの変換や取得を簡略化するためのユーティリティ関数を提供している:

  • UnityObjectToWorldNormal(normal):法線をオブジェクト空間からワールド空間へ変換
  • WorldSpaceLightDir(worldPos):ワールド座標から光源への方向を取得
  • UnityWorldSpaceViewDir(worldPos):ワールド座標からカメラへの視線方向を取得

これらの関数を使うことで、マトリクス操作を直接記述する必要がなくなり、コードの可読性と保守性が向上する。

統合シェーダの完成形

最終的に、ディフューズとスペキュラを統合したシェーダは以下のようになる:

Shader "Custom/DiffuseSpecular"
{
    Properties
    {
        _Diffuse ("Diffuse", Color) = (1,1,1,1)
        _Specular ("Specular", Color) = (1,1,1,1)
        _Gloss ("Gloss", Range(10, 200)) = 50
    }
    SubShader
    {
        Pass
        {
            Tags { "LightMode" = "ForwardBase" }
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "Lighting.cginc"

            fixed4 _Diffuse;
            fixed4 _Specular;
            half _Gloss;

            struct a2v {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

            struct v2f {
                float4 pos : SV_POSITION;
                float3 worldNormal : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
            };

            v2f vert(a2v v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                fixed3 N = normalize(i.worldNormal);
                fixed3 L = normalize(WorldSpaceLightDir(i.worldPos));
                fixed3 V = normalize(UnityWorldSpaceViewDir(i.worldPos));
                fixed3 H = normalize(L + V);

                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb;
                fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(dot(N, L), 0);
                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(dot(N, H), 0), _Gloss);

                return fixed4(ambient + diffuse + specular, 1.0);
            }
            ENDCG
        }
    }
    FallBack "Specular"
}

タグ: Unity Shader CG HLSL Blinn-Phong

6月6日 17:37 投稿