[Unity] Post-processing stack v2 で輪郭線抽出のカスタムエフェクトを作る

2020-04-06

経緯

こういうのが作りたかった:

こういうのを作るために輪郭線抽出系のポストエフェクトを実装しようと思った

Post-processing stack v2 のカスタムエフェクトを作る

シェーダを書く

  • Post-processing stack v2(以下 PPSv2)はシェーダの書き方が従来とちょっと違う
    • CG ではなく HLSLPROGRAM
    • SRP (Scriptable Render Pipeline) との互換性を保つためにこのようになっている
  • そのため昔書いたシェーダをそのまま持ってきても動かなかった
    • 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 をかけたりすると冒頭の映像のような表現になる
  • ポストエフェクトをかける前は、下図のように黄色いオブジェクトだったりする:
  • 映像で時々黄色っぽく光っているのは、ポストエフェクトとオリジナルの画面をブレンドすることで実現している