ホーム
ゲーム開発
Unity
UnrealEngine
C++
ゲーム数学
ゲームAI
サウンド
アニメーション
GBDK
制作日記
3DCG
Houdini
Blender
USD
グラフィックス
テクノロジ
生成AI関連
動画制作
ツール開発
フロントエンド関連
サーバサイド関連
ソフトウェア設計
ハードウェア関連
おすすめ技術書
音楽
DTM
楽器・機材
ピアノ
その他
ブログについて
都会のエレキベア
ラーメン日記
四コマ漫画
おすすめアイテム
おもしろコラム
お問い合わせ
  • ゲーム開発
    • Unity
    • UnrealEngine
    • C++
    • ゲーム数学
    • ゲームAI
    • サウンド
    • アニメーション
    • GBDK
    • 制作日記
  • 3DCG
    • Houdini
    • Blender
    • USD
    • グラフィックス
  • テクノロジ
    • 生成AI関連
    • 動画制作
    • ツール開発
    • フロントエンド関連
    • サーバサイド関連
    • ソフトウェア設計
    • ハードウェア関連
    • おすすめ技術書
  • 音楽
    • DTM
    • 楽器・機材
    • ピアノ
  • その他
JP|EN
  1. ホーム
  2. 20230228_01

【Unity】第一回 シェーダーライティング入門 〜基本のライティング〜(Lambert、Phong、HalfLambert、Blinn-Phong、リムライト)【シェーダー】

Unityグラフィックスシェーダー
2023-02-28

マイケル
マイケル
みなさんこんにちは!
マイケルです!
エレキベア
エレキベア
こんにちクマ〜〜〜
マイケル
マイケル
ここ最近はずっとシェーダー周りを触っていてある程度慣れてきたので、
整理も含めてぼちぼち体系的にまとめていこうかと思います!
エレキベア
エレキベア
いや〜〜〜
シェーダーは楽しいクマね〜〜〜
マイケル
マイケル
まずは基礎となるライティング関連のシェーダー実装について何回かに分けて書いていこうかと思います!
第一回で紹介するのは・・・こちら!

  • Lambert拡散反射
  • Phong鏡面反射
  • HalfLambert拡散反射
  • Blinn-Phong鏡面反射
  • リムライト

マイケル
マイケル
シェーダーの学習を始めたら初めに出てくるであろう
Lambert、Phongとその改良版の実装を紹介します。
最後に応用としてリムライトの実装もしてみます!
エレキベア
エレキベア
HalfLambertとBlinn-Phong・・・
なんか難しそうクマがいきなり飛ばしすぎではないクマ??
マイケル
マイケル
一見難しそうだけど、どちらも元の考え方を少しアレンジしただけだから
まとめて紹介していくよ!!
決して分けるのが面倒くさかったわけではありません!!
エレキベア
エレキベア
(分けた方がブログPV的にはいいのにクマ・・・)
マイケル
マイケル
なお、Unityは下記のバージョンを使用していて、
実装した内容はGitHubにも上げています!
こちらも是非参照してください!!

[使用したバージョン]

Unity 2021.3.1f1

GitHub – unity-lighting-shader-sample / masarito617

エレキベア
エレキベア
毎度お馴染みクマね

参考書籍

マイケル
マイケル
シェーダー実装や内容をまとめるにあたり、
下記の書籍を参考にさせていただきました!

HLSL シェーダーの魔導書 シェーディングの基礎からレイトレーシングまで

DirectX 9 シェーダプログラミングブック

マイケル
マイケル
HLSL シェーダーの魔導書 は割と最近発売されたので情報も比較的新しく、ハンズオン形式で一から解説してくれるため初めの一冊におすすめです!!
また、DirectX 9 シェーダプログラミングブック はDirectX9時代に発売されたもので既に廃盤ですが、シェーダー周りの実装について広く解説されている名著のため、見かけたら手に入れたほうがよいです!
僕はメルカリで粘って手に入れました!!
エレキベア
エレキベア
日本語で解説されているシェーダー書籍はこの辺りが一番クマ〜〜〜

