2024년 9월 초 U/Day Tokyo 2024 에서 소개 된 URP17 과 렌더그래프에 대한 활용방법이라는 주제의 강연입니다.
그리고 추가로... 게임개발자이신 쿄우카이님의 Zenn 블로그에 실린 글을 간략히 소개 해 보겠습니다.
유니티의 RenderGraph System(URP17)을 지원하는 가장 심플한 Custom RP를 만들어보기 라는 블로그 글을 발췌 했습니다.
국내에는 아직 딱히 렌더그래프에 대한 스터디가 많이 없는 것 같네요.
소개
글쓰기가 서툴러서 연습도 할겸 여러 가지 글을 써보자는 생각으로 글을 올리기 시작했습니다. 이번에는 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만 숨겨져 있습니다.
- 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)에 대응하는 가장 간단한 커스텀 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 자체)를 만드는 글도 쓸 수 있으면 좋겠네요.
'TECH.ART.FLOW.IO' 카테고리의 다른 글
[번역] Unity6에서 RenderGraph를 활용해보자 - 구현 응용편 1편 (0) | 2024.10.06 |
---|---|
[번역]Unity6에서 RenderGraph를 활용해보자 - 기본 기능편 (1) | 2024.10.06 |
[소식] 유나이트 2024 하이라이트 게임 개발의 도구, 강연 및 변화 (17) | 2024.09.27 |
[번역] Optimizing AMD FSR for Mobiles (1) | 2024.09.20 |
[소식] 유니티 2024 테크데모 공개. (2) | 2024.09.20 |