[Unity] Post-processing stack v2 で輪郭線抽出のカスタムエフェクトを作る
2020-04-06
経緯
こういうのが作りたかった:
こういうのを作るために輪郭線抽出系のポストエフェクトを実装しようと思った
Post-processing stack v2 のカスタムエフェクトを作る
- 輪郭線抽出系のポストエフェクトは前に作って遊んだことがあったが、
Graphics.Blit()
経由で実装していた - Unity でポストエフェクトをやるには Post-processing stack という仕組みがあるが、 調べるとカスタムエフェクトも書けるらしいので試してみようと思った
- 公式マニュアルや世のブログを参考にやってみる:
シェーダを書く
- Post-processing stack v2(以下 PPSv2)はシェーダの書き方が従来とちょっと違う
- CG ではなく
HLSLPROGRAM
- SRP (Scriptable Render Pipeline) との互換性を保つためにこのようになっている
- CG ではなく
- そのため昔書いたシェーダをそのまま持ってきても動かなかった
- Web の資料やドキュメントを適当に漁りながら手探りで書いていく
- 最終的に以下のようなシェーダで PPSv2 向けの輪郭線抽出エフェクトになった:
Shader "Hidden/Custom/DetectEdge"
{
HLSLINCLUDE
#include "Packages/com.unity.postprocessing/PostProcessing/Shaders/StdLib.hlsl"
TEXTURE2D_SAMPLER2D(_MainTex, sampler_MainTex);
uniform float4 _MainTex_TexelSize;
half4 _MainTex_ST;
uniform half _Blend;
uniform half _BlendScale;
uniform half _SampleDistance;
uniform half _Sensitivity;
uniform half _Threshold;
uniform half4 _EdgeColor;
uniform half _Negaposi;
struct v2f {
float4 pos : SV_POSITION;
float2 uv[5] : TEXCOORD0;
};
v2f vert(AttributesDefault v)
{
v2f o;
o.pos = float4(v.vertex.xy, 0.0, 1.0);
float2 uv = TransformTriangleVertexToUV(v.vertex.xy);
#if UNITY_UV_STARTS_AT_TOP
uv = uv * float2(1.0,-1.0) + float2(0.0, 1.0);
#endif
o.uv[0] = UnityStereoScreenSpaceUVAdjust(uv, _MainTex_ST);
o.uv[1] = UnityStereoScreenSpaceUVAdjust(uv + _MainTex_TexelSize.xy * half2( 1, 1) * _SampleDistance, _MainTex_ST);
o.uv[2] = UnityStereoScreenSpaceUVAdjust(uv + _MainTex_TexelSize.xy * half2(-1,-1) * _SampleDistance, _MainTex_ST);
o.uv[3] = UnityStereoScreenSpaceUVAdjust(uv + _MainTex_TexelSize.xy * half2(-1, 1) * _SampleDistance, _MainTex_ST);
o.uv[4] = UnityStereoScreenSpaceUVAdjust(uv + _MainTex_TexelSize.xy * half2( 1,-1) * _SampleDistance, _MainTex_ST);
return o;
}
half4 frag(v2f i) : SV_Target {
half2 uv = (i.uv[0] - 0.5) * _BlendScale + 0.5;
half4 col0 = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv);
half3 col1 = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv[1]);
half3 col2 = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv[2]);
half3 col3 = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv[3]);
half3 col4 = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv[4]);
float3 d1 = col2 - col1;
float3 d2 = col4 - col3;
float d = sqrt(dot(d1, d1) + dot(d2, d2));
half4 edge = d * _Sensitivity;
half4 result = _EdgeColor * saturate(edge - _Threshold);
half4 nega = half4(1 - result.r, 1 - result.g, 1 - result.b, 1);
result = lerp(result, nega, _Negaposi);
return lerp(col0, result, _Blend);
}
ENDHLSL
SubShader
{
Cull Off ZWrite Off ZTest Always
Pass
{
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
ENDHLSL
}
}
}
- ※ 演出用にネガポジとか余計なものも混じってる
- 輪郭線抽出のアルゴリズムは Roverts Cross というやつ
- Roberts cross - Wikipedia
- (4 点サンプリングで行うもので、ベーシックな Sobel Filter と比較して軽い)
設定用のクラスを書く
PostProcessEffectSettings
を継承したシェーダのパラメータ設定用クラスと、PostProcessEffectRenderer
を継承したクラスを書く- これで PPSv2 向けカスタムエフェクトが使えるようになる
using System;
using UnityEngine;
using UnityEngine.Rendering.PostProcessing;
[Serializable]
[PostProcess(typeof(DetectEdgeRenderer), PostProcessEvent.BeforeStack, "Custom/DetectEdge")]
public sealed class DetectEdge : PostProcessEffectSettings
{
[Range(0f, 1f)]
public FloatParameter blend = new FloatParameter { value = 1.0f };
[Range(0f, 2f)]
public FloatParameter blendScale = new FloatParameter { value = 1.0f };
[Range(0f, 8f)]
public FloatParameter sampleDistance = new FloatParameter { value = 1.0f };
[Range(0f, 8f)]
public FloatParameter sensitivity = new FloatParameter { value = 0.5f };
[Range(-1f, 1f)]
public FloatParameter threshold = new FloatParameter { value = -0.15f };
public ColorParameter edgeColor = new ColorParameter { value = Color.white };
[Range(-1f, 3f)]
public FloatParameter negaposi = new FloatParameter { value = 0f };
public override bool IsEnabledAndSupported(PostProcessRenderContext context)
{
return base.IsEnabledAndSupported(context) && blend >= 0.1;
}
}
public sealed class DetectEdgeRenderer : PostProcessEffectRenderer<DetectEdge>
{
public override void Render(PostProcessRenderContext context)
{
var sheet = context.propertySheets.Get(Shader.Find("Hidden/Custom/DetectEdge"));
sheet.properties.SetFloat("_Blend", settings.blend);
sheet.properties.SetFloat("_BlendScale", settings.blendScale);
sheet.properties.SetFloat("_SampleDistance", settings.sampleDistance);
sheet.properties.SetFloat("_Sensitivity", settings.sensitivity);
sheet.properties.SetFloat("_Threshold", settings.threshold);
sheet.properties.SetColor("_EdgeColor", settings.edgeColor);
sheet.properties.SetFloat("_Negaposi", settings.negaposi);
context.command.BlitFullscreenTriangle(context.source, context.destination, sheet, 0);
}
}
できた
- あとはスクリプトでオブジェクトをごちゃごちゃ出したり、 シェーダのパラメータを動的になんやかやしつつ、 適当に Bloom をかけたりすると冒頭の映像のような表現になる
- ポストエフェクトをかける前は、下図のように黄色いオブジェクトだったりする:
- 映像で時々黄色っぽく光っているのは、ポストエフェクトとオリジナルの画面をブレンドすることで実現している