 
以前書いた
ですが、当時からUniversalRPがどんどん更新されまして、SwapBufferなる便利な仕組みが入りましたので
そちらを利用してポストプロセスを出来るだけシンプルに表記する方法を紹介します。
前回はモノクロ化するポストプロセスを紹介しましたので、今回もモノクロ化するポストプロセスで記述してみます
環境としては
- Unity2021.3.2f1
- UniversalRP 12.1.6
を対象としていますが、SwapBufferが有効になったUniversalRP 12以降なら動作対象となるかと思います。
必要なコード
必要なコードは3点。以前の記事では4点でしたが、今回は3つにまとめました
| ファイル名 | ファイル種別 | 内容 | 
|---|---|---|
| Monochrome.cs | csファイル | パラメータ調整用のVolumeComponent | 
| MonochromePostProcessRendererFeature.cs | csファイル | 描画のメイン部分 | 
| Monochrome.shader | shaderファイル | 描画用のシェーダ | 
という事で上から見ていきましょう。
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 (0f, 0f, 1f);
        public bool IsActive() => weight.value > 0f;
        public bool IsTileCompatible() => false;
    }
}
weightを触るためのVolumeComponent派生のコンポーネントです。
1年前と比べてCSのバージョンも上がり、ますますシンプルなコードになりました。
MonochromePostProcessRendererFeature.cs
これが今回の肝のコードです
using JetBrains.Annotations;
using ScreenPocket.PostProcess;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
namespace ScreenPocket.Rendering
{
    public sealed class MonochromePostProcessRendererFeature : ScriptableRendererFeature
    {
        private sealed class Pass : ScriptableRenderPass
        {
            private Material _material;
            public Pass(RenderPassEvent evt)
            {
                profilingSampler = new ProfilingSampler("Monochrome PostProcess Pass");
                renderPassEvent = evt;
                _material = FindMaterial("ScreenPocket/URP/PostProcess/Monochrome");
            }
            
            [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 monochrome = stack.GetComponent<Monochrome>();
                
                if (!monochrome.active)
                {
                    return;
                }
                var cmd = CommandBufferPool.Get();
                using (new ProfilingScope(cmd, profilingSampler))
                {
                    _material.SetFloat("_Weight", monochrome.weight.value);
                    Blit(cmd, ref renderingData, _material);//←☆これが今回のポイント!SwapBufferを利用したBlit()!
                }
                context.ExecuteCommandBuffer(cmd);
                CommandBufferPool.Release(cmd);
            }
        }//end class Pass
        
        /// <summary>
        /// ↑で定義したパス
        /// </summary>
        private Pass _pass;
        
        public override void Create()
        {
            //タイミングを調整したい場合は、例えば RenderPassEvent.BeforeRenderingPostProcessing+1 など値を加減算する事でで調整可能
            var timing = RenderPassEvent.BeforeRenderingPostProcessing;
            _pass = new Pass(timing);
        }
        public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
        {
            //Setupしたい場合はここに差し込む事 _pass.Setup(renderer);的な
            renderer.EnqueuePass(_pass);
        }
    }
}
という事で、前回別ファイルに分けた描画パスをクラス内に移動しました。
前回はポストプロセスをまとめる用のPassを想定していましたが、今回はモノクロを描画するためだけのFeatureとPassとなっています。
このコードで1番のポイントが//←☆のコメントを付けた1行。
ScriptableRenderPass.Blit()を使ってBlit()を行っています。
それによって、前回
        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);
だった部分が、SwapBufferを利用する事で
        var cmd = CommandBufferPool.Get();
        using (new ProfilingScope(cmd, profilingSampler))
        {
            _material.SetFloat("_Weight", monochrome.weight.value);
            Blit(cmd, ref renderingData, _material);//←☆これが今回のポイント!SwapBufferを利用したBlit()!
        }
        context.ExecuteCommandBuffer(cmd);
        CommandBufferPool.Release(cmd);
こんなにすっきり記述することが出来ました!すごい!
Monochrome.shader
最後にマテリアルに設定するためのシェーダファイルです
Shader "ScreenPocket/URP/PostProcess/Monochrome"
{
    HLSLINCLUDE
        #include "Packages/com.unity.render-pipelines.universal/Shaders/PostProcessing/Common.hlsl"
        TEXTURE2D_X(_SourceTex);
        SAMPLER(sampler_SourceTex);
        float _Weight;
        half4 Frag(Varyings input) : SV_Target
        {
            half4 col = SAMPLE_TEXTURE2D_X( _SourceTex, sampler_SourceTex, input.uv );
            half luminance = Luminance(col.rgb);
            return half4(lerp(col.rgb, half3(luminance,luminance,luminance),_Weight),1);
        }
    ENDHLSL
    
    SubShader
    {
        Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline"}
        LOD 100
        Cull Off ZWrite Off ZTest Always
        Pass
        {
            Name "Monochrome"
            HLSLPROGRAM
                #pragma vertex FullscreenVert
                #pragma fragment Frag
            ENDHLSL
        }
    }
}
という事で、前回との内容的な違いとしては _MainTexが_SourceTexにリネームされていることでしょうか。
SwapBufferで渡されるテクスチャ名が_SourceTexなので間違えないようにしておきましょう。
動作確認
今回も、残りの手順を列挙しておきます
- PackageManagerでUniversalRPをImport
- Projectツリービューで右クリック>Create>Rendering>Universal
- ProjectSettingsのGraphics設定画面で作ったURPアセットを設定
- 使っているRendererにMonochromePostprocessRendererFeatureを追加する
- 新規のGameObjectを作成しVolumeコンポーネントを追加 > VolumeProfileの「new」を選択してProfileを作成する
- VolumeProfileにScreenPocket/PostProcess/Monochromeを追加して、Weightを操作する
- カメラのPostProcessトグルにチェックを入れる
- カメラのVolumeTriggerに5で作ったGameObjectをアタッチする
とまぁ、ズババーッと書きましたが、これについては通常のURPのポストプロセス設定と特に違いは無いです。
ココを掘り下げる予定は無いので、他所のもっと詳細なポストプロセス導入記事を参照すると良いかと。
おわりに
という事で、SwapBufferの恩恵により、よりシンプルにポストプロセスを実装できることになった喜びを共有する記事でした。
たった130行弱でポストプロセスが実装できるとは、良い時代になった物です・・・!
参考資料
追記
Universal13に更新したところ、正しく表示されなくなってしまったようです
記事中の
                using (new ProfilingScope(cmd, profilingSampler))
                {
                    _material.SetFloat("_Weight", monochrome.weight.value);
                    Blit(cmd, ref renderingData, _material);//
                }
を
                using (new ProfilingScope(cmd, profilingSampler))
                {
                    _material.SetFloat("_Weight", monochrome.weight.value);
                    cmd.SetGlobalTexture(Shader.PropertyToID("_SourceTex"), 
                        renderingData.cameraData.renderer.cameraColorTargetHandle.nameID);
                    Blit(cmd, ref renderingData, _material);//
                }
とすることで、とりあえずは対応可能ですが、より良い解消法は調査中です。