【Unity】セルシェーディングを1から作ってみるメモ その2 アウトライン編【Shader】

投稿者: | 2020年2月2日

前回の続きです。
アウトラインを実装していきます。

↓前回

前回

完成形

変更点

  • モデルデータを UnityChan ver2.0.7 に変更しました。
  • テクスチャーも変わったので修正。
  • ハイライトの調整
  • リムライトの調整
  • トゥーン処理の彩度・明度の調整

アウトラインの実装

エッジ検出には3種類の方法をあわせて使っています。

  • 深度チェック
  • 法線チェック
  • 明度チェック

深度・法線は Outline Shader 様の実装をほぼそのままつわかせていただいてます。

ポストプロセッシングを使用

実装は メッシュの膨張は使わず ポストプロセッシング(ポストエフェクト)を利用してます。
インストール方法は以前紹介しました。

隣接するピクセルとの差でエッジと判断

 基本的に何かしらの要素が隣接するピクセルとの差が大きい場合にエッジとし、ピクセルを置いていきアウトラインにしていくことになります。

選定するピクセルは Outline Shader をそのまま流用してます。

float halfScaleFloor = floor(_Scale * 0.5);
float halfScaleCeil = ceil(_Scale * 0.5);
float2 bottomLeftUV = i.texcoord - float2(_MainTex_TexelSize.x, _MainTex_TexelSize.y) * halfScaleFloor;
float2 topRightUV = i.texcoord + float2(_MainTex_TexelSize.x, _MainTex_TexelSize.y) * halfScaleCeil;  
float2 bottomRightUV = i.texcoord + float2(_MainTex_TexelSize.x * halfScaleCeil, -_MainTex_TexelSize.y * halfScaleFloor);
float2 topLeftUV = i.texcoord + float2(-_MainTex_TexelSize.x * halfScaleFloor, _MainTex_TexelSize.y * halfScaleCeil);
Outline Shader より転載

  選定されたピクセルで交差するピクセルの差で判定してます。 ロバーツクロス演算子という画像処理アルゴリズムだそうです。

クロスするピクセルで判定して精度を上げてます。

あとは何をもってエッジとするかですが、Unity のカメラでは NormalBuffer や DepthBuffer・ColorAlphaBuffer が参照可能なのでそれらを利用します。

深度バッファによるエッジ検出

深度バッファを走査していき、隣接する深度差が大きい場合エッジとしてます。

深度バッファのみを表示したもの

深度バッファ 差からエッジを抽出するコード

// 深度バッファからエッジを抽出
float depthLB = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, bottomLeftUV).r;
float depthRT = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, topRightUV).r;
float depthRB = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, bottomRightUV).r;
float depthLT = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, topLeftUV).r;

float depthThreshold = _DepthThreshold * depthLB * normalThreshold;
float depthFiniteDifference0 = depthLT - depthRB;
float depthFiniteDifference1 = depthRT - depthLB;
float edgeDepth = sqrt( pow( depthFiniteDifference0, 2) + pow( depthFiniteDifference1, 2));
edgeDepth = 1-step( edgeDepth, _DepthThreshold); // edgeDepth > _DepthThreshold ? 1 : 0;

深度バッファをフェッチする場合は SAMPLE_TEXTURE2D ではなく SAMPLE_DEPTH_TEXTURE マクロを使用します。

深度差により抽出したエッジのみレンダリング

法線バッファ によるエッジ検出

次に面の向きが急激に変化しているようであれば、エッジとします。
これには法線マップを利用します。

法線マップをRGBで出力した結果

法線の差からエッジを抽出するコード

