TECHARTNOMAD | TECHARTFLOWIO.COM

TECH.ART.FLOW.IO

[소식][번역]URP 17로의 업그레이드와 Render Graph 활용 방법

jplee 2024. 10. 6. 20:21

2024년 9월 초 U/Day Tokyo 2024 에서 소개 된 URP17 과 렌더그래프에 대한 활용방법이라는 주제의 강연입니다.

 

그리고 추가로... 게임개발자이신 쿄우카이님의 Zenn 블로그에 실린 글을 간략히 소개 해 보겠습니다.

유니티의 RenderGraph System(URP17)을 지원하는 가장 심플한 Custom RP를 만들어보기 라는 블로그 글을 발췌 했습니다.

 

Unity의 RenderGraphSystem(URP17)을 지원하는 가장 심플한 Custom RP를 만들어보기

서론 출력을 잘 못해서 연습도 겸해서 여러 가지를 작성해 보려고 글을 올리기 시작했습니다. 이번에는 Unity 2023.3에서 URP에서도 RenderGraph를 활성화할 수 있게 되었기 때문에 RenderGraphSyste vs.

zenn.dev

국내에는 아직 딱히 렌더그래프에 대한 스터디가 많이 없는 것 같네요.

소개

글쓰기가 서툴러서 연습도 할겸 여러 가지 글을 써보자는 생각으로 글을 올리기 시작했습니다. 이번에는 Unity 2023.3에서 URP에서도 RenderGraph를 활성화할 수 있게 되면서 RenderGraphSyste에 대응하는 간단한 ScriptableRenderPass를 작성해 보았습니다.

이 글의 내용

  • URP의 OpaquePass 그리기 후 화면 효과를 적용하는 ScriptableRenderPass(ScriptableRendererFeature)를 생성합니다.
    • 이 글을 쓰는 시점에서 FullScreenPassRendererFeature가 표준으로 존재하기 때문에 특별히 생성물 자체에 의미가 없으므로 그냥 넘어가도록 하자.
  • RenderGraph 미지원(패키지 내 주석에 따라 non-RG)인 경우와 RenderGraph 지원(역시 주석에 따라 RG)인 경우의 구현을 모두 기술하고 있다.

이 글에서 다루지 않은 것들

  • URP, SRP 자체의 이야기.
    • 설명서를 보는 것이 더 이해하기 쉬울 것입니다.특히 RenderGrpah의 장점 등에 대해서요.
    • URP
    • 매뉴얼SRP 매뉴얼
  • GameObject(뭔가 Renderer를 가지고 있는 등 그리기 대상이 될 수 있는)를 그리는 경우의 RenderGraph 대응에 대해서(다른 글에서 작성할 수도 있습니다)
    • 안 건드린다고 하면서 가볍게 건드리면,ScriptableRenderContext.DrawRenderers에 의한 드로잉이 아니라 RendererListHandle을 가져와서 CommandBuffer.DrawRendererList(CommandBuffer도CommandBuffer 또한 목적에 맞게 여러 종류가 있으므로 RasterCommandBuffer 등을 사용)로 작성하도록 되어 있다.
    • RenderGraph와 마찬가지로 조금 전 버전부터 포함되어 있습니다.

기사 작성 시점의 환경

  • Unity 2023.3.0b7
  • Universal RP 17.0.2
  • Core RP 17.0.2

해봤습니다

사용 장면

  • Uinversal RP 패키지의 Sample에 포함된 Lit 장면을 사용합니다.
    • UI만 숨겨져 있습니다.

이번에 사용할 화면 효과 셰이더

  • 먼저 화면에 효과를 적용할 셰이더를 준비합니다.
  • 셰이더 그래프에서 채도를 조작하는 노드(saturation 노드)를 끼워 넣는 것만으로 간단해집니다.

RendererFeature

  • RendererFeature는 기존 구현과 RenderGraph 지원 이후에도 동일하게 다음과 같이 사용합니다.
using UnityEngine;
using UnityEngine.Rendering.Universal;

