【Unity】UniversalRPでカスタムポストプロセスを作る【ZoomBlur】

# 概要
本記事は[Unity #2 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/unity2)　の15日目の記事です。
今回UnityのUniversalRenderPipeline（以下、UniversalRP）を使用してZoomBlur【咆哮のポストエフェクト】を実装しました。
![ZoomBlur.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148284/bc5f300f-23fd-4b07-08de-19a6247988c3.gif)

この素晴らしいドラゴンは[アセットストア](https://assetstore.unity.com/packages/3d/characters/creatures/dragon-the-terror-bringer-and-dragon-boar-77121)から無料でダウンロードしました。感謝:relaxed:

# 環境
下記の環境で実装しております。

- Unity2019.3.0b12
- Windows10
- UniversalRP 7.1.5

# UniversalRPのVolumeとPostProcessingStackv2の比較

ppsv2に相当する機能が、`UniversalRP`では`Volume`という名前に変わっていました。
他にもクラス名を比較すると下記のような対応関係になっています。

| ppsv2            |       UniversalRenderPipeline |
|-----------------|------------------|
| PostProcessEffectSettings | VolumeComponent |
| PostProcessProfile | VolumeProfile |
| PostProcessManager | VolumeManager |

ppsv2では`PostProcessEffectSettings`と`PostProcessEffectRenderer`を継承してカスタムエフェクトを作成しました。
[参考:Writing-Custom-Effects](https://github.com/Unity-Technologies/PostProcessing/wiki/Writing-Custom-Effects)

UniversalRPでは公式的にカスタムエフェクトの実装方法は提供されていませんが
下記のような手法で実装できました。

# 実装

まずは`VolumeComponent`を継承してカスタムエフェクトのパラメータを定義してみましょう。

### VolumeComponent
`VolumeComponent`を継承してParameterを定義します。

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

public class ZoomBlur : VolumeComponent, IPostProcessComponent
{
    [Range(0f, 100f), Tooltip("強くすることで強いBlurがかかります。")]
    public FloatParameter focusPower = new FloatParameter(0f);

    [Range(0, 10), Tooltip("値が大きいほど綺麗にでますが負荷が高まるので注意してください。")]
    public IntParameter focusDetail = new IntParameter(5);

    [Tooltip("ブラーの中心座標。スクリーンの中心を(0,0)としています。")]
    public Vector2Parameter focusScreenPosition = new Vector2Parameter(Vector2.zero);

    [Tooltip("基準となる横幅解像度です。")]
    public IntParameter referenceResolutionX = new IntParameter(1334);

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

    public bool IsTileCompatible() => false;
}
```

### ScriptableRendererFeatureとScriptableRenderPass
次に独自の描画パスを差し込めるようにするために２つのクラスとシェーダーを実装します。

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

public class ZoomBlurRenderFeature : ScriptableRendererFeature
{
    ZoomBlurPass zoomBlurPass;

    public override void Create()
    {
        zoomBlurPass = new ZoomBlurPass(RenderPassEvent.BeforeRenderingPostProcessing);
    }

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

```csharp:ZoomBlurPass.cs
public class ZoomBlurPass : ScriptableRenderPass
{
    static readonly string k_RenderTag = "Render ZoomBlur Effects";
    static readonly int MainTexId = Shader.PropertyToID("_MainTex");
    static readonly int TempTargetId = Shader.PropertyToID("_TempTargetZoomBlur");
    static readonly int FocusPowerId = Shader.PropertyToID("_FocusPower");
    static readonly int FocusDetailId = Shader.PropertyToID("_FocusDetail");
    static readonly int FocusScreenPositionId = Shader.PropertyToID("_FocusScreenPosition");
    static readonly int ReferenceResolutionXId = Shader.PropertyToID("_ReferenceResolutionX");
    ZoomBlur zoomBlur;
    Material zoomBlurMaterial;
    RenderTargetIdentifier currentTarget;

    public ZoomBlurPass(RenderPassEvent evt)
    {
        renderPassEvent = evt;
        var shader = Shader.Find("PostEffect/ZoomBlur");
        if (shader == null)
        {
            Debug.LogError("Shader not found.");
            return;
        }
        zoomBlurMaterial = CoreUtils.CreateEngineMaterial(shader);
    }

    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    {
        if (zoomBlurMaterial == null)
        {
            Debug.LogError("Material not created.");
            return;
        }

        if (!renderingData.cameraData.postProcessEnabled) return;

        var stack = VolumeManager.instance.stack;
        zoomBlur = stack.GetComponent<ZoomBlur>();
        if (zoomBlur == null) { return; }
        if (!zoomBlur.IsActive()) { return; }

        var cmd = CommandBufferPool.Get(k_RenderTag);
        Render(cmd, ref renderingData);
        context.ExecuteCommandBuffer(cmd);
        CommandBufferPool.Release(cmd);
    }

    public void Setup(in RenderTargetIdentifier currentTarget)
    {
        this.currentTarget = currentTarget;
    }

    void Render(CommandBuffer cmd, ref RenderingData renderingData)
    {
        ref var cameraData = ref renderingData.cameraData;
        var source = currentTarget;
        int destination = TempTargetId;

        var w = cameraData.camera.scaledPixelWidth;
        var h = cameraData.camera.scaledPixelHeight;
        zoomBlurMaterial.SetFloat(FocusPowerId, zoomBlur.focusPower.value);
        zoomBlurMaterial.SetInt(FocusDetailId, zoomBlur.focusDetail.value);
        zoomBlurMaterial.SetVector(FocusScreenPositionId, zoomBlur.focusScreenPosition.value);
        zoomBlurMaterial.SetInt(ReferenceResolutionXId, zoomBlur.referenceResolutionX.value);

        int shaderPass = 0;
        cmd.SetGlobalTexture(MainTexId, source);
        cmd.GetTemporaryRT(destination, w, h, 0, FilterMode.Point, RenderTextureFormat.Default);
        cmd.Blit(source, destination);
        cmd.Blit(destination, source, zoomBlurMaterial, shaderPass);
    }
}
```


### シェーダー
シェーダーは[ShaderToy](https://www.shadertoy.com/view/3slSRj)をUnity用にカスタムして使わせていただきました。

```csharp:ZoomBlur.shader
Shader "PostEffect/ZoomBlur"
{
    Properties
    {
        _MainTex("Texture", 2D) = "white" {}
    }
    
    SubShader
    {
        Cull Off ZWrite Off ZTest Always
        Tags { "RenderPipeline" = "UniversalPipeline"}
        Pass
        {
            CGPROGRAM
                #pragma vertex Vert
                #pragma fragment Frag
                
                sampler2D _MainTex;
                float2 _FocusScreenPosition;
                float _FocusPower;
                int _FocusDetail;
                int _ReferenceResolutionX;

                struct appdata
                {
                    float4 vertex : POSITION;
                    float2 uv : TEXCOORD0;
                };

                struct v2f
                {
                    float2 uv : TEXCOORD0;
                    float4 vertex : SV_POSITION;
                };

                v2f Vert(appdata v)
                {
                    v2f o;
                    o.vertex = UnityObjectToClipPos(v.vertex);
                    o.uv = v.uv;
                    return o;
                }

                float4 Frag (v2f i) : SV_Target
                {
                    float2 screenPoint = _FocusScreenPosition + _ScreenParams.xy/2;
                    float2 uv = i.uv;
                    float2 mousePos = (screenPoint.xy / _ScreenParams.xy);
                    float2 focus = uv - mousePos;
                    fixed aspectX = _ScreenParams.x/_ReferenceResolutionX;
                    float4 outColor = float4(0, 0, 0, 1);
                    for (int i=0; i<_FocusDetail; i++) {
                        float power = 1.0 - _FocusPower * (1.0/_ScreenParams.x * aspectX) * float(i);
                        outColor.rgb += tex2D(_MainTex , focus * power + mousePos).rgb;
                    }
                    outColor.rgb *= 1.0 / float(_FocusDetail);
                    return outColor;
                }
            ENDCG
        }
    }
}

```

# セットアップ
Main CameraのPost Processingはチェックを忘れずに入れておきます。

![2019-12-08_23h36_51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148284/df99ea18-913e-0c39-44d9-155c1cf2a1ac.png)

### Volumeの追加
`ヒエラルキーの右クリック/Volume/Global Volume`でオブジェクトを追加します。
![2019-12-09_11h30_53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148284/b0d57a04-626c-897a-fd5e-d8d5c5c1160a.png)

`Add Override`にて`Zoom Blur`を追加します。
プロファイルが設定されていなければ`New`ボタンでプロファイルアセットを作成します。
![2019-12-08_23h32_33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148284/38332139-60f5-b318-18fe-119b3014783f.png)
（Bloomは趣味でつけてます）

### RenderPipelineアセットの作成とRenderFeatureの登録
`UniversalRenderPipelineAsset`の作成をします。
`Create/Rendering/Universal Render Pipeline/Pipeline Asset(Forward Renderer)`
![2019-12-09_11h33_41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148284/cb0803e7-0c32-20aa-9c3a-b88376ac10e5.png)

同時に作成される`UniversalRenderPipelineAsset_Renderer`に`+`ボタンで`Zoom Blur Render Feature`を登録します。
![2019-12-09_11h36_24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148284/12b8a51a-2172-b849-9dd4-70d008e171dc.png)

`ProjectSettings/Graphics/Scriptable Render Pipeline Settings`に`UniversalRenderPipelineAsset`をセットします。
![2019-12-09_11h41_03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148284/a6c85ec0-4237-1e6a-c1b9-b711daeb20bf.png)

これで独自のポストプロセス描画パスが反映されるようになりました。

# アニメーター連携
アニメーターから連携してZoomBlurのパラメータを操作できるように
コントローラとなるコンポーネントを作成します。

```csharp:ZoomBlurController.cs
using UnityEngine;
using UnityEngine.Rendering;

[ExecuteAlways]
public class ZoomBlurController : MonoBehaviour
{
    public VolumeProfile volumeProfile; // プロジェクトに作ったPostProcessVolume Profileをアタッチします
    [Range(0f, 100f)]
    public float focusPower = 10f;
    [Range(0, 10)]
    public int focusDetail = 5;
    public int referenceResolutionX = 1334;
    public Vector2 focusScreenPosition = Vector2.zero;
    ZoomBlur zoomBlur;

    void Update()
    {
        if (volumeProfile == null) return;
        if (zoomBlur == null) volumeProfile.TryGet<ZoomBlur>(out zoomBlur);
        if (zoomBlur == null) return;

        zoomBlur.focusPower.value = focusPower;
        zoomBlur.focusDetail.value = focusDetail;
        zoomBlur.focusScreenPosition.value = focusScreenPosition;
        zoomBlur.referenceResolutionX.value = referenceResolutionX;
    }
}
```
![2019-12-14_17h23_50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148284/107b57d2-f250-b662-1dab-a155d0f90b24.png)

# プロジェクトデータ
サンプルプロジェクトデータを[GitHub](https://github.com/t-macnaga/UniversalRPPostProcessing)にアップしましたので
ご自由にお使いください。

# 参考サイト様
- [【Unity2019 LWRP】ポストエフェクトを自作してゲームから利用する](https://qiita.com/r-ngtm/items/934493e1e2c364f7019e)
- [PostProcessing](https://github.com/Unity-Technologies/PostProcessing/wiki/Writing-Custom-Effects)
- [ShaderToy](https://www.shadertoy.com/view/3slSRj)

# 追記１(2020/03/08)

## 環境
* Unity2019.3.3f1
* UniversalRP 7.2.1

## ppv2互換性について
* `UniversalRenderPipelineAsset`の`Post-processing`で `Integrated`のほかに、`Post Processing V2`を選択可能です。
![2020-03-08_17h26_29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148284/8bdd784f-c8db-accd-4fc4-5b064b0dccf8.png)
![2020-03-08_17h27_08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148284/22ae7500-affa-8841-72e5-01fc949bedc0.png)

* `Post Processing V2`を選択すると警告がでています。将来的には廃止するのであくまで互換性維持のために用いるべきとでています。
* ppv2を使用すると、`UniversalRP 7.2.0`から同じく使用可能になった <font color="Red">Camera Stackingが *使用不可* </font>になるようです。
![2020-03-08_17h38_58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148284/83bedeb4-6292-4578-02a2-99efd241af23.png)

# 追記２(2020/09/21)
端末の解像度の違いによって見栄えに違いが出ないようにする修正を加えました。

* `ZoomBlur.shader`に`_ReferenceResolutionX`: ターゲット横幅解像度 を追加しました。
* あわせて`_ReferenceResolutionX`をスクリプト側から渡せるように修正してます。
* - `float power = 1.0 - _FocusPower * (1.0/_ScreenParams.x) * float(i);`
* + `float power = 1.0 - _FocusPower * (1.0/_ScreenParams.x * aspectX) * float(i);`
    * ↑ `aspectX` を乗算して解像度の違いの吸収をします。

 
※[こちらのコミット](https://github.com/t-macnaga/UniversalRPPostProcessing/commit/4678356ca6c55f78c74ef6fbbd68de9628152f59)で修正しました。