【Unity】Post Processingで自作のポストエフェクトを実装する

自作のポストエフェクトをPost Processingのカスタムエフェクトとして実装する方法です。

Unity2018.3.1f1
Post Processing 2.1.2

Post Processingを導入する

Post Processingの導入方法は下記の記事にまとめてありますので必要に応じて参照してください。

light11.hatenadiary.com

シェーダを書く

まずはシェーダを書きます。
今回はレンダリング結果を拡大できるポストエフェクトを書いてみます。

Shader "Hidden/Custom/Scaling"
{
    SubShader
    {
        Cull Off ZWrite Off ZTest Always

        Pass
        {
            // CGPROGRAMではなくHLSLPROGRAMにする
            HLSLPROGRAM

            #pragma vertex VertDefault
            #pragma fragment Frag
            
            // VaryingsDefaultとかを使うためにインクルードする必要あり
            #include "Packages/com.unity.postprocessing/PostProcessing/Shaders/StdLib.hlsl"

            TEXTURE2D_SAMPLER2D(_MainTex, sampler_MainTex);

            float _Scale;

            float4 Frag(VaryingsDefault i) : SV_Target
            {
                // テクスチャの取り方とかはCGとは異なるので注意
                i.texcoord -= 0.5; // 0~1の値を-0.5~0.5の値にする
                i.texcoord *= 1.0 - _Scale; // スケーリング
                i.texcoord += 0.5;
                float4 color = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.texcoord);
                return color;
            }
            ENDHLSL
        }
    }
}

このシェーダはSRPとの互換性を保つことを前提としているためいくつか従来の書き方と異なる点があります。

  1. CGPROGRAMではなくHLSLPROGRAMを使う
  2. StdLib.hlslをインクルードする必要がある
  3. テクスチャのサンプリング方法など、一部APIが従来のものと異なる

3については下記にAPIがまとまっているのでこれを参照すると良いようです。
まあここは少しずつ調べていけばいいでしょう。

github.com

PostProcessEffectSettingsを書く

次にPostProcessEffectSettingsというクラスを継承したクラスを作ります。
ScriptableObjectを継承しているのでクラス名とファイル名は一致させてください。

using System;
using UnityEngine;
// UnityEngine.Rendering.PostProcessingをusing
using UnityEngine.Rendering.PostProcessing;


[Serializable] // 必ずSerializableアトリビュートを付ける
[PostProcess(typeof(ScalingRenderer), PostProcessEvent.AfterStack, "Custom/Scaling", true)]
public sealed class Scaling : PostProcessEffectSettings
{
    [Range(0f, 1f)]
    public FloatParameter scale = new FloatParameter { value = 0.0f };
    
    // 有効化する条件はこうやって指定する(ちゃんとやっておいたほうがパフォーマンスにつながりそう)
    public override bool IsEnabledAndSupported(PostProcessRenderContext context)
    {
        return base.IsEnabledAndSupported(context) || scale != 0;
    }
}

簡単な説明はコメントに書いていますが、以下に何点か補足します。

まずクラスにはPostProcessアトリビュートをつけます。
この第一引数には対応するPostProcessEffectRendererを指定しますが、これは次節で説明します。
第二引数にはポストエフェクトを掛けるタイミングを設定しますが、これも適用順序の節で後述します。
第三引数はポストエフェクトをInspectorから選択するときのパスを指定します。
第四引数はポストエフェクトをSceneビューにも適用するかどうかのフラグで、デフォルトはtrueで省略可能です。

また、Inspectorから調整できるようにするパラメータはXxxParameterという型で定義します。
組み込まれている型は下記から確認できます。

github.com

現状定義されているものは次の通りです。

  • FloatParameter
  • IntParameter
  • BoolParameter
  • ColorParameter
  • Vector2Parameter
  • Vector3Parameter
  • Vector4Parameter
  • SplineParameter
  • TextureParameter

このパラメータについてはParameterOverrideクラスを継承すれば自作できるようです。

PostProcessEffectRendererを書く

次にPostProcessEffectRendererを継承したクラスを作ります。
こちらはファイル名とクラス名を合わせる必要はないので前節のコードと同じファイルに定義しても問題ないです。

using UnityEngine;
using UnityEngine.Rendering.PostProcessing;

public sealed class ScalingRenderer : PostProcessEffectRenderer<Scale>
{
    // 初期化時の処理
    public override void Init()
    {
        base.Init();
    }

