Unity3Dシェーダーにおけるライティングモデルの実装

ライティングモデルの概要

ライティングモデルとは、特定のポイントでの照明効果を計算するための数式です。標準的なライティングモデルでは、カメラに入る光を以下の4つの要素に分類します:

  • エミッション
    自然界の蛍や発光する生物のように、自ら光を放つ現象を模倣します。
  • スペキュラ反射
    鏡面反射を想定し、入射した光をほぼ完全に反射する状態を表現します。
  • ディフューズ反射
    壁やテーブルなどへの光の反射を模倣し、全方位に散乱する反射特性を示します。
  • アンビエント光
    シーン内での間接照明を表し、物体間で反射された光による影響を考慮します。

これらのモデルは現実世界の光学現象をシミュレーションしたものであり、実際の物理的な光の挙動とは異なります。実際の光の計算はさらに複雑です。

ディフューズ反射の計算方法

頂点シェーダ内でディフューズ反射を計算することで、頂点単位のライティングを実現できます。

ディフューズ反射の基本式:
Disffuse = 直射光カラー × max(0, cosθ)
ここでθは法線ベクトルと光源ベクトルのなす角です。角度が負の場合、裏面として黒色を返します。

必要な数学的知識:

  • ベクトルの内積(スカラー結果)
    a・b = |a||b|cosθ
  • ベクトルの外積(ベクトル結果)
    a×b = c(cはベクトルで、大きさは|a||b|sinθ)

ディフューズシェーダの実装

Passブロック内にTags{"LightMode"="ForwardBase"}を追加し、#include "Lighting.cginc"をインクルードすることで、Unityが提供するライティング関連の機能を利用できます。

適切なLightModeを設定することで、Unityの組み込みライティング変数にアクセスできるようになります。

  • normalize() - ベクトルを正規化します
  • max() - 最大値を取得します
  • dot() - 2つのベクトルの内積を計算します
  • _WorldSpaceLightPos0 - 平行光源の位置情報を取得
  • _LightColor0 - 平行光源のカラー情報を取得
  • UNITY_MATRIX_MVP - モデル空間からクリップ空間への変換行列
  • _World2Object - 世界空間からオブジェクト空間への変換行列
  • UNITY_LIGHTMODEL_AMBIENT - アンビエント光情報を取得
Shader "Custom/DiffuseVertexShader"
{
    Properties
    {
        
    }
    SubShader {
        Pass 
        {
            Tags{"LightMode"="ForwardBase"}

        CGPROGRAM
#include "Lighting.cginc"

#pragma vertex vsMain
#pragma fragment fsMain 

        struct VertexInput 
        {
            float4 pos:POSITION;
            float3 norm:NORMAL;
        };

        struct VertexOutput
        {
            float4 clipPos:SV_POSITION;
            fixed3 outputColor:COLOR;
        };
        
        VertexOutput vsMain(VertexInput input)
        {
            VertexOutput output;
            output.clipPos = mul(UNITY_MATRIX_MVP, input.pos);

            fixed3 worldNormal = normalize(mul(input.norm, (float3x3)unity_WorldToObject));

            fixed3 lightDirection = _WorldSpaceLightPos0.xyz;

            fixed3 diffuseResult = _LightColor0.rgb * max(0, dot(worldNormal, lightDirection));
            output.outputColor = diffuseResult;
            return output;
        }

        fixed4 fsMain(VertexOutput input):SV_Target
        {
            return fixed4(input.outputColor, 1);
        }

        ENDCG
        }
    }
    Fallback "VertexLit"
}

結果:
ライティングを適用する前の状態では、赤色が均一に表示されます。
ライティングを実装後、光源に向いている面は明るく、背いている面は暗くなることが確認できます。

現在の実装では頂点単位で計算を行っているため、頂点数が限られていることから滑らかなライティング効果を得るのが難しい場合があります。フラグメントシェーダで計算を行うことでより滑らかな効果を得られますが、その分計算負荷も増加します。この手法をピクセル単位ライティングと呼びます。

マテリアルカラーとアンビエント光の統合

これまでの実装では、物体の色がアンビエント光のみに依存していましたが、現実世界では物体の固有色と環境光が混ざった結果が見えるのが一般的です。以下のようにコードを修正し、物体の固有カラーを追加します:

Shader "Custom/DiffuseWithMaterial"
{
    Properties
    {
        _MaterialColor("Material Color", Color) = (1,1,1,1)
    }
    SubShader {
        Pass 
        {
            Tags{"LightMode"="ForwardBase"}

        CGPROGRAM
#include "Lighting.cginc"

#pragma vertex vsMain
#pragma fragment fsMain 
    
        fixed4 _MaterialColor;

        struct VertexInput 
        {
            float4 pos:POSITION;
            float3 norm:NORMAL;
        };

        struct VertexOutput
        {
            float4 clipPos:SV_POSITION;
            fixed3 outputColor:COLOR;
        };
        
        VertexOutput vsMain(VertexInput input)
        {
            VertexOutput output;
            output.clipPos = mul(UNITY_MATRIX_MVP, input.pos);

            fixed3 ambientLight = UNITY_LIGHTMODEL_AMBIENT.rgb;

            fixed3 worldNormal = normalize(mul(input.norm, (float3x3)unity_WorldToObject));

            fixed3 lightDirection = _WorldSpaceLightPos0.xyz;

            fixed3 diffuseResult = _LightColor0.rgb * max(0, dot(worldNormal, lightDirection)) * _MaterialColor.rgb;
            output.outputColor = diffuseResult + ambientLight;
            return output;
        }

        fixed4 fsMain(VertexOutput input):SV_Target
        {
            return fixed4(input.outputColor, 1);
        }

        ENDCG
        }
    }
    Fallback "VertexLit"
}