事前準備:内積を感じよう

マイケル
マイケル
シェーダー実装の話に入る前にまずはウォーミングアップ!
ライティングで頻繁に出てくる、内積のイメージを掴みましょう!
数学に自信がある方はこの節は飛ばして問題ありません!
↑内積の計算はシェーダー実装で頻繁に出てくる
エレキベア
エレキベア
内積・・・
確かに言葉だけ聞いてもあまりピンとこないかもクマ
マイケル
マイケル
内積は下記の式で表すことができる式のことで、ベクトル同士の内積を計算する場合、
・ベクトル同士の向きが近いほど1に近づく
・ベクトル同士の向きが直角になるほど0に近づく
・ベクトル同士の向きが90〜180度の場合は負の値になる

といった特性を持っています。

$$ cos\theta = \frac{a \times b}{|a||b|} = \hat{a} \cdot \hat{b} = a_x b_x + a_y b_y $$


エレキベア
エレキベア
正規化したベクトル同士の内積はcosθに対応してるのクマね
だから角度が0度の時には結果が1になるクマ
マイケル
マイケル
こちらの説明だけではあまりピンとこない方もいると思うので、p5.jsで簡単なシミュレータを作ってみました!
下記でドラッグすることでベクトルの向きを変えれるので、内積の値がどのように変化するか感じてみてください!

See the Pen
20230226_dot_vec
by masarito617 (@masarito617)
on CodePen.

エレキベア
エレキベア
実際に手で触ってみると分かりやすいクマね〜〜
マイケル
マイケル
実際に内積計算している関数は下記になります!
JavaScriptなので直接実装していますが、シェーダー言語の場合はdot関数として用意されていることが多いです。
// 2次元ベクトルの内積
function dot(v1, v2) {
  return v1.x*v2.x + v1.y*v2.y;
}
マイケル
マイケル
触ってみると先ほど説明した特性になっていることが確認できます。
↑ベクトルの向きが同じだと1に近づく
↑ベクトルの向きが直角だと0に近づく
↑90度を超えると負の値になる
エレキベア
エレキベア
内積はもう充分クマ〜〜〜〜
マイケル
マイケル
ライティングの基本となるLambert拡散反射の実装では、ライトの向きのベクトルと法線ベクトルの内積を取ることで、どの程度の光の量が面に当たっているかを計算します。
エレキベア
エレキベア
光がまっすぐ面に当たっていると最大の1が返ってくるクマね
マイケル
マイケル
そして90度を超える角度(負の値)は無視して0で丸めることが多いです。
これはmax関数を使用してもよいですが、シェーダー言語では専用のsaturate関数というものが用意されています。
こちらもよく見かけることになると思うので覚えておきましょう!

$$ max(0, (\hat{V_a} \cdot \hat{V_b})) = saturate(\hat{V_a} \cdot \hat{V_b}) $$


エレキベア
エレキベア
サチュレイト・・・
かっこいいクマ〜〜〜
マイケル
マイケル
パフォーマンス的にはどちらを使用してもさほど変わりないという噂なので、好きな方を使いましょう!
僕は意味が明確になるのとカッコいいのでsaturateを使います!!
エレキベア
エレキベア
クマもサチュレイトを使うクマ〜〜〜

Lambert拡散反射とPhong鏡面反射

マイケル
マイケル
内積が分かったところでシェーダー実装に移っていきましょう!
今回紹介するシェーダーは内積が分かれば何なく理解できると思います!
エレキベア
エレキベア
楽しみクマ〜〜〜

Lambert拡散反射

