Unity URPでGrabPassっぽいことをやる (2D対応版)

2022/08/21に公開

URPにはGrabPassがない

Builtin RPではシェーダー内でGrabPass {}というパスを記述することで、直前の画面の内容をテクスチャとして取得できる機能がありました。これは歪みや複雑なブレンドを表現することができるため非常に便利で、私もエフェクトづくりの際に多用していました。

このGrabPassですが、URPやHDRPでは使用することができません。今回はURPでGrabPassっぽいことをやる方法をいろいろとまとめました。わかったこととしては以下の通りです。

  • そもそもSRP上でのGrabPassの完全な再現は難しい
  • 3D (Opaque) ではURPの標準機能である程度代替可能
  • 2D (Transparent) では:
    • 2D RendererではCamera Sorting Layer Textureで代替できるケースもある
    • それ以外のケースやUniversal Rendererの場合はURPを改造して代替できるかも
    • どちらにしろ完全な代替は不可能

3D (Opaque) の場合

URPにはCameraOpaqueTextureという機能があり、不透明パスの描画がすべて完了したタイミングで画面の内容をキャプチャしてくれます。
これについては既に様々な記事で解説されているので、説明は省略します。
https://teruama.com/2021/08/15/【unity】grabpassと同じようなことをurpでする方法【shader-graph】/

また、RendererFeatureを作成することでキャプチャのタイミングはある程度制御することが可能です。こちらのパッケージではそちらのアプローチを使用しています。
https://github.com/Haruma-K/URPGrabPass

2D (Transparent) の場合

さて、今回の本題はこちらです。

2Dゲームではほとんどのオブジェクトが半透明オブジェクトとして描画されるため、CameraOpaqueTextureには何も映りません。また、RendererFeatureを作成してTransparentパスの後で画面をキャプチャすることも可能ですが、これではOpaqueパスやTransparentパス内でテクスチャを使用することができません。(厳密には可能ですが、最初のフレームでテクスチャの内容が存在しなかったり、表示が遅延・ループして見た目がバグったりします)

この問題についてはUnity Forumでも言及されていますが、公式の回答ではGrabPassはパフォーマンス上の理由から削除され、CameraOpaqueTextureやRendererFeatureで代替してね!ということのようです。今後GrabPassが実装される望みは薄そうです。
https://forum.unity.com/threads/the-scriptable-render-pipeline-how-to-support-grabpass.521473/page-2#post-5038223

調査編

ここは問題解決への経緯を記載しています。結論だけ欲しい方は解決編をご覧ください。

調査①:UniversalRendererのTransparent描画パス

https://github.com/Unity-Technologies/Graphics/blob/2021.3/staging/Packages/com.unity.render-pipelines.universal/Runtime/UniversalRenderer.cs#L299

ForwardかDeferredかにかかわらず、TransparentはDrawObjectsPassというパスで描画されるようです。DrawObjectsPassを見に行ってみます。

https://github.com/Unity-Technologies/Graphics/blob/2021.3/staging/Packages/com.unity.render-pipelines.universal/Runtime/Passes/DrawObjectsPass.cs#L128

ScriptableRenderContext.DrawRenderers()でパスを発行しています。
内容はC++側に隠れており、ソーティングやバッチング(SRP Batcher)、個別のパスの発行はこのメソッドでまとめて行わてれいるようです。

調査②:2DRendererのTransparent描画パス

https://github.com/Unity-Technologies/Graphics/blob/2021.3/staging/Packages/com.unity.render-pipelines.universal/Runtime/2D/Renderer2D.cs#L55

Render2DLightingPassを見に行きます。

https://github.com/Unity-Technologies/Graphics/blob/2021.3/staging/Packages/com.unity.render-pipelines.universal/Runtime/2D/Passes/Render2DLightingPass.cs

こちらもScriptableRenderContext.DrawRenderers()を使用していますが、ライティングのための処理がいろいろ入ってます。