// 法線バッファからエッジを抽出
float3 normalLB = SAMPLE_TEXTURE2D(_CameraDepthNormalsTexture, sampler_CameraDepthNormalsTexture, bottomLeftUV).rgb;
float3 normalRT = SAMPLE_TEXTURE2D(_CameraDepthNormalsTexture, sampler_CameraDepthNormalsTexture, topRightUV).rgb;
float3 normalRB = SAMPLE_TEXTURE2D(_CameraDepthNormalsTexture, sampler_CameraDepthNormalsTexture, bottomRightUV).rgb;
float3 normalLT = SAMPLE_TEXTURE2D(_CameraDepthNormalsTexture, sampler_CameraDepthNormalsTexture, topLeftUV).rgb;
float3 normalFiniteDifference0 = normalLT - normalRB;
float3 normalFiniteDifference1 = normalRT - normalLB;
float edgeNormal = sqrt( dot( normalFiniteDifference0, normalFiniteDifference0) + dot( normalFiniteDifference1, normalFiniteDifference1));
edgeNormal = 1-step( edgeNormal, _NormalThreshold); // edgeNormal > _NormalThreshold ? 1 : 0;

法線の内積を求めて 差が大きいとエッジとしています。

法線の向きの差からエッジを抽出

これらを合成した結果

深度エッジ+法線 エッジ
合成してみる

ここまでは定番のアウトライン算出ですが、このままでは テクスチャーでの色の差にアウトラインが出ていないため、セル画っぽくありません。

服と肌の境などエッジがない

色の明度差 によるエッジ検出

色の明度差でエッジを検出していきます。
ただし、既存のカラーテクスチャーから検出すると出てほしくない、目やトゥーンの境目までエッジが出てしまいます。

そこでエッジ出力用テクスチャーを用意しました。

こんなの

アウトラインマップのインスペクタは、この様になってます。
Format はR8
Mipmap は無し
が良い感じで出力されました。

これをカラーとしてレンダリングするとこのようになります。

この明度の差でエッジを抽出します。

出力先をどこにするか悩んだのですが、ステンシルに出力したくとも明度単位でPass を各羽目になるので却下。コンピュートバッファに出力するのも面倒なのでアルファチャンネルに出力します。

つまり半透明が使えないということです:p
あと、どうやらHDRモードはアルファチャンネルを利用しているようで HDRも使えません。

前回の Shader に下記のパスを追加します。

// パーツマップ書き込み
// アウトライン用にアルファチャンネルにパーツマップを書き込む
Pass
{
	ColorMask A

	CGPROGRAM

	#pragma vertex vert
	#pragma fragment frag

	struct appdata
	{
		float4 vertex : POSITION;
		float4 uv : TEXCOORD0;
	};

	struct v2f
	{
		float4 vertex : SV_POSITION;
		float2 uv : TEXCOORD0;
	};

	sampler2D _OutlineMap;
	float4 _OutlineMap_ST;
	float4 _OutlineMap_TexelSize;

	v2f vert( appdata v)
	{
		v2f o = (v2f)0;
		o.vertex = UnityObjectToClipPos( v.vertex);
		o.uv = TRANSFORM_TEX( v.uv, _OutlineMap);
		return o;
	}

	float _Scale;

	half4 frag( v2f i) : SV_Target
	{
		float sample = tex2D( _OutlineMap, i.uv);
		return sample;
	}

	ENDCG
}//Pass

アルファチャンネルにだけ書き込むためカラーマスクを設定します。

ColorMask A

ポストエフェクト側のコードにもアルファからエッジを抽出していきます。

// アルファからエッジを抽出
float alphaLB = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, bottomLeftUV).a;
float alphaRT = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, topRightUV).a;
float alphaRB = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, bottomRightUV).a;
float alphaLT = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, topLeftUV).a;
float alphaFiniteDifference0 = alphaLT - alphaRB;
float alphaFiniteDifference1 = alphaRT - alphaLB;
float edgeAlpha = sqrt( pow( alphaFiniteDifference0, 2) + pow( alphaFiniteDifference1, 2));
edgeAlpha = 1-step( edgeAlpha, 1/200.0); // edgeAlpha > 1/200.0 ? 1 : 0;

抽出結果がこちらになります。

