Unity Shader #1 リムライト
このノートでは、フレネルリムライト効果を使って基本的なシェーダー操作に慣れます。
NdotV
ライティング計算では、頂点の法線情報(N)があると仮定します。同時に、視線方向の単位ベクトルViewDir(V)もあります。
この2つのベクトルの内積の結果を考えてみましょう:
- N · V の結果が0に近い場合、2つのベクトルはほぼ垂直です。視線方向が法線とほぼ垂直になるのはどのような場合でしょうか?明らかに、この頂点が観測のエッジにあるときです。
- N · V の結果が1に近いほど、2つのベクトルはほぼ平行です。明らかに、可視範囲内で視線に見える場所にあります。
したがって、必要なのは頂点の法線です。
Plain Textstruct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
};
頂点シェーダー
まず、フラグメントシェーダーが頂点シェーダーから必要とする情報を考えます。
- クリップ空間の座標:pos : SV_POSITION
- オブジェクトのUV座標:uv : TEXCOORD0
- ワールド空間の法線:normal_world : TEXCOORD1
- ワールド空間の視線方向:view_world: TEXCOORD2
Plain Textstruct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float3 normal_world : TEXCOORD1;
float3 view_world: TEXCOORD2;
};
頂点シェーダーでは、モデル空間の座標(vertex : POSITION)をクリップ空間に変換する(ここではUnityObjectToClipPos()関数を直接使用します。実際、mulと以前の変換行列を使うと、Unityが自動的にこれに置き換えます)ことに加えて、法線をモデル座標からワールド座標に変換する必要があります。
Plain Textv2f vert(appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.normal_world = normalize(mul(float4(v.normal, 0.0), unity_WorldToObject).xyz);
o.uv = v.uv * _MainTex_ST.xy + _MainTex_ST.zw;
return o;
}
視線方向ベクトルも必要です。
Plain Textv2f vert(appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.normal_world = normalize(mul(float4(v.normal, 0.0), unity_WorldToObject).xyz);
float3 pos_world = mul(unity_ObjectToWorld, v.vertex);
// _WorldSpaceCameraPosはUnityCG.cgincヘッダーから取得
o.view_world = normalize(_WorldSpaceCameraPos.xyz - pos_world);
o.uv = v.uv * _MainTex_ST.xy + _MainTex_ST.zw;
return o;
}
最後にフラグメントシェーダーで、法線と視線方向の内積であるNdotV部分を計算します。
Plain Textfloat4 frag(v2f i) : SV_Target
{
float3 normal_world = normalize(i.normal_world);
float3 view_world = normalize(i.view_world);
float NdotV = dot(normal_world, view_world);
return NdotV.xxxx;
}
正規化された2つのベクトルの内積は(-1,1)の範囲に収まるはずなので、NdotVの結果にsaturate()を適用するのが理にかなっています。リムライトは1とsaturate(NdotV)の差でよいです。
Plain Textfloat4 frag(v2f i) : SV_Target
{
float3 normal_world = normalize(i.normal_world);
float3 view_world = normalize(i.view_world);
float NdotV = dot(normal_world, view_world);
float rim = 1.0 - saturate(NdotV);
return rim.xxxx;
}
ブレンドモードBlend SrcAlpha Oneを使うと、とてもきれいなリムライト効果が得られます。
リムライトの色を調整したい場合は、rimをアルファ値として使い、色プロパティで最終的な仕上がりを決定できます。
Plain Textfloat4 frag(v2f i) : SV_Target
{
float3 normal_world = normalize(i.normal_world);
float3 view_world = normalize(i.view_world);
// _Colorはfloat4プロパティ、_Emissはfloatプロパティ
float3 color = _Color.xyz * _Emiss;
float NdotV = dot(normal_world, view_world);
float rim = saturate((1.0 - saturate(NdotV)) * _Emiss);
return float4(color, rim);
}
リムライトのフォールオフを調整
pow()を使って値のフォールオフを調整するのはシェーダーコードでよく使われるテクニックです。フォールオフに制御を加えたい場合は、別のプロパティを追加するだけです。
Plain TextProperties
{
// 他のプロパティはここで省略
_RimFalloff("RimFalloff", Float) = 1.0
}
float4 frag(v2f i) : SV_Target
{
float3 normal_world = normalize(i.normal_world);
float3 view_world = normalize(i.view_world);
float3 color = _Color.xyz * _Emiss;
float NdotV = saturate(dot(normal_world, view_world));
float fresnel = pow((1.0 - NdotV), _RimFalloff);
float rim = saturate(fresnel * _Emiss);
return float4(color, rim);
}
RimFalloffを調整すると、リムライトをより境界に寄せることができます。
X線効果の修正
上記の実装ではモデルの内部が見えてしまい、望ましくない場合があります。X線効果を修正する必要があります。
ステップ1:ZWriteをオンにする。特定の視点からは改善されますが、まだゴーストのような内部の可視性が残っています。
ステップ2:前に書いたPassの前に別のPassを追加する。
Plain TextPass
{
Cull Off
ZWrite On
// 深度のみを書き込む
ColorMask 0
CGPROGRAM
float4 _MainColor;
#pragma vertex vert
#pragma fragment frag
float4 vert(float4 vertexPos : POSITION) : SV_POSITION
{
return UnityObjectToClipPos(vertexPos);
}
float4 frag(void) : COLOR
{
return _MainColor;
}
ENDCG
}
