URPでのシンプルなポストプロセス　陽炎(HeatHaze)

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ちょっと気になる事があって陽炎(HeatHaze)ポストプロセスをURP対応させた。<br>せっかくなのでQiita書きます <a href="https://t.co/hnHBV8GJIY">pic.twitter.com/hnHBV8GJIY</a></p>&mdash; MIYAKE (@ScreenPocket) <a href="https://twitter.com/ScreenPocket/status/1573230472918081536?ref_src=twsrc%5Etfw">September 23, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

以前書いた陽炎(HeatHaze)ImageEffect

https://qiita.com/ScreenPocket/items/05be246612f4b5bdae8a

ちょっと試したいことが有ったので、URPのPostProcess化を行いました。

# 必要なコード

以前書いた「シンプルなURPポストプロセス」

https://qiita.com/ScreenPocket/items/b45100d6262c9b005a14

の記事では3つのコードを用意しましたが、今回は更にコードをまとめて2つのコードに抑えました。

| ファイル名 | ファイル種別 |内容|
|:-----------|:------------:|:------------|
|HeatHazePostProcessRendererFeature.cs|csファイル|描画のメイン部分|
|HeatHaze.shader|shaderファイル|描画用のシェーダ|

最近は1ポストプロセスごとに1RendererFeatureにまとめる方が管理しやすい＆分離しやすくて好きなので、そのようにしています。

# HeatHazePostProcessRendererFeature.cs

早速コードを書きます。

```cs:HeatHazePostProcessRendererFeature.cs
using System;
using JetBrains.Annotations;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

namespace ScreenPocket.Rendering
{
    /// <summary>
    /// カゲロウのポストプロセス
    /// </summary>
    [DisallowMultipleRendererFeature("ScreenPocket/PostProcess/HeatHaze")]
    public sealed class HeatHazePostProcessRendererFeature : ScriptableRendererFeature
    {
        /// <summary>
        /// VolumeComponentも内包してみる
        /// </summary>
        [Serializable, VolumeComponentMenu("ScreenPocket/PostProcess/HeatHaze")]
        public sealed class HeatHaze : VolumeComponent, IPostProcessComponent
        {
            public ClampedFloatParameter weight = new (0f, 0f,1f);
        
            /// <summary>
            /// 回転部分の数
            /// </summary>
            public FloatParameter waveCycle = new (8f);
            /// <summary>
            /// 回転の幅
            /// </summary>
            public FloatParameter length = new (8f);
            /// <summary>
            /// 回転速度
            /// </summary>
            public FloatParameter speed = new (1f);
            /// <summary>
            /// 更新間隔
            /// </summary>
            public MinFloatParameter updateSpacingTime = new (1f/60f, 1f/60f);
            
            public bool IsActive() => weight.value > 0f;
            public bool IsTileCompatible() => false;
        }
        
        /// <summary>
        /// 描画パス
        /// </summary>
        private sealed class Pass : ScriptableRenderPass
        {
            private Material _material;
            
            public Pass(RenderPassEvent evt)
            {
                profilingSampler = new ProfilingSampler("HeatHaze PostProcess Pass");
                renderPassEvent = evt;
                _material = FindMaterial("ScreenPocket/URP/PostProcess/HeatHaze");
            }
            
            [CanBeNull]
            private static Material FindMaterial(string shaderName)
            {
                var shader = Shader.Find(shaderName);
                if (shader == null)
                {
                    Debug.LogError($"Not found shader!{shaderName}");
                    return null;
                }
                return CoreUtils.CreateEngineMaterial(shader);
            }
            
            public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
            {
                if (_material == null)
                {
                    return;
                }

                var stack = VolumeManager.instance.stack;
                var heatHaze = stack.GetComponent<HeatHaze>();
                
                if (!heatHaze.IsActive())
                {
                    return;
                }

                var cmd = CommandBufferPool.Get();
                using (new ProfilingScope(cmd, profilingSampler))
                {
                    _material.SetFloat("_Weight", heatHaze.weight.value);
                    _material.SetFloat("_WaveCycle", heatHaze.waveCycle.value);
                    _material.SetFloat("_Length", heatHaze.length.value);
                    _material.SetFloat("_Speed", heatHaze.speed.value);
                    _material.SetFloat("_UpdateSpacingTime", heatHaze.updateSpacingTime.value);
                    cmd.SetGlobalTexture(Shader.PropertyToID("_SourceTex"), 
                        renderingData.cameraData.renderer.cameraColorTargetHandle.nameID);
                    //SwapBufferでBlit()
                    Blit(cmd, ref renderingData, _material);
                }

                context.ExecuteCommandBuffer(cmd);
                CommandBufferPool.Release(cmd);
            }
        }// end class Pass
        
        /// <summary>
        /// ↑で定義したパス
        /// </summary>
        private Pass _pass;

        [SerializeField]
        private RenderPassEvent _renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing;
        public override void Create()
        {
            _pass = new Pass(_renderPassEvent);
        }

        public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
        {
            //Setupしたい場合はここに差し込む事 _pass.Setup(renderer);的な
            renderer.EnqueuePass(_pass);
        }
    }
}

```

