book/03-urp-architecture.md

03. URP 아키텍처: Asset / Renderer / Feature / Pass

URP 확장 포인트(Feature/Pass), 생명주기(Create/AddRenderPasses/Setup/Dispose), Injection Point 선택 기준을 정리한다

03. URP 아키텍처: Asset / Renderer / Feature / Pass

이 챕터는 URP에서 “어디에 코드를 넣어야 확장되는가?”를 구조적으로 정리합니다. Render Feature/Pass를 작성할 때 삽입 지점(Injection Point) 을 결정하는 감각이 목표입니다.

3.1 URP의 구성요소

  • UniversalRenderPipelineAsset (URP 전역 설정)
  • ScriptableRenderer (카메라 렌더링을 실제로 구성/실행)
  • ScriptableRendererFeature (사용자가 렌더러에 기능을 “장착”하는 확장 포인트)
  • ScriptableRenderPass (한 번의 렌더 패스 단위)

일반적으로 개발자는 RendererFeature를 만들어 렌더러에 붙이고, 그 안에서 패스를 생성해 렌더링 파이프라인에 삽입합니다.

3.1.1 (중요) Feature의 생명주기: Create / AddRenderPasses / SetupRenderPasses / Dispose

URP는 ScriptableRendererFeature에 대해 대략 다음 순서로 호출합니다.

  1. Create()
    • Feature가 로드되거나, 활성/비활성 토글되거나, 인스펙터 값이 바뀌는 등 “구성이 바뀌는 시점”에 호출
    • 리소스(머티리얼/패스 인스턴스/버퍼 등) 생성은 여기서 하는 것이 원칙
  2. AddRenderPasses(renderer, ref RenderingData)
    • 매 프레임, 카메라마다 1회 호출
    • renderer.EnqueuePass(pass)로 패스를 파이프라인에 “등록”
    • 주의: 이 함수는 호출 빈도가 매우 높으므로 할당/생성 작업을 하면 안 됩니다.
  3. SetupRenderPasses(renderer, in RenderingData)
    • 카메라 타겟(컬러/뎁스)을 포함해, 패스 실행 전에 필요한 값을 “세팅”하는 단계
    • Unity 문서에서도 cameraColorTarget 같은 카메라 타겟 접근이 필요할 경우 AddRenderPasses가 아니라 SetupRenderPasses에서 하도록 가이드합니다.
  4. Dispose(bool)
    • Feature가 소멸될 때 리소스 해제

실전 규칙

  • Create: “생성/초기화(한 번)”
  • AddRenderPasses: “어떤 카메라에 적용할지 판단 + Enqueue”
  • SetupRenderPasses: “카메라 타겟/프레임별 파라미터 세팅”
  • Dispose: “정리”

관련 공식 문서:

3.2 Pass는 어디에 끼우나? (RenderPassEvent)

URP는 패스가 실행될 타이밍을 이벤트 열거형으로 표현합니다(예: Opaque 전/후, Transparent 전/후, 포스트 전/후 등).

실무에서 가장 흔한 타이밍:

  • “Opaque 이후, Transparent 이전”: 화면 기반 이펙트/깊이 기반 효과
  • “포스트 직전/직후”: 컬러 그레이딩/블러/아웃라인 등

RenderGraph 경로에서는 이벤트만으로는 부족할 수 있으며, 실제 의존성은 “어떤 리소스를 읽고/쓰는지”가 결정합니다. (RenderGraph는 04. RenderGraph)

3.2.1 Injection Point를 고르는 실전 기준

Renderer Feature를 설계할 때, Injection Point(=패스 실행 타이밍)는 다음 질문으로 결정하면 실수가 줄어듭니다.

  1. 입력 텍스처는 무엇인가?
    • Opaque만 반영된 컬러? Transparent까지 포함된 컬러?
    • Depth/Normals가 필요한가?
  2. 출력은 어디로 갈 것인가?
    • 카메라 컬러에 덮어쓰기?
    • 임시 RT에 쓰고 이후에 합성?
  3. 후속 패스가 무엇을 기대하는가?
    • 포스트프로세싱 전에 적용해야 하는가? 이후에 적용해야 하는가?
  4. XR/동적 해상도/다중 카메라에서 안전한가?
    • 카메라마다 서로 다른 타겟/스케일을 쓰는지

가장 흔한 실수는 “이벤트만 보고” 타이밍을 잡는 것입니다.
RenderGraph에서는 이벤트보다 리소스 의존성(읽기/쓰기 선언) 이 실제 순서를 더 강하게 결정합니다.

3.2.2 Unity 6 프레임 라이프사이클로 주입 시점 다시 보기

RenderPassEvent는 "언제 실행할지"를 정하는 힌트이고, 실제로는 프레임 단계와 입력 리소스 준비 상태를 같이 봐야 안전합니다.

