TECHARTNOMAD | TECHARTFLOWIO.COM

TECH.ART.FLOW.IO

[번역] Unity6에서 RenderGraph를 활용해보자!응용 구현편 2 '데이터 전달'

jplee 2024. 10. 21. 12:05

역자 주 : RenderGraph에서 간단한 처리를 위해서는 생각보다 간단해 진 경향이 있습니다. 다만 이 전에 여러가지 데이터 타입을 사용했었다면 API 문서를 더 자세하게 읽고 테스트 해 볼 필요가 있더라고요... 데이터 타입이 병합 되었고 자동으로 처리 해 주는 몇 가지가 있는가 하면 버퍼 플립을 자동으로 해 주던 것을 이제는 수동으로 해 줘야 하기도 하고 말이죠.

일본 사이버 에이전트 장유빈 엔지니어의 새로운 글이 추가 되어 소개 합니다.


소개

안녕하세요, CyberAgent SGE 코어텍 소속 장유빈입니다.

지난 시간에는 RenderGraph에서 간단한 Blit 작업을 구현하는 방법에 대해 알아보았습니다.

이번 시간에는 Pass 간 데이터 전달 방법에 대해서 알아보도록 하겠습니다.

지금까지의 기사

 

[번역]Unity6에서 RenderGraph를 활용해보자 - 기본 기능편

역자의 말.최근 보름동안 유니티2022 버전에서 개발 된 렌더링 메서드들을 유니티6 으로 전환하고 있습니다. 역자 역시 Render Graph 를 최근에서야 다뤄보고 있기 때문에 많은 자료와 예제들을 찾아

techartnomad.tistory.com

 

 

[번역] Unity6에서 RenderGraph를 활용해보자 - 구현 응용편 1편

이전 글 Unity6에서 RenderGraph를 활용해보자 - 기본 기능편 을 먼저 읽고 오는 것이 좋아요. [번역]Unity6에서 RenderGraph를 활용해보자 - 기본 기능편역자의 말.최근 보름동안 유니티2022 버전에서 개발

techartnomad.tistory.com

Unity6에서 RenderGraph를 잘 사용해보자! 응용 구현편 2「데이터 전달」←지금은 여기

계속

작성 시점의 환경

  • Unity6 (6000.0.19f1)
  • URP 17.0.3

기존 패스 간 데이터 전달 방식

RenderGraph에 대해 이야기하기 전에, 먼저 기존에 많이 사용되던 데이터 전달 방법에 대해 소개하겠습니다.

1. 데이터 직접 전달 (추천도 : △)

Pass 클래스의 Get, Set 등의 함수나 프로퍼티를 통해 데이터를 주고받는 방식입니다.

샘플 코드

class PassA
    {
        // PassA의 처리 결과는 _resultRT로 출력한다.
        RTHandle _resultRT;
        public RTHandle GetResultRT()
        {
            return _resultRT:
        }
    }
    
    class PassB
    {
        // PassA 인스턴스 참조 필요
        PassA _passA;
        // 전략 계획
        void Draw()
        {
            var passAResultRT = _passA.GetResultRT();
            // 후속 처리
        }
    }

이해하기 쉽고 구현하기 쉬운 방법이지만, 클래스 간 직접 참조가 필요하고, 구현이 밀집되어 있는 경향이 있습니다.따라서 테스트 데모나 제한된 범위 내에서만 사용하는 것 외에는 별로 추천하지 않습니다.

2. Shader의 Global 파라미터로 설정하기 (추천도 : ○)

Shader에 전달되는 데이터라면 Shader의 Global 파라미터로 설정하여 이후 실행되는 Shader에서 직접 접근할 수 있도록 하는 방식입니다.

샘플 코드

 // Shader 클래스의 Static 함수로 설정 가능
Shader.SetGlobalFloat(propertyID, data);
Shader.SetGlobalTexture(propertyID, rtHandle);

// CommandBuffer로도 가능
// 설정 타이밍을 정할 수 있기 때문에 기본적으로 이쪽을 추천합니다.
cmd.SetGlobalFloat(propertyID, data);
cmd.SetGlobalTexture(propertyID, rtHandle);