結果として、直射光を白色、マテリアルカラーを緑色に設定すると、パネルで色調整しながら物体がそのように表示されるようになります。カラーの乗算は色調変化、加算は明るさの強調に使われます。

アンビエント光の設定方法:
Window → Lighting → Ambient Source → Color
通常、強いアンビエント光は推奨されません。

ピクセル単位ライティング

Shader "Custom/PixelDiffuseShader"
{
    Properties
    {
        _MaterialColor("Material Color", Color) = (1,1,1,1)
    }
    SubShader {
        Pass 
        {
            Tags{"LightMode"="ForwardBase"}

        CGPROGRAM
#include "Lighting.cginc"

#pragma vertex vsMain
#pragma fragment fsMain 
    
        fixed4 _MaterialColor;

        struct VertexInput 
        {
            float4 pos:POSITION;
            float3 norm:NORMAL;
        };

        struct VertexOutput
        {
            float4 clipPos:SV_POSITION;
            fixed3 worldNormal:TEXCOORD0;
        };
        
        VertexOutput vsMain(VertexInput input)
        {
            VertexOutput output;
            output.clipPos = mul(UNITY_MATRIX_MVP, input.pos);
            output.worldNormal = mul(input.norm, (float3x3)unity_WorldToObject);
            return output;
        }

        fixed4 fsMain(VertexOutput input):SV_Target
        {
            fixed3 ambientLight = UNITY_LIGHTMODEL_AMBIENT.rgb;

            fixed3 normalizedNormal = normalize(input.worldNormal);

            fixed3 lightDirection = _WorldSpaceLightPos0.xyz;

            fixed3 diffuseResult = _LightColor0.rgb * max(0, dot(normalizedNormal, lightDirection)) * _MaterialColor.rgb;
            fixed3 finalColor = diffuseResult + ambientLight;
            return fixed4(finalColor, 1);
        }

        ENDCG
        }
    }
    Fallback "VertexLit"
}

頂点単位ライティングとピクセル単位ライティングの違いは視覚的に確認できます。ピクセル単位の方がパフォーマンスを消費しますが、高精度が必要ない場合は頂点単位でも十分です。

ハーフランバートライティングモデル

ランバートライティングモデルでは、表面の明るさは光源ベクトルとサーフェス法線の角度のコサイン値によって決まります。

ディフューズ反射強度をDiffuse、入射光強度をI、角度をθとした場合、ランバートモデルの式は:
Diffuse = I × cosθ

しかし、ランバートモデルでは裏面が完全に黒くなってしまう問題があります。

この問題を解決するために、ハーフランバートライティングモデルが開発されました:
Diffuse = 直射光カラー × (cosθ × 0.5 + 0.5)

この式により、裏面も完全に黒くならず、光の強度変化に応じたグラデーションを維持できます。

シェーダの修正:

Shader "Custom/HalfLambertDiffuse"
{
    Properties
    {
        _MaterialColor("Material Color", Color) = (1,1,1,1)
    }
    SubShader {
        Pass 
        {
            Tags{"LightMode"="ForwardBase"}

        CGPROGRAM
#include "Lighting.cginc"

#pragma vertex vsMain
#pragma fragment fsMain 
    
        fixed4 _MaterialColor;

        struct VertexInput 
        {
            float4 pos:POSITION;
            float3 norm:NORMAL;
        };

        struct VertexOutput
        {
            float4 clipPos:SV_POSITION;
            fixed3 worldNormal:TEXCOORD0;
        };
        
        VertexOutput vsMain(VertexInput input)
        {
            VertexOutput output;
            output.clipPos = mul(UNITY_MATRIX_MVP, input.pos);
            output.worldNormal = mul(input.norm, (float3x3)unity_WorldToObject);
            return output;
        }

        fixed4 fsMain(VertexOutput input):SV_Target
        {
            fixed3 ambientLight = UNITY_LIGHTMODEL_AMBIENT.rgb;

            fixed3 normalizedNormal = normalize(input.worldNormal);

            fixed3 lightDirection = _WorldSpaceLightPos0.xyz;

            float halfLambertValue = dot(normalizedNormal, lightDirection) * 0.5 + 0.5;
            
            fixed3 diffuseResult = _LightColor0.rgb * halfLambertValue * _MaterialColor.rgb;
            fixed3 finalColor = diffuseResult + ambientLight;
            return fixed4(finalColor, 1);
        }

        ENDCG
        }
    }
    Fallback "VertexLit"
}

この実装により、モデル全体がより明るくなり、裏面も純粋な黒ではなくグラデーションを持つようになります。また、前面の色もより鮮やかになり、全体的な視覚効果が向上します。

タグ: unity3d Shader lighting-model cg-shader computer-graphics

8月2日 07:10 投稿