https://github.com/Unity-Technologies/Graphics/blob/2021.3/staging/Packages/com.unity.render-pipelines.universal/Runtime/2D/Passes/Render2DLightingPass.cs#L376

LayerUtility.CalculateBatches()でライティングのためにバッチを分割しているようです。各バッチにはSortingLayerの範囲が設定されており、複数のSortingLayer単位でまとめてDrawRenderersしています。

調査③:2DRendererのCamera Sorting Layer Texture

2D RendererにはCamera Sorting Layer Textureという機能があり、特定のSorting Layerの描画完了後に画面のコピーを作成してシェーダーから参照できる機能があります。ある特定のSorting Layerの後の画面をとれるだけでOKならば、これを使ってGrabPassを代替できそうです。

調査④:特定のRendererの直前にパスを挟む方法?

GrabPassは特定のRendererの描画パスの直前に画面をコピーするパスを挟み込んでいました。URP(SRP)で同じことをやる方法ですが……残念ながら見つけられませんでした。
CommandBuffer.DrawRenderer()などを使用すれば個別のRendererに対する描画パスの発行はできるかもしれませんが、SRPのバッチングやソーティングの機能にアクセスする方法が存在しないようです。(ご存じの方がいれば是非教えてください……)
もしSRPの上で一からレンダリングパイプラインを作成したとしても、GrabPassの機能を完全に再現することは難しいかもしれません。

解決編

さて、完全な代替が難しいことが分かってしまったので、ちょっと妥協して解決します。

まず、2D Rendererを使用していて、ある特定のSorting Layerの後の画面をとれるだけでOKならば、Camera Sorting Layer Textureを使用することができます。

https://docs.unity3d.com/ja/Packages/com.unity.render-pipelines.universal@14.0/manual/2DRendererData_overview.html#camera-sorting-layer-texture

次に、Universal Rendererを使用している場合は、URPを改造して対応します。

SRPのDrawRenderersは描画するSortingLayerの範囲を指定することが可能なので、ひとつSortingLayerを描画するごとに画面のコピーを作成する、といった改造が可能です。残念ながらGrabPassほどの柔軟な表現力は得られませんが、多くのケースはこれでカバーできるのではないかと思います。

それではやっていきます。URPのソースをPackagesフォルダにコピーして改造していきます。

UniversalRendererを改造する

DrawObjectsPassをコピーしてGrabbedObjectsPassを作ります。

GrabbedDrawObjectsPass.cs
using System;
using System.Collections.Generic;
using UnityEngine.Profiling;

namespace UnityEngine.Rendering.Universal.Internal
{
    public class GrabbedDrawObjectsPass : ScriptableRenderPass
    {
        // 中略
	
        public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
        {
            if (renderingData.cameraData.renderer.useDepthPriming && m_IsOpaque && (renderingData.cameraData.renderType == CameraRenderType.Base || renderingData.cameraData.clearDepth))
            {
                m_RenderStateBlock.depthState = new DepthState(false, CompareFunction.Equal);
                m_RenderStateBlock.mask |= RenderStateMask.Depth;
            }
            else if (m_RenderStateBlock.depthState.compareFunction == CompareFunction.Equal)
            {
                m_RenderStateBlock.depthState = new DepthState(true, CompareFunction.LessEqual);
                m_RenderStateBlock.mask |= RenderStateMask.Depth;
            }
            
	    //追加:Grabテクスチャの取得
            cmd.GetTemporaryRT(grabTarget.id, renderingData.cameraData.cameraTargetDescriptor);
        }

	//追加:Grabテクスチャの解放
        public override void OnCameraCleanup(CommandBuffer cmd)
        {
            cmd.ReleaseTemporaryRT(grabTarget.id);
        }
	
	//追加:Grabテクスチャの設定
        private RenderTargetHandle grabTarget;
        private Material blitMaterial;
        
