🗒️

【Unity6】RenderGraphにおけるマルチレンダーターゲットの実装例【MRT】

に公開1

はじめに

Unity6が登場しURP17になったことでパイプライン関連がガラッと変わりRenderGraphの対応をしなきゃいけなくて困ってる人多いと思います。
今回はそんなRenderGraphにおいてカラーとノーマルを別々のレンダーターゲットに書き込むシンプルなマルチレンダーターゲットを実装をしてみます。

環境

Unity 6000.0.45f1
URP 17.0.4

前のバージョンと何が違うのか

基本的に異なるのはRenderPassのみです。
RendererFeatureやShaderにおいては既存の実装で変わりはありません。
なので変更のあるPassの部分だけ厚めに解説しようかなと思います。

RendererFeature

マルチレンダーターゲットのパスのMRTRenderPassとそのレンダーターゲットを合成するCombinePassの二つを用意します。
以前と変わらずCreateで作成してAddRenderPassesでEnqueuePassをするだけです。

public class MRTRendererFeature : ScriptableRendererFeature
{
    private MRTRenderPass _mrtRenderPass;
    private CombinePass _combinePass;
    
    public override void Create()
    {
        ShaderTagId mrtShaerTagId = new ShaderTagId("MRTRender");
        _mrtRenderPass = new(mrtShaerTagId);
        _combinePass = new();
    }

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        renderer.EnqueuePass(_mrtRenderPass);
        renderer.EnqueuePass(_combinePass);
    }

    protected override void Dispose(bool disposing)
    {
        base.Dispose(disposing);

        if (disposing == false)
        {
            return;
        }

        _combinePass.Dispose();
    }
}

MRTRenderPass

Passの作成

次に本題のRenderPassの話をします。
まずはMRTのパスを作成します。
今回からExcuteではなく描画のパイプラインはRecordRenderGraphに書くことになります。
RenderTargetの作成もドローコールも全部ここに書きます。

public class MRTRenderPass : ScriptableRenderPass
{
    public MRTRenderPassSample()
    {
    }

    public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
    {
    }
}

ラスターレンダーパスの追加

まず初めにどういうパスを作るかを指定しないといけません。

private class PassData
{
    public RendererListHandle RendererListHandle;
}

using (var builder = renderGraph.AddRasterRenderPass<PassData>("MRTRenderPass", out var passData))
{

}

今回はラスターレンダーパスを追加します。
他にもAddUnsafePassやAddComputePassなどがあります。
またテンプレートに指定しているPassDataは描画メソッドに送るデータを作成することが出来ます。
このパスでは描画リストが必要なのでRendererListHandleを宣言してます。

RenderTargetの作成

まずレンダーターゲットの作成をします。

// リソースデータの取得
var resourceData = frameData.Get<UniversalResourceData>();

// 現在使用しているカラーテクスチャの取得
var sourceTextureHandle = resourceData.activeColorTexture;

// アルベドテクスチャの作成
var albedoTextureDescriptor = renderGraph.GetTextureDesc(sourceTextureHandle);
albedoTextureDescriptor.name = "_CustomAlbedoTexture";
albedoTextureDescriptor.depthBufferBits = 0;
_albedoTextureHandle = renderGraph.CreateTexture(albedoTextureDescriptor);

// ノーマルテクスチャの作成
var normalTextureDescriptor = renderGraph.GetTextureDesc(sourceTextureHandle);
normalTextureDescriptor.name = "_CustomNormalTexture";
normalTextureDescriptor.clearColor = Color.clear;
normalTextureDescriptor.depthBufferBits = 0;
_normalTextureHandle = renderGraph.CreateTexture(normalTextureDescriptor);

// レンダーターゲットを設定
builder.SetRenderAttachment(_albedoTextureHandle, _abledoTextureIndex);
builder.SetRenderAttachment(_normalTextureHandle, _normalTextureIndex);
        
// デプステクスチャを設定
builder.SetRenderAttachmentDepth(resourceData.activeDepthTexture);

