Unity 6 URP Render Graph 패스 종류 완전 정복: AddBlitPass vs AddRasterRenderPass

Unity 6 URP Render Graph 5부작 시리즈
1편: 기초와 구현 원리
2편: 패스 종류 Deep Dive
3편: Render Graph Viewer 완전 정복
4편: Migration 준비편
5편: Migration 실전·운영편

TL;DR (Unity 6 URP 기준)
– 패스 선택 기준은 하나 — Render Graph가 리소스 흐름을 추적할 수 있는가
– 기본 선택지: AddRasterRenderPass(Safe, FBF·머지 가능) / 단순 복사: AddCopyPass / Material Blit: AddBlitPass / 크기 불일치 Blit: AddUnsafePass
– Frame Buffer Fetch: Vulkan·Metal·DX12 지원, TBDR GPU(모바일·Apple Silicon)에서 VRAM 왕복 없이 온칩 메모리 직접 읽기
– Unsafe 패스가 늘수록 머지 불가·최적화 여지 감소 → 처음 설계부터 Safe 패스 위주로


목차

  1. 1편에서 남긴 질문 — 왜 머지가 안 됐는가
  2. 패스 6종 한눈에 비교
  3. Frame Buffer Fetch 원리 — GPU 칩 내부에서 읽는다
  4. Safe vs Unsafe 패스와 패스 머지
  5. RequireIntermediateTexture — Back Buffer 제한을 넘는 법
  6. Frame Data와 ContextItem — 패스 간 데이터 공유
  7. Output Texture — Shader Graph에서 이름으로 접근
  8. MRT(Multiple Render Targets) — 한 패스에서 여러 버퍼 출력
  9. Renderer List와 Culling — 레이어로 오브젝트 필터링
  10. 마무리 — 다음 편 예고

1. 1편에서 남긴 질문 — 왜 머지가 안 됐는가

1편에서 TintingRenderPass를 구현할 때 AddBlitPass를 사용했습니다. Render Graph Viewer를 열면 이 패스는 주변 패스들과 파란 연결선 없이 홀로 서 있습니다. 머지가 되지 않은 것입니다.
왜일까요?
Render Graph Viewer에서 TintingPass가 파란 연결선 없이 단독으로 표시된 화면 — AddBlitPass 사용으로 인해 패스 머지가 적용되지 않은 상태

이유는 간단합니다. AddBlitPass는 Unsafe 패스입니다. Render Graph는 Unsafe 패스의 내부 리소스 접근 패턴을 추적할 수 없기 때문에, 자동 최적화(패스 머지·메모리 재사용)를 포기합니다. 이것이 바로 ‘Render Graph가 내부를 추적할 수 없는 Unsafe 패스(AddBlitPass 등)’‘리소스 흐름을 직접 선언하는 Safe 패스(AddRasterRenderPass 등)’의 차이입니다.

Render Graph에서 제공하는 패스 추가 메서드는 AddCopyPass(Safe), AddBlitPass(Unsafe), AddRasterRenderPass(Safe), AddUnsafePass(Unsafe), AddComputePass, AddRendererListPass 이렇게 여섯 가지입니다. AddComputePassAddRendererListPass는 Safe/Unsafe 분류가 아닌 별도 실행 모델(Compute Shader, Renderer List)이며, 이번 편에서는 각각이 언제 쓰이고, 왜 그 설계를 선택해야 하는지를 코드와 함께 살펴봅니다.


2. 패스 6종 한눈에 비교

아래 표는 Unity 공식 URP Render Graph Samples에서 각 패스가 어떤 역할을 담당하는지 정리한 것입니다.

API FBF 지원 패스 머지 주요 용도
AddCopyPass ✅ 자동 ✅ 가능 텍스처 단순 복사 (FBF 자동 적용)
AddBlitPass ❌ 없음 ❌ 불가 Material 적용 Blit (Unsafe)
AddRasterRenderPass ✅ 수동 ✅ 가능 커스텀 렌더·FBF 직접 제어
AddUnsafePass ❌ 없음 ❌ 불가 크기 다른 Blit, 레거시 호환
AddComputePass Compute Shader
AddRendererListPass 계열 오브젝트 레이어 필터 렌더

