book/05-texture-resource-and-ids.md

05. 텍스처 자원과 ID 구조

RenderTexture/RTHandle/TextureHandle 및 Shader Property ID의 차이와 RenderGraph/Compatibility Mode에서의 주고받는 패턴을 정리한다

05. 텍스처 자원과 “ID” 구조: RenderTexture / RTHandle / TextureHandle

이 챕터는 “렌더 텍스처를 주고받는 방식”과 흔히 말하는 “레퍼런스 ID”가 정확히 무엇인지 정리합니다.

선행: 04. RenderGraph

5.1 용어 정리

  • RenderTexture: Unity 엔진 레벨의 텍스처 리소스(렌더 타겟 가능)
  • RTHandle: 동적 해상도/스케일을 고려한 렌더 타겟 핸들(URP/HDRP 공용)
  • TextureHandle: RenderGraph가 리소스를 추적하기 위한 핸들(그래프 내부 표현)
  • “Shader Property ID”: Shader.PropertyToID("_MyTex") 같은 전역 프로퍼티 슬롯 식별자

“ID”가 섞여서 혼란이 생기는 이유

  1. 전역 슬롯 ID(정수): 셰이더 전역 텍스처/상수 설정 시 키로 사용
  2. 리소스 핸들(RTHandle/TextureHandle): 실제 GPU 리소스(또는 그 래핑)
  3. RenderTexture instance: C# 객체 레퍼런스

실무에서는 이 3개를 상황에 맞게 연결해야 합니다.

5.2 전역 텍스처 슬롯: Shader.PropertyToID

가장 오래된(그리고 여전히 흔한) 패턴은 다음입니다.

  • _MyColorTex 같은 이름에 대해 int id = Shader.PropertyToID("_MyColorTex")
  • cmd.SetGlobalTexture(id, rt) 혹은 cmd.SetGlobalTexture("_MyColorTex", rt)

여기서 id는 “이름을 정수로 해시한 슬롯 키” 일 뿐이며, 텍스처 자체가 아닙니다.

5.2.1 “ID 슬롯”을 쓰는 이유와 함정

  • 이유: 문자열 비교 비용을 줄이고, 프로퍼티 접근을 빠르게 함
  • 함정: “ID가 곧 리소스”라고 착각하면 설계가 꼬입니다.

ID는 단지 (이름 → 슬롯) 변환 결과입니다.

  • 리소스는 RenderTexture/RTHandle/TextureHandle
  • 슬롯은 int 또는 string

이고, 둘 사이를 cmd.SetGlobalTexture 같은 호출이 연결합니다.

5.3 RenderTexture를 주고받는 기본 패턴(Compatibility Mode)

가장 단순한 방식은:

  1. 임시 렌더 타겟을 만든다(GetTemporaryRT 또는 RTHandle 할당)
  2. 어떤 패스에서 렌더한다(Blit/Draw)
  3. 전역 슬롯에 바인딩한다(SetGlobalTexture)
  4. 다른 패스/셰이더에서 샘플링한다
C#
static readonly int MyTexId = Shader.PropertyToID("_MyTex");

var cmd = CommandBufferPool.Get("Example");
try
{
    cmd.GetTemporaryRT(MyTexId, width, height, 0, FilterMode.Bilinear, RenderTextureFormat.ARGBHalf);
    cmd.SetRenderTarget(MyTexId);
    // ... draw ...

    cmd.SetGlobalTexture(MyTexId, MyTexId);
    context.ExecuteCommandBuffer(cmd);
}
finally
{
    cmd.ReleaseTemporaryRT(MyTexId);
    CommandBufferPool.Release(cmd);
}

위 예제는 “개념” 설명용입니다. 실무에서는 RTHandle/RenderGraph로 넘어가는 것이 권장됩니다.

5.3 RTHandle: 왜 필요한가?

동적 해상도(Scale, XR, 카메라별 다른 크기)를 다루다 보면 RenderTexture를 직접 들고 다니는 방식이 누수/재할당 문제를 만들기 쉽습니다.

RTHandle은:

  • 스케일/동적 해상도 대응
  • 프레임 간 재사용과 수명 관리
  • URP/HDRP 공용 유틸과 함께 사용

을 위해 도입된 “핸들 시스템”입니다.

5.3.1 RTHandle과 해상도 스케일(동적 해상도)

Forward+/포스트/업스케일링을 하다 보면 카메라 타겟이:

  • 화면 크기와 다를 수 있고
  • 렌더 스케일(Render Scale)을 적용받고
  • XR에서 눈별로 다를 수 있습니다.

이때 RenderTexture를 “정확한 픽셀 크기”로 직접 생성/관리하면 매우 쉽게 오류가 납니다. RTHandle은 이런 경우를 위해 “레퍼런스(핸들) + 실제 텍스처(백킹)”를 분리해 관리하는 방식입니다.

5.4 TextureHandle: RenderGraph 내부 “리소스 레퍼런스”

RenderGraph는 내부적으로 텍스처 리소스를 TextureHandle로 참조합니다.

  • 그래프가 생성한 텍스처는 CreateTexture()로 얻은 handle로 참조
  • 외부 RTHandle/Texture는 ImportTexture()로 handle로 가져와 참조

핵심은 “리소스 의존성(읽기/쓰기)”를 handle 단위로 선언한다는 점입니다.

5.5 RenderGraph에서 텍스처를 주고받는 기본 패턴(권장)

RenderGraph에서는 보통 다음 중 하나로 흐릅니다.

  1. URP가 제공하는 카메라 텍스처를 읽는다 (UniversalResourceData.activeColorTexture 등)
  2. 그래프 내부에서 임시 텍스처를 만든다 (CreateTexture)
  3. 패스에서 읽고/쓰기를 선언한다 (UseTexture/SetRenderAttachment)
  4. 후속 패스가 참조할 리소스를 갱신한다 (예: activeColorTexture를 temp로 교체)

중요한 차이:

  • Compatibility Mode는 “전역 슬롯”이 흐름의 중심이 되기 쉽고
  • RenderGraph는 “핸들(의존성)”이 흐름의 중심이 됩니다.

5.5 주고받는 방식 패턴 3종(정리)

  1. (레거시) CommandBuffer + GlobalTexture 슬롯

    • 장점: 간단, 많은 예제가 있음
    • 단점: 리소스 수명/의존성이 코드에 흩어짐(디버깅/최적화 어려움)
  2. RTHandle 기반(URP 내부 스타일)

    • 렌더러/패스가 RTHandle을 중심으로 리소스를 유지
  3. RenderGraph(TextureHandle) 기반(현행/권장)

    • 패스가 handle을 통해 읽기/쓰기 선언
    • 수명/재사용은 그래프가 도와줌

5.6 “카메라 컬러/뎁스 텍스처는 어디서 오나?”를 답하는 방법

RenderGraph 경로에서 가장 먼저 할 일:

  • Frame Data 문서를 읽고(개념),
  • RenderGraph Viewer로 실제 그래프를 보고(현상),
  • URP 설정(Requirements/Depth Texture/Normal Texture) 을 바꿔보는 것(원인).

관련 공식 문서:

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