한번 Global 파라미터로 설정해두면 후속 Pass에 매번 데이터를 전달하는 수고를 덜 수 있고, Material.Set 처리 시간도 절약할 수 있는 장점이 있습니다.

URP 내부에서는 CameraDepth, CameraNormal, MotionVector, 각종 GBuffer, 각종 카메라 매트릭스 등 후속 Pass에서 사용되는 데이터를 Global 파라미터로 설정한다.

한편, Global 파라미터는 PropertyID와 데이터를 바인딩할 뿐이므로, 같은 PropertyID에 다른 데이터를 설정하면 바인딩 대상이 바뀌어 버립니다.또한 URP 내부에는 _ScreenSize, _ScreenParams, _Time 등 일반적인 단어가 사용된 Global 파라미터도 존재하기 때문에 실수로 이들을 대체할 경우 이후 드로잉이 엉망이 될 위험이 있다.

또한, 무분별하게 많은 데이터를 Global 파라미터화하면 성능이나 메모리 사용량에 악영향을 끼칠 수 있다.

따라서 이점을 극대화하고 리스크를 최소화하기 위해서는 후속 드로잉 처리에서 여러 번 사용되는 데이터만 Global 파라미터로 설정하는 것을 권장합니다.또한, Global 파라미터로 설정할 때는 URP나 타사 패키지에서 사용되는 Property 이름과 중복되지 않도록 한 번 검색을 통해 확인하는 것이 좋습니다.

3. 데이터 컨테이너를 통해 데이터 전달(추천도 : ◎)

Pass 클래스와는 별도로 중개자 클래스를 만들어 모든 Pass가 접근할 수 있도록 하여 데이터를 전달하는 방식입니다.

샘플 코드

// 각종 데이터를 저장하는 컨테이너
    class DataContainer
    {
        public float DataA { get; set; }
        public RTHandle TexA { get; set; }
        // 그의 색깔...
    }
    
    class PassA
    {
        private RTHandle _resultRT;
        private float _data;
            
        public Draw(DataContainer container)
        {
            // 처리 약어
            // 출력 데이터를 holder에게 전달
            _dataContainer.DataA = _data;
            _dataContainer.TexA = _resultRT;
        }
    }
    
    class PassB
    {
        public Draw(DataContainer container)
        {
             // holder로부터 데이터 수집
            var dataA = container.DataA;
            var texA = container.TexA;
            // 후속 처리 약어
        }
    }
    
    class SampleRenderer
    {
        // Renderer 측에서 컨테이너 인스턴스를 가지고
        DataContainer _container;
        PassA _passA;
        PassB _passB;
        
        void DrawPasses()
        {
            // 각 Pass 실행 시 컨테이너 전달
            _passA.Draw(_container);
            _passB.Draw(_container);
        }
    }

이러한 구현을 DTO (Data Transfer Object) 패턴이라고 하는데, Pass 클래스 간의 밀집된 결합을 방지하고 데이터를 전달할 수 있습니다.

Shader용 데이터에만 사용할 수 있는 Global 파라미터화 방식과 달리 모든 데이터에 사용할 수 있으며, Shader에서 사용하지 않는 데이터나 Shader용 데이터라도 광범위하게 사용되지 않는 데이터를 전달할 때 가장 추천하는 방식입니다.

기존 시스템에서 URP가 준비해준 데이터 컨테이너

샘플 코드

public virtual void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
    {}
    
public virtual void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{}

기존 시스템에서 OnCameraSetup, Execute 등의 두 번째 인수인 RenderingData가 바로 이 데이터 컨테이너에 해당한다.

이 안에서 URP가 제공하는 CameraData, LightData, ShadowData, PostProcessingData 등 렌더링에 사용할 수 있는 다양한 데이터를 가져올 수 있다.

하지만 이 컨테이너는 URP 데이터를 가져오는 데만 한정되어 있고, 자체 데이터를 추가할 수 없기 때문에 데이터 컨테이너로서의 기능은 불완전했다.