マイケル
マイケル
まずは最も基本となるLambert拡散反射から!
拡散反射という名の通り、全ての面に同じ角度の光が当たっていると仮定して拡散する光の量を計算する、擬似的な反射モデルです!
エレキベア
エレキベア
本来、光は様々な物質に反射しているクマが
それを簡潔に計算できるようにしたものクマね
↑Lambert拡散反射モデル
マイケル
マイケル
計算は単純で、光源の向きLと面の法線Nの内積を取ることで、
面にどの程度の光が当たるかを計算します。
エレキベア
エレキベア
ここでさっきの内積の考えが出てきたクマね
マイケル
マイケル
これに拡散反射色Kdも加えた式が下記になります!

$$ I_d = k_d cos \theta_a = k_d (\hat{N} \cdot \hat{L}) \\ k_d: 拡散反射色 $$

エレキベア
エレキベア
非常にシンプルクマね
マイケル
マイケル
しかし、これだけでは内積の特性上、角度が90度付近の面がかなり真っ暗になってしまいます。
そのため光の向きに限らず全方位に当たる光として、環境光を加えることが多いです。
こちらの環境色Kaを加えた式が下記になります。

$$ I = k_a + k_d (\hat{N} \cdot \hat{L}) \\ k_a: 環境色 \\ k_d: 拡散反射色$$


エレキベア
エレキベア
環境光+拡散反射光で実装するのクマね
マイケル
マイケル
こちらをシェーダーで実装したものがこちらになります!
// Lambert拡散反射モデル
Shader "Custom/LambertShader"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        // 環境色
        _Ka ("Ka", Range(0.01, 1)) = 0.8
        // 拡散反射色
        _DiffuseColor ("Diffuse Color", Color) = (0.8, 0.8, 0.8, 1)
        _Kd ("Kd", Range(0.01, 1)) = 1.0
    }
    SubShader
    {
        // フォワードレンダリングパイプラインのベースパスであることを示す
        // ディレクショナルライト 方向: _WorldSpaceLightPos0
        Tags { "LightMode"="ForwardBase" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

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

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

            sampler2D _MainTex;
            float4 _MainTex_ST;

            float _Ka;
            fixed4 _DiffuseColor;
            float _Kd;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.normal = mul(unity_ObjectToWorld, v.normal);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // 拡散反射光
                const float3 l = normalize(_WorldSpaceLightPos0.xyz);
                const float3 n = normalize(i.normal);
                const float3 nl = saturate(dot(n, l));
                const float3 diffuse = _Kd * _DiffuseColor.xyz * nl; // ライトの色を使う場合は_LightColor0を指定すればよい

                // 環境光
                const half3 ambient = _Ka * ShadeSH9(half4(i.normal, 1));

                // 環境光+拡散反射光
                const float3 lambert = ambient + diffuse;

                // 最終的なカラーに乗算
                fixed4 col = tex2D(_MainTex, i.uv);
                col.xyz *= lambert;
                return col;
            }
            ENDCG
        }
    }
}
マイケル
マイケル
計算式に準拠してかなり丁寧に記述しています。
くどいくらいだとは思いますが、気になる方は省略して記述してみてください!
実際に計算を行なっているのは下記部分です!
            fixed4 frag (v2f i) : SV_Target
            {
                // 拡散反射光
                const float3 l = normalize(_WorldSpaceLightPos0.xyz);
                const float3 n = normalize(i.normal);
                const float3 nl = saturate(dot(n, l));
                const float3 diffuse = _Kd * _DiffuseColor.xyz * nl; // ライトの色を使う場合は_LightColor0を指定すればよい

                // 環境光
                const half3 ambient = _Ka * ShadeSH9(half4(i.normal, 1));

                // 環境光+拡散反射光
                const float3 lambert = ambient + diffuse;

                // 最終的なカラーに乗算
                fixed4 col = tex2D(_MainTex, i.uv);
                col.xyz *= lambert;
                return col;
            }
エレキベア
エレキベア
さっきの計算式まんまクマね
マイケル
マイケル
環境光はShadeSH9関数を使用して取得できます。
また、拡散反射色について今回はプロパティで設定できるようにしてみましたが、Unityシーン上のライトの色を使用する場合は_LightColor0で取得することができます。
その場合はUnityLightingCommon.cgincを追加でインクルードする必要があるため注意しましょう!
エレキベア
エレキベア
これがライティングの基本になるクマね