아래 의사 결정 트리가 패스 선택의 기준이 됩니다.

커스텀 렌더 패스가 필요한가? RecordRenderGraph 진입점 GPU 연산(Compute Shader)? 버퍼 병렬처리, 범용 연산 Yes AddComputePass No 오브젝트 레이어 필터 렌더? DrawRendererList, 커스텀 컬링 Yes AddRenderer ListPass No Source와 Target 크기가 다른가? Blit 크기 불일치 → 머지 자체 불가 Yes AddUnsafePass No Material(쉐이더) 적용 Blit? FBF 없음, 성능보다 편의 우선 Yes AddBlitPass No 단순 텍스처 복사? FBF 자동, 머지 가능 Yes AddCopyPass No (FBF 수동) AddRaster RenderPass Safe (머지 가능) Unsafe (머지 불가)

3. Frame Buffer Fetch 원리 — GPU 칩 내부에서 읽는다

일반적인 Blit 흐름을 생각해 봅시다. Fragment Shader가 소스 텍스처를 읽으려면 GPU의 텍스처 샘플러가 VRAM(Video RAM)에 저장된 데이터를 버스를 통해 가져와야 합니다. 이 과정에서 메모리 대역폭이 소모되고, 특히 모바일 GPU에서는 배터리와 발열에 직결됩니다.

Frame Buffer Fetch(FBF)는 다른 경로를 취합니다. FBF는 Vulkan, Metal, DirectX 12 API에서 지원되며(DirectX 11은 미지원), 실질적인 성능 이점은 TBDR(타일 기반 렌더링) GPU에서만 얻을 수 있습니다. TBDR GPU — 모바일(Mali, Adreno)과 Apple Silicon — 는 현재 타일의 픽셀 데이터를 GPU 온칩(on-chip) 메모리에 유지하기 때문에, FBF를 통해 VRAM을 경유하지 않고 Fragment Shader에서 직접 읽을 수 있습니다. 데스크톱 NVIDIA(Maxwell 이후)와 AMD 최근 아키텍처도 타일 기반 래스터라이저를 부분 도입하여 DX12 Native RenderPass 환경에서 FBF를 사용할 수 있으나, 완전한 온칩 타일 메모리 구조가 아니므로 모바일 대비 성능 이점은 제한적입니다.

일반 Blit (AddBlitPass) GPU Fragment Shader Texture Sampler VRAM (Video RAM) 대역폭 소모 ↑ 발열·배터리 ↑ 버스 왕복 ↓쓰기 ↑읽기 Frame Buffer Fetch (AddRasterRenderPass) GPU Fragment Shader On-chip Tile Buffer 직접 VRAM (사용 안 함) 대역폭 소모 없음 → 성능 ↑ 모바일: GPU 대역폭 ↑ 발열 ↑ 모바일: GPU 대역폭 ↓ 성능 ↑

FBF를 코드에서 활성화하려면 두 단계가 필요합니다. 첫째, AddRasterRenderPass의 Builder에서 소스를 Input Attachment로 등록합니다. 둘째, Fragment Shader에서 일반 tex2D 대신 FRAMEBUFFER_INPUT_HALF 매크로를 사용합니다.

// AddRasterRenderPass로 FBF 패스 추가 예시
void FBFetchPass(RenderGraph renderGraph, ContextContainer frameData,
    TextureHandle source, TextureHandle destination)
{
    using var builder = renderGraph.AddRasterRenderPass(
        "FBFetch Pass", out var passData);

    // 소스를 Input Attachment로 설정 → FBF 활성화
    passData.source = builder.SetInputAttachment(source, 0, AccessFlags.Read);
    passData.destination = builder.SetRenderAttachment(destination, 0, AccessFlags.Write);

    builder.SetRenderFunc((PassData data, RasterGraphContext ctx) =>
    {
        // DrawProcedural로 FBF 셰이더 실행
        ctx.cmd.DrawProcedural(Matrix4x4.identity, fbfMaterial, 0,
            MeshTopology.Triangles, 3);
    });
}