明度エッジ
意図したところにエッジが出るようになりました。

欠点

テクセル単位のエッジの抽出のため拡大するとカクカクのエッジになってしまいます。

カメラが寄った状態

回避構想はあるのですが、暇ができたら試してみようと思います。

それぞれ3種類を合成した結果。必要ならいんは大体揃っているはず。

深度エッジ + 法線エッジ + 明度エッジ

トゥーンシェーダーと合成した結果。
おー、カワイイ

アップ画像

アウトラインエフェクトコード

ポストエフェクトプロファイル側のコードです。

using System;
using UnityEngine;
using UnityEngine.Rendering.PostProcessing;

namespace neko {

[Serializable]
[PostProcess(typeof(OutlineRenderer), PostProcessEvent.BeforeStack, "Neko/OutlineSettings")]
public sealed class OutlineSettings : PostProcessEffectSettings
{
    [Tooltip("Number of pixels between samples that are tested for an edge. When this value is 1, tested samples are adjacent.")]
    public IntParameter scale = new IntParameter { value = 2 };
    public ColorParameter color = new ColorParameter { value = Color.black };
    [Tooltip("Difference between depth values, scaled by the current depth, required to draw an edge.")]
    public FloatParameter depthThreshold = new FloatParameter { value = 0.02f };
    [Range(0, 1), Tooltip("The value at which the dot product between the surface normal and the view direction will affect " +
        "the depth threshold. This ensures that surfaces at right angles to the camera require a larger depth threshold to draw " +
        "an edge, avoiding edges being drawn along slopes.")]
    public FloatParameter depthNormalThreshold = new FloatParameter { value = 0.5f };
    [Tooltip("Scale the strength of how much the depthNormalThreshold affects the depth threshold.")]
    public FloatParameter depthNormalThresholdScale = new FloatParameter { value = 7 };
    [Range(0, 1), Tooltip("Larger values will require the difference between normals to be greater to draw an edge.")]
    public FloatParameter normalThreshold = new FloatParameter { value = 0.4f };    

	public override bool IsEnabledAndSupported( PostProcessRenderContext context)
	{
		bool state = enabled.value && scale.value > 0;
		return state;
	}
} // class OutlineSettings

public sealed class OutlineRenderer : PostProcessEffectRenderer<OutlineSettings>
{
	//----------------------------------------------------------------------------
	public override void Init() => base.Init();
	//----------------------------------------------------------------------------
	public override void Release() => base.Release();
	//----------------------------------------------------------------------------
	public override DepthTextureMode GetCameraFlags() => DepthTextureMode.DepthNormals;
	//----------------------------------------------------------------------------
    public override void Render(PostProcessRenderContext context)
    {
        var sheet = context.propertySheets.Get(Shader.Find("Hidden/Neko/Outline"));
        sheet.properties.SetFloat("_Scale", settings.scale);
        sheet.properties.SetColor("_Color", settings.color);
        sheet.properties.SetFloat("_DepthThreshold", settings.depthThreshold);
        sheet.properties.SetFloat("_DepthNormalThreshold", settings.depthNormalThreshold);
        sheet.properties.SetFloat("_DepthNormalThresholdScale", settings.depthNormalThresholdScale);
        sheet.properties.SetFloat("_NormalThreshold", settings.normalThreshold);
        sheet.properties.SetColor("_Color", settings.color);

        Matrix4x4 clipToView = GL.GetGPUProjectionMatrix(context.camera.projectionMatrix, true).inverse;
        sheet.properties.SetMatrix("_ClipToView", clipToView);

        context.command.BlitFullscreenTriangle(context.source, context.destination, sheet, 0);
    }
} // class OutlineRenderer

} // namespace neko

ファイル名とクラス名が同じでないと保存されません。←ハマった

DepthTextureMode.DepthNormals を返すようにしないと 深度バッファ・法線バッファが利用できません。 ←ハマった

public override DepthTextureMode GetCameraFlags() => DepthTextureMode.DepthNormals;