Phong鏡面反射

マイケル
マイケル
そして次はPhong鏡面反射です!
こちらは下記のように光沢のような反射を考慮したモデルになります。
↑Phong鏡面反射
マイケル
マイケル
考え方としては、Lambert拡散反射の時に加えて、
ライトの反射ベクトルRと面から視線への向きVが追加されます。
こちらの内積を反射量として計算します。
エレキベア
エレキベア
Lambertにちょちょいと足した感じクマね
マイケル
マイケル
鏡面反射色Ksを加えた反射量の計算は下記のようになります。

$$ I_s = k_s cos \theta_r = k_s (\hat{V} \cdot \hat{R}) \\ k_s: 鏡面反射色$$


マイケル
マイケル
Lambert計算で使用した環境光、拡散反射光に加えてこの鏡面反射光を加えたものを最終的な光の量として計算します。

$$ I = k_a + k_d (\hat{N} \cdot \hat{L}) + k_s (\hat{V} \cdot \hat{R})^a \\ k_a: 環境色 \\ k_d: 拡散反射色 \\ k_s: 鏡面反射色 \\ a: 鏡面反射指数$$


エレキベア
エレキベア
考え方としては同じようなものクマね
このaって何クマ??
マイケル
マイケル
aのべき乗している部分は鏡面反射指数といって、いわゆる反射量の絞りを入れるために使用するよ!
実際にライティングした例を出すと下記のような感じです。
↑a=2の場合
↑a=10の場合
エレキベア
エレキベア
なるほどクマ
反射量と範囲が変わるクマね
マイケル
マイケル
こちらのシェーダー実装は下記になります!
// Phong鏡面反射モデル
Shader "Custom/PhongShader"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        // 環境色
        _Ka ("Ka", Range(0.01, 1)) = 0.8
        // 拡散反射色
        _DiffuseColor ("Diffuse Color", Color) = (0.8, 0.8, 0.8, 1)
        _Kd ("Kd", Range(0.01, 1)) = 0.8
        // 鏡面反射色
        _SpecularColor ("Specular Color", Color) = (1, 1, 1, 1)
        _Ks ("Ks", Range(0.01, 1)) = 1.0
        _SpecularLevel ("Specular Level", Range(0.1, 30)) = 5.0 // 鏡面反射指数 a
    }
    SubShader
    {
        Tags { "LightMode"="ForwardBase" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

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

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
                float3 vertexWorld : TEXCOORD1; // ワールド座標系の頂点座標
                float3 normal : NORMAL;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            float _Ka;
            fixed4 _DiffuseColor;
            float _Kd;
            fixed4 _SpecularColor;
            float _Ks;
            float _SpecularLevel;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.vertexWorld = mul(unity_ObjectToWorld, v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.normal = mul(unity_ObjectToWorld, v.normal);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // 拡散反射光
                const float3 l = normalize(_WorldSpaceLightPos0.xyz);
                const float3 n = normalize(i.normal);
                const float3 nl = saturate(dot(n, l));
                const float3 diffuse = _Kd * _DiffuseColor.xyz * nl;

                // 鏡面反射光
                const float3 r = reflect(-l, n); // 反射ベクトル
                const float3 v = normalize(_WorldSpaceCameraPos - i.vertexWorld); // 視線->オブジェクト
                const float3 rv = pow(saturate(dot(r, v)), _SpecularLevel); // 絞りを入れる
                const float3 specular = _Ks * _SpecularColor * rv;

                // 環境光
                const half3 ambient = _Ka * ShadeSH9(half4(i.normal, 1));

                // 環境光+拡散反射光+鏡面反射光
                const float3 phong = ambient + diffuse + specular;

                // 最終的なカラーに乗算
                fixed4 col = tex2D(_MainTex, i.uv);
                col.xyz *= phong;
                return col;
            }
            ENDCG
        }
    }
}
マイケル
マイケル
計算を行なっている部分は下記になります!
反射ベクトルはreflect関数を使用して求めることができます。
視線への向きに関しては、頂点のワールド座標を使用して計算を行う必要がある点には注意しましょう!
            fixed4 frag (v2f i) : SV_Target
            {
                // 拡散反射光
                const float3 l = normalize(_WorldSpaceLightPos0.xyz);
                const float3 n = normalize(i.normal);
                const float3 nl = saturate(dot(n, l));
                const float3 diffuse = _Kd * _DiffuseColor.xyz * nl;

                // 鏡面反射光
                const float3 r = reflect(-l, n); // 反射ベクトル
                const float3 v = normalize(_WorldSpaceCameraPos - i.vertexWorld); // 視線->オブジェクト
                const float3 rv = pow(saturate(dot(r, v)), _SpecularLevel); // 絞りを入れる
                const float3 specular = _Ks * _SpecularColor * rv;

                // 環境光
                const half3 ambient = _Ka * ShadeSH9(half4(i.normal, 1));

                // 環境光+拡散反射光+鏡面反射光
                const float3 phong = ambient + diffuse + specular;

                // 最終的なカラーに乗算
                fixed4 col = tex2D(_MainTex, i.uv);
                col.xyz *= phong;
                return col;
            }