今回はframeData.Get<T>();でデータを取得するようになってます
他にも

var cameraData = frameData.Get<UniversalCameraData>();
var renderingData = frameData.Get<UniversalRenderingData>();
var lightData = frameData.Get<UniversalLightData>();

などがあります。
また自作のデータをセットすることができ、

var customdata = frameData.Create<CustomData>

このようにすれば自作のデータも作成することが出来ます。

レンダーターゲットの作成方法は以下の通りで

_albedoTextureHandle = renderGraph.CreateTexture(albedoTextureDescriptor);
_normalTextureHandle = renderGraph.CreateTexture(normalTextureDescriptor);

デスクリプタをセットして作成します。
そして作成したRenderTargetをセットする箇所がここです。

builder.SetRenderAttachment(_albedoTextureHandle, _abledoTextureIndex);
builder.SetRenderAttachment(_normalTextureHandle, _normalTextureIndex);
builder.SetRenderAttachmentDepth(resourceData.activeDepthTexture);

第三引数にアクセス指定があります。
このパスは書き込むだけなのでデフォルトのWriteで十分です。
またここで気を付けないといけないのがデプスを含めたこの三つのレンダーターゲットの設定の中で違うとエラーが起きるものがあるということです。
サイズなどはもちろんのこと、msaaSamplesなどを一個だけ変えるなどするとエラーが起きる項目があるので何かエラーが起きた場合はそこを疑うといいかもしれません。

RenderTargetのをグローバルプロパティに設定する

セットしたテクスチャはあとで合成したいのでグローバルプロパティに設定します。

// GlobalStateの変更を許容する
builder.AllowGlobalStateModification(true);
// パスの実行後テクスチャをグローバルプロパティに設定
builder.SetGlobalTextureAfterPass(_albedoTextureHandle, Shader.PropertyToID("_CustomAlbedoTexture"));
builder.SetGlobalTextureAfterPass(_normalTextureHandle, Shader.PropertyToID("_CustomNormalTexture"));

シェーダーのグローバルプロパティなどを変更する場合以下の指定が必要です。

// GlobalStateの変更を許容する
builder.AllowGlobalStateModification(true);

グローバルプロパティに設定する場合TextureHandleとシェーダー側のプロパティ名をIDに変換したものをセットします。

builder.SetGlobalTextureAfterPass(_albedoTextureHandle, Shader.PropertyToID("_CustomAlbedoTexture"));
builder.SetGlobalTextureAfterPass(_normalTextureHandle, Shader.PropertyToID("_CustomNormalTexture"));

これであとのパスでこのテクスチャがグローバルプロパティとして使用することが出来ます。
以下の画像のようにRenderGraphViewerで地球マークがついたらグローバルプロパティ化した証です。

描画する

あとは実際に指定したシェーダータグに対応したオブジェクトを描画します。

// 描画データを設定
var cameraData = frameData.Get<UniversalCameraData>();
var renderingData = frameData.Get<UniversalRenderingData>();
var lightData = frameData.Get<UniversalLightData>();

var drawSettings = RenderingUtils.CreateDrawingSettings(_shaderTagId, renderingData, cameraData, lightData, _sortingCriteria);

var rendererListParams = new RendererListParams(renderingData.cullResults, drawSettings, _filteringSettings);
passData.RendererListHandle = renderGraph.CreateRendererList(rendererListParams);
builder.UseRendererList(passData.RendererListHandle);
            
// 描画
builder.SetRenderFunc
(
    static (PassData passData, RasterGraphContext context) =>
    {
        context.cmd.DrawRendererList(passData.RendererListHandle);
    }
);

描画用のパラメーターを設定します。

// 描画データを設定
var cameraData = frameData.Get<UniversalCameraData>();
var renderingData = frameData.Get<UniversalRenderingData>();
var lightData = frameData.Get<UniversalLightData>();

var drawSettings = RenderingUtils.CreateDrawingSettings(_shaderTagId, renderingData, cameraData, lightData, _sortingCriteria);
var rendererListParams = new RendererListParams(renderingData.cullResults, drawSettings, _filteringSettings);