~~という事で、結局VolumeComportもScriptableRenderPassも、全部ScriptableRendererFeatureのクラスに内包する形にしてしまいました。
どうせPostProcessとRendererFeatureで1:1の関係ですしね。まとめてしまって問題無いでしょう。
Unity標準のPostProcessの様に複数のPostProcessで1つのバッファを共有したりする場合は無理にまとめない方が良いと思います。~~
**☆末尾に追記あり。Unityエディタで不具合が発生する様でした。対策も後述。**

内容としては、今まで書いてきた記事と変わらず、パラメータの定義と、描画コマンドの発行くらいです。特に特殊な事はしていません。
ImageEffectの時との違いとしてはUpdateSpaceTimeが追加されたことくらいでしょうか。

# HeatHaze.shader

次にシェーダコードです

```hlsl:HeatHaze.shader
Shader "ScreenPocket/URP/PostProcess/HeatHaze"
{
    HLSLINCLUDE
        #include "Packages/com.unity.render-pipelines.universal/Shaders/PostProcessing/Common.hlsl"

        TEXTURE2D_X(_SourceTex);
        SAMPLER(sampler_SourceTex);
        float _Weight;
        float _WaveCycle;
        float _Length;
        float _Speed;
        float _UpdateSpacingTime;

        half4 Frag(Varyings input) : SV_Target
        {
            float2 uv;

            float time = _Time.w;

            //粗階調化
            time = floor(time / _UpdateSpacingTime) * _UpdateSpacingTime;

            float2 length = (_ScreenParams.zw-1) * _Length;
            uv.x = input.uv.x + cos(input.uv.y * _WaveCycle * TWO_PI + time * _Speed) * length.x * sin(input.uv.x * TWO_PI);
            uv.y = input.uv.y + sin(input.uv.x * _WaveCycle * TWO_PI + time * _Speed) * length.y * sin(input.uv.y * TWO_PI);

            uv = lerp(input.uv, uv, _Weight);

            return SAMPLE_TEXTURE2D_X( _SourceTex, sampler_SourceTex, uv );
        }
    ENDHLSL
    
    SubShader
    {
        Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline"}
        LOD 100
        Cull Off ZWrite Off ZTest Always

        Pass
        {
            Name "HeatHaze"
            HLSLPROGRAM
                #pragma vertex FullscreenVert
                #pragma fragment Frag
            ENDHLSL
        }
    }
}

```

こちらも以前ImageEffect用に準備した時とそれほど内容は変わっていません。
_UpdateSpacingTimeで更新時間の粗階調化を追加してカチカチ動かすことが出来るようになったのは追加機能ですね。
後は_ScreenParamsでズラシ幅の調整を入れているのが、前回からの改良点ですかね。

# 使い方
そもそものポストプロセスの使い方は別記事を参照してもらうとして、下記の手順で確認ができるかと思います

 1. UniversalRPのプロジェクトを作る
 1. 上記コード HeatHazePostProcessRendererFeature.cs,HeatHaze.shaderをプロジェクトに組み込む
 1. プロジェクトのUniversalRendererDataにHeatHazePostProcessRendererFeatureを加える
 1. ProjectにVolumeProfileを作成し、HeatHazeを追加
 1. GameObjectを新たに追加し、Volumeコンポーネントを付けて、VolumeProfileを関連付ける
 1. カメラのPostProcessチェックを入れて、Volume関連の設定を正しく行う

# 終わりに

とりあえず試したかった
「ポストプロセス処理っていっそ１つのcsコードにまとめた方が見やすいんじゃない？」
という点については実践できたのが良かったです。

あと、本当の狙いは「_UpdateSpaceingTimeでパカパカ歪ませる事で、ポップな動きを表現できるのではないかな？」と思ったのが出発点だったのですが、
それについては円運動の動きが滑らかすぎる事と、周期性がある事で動きがまとまり過ぎている事もあり狙った効果にはなりませんでした。

乱数を加えてみるとか、そもそも円運動を止めてみるとか、
もうちょっと別アプローチをやってみようかと思います。

# 追記

最新のUnity環境(2023.2)で、VolumeComponentはcsファイル名と一致していないと再起動時に参照が外れる現象が発生しました。

```cs:HeatHaze.csを作って、そちらに移動した方が良い部分
        [Serializable, VolumeComponentMenu("ScreenPocket/PostProcess/HeatHaze")]
        public sealed class HeatHaze : VolumeComponent, IPostProcessComponent
        {
            public ClampedFloatParameter weight = new (0f, 0f,1f);
        
            /// <summary>
            /// 回転部分の数
            /// </summary>
            public FloatParameter waveCycle = new (8f);
            /// <summary>
            /// 回転の幅
            /// </summary>
            public FloatParameter length = new (8f);
            /// <summary>
            /// 回転速度
            /// </summary>
            public FloatParameter speed = new (1f);
            /// <summary>
            /// 更新間隔
            /// </summary>
            public MinFloatParameter updateSpacingTime = new (1f/60f, 1f/60f);
            
            public bool IsActive() => weight.value > 0f;
            public bool IsTileCompatible() => false;
        }
```
については　HeatHaze.csファイルを追加して、そちらに記載する方が良さそうです。
ご注意ください。