URPで無理やり Custom Post Processing する

# URPとCustom Post Processing
URP (Universal Render Pipeline)における公式実装によるCustom Post  Processingは**現時点(2020/06/27)で非対応**です。しかし[公式ブログ](https://blogs.unity3d.com/jp/2019/09/20/how-the-lightweight-render-pipeline-is-evolving/)には**今後対応予定**と記載されているので、この記事を読む前に最新の情報をチェックしてくださいね！

## 非対応だってよ、どうするよ
とりあえずURPのソースを見てみました。PostProcessPass.csにビルトインのポストエフェクトのパスが定義されています。
[PostProcessPass.cs](https://github.com/Unity-Technologies/Graphics/blob/647e4729c23dfb911cbf6120b76273aaa87dd8e2/com.unity.render-pipelines.universal/Runtime/Passes/PostProcessPass.cs#L175)
はい、どう見ても決め打ちでカスタムエフェクトを挟む隙などありません。PostProcessPassのことはいったん忘れます。

今回の主役は**RendererFeature**という機能になります。これはURPのForward Rendererのセットアップ中に任意の処理を挟み込める機能で、これを使えば任意のパスを設定することが可能です。今回はこいつを使って独自にポストプロセスのパスを挟み込むという算段です。

なお今回のコードは[Unity公式のURPサンプル](https://github.com/Unity-Technologies/UniversalRenderingExamples/blob/master/Assets/Scripts/Runtime/RenderPasses/Blit.cs)をベースにしていますのでそちらも参考にどうぞ。

## RendererFeature

RendererFeatureを使って任意のパスを作成するには、主にふたつのクラスが必要です。ひとつは**ScriptableRendererFeature**を継承したクラス、もうひとつは**ScriptableRenderPass**を継承したクラスです。前者はパスのセットアップと挟み込みを担当し、後者は実際のパスの動作を定義します。

とりあえず今回は画面をグレイスケール化するポストエフェクトを挟んでみます。

```csharp:CustomPostProcess.cs
using UnityEngine;
using UnityEngine.Rendering.Universal;

public class CustomPostProcess : ScriptableRendererFeature
{
    [System.Serializable]
    public class CustomPostProcessSettings
    {
        //パスの実行タイミング
        public RenderPassEvent Event = RenderPassEvent.BeforeRenderingPostProcessing;
        //使用するシェーダー
        public Shader GrayScaleShader;
    }

    public CustomPostProcessSettings settings = new CustomPostProcessSettings();

    private CustomPostProcessPass pass;

    // ScriptableRendererFeatureはScriptableObjectとしてRendererData内部に格納される。
    // ScriptableObjectのシリアライズのタイミングで呼ばれる。
    public override void Create()
    {
        this.name = "Custom PostProcess";
        pass = new CustomPostProcessPass(settings.Event, settings.GrayScaleShader);
    }

    //パスの差し込み。URPのSetupで呼ばれる。
    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        //入出力を指定してパスを初期化
        pass.Setup(renderer.cameraColorTarget, RenderTargetHandle.CameraTarget);
        //パスの差し込み
        renderer.EnqueuePass(pass);
    }
}
```
```csharp:CustomPostProcessPass.cs
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class CustomPostProcessPass : ScriptableRenderPass
{
    //CommandBufferの取得に使用する名前
    const string k_RenderCustomPostProcessingTag =
        "Render Custom PostProcessing Effects";

    //入出力
    private RenderTargetIdentifier passSource;
    private RenderTargetHandle passDestination;

    //Blitに使用するマテリアル
    private Material grayScaleMaterial;

    //一時的なレンダーターゲット（パスの入出力が同一の場合、いちど中間バッファを挟んでBlitする必要があるため）
    RenderTargetHandle m_TemporaryColorTexture;

    public CustomPostProcessPass(RenderPassEvent renderPassEvent, Shader grayScaleShader)
    {
        //パスの実行タイミングを指定
        this.renderPassEvent = renderPassEvent;
        if (grayScaleShader)
            grayScaleMaterial = new Material(grayScaleShader);

        //一時バッファの設定
        m_TemporaryColorTexture.Init("_TemporaryColorTexture");
    }

    public void Setup(RenderTargetIdentifier source, RenderTargetHandle destination)
    {
        this.passSource = source;
        this.passDestination = destination;
    }

    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    {
        //レンダリング情報（画面サイズなど）。一時バッファの作成に使用。
        RenderTextureDescriptor opaqueDesc = renderingData.cameraData.cameraTargetDescriptor;
        opaqueDesc.depthBufferBits = 0;

        var cmd = CommandBufferPool.Get(k_RenderCustomPostProcessingTag);
        
        Render(cmd, ref renderingData, opaqueDesc);

        context.ExecuteCommandBuffer(cmd);
        CommandBufferPool.Release(cmd);
    }

    void Render(CommandBuffer cmd, ref RenderingData renderingData, RenderTextureDescriptor opaqueDesc)
    {
        cmd.GetTemporaryRT(m_TemporaryColorTexture.id, opaqueDesc, FilterMode.Bilinear);

        //エフェクトの適用
        DoEffectGrayScale(cmd, passSource, m_TemporaryColorTexture, opaqueDesc);

        //出力先へコピー
        if (passDestination == RenderTargetHandle.CameraTarget)
        {
            Blit(cmd, m_TemporaryColorTexture.Identifier(), passSource);
        }
        else
        {
            Blit(cmd, m_TemporaryColorTexture.Identifier(), passDestination.Identifier());
        }
    }

    private void DoEffectGrayScale(CommandBuffer cmd, RenderTargetIdentifier source, RenderTargetHandle destination,
        RenderTextureDescriptor opaqueDesc)
    {
        Blit(cmd, source, destination.Identifier(), grayScaleMaterial, 0);
    }

    public override void FrameCleanup(CommandBuffer cmd)
    {
        if (passDestination == RenderTargetHandle.CameraTarget)
            cmd.ReleaseTemporaryRT(m_TemporaryColorTexture.id);
    }
}
```

これでRendererFeatureが定義できました。

### RendererFeatureの登録

次に、このRendererFeatureをURPに登録します。
URP Assetに設定したRenderer AssetのInspectorを開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124429/e00fd0ce-9cb2-d4be-419f-9740d731eee8.png)
下部に**Add Renderer Feature**というボタンがあるので押します。出現したメニューに先ほど作成した**Custom Post Process**の項目があるので、選択して追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124429/c891c104-8c7a-42cd-938e-eaca0cddb232.png)
これでRendererFeatureが設定できました。

### シェーダーの作成
あとは実際に画面をグレイスケール化するシェーダーを書きましょう。
URP (というかSRP)ではシェーダーは**HLSL**で書きます。ShaderLab (Cg/HLSL)と基本的な文法は同じですが、いろいろとお作法に相違点があるので注意が必要です。
URPに含まれているビルトインのポストエフェクトシェーダーを見ながら書くと手っ取り早いのでオススメ。

```GrayScale.shader
Shader "Hidden/Universal Render Pipeline/Custom/GrayScale"
{
    Properties
    {
        _MainTex("Source", 2D) = "white" {}
    }
    
    HLSLINCLUDE
        #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
        #include "Packages/com.unity.render-pipelines.universal/Shaders/PostProcessing/Common.hlsl"
        
        TEXTURE2D_X(_MainTex);
        
        half4 Frag(Varyings input) : SV_Target
        {
            float4 source = SAMPLE_TEXTURE2D_X(_MainTex, sampler_LinearClamp, input.uv);
            float y = 0.299 * source.r + 0.587 * source.g + 0.144 * source.b;
            return half4(y, y, y, 1.0);
        }
        
    ENDHLSL
    
    SubShader
    {
        Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline"}
        LOD 100
        ZTest Always ZWrite Off Cull Off

        Pass
        {
            Name "GrayScale"

            HLSLPROGRAM
                #pragma vertex Vert
                #pragma fragment Frag
            ENDHLSL
        }
    }
}

```

あとはこのシェーダーをRendererFeatureにセットして……

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124429/306cf236-cc49-ccd2-2d37-844a19112e56.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124429/8adc0bd4-5714-35ea-c626-3b2a745cc7c7.png)

見慣れた画面がグレイスケールになりました。

## おしまい
今回はサンプルとしてグレイスケール化をやってみましたが、スクリプト側でやってることはシェーダーによるBlitだけなので、適当に拡張できると思います。
あとは普通のポストプロセスと同様にVolumeからエフェクトの設定ができれば最高なのですが、そのへんは~~めんどくさいので~~公式対応に任せておくとして、こんなところで終わりにしたいと思います。