나중에 자세히 설명하겠지만, RenderGraph에서는 URP 데이터를 가져오는 것 외에도 자체 데이터를 자유롭게 추가할 수 있는 새로운 데이터 컨테이너가 제공되어 패스 간 데이터 전달이 매우 편리해졌습니다.

RenderGraph에서 데이터 전달을 구현하는 방법

이제부터 샘플 코드를 통해 RenderGraph에서 데이터 전달을 구현하는 방법에 대해 알아보겠습니다.

다음 기능을 예로 들어 설명한다.

설정된 원의 내부에만 색상과 상하가 반전된 이미지를 그린다.(색상이나 UV 반전 등의 조작은 1Pass로도 구현할 수 있지만, 2Pass로 나눈 것은 Pass 간 데이터 전달을 설명하기 위함)

샘플 이미지

RendererFeature 인스펙터

RenderGraph에서 Shader Global 파라미터를 설정하는 방법

샘플 코드

Shader
Shader "Hidden/Sample/DataTransfer"
{
    SubShader
    {
       Tags { "RenderType"="Opaque" "RenderPipeline" = "UniversalPipeline"}
       ZTest Always ZWrite Off Cull Off

       Pass     // 색상, 상하 반전 텍스처 그리기
        {
            Name "DrawNegative"

            HLSLPROGRAM
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.core/Runtime/Utilities/Blit.hlsl"

            #pragma vertex Vert
            #pragma fragment Frag

            half4 Frag(Varyings input) : SV_TARGET
            {
                float2 uv = input.texcoord.xy;
                uv.y = 1 - uv.y; // 위아래로 돌리기
                half4 color = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv);
                half4 negative = half4(1 - color.rgb, color.a); // 색상 반전
                return negative;
            }
            ENDHLSL
        }

        Pass    // 카메라 색상과 합성
        {
            Name "Combine"

            HLSLPROGRAM
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.core/Runtime/Utilities/Blit.hlsl"

            #pragma vertex Vert
            #pragma fragment Frag

            TEXTURE2D_X(_NegativeTexture);
            float4 _Params;

            #define CENTER_UV       _Params.xy
            #define RADIUS          _Params.z
            #define ASPECT_RATIO    _Params.w

            half4 Frag(Varyings input) : SV_TARGET
            {
                float2 uv = input.texcoord.xy;
                float2 uvDiff = uv - CENTER_UV;

                uvDiff.x *= ASPECT_RATIO;
                float distSqr = dot(uvDiff, uvDiff);
                float radiusSqr = RADIUS * RADIUS;

                half4 negativeColor = SAMPLE_TEXTURE2D_X(_NegativeTexture, sampler_LinearClamp, uv);
                half4 sceneColor = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv);

                // 원 안의 부분을 반전 색상으로 만들기
                half inCircle = distSqr < radiusSqr;
                half4 color = inCircle ? negativeColor : sceneColor;

                return color;
            }
            ENDHLSL
        }
    }
}

 

RenderPass 및 RendererFeature
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.RenderGraphModule;
using UnityEngine.Rendering.Universal;

public class DrawNegativePass : ScriptableRenderPass
{
    private static readonly int NegativeTexturePropertyId = Shader.PropertyToID("_NegativeTexture");
    private static readonly int ParamsPropertyId = Shader.PropertyToID("_Params");

    private Material _material;
    private Vector2 _center;
    private float _radius;

    public void SetData(Material material, Vector2 center, float radius)
    {
        _material = material;
        _center = center;
        _radius = radius;
    }
    private class PassData
    {
        internal Material Material;
        internal TextureHandle SourceTexture;
        //internal TextureHandle NegativeTexture;
        internal Vector4 Params;
    }