アウトラインシェーダー

/*
	参考サイト

	Outline Shader
	https://roystan.net/articles/outline-shader.html
*/

Shader "Hidden/Neko/Outline"
{
    SubShader
    {
        Cull Off ZWrite Off ZTest Always

		Pass
		{
			HLSLPROGRAM
			#pragma vertex Vert
			#pragma fragment Frag
			#include "Packages/com.unity.postprocessing/PostProcessing/Shaders/StdLib.hlsl"

			TEXTURE2D_SAMPLER2D( _MainTex, sampler_MainTex);
			TEXTURE2D_SAMPLER2D( _CameraDepthNormalsTexture, sampler_CameraDepthNormalsTexture);
			TEXTURE2D_SAMPLER2D( _CameraDepthTexture, sampler_CameraDepthTexture);

			float4 _MainTex_TexelSize;
			float4x4 _ClipToView;

			float4 alphaBlend(float4 top, float4 bottom)
			{
				float3 color = (top.rgb * top.a) + (bottom.rgb * (1 - top.a));
				float alpha = top.a + bottom.a * (1 - top.a);

				return float4(color, alpha);
			}

			struct v2f
			{
				float4 vertex : SV_POSITION;
				float2 texcoord : TEXCOORD0;
				float2 texcoordStereo : TEXCOORD1;
				float3 viewSpaceDir : TEXCOORD2;
			};

			v2f Vert( AttributesDefault v)
			{
				v2f o = (v2f)0;
				o.vertex = float4( v.vertex.xy, 0.0, 1.0);
				o.texcoord = TransformTriangleVertexToUV( v.vertex.xy);
				o.viewSpaceDir = mul(_ClipToView, o.vertex).xyz;
#if UNITY_UV_STARTS_AT_TOP
				o.texcoord = o.texcoord * float2(1.0, -1.0) + float2(0.0, 1.0);
#endif
				o.texcoordStereo = TransformStereoScreenSpaceTex( o.texcoord, 1.0);

				return o;
			}

			float _Scale;
			float _DepthThreshold;
			float _NormalThreshold;
			float4 _Color;
			float _DepthNormalThreshold;
			float _DepthNormalThresholdScale;

			float4 Frag( v2f i) : SV_Target
			{
				float halfScaleFloor = floor(_Scale * 0.5);
				float halfScaleCeil = ceil(_Scale * 0.5);
				float2 bottomLeftUV = i.texcoord - float2(_MainTex_TexelSize.x, _MainTex_TexelSize.y) * halfScaleFloor;
				float2 topRightUV = i.texcoord + float2(_MainTex_TexelSize.x, _MainTex_TexelSize.y) * halfScaleCeil;  
				float2 bottomRightUV = i.texcoord + float2(_MainTex_TexelSize.x * halfScaleCeil, -_MainTex_TexelSize.y * halfScaleFloor);
				float2 topLeftUV = i.texcoord + float2(-_MainTex_TexelSize.x * halfScaleFloor, _MainTex_TexelSize.y * halfScaleCeil);

				float depthLB = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, bottomLeftUV).r;
				float depthRT = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, topRightUV).r;
				float depthRB = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, bottomRightUV).r;
				float depthLT = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, topLeftUV).r;

				float3 normalLB = SAMPLE_TEXTURE2D(_CameraDepthNormalsTexture, sampler_CameraDepthNormalsTexture, bottomLeftUV).rgb;
				float3 normalRT = SAMPLE_TEXTURE2D(_CameraDepthNormalsTexture, sampler_CameraDepthNormalsTexture, topRightUV).rgb;
				float3 normalRB = SAMPLE_TEXTURE2D(_CameraDepthNormalsTexture, sampler_CameraDepthNormalsTexture, bottomRightUV).rgb;
				float3 normalLT = SAMPLE_TEXTURE2D(_CameraDepthNormalsTexture, sampler_CameraDepthNormalsTexture, topLeftUV).rgb;

				float3 viewNormal = normalLB * 2 - 1;
				float NdotV = 1 - dot( viewNormal, -i.viewSpaceDir);
				float normalThreshold01 = saturate((NdotV - _DepthNormalThreshold) / (1 - _DepthNormalThreshold));
				float normalThreshold = normalThreshold01 * _DepthNormalThresholdScale + 1;

				// 深度バッファからエッジを抽出
				float depthThreshold = _DepthThreshold * depthLB * normalThreshold;
				float depthFiniteDifference0 = depthRT - depthLB;
				float depthFiniteDifference1 = depthLT - depthRB;
				float edgeDepth = sqrt( pow( depthFiniteDifference0, 2) + pow( depthFiniteDifference1, 2));
				edgeDepth = 1-step( edgeDepth, _DepthThreshold); // edgeDepth > _DepthThreshold ? 1 : 0;

				// 法線バッファからエッジを抽出
				float3 normalFiniteDifference0 = normalLT - normalRB;
				float3 normalFiniteDifference1 = normalRT - normalLB;
				float edgeNormal = sqrt( dot( normalFiniteDifference0, normalFiniteDifference0) + dot( normalFiniteDifference1, normalFiniteDifference1));
				edgeNormal = 1-step( edgeNormal, _NormalThreshold); // edgeNormal > _NormalThreshold ? 1 : 0;

				float dn = max( edgeDepth, edgeNormal);


				// アルファからエッジを抽出
				float alphaLB = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, bottomLeftUV).a;
				float alphaRT = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, topRightUV).a;
				float alphaRB = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, bottomRightUV).a;
				float alphaLT = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, topLeftUV).a;
				float alphaFiniteDifference0 = alphaLT - alphaRB;
				float alphaFiniteDifference1 = alphaRT - alphaLB;
				float edgeAlpha = sqrt( pow( alphaFiniteDifference0, 2) + pow( alphaFiniteDifference1, 2));
				edgeAlpha = 1-step( edgeAlpha, 1/200.0); // edgeAlpha > 1/200.0 ? 1 : 0;

				// エッジを合成
				float edge = max( max( edgeDepth, edgeNormal), edgeAlpha);

				float4 edgeColor = float4(_Color.rgb, _Color.a * edge);
				float4 color = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.texcoord);

				return alphaBlend( edgeColor, color);
			}
			ENDHLSL
		}
    }
}