        public void ConfigureGrabTarget(RenderTargetHandle grabTarget, Material blitMaterial)
        {
            this.grabTarget = grabTarget;
            this.blitMaterial = blitMaterial;
        }

        //追加:SortingLayerの取得
        private SortingLayer[] cachedLayers = default;

        private SortingLayer[] GetSortingLayers()
        {
            if (cachedLayers is null)
            {
                cachedLayers = SortingLayer.layers;
            }
#if UNITY_EDITOR
            if (!Application.isPlaying)
                cachedLayers = SortingLayer.layers;
#endif
            return cachedLayers;
        }

        /// <inheritdoc/>
        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
	    CommandBuffer cmd = CommandBufferPool.Get();
            using (new ProfilingScope(cmd, m_ProfilingSampler))
            {
	        
                //中略

                DrawingSettings drawSettings = CreateDrawingSettings(m_ShaderTagIdList, ref renderingData, sortFlags);
                
                //追加:SortingLayerごとにDrawRenderersを発行
                var layers = GetSortingLayers();

                var activeDebugHandler = GetActiveDebugHandler(renderingData);
                if (activeDebugHandler != null)
                {
                    for (int i = 0; i < layers.Length; i++)
                    {
                        var layer = layers[i];
                        var value = (short)layer.value;
                        filterSettings.sortingLayerRange = new SortingLayerRange(i == 0 ? short.MinValue : value,
                            i == layers.Length - 1 ? short.MaxValue : value);
                        
                        activeDebugHandler.DrawWithDebugRenderState(context, cmd, ref renderingData, ref drawSettings, ref filterSettings, ref m_RenderStateBlock,
                            (ScriptableRenderContext ctx, ref RenderingData data, ref DrawingSettings ds, ref FilteringSettings fs, ref RenderStateBlock rsb) =>
                            {
                                ctx.DrawRenderers(data.cullResults, ref ds, ref fs, ref rsb);
                            });
                        
                        var renderer = renderingData.cameraData.renderer;
                        var colorTarget = renderer.cameraColorTarget;
                        
                        RenderingUtils.Blit(cmd, colorTarget, grabTarget.Identifier(), null);
                        CoreUtils.SetRenderTarget(cmd, colorTarget, depthAttachment, ClearFlag.None, Color.white);
                        
                        context.ExecuteCommandBuffer(cmd);
                    }
                }
                else
                {
                    for(int i = 0; i < layers.Length; i++)
                    {
                        var layer = layers[i];
                        var value = (short)layer.value;
                        filterSettings.sortingLayerRange = new SortingLayerRange(i == 0 ? short.MinValue : value,
                            i == layers.Length - 1 ? short.MaxValue : value);
                        
                        context.DrawRenderers(renderingData.cullResults, ref drawSettings, ref filterSettings, ref m_RenderStateBlock);
                        
                        var renderer = renderingData.cameraData.renderer;
                        var colorTarget = renderer.cameraColorTarget;
                        
                        RenderingUtils.Blit(cmd, colorTarget, grabTarget.Identifier(), null);
                        CoreUtils.SetRenderTarget(cmd, colorTarget, depthAttachment, ClearFlag.None, Color.white);
                        
                        context.ExecuteCommandBuffer(cmd);
                    }

                    // Render objects that did not match any shader pass with error shader
                    RenderingUtils.RenderObjectsWithError(context, ref renderingData.cullResults, camera, filterSettings, SortingCriteria.None);
                }
            }
            context.ExecuteCommandBuffer(cmd);
            CommandBufferPool.Release(cmd);
        }
    }
}

次に、UniversalRendererGrabbedDrawObjectsPassを使うように改変します。

UniversalRenderer.cs
using System.Collections.Generic;
using UnityEngine.Rendering.Universal.Internal;

namespace UnityEngine.Rendering.Universal
{
    
    //中略
    
    public sealed class UniversalRenderer : ScriptableRenderer
    {
        
        //中略
	
