book/09-writing-urp-compatible-shaders.md

09. URP 기준 완전 호환 셰이더 작성 가이드

Unity 6.3(6000.3) / URP 17.3.0 기준으로 Pass 태그/키워드/CBUFFER(SRP Batcher)/Forward+·Deferred·MotionVectors 계약까지 맞추는 ShaderLab/HLSL 체크리스트

09. URP 기준 “완전 호환” 셰이더 작성 가이드 (HLSL)

이 챕터의 목표는 URP의 기존 셰이더/파이프라인과 호환되는 커스텀 HLSL 셰이더를 작성하는 체크리스트를 제공하는 것입니다.

호환성은 크게 4가지로 나뉩니다.

  1. 키워드/Variant 호환: URP가 기대하는 #pragma multi_compile/shader_feature
  2. 패스 태그/라이트모드: URP가 패스를 찾는 규칙
  3. 상수버퍼 레이아웃(SRP Batcher): CBUFFER 구조/정렬 규칙
  4. Forward+/Deferred 등 경로 호환: 라이트 루프 분기

이 4가지를 동시에 만족시키면, “URP 내장 Lit 계열과 같은 환경에서” 커스텀 셰이더가 안정적으로 동작합니다.

9.0 정확 레퍼런스(Generated, URP 17.3.0)

이 챕터는 “개념 체크리스트”로 끝나지 않도록, 로컬 URP/Core 소스에서 자동 추출한 레퍼런스와 함께 읽는 것을 전제로 합니다.

특히 Pass 계약(어떤 LightMode가 언제 소비되나)은 별도 챕터로 고정합니다.

선행: 08. URP HLSL 라이브러리 지도, 07. Forward/Forward+/Lights

9.1 Pass 태그: URP가 패스를 인식하는 방식

URP는 “어떤 패스가 어떤 용도인지”를 LightMode 등의 태그로 구분합니다.

커스텀 셰이더가 특정 렌더링 단계(예: DepthOnly, ShadowCaster)에 참여하려면, URP가 사용하는 LightMode 태그와 패스 구성을 맞춰야 합니다.

실무 팁: 가장 안전한 방법은 URP의 기존 셰이더(Lit/SimpleLit/Unlit)의 Pass 섹션을 복제(구조만) 하고, 내부 HLSL만 교체하는 방식으로 시작하는 것입니다.

9.1.1 “패스”는 두 종류가 있다: (A) 셰이더 패스 vs (B) 렌더 패스

혼동 포인트를 먼저 분리합니다.

  • (A) ShaderLab Pass: 한 셰이더 안에 정의된 Pass 블록(ForwardLit, ShadowCaster 등)
  • (B) URP Render Pass(ScriptableRenderPass): URP 파이프라인에 삽입되는 렌더 단계

URP의 Render Pass는 특정 시점에 특정 큐를 그릴 때, 셰이더의 특정 ShaderLab Pass(=LightMode)를 선택해 사용합니다.

즉, “호환”이란:

  • URP가 기대하는 LightMode Pass가 존재하고
  • 그 Pass가 URP가 기대하는 입력/키워드/버퍼 레이아웃을 만족

하는 상태를 의미합니다.

9.1.2 LightMode 태그는 “계약”이다(URP 17.3.0 Lit 기준)

URP는 Render Pass(파이프라인 단계)에서 LightMode를 기준으로 ShaderLab Pass를 선택합니다.

따라서 “완전 호환”이라는 말은:

  • 프로젝트가 소비하는 LightMode Pass를 제공하고,
  • 그 Pass가 요구하는 출력/키워드/입력 구조를 만족한다

는 뜻입니다.

URP 17.3.0의 Lit.shader가 실제로 포함하는 LightMode 9종(Generated 기준):

LightMode 권장 분류 주 소비 단계(요약) 비고
UniversalForward 필수급 Forward 컬러 Forward+ 분기 포함
ShadowCaster 필수급 그림자 맵 알파 클립/바이어스
DepthOnly 보통 필수 DepthTexture/Depth prepass ColorMask 설정 주의
DepthNormals 기능 의존 DepthNormalsTexture SSAO/Outline/SSR류 기반
Meta 기능 의존 라이트맵 베이킹 베이킹을 쓰면 사실상 필수
UniversalGBuffer 기능 의존 Deferred(GBuffer) Deferred 프로젝트면 필수급
MotionVectors 기능 의존 velocity TAA/모션블러/리프로젝션
XRMotionVectors XR 의존 XR velocity 스텐실 계약 포함
Universal2D 2D 의존 2D Renderer 2D 파이프라인 계약