passData.RendererListHandle = renderGraph.CreateRendererList(rendererListParams);
builder.UseRendererList(passData.RendererListHandle);

builder.UseRendererListこれなくてもエラーは出なく以外と忘れがちなので気を付けてください。
無かったら描画されません。
設定された情報をもとに描画します。

// 描画
builder.SetRenderFunc
(
    static (PassData passData, RasterGraphContext context) =>
    {
        context.cmd.DrawRendererList(passData.RendererListHandle);
    }
);

これでマルチレンダーターゲットのパスは終了です。

MRTRenderPassの全貌

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.RenderGraphModule;
using UnityEngine.Rendering.Universal;

public class MRTRenderPass : ScriptableRenderPass
{
    private class PassData
    {
        public RendererListHandle RendererListHandle;
    }

    private readonly ShaderTagId _shaderTagId;
    private readonly SortingCriteria _sortingCriteria;
    private readonly FilteringSettings _filteringSettings;

    private readonly int _abledoTextureIndex = 0;
    private readonly int _normalTextureIndex = 1;
    
    private TextureHandle _albedoTextureHandle;
    private TextureHandle _normalTextureHandle;

    public MRTRenderPass(
        ShaderTagId shaderTagId,
        RenderPassEvent renderPassEvent = RenderPassEvent.BeforeRenderingOpaques,
        SortingCriteria sortingCriteria = SortingCriteria.CommonOpaque,
        FilteringSettings filteringSettings = default)
    {
        this.renderPassEvent = renderPassEvent;
        
        _shaderTagId = shaderTagId;
        _sortingCriteria = sortingCriteria;
        _filteringSettings = filteringSettings == default ?  new FilteringSettings(RenderQueueRange.opaque) : filteringSettings;
    }
    
    public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
    {
        using (var builder = renderGraph.AddRasterRenderPass<PassData>("MRTRenderPass", out var passData))
        {
            // カリングを許容しない
            builder.AllowPassCulling(false);
            
            // GlobalStateの変更を許容する
            builder.AllowGlobalStateModification(true);

            // リソースデータの取得
            var resourceData = frameData.Get<UniversalResourceData>();

            // 現在使用しているカラーテクスチャの取得
            var sourceTextureHandle = resourceData.activeColorTexture;

            // アルベドテクスチャの作成
            var albedoTextureDescriptor = renderGraph.GetTextureDesc(sourceTextureHandle);
            albedoTextureDescriptor.name = "_CustomAlbedoTexture";
            albedoTextureDescriptor.depthBufferBits = 0;
            _albedoTextureHandle = renderGraph.CreateTexture(albedoTextureDescriptor);
            
            // ノーマルテクスチャの作成
            var normalTextureDescriptor = renderGraph.GetTextureDesc(sourceTextureHandle);
            normalTextureDescriptor.name = "_CustomNormalTexture";
            normalTextureDescriptor.clearColor = Color.clear;
            normalTextureDescriptor.depthBufferBits = 0;
            _normalTextureHandle = renderGraph.CreateTexture(normalTextureDescriptor);

            // レンダーターゲットを設定
            builder.SetRenderAttachment(_albedoTextureHandle, _abledoTextureIndex);
            builder.SetRenderAttachment(_normalTextureHandle, _normalTextureIndex);
            
            // デプステクスチャを設定
            builder.SetRenderAttachmentDepth(resourceData.activeDepthTexture);
            
            // パスの実行後テクスチャをグローバル変数に設定
            builder.SetGlobalTextureAfterPass(_albedoTextureHandle, Shader.PropertyToID("_CustomAlbedoTexture"));
            builder.SetGlobalTextureAfterPass(_normalTextureHandle, Shader.PropertyToID("_CustomNormalTexture"));
            
            // 描画データを設定
            var cameraData = frameData.Get<UniversalCameraData>();
            var renderingData = frameData.Get<UniversalRenderingData>();
            var lightData = frameData.Get<UniversalLightData>();

            var drawSettings = RenderingUtils.CreateDrawingSettings(_shaderTagId, renderingData, cameraData, lightData, _sortingCriteria);

            var rendererListParams = new RendererListParams(renderingData.cullResults, drawSettings, _filteringSettings);
            passData.RendererListHandle = renderGraph.CreateRendererList(rendererListParams);
            builder.UseRendererList(passData.RendererListHandle);
            
            // 描画
            builder.SetRenderFunc
            (
                static (PassData passData, RasterGraphContext context) =>
                {
                    context.cmd.DrawRendererList(passData.RendererListHandle);
                }
            );
        }
    }
}