	//変更:DrawObjectsPass -> GrabbedDrawObjectsPass
        GrabbedDrawObjectsPass m_RenderTransparentForwardPass;
	
	//追加:Grabテクスチャ
	readonly RenderTargetHandle k_GrabTextureHandle;
	
	//中略
	
        public UniversalRenderer(UniversalRendererData data) : base(data)
        {
	    
	        //中略
		
		//追加:Grabテクスチャの設定
	        k_GrabTextureHandle.Init("_GrabTexture");
		
		//変更:DrawObjectsPass -> GrabbedDrawObjectsPass
                m_RenderTransparentForwardPass = new GrabbedDrawObjectsPass(URPProfileId.DrawTransparentObjects, false, RenderPassEvent.BeforeRenderingTransparents, RenderQueueRange.transparent, data.transparentLayerMask, m_DefaultStencilState, stencilData.stencilReference);
		
                //中略
        }
	
	//中略
	
        /// <inheritdoc />
        public override void Setup(ScriptableRenderContext context, ref RenderingData renderingData)
        {
	        //中略
		
                m_RenderTransparentForwardPass.ConfigureColorStoreAction(transparentPassColorStoreAction);
                m_RenderTransparentForwardPass.ConfigureDepthStoreAction(transparentPassDepthStoreAction);
		
	        //追加:Grabテクスチャの設定
                m_RenderTransparentForwardPass.ConfigureGrabTarget(k_GrabTextureHandle, m_BlitMaterial);
		
                EnqueuePass(m_RenderTransparentForwardPass);
		
	        //中略
		
	}
	
	//中略
	
    }
}

シェーダーから使う

さて、_GrabTextureという名前でテクスチャが参照できるようになりました。
ShaderGraphでは次のようなプロパティを作成してテクスチャを参照できます。

Sub Graphをつくる

シェーダーごとにプロパティを作成するのは面倒なので、利便性のためにSub Graphを作っておきます。
Sub GraphではなぜかExposedがオフのプロパティを作成できないので、Custom Function Nodeを使います。SampleGrabTextureという名前のSubGraphを作成してその中にCustom Function Nodeを作ります。

SubGraphの入力はVector2、出力はVector4に設定します。
Custom Function Nodeの入力はuvという名前のVector2、出力はcolorという名前のVector4に設定します。

また、Graph SettingsでPrecisionをSingleに設定しておきます。

Custom Function Nodeの内容ですが、別途SampleGrabTexture.hlslというファイルを作成して参照します。

SampleGrabTexture.hlsl
#ifndef SAMPLEGRABTEXTURE_INCLUDED
#define SAMPLEGRABTEXTURE_INCLUDED

TEXTURE2D(_GrabTexture);
SAMPLER(sampler_GrabTexture);

void SampleGrabTexture_float(float2 uv, out float4 color)
{
    color = SAMPLE_TEXTURE2D(_GrabTexture, sampler_GrabTexture, uv);
}
#endif

Custom Function NodeのTypeをFile、NameをSampleGrabTexture、SourceにSampleGrabTexture.hlslを指定して完了です。

使ってみる

Invertという名前のShader Graphを作成します。Grabした色を反転させるだけのシェーダーです。
SampleGrabTextureノードにはScreen Positionノードの値を流し込みます。

Graph Settingsは以上のように、TransparentパスでDepth Writeはオフにしておきます。

さて、今回実装したGrabPassもどきはSorting Layerごとに画面のコピーを作成する仕組みなので、Invertシェーダーはその前のSorting Layerまでの描画内容を参照します。

Defaultレイヤーの前面に新しくEffectというSorting Layerを作成し、Invertはそちらで行うことにします。

新たに作成したSpriteに先程のInvertシェーダーを指定し、Sorting LayerにはEffectを指定します。Defaultに適当な背景を設定します。

うまく表示されました。

2D Rendererで上記のような改造を行う方法は詳しく解説しませんが、基本的にはUniversal Rendererと同様の考え方で改造が可能です。

Discussion