    public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
    {
        var cameraData = frameData.Get<UniversalCameraData>();
        var resourceData = frameData.Get<UniversalResourceData>();

        var sourceTextureHandle = resourceData.activeColorTexture;
        var aspectRatio = cameraData.cameraTargetDescriptor.width / (float)cameraData.cameraTargetDescriptor.height;

        var negativeDescriptor = renderGraph.GetTextureDesc(sourceTextureHandle);
        negativeDescriptor.name = "_NegativeTexture";
        negativeDescriptor.clearBuffer = false;
        negativeDescriptor.msaaSamples = MSAASamples.None;
        negativeDescriptor.depthBufferBits = 0;

        var negativeTextureHandle = renderGraph.CreateTexture(negativeDescriptor);

        // 카메라 컬러를 반전시켜 출력용 텍스처로 그리는 RasterRenderPass를 생성하고 RenderGraph에 추가합니다.
        using (var builder = renderGraph.AddRasterRenderPass<PassData>("DrawNegativePass", out var passData))
        {
            // passData에 필요한 데이터 입력
            passData.Material = _material;
            passData.SourceTexture = sourceTextureHandle;
            //passData.NegativeTexture = negativeTextureHandle;
            passData.Params = new Vector4(_center.x, _center.y, _radius, aspectRatio);

            // 드로잉 타겟 설정
            builder.SetRenderAttachment(negativeTextureHandle, 0, AccessFlags.Write);
            builder.UseTexture(sourceTextureHandle, AccessFlags.Read);
            // 해설 *1
            // Shader의 Global 변수에 대한 설정이 가능하도록 하기 위해
            // 주의하세요!
            builder.AllowGlobalStateModification(true);
            // 해설 *2
            // negativeTextureHandle이 그려진 후 "_NegativeTexture"라는 이름의 GlobalTexture로 설정한다.
            builder.SetGlobalTextureAfterPass(negativeTextureHandle, NegativeTexturePropertyId);
            builder.SetRenderFunc(static (PassData data, RasterGraphContext context) =>
            {
                var cmd = context.cmd;
                var material = data.Material;
                var source = data.SourceTexture;
                var parameters = data.Params;

                Blitter.BlitTexture(cmd, source, Vector2.one, material, 0);

                // 해설 *3
                // Texture 이외의 변수는 그리기 함수 내에서 CommandBuffer를 사용하여 Global로 설정한다.
                cmd.SetGlobalVector(ParamsPropertyId, parameters);

                // 해설 *4
                // Texture에서도 그리기 함수 내에서 CommandBuffer를 사용하여 Global로 설정할 수 있지만, RenderGraphViewer에서 감지하지 못하기 때문에 비추천
                //cmd.SetGlobalTexture(NegativeTexturePropertyId, data.NegativeTexture);
            });
        }
    }
}

public class CombinePass : ScriptableRenderPass
{
    private static readonly int NegativeTexturePropertyId = Shader.PropertyToID("_NegativeTexture");

    private Material _material;

    public void SetData(Material material)
    {
        _material = material;
    }
    private class PassData
    {
        internal Material Material;
        internal TextureHandle SourceTexture;
        internal Vector4 Params;
    }

    public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
    {
        var resourceData = frameData.Get<UniversalResourceData>();
        var sourceTextureHandle = resourceData.activeColorTexture;

        // 현재 활성화된 카메라 컬러를 기반으로 합성 텍스처를 생성합니다.
        var targetDesc = renderGraph.GetTextureDesc(sourceTextureHandle);
        targetDesc.name = "_CombineTexture";
        targetDesc.clearBuffer = false;
        targetDesc.depthBufferBits = 0;
        var combineTextureHandle = renderGraph.CreateTexture(targetDesc);

        // 합성한 텍스처를 카메라 컬러로 설정합니다.
        resourceData.cameraColor = combineTextureHandle;

        // 카메라 색상을 반전시켜 출력용 텍스처로 그리는 RasterRenderPass를 생성하고 RenderGraph에 추가합니다.
        using (var builder = renderGraph.AddRasterRenderPass<PassData>("CombinePass", out var passData))
        {
            // passData에 필요한 데이터 입력
            passData.Material = _material;
            passData.SourceTexture = sourceTextureHandle;

            // 드로잉 타겟 설정
            builder.SetRenderAttachment(combineTextureHandle, 0, AccessFlags.Write);
            builder.UseTexture(sourceTextureHandle, AccessFlags.Read);
            // 해설 *5
            // GlobalTexture 사용 선언
            builder.UseGlobalTexture(NegativeTexturePropertyId, AccessFlags.Read);
            // 해설 *6
            // 모든 GlobalTexture 사용 신청
            // builder.UseAllGlobalTexture(true);
            builder.SetRenderFunc(static (PassData data, RasterGraphContext context) =>
            {
                var cmd = context.cmd;
                var material = data.Material;
                var source = data.SourceTexture;
                
                // Params와 NegativeTexture가 이전 Pass에서 Global 파라미터로 설정되었기 때문에
                // 이 패스로 머티리얼을 설정하고 싶어도 그대로 사용할 수 있습니다.
                Blitter.BlitTexture(cmd, source, Vector2.one, material, 1);
            });
        }
    }
}