エレキベア
エレキベア
基本的な計算の考え方はLambertの時と変わらないクマね

LambertとPhongの改良モデル

マイケル
マイケル
LambertとPhongの実装を知ったところで、これらを改良した
・HalfLambert拡散反射
・Blinn-Phong鏡面反射
の実装についても見てみましょう!

HalfLambert拡散反射

マイケル
マイケル
HalfLambert拡散反射は、Lambert拡散反射の時に問題となっていた角度が90度近くになると真っ暗な部分が出来てしまうという問題を解消したものです。
通常のLambertと比べて陰影部分を少し柔らかく付けることができます。
↑HalfLambert拡散反射
マイケル
マイケル
拡散反射を計算する際に下記式を使用することで、陰影を柔らかくしています。
具体的には、内積結果を-1.0〜1.0から0.0〜1.0に変換した後に2乗しています。

$$ I_d = k_d ((\hat{N} \cdot \hat{L}) \times 0.5 + 0.5)^2 $$


↑通常のLambertと比べて暗い部分が柔らかくなっていることが分かる

参考:
Half Lambert – Valve Developer Community

エレキベア
エレキベア
なるほどクマ
範囲を半分にしていることからハーフランバートなのクマね(恐らく)
マイケル
マイケル
通常のLambertと並べてみた結果が下記になります。
HalfLambertの方が柔らかく、環境光無しでもそれなりに全体は見えていることが確認できますね
↑左: Lambert 右: HalfLambert
↑左: Lambert 右: HalfLambert
マイケル
マイケル
実際の計算部分は下記のようになります!
実装はほぼ同じなので、フラグメントシェーダ部分のみ載せておきます。
            fixed4 frag (v2f i) : SV_Target
            {
                // 拡散反射光
                const float3 l = normalize(_WorldSpaceLightPos0.xyz);
                const float3 n = normalize(i.normal);
                const float3 nl = saturate(dot(n, l));
                // -1.0〜1.0 -> 0.0〜1.0に変換して2乗することで陰影結果を和らげる
                const float3 half_nl = nl * 0.5 + 0.5;
                const float3 diffuse = _Kd * _DiffuseColor.xyz * half_nl * half_nl;

                // 環境光
                const half3 ambient = _Ka * ShadeSH9(half4(i.normal, 1));

                // 環境光+拡散反射光
                const float3 lambert = ambient + diffuse;

                // 最終的なカラーに乗算
                fixed4 col = tex2D(_MainTex, i.uv);
                col.xyz *= lambert;
                return col;
            }
エレキベア
エレキベア
これだけの味付けで綺麗にできるのは便利クマね

