【Unity】セルシェーディングを1から作ってみるメモ その2 アウトライン編【Shader】
前回の続きです。
アウトラインを実装していきます。
↓前回
完成形
変更点
- モデルデータを 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);
選定されたピクセルで交差するピクセルの差で判定してます。 ロバーツクロス演算子という画像処理アルゴリズムだそうです。
あとは何をもってエッジとするかですが、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 マクロを使用します。
法線バッファ によるエッジ検出
次に面の向きが急激に変化しているようであれば、エッジとします。
これには法線マップを利用します。
法線の差からエッジを抽出するコード
// 法線バッファからエッジを抽出
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 を使わないアウトライン書きました。