namespace Sample.CustomRP
{
    public class CustomRendererFeature : ScriptableRendererFeature
    {
        [SerializeField] Material blitMaterial;

        CustomSamplePass customSamplePass = null;

        public override void Create()
        {
            if (customSamplePass != null) return;
            
            customSamplePass = new CustomSamplePass(RenderPassEvent.AfterRenderingOpaques, "CustomSamplePass", blitMaterial);
        }

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

}

RenderGraph를 사용하지 않는 기존 RenderPass의 경우

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using ProfilingScope = UnityEngine.Rendering.ProfilingScope;

namespace Sample.CustomRP
{
    public class CustomSamplePass : ScriptableRenderPass
    {
        readonly Material blitMaterial;
        readonly string profilerTag;
        
        public CustomSamplePass(RenderPassEvent renderPassEvent, string profilerTag, Material blitMaterial)
        {
            this.renderPassEvent  = renderPassEvent;
            this.profilerTag      = profilerTag;
            this.profilingSampler = new ProfilingSampler(profilerTag);
            this.blitMaterial     = blitMaterial;
        }
        
        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
            var commandBuffer = CommandBufferPool.Get(profilerTag);

            using (new ProfilingScope(commandBuffer, profilingSampler))
            {
                // 여담: RenderTarget의 스왑까지 해준 Blit이 Obsolete로 ......
                Blit(commandBuffer, ref renderingData, blitMaterial, 0);
            }
            
            context.ExecuteCommandBuffer(commandBuffer);
            commandBuffer.Clear();

            CommandBufferPool.Release(commandBuffer);
        }
    }
}
  • 실행하면 다음과 같습니다(saturation:0.2로 설정했습니다).
  • 불투명하게 그려진 구체와 받침대, 지면만 채도를 낮추고 있습니다.

RenderGraph를 사용하는 경우

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.RenderGraphModule;
using UnityEngine.Rendering.Universal;
using ProfilingScope = UnityEngine.Rendering.ProfilingScope;

namespace Sample.CustomRP
{
    public class CustomSamplePass : ScriptableRenderPass
    {
        readonly Material blitMaterial;
        readonly string profilerTag;

        public CustomSamplePass(RenderPassEvent renderPassEvent, string profilerTag, Material blitMaterial)
        {
            this.renderPassEvent = renderPassEvent;
            this.profilerTag     = profilerTag;
            profilingSampler   = new ProfilingSampler(profilerTag);
            this.blitMaterial    = blitMaterial;
        }

        // Pass에서 사용할 데이터 정의하기
        internal class PassData
        {
            internal TextureHandle sourceHandle;
            internal TextureHandle tempCopy;

            internal Material material;
        }