프레임 단계 일반 목적 추천 이벤트(예시) 흔한 실수
Setup/Culling 이후 프레임 입력 준비 확인 BeforeRenderingPrePasses 계열 Depth/Normals가 없는 카메라에서 강행
Depth Prepass 이후 깊이 기반 효과 AfterRenderingPrePasses Raw depth 선형화 누락
Opaque 이후 실루엣/마스크/왜곡 준비 AfterRenderingOpaques 이후 소비 패스 없이 출력만 생성
Transparent 이후 화면 왜곡/합성 AfterRenderingTransparents 투명 정렬/소팅 가정 누락
Post 전/후 최종 톤/화면 덮기 Before/AfterRenderingPostProcessing UI/Overlay 카메라 영향 범위 미정의

3.3 Compatibility Mode vs RenderGraph

URP 커스텀 패스 작성에는 두 길이 있습니다.

  1. Compatibility Mode: Execute()에서 CommandBuffer로 직접 그리기(레거시 접근)
  2. RenderGraph 경로: RecordRenderGraph()에서 패스와 리소스 의존성을 “선언”하기(현행/권장)

URP 문서에서도 RenderGraph 기반 패스 작성이 중심이며, 비-RenderGraph 경로는 더 이상 적극 개발되지 않는다는 점을 강조합니다.

3.4 최소 동작 예제(스켈레톤)

아래는 구조만 보여주는 스켈레톤입니다. 실제 RenderGraph 패스 예제는 04. RenderGraph에서 다룹니다.

C#
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public sealed class ExampleFeature : ScriptableRendererFeature
{
    sealed class ExamplePass : ScriptableRenderPass
    {
        // RenderGraph 경로(권장)
        public override void RecordRenderGraph(UnityEngine.Rendering.RenderGraphModule.RenderGraph renderGraph,
            ContextContainer frameData)
        {
            // TODO: renderGraph.AddRasterRenderPass(...) 등으로 패스 선언
        }

        // Compatibility Mode 경로(레거시)
        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
            // TODO: CommandBufferPool.Get/Release, Blit, DrawRenderer 등
        }
    }

    ExamplePass _pass;

    public override void Create()
    {
        _pass = new ExamplePass();
    }

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

3.4.1 카메라 타입별 적용(실무 필수)

URP는 씬뷰/게임뷰/프리뷰/리플렉션 등 여러 카메라 타입을 렌더링합니다. 실무에서는 대개 “게임 카메라에만” 적용하거나, “씬뷰는 디버그용으로만” 적용합니다.

C#
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
    if (renderingData.cameraData.cameraType != CameraType.Game)
        return;

    renderer.EnqueuePass(_pass);
}

3.5 Pass 구조를 ‘텍스처 요구사항’으로 설계하기

URP 패스는 “이 패스가 어떤 텍스처/입력이 필요하다”를 선언해야 합니다.

  • Depth가 필요하면 DepthTexture 생성이 필요할 수 있음
  • Normal이 필요하면 DepthNormals 패스가 선행되어야 할 수 있음
  • Motion Vector/History가 필요하면 별도 요구가 생김

RenderGraph 경로에서는 이런 요구가 UniversalResourceData/Frame Data 형태로 구성되며, Full Screen Pass 같은 Feature에서는 “Requirements”로 이를 지정합니다.

관련 문서:

3.6 RenderGraph 시대 Renderer Feature 설계 체크리스트

3.6.1 리소스 준비/수명

  • Depth/Normals/MotionVectors가 모든 카메라에서 항상 준비된다고 가정하지 말 것
  • 텍스처 포맷(sRGB/Linear), Render Scale, DRS를 입력 계약에 포함할 것
  • 외부 RT를 가져오면(ImportTexture) 생명주체(그래프/외부) 경계를 문서화할 것

3.6.2 Pass Culling 생존성

RenderGraph는 "출력이 최종 결과로 이어지지 않으면" 패스를 제거할 수 있습니다.

  • 출력 핸들이 후속 패스에서 실제로 읽히는지 확인
  • 디버그 패스라도 부작용(전역 바인딩 등)에만 의존하지 않도록 설계
  • 마스크 생성 패스는 바로 다음 합성 패스와 묶어 검증

3.6.3 카메라 스택/멀티 카메라 안정성

  • 전역 텍스처 키(SetGlobalTexture)는 멀티 카메라 충돌 위험이 큼
  • Base/Overlay 카메라 경로를 분리해 적용 조건을 명시
  • 입력이 없는 카메라(예: Depth 미생성)에서는 early-out으로 안전 종료

3.7 샘플 기반 참고 노트

아래 샘플 문서의 개념을 이 장의 설계 기준에 반영했습니다.

  • samples/Unity6_Rendering_Bible/01_Architecture_and_Pipeline/02_URP_Lifecycle.md
  • samples/Unity6_Rendering_Bible/01_Architecture_and_Pipeline/04_URP_Renderer_Features_Design.md
  • samples/Unity6_Rendering_Bible/04_Render_Graph_System/03_RecordRenderGraph_Migration_Checklist.md

추가 읽을거리(공식/권위 자료)