public class DataTransferRendererFeature : ScriptableRendererFeature
{
    [SerializeField]
    private Vector2 center = new(0.5f, 0.5f);
    [SerializeField]
    private float radius = 0.5f;

    private const string ShaderPath = "Hidden/Sample/DataTransfer";
    private Material _material;
    private Material material
    {
        get
        {
            if (_material == null)
            {
                _material = CoreUtils.CreateEngineMaterial(ShaderPath);
            }
            return _material;
        }
    }

    private DrawNegativePass _drawNegativePass;
    private CombinePass _combinePass;

    public override void Create()
    {
        _drawNegativePass = new DrawNegativePass
        {
            renderPassEvent = RenderPassEvent.AfterRenderingPostProcessing
        };
        _combinePass = new CombinePass
        {
            renderPassEvent = RenderPassEvent.AfterRenderingPostProcessing
        };
    }

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
            if (renderingData.cameraData.cameraType == CameraType.Preview)
        {
            // Preview 카메라는 효과 대상에서 제외되므로 제외
            return;
        }

        _drawNegativePass.SetData(material, center, radius);
        _combinePass.SetData(material);

        renderer.EnqueuePass(_drawNegativePass);
        renderer.EnqueuePass(_combinePass);
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            CoreUtils.Destroy(material);
        }
    }
}
 
 

코드 설명

  1. RenderGraph에서 데이터를 Global로 설정하기 전에 먼저 builder.AllowGlobalStateModification(true) 로 GlobalState 수정 권한을 얻어야 한다.그렇지 않으면 SetGlobalXXX 행위를 할 때 오류가 발생한다.
  2. builder.SetGlobalTextureAfterPass(textureHanlde, propertyId); 를 실행하면, 이 Pass가 실행 종료 시점에 textureHanlde가 자동으로 propertyId라는 Global Slot에 설정된다.
  3. Texture 타입 이외의 데이터는 기존 시스템과 비슷하게 드로잉 함수 내에서 cmd.SetGlobalXXX로 Globalize하는 RenderGraphViewer에서 확인 가능
  4. GlobalTexture도 사용 신청이 필요하며, 신청을 하지 않으면 Pass가 실행되는 시점에 Texture가 파기되는 경우가 있다.


  5. Texture 타입의 데이터도 드로잉 함수 내에서 cmd.SetGlobalTexture를 사용하여 Global화할 수 있지만, RenderGraphViewer에서 감지하지 못하고 예기치 않은 오류가 발생할 수 있으므로 권장하지 않는다.특히 Attachment에 설정된 Texture에 대해 사용하면 반드시 오류가 발생한다.
  6. RenderGraph는 드로잉 단계에 들어가기 전에 모든 드로잉 패스가 신청한 텍스처를 체크하여 각 텍스처의 폐기 시점을 결정하고, 텍스처는 후속 패스에서 더 이상 사용되지 않는 시점에 폐기된다(RenderGraph는 Shader 내의사용을 사전에 감지할 수 없으므로, 사전 사용 신청으로 파기 타이밍을 결정한다).그림과 같이 GlobalTexture의 사용 신청을 하지 않으면 반전 텍스처가 파기되어 획득할 수 없게 된다.
     
  7. 사용하고자 하는 GlobalTexture가 많아 일일이 작성하는 번거로움을 줄이고 싶거나, 코드의 가독성과 유지보수성을 높이고 싶다면 builder.UseAllGlobalTextures(true)로 모든 Global Texture에 대해 사용 신청을 할 수도 있다.
    • 하지만 그렇게 되면 실제로 사용하지 않았는데도 사용 예정으로 판단되어 텍스처 폐기 타이밍이 늦어져 드로잉 성능에 악영향을 끼칠 수 있다.
    • 다만, URP 내부에서도 builder.UseAllGlobalTextures(true) 가 많이 사용되기 때문에 큰 악영향은 없을 것으로 예상한다.

 