        public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
        {
            // RenderingData가 아닌 ContextContainer에서 직접 필요한 데이터를 가져오게 되었다.
            UniversalCameraData    cameraData    = frameData.Get<UniversalCameraData>();
            
            // IRenderGraphBuilder를 사용하여 구축하기
            // Pass의 유무에 따라 RasterPass, UnsafePass 등을 선택
            // 이번에는 UnsafePass로 추가
            using (var builder = renderGraph.AddUnsafePass<PassData>(profilerTag, out var passData))
            {
                UniversalResourceData resourceData = frameData.Get<UniversalResourceData>();
                
                passData.material = blitMaterial;
                passData.sourceHandle = resourceData.activeColorTexture;

                // 이번에는 Cameara의 RenderTarget과 유사한 것을 원하기 때문에
                // RenderTargetDescriptor에서 TextureHandle을 만드는 것을 사용했다.
                var desc = cameraData.cameraTargetDescriptor;
                desc.depthBufferBits = 0;
                passData.tempCopy    = UniversalRenderer.CreateRenderGraphTexture(renderGraph, desc, "_tempCopy", true);
                // 그 외
                // builder.CreateTransientTexture(in TextureDesc desc);
                //에서 builder로 생성하기
                // renderGraph.ImportTexture(RTHandle rt);
                // 등의 RTHandle에서 생성하는 방법도 있다. 
                // 외부에서 관리하는 RTHandle을 사용하는 경우 등의 패턴은 다음과 같다.

                // Pass 중에 사용할 경우 선언해 둔다.
                builder.UseTexture(passData.tempCopy);


                // Pass를 컬링되지 않도록 설정
                builder.AllowPassCulling(false);
                builder.AllowGlobalStateModification(true);


                // UnsafePass의 실행을 함수를 설정합니다(즉, 기존 Execute에서 호출하던 Pass의 드로잉을 중심으로).
                builder.SetRenderFunc((PassData data, UnsafeGraphContext context) =>
                {
                    using (new ProfilingScope(context.cmd, profilingSampler))
                    {
                        // Blitter.Blitter.BlitCameraTexture가 일반 CommandBuffer를 인수로 받기 때문에 준비된 Helper로 변환
                        // builder 생성 시 renderGraph.AddRenderPass를 사용하면 변환이 필요 없지만 builder.UseTexture 등은 사용할 수 없다.
                        var commandBuffer = CommandBufferHelpers.GetNativeCommandBuffer(context.cmd);

                        // RenderGraph에서 타겟의 Swap 조작은 어떻게 하는 것이 좋을까?
                        // 일단 source를 copy
                        Blitter.BlitCameraTexture(commandBuffer, data.sourceHandle, data.tempCopy);
                        // Blit
                        Blitter.BlitCameraTexture(commandBuffer, data.tempCopy, data.sourceHandle, data.material, 0);
                    }
                });
            }
        }

    }
}
  • RenderGraph를 활성화해야 하며, ProjectSettings>Graphics>URP의 Render Graph Settings>Compatibility Mode (Render Graph Disabled)의 체크를 해제한다.
  • 실행 결과는 비슷할 것 같습니다.

 

그리고 추가로...

 

Unity RenderGraphSystem(URP17)에 대응하는 가장 간단한 Custom RP를 만들어 보기 2편

지난번 Unity의 RenderGraphSystem(URP17)에 대응하는 가장 간단한 Custom RP 만들기 지난 글에 이어 출력 실습 글입니다. 이 글의 내용 LayerMask와 ShaderTag(와 RenderQueueRange)로 필터링하고

zenn.dev

 

Unity의 RenderGraphSystem(URP17)에 대응하는 가장 간단한 커스텀 RP를 만들어 보기 2편

전편

위 개시글을 참조하세요.

이 글의 내용

  • LayerMask와 ShaderTag(와 RenderQueueRange)로 필터링한 오브젝트를 그리는 RenderPass를 생성합니다.
  • 지난번과 마찬가지로 URP+RendererFeature를 사용한다면 보다 범용적인 기능이 추가된 RenderObjects(RendererFeature)가 존재하기 때문에 구현물 자체의 의미는 없습니다.

글 작성 시점의 환경

  • Unity 2023.3.0b8
  • Universal RP 17.0.2
  • Core RP 17.0.2

해봤습니다

사용 장면

  • 상자 세 개만 놓여 있는 장면입니다.
  • 사전에 Renderer 측 설정을 변경하여 UnivdersalRenderer 측에서 ObjectRenderer의 대상이 되지 않도록 설정해 둡니다.

RendererFeature

  • 이전과 마찬가지로 기존 구현도 RenderGraph 지원 이후에도 동일하게 다음과 같이 구현합니다.
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering.Universal;

namespace Sample.CustomRP
{
    public class CustomRendererFeature : ScriptableRendererFeature
    {
        [SerializeField] private RenderPassEvent renderPassEvent = RenderPassEvent.AfterRenderingOpaques;
        [SerializeField] private LayerMask layerMask = -1;
        [SerializeField] private RenderQueueType renderQueueType;
        [SerializeField] private List<string> shaderTagList = new() { "UniversalForward" };
        
        CustomSamplePass customSamplePass = null;

        public override void Create()
        {
            if (customSamplePass != null) return;
            
            customSamplePass = new CustomSamplePass(renderPassEvent, "CustomSamplePass",layerMask, renderQueueType,shaderTagList);
        }

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

}

RenderGraph를 사용하지 않는 전통적인 경우의 RenderPass