셰이더 측에서는 URP의 FRAMEBUFFER_INPUT_HALF_DECLARE 매크로를 사용합니다. tex2D처럼 UV 좌표를 지정하는 것이 아니라 현재 픽셀의 데이터를 그대로 온칩에서 읽어 옵니다.

// FBF 셰이더 핵심 코드
FRAMEBUFFER_INPUT_HALF(0);   // Input Attachment 0 선언

half4 frag(Varyings input) : SV_Target
{
    half4 color = LOAD_FRAMEBUFFER_INPUT(0, input.positionHCS.xy);
    // 픽셀 데이터 조작 (예: 파란 틴트)
    return color * half4(0, 0, 1, 1);
}

4. Safe vs Unsafe 패스와 패스 머지

Render Graph가 “Safe”라고 부르는 패스는 리소스 접근 정보를 Render Graph에 완전히 선언하는 패스입니다. Render Graph는 이 정보를 기반으로 두 가지 최적화를 수행합니다.

패스 머지(Pass Merge): 서로 의존하지 않는 연속 패스들을 단일 GPU 렌더 패스로 묶습니다. Render Graph Viewer에서 파란 연결선으로 표시됩니다. (아래 이미지의 빨간색 네모 참조) 패스 머지로 얻는 이점은 세 가지입니다. 첫째, API 호출 오버헤드 감소 — BeginRenderPass/EndRenderPass 호출 횟수가 줄어듭니다. 둘째, VRAM 대역폭 절감 — 머지된 패스 사이의 중간 렌더 텍스처를 VRAM에 쓰고 다시 읽는 왕복이 생략됩니다(TBDR GPU에서는 온칩 메모리에 그대로 유지). 셋째, GPU 동기화 배리어 감소 — 패스가 분리되면 삽입되던 파이프라인 배리어가 줄어들어 GPU 스톨이 감소합니다.

리소스 재사용: 패스가 끝나면 해당 TextureHandle의 메모리를 즉시 다른 패스에 재할당합니다. 전통적인 방식에서 모든 렌더 텍스처를 게임 내내 유지하던 것과 대조됩니다. 이를 통해 프레임 내 최대 GPU 메모리 점유량이 줄어들고, 메모리 단편화도 억제됩니다.

AddBlitPassAddUnsafePassUnsafe 패스입니다. 이 패스들은 CommandBuffer API(cmd.Blit, cmd.SetRenderTarget 등)를 내부적으로 사용하는데, Render Graph가 이런 로우레벨 명령 안의 리소스 접근을 추적하지 못합니다. 결과적으로 머지가 불가능해집니다.

Render Graph Viewer에서 Unsafe 패스를 보려면 Pass Filter → Unsafe Pass 토글을 활성화해야 합니다. (아래 이미지의 오렌지색 네모 참조)

unsasfe-filter-and-merged-passes]

5. RequireIntermediateTexture — Back Buffer 제한을 넘는 법

URP에서 화면에 최종 출력되는 버퍼를 Back Buffer라고 부릅니다. 커스텀 패스가 Back Buffer를 소스로 읽으면서 동시에 Back Buffer에 쓰려 하면 GPU에서 정의되지 않은 동작이 발생합니다.

이를 막기 위해 URP는 자동으로 중간 텍스처(Intermediate Texture)를 만들어 Back Buffer 대신 사용합니다. 그런데 이 동작은 커스텀 패스에서 명시적으로 요청해야 합니다.

// ScriptableRenderPass 생성자에서
public TintingRenderPass()
{
    // Back Buffer를 소스로 읽는 패스라면 반드시 true
    requiresIntermediateTexture = true;
}

requiresIntermediateTexture = true를 설정하면 URP가 현재 Active Color Texture를 Back Buffer가 아닌 별도 RenderTexture로 만들어 줍니다. 1편의 TintingRenderPass에서 이 설정이 필요했던 것도 Back Buffer를 소스 텍스처로 읽기 때문이었습니다.