MRTShader

MRTのシェーダーです。
これは既存と変わりません。
albedoとnormalを分けて描画します。
SV_Target[i]がSetRenderAttachmentの第二引数と一致するようにしてください。

Shader "Custom/MRTShader"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        
    }
    SubShader
    {
        Pass
        {
            Name "MRTRender"
            
            Tags
            {
                "LightMode" = "MRTRender"
            }
            
            Cull Back
            ZWrite On
            ZTest LEqual
            
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #pragma target 3.5

            #pragma multi_compile_instancing

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
            
            TEXTURE2D_X(_MainTex);
            SAMPLER(sampler_MainTex);

            CBUFFER_START(UnityPerMaterial)
                float4 _MainTex_ST;
                float4 _Color;
            CBUFFER_END

            struct Attributes
            {
                float4 positionOS : POSITION;
                float3 normalOS : NORMAL;
                float2 uv : TEXCOORD0;
            };

            struct Varyings
            {
                float4 positionCS : SV_POSITION;
                float2 uv : TEXCOORD0;
                float3 normalWS : TEXCOORD1;
            };

            struct Output
            {
                float4 albedo : SV_Target0;
                float4 normal : SV_Target1;
            };

            Varyings vert(Attributes input)
            {
                Varyings output = (Varyings)0;

                VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz);
                VertexNormalInputs normalInput = GetVertexNormalInputs(input.normalOS);

                output.positionCS = vertexInput.positionCS;
                output.uv = TRANSFORM_TEX(input.uv, _MainTex);
                output.normalWS = normalInput.normalWS;

                return output;
            }

            Output frag(Varyings input) : SV_Target
            {
                Output output = (Output)0;

                float4 albedo = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv) * _Color;

                output.albedo = half4(albedo.rgb, 1.0f);
                float3 normal = (normalize(input.normalWS) + 1.0f) * 0.5f;
                output.normal = half4(normal, 1.0f);

                return output;
            }
            ENDHLSL
        }
    }
}

CombinePass

二つのレンダーターゲットの出力を合成するパスを作成します。

Materialの指定

まずは合成するためのシェーダーを指定します。

private const string ShaderName = "Hidden/MRTCombine";

private Material _material;

private Material Material
{
    get
    {
        if (_material == null)
        {
            _material = new Material(Shader.Find(ShaderName));
        }
            
        return _material;
    }
}

Pass内でマテリアルを作成します。
他にもRendererFeatureでマテリアルをセットする方法などありますが、シェーダーを変えない場合はこれが楽だと思います。
また最後に破棄をする必要があります。

public void Dispose()
{
    CoreUtils.Destroy(_material);
}

実際に描画する

MRTと違って描画するだけなので短めです。

public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
    using (var builder = renderGraph.AddRasterRenderPass<PassData>("CombinePass", out var passData))
    {
        builder.AllowPassCulling(false);
        builder.AllowGlobalStateModification(true);
            
        var resourceData = frameData.Get<UniversalResourceData>();
            
        passData.Material = Material;
            
        builder.SetRenderAttachment(resourceData.activeColorTexture, 0);

        builder.SetRenderFunc
        (
            static (PassData passData, RasterGraphContext context) =>
            {
                context.cmd.DrawProcedural(Matrix4x4.identity, passData.Material, 0, MeshTopology.Triangles, 3);
            }
        );
    }
}

レンダーターゲットは変更する必要が無くてもセットしないといけません。