  • RenderTarget 주변은 조작하지 않으므로 Execute만 구현
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

namespace Sample.CustomRP
{
    public class CustomSamplePass : ScriptableRenderPass
    {
        readonly string profilerTag;
        
        LayerMask layerMask;
        RenderQueueType renderQueueType;
        List<ShaderTagId> shaderTagIds = new();
        
        public CustomSamplePass(RenderPassEvent renderPassEvent, string profilerTag, LayerMask layerMask, RenderQueueType renderQueueType, List<string> shaderTagList)
        {
            this.renderPassEvent = renderPassEvent;
            this.profilerTag     = profilerTag;
            profilingSampler     = new ProfilingSampler(profilerTag);
            this.layerMask       = layerMask;
            this.renderQueueType = renderQueueType;
            foreach (var tag in shaderTagList)
            {
                shaderTagIds.Add(new ShaderTagId(tag));
            }
        }


        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
            // Render시 정렬 조건
            SortingCriteria sortingCriteria = (renderQueueType == RenderQueueType.Transparent)
                ? SortingCriteria.CommonTransparent
                : renderingData.cameraData.defaultOpaqueSortFlags;

            // 이번에는 드로잉 설정, 머티리얼 오버라이드 등을 하지 않으므로 ShaderTag와 정렬 조건만 설정하고, RenderStateBlock도 마찬가지입니다.
            DrawingSettings  drawingSettings  = CreateDrawingSettings(shaderTagIds, ref renderingData, sortingCriteria);
            RenderStateBlock renderStateBlock = new RenderStateBlock(RenderStateMask.Nothing);
            
            
            // RenderQueueRange와 LayerMask로 렌더러가 그릴 대상의 필터 조건을 생성합니다.
            RenderQueueRange renderQueueRange = (renderQueueType == RenderQueueType.Transparent)
                                                    ? RenderQueueRange.transparent
                                                    : RenderQueueRange.opaque;
            FilteringSettings filteringSettings = new FilteringSettings(renderQueueRange, layerMask);

            var cmd = CommandBufferPool.Get(profilerTag);
            using (new ProfilingScope(cmd, profilingSampler))
            {
                // 그리기
                context.DrawRenderers(renderingData.cullResults, ref drawingSettings, ref filteringSettings, ref renderStateBlock);
                
                cmd.Clear();
                context.ExecuteCommandBuffer(cmd);
            }
            CommandBufferPool.Release(cmd);
        }
    }
}

RenderGraph를 사용하는 경우

  • RendererListHandle을 가져와서 사용하게 됩니다.
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.RenderGraphModule;
using UnityEngine.Rendering.Universal;
using ProfilingScope = UnityEngine.Rendering.ProfilingScope;

namespace Sample.CustomRP
{
    public class CustomSamplePass : ScriptableRenderPass
    {
        readonly string profilerTag;
        
        LayerMask layerMask;
        RenderQueueType renderQueueType;
        List<ShaderTagId> shaderTagIds = new();

        public CustomSamplePass(RenderPassEvent renderPassEvent, string profilerTag, LayerMask layerMask, RenderQueueType renderQueueType, List<string> shaderTagList)
        {
            this.renderPassEvent = renderPassEvent;
            this.profilerTag     = profilerTag;
            profilingSampler     = new ProfilingSampler(profilerTag);
            this.renderQueueType = renderQueueType;
            foreach (var tag in shaderTagList)
            {
                shaderTagIds.Add(new ShaderTagId(tag));
            }
        }

        // Pass에서 사용할 데이터 정의하기
        internal class PassData
        {
            internal TextureHandle colorHandle;
            internal TextureHandle depthHandle;
            // このHandleをCommandBuffer.DrawRendererに渡す
            internal RendererListHandle rendererList;
        }