Blinn-Phong鏡面反射

マイケル
マイケル
そして次はBlinn-Phong鏡面反射!
こちらは反射ベクトルRの計算負荷を抑えるために考えられた手法です。
実装時にはreflect関数を使用しましたが、中身は下記のようになっていて、それなりに複雑になっています。

$$ R=F+2(-F \cdot N)N $$


エレキベア
エレキベア
こんな計算を行なっていたクマね
マイケル
マイケル
Blinn-Phongでは、反射ベクトルRの代わりに、光源の向きと視線の向きの中間であるハーフベクトルHを使用します。
こちらと法線の向きの内積を反射量として使用する考え方です。

$$ I_s = k_s (\hat{N} \cdot \hat{H})^a $$


エレキベア
エレキベア
なるほどクマ
これでも同じように反射量が求められるクマね
マイケル
マイケル
見た目も若干差異はあるけどほとんど変わらないね!
実装は下記のようになります。
こちらもPhongの時とほぼ同じのため、フラグメントシェーダ部分のみ載せておきます。
            fixed4 frag (v2f i) : SV_Target
            {
                // 拡散反射光
                const float3 l = normalize(_WorldSpaceLightPos0.xyz);
                const float3 n = normalize(i.normal);
                const float3 nl = saturate(dot(n, l));
                const float3 diffuse = _Kd * _DiffuseColor.xyz * nl;

                // 鏡面反射光
                const float3 v = normalize(_WorldSpaceCameraPos - i.vertexWorld);
                const float3 h = normalize(l+v); // 反射ベクトルの代わりにハーフベクトルを使用する
                const float3 hv = pow(saturate(dot(n, h)), _SpecularLevel); // 法線とハーフベクトルの内積で計算する
                const float3 specular = _Ks * _SpecularColor * hv;

                // 環境光
                const half3 ambient = _Ka * ShadeSH9(half4(i.normal, 1));

                // 環境光+拡散反射光+鏡面反射光
                const float3 phong = ambient + diffuse + specular;

                // 最終的なカラーに乗算
                fixed4 col = tex2D(_MainTex, i.uv);
                col.xyz *= phong;
                return col;
            }
エレキベア
エレキベア
ハーフベクトル導出部分が変わったくらいクマね
マイケル
マイケル
なお、補足ですがSurfaceシェーダではデフォルトで
LambertとBlinn-Phongモデルが用意されているようですね

サーフェスシェーダーの記述 – Unity マニュアル

エレキベア
エレキベア
Blinn-Phongが採用されているのクマね

リムライトの実装

マイケル
マイケル
それでは最後にライティング応用として、下記のようなリムライトの実装を行なってみます。
こちらもこれまでの考え方を元に実装することができます。
↑リムライト
エレキベア
エレキベア
これはこれまでと違いそうクマがどうやるのクマ?
マイケル
マイケル
これも視線の向きと法線の向きを描いて考えてみます。
下記のように、周りが明るくなるリムライトの場合は、光が強く出てほしいのはこれらの向きが直角な場合になります。
↑リムライトの場合、直角になっている部分が強く出てほしい
エレキベア
エレキベア
なるほどクマ・・・
つまり、角度が90度に近づくほど強く、0度になるほど弱くするクマね
マイケル
マイケル
その通り!!
つまるところ、計算式は下記のようになります!

$$ I_r = 1 – max(0, (\hat{N} \cdot \hat{V})) $$