주의

AllowGlobalStateModification에 대한 공식적인 주의 의견은 다음과 같습니다.

Allow commands in the command buffer to modify global state. 
This will introduce a render graph sync-point in the frame and cause all passes after this pass to never be reordered before this pass.
This may nave negative impact on performance and memory use if not used carefully so it is recommended to only allow this in specific use cases.

즉, Pass에 Global 파라미터 변경을 허용하면 해당 Pass가 실행되는 시점에 RenderGraph의 동기화 포인트(CPU와 GPU 간 데이터 전송 포인트)가 삽입되어 성능 및 메모리 사용량에 악영향을 미칠 수 있습니다.있습니다.꼭 필요한 경우에만 Global 파라미터화를 사용하세요.

RenderGraph에서 데이터 컨테이너를 사용하는 방법

RenderGraph는 ContextContainer라는 매우 유용한 컨테이너 클래스를 제공합니다.

 ContextContainer의 인스턴스가 Renderer 측에 있으며, RecordRenderGraph 함수의 두 번째 인수로 Pass에 전달되어 그리기 순서대로 각 Pass에 전달됩니다.

public virtual void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)

그 ContextContainer에는 URP 내부의 많은 데이터가 저장되어 있으며, 자체 제작한 Pass로 가져와서 사용할 수 있다.또한, 임의의 시점에 자체 데이터를 추가할 수도 있습니다.

사용방법 목록

데이터 수집 및 추가는 다음과 같은 기능을 통해 가능합니다.
// 컨테이너 변수 이름을 frameData로 설정한다.
ContextContainer frameData;

// <DataType> 타입의 데이터 존재 여부 확인
var isContains = frameData.Contains<DataType>();

// <DataType> 타입의 데이터를 가져옵니다.
// * 존재하지 않는 경우 오류가 발생함
var data0 = frameData.Get<DataType>();

// <DataType> 타입의 데이터를 새로 생성합니다.
// * 이미 존재하는 경우 오류가 발생합니다.
var data1 = frameData.Create<DataType>();

// <DataType> 타입의 데이터를 가져옵니다.
// * 존재하지 않는 경우 새로 생성하기
var data2 = frameData.GetOrCreate<DataType>();

 

URP가 제공한 데이터는 이전 기사에서도 언급했듯이 주로 다음과 같다.
// 카메라 관련 정보: 변환 매트릭스, 카메라 설정 등
var cameraData = frameData.Get<UniversalCameraData>();
// 리소스 관련 정보: 카메라 컬러 버퍼, 카메라 심도 버퍼 등
var resourceData = frameData.Get<UniversalResourceData>();
// 렌더링 관련 정보: RenderCullingResults, RenderLayerMask 등
var renderingData = frameData.Get<UniversalRenderingData>();
// 조명 관련 정보: MainLight, AdditionalLights에 대한 정보 등
var lightData = frameData.Get<UniversalLightData>();

샘플 코드

 

앞 절의 샘플 데이터 전달 방식을 컨테이너를 사용하는 것으로 변경하면 다음과 같습니다.

  • RendererFeature와 Shader는 변경 사항이 없으므로 생략합니다.
RenderPass
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.RenderGraphModule;
using UnityEngine.Rendering.Universal;

// 해설 *1
// ContextItem 상속
public class NegativeCircleData : ContextItem
{
    // 저장할 데이터(모든 유형 가능)
    public TextureHandle NegativeTexture;
    public Vector4 NegativeParams;

    // 프레임 종료 시 리셋 처리
    public override void Reset()
    {
        // TextureHandle은 매 프레임마다 리셋해야 한다.
        NegativeTexture = TextureHandle.nullHandle;
    }
}

