T.TAO
Back to Blog
/6 min read/Others

Unity Shader #1 Rim Light

Unity Shader #1 Rim Light

Unity Shader #1 リムライト

このノートでは、フレネルリムライト効果を使って基本的なシェーダー操作に慣れます。

NdotV

ライティング計算では、頂点の法線情報(N)があると仮定します。同時に、視線方向の単位ベクトルViewDir(V)もあります。

この2つのベクトルの内積の結果を考えてみましょう:

  1. N · V の結果が0に近い場合、2つのベクトルはほぼ垂直です。視線方向が法線とほぼ垂直になるのはどのような場合でしょうか?明らかに、この頂点が観測のエッジにあるときです。
  2. N · V の結果が1に近いほど、2つのベクトルはほぼ平行です。明らかに、可視範囲内で視線に見える場所にあります。

したがって、必要なのは頂点の法線です。

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

頂点シェーダー

まず、フラグメントシェーダーが頂点シェーダーから必要とする情報を考えます。

  1. クリップ空間の座標:pos : SV_POSITION
  2. オブジェクトのUV座標:uv : TEXCOORD0
  3. ワールド空間の法線:normal_world : TEXCOORD1
  4. ワールド空間の視線方向: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
}