언제 false로 둬도 되는가? 패스가 소스를 읽지 않고 쓰기만 하거나, Injection Point가 After Rendering처럼 Back Buffer 이후인 경우입니다. 중간 텍스처 생성에는 메모리 비용이 따르므로, 필요하지 않다면 false가 기본값입니다.


6. Frame Data와 ContextItem — 패스 간 데이터 공유

복수의 패스가 동일한 텍스처를 공유해야 할 때, 과거에는 Global Texture(Shader.SetGlobalTexture)를 흔히 사용했습니다. 하지만 Render Graph 환경에서 Global Texture는 두 가지 문제를 야기합니다. Render Graph가 이 텍스처에 대한 접근을 추적하지 못해 최적화 기회를 잃고, 모든 셰이더가 노출된 텍스처를 의도치 않게 읽을 위험이 생깁니다.

Unity가 권장하는 대안은 Frame Data입니다. ContextContainer(이하 frameData)는 현재 프레임에서만 유효한 딕셔너리로, 패스 간 데이터 공유의 공식 통로입니다.

// 1) ContextItem을 확장한 커스텀 데이터 클래스
public class BlitFrameData : ContextItem
{
    public TextureHandle savedTexture;

    public override void Reset() => savedTexture = TextureHandle.nullHandle;
}

// 2) 첫 번째 패스에서 저장
public override void RecordRenderGraph(RenderGraph renderGraph,
    ContextContainer frameData)
{
    var blitData = frameData.Create();

    using var builder = renderGraph.AddRasterRenderPass(
        "Save Texture", out var passData);

    var colorTexture = frameData.Get().activeColorTexture;
    blitData.savedTexture = colorTexture;
    // ...
}

// 3) 이후 패스에서 읽기
public override void RecordRenderGraph(RenderGraph renderGraph,
    ContextContainer frameData)
{
    var blitData = frameData.Get();
    var savedTex = blitData.savedTexture;
    // savedTex를 소스로 사용하는 패스 구성...
}

ContextItem.Reset()은 프레임 종료 시 자동 호출됩니다. TextureHandle을 null로 초기화하면 Render Graph가 메모리를 올바르게 회수합니다.


7. Output Texture — Shader Graph에서 이름으로 접근

Output Texture 샘플은 렌더 패스가 만든 텍스처를 이름(문자열) 기반으로 Shader Graph 또는 다른 머티리얼에서 접근할 수 있도록 노출하는 패턴을 보여줍니다.

// AddBlitPass에 텍스처 이름을 Property ID로 지정
var blitParams = new BlitMaterialParameters(
    source: sourceTexture,
    destination: activeColorTexture,
    material: blitMaterial,
    passIndex: 0
);
// 셰이더 프로퍼티 "_InputTexture" 이름으로 소스 바인딩
blitParams.sourceTexturePropertyID = Shader.PropertyToID("_InputTexture");
renderGraph.AddBlitPass(blitParams, "Output Texture Pass");

_InputTexture는 예약어가 아닙니다. PropertyToID에 전달한 문자열과 Shader Graph Texture2D 프로퍼티 이름이 정확히 일치해야 렌더 패스가 주입한 텍스처가 연결됩니다. 이름을 _MyBuffer 등으로 바꿔도 양쪽을 동일하게 맞추면 동작합니다. 포스트 이펙트와 월드 오브젝트 머티리얼이 동일한 중간 버퍼를 공유해야 할 때 유용한 패턴입니다.


8. MRT(Multiple Render Targets) — 한 패스에서 여러 버퍼 출력

디퍼드 렌더링에서 G-Buffer는 단일 패스가 Color, Normal, Depth 등 여러 버퍼에 동시에 출력하는 구조입니다. Render Graph에서도 AddRasterRenderPass의 Builder에 복수의 Render Attachment를 등록해 MRT를 구현합니다.

Single RenderPass AddRasterRenderPass Color Buffer Attachment Index 0 Normal Buffer Attachment Index 1 Depth Buffer Attachment Index 2 RGBA 색상 법선 벡터 깊이값 단일 패스 → 3개 버퍼 동시 출력 (Draw Call 1회)
using var builder = renderGraph.AddRasterRenderPass("MRT Pass", out var passData);