public class DrawNegativePass : ScriptableRenderPass
{
    private Material _material;
    private Vector2 _center;
    private float _radius;

    public void SetData(Material material, Vector2 center, float radius)
    {
        _material = material;
        _center = center;
        _radius = radius;
    }
    private class PassData
    {
        internal Material Material;
        internal TextureHandle SourceTexture;
    }

    public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
    {
        var cameraData = frameData.Get<UniversalCameraData>();
        var resourceData = frameData.Get<UniversalResourceData>();

        var sourceTextureHandle = resourceData.activeColorTexture;
        var aspectRatio = cameraData.cameraTargetDescriptor.width / (float)cameraData.cameraTargetDescriptor.height;

        var negativeDescriptor = renderGraph.GetTextureDesc(sourceTextureHandle);
        negativeDescriptor.name = "_NegativeTexture";
        negativeDescriptor.clearBuffer = false;
        negativeDescriptor.depthBufferBits = 0;

        var negativeTextureHandle = renderGraph.CreateTexture(negativeDescriptor);

        // 해설 *2
        // frameDataにNegativeCircleDataを作成する
        // 중복 생성 우려가 있는 경우 frameData.GetOrCreate를 사용한다.
        var negativeCircleData = frameData.Create<NegativeCircleData>();
        // 다음 Pass에 전달할 데이터를 넣는다.
        negativeCircleData.NegativeParams = new Vector4(_center.x, _center.y, _radius, aspectRatio);
        negativeCircleData.NegativeTexture = negativeTextureHandle;

        // 카메라 색상을 반전시켜 출력용 텍스처로 그리는 RasterRenderPass를 생성하고 RenderGraph에 추가합니다.
        using (var builder = renderGraph.AddRasterRenderPass<PassData>("DrawNegativePass", out var passData))
        {
            // passData에 필요한 데이터 입력
            passData.Material = _material;
            passData.SourceTexture = sourceTextureHandle;

            // 드로잉 타겟 설정
            builder.SetRenderAttachment(negativeTextureHandle, 0, AccessFlags.Write);
            builder.UseTexture(sourceTextureHandle, AccessFlags.Read);
            builder.SetRenderFunc(static (PassData data, RasterGraphContext context) =>
            {
                var cmd = context.cmd;
                var material = data.Material;
                var source = data.SourceTexture;
                Blitter.BlitTexture(cmd, source, Vector2.one, material, 0);
            });
        }
    }
}

public class CombinePass : ScriptableRenderPass
{
    private static readonly int NegativeTexturePropertyId = Shader.PropertyToID("_NegativeTexture");
    private static readonly int ParamsPropertyId = Shader.PropertyToID("_Params");
    private Material _material;

    public void SetData(Material material)
    {
        _material = material;
    }
    private class PassData
    {
        internal Material Material;
        internal TextureHandle SourceTexture;
        internal TextureHandle NegativeTexture;
        internal Vector4 NegativeParams;
    }

