
マイケルです!


整理も含めてぼちぼち体系的にまとめていこうかと思います!

シェーダーは楽しいクマね〜〜〜

第一回で紹介するのは・・・こちら!
- Lambert拡散反射
- Phong鏡面反射
- HalfLambert拡散反射
- Blinn-Phong鏡面反射
- リムライト

Lambert、Phongとその改良版の実装を紹介します。
最後に応用としてリムライトの実装もしてみます!

なんか難しそうクマがいきなり飛ばしすぎではないクマ??

まとめて紹介していくよ!!
決して分けるのが面倒くさかったわけではありません!!


実装した内容はGitHubにも上げています!
こちらも是非参照してください!!
[使用したバージョン]
Unity | 2021.3.1f1 |
GitHub – unity-lighting-shader-sample / masarito617

参考書籍

下記の書籍を参考にさせていただきました!
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 $$

だから角度が0度の時には結果が1になるクマ

下記でドラッグすることでベクトルの向きを変えれるので、内積の値がどのように変化するか感じてみてください!
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;
}








これはmax関数を使用してもよいですが、シェーダー言語では専用のsaturate関数というものが用意されています。
こちらもよく見かけることになると思うので覚えておきましょう!
$$ max(0, (\hat{V_a} \cdot \hat{V_b})) = saturate(\hat{V_a} \cdot \hat{V_b}) $$

かっこいいクマ〜〜〜

僕は意味が明確になるのとカッコいいのでsaturateを使います!!

Lambert拡散反射とPhong鏡面反射

今回紹介するシェーダーは内積が分かれば何なく理解できると思います!

Lambert拡散反射

拡散反射という名の通り、全ての面に同じ角度の光が当たっていると仮定して拡散する光の量を計算する、擬似的な反射モデルです!

それを簡潔に計算できるようにしたものクマね


面にどの程度の光が当たるかを計算します。



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


そのため光の向きに限らず全方位に当たる光として、環境光を加えることが多いです。
こちらの環境色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;
}


また、拡散反射色について今回はプロパティで設定できるようにしてみましたが、Unityシーン上のライトの色を使用する場合は_LightColor0で取得することができます。
その場合はUnityLightingCommon.cgincを追加でインクルードする必要があるため注意しましょう!

Phong鏡面反射

こちらは下記のように光沢のような反射を考慮したモデルになります。


ライトの反射ベクトルRと面から視線への向きVが追加されます。
こちらの内積を反射量として計算します。



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

$$ 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って何クマ??

実際にライティングした例を出すと下記のような感じです。



反射量と範囲が変わるクマね

// 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とPhongの改良モデル

・HalfLambert拡散反射
・Blinn-Phong鏡面反射
の実装についても見てみましょう!
HalfLambert拡散反射

通常のLambertと比べて陰影部分を少し柔らかく付けることができます。


具体的には、内積結果を-1.0〜1.0から0.0〜1.0に変換した後に2乗しています。
$$ I_d = k_d ((\hat{N} \cdot \hat{L}) \times 0.5 + 0.5)^2 $$

参考:
Half Lambert – Valve Developer Community

範囲を半分にしていることからハーフランバートなのクマね(恐らく)

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鏡面反射

こちらは反射ベクトルRの計算負荷を抑えるために考えられた手法です。
実装時にはreflect関数を使用しましたが、中身は下記のようになっていて、それなりに複雑になっています。
$$ R=F+2(-F \cdot N)N $$


こちらと法線の向きの内積を反射量として使用する考え方です。

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


LambertとBlinn-Phongモデルが用意されているようですね

リムライトの実装

こちらもこれまでの考え方を元に実装することができます。



下記のように、周りが明るくなるリムライトの場合は、光が強く出てほしいのはこれらの向きが直角な場合になります。


つまり、角度が90度に近づくほど強く、0度になるほど弱くするクマね

つまるところ、計算式は下記のようになります!
$$ I_r = 1 – max(0, (\hat{N} \cdot \hat{V})) $$

これで実装できそうな気がしてきましたね!
実際の実装内容は下記のようになります。
// リムライト
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;
}

おわりに

どうだったかな??

内積も感じられるようになったクマ〜〜〜

しっかり覚えておこう!!

それでは今日はこの辺で!
アデューーーー!!

【Unity】第一回 シェーダーライティング入門 〜基本のライティング〜(Lambert、Phong、HalfLambert、Blinn-Phong、リムライト)【シェーダー】〜完〜
※↓続きはこちら!
コメント