// 출력 텍스처 3개를 각각 다른 인덱스에 등록
builder.SetRenderAttachment(colorHandle,  0, AccessFlags.Write);
builder.SetRenderAttachment(normalHandle, 1, AccessFlags.Write);
builder.SetRenderAttachment(depthHandle,  2, AccessFlags.Write);

builder.SetRenderFunc((PassData data, RasterGraphContext ctx) =>
{
    // DrawProcedural 또는 DrawRendererList로 MRT 출력
    ctx.cmd.DrawProcedural(Matrix4x4.identity, mrtMaterial, 0,
        MeshTopology.Triangles, 3);
});

셰이더에서는 SV_Target0, SV_Target1, SV_Target2로 각 출력을 구분합니다. 단일 드로우 콜로 여러 버퍼를 채우므로, G-Buffer를 별도 패스로 채우는 방식보다 드로우 콜 수가 줄어듭니다.


9. Renderer List와 Culling — 레이어로 오브젝트 필터링

AddRendererListPass(또는 Builder의 UseRendererList) 계열 패스는 레이어 마스크로 필터링한 오브젝트 목록을 렌더합니다. 커스텀 아웃라인, 선택 하이라이트, 미니맵처럼 특정 레이어만 그려야 할 때 사용합니다.

// RendererList 초기화 (RecordRenderGraph 안에서 호출)
RendererListHandle InitRendererList(RenderGraph renderGraph, ContextContainer frameData)
{
    var renderingData = frameData.Get();
    var cameraData   = frameData.Get();
    var lightData    = frameData.Get();

    var filterSettings = new FilteringSettings(RenderQueueRange.opaque, layerMask);
    var shaderTagIds   = new ShaderTagId[] { new ShaderTagId("UniversalForwardOnly") };
    var drawSettings   = RenderingUtils.CreateDrawingSettings(
        shaderTagIds, ref renderingData, ref cameraData, ref lightData,
        SortingCriteria.CommonOpaque);

    var rlParams = new RendererListParams(
        renderingData.cullResults, drawSettings, filterSettings);

    return renderGraph.CreateRendererList(rlParams);
}

Culling 샘플은 한 걸음 더 나아가 카메라의 컬링 결과를 직접 조작합니다. CullContextData.Cull(cullingParams) 호출로 카메라 시야각, 쉐도우 레이어 등을 커스터마이징한 뒤 렌더 리스트를 구성합니다. 멀티플레이어에서 팀별로 보이는 오브젝트를 달리해야 하거나, RTS처럼 전장 안개(Fog of War)를 레이어 단위로 제어해야 할 때 이 패턴이 가장 직접적인 해법이 됩니다.


10. 마무리 — 다음 편 예고

패스 선택의 핵심은 결국 하나입니다. Render Graph가 리소스 흐름을 알 수 있는가. AddRasterRenderPass가 기본 선택지이고, 단순 복사는 AddCopyPass, Material Blit은 AddBlitPass, 크기가 다른 Blit만 AddUnsafePass를 쓰면 됩니다. Unsafe 패스가 늘수록 최적화 여지가 좁아지니, 처음 설계할 때부터 Safe 패스 위주로 구성하는 습관이 중요합니다.

패스를 올바르게 골랐다면 다음 질문은 자연스럽게 따라옵니다. “이게 실제로 머지되고 있는가?” 3편에서는 Render Graph Viewer로 이 질문에 직접 답합니다. 색상 코드, 아이콘 의미, Frame Debugger와의 연동까지 — 파란 연결선이 어디서 끊기는지 눈으로 확인하는 방법을 다룹니다.


이전 편: 1편 — Render Graph 기초와 구현 원리
다음 편: 3편 — Render Graph Viewer 완전 정복


함께 읽으면 좋은 포스팅


더 깊이 공부하려면

이 글을 쓰면서 직접 확인한 자료들입니다.

댓글 남기기