정확 include/엔트리/정의 위치:

계약(언제/왜 소비되나) 상세:

9.2 URP 셰이더의 기본 골격(ShaderLab)

URP 호환 셰이더를 만들 때 “Pass 계약까지 포함한” 기본 골격은 다음 형태를 기준으로 잡는 것이 안전합니다.

ShaderLab
Shader "Custom/URP/ExampleLit"
{
    Properties
    {
        _BaseMap ("Base Map", 2D) = "white" {}
        _BaseColor ("Base Color", Color) = (1,1,1,1)
    }

    SubShader
    {
        Tags
        {
            "RenderPipeline"="UniversalPipeline"
            "RenderType"="Opaque"
            "Queue"="Geometry"
        }

        // Pass: Forward(컬러)
        Pass
        {
            Name "ForwardLit"
            Tags { "LightMode"="UniversalForward" }

            HLSLPROGRAM
            #pragma vertex Vert
            #pragma fragment Frag

            // (A) URP 키워드/variant
            // (B) include/CBUFFER
            // (C) 엔트리 함수
            ENDHLSL
        }

        // Pass: ShadowCaster(그림자)
        Pass
        {
            Name "ShadowCaster"
            Tags { "LightMode"="ShadowCaster" }
            HLSLPROGRAM
            #pragma vertex VertShadow
            #pragma fragment FragShadow
            ENDHLSL
        }

        // Pass: DepthOnly(깊이)
        Pass { Name "DepthOnly" Tags { "LightMode"="DepthOnly" } }

        // Pass: DepthNormals(깊이+노말)
        Pass { Name "DepthNormals" Tags { "LightMode"="DepthNormals" } }

        // Pass: Meta(베이킹)
        Pass { Name "Meta" Tags { "LightMode"="Meta" } }

        // (옵션) Deferred를 목표로 한다면:
        Pass { Name "GBuffer" Tags { "LightMode"="UniversalGBuffer" } }

        // (옵션) TAA/모션블러/리프로젝션을 목표로 한다면:
        Pass { Name "MotionVectors" Tags { "LightMode"="MotionVectors" } }

        // (옵션) XR에서 모션 벡터를 쓰면:
        Pass { Name "XRMotionVectors" Tags { "LightMode"="XRMotionVectors" } }

        // (옵션) 2D Renderer 호환이 필요하면:
        Pass { Name "Universal2D" Tags { "LightMode"="Universal2D" } }
    }
}

위 코드는 “Pass 계약 목록”을 보여주기 위한 골격입니다.
실제 구현은 URP Lit Pass 구성을 출발점으로 하는 것이 가장 안전합니다:

9.3 URP include 구성(실전 최소 세트)

URP 셰이더는 보통 다음 include를 기반으로 시작합니다.

HLSL
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

URP 17.3.0에서 Core.hlsl은 사실상 “루트 include”입니다.

  • Forward+ 분기 스위치: _CLUSTER_LIGHT_LOOPUSE_CLUSTER_LIGHT_LOOP
  • SRP Core(공통) include를 끌어옴(Common.hlsl, Packing.hlsl 등)
  • URP Input.hlsl을 include(전역 상수/구조체/유틸 기반)

관련(정확 정의 위치는 generated 기준):

  • USE_CLUSTER_LIGHT_LOOP: <URP>/ShaderLibrary/Core.hlsl
  • _CLUSTER_LIGHT_LOOP: <URP>/ShaderLibrary/ForwardPlusKeyword.deprecated.hlsl

여기에 목적에 따라:

  • 라이팅이 필요하면 Lighting/RealtimesLights 계열 include
  • 뎁스 샘플링이 필요하면 DeclareDepthTexture 계열 include
  • 표면 텍스처 샘플링을 URP 스타일로 하고 싶으면 SurfaceInput 계열 include

를 추가합니다. 구체적인 “어떤 파일을 include해야 하는가”는 URP 버전별로 달라질 수 있으니, URP Lit가 include하는 목록을 추적하는 방식을 권장합니다. (관련: 08. URP HLSL 라이브러리 지도)

참고: #include_with_pragmas가 필요한 경우
URP는 일부 파일(DOTS, ObjectMotionVectors 등)을 #include_with_pragmas로 포함합니다.
해당 파일이 #pragma를 내부에 포함하는 경우(예: MotionVectors pass), 일반 #include만으로는 “필요한 pragma가 누락”될 수 있습니다.
자세한 Pass 계약/예시는 book/20-urp-pass-tags-and-lightmode-contract.md 참고.

