[Unity]URPでオブジェクトモーションブラーを使う

本記事は[QualiArts Advent Calendar 2022](https://qiita.com/advent-calendar/2022/qualiarts) 10日目の記事です。

## オブジェクトモーションブラーとは？
オブジェクトモーションブラーとはその名前の通り、高速に変化するオブジェクトに対してかかるブラーです。

|  オブジェクトモーションブラーOFF |  オブジェクトモーションブラーON  |
| ---- | ---- |
|   ![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213392/0933fd8d-a893-df6c-c8b1-38cf3bf91f30.gif)   |  ![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213392/e12dfa34-f23f-243a-4af4-b083c0fb8813.gif)    |




## URPに搭載されているカメラモーションブラー
URPにはMotionBlurのVolumeが存在します。
しかしこれはカメラモーションブラーといい、カメラの移動によってかかるブラーのため、上記のような高速で回転するオブジェクトにはブラーがかかりません。
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213392/0b040212-ef6e-349e-b006-88b3724ebeb1.png">

|  URPのモーションブラーOFF |  URPのモーションブラーON  |
| ---- | ---- |
|  ![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213392/11aa93ac-060d-eca5-3258-3595b6ce9ebd.gif)  |  ![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213392/4e8e079d-7039-6327-5139-e164ab1dce8c.gif)  |







## PostProcessingStack V2に搭載されているオブジェクトモーションブラー
一方でPostProcessingStack V2(以下PostProcessing)には[オブジェクトモーションブラーが実装されています。](https://docs.unity3d.com/ja/2018.2/Manual/PostProcessing-MotionBlur.html)

そこで今回はPostProcessingに搭載されているオブジェクトモーションブラーをURPに移植することでURPでオブジェクトモーションブラーを使えるようにします。

## 配布プロジェクト
本解説で作成したプロジェクトはこちらで公開しています。

https://github.com/rarudo/UnityObjectMotionBlurSample


## 解説

本記事ではURPのRenderFeatureやVolumeの仕組みをある程度理解している方を対象にしています。

またオブジェクトモーションブラーの動作原理については、私自身が十分に理解しておらず、詳細な情報を伝えることができませんが、興味がある方は[A Fast and Stable Feature-Aware Motion Blur Filter](https://casual-effects.com/research/Guertin2014MotionBlur/index.html)の論文をお読みください。

### 環境
* Unity2021.3.13f1
* Universal Render Pipeline 12.1.7


## 処理を移植する

### VolumeComponentの移植
まずオブジェクトモーションブラーパラメーターをURPで実装します。
PostProcessingでは[ここ](https://github.com/Unity-Technologies/PostProcessing/blob/v2/PostProcessing/Runtime/Effects/MotionBlur.cs#L8-L43)で実装さています。

これによりオブジェクトモーションブラーのパラメーターをVolumeComponentから設定できるようになります。
![スクリーンショット 2022-12-04 20.37.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213392/1cf88ca3-75b0-79a9-db73-220e2deb99a1.png)


```C#
using UnityEngine.Rendering;

[System.Serializable, VolumeComponentMenu("Custom/Object Motion Blur")]
public class ObjectMotionBlur : VolumeComponent
{
    public ClampedFloatParameter shutterAngle = new ClampedFloatParameter(0, 0, 360);

    public ClampedIntParameter sampleCount = new ClampedIntParameter(8, 4, 32);

    public bool IsActive()
    {
        return  active && shutterAngle.overrideState && shutterAngle.value > 0 && sampleCount.value > 0;
    }
}
```

### シェーダーの移植
次にオブジェクトモーションブラーを実現するシェーダーをPostProcessingから移植します。

PostProcessingでは[ここで](https://github.com/Unity-Technologies/PostProcessing/blob/v2/PostProcessing/Shaders/Builtins/MotionBlur.shader)実装されている処理になります。

<details><summary>移植後のシェーダー</summary><div>


```HLSL
Shader "Hidden/PostEffect/ObjectMotionBlur"
{
    HLSLINCLUDE
        #pragma target 3.0
        
        #include "Packages/com.unity.render-pipelines.universal/Shaders/PostProcessing/Common.hlsl"
        #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
        
        // SourceTexture
        TEXTURE2D(_SourceTex); float4 _SourceTex_TexelSize;

        // Camera depth texture
        TEXTURE2D_X_FLOAT(_CameraDepthTexture); SAMPLER(sampler_CameraDepthTexture);

        // Camera motion vectors texture
        TEXTURE2D(_MotionVectorTexture); SAMPLER(sampler_MotionVectorTexture);
        float4 _MotionVectorTexture_TexelSize;

        // Packed velocity texture (2/10/10/10)
        TEXTURE2D(_VelocityTex); SAMPLER(sampler_VelocityTex);
        float2 _VelocityTex_TexelSize;

        // NeighborMax texture
        TEXTURE2D(_NeighborMaxTex); SAMPLER(sampler_NeighborMaxTex);
        float2 _NeighborMaxTex_TexelSize;

        // Velocity scale factor
        float _VelocityScale;

        // TileMax filter parameters
        int _TileMaxLoop;
        float2 _TileMaxOffs;

        // Maximum blur radius (in pixels)
        half _MaxBlurRadius;
        float _RcpMaxBlurRadius;

        // Filter parameters/coefficients
        half _LoopCount;

        // struct VaryingsDefault
        // {
        //     float4 vertex : SV_POSITION;
        //     float2 texcoord : TEXCOORD0;
        //     float2 texcoordStereo : TEXCOORD1;
        // };

        // -----------------------------------------------------------------------------
        // Prefilter
        float Linear01DepthPPV2(float z)
        {
            float isOrtho = unity_OrthoParams.w;
            float isPers = 1.0 - unity_OrthoParams.w;
            z *= _ZBufferParams.x;
            return (1.0 - isOrtho * z) / (isPers * z + _ZBufferParams.y);
        }

        // Velocity texture setup
        half4 FragVelocitySetup(Varyings i) : SV_Target
        {
            // Sample the motion vector.
            float2 v = SAMPLE_TEXTURE2D(_MotionVectorTexture, sampler_MotionVectorTexture, i.uv).rg;

            // Apply the exposure time and convert to the pixel space.
            v *= (_VelocityScale * 0.5) * _MotionVectorTexture_TexelSize.zw;

            // Clamp the vector with the maximum blur radius.
            v /= max(1.0, length(v) * _RcpMaxBlurRadius);

            // Sample the depth of the pixel.
            half d = Linear01Depth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, i.uv),_ZBufferParams);

            // Pack into 10/10/10/2 format.
            return half4((v * _RcpMaxBlurRadius + 1.0) * 0.5, d, 0.0);
        }

        half2 MaxV(half2 v1, half2 v2)
        {
            return dot(v1, v1) < dot(v2, v2) ? v2 : v1;
        }

        // TileMax filter (2 pixel width with normalization)
        half4 FragTileMax1(Varyings i) : SV_Target
        {
            float4 d = _SourceTex_TexelSize.xyxy * float4(-0.5, -0.5, 0.5, 0.5);

            half2 v1 = SAMPLE_TEXTURE2D(_SourceTex, sampler_LinearClamp, i.uv + d.xy).rg;
            half2 v2 = SAMPLE_TEXTURE2D(_SourceTex, sampler_LinearClamp, i.uv + d.zy).rg;
            half2 v3 = SAMPLE_TEXTURE2D(_SourceTex, sampler_LinearClamp, i.uv + d.xw).rg;
            half2 v4 = SAMPLE_TEXTURE2D(_SourceTex, sampler_LinearClamp, i.uv + d.zw).rg;

            v1 = (v1 * 2.0 - 1.0) * _MaxBlurRadius;
            v2 = (v2 * 2.0 - 1.0) * _MaxBlurRadius;
            v3 = (v3 * 2.0 - 1.0) * _MaxBlurRadius;
            v4 = (v4 * 2.0 - 1.0) * _MaxBlurRadius;

            return half4(MaxV(MaxV(MaxV(v1, v2), v3), v4), 0.0, 0.0);
        }

        // TileMax filter (2 pixel width)
        half4 FragTileMax2(Varyings i) : SV_Target
        {
            float4 d = _SourceTex_TexelSize.xyxy * float4(-0.5, -0.5, 0.5, 0.5);

            half2 v1 = SAMPLE_TEXTURE2D(_SourceTex, sampler_LinearClamp, i.uv + d.xy).rg;
            half2 v2 = SAMPLE_TEXTURE2D(_SourceTex, sampler_LinearClamp, i.uv + d.zy).rg;
            half2 v3 = SAMPLE_TEXTURE2D(_SourceTex, sampler_LinearClamp, i.uv + d.xw).rg;
            half2 v4 = SAMPLE_TEXTURE2D(_SourceTex, sampler_LinearClamp, i.uv + d.zw).rg;

            return half4(MaxV(MaxV(MaxV(v1, v2), v3), v4), 0.0, 0.0);
        }

        // TileMax filter (variable width)
        half4 FragTileMaxV(Varyings i) : SV_Target
        {
            float2 uv0 = i.uv + _SourceTex_TexelSize.xy * _TileMaxOffs.xy;

            float2 du = float2(_SourceTex_TexelSize.x, 0.0);
            float2 dv = float2(0.0, _SourceTex_TexelSize.y);

            half2 vo = 0.0;

            UNITY_LOOP
            for (int ix = 0; ix < _TileMaxLoop; ix++)
            {
                UNITY_LOOP
                for (int iy = 0; iy < _TileMaxLoop; iy++)
                {
                    float2 uv = uv0 + du * ix + dv * iy;
                    vo = MaxV(vo, SAMPLE_TEXTURE2D(_SourceTex, sampler_LinearClamp, uv).rg);
                }
            }

            return half4(vo, 0.0, 0.0);
        }

        // NeighborMax filter
        half4 FragNeighborMax(Varyings i) : SV_Target
        {
            const half cw = 1.01; // Center weight tweak

            float4 d = _SourceTex_TexelSize.xyxy * float4(1.0, 1.0, -1.0, 0.0);

            half2 v1 = SAMPLE_TEXTURE2D(_SourceTex, sampler_LinearClamp, i.uv - d.xy).rg;
            half2 v2 = SAMPLE_TEXTURE2D(_SourceTex, sampler_LinearClamp, i.uv - d.wy).rg;
            half2 v3 = SAMPLE_TEXTURE2D(_SourceTex, sampler_LinearClamp, i.uv - d.zy).rg;

            half2 v4 = SAMPLE_TEXTURE2D(_SourceTex, sampler_LinearClamp, i.uv - d.xw).rg;
            half2 v5 = SAMPLE_TEXTURE2D(_SourceTex, sampler_LinearClamp, i.uv).rg * cw;
            half2 v6 = SAMPLE_TEXTURE2D(_SourceTex, sampler_LinearClamp, i.uv + d.xw).rg;

            half2 v7 = SAMPLE_TEXTURE2D(_SourceTex, sampler_LinearClamp, i.uv + d.zy).rg;
            half2 v8 = SAMPLE_TEXTURE2D(_SourceTex, sampler_LinearClamp, i.uv + d.wy).rg;
            half2 v9 = SAMPLE_TEXTURE2D(_SourceTex, sampler_LinearClamp, i.uv + d.xy).rg;

            half2 va = MaxV(v1, MaxV(v2, v3));
            half2 vb = MaxV(v4, MaxV(v5, v6));
            half2 vc = MaxV(v7, MaxV(v8, v9));

            return half4(MaxV(va, MaxV(vb, vc)) * (1.0 / cw), 0.0, 0.0);
        }

        // -----------------------------------------------------------------------------
        // Reconstruction
        
        // Interleaved gradient function from Jimenez 2014
        // http://www.iryoku.com/next-generation-post-processing-in-call-of-duty-advanced-warfare
        float GradientNoise(float2 uv)
        {
            uv = floor(uv * _ScreenParams.xy);
            float f = dot(float2(0.06711056, 0.00583715), uv);
            return frac(52.9829189 * frac(f));
        }

        // Returns true or false with a given interval.
        bool Interval(half phase, half interval)
        {
            return frac(phase / interval) > 0.499;
        }

        // Jitter function for tile lookup
        float2 JitterTile(float2 uv)
        {
            float rx, ry;
            sincos(GradientNoise(uv + float2(2.0, 0.0)) * TWO_PI, ry, rx);
            return float2(rx, ry) * _NeighborMaxTex_TexelSize.xy * 0.25;
        }

        // Velocity sampling function
        half3 SampleVelocity(float2 uv)
        {
            half3 v = SAMPLE_TEXTURE2D_LOD(_VelocityTex, sampler_VelocityTex, uv, 0.0).xyz;
            return half3((v.xy * 2.0 - 1.0) * _MaxBlurRadius, v.z);
        }

        // Reconstruction filter
        half4 FragReconstruction(Varyings i) : SV_Target
        {
            // Color sample at the center point
            const float4 c_p = SAMPLE_TEXTURE2D(_SourceTex, sampler_LinearClamp, i.uv);

            // Velocity/Depth sample at the center point
            const float3 vd_p = SampleVelocity(i.uv);
            const float l_v_p = max(length(vd_p.xy), 0.5);
            const float rcp_d_p = 1.0 / vd_p.z;

            // NeighborMax vector sample at the center point
            const float2 v_max = SAMPLE_TEXTURE2D(_NeighborMaxTex, sampler_NeighborMaxTex, i.uv + JitterTile(i.uv)).xy;
            const float l_v_max = length(v_max);
            const float rcp_l_v_max = 1.0 / l_v_max;

            // Escape early if the NeighborMax vector is small enough.
            if (l_v_max < 2.0) return c_p;

            // Use V_p as a secondary sampling direction except when it's too small
            // compared to V_max. This vector is rescaled to be the length of V_max.
            const half2 v_alt = (l_v_p * 2.0 > l_v_max) ? vd_p.xy * (l_v_max / l_v_p) : v_max;

            // Determine the sample count.
            const half sc = floor(min(_LoopCount, l_v_max * 0.5));

            // Loop variables (starts from the outermost sample)
            const half dt = 1.0 / sc;
            const half t_offs = (GradientNoise(i.uv) - 0.5) * dt;
            float t = 1.0 - dt * 0.5;
            float count = 0.0;

            // Background velocity
            // This is used for tracking the maximum velocity in the background layer.
            float l_v_bg = max(l_v_p, 1.0);

            // Color accumlation
            float4 acc = 0.0;

            UNITY_LOOP
            while (t > dt * 0.25)
            {
                // Sampling direction (switched per every two samples)
                const float2 v_s = Interval(count, 4.0) ? v_alt : v_max;

                // Sample position (inverted per every sample)
                const float t_s = (Interval(count, 2.0) ? -t : t) + t_offs;

                // Distance to the sample position
                const float l_t = l_v_max * abs(t_s);

                // UVs for the sample position
                const float2 uv0 = i.uv + v_s * t_s * _SourceTex_TexelSize.xy;
                const float2 uv1 = i.uv + v_s * t_s * _VelocityTex_TexelSize.xy;

                // Color sample
                const float3 c = SAMPLE_TEXTURE2D_LOD(_SourceTex, sampler_LinearClamp, uv0, 0.0).rgb;

                // Velocity/Depth sample
                const float3 vd = SampleVelocity(uv1);

                // Background/Foreground separation
                const float fg = saturate((vd_p.z - vd.z) * 20.0 * rcp_d_p);

                // Length of the velocity vector
                const float l_v = lerp(l_v_bg, length(vd.xy), fg);

                // Sample weight
                // (Distance test) * (Spreading out by motion) * (Triangular window)
                const float w = saturate(l_v - l_t) / l_v * (1.2 - t);

                // Color accumulation
                acc += half4(c, 1.0) * w;

                // Update the background velocity.
                l_v_bg = max(l_v_bg, l_v);

                // Advance to the next sample.
                t = Interval(count, 2.0) ? t - dt : t;
                count += 1.0;
            }

            // Add the center sample.
            acc += float4(c_p.rgb, 1.0) * (1.2 / (l_v_bg * sc * 2.0));

            //return half4(0,0,1,1);
            return half4(acc.rgb / acc.a, c_p.a);
        }

    ENDHLSL

    SubShader
    {
        Cull Off ZWrite Off ZTest Always

        // (0) Velocity texture setup
        Pass
        {
            HLSLPROGRAM

                #pragma vertex Vert
                #pragma fragment FragVelocitySetup

            ENDHLSL
        }

        // (1) TileMax filter (2 pixel width with normalization)
        Pass
        {
            HLSLPROGRAM

                #pragma vertex Vert
                #pragma fragment FragTileMax1

            ENDHLSL
        }

        //  (2) TileMax filter (2 pixel width)
        Pass
        {
            HLSLPROGRAM

                #pragma vertex Vert
                #pragma fragment FragTileMax2

            ENDHLSL
        }

        // (3) TileMax filter (variable width)
        Pass
        {
            HLSLPROGRAM

                #pragma vertex Vert
                #pragma fragment FragTileMaxV

            ENDHLSL
        }

        // (4) NeighborMax filter
        Pass
        {
            HLSLPROGRAM

                #pragma vertex Vert
                #pragma fragment FragNeighborMax

            ENDHLSL
        }

        // (5) Reconstruction filter
        Pass
        {
            HLSLPROGRAM

                #pragma vertex Vert
                #pragma fragment FragReconstruction

            ENDHLSL
        }
    }
}
```

</div></details>


### パスの移植
次にCPU側の処理を移植します。
このコードは上記のシェーダーで定義したパスの呼び出しやパラメーターを制御します。
これは後述するScriptableRenderPassを継承したクラスのExecuteメソッド中に入れる事もできますが、今回はわかりやすくするためにPostProcessingMotionBlurというクラスに分離しました。
PostProcessingでは[ここ](https://github.com/Unity-Technologies/PostProcessing/blob/v2/PostProcessing/Runtime/Effects/MotionBlur.cs)で実装されている処理です。

<details><summary>コード全文</summary><div>

```C#
using UnityEngine;
using UnityEngine.Rendering;

public class PostProcessingMotionBlur
{
    enum Pass
    {
        VelocitySetup,
        TileMax1,
        TileMax2,
        TileMaxV,
        NeighborMax,
        Reconstruction
    }

    class ShaderIDs
    {
        internal static readonly int VelocityScale = Shader.PropertyToID("_VelocityScale");
        internal static readonly int MaxBlurRadius = Shader.PropertyToID("_MaxBlurRadius");
        internal static readonly int RcpMaxBlurRadius = Shader.PropertyToID("_RcpMaxBlurRadius");
        internal static readonly int VelocityTex = Shader.PropertyToID("_VelocityTex");
        internal static readonly int Tile2RT = Shader.PropertyToID("_Tile2RT");
        internal static readonly int Tile4RT = Shader.PropertyToID("_Tile4RT");
        internal static readonly int Tile8RT = Shader.PropertyToID("_Tile8RT");
        internal static readonly int TileMaxOffs = Shader.PropertyToID("_TileMaxOffs");
        internal static readonly int TileMaxLoop = Shader.PropertyToID("_TileMaxLoop");
        internal static readonly int TileVRT = Shader.PropertyToID("_TileVRT");
        internal static readonly int NeighborMaxTex = Shader.PropertyToID("_NeighborMaxTex");
        internal static readonly int LoopCount = Shader.PropertyToID("_LoopCount");
    }

    private void CreateTemporaryRT(CommandBuffer cmd, RenderTextureDescriptor rtDesc, int nameID, int width,
        int height,
        RenderTextureFormat rtFormat)
    {
        rtDesc.width = width;
        rtDesc.height = height;
        rtDesc.colorFormat = rtFormat;
        cmd.GetTemporaryRT(nameID, rtDesc, FilterMode.Point);
    }

    public void ObjectMotionBlur(
        CommandBuffer cmd,
        Material material,
        RenderTargetIdentifier source,
        RenderTargetIdentifier destination,
        RenderTextureDescriptor desc)
    {
        var objectMotionBlur = VolumeManager.instance.stack.GetComponent<ObjectMotionBlur>();

        const float kMaxBlurRadius = 5f;
        var vectorRTFormat = RenderTextureFormat.RGHalf;
        var packedRTFormat = SystemInfo.SupportsRenderTextureFormat(RenderTextureFormat.ARGB2101010)
            ? RenderTextureFormat.ARGB2101010
            : RenderTextureFormat.ARGB32;

        // var desc = GetCompatibleDescriptor();
        var width = desc.width;
        var height = desc.height;
        desc.colorFormat = packedRTFormat;

        // Calculate the maximum blur radius in pixels.
        // int maxBlurPixels = (int)(kMaxBlurRadius * context.height / 100);
        int maxBlurPixels = (int)(kMaxBlurRadius * height / 100);

        // Calculate the TileMax size.
        // It should be a multiple of 8 and larger than maxBlur.
        int tileSize = ((maxBlurPixels - 1) / 8 + 1) * 8;

        // Pass 1 - Velocity/depth packing
        var velocityScale = objectMotionBlur.shutterAngle.value / 360f;
        material.SetFloat(ShaderIDs.VelocityScale, velocityScale);
        material.SetFloat(ShaderIDs.MaxBlurRadius, maxBlurPixels);
        material.SetFloat(ShaderIDs.RcpMaxBlurRadius, 1f / maxBlurPixels);


        int vbuffer = ShaderIDs.VelocityTex;
        CreateTemporaryRT(cmd, desc, vbuffer, width, height, packedRTFormat);
        // cmd.Blit(BuiltinRenderTextureType.None, vbuffer, material, (int)Pass.VelocitySetup);
        Blit(cmd, BuiltinRenderTextureType.None, vbuffer, material, (int)Pass.VelocitySetup);

        // Pass 2 - First TileMax filter (1/2 downsize)
        int tile2 = ShaderIDs.Tile2RT;
        CreateTemporaryRT(cmd, desc, tile2, width / 2, height / 2, vectorRTFormat);
        Blit(cmd, vbuffer, tile2, material, (int)Pass.TileMax1);

        // Pass 3 - Second TileMax filter (1/2 downsize)
        int tile4 = ShaderIDs.Tile4RT;
        CreateTemporaryRT(cmd, desc, tile4, width / 4, height / 4, vectorRTFormat);
        Blit(cmd, tile2, tile4, material, (int)Pass.TileMax2);
        cmd.ReleaseTemporaryRT(tile2);

        // Pass 4 - Third TileMax filter (1/2 downsize)
        int tile8 = ShaderIDs.Tile8RT;
        CreateTemporaryRT(cmd, desc, tile8, width / 8, height / 8, vectorRTFormat);
        Blit(cmd, tile4, tile8, material, (int)Pass.TileMax2);
        cmd.ReleaseTemporaryRT(tile4);

        // Pass 5 - Fourth TileMax filter (reduce to tileSize)
        var tileMaxOffs = Vector2.one * (tileSize / 8f - 1f) * -0.5f;
        material.SetVector(ShaderIDs.TileMaxOffs, tileMaxOffs);
        material.SetFloat(ShaderIDs.TileMaxLoop, (int)(tileSize / 8f));
        int tile = ShaderIDs.TileVRT;
        CreateTemporaryRT(cmd, desc, tile, width / tileSize, height / tileSize, vectorRTFormat);
        Blit(cmd, tile8, tile, material, (int)Pass.TileMaxV);
        cmd.ReleaseTemporaryRT(tile8);

        // Pass 6 - NeighborMax filter
        int neighborMax = ShaderIDs.NeighborMaxTex;
        CreateTemporaryRT(cmd, desc, neighborMax, width / tileSize, height / tileSize, vectorRTFormat);
        Blit(cmd, tile, neighborMax, material, (int)Pass.NeighborMax);
        cmd.ReleaseTemporaryRT(tile);

        // Pass 7 - Reconstruction pass
        material.SetFloat(ShaderIDs.LoopCount, Mathf.Clamp(objectMotionBlur.sampleCount.value / 2, 1, 64));
        Blit(cmd, source, destination, material, (int)Pass.Reconstruction);

        cmd.ReleaseTemporaryRT(vbuffer);
        cmd.ReleaseTemporaryRT(neighborMax);
    }

    private void Blit(CommandBuffer cmd, RenderTargetIdentifier source, RenderTargetIdentifier destination,
        Material material, int passIndex = 0)
    {
        cmd.SetGlobalTexture(Shader.PropertyToID("_SourceTex"), source);
        cmd.Blit(source, destination, material, passIndex);
    }
}
```
</div></details>

### ScriptableRenderPassを継承したPassの実装
ScriptableRenderPassを継承したパスを実装します。
オブジェクトモーションブラーを実現するためには、モーションベクターが必要になるため`ConfigureInput(ScriptableRenderPassInput.Motion)`を指定して、URP側にモーションベクターパスの描画を要求します。

```C#
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class ObjectMotionBlurPass : ScriptableRenderPass
{
    private readonly ProfilingSampler _objectMotionBlurSampler = new("Object Motion Blur");
    private readonly PostProcessingMotionBlur _postProcessingMotionBlur;
    private readonly RenderTargetHandle _tmpColorBuffer;
    private Material _material;

    public ObjectMotionBlurPass(Shader shader)
    {
        // MotionVector要求する
        ConfigureInput(ScriptableRenderPassInput.Motion);
        
        _postProcessingMotionBlur = new PostProcessingMotionBlur();
        _tmpColorBuffer.Init("_TempColorBuffer");
        _material = CoreUtils.CreateEngineMaterial(shader);
    }

    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    {
        ref var cameraData = ref renderingData.cameraData;
        // SceneViewではブラーをかけない
        if(cameraData.cameraType == CameraType.SceneView) return;
        
        CommandBuffer cmd = CommandBufferPool.Get();

        using (new ProfilingScope(cmd, _objectMotionBlurSampler))
        {
            var descriptor = cameraData.cameraTargetDescriptor;
            var colorTarget = cameraData.renderer.cameraColorTarget;
            
            // カメラの画像を_TempColorBufferにコピーする
            cmd.GetTemporaryRT(_tmpColorBuffer.id, descriptor);
            Blit(cmd, colorTarget, _tmpColorBuffer.id);

            // オブジェクトモーションブラー
            _postProcessingMotionBlur.ObjectMotionBlur(cmd, _material, _tmpColorBuffer.id, colorTarget, descriptor);

            cmd.ReleaseTemporaryRT(_tmpColorBuffer.id);
        }

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

```


## RenderFeatureの設定
最後にRendererにRenderFeatureを設定し、作成したシェーダーを割り当てたら完成です。

![スクリーンショット 2022-12-09 22.00.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213392/76fa095d-411c-acc5-aa04-a422048727b2.png)

Shuntter Angleを変えることでブラーのかかり具合、Sample Countを変える事でブラーの精度(処理負荷)を制御できます。
![画面収録 2022-12-09 22.18.00-lossy.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213392/fafebad9-745b-db00-f68d-9ca925d97df7.gif)