    public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
    {
        var resourceData = frameData.Get<UniversalResourceData>();
        var sourceTextureHandle = resourceData.activeColorTexture;

        // 해설 *3
        if (!frameData.Contains<NegativeCircleData>())
        {
            // NegativeCircleData가 없으면 이 Pass를 실행하지 않습니다.
            Debug.LogError("NegativeCircleData is not found.");
            return;
        }
        // frameData에서 NegativeCircleData 가져오기
        var negativeCircleData = frameData.Get<NegativeCircleData>();

        // 현재 활성화된 카메라 컬러를 기반으로 합성 텍스처를 생성합니다.
        var targetDesc = renderGraph.GetTextureDesc(sourceTextureHandle);
        targetDesc.name = "_CombineTexture";
        targetDesc.clearBuffer = false;
        targetDesc.depthBufferBits = 0;
        var combineTextureHandle = renderGraph.CreateTexture(targetDesc);

        // 합성한 텍스처를 카메라 컬러로 설정합니다.
        resourceData.cameraColor = combineTextureHandle;

        // 카메라 색상을 반전시켜 출력용 텍스처로 그리는 RasterRenderPass를 생성하고 RenderGraph에 추가합니다.
        using (var builder = renderGraph.AddRasterRenderPass<PassData>("CombinePass", out var passData))
        {
            // passData에 필요한 데이터 입력
            passData.Material = _material;
            passData.SourceTexture = sourceTextureHandle;
            passData.NegativeTexture = negativeCircleData.NegativeTexture;
            passData.NegativeParams = negativeCircleData.NegativeParams;

            // 드로잉 타겟 설정
            builder.SetRenderAttachment(combineTextureHandle, 0, AccessFlags.Write);
            builder.UseTexture(sourceTextureHandle, AccessFlags.Read);
            // 해설 *4
            // NegativeTexture는 물론 사용 신청이 필요합니다.
            builder.UseTexture(negativeCircleData.NegativeTexture, AccessFlags.Read);
            builder.SetRenderFunc(static (PassData data, RasterGraphContext context) =>
            {
                var cmd = context.cmd;
                var material = data.Material;
                var source = data.SourceTexture;
                var negativeTexture = data.NegativeTexture;
                var negativeParams = data.NegativeParams;

                // 필요한 데이터가 Global화되어 있지 않으므로 여기서 Material에 텍스처와 파라미터를 설정한다.
                material.SetTexture(NegativeTexturePropertyId, negativeTexture);
                material.SetVector(ParamsPropertyId, negativeParams);

                Blitter.BlitTexture(cmd, source, Vector2.one, material, 1);
            });
        }
    }
}

코드 설명

 

  1. ContextContainer에 넣을 데이터 클래스는 반드시 ContextItem을 상속해야 한다.
    • Reset()은 반드시 오버라이드해야 하며, 매 프레임마다 리셋 작업을 그 안에 기술한다.
  2. 같은 데이터 타입으로 Create()를 두 번 이상 호출하면 에러가 발생하므로, 신규 데이터 생성(메모리 확보) 타이밍을 엄격하게 제어할 필요가 없다면 GetOrCreate()를 사용하는 것이 무난할 것 같다.
  3. NegativeCircleData가 존재하지 않는 경우 후속 처리 건너뛰기
    • CombinePass의 실행은 DrawNegativePass의 실행 결과에 의존하지만, 직접 선행 Pass의 실행 상태를 확인하는 것이 아니라 컨테이너 내 필요한 데이터의 유무로 실행 여부를 판단하여 선행 조건이 되는 Pass에 대한 직접적인 참조를 피한다.
    • 이를 통해 패스 간의 밀집 결합을 방지할 수 있다.
  4. 간과하기 쉽지만, 사용하는 RenderTexture는 출처와 상관없이 사용 신청을 해야 합니다.
RenderGraphViewer로 확인

마지막으로

이번 글에서는 RenderGraph를 사용할 때 데이터 전달 방법에 대해 Global 파라미터와 데이터 컨테이너 두 가지 방법을 소개했습니다.각각의 적용 상황이 다르기 때문에 잘 구분해서 사용하는 것이 중요합니다.

Global 매개변수 사용 권장

  • 셰이더 내에서 사용되며, 이후 여러 패스 및 셰이더에서 공유되는 데이터
  • 자주 교체할 필요가 없는 데이터

데이터 컨테이너 사용 권장

  • Shader 내에서 사용하지 않는 데이터
  • 셰이더 내에서 사용되지만, 이후 여러 패스나 셰이더에서 공유되어 사용되지 않는 데이터
  • 자주 교체가 필요한 데이터

적절한 방법을 선택하면 보다 효율적이고 유지보수성이 높은 렌더링 파이프라인을 구축할 수 있습니다. 상황에 따라 두 가지 방법을 적절히 조합하는 것이 중요합니다.

 

편지에 대한 서면 답장

다음 시간에는 Render Graph의 FrameBufferFetch 기능에 대해 소개할 예정입니다. 그럼 다음에 또 만나요.


원문

https://blog.sge-coretech.com/entry/2024/10/16/183856

 

Unity6の新機能RenderGraphの解説 - CORETECH ENGINEER BLOG

Unity6 RenderGraphでPass間のデータ受け渡し手法解説

blog.sge-coretech.com