9.4 키워드/Variants: “맞춰야 하는 것”과 “줄여야 하는 것”

URP 호환을 위해 키워드를 맞추다 보면 variant가 폭발하기 쉽습니다.

  • 장점: URP 기능(추가 라이트, 그림자, 소프트 쉐도우, 라이트맵, 포그…)과 자연스럽게 결합
  • 단점: 빌드 시간/메모리/로딩 시간이 늘어남

9.4.1 라이팅/그림자 관련 키워드(대표)

Forward(+)/그림자를 포함하는 셰이더에서 흔히 등장:

  • Additional Lights: _ADDITIONAL_LIGHTS, _ADDITIONAL_LIGHTS_VERTEX
  • Additional Light Shadows: _ADDITIONAL_LIGHT_SHADOWS
  • Forward+ loop: _CLUSTER_LIGHT_LOOP
  • Soft Shadows: _SHADOWS_SOFT(또는 유사 키워드)
  • Main Light Shadows/캐스케이드: URP 버전/셰이더에 따라 키워드 구성이 상이

권장: 첫 구현은 “정말 필요한 것만” 켜고, 기능을 추가할 때마다 키워드를 늘리며 검증하세요.

9.5 SRP Batcher 호환: CBUFFER 규칙(매우 중요)

SRP Batcher는 머티리얼 상수 업로드 비용을 줄이기 위해 머티리얼 상수 버퍼 레이아웃을 전제로 최적화합니다.

따라서, 커스텀 셰이더에서 SRP Batcher를 살리려면:

  • 머티리얼 프로퍼티를 CBUFFER_START(UnityPerMaterial) ... CBUFFER_END에 넣고
  • 레이아웃(타입/순서)이 조건부 컴파일로 바뀌지 않게 하고
  • 불필요하게 전역(비-CBUFFER) 변수로 머티리얼 값을 두지 않는 것

이 핵심입니다.

HLSL
CBUFFER_START(UnityPerMaterial)
float4 _BaseColor;
float4 _BaseMap_ST;
CBUFFER_END

9.5.1 자주 SRP Batcher를 깨는 패턴

  • #if로 프로퍼티를 빼거나 타입을 바꿔 CBUFFER 레이아웃이 달라짐
  • pass마다 CBUFFER 내용/순서가 달라짐
  • 머티리얼 값인데 전역 변수/상수버퍼 밖에 흩어져 있음

관련 공식 문서:

9.6 Forward+ 호환: 라이트 루프 분기

Forward+를 지원하려면 07. Forward/Forward+/Lights에서 설명한 키워드와 분기 패턴이 필요합니다.

Forward+는 셰이더 측에서 “추가 라이트를 기존 방식으로 단순 루프”하면 안 되는 경우가 있으므로, URP 제공 매크로/함수를 사용해 클러스터 루프 경로를 반드시 점검하세요.

핵심 체크:

  • _CLUSTER_LIGHT_LOOP variant가 존재하는가?
  • Forward+에서 GetAdditionalLightsCount()가 0을 반환할 수 있음을 알고 있는가?
    • 해결: LIGHT_LOOP_BEGIN/END 패턴 사용(URP 내부에서 Forward/Forward+ 루프를 매크로로 통일)

정확 정의/참조 위치는 generated xref로 고정:

관련 공식 문서:

9.7 “완전 호환” 체크리스트(요약)

A) 파이프라인/패스

  • Tags { "RenderPipeline"="UniversalPipeline" }가 있는가
  • 필요한 LightMode Pass(Forward/ShadowCaster/DepthOnly/DepthNormals/Meta)가 존재하는가

B) 키워드/Variant

  • 프로젝트에서 사용하는 URP 기능(Additional Lights, Shadows, Soft Shadows, Fog…)에 필요한 키워드가 포함되어 있는가
  • Forward+ 사용 시 _CLUSTER_LIGHT_LOOP가 포함되어 있는가

C) 상수버퍼(SRP Batcher)

  • 머티리얼 프로퍼티가 UnityPerMaterial CBUFFER에 모여 있는가
  • pass 간 CBUFFER 레이아웃이 일관적인가

D) 리소스 입력(Depth/Normals 등)

  • Depth/Normal/History가 필요한 경우, 파이프라인에서 실제로 생성되도록 요구/설정했는가
  • 셰이더에서 올바른 include/샘플링/선형화를 했는가

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