トゥーンシェーダー

Shader "Neko/Toon"
{
	Properties
	{
		_MainTex("Main Texture", 2D) = "white" {}
		[NoScaleOffset]_ToonMap( "Toon Map", 2D) = "white" {}
		_HighlightMask( "Highlight Mask", 2D) = "black" {}
		_HighlightPower("Highlight Power", Range(0, 100)) = 15
		_OutlineMap( "Outline Map", 2D) = "black" {}
		_RimMask( "RimLight Mask", 2D) = "black" {}
		_RimBias("Rim light Bias", Range(0, 1)) = 0.5
	} // Properties

	CGINCLUDE
	#include "UnityCG.cginc"
	#include "Lighting.cginc"

	// RGB->HSV変換
	float3 rgb2hsv( float3 rgb)
	{
		float3 hsv;

		// RGBの三つの値で最大のもの
		float maxValue = max( rgb.r, max( rgb.g, rgb.b));
		// RGBの三つの値で最小のもの
		float minValue = min( rgb.r, min( rgb.g, rgb.b));
		// 最大値と最小値の差
		float delta = maxValue - minValue;

		// V(明度)
		// 一番強い色をV値にする
		hsv.z = maxValue;

		// S(彩度)
		// 最大値と最小値の差を正規化して求める
		hsv.y = lerp( 0.0, delta / maxValue, abs( sign( maxValue - 0.0)));

		// H(色相)
		// RGBのうち最大値と最小値の差から求める
		if( hsv.y > 0.0)
		{
			hsv.x = lerp( 
				lerp( 4 + (rgb.r - rgb.g) / delta, 2 + (rgb.b - rgb.r) / delta, 1-abs(sign(rgb.g-maxValue))),
				(rgb.g - rgb.b) / delta,
				1-abs(sign(rgb.r - maxValue))
			) / 6.0;
			hsv.x += lerp( 0.0, 1.0, 1-step( 0, hsv.x));
		}
		return hsv;
	}

	// HSV->RGB変換
	float3 hsv2rgb( float3 hsv)
	{
		float3 rgb;

		if( hsv.y == 0)
		{
			// S(彩度)が0と等しいならば無色もしくは灰色
			rgb.r = rgb.g = rgb.b = hsv.z;
		}
		else
		{
			// 色環のH(色相)の位置とS(彩度)、V(明度)からRGB値を算出する
			hsv.x *= 6.0;
			float i = floor (hsv.x);
			float f = hsv.x - i;
			float aa = hsv.z * (1 - hsv.y);
			float bb = hsv.z * (1 - (hsv.y * f));
			float cc = hsv.z * (1 - (hsv.y * (1 - f)));

			rgb =
				lerp(
					lerp(
						lerp(
							lerp(
								lerp(
									float3( hsv.z, aa, bb),
									float3( cc, aa, hsv.z),
									1-step(5,i)
								),
								float3( aa, bb, hsv.z), 1-step(4,i)
							),
							float3( aa, hsv.z, cc), 1-step(3,i)
						),
						float3( bb, hsv.z, aa), 1-step(2,i)
					),
					float3( hsv.z, cc, aa), 1-step(1,i)
				);
		}
		return rgb;
	}

	float bias( float b, float x)
	{
		return pow( x, log(b) / log(0.5));
	}

	float gain( float g, float x)
	{
		if( x < 0.5)
		{
			return bias( 1.0 - g, 2.0 * x) / 2.0;
		}
		else
		{
			return 1.0 - bias(1.0 - g, 2.0 - 2.0 * x) / 2.0;
		}
	}

	ENDCG

	SubShader
	{
		Tags { "RenderType"="Opaque" }

		// ディフューズをレンダリング
		// ディフューズを計算してトゥーンマップを参照し、アルベドを書き込む
		Pass
		{
			CGPROGRAM

			#pragma vertex vert
			#pragma fragment frag

			struct appdata
			{
				float4 vertex : POSITION;
				float3 normal : NORMAL;
				float4 uv : TEXCOORD0;
			};

			struct v2f
			{
				float4 vertex : SV_POSITION;
				float3 normal : NORMAL;
				float4 vertexW : TEXCOORD0;
				float2 uv : TEXCOORD1;
			};

			sampler2D _MainTex;
			float4 _MainTex_ST;
			sampler2D _ToonMap;

			v2f vert( appdata v)
			{
				v2f o = (v2f)0;
				o.vertex = UnityObjectToClipPos( v.vertex);
				o.normal = UnityObjectToWorldNormal( v.normal);
				o.vertexW = mul( unity_ObjectToWorld, v.vertex);
				o.uv = TRANSFORM_TEX( v.uv, _MainTex);

				return o;
			}

			half4 frag( v2f i) : SV_Target
			{
				float3 L = normalize( _WorldSpaceLightPos0.xyz);
				float3 V = normalize( _WorldSpaceCameraPos - i.vertexW.xyz);
				float3 N = i.normal;

				float nl = max( 0.0, dot( N, L));
				float toon = 1-tex2D( _ToonMap, half2( nl, 0));

				float3 sample = tex2D( _MainTex, i.uv).rgb;
				// hsvに変換し彩度・明度を調整する
				float3 hsv = rgb2hsv( sample);
				hsv.y = min( hsv.y + toon * hsv.y, 1.0); // 彩度変更
				hsv.z = hsv.z * lerp( 0.0, 1.0, 1-toon); // 明度変更
				float4 color = float4( hsv2rgb( hsv), 1);
				return color;
			}
			ENDCG
		}


		// ハイライトをレンダリング
		// ハイライトを計算し、ハイライトマップを参照しつつ加算する
		Pass
		{
			Blend One One // 加算

			CGPROGRAM

			#pragma vertex vert
			#pragma fragment frag

			struct appdata
			{
				float4 vertex : POSITION;
				float3 normal : NORMAL;
				float4 uv : TEXCOORD0;
			};

			struct v2f
			{
				float4 vertex : SV_POSITION;
				float4 vertexW : TEXCOORD0;
				float3 normal : TEXCOORD1;
				float2 uv : TEXCOORD2;
			};

			float4 _HighlightMask_ST;

			v2f vert( appdata v)
			{
				v2f o = (v2f)0;
				o.vertex = UnityObjectToClipPos( v.vertex);
				o.vertexW = mul( unity_ObjectToWorld, v.vertex);
				o.normal = UnityObjectToWorldNormal( v.normal);
				o.uv = float2( v.uv * _HighlightMask_ST.xy + _HighlightMask_ST.zw);

				return o;
			}

			float _HighlightPower;
			sampler2D _HighlightMask;

			half4 frag( v2f i) : SV_Target
			{
				// ハイライトマップを見て描画の有無を判定する
				half4 cutoff = tex2D( _HighlightMask, i.uv);
				if( cutoff.r < 0.5)
				{
					discard; // 中止
				}

				// ハイライト計算
				float3 L = normalize( _WorldSpaceLightPos0.xyz);
				float3 V = normalize( _WorldSpaceCameraPos - i.vertexW.xyz);
				float3 N = i.normal;
				float specular = pow( max( 0.0, dot( reflect(-L, N), V)), _HighlightPower);
				if( specular < 0.5)
				{
					discard; // 中止
				}
				specular = gain( 0.8, specular) * 0.2;

				return half4( specular, specular, specular, 1);
			}

			ENDCG
		}//Pass


		// 影キャスト
		Pass
		{
			Name "ShadowCast"
			Tags {"LightMode" = "ShadowCaster"}

			CGPROGRAM

			#pragma vertex vert
			#pragma fragment frag
			#pragma multi_compile_shadowcaster

			struct v2f
			{
				V2F_SHADOW_CASTER;
			};

			v2f vert( appdata_base v)
			{
				v2f o;
				TRANSFER_SHADOW_CASTER_NORMALOFFSET( o)
				return o;
			}

			float4 frag( v2f i) : SV_Target
			{
				SHADOW_CASTER_FRAGMENT( i)
			}

			ENDCG
		}//Pass

		// 影レシーブ
		// 影の濃さからトゥーンマップを参照し、アルベドを上書きする
		Pass
		{
			Name "ShadowReceive"
			Tags {"LightMode" = "ForwardBase"}

			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#pragma multi_compile_fwdbase
			#include "AutoLight.cginc"

			struct v2f
			{
				float2 uv : TEXCOORD0;
				SHADOW_COORDS(1) // put shadows data into TEXCOORD1
				float4 pos : SV_POSITION;
			};

			sampler2D _ToonMap;
			sampler2D _MainTex;
			float4 _MainTex_ST;

			v2f vert( appdata_base v)
			{
				v2f o;
				o.pos = UnityObjectToClipPos( v.vertex);
				o.uv = TRANSFORM_TEX( v.texcoord, _MainTex);
				// compute shadows data
				TRANSFER_SHADOW(o)
				return o;
			}

			fixed4 frag(v2f i) : SV_Target
			{
				// compute shadow attenuation (1.0 = fully lit, 0.0 = fully shadowed)
				fixed shadow = SHADOW_ATTENUATION( i);
				// 濃いめの影だけ描画する
				if( shadow > 0.25)
				{
					discard; // 中止
				}
				float toon = 1-tex2D( _ToonMap, half2( shadow, 0));

				float3 sample = tex2D( _MainTex, i.uv).rgb;
				// hsvに変換し彩度・明度を調整する
				float3 hsv = rgb2hsv( sample);
				hsv.y = min( hsv.y + toon * hsv.y, 1.0); // 彩度変更
				hsv.z = hsv.z * lerp( 0.0, 1.0, 1-toon); // 明度変更
				float3 color = hsv2rgb( hsv);
				return float4( color, 1);
			}
			ENDCG
		}//Pass


		// リムライト
		Pass
		{
			Blend OneMinusDstColor One // スクリーン / 比較(明)

			CGPROGRAM

			#pragma vertex vert
			#pragma fragment frag

			struct appdata
			{
				float4 vertex : POSITION;
				float3 normal : NORMAL;
				float4 uv : TEXCOORD0;
			};

			struct v2f
			{
				float4 vertex : SV_POSITION;
				float4 vertexW : TEXCOORD0;
				float3 normal : TEXCOORD1;
				float2 uv : TEXCOORD2;
			};

			sampler2D _RimMask;
			float4 _RimMask_ST;

			v2f vert( appdata v)
			{
				v2f o = (v2f)0;
				o.vertex = UnityObjectToClipPos( v.vertex);
				o.vertexW = mul( unity_ObjectToWorld, v.vertex);
				o.normal = UnityObjectToWorldNormal( v.normal);
				o.uv = float2( v.uv * _RimMask_ST.xy + _RimMask_ST.zw);

				return o;
			}

			float _RimBias;

			half4 frag( v2f i) : SV_Target
			{
				// リムライトマスクを見て描画の有無を判定する
				half4 cutoff = tex2D( _RimMask, i.uv);
				if( cutoff.r < 0.5)
				{
					discard; // 中止
				}

				float3 V = normalize( _WorldSpaceCameraPos - i.vertexW.xyz);
				float rim = 1 - saturate( dot( i.normal, normalize( V)));
				rim = pow( rim, log( _RimBias) / log( 0.5));

				return float4( rim, rim, rim, 1);
			}

			ENDCG
		}//Pass


		// パーツマップ書き込み
		// アウトライン用にアルファチャンネルにパーツマップを書き込む
		Pass
		{
			ColorMask A

			CGPROGRAM

			#pragma vertex vert
			#pragma fragment frag

			struct appdata
			{
				float4 vertex : POSITION;
				float4 uv : TEXCOORD0;
			};

			struct v2f
			{
				float4 vertex : SV_POSITION;
				float2 uv : TEXCOORD0;
			};

			sampler2D _OutlineMap;
			float4 _OutlineMap_ST;
			float4 _OutlineMap_TexelSize;

			v2f vert( appdata v)
			{
				v2f o = (v2f)0;
				o.vertex = UnityObjectToClipPos( v.vertex);
				o.uv = TRANSFORM_TEX( v.uv, _OutlineMap);
				return o;
			}

			float _Scale;

			half4 frag( v2f i) : SV_Target
			{
				float sample = tex2D( _OutlineMap, i.uv);
				return sample;
			}

			ENDCG
		}//Pass


	}//SubShader
	FallBack "Diffuse"
}
この作品はユニティちゃんライセンス条項の元に提供されています

Post-process を使わないアウトライン書きました。

【Unity】セルシェーディングを1から作ってみるメモ その2 アウトライン編【Shader】」への3件のフィードバック

  1. ピンバック: 【Unity】セルシェーディングを1から作ってみるメモ【Shader】 | | ぶろねこ -Blog on NEKOTEAM-

  2. ピンバック: 【Unity】アウトラインを再考察してみた【車輪の再発明】 | | ぶろねこ -Blog on NEKOTEAM-

  3. ピンバック: 【Unity】セルシェーディングを1から作ってみるメモ その3 脱Post-process【Shader】 | | ぶろねこ -Blog on NEKOTEAM-

コメントを残す