        public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
        {
            //  RenderingData가 아닌 ContextContainer에서 직접 필요한 데이터를 가져오게 되었다.
            UniversalCameraData    cameraData    = frameData.Get<UniversalCameraData>();
            UniversalRenderingData renderingData = frameData.Get<UniversalRenderingData>();
            UniversalLightData     lightData     = frameData.Get<UniversalLightData>();
            UniversalResourceData  resourceData  = frameData.Get<UniversalResourceData>();
            
            // 이번에는 RasterRenderPass로 제작
            using (var builder = renderGraph.AddRasterRenderPass<PassData>(profilerTag, out var passData))
            {
                // Shader 등으로 Global에 접근할 가능성이 있는 경우 True로 설정한다.
                builder.UseAllGlobalTextures(true);
                
                // 특별히 타깃을 바꾸지 않더라도 타깃을 설정하지 않으면 화를 낸다.
                passData.colorHandle = resourceData.activeColorTexture;
                builder.SetRenderAttachment(passData.colorHandle,0);

                passData.depthHandle = resourceData.activeDepthTexture;
                builder.SetRenderAttachmentDepth(passData.depthHandle);
                
                // Render 시 정렬 조건
                SortingCriteria sortingCriteria = (renderQueueType == RenderQueueType.Transparent)
                                                      ? SortingCriteria.CommonTransparent
                                                      : cameraData.defaultOpaqueSortFlags;
                
                // 이번에는 드로잉 설정, 머티리얼 오버라이드 등을 하지 않으므로 ShaderTag와 정렬 조건만 설정합니다.
                // context.DrawRenderers를 사용하던 때와 달리 RenderStaeBlock 오버라이드가 필요하지 않으면 준비하지 않아도 된다.
                // RenderStaeteBlock을 지정하고 싶다면 아래의 RenderListParam으로 설정합니다. 이번에는 없음.
                DrawingSettings  drawSettings     = RenderingUtils.CreateDrawingSettings(shaderTagIds, renderingData, cameraData, lightData, sortingCriteria);
                
                RenderQueueRange renderQueueRange = (renderQueueType == RenderQueueType.Transparent)
                                                        ? RenderQueueRange.transparent
                                                        : RenderQueueRange.opaque;
                FilteringSettings filteringSettings = new FilteringSettings(renderQueueRange, layerMask);
               
                // RenderListHandle 가져오기
                // AssempblyReference에서 URP 패키지를 참조하거나 URP 패키지 내에 직접 커스텀으로 생성하거나 URP 패키지의 AssemblyInfo에서 Internal 접근을 허용한 경우 m, RenderingUtils. CreateRendererList()를 사용할 수 있어 편리하다.
                // URP를 사용하지 않는 경우도 있기 때문에 Core RP만 이용하는 패턴(URP17이라고 하면서)
                RendererListParams rendererListParams = new RendererListParams(renderingData.cullResults, drawSettings, filteringSettings);
                passData.rendererList          = renderGraph.CreateRendererList(rendererListParams);
                
                // 이 Pass에서 사용할 리소스로 선언하기
                builder.UseRendererList(passData.rendererList);
                
                
                builder.SetRenderFunc((PassData data, RasterGraphContext context) =>
                {
                    using (new ProfilingScope(context.cmd, profilingSampler))
                    {
                        // 그리기
                        context.cmd.DrawRendererList(data.rendererList);
                    }
                });
            }
        }

    }
}

이번 정리

  • 특히 Material이나 Camera의 오버라이드, RenderingLayer의 Filter 등 특별한 경우를 제외하고는 RendererFeature로 구현하지 않을 것 같은 내용입니다.(하지만 게임에 따라서는 가끔 사용하는 경우가 있습니다.)
  • 특정 오브젝트를 두 번씩 나눠서 그리거나 할 때도 RenderObjects를 사용하는 것이 편하고요 ......
  • ScriptableRenderer로 Renderer 자체를 풀로 만들려면 이번에 한 것과 같은 작업을 해야 할지도 모르겠네요.
  • 언젠가 Renderer 자체(더 나아가 Pipeline 자체)를 만드는 글도 쓸 수 있으면 좋겠네요.