    public override void Render(PostProcessRenderContext context)
    {
        // 内部的にプールされているMaterialPropertyBlockが保存されているPropertySheetを取得
        var sheet = context.propertySheets.Get(Shader.Find("Hidden/Custom/Scaling"));

        // MaterialPropertyBlockに対してプロパティをセット
        sheet.properties.SetFloat("_Scale", settings.scale);

        // CommandBufferのBlitFullscreenTriangleを使って描画
        context.command.BlitFullscreenTriangle(context.source, context.destination, sheet, 0);
    }

    // 破棄時の処理
    public override void Release()
    {
        base.Release();
    }
}

説明はコメントに書いた通りです。
内部的にはMaterialPropertyBlockCommandBufferを使って実装しているようです。

使ってみる

これで実装は完了したので使ってみます。

Post Process VolumeのAdd effect...ボタンからCustom > Scalingを選択します。

f:id:halya_11:20190120202254p:plain

結果は次のようになります。

f:id:halya_11:20190120202446g:plain

ちゃんと動いていることが確認できました。

シェーダがビルドに含まれない?

前々節のPostProcessEffectRendererではシェーダをShader.Find()で検索しています。
そのため、Always Includedにの設定をするかResourcesに入れないとビルドに含まれないようです。

if the shader is never referenced in any of your scenes it won't get built and the effect will not work when running the game outside of the editor. Either add it to a Resources folder or put it in the Always Included Shaders list in Edit -> Project Settings -> Graphics.
Writing Custom Effects | Package Manager UI website

これは非常にミスりそうです。
未検証ですが、こんな感じでPostProcessEffectSettingsシリアライズしておけばいい気もします。

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

[Serializable]
[PostProcess(typeof(ScalingRenderer), PostProcessEvent.AfterStack, "Custom/Scaling", true)]
public sealed class Scale : PostProcessEffectSettings
{
    // シェーダの参照をScriptableObjectに持たせておく
    public Shader shader;

    [Range(0f, 1f)]
    public FloatParameter scale = new FloatParameter { value = 0.0f };
}

public sealed class ScalingRenderer : PostProcessEffectRenderer<Scale>
{
    public override void Render(PostProcessRenderContext context)
    {
        // ScriptableObjectからシェーダを取得
        var sheet = context.propertySheets.Get(settings.shader);

        sheet.properties.SetFloat("_Scale", settings.scale);
        context.command.BlitFullscreenTriangle(context.source, context.destination, sheet, 0);
    }
}

f:id:halya_11:20190120181552p:plain

カスタムポストエフェクトの適用順序を変更する

次にポストエフェクトを掛けるタイミングの制御の仕方を説明します。

まず、PostProcessEffectSettingsにつけるPostProcessアトリビュートの第二引数で、
組み込みエフェクトの前に掛けるか後に掛けるかといった大枠のタイミングが指定ができます。

第二引数 意味
PostProcessEvent.BeforeTransparent 半透明描画の前に処理
PostProcessEvent.BeforeStack 組み込みエフェクトの前に処理
PostProcessEvent.AfterStack 組み込みエフェクトの後に処理(ただしFXAAよりは前)

また、Post Process LayerのCustom Effect Sortingを使うとエフェクト毎の適用順序を変更できます。

f:id:halya_11:20190120203115p:plain

Inspectorを拡張する

カスタムポストエフェクトのInspectorを拡張する場合はPostProcessEffectEditorを継承したクラスを作成します。

using UnityEditor;
using UnityEditor.Rendering.PostProcessing;

[PostProcessEditor(typeof(Scaling))]
public sealed class ScalingEditor : PostProcessEffectEditor<Scaling>
{
    SerializedParameterOverride _scale;

    public override void OnEnable()
    {
        _scale = FindParameterOverride(x => x.scale);
    }

    public override void OnInspectorGUI()
    {
        EditorGUILayout.LabelField("Scale Settings", EditorStyles.boldLabel);
        using (new EditorGUI.IndentLevelScope()) {
            PropertyField(_scale);
        }
    }
}

OnInspectorGUI()の書き方は普通のInspector拡張と同じようにEditorGUILayoutなどを使って組んでいけばよさそうです。

FXAAを使う場合は書き方に注意する

FXAAを使っている場合、FXAAはカスタムポストエフェクトの後に処理されることになります。
このとき、FXAAはレンダリング結果のアルファ値に輝度情報が入っている前提で処理をします。

よってFXAAを有効にしていて、かつ処理順序をPostProcessEvent.AfterStackにする場合には、
アルファ値を変更しないように気を付ける必要があるようです。

関連

light11.hatenadiary.com

参考サイト

docs.unity3d.com