UnityでPhong鏡面反射シェーダーを実装する方法について紹介します。
Contents
Phong鏡面反射モデルとは
Phong(フォン)鏡面反射モデルとは、反射ベクトルと視線ベクトルが一致しているほど、ライトが強く照りかえっているとみなすシェーダー技法です。「鏡面反射」とあるため勘違いしやすいのですが、鏡のように反射先の景色が映るという処理までは定義されていません。もしUnityで鏡のような反射を実装したい場合は、こちらの記事をご覧ください。
Unityでスカイボックスの反射をシェーダーに実装する方法(準備中)
反射ベクトルの考え方
反射ベクトルの考え方は、以下のとおりです。
上の図を参考に、反射ベクトル$\vec{R}$は、ライトベクトル$\vec{L}$に法線ベクトル$\alpha \vec{N}$を二つ分を加算したものということがわかります。
$\vec{R} = \vec{L} + 2 \times \alpha \vec{N}$
$\vec{R}$ : 反射ベクトル
$\vec{L}$ : ライトベクトル
$\vec{N}$ : 法線ベクトル
法線ベクトル$\alpha \vec{N}$にかかっている係数$\alpha$は、未知のパラメータです。ところが、この数値は内積の基本性質から求めることができ、
$\alpha = -\vec{L} \cdot \vec{N}$
となります。そのため、$\alpha$を$-\vec{L} \cdot \vec{N}$に置き換えた式、
$\vec{R} = \vec{L} + 2 \times (-\vec{N} \cdot \vec{L} ) \times \vec{N} $
が得られます。
反射ベクトルの求め方
と、ここまで反射ベクトルの考え方について解説しましたが、実は数式をコードにする必要はありません。というのも、HLSL側で専用のメソッドが用意されているためです。反射ベクトルを求めるメソッドは、以下の通りです
float3 refVec = reflect(-ligDirection, normal);
引数に使うベクトルの向きには、注意が必要です。法線はそのままでも問題ありませんが、ライトベクトルは向きを逆転させないといけないので注意してください。
視線ベクトルの求め方
視線ベクトルの求め方は、以下の通りです。_WorldSpaceCameraPosというマクロから、シーンのカメラ位置を取得することができます。
fixed4 frag (v2f i) : SV_Target
{
float3 toEye = _WorldSpaceCameraPos - i.worldPos;
}
視線ベクトルと反射ベクトルが重なるほど照り返す
ここまで反射ベクトルと視線ベクトルを求めました。最後は、これらの内積を取り、その値が1に近いほどハイライトを強く表示し、0に近いほどハイライトを暗くするという処理を挟みます。
float t = dot(refVec, toEye); //反射ベクトルと視線ベクトルで内積を計算
t = max(0, t); //計算した内積のうち、t < 0は必要ないのでクランプ
シェーダー全体
これらをすべてまとめたシェーダーが以下の通りです。プロパティには、反射ハイライトの絞り具合を調整するスライダーを設定してあります。
Shader "CAGraphicsAcademy/Phong"
{
Properties
{
_ReflectionLevel("Reflection Level", Range(0, 10)) = 0
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f
{
float4 vertex : SV_POSITION;
float3 normal : NORMAL;
float4 worldPos : TEXCOORD0;
};
float _ReflectionLevel;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex); //頂点をMVP行列変換
o.worldPos = v.vertex; //各頂点のワールド座標を代入
o.normal = mul(unity_ObjectToWorld, v.normal);//各頂点が持つ法線(オブジェクト座標系)をワールド座標系に変換
return o;
}
fixed4 frag (v2f i) : SV_Target
{
float3 ligDirection = normalize(_WorldSpaceLightPos0.xyz); //シーンのディレクショナルライト方向を取得
fixed3 ligColor = _LightColor0.xyz; //ディレクショナルライトのカラーを取得
float3 refVec = reflect(-ligDirection, i.normal); //ライト方向と法線方向から反射ベクトルを計算
float3 toEye = _WorldSpaceCameraPos - i.worldPos; //カメラからの視線ベクトルを計算
toEye = normalize(toEye); //視線ベクトルを正規化
float t = dot(refVec, toEye); //反射ベクトルと視線ベクトルで内積を計算
t = max(0, t); //計算した内積のうち、t < 0は必要ないのでクランプ
t = pow(t, _ReflectionLevel); //反射の絞りを調整
float3 specularLig = ligColor * t; //内積が1に近いほど照り返しが強いとみなし、ライトカラーを強く乗算
float4 finalColor = float4(1, 1, 1, 1);
finalColor.xyz *= specularLig;
return finalColor;
}
ENDCG
}
}
}