builder.SetRenderAttachment(resourceData.activeColorTexture, 0);

最後に画面全体を覆うポリゴンを描画します。

builder.SetRenderFunc
(
    static (PassData passData, RasterGraphContext context) =>
    {
        context.cmd.DrawProcedural(Matrix4x4.identity, passData.Material, 0, MeshTopology.Triangles, 3);
    }
);

以上が合成するパスの内容です。

CombinePassの全貌

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.RenderGraphModule;
using UnityEngine.Rendering.Universal;

public class CombinePass : ScriptableRenderPass
{
    private class PassData
    {
        public Material Material;
    }

    private const string ShaderName = "Hidden/MRTCombine";
    
    private Material _material;

    private Material Material
    {
        get
        {
            if (_material == null)
            {
                _material = new Material(Shader.Find(ShaderName));
            }
            
            return _material;
        }
    }

    public CombinePass(RenderPassEvent renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing)
    {
        this.renderPassEvent = renderPassEvent;
    }
    
    public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
    {
        using (var builder = renderGraph.AddRasterRenderPass<PassData>("CombinePass", out var passData))
        {
            builder.AllowPassCulling(false);
            builder.AllowGlobalStateModification(true);
            
            var resourceData = frameData.Get<UniversalResourceData>();
            
            passData.Material = Material;
            
            builder.SetRenderAttachment(resourceData.activeColorTexture, 0);
            
            builder.SetRenderFunc
            (
                static (PassData passData, RasterGraphContext context) =>
                {
                    context.cmd.DrawProcedural(Matrix4x4.identity, passData.Material, 0, MeshTopology.Triangles, 3);
                }
            );
        }
    }

    public void Dispose()
    {
        CoreUtils.Destroy(_material);
    }
}

CombineShader

最後に合成するシェーダーです。
適当にハーフランバート入れてますが、とりあえずアルベドとノーマルを合成できるか確認している程度です。

Shader "Hidden/MRTCombine"
{
    SubShader
    {
        Cull Off
        ZWrite Off
        ZTest Always
        LOD 100
        
        Tags
        {
            "RenderType" = "Opaque"
            "RenderPipeline" = "UniversalPipeline"
        }
        LOD 200
        
        Pass
        {
            Name "Combine"
            
            HLSLPROGRAM
            
            #pragma vertex vert
            #pragma fragment frag

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

            TEXTURE2D_X(_CustomAlbedoTexture);
            SAMPLER(sampler_CustomAlbedoTexture);

            TEXTURE2D_X(_CustomNormalTexture);
            SAMPLER(sampler_CustomNormalTexture);

            struct Attribute
            {
                uint vertexID : SV_VertexID;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct Varyings
            {
                float4 positionCS : SV_POSITION;
                float2 texcoord : TEXCOORD0;
                UNITY_VERTEX_OUTPUT_STEREO
            };

            Varyings vert(Attribute input)
            {
                Varyings output;
                UNITY_SETUP_INSTANCE_ID(input);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output)

                output.positionCS = GetFullScreenTriangleVertexPosition(input.vertexID);
                output.texcoord = GetFullScreenTriangleTexCoord(input.vertexID);

                return output;
            }

            half4 frag(const Varyings IN) : SV_Target
            {
                DEFAULT_UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
                float2 uv = UnityStereoTransformScreenSpaceTex(IN.texcoord);
                half4 albedo = SAMPLE_TEXTURE2D_X(_CustomAlbedoTexture, sampler_CustomAlbedoTexture, uv);
                half4 normal = SAMPLE_TEXTURE2D_X(_CustomNormalTexture, sampler_CustomNormalTexture, uv);

                 Light light = GetMainLight();

                half halfLambert = dot(normal, light.direction) * 0.5 + 0.5;

                albedo *= halfLambert;

                return half4(albedo.rgb, 1.0f);
            }
            
            ENDHLSL
        }
    }
}

最後に

以上がRenderGraphにおけるMRTの実装例です。
何かのご参考になれば幸いです。

Discussion