UniversalRPでのミニマムなカスタムPostProcessを作成する

UnityのUniversalRenderingPipeline(URP)で出来るだけ小さめに自前のポストプロセスを作成する手順をまとめておきます。
今回は以前作ったモノクロImageEffectをベースにURPのPostProcessを作ってみます。
[シンプルなImageEffectその２。モノクロ。](https://qiita.com/ScreenPocket/items/4e33b8f4a2925f109e97)

![スクリーンショット 2021-02-03 020621.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61266/f7b29687-54af-5f6e-b45e-07305aa1ac11.png)

また、今回の記事を書く上で、t-matsunagaさんの
[【Unity】UniversalRPでカスタムポストプロセスを作る【ZoomBlur】](https://qiita.com/t-matsunaga/items/09343ae7c683269374c4)
記事を参考にさせて頂きました。

なお、今回の記事はURPを導入する方法の記事ではなく、
**すでにURP環境になったプロジェクトに自前のカスタムPostProcessを追加するための手順メモ**です。
URPの導入方法については既にたくさんの記事が存在しているはずなので別途検索してください。

#必要な要素
最低限必要なものは下記です。
URPのPostProcessはPostProcessingStackV2(PPSV2)とはまた違った要素が必要です。

|ファイル|備考|
|:--|:--|
|Monochrome.shader|ポストエフェクトをかける際に使用するシェーダ。HLSLで記載する必要あり|
|Monochrome.cs|VolumeComponent派生。(PPSV2で言うところのPostProcessEffectSettings派生)調整するべきパラメータなどがこちらに格納されます|
|PostProcessPass.cs|ScriptableRenderPass派生(PPSV2で言うところのPostProcessEffectRenderer)と似た役割)。メインの描画処理|
|MainRendererFeature.cs|ScriptableRendererFeature派生。Rendererに描画を差し込むための仕組み|
コードが4つも必要なのか…とこの時点で少々しんどいですが、上から順に見ていきましょう

#各ファイルについて
##Monochrome.shader
先ずはシェーダから見ていきます

```glsl:Monochrome.shader
Shader "ScreenPocket/PostProcess/Monochrome"
{
    SubShader
    {
        Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline"}
        Cull Off ZWrite Off ZTest Always

        Pass
        {
            HLSLPROGRAM
            #include "Packages/com.unity.render-pipelines.universal/Shaders/PostProcessing/Common.hlsl"

            #pragma vertex FullscreenVert
            #pragma fragment Frag

            TEXTURE2D_X(_MainTex);
            SAMPLER(sampler_MainTex);
            float _Weight;

            half4 Frag(Varyings input) : SV_Target
            {
                half4 col = SAMPLE_TEXTURE2D_X( _MainTex, sampler_MainTex, input.uv );
                half luminance = Luminance(col.rgb);
                return half4(lerp(col.rgb, half3(luminance,luminance,luminance),_Weight),1);
            }
            ENDHLSL
        }
    }
}
```
ポイントは

* CGPROGRAM→HLSLPOGRAMになっている点
* それに伴ってfixed4を使わなくなっている点
* 頂点シェーダはUnityが準備してくれているFullscreenVertを使用している点
* _MainTexの定義やサンプリングの書式が変わっている

辺りでしょうか。


##Monochrome.cs
次は調整用のパラメータをまとめたファイルです

```cs:Monochrome.cs
using System;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

namespace ScreenPocket.PostProcess
{
    [Serializable, VolumeComponentMenu("ScreenPocket/PostProcess/Monochrome")]
    public sealed class Monochrome : VolumeComponent, IPostProcessComponent
    {
        public ClampedFloatParameter weight = new ClampedFloatParameter(0f, 0f, 1f);

        public bool IsActive() => weight.value > 0f;

        public bool IsTileCompatible() => false;
    }
}

```
PPSV2では new FloatParameter {value = 0f}; としていたパラメータの初期化部分が　new FloatParameter(0f);に代わっているあたりがポイントでしょうか。
また、Attributeの"ScreenPocket/PostProcess/Monochrome"がProfileにAddする際の名前付けとなります

##PostProcessPass.cs
ポイントはPPSV2の時には名前がMonochromePassのですが、そうではなくPostProcessPassになっている点。
つまりMonochrome専用の描画処理ではなくMonochrome以外も含めたPostProcess描画として使用します。

```cs:PostProcessPass.cs
using ScreenPocket.PostProcess;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

namespace ScreenPocket.Rendering
{
    public sealed class PostProcessPass : ScriptableRenderPass
    {
        private static readonly int keepFrameBuffer = Shader.PropertyToID("KeepFrameBuffer");
        private Monochrome _monochrome;
        private Material _monochromeMaterial;
        private RenderTargetIdentifier _target;
        
        private const string renderPostProcessingTag = "Render PostProcessing Effects";

        private static readonly ProfilingSampler profilingRenderPostProcessing = new ProfilingSampler(renderPostProcessingTag);
        public PostProcessPass(RenderPassEvent evt)
        {
            base.profilingSampler = new ProfilingSampler(nameof(PostProcessPass));
            renderPassEvent = evt;
            var shaderName = "ScreenPocket/PostProcess/Monochrome";
            var shader = Shader.Find(shaderName);
            if (shader == null)
            {
                Debug.LogError($"Not found shader!{shaderName}");
                return;
            }
            _monochromeMaterial = CoreUtils.CreateEngineMaterial(shader);
        }

        public void Setup(RenderTargetIdentifier target)
        {
            _target = target;
        }
        
        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
            if (!renderingData.cameraData.postProcessEnabled)
            {
                return;
            }

            var stack = VolumeManager.instance.stack;
            _monochrome = stack.GetComponent<Monochrome>();
            if (_monochrome == null)
            {
                return;
            }

            if (!_monochrome.IsActive())
            {
                return;
            }

            var cmd = CommandBufferPool.Get();
            using (new ProfilingScope(cmd, profilingRenderPostProcessing))
            {
                ref var cameraData = ref renderingData.cameraData;
                var destination = keepFrameBuffer;
                
                _monochromeMaterial.SetFloat("_Weight", _monochrome.weight.value);

                var width = cameraData.cameraTargetDescriptor.width;
                var height = cameraData.cameraTargetDescriptor.height;
                
                cmd.GetTemporaryRT(destination, width, height,
                    0, FilterMode.Point, RenderTextureFormat.Default);
                cmd.SetGlobalTexture("_MainTex", destination);
                cmd.Blit(_target, destination);
                cmd.Blit(destination, _target, _monochromeMaterial, 0);
                cmd.ReleaseTemporaryRT(destination);
            }
            context.ExecuteCommandBuffer(cmd);
            CommandBufferPool.Release(cmd);
        }

        public void Cleanup()
        {
            CoreUtils.Destroy(_monochromeMaterial);
        }
    }
}

```

##MainRendererFeature.cs
最後に、描画を差し込むためのRendererFeatureを準備します

```cs:MainRendererFeature.cs
using UnityEngine.Rendering.Universal;

namespace ScreenPocket.Rendering
{
    public sealed class MainRendererFeature : ScriptableRendererFeature
    {
        private PostProcessPass _postProcessPass;

        public override void Create()
        {
            _postProcessPass = new PostProcessPass(RenderPassEvent.BeforeRenderingPostProcessing);
        }

        public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
        {
            _postProcessPass.Setup(renderer.cameraColorTarget);
            renderer.EnqueuePass(_postProcessPass);
        }

        protected override void Dispose(bool disposing)
        {
            _postProcessPass.Cleanup();
            base.Dispose(disposing);
        }
    }
}

```

#Unityへの流し込み
とりあえずやるべき事を箇条書きにします

1. シーン内に空のGameObjectを追加し、Volumeコンポーネントを付ける（以下VolumeObject）
2. VolumeコンポーネントのProfileを新規に作るか、すでに有るProfileを設定する
3. Profileに自分が作ったPostProcess（ここで言うScreenPocket/PostProcess/Monochrome）を追加
4. シーン内カメラのCamera > PostProcess　のチェックボックスにチェックを入れる
5. もしVolumeObjectのレイヤーがDefaultではないのならCamera > Environment > Volume MaskでVolumeObjectのレイヤーを見えるようにしておく
6. RendererのAdd RendererFeatureを選択して、MainRendererFeatureを追加しておく

とすることで、画面のモノクロ化が達成できるかと思います。
![スクリーンショット 2021-02-03 020621.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61266/f7b29687-54af-5f6e-b45e-07305aa1ac11.png)


#まとめ
という事で、出来るだけ要素を少なめにした上でのカスタムPostProcessの追加方法の紹介でした。
「全然少なくないじゃん」と思うかもしれませんが自分もそう思います。
更に詳しく知りたい方は下記の参考資料の欄にある、t-matsunagaさんの記事を参考にされるとなお理解が深まるかと思います。

参考資料
[Unity-Technologies/Graphics: Unity Graphics - Including Scriptable Render Pipeline](https://github.com/Unity-Technologies/Graphics)
[【Unity】UniversalRPでカスタムポストプロセスを作る【ZoomBlur】](https://qiita.com/t-matsunaga/items/09343ae7c683269374c4)