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