マイケル
マイケル
1から内積結果を引くだけ!
これで実装できそうな気がしてきましたね!
実際の実装内容は下記のようになります。
// リムライト
Shader "Custom/RimLightShader"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        // リムライト
        _RimColor ("Rim Color", Color) = (1, 1, 1, 1)
        _RimPower ("Rim Power", Range(0.0, 5.0)) = 2.0
    }
    SubShader
    {
        Tags { "LightMode"="ForwardBase" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

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

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

            sampler2D _MainTex;
            float4 _MainTex_ST;

            fixed4 _RimColor;
            float _RimPower;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.vertexWorld = mul(unity_ObjectToWorld, v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.normal = mul(unity_ObjectToWorld, v.normal);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // 法線とカメラからオブジェクトへの向きが垂直な部分を強くする
                const float3 n = normalize(i.normal);
                const float3 v = normalize(_WorldSpaceCameraPos - i.vertexWorld);
                const float3 nv = saturate(dot(n, v));
                const float rimPower = 1.0 - nv;

                // 絞りも入れた最終的な反射光
                const float3 rimColor = _RimColor * pow(rimPower, _RimPower);

                // 環境光も足す
                const half3 ambient = ShadeSH9(half4(i.normal, 1));
                const float3 finalLight = ambient + rimColor;

                // 最終的なカラーに乗算
                fixed4 col = tex2D(_MainTex, i.uv);
                col.xyz *= finalLight;
                return col;
            }
            ENDCG
        }
    }
}
マイケル
マイケル
先ほどの計算式を落とし込んでいるだけですね。
あとは鏡面反射の時と同様、絞りも入れることで調整できるようにしています!
            fixed4 frag (v2f i) : SV_Target
            {
                // 法線とカメラからオブジェクトへの向きが垂直な部分を強くする
                const float3 n = normalize(i.normal);
                const float3 v = normalize(_WorldSpaceCameraPos - i.vertexWorld);
                const float3 nv = saturate(dot(n, v));
                const float rimPower = 1.0 - nv;

                // 絞りも入れた最終的な反射光
                const float3 rimColor = _RimColor * pow(rimPower, _RimPower);

                // 環境光も足す
                const half3 ambient = ShadeSH9(half4(i.normal, 1));
                const float3 finalLight = ambient + rimColor;

                // 最終的なカラーに乗算
                fixed4 col = tex2D(_MainTex, i.uv);
                col.xyz *= finalLight;
                return col;
            }
エレキベア
エレキベア
合わせ技クマ〜〜〜〜

おわりに

マイケル
マイケル
というわけで今回はライティング関連のシェーダー実装でした!
どうだったかな??
エレキベア
エレキベア
だいぶライティングのやり方が分かった気がするクマ
内積も感じられるようになったクマ〜〜〜
マイケル
マイケル
見た目的には地味だけど、陰影の付け方はシェーダー実装の基礎になるから
しっかり覚えておこう!!
マイケル
マイケル
次回以降は、法線マップを絡めたライティングやPBR実装にも挑戦してみようと思います!
それでは今日はこの辺で!
アデューーーー!!
エレキベア
エレキベア
クマ〜〜〜〜〜

【Unity】第一回 シェーダーライティング入門 〜基本のライティング〜(Lambert、Phong、HalfLambert、Blinn-Phong、リムライト)【シェーダー】〜完〜

※↓続きはこちら!

Unityグラフィックスシェーダー
2023-02-28
記事をSNSで共有する
X
Facebook
LINE
はてなブックマーク
Pocket
LinkedIn
Reddit

著者の各種アカウント
フォローいただけると大変励みになります!

関連記事
【Unity】Light2DとTimelineを活用した2Dライブシーンの構築
2026-07-20
【Three.js】カスタムシェーダーでトゥーン+背面法アウトラインを実装する
2026-02-15
【Three.js】Three.js入門 - シーン構築・モデル読み込み・ポストプロセスまで
2026-02-15
【Houdini21.0】3Dビル群っぽいブログヘッダー画像を作成する
2026-01-10
【Houdini21.0】Solaris徹底入門:USD構成を意識した基本的な作業フローについてまとめる
2025-12-31
【ゲーム数学】第十回 p5.js(+α)で学ぶゲーム数学「複素数とフラクタル」
2025-11-02
【プロシージャル】Pythonで学ぶ波動関数崩壊アルゴリズム(Wave Function Collapse)
2025-06-22
【UE5.5】Nanite、Lumen、VSMの概要についてまとめる
2025-05-12