book/07-forward-forwardplus-and-lights.md

07. Forward / Forward+ / Lights

Unity 6.3(6000.3) / URP 17.3.0 기준으로 Additional Lights의 루프(Forward vs Forward+) 계약, 시그니처, 매크로, 디버깅 루틴을 고정한다

07. Forward / Forward+ / Lights: URP에서 Additional Lights를 “정확하게” 다루기

이 챕터는 Unity 6.3(6000.3) / URP 17.3.0에서 추가 광원(Additional Lights) 이 셰이더에서 어떻게 처리되는지, 그리고 Forward+ 에서 왜 “같은 코드가 다른 결과”를 내는지(=루프 계약이 바뀜)를 시그니처/정의 위치/호출 흐름 기준으로 고정합니다.

이 챕터의 원칙

  • “개념”만 설명하지 않습니다. 반드시 (1) 시그니처/반환값 (2) 정의 위치 (3) 루프 분기 구조 (4) 검증 방법을 함께 제공합니다.
  • 최종 판단은 로컬 URP 패키지 소스를 기준으로 하세요.

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

이 문서의 “정확성 앵커(anchor)”는 아래 자동 생성물입니다.

추가로 “Lit.shader가 실제로 어떤 Pass를 가지는지”는 여기서 고정합니다.

7.1 Forward와 Forward+의 차이: “라이트 선택(Select)”의 위치

Forward 계열은 기본적으로 “빛을 누적(accumulate)”하지만, 어떤 빛을 대상 픽셀에서 계산할지(selection)는 구현에 따라 달라집니다.

  • Forward(고전 추가 라이트 루프): “픽셀에서 추가 라이트 인덱스 0..N-1를 돈다”
  • Forward+(Clustered Forward): “픽셀의 screen/WS 위치로 클러스터를 조회해, 그 클러스터에 속한 라이트만 돈다”

즉, Forward+는 “루프 대상(light list)”이 픽셀마다 다르게 결정되며, 이 때문에 아래 두 가지가 매우 중요해집니다.

  1. Shader keyword: _CLUSTER_LIGHT_LOOP (컴파일 변형/경로 선택)
  2. 코드 경로: USE_CLUSTER_LIGHT_LOOP / LIGHT_LOOP_BEGIN/END (실제 루프 구현)

7.2 Light 구조체: 셰이더가 소비하는 “조명 계약”

URP의 라이트 함수들은 결국 Light 구조체를 반환(또는 채움)합니다. 이 구조체가 “뭘 갖고 있나”를 모르고 BRDF만 손대면, 디버깅이 매우 힘들어집니다.

URP 17.3.0의 Light 구조체(정확 정의 위치 포함):

Field Type 의미(실무 해석)
direction half3 라이트 방향(관례적으로 “표면에서 라이트로” 또는 “라이트에서 표면으로”가 혼동 포인트 — URP의 라이팅 함수 사용을 기준으로 맞추기)
color half3 라이트 색(강도 포함)
distanceAttenuation float 거리 감쇠(플랫폼 정밀도 이슈로 float 유지)
shadowAttenuation half 그림자 감쇠(0..1)
layerMask uint Light Layers / Rendering Layers 매칭용

팁: Light가 “스팟 각 감쇠” 같은 걸 직접 들고 있지 않은 이유
URP는 내부적으로 라이트 데이터 버퍼/텍스처에서 원천 데이터를 읽고(LightData 등), 최종 shading에 필요한 요약형을 Light로 제공합니다.

7.3 메인 라이트: GetMainLight(...) 시그니처(반환값 포함)

URP 17.3.0 기준(정의 위치/라인은 generated 기준):

API Returns Params Defined-in
GetMainLight Light (none) <URP>/ShaderLibrary/RealtimeLights.hlsl:81
GetMainLight Light float4 shadowCoord <URP>/ShaderLibrary/RealtimeLights.hlsl:102
GetMainLight Light float4 shadowCoord, float3 positionWS, half4 shadowMask <URP>/ShaderLibrary/RealtimeLights.hlsl:109
GetMainLight Light InputData inputData, half4 shadowMask, AmbientOcclusionFactor aoFactor <URP>/ShaderLibrary/RealtimeLights.hlsl:122

정리:

  • “그림자/혼합/AO” 같은 기능이 켜지면 overload가 바뀌는 게 아니라, 더 많은 입력을 주는 overload를 호출하는 식으로 확장됩니다.
  • 커스텀 셰이더는 “현재 패스/기능에서 무엇이 필요한가”를 결정하고, 그에 맞는 overload를 택해야 합니다.

7.4 Additional Lights (Forward): GetAdditionalLightsCount + GetAdditionalLight

Forward(비-클러스터) 경로의 기본 패턴은 다음입니다.

  1. int count = GetAdditionalLightsCount();
  2. for (i=0..count-1) { Light l = GetAdditionalLight(i, positionWS); ... }

URP 17.3.0 기준 시그니처:

API Returns Params Defined-in
GetAdditionalLightsCount int (none) <URP>/ShaderLibrary/RealtimeLights.hlsl:271
GetAdditionalLight Light uint i, float3 positionWS <URP>/ShaderLibrary/RealtimeLights.hlsl:224
GetAdditionalLight Light uint i, float3 positionWS, half4 shadowMask <URP>/ShaderLibrary/RealtimeLights.hlsl:234
GetAdditionalLight Light uint i, InputData inputData, half4 shadowMask, AmbientOcclusionFactor aoFactor <URP>/ShaderLibrary/RealtimeLights.hlsl:257

이 경로의 핵심은:

  • GetAdditionalLightsCount()가 “현재 픽셀에서 적용될 라이트 수”가 아니라, forward path에서의 최대/가시 추가 라이트 수에 가까운 값을 주는 점입니다(플랫폼/설정/파이프라인 제한의 영향을 받음).

7.5 Forward+의 분기 핵심: _CLUSTER_LIGHT_LOOP(keyword) → USE_CLUSTER_LIGHT_LOOP(macro)

URP 17.3.0에서 _CLUSTER_LIGHT_LOOP는 “Forward+용 shader keyword”이고, USE_CLUSTER_LIGHT_LOOP실제 루프 구현을 선택하는 매크로입니다.

정의 위치(Generated xref 기준):

  • _CLUSTER_LIGHT_LOOP 관련: <URP>/ShaderLibrary/Core.hlsl:13<URP>/ShaderLibrary/ForwardPlusKeyword.deprecated.hlsl:20
  • USE_CLUSTER_LIGHT_LOOP 관련: <URP>/ShaderLibrary/Core.hlsl:14 / :16

요지는 아래처럼 요약할 수 있습니다(개념 형태):

HLSL
// <URP>/ShaderLibrary/Core.hlsl
#if defined(_CLUSTER_LIGHT_LOOP)
    #define USE_CLUSTER_LIGHT_LOOP 1
#else
    #define USE_CLUSTER_LIGHT_LOOP 0
#endif

왜 두 개로 나누나?

  • _CLUSTER_LIGHT_LOOP: “variant(컴파일 결과물)”을 나누는 키워드
  • USE_CLUSTER_LIGHT_LOOP: URP 내부 include들에서 “같은 API 이름”으로 루프 구현을 바꿔치기하기 위한 스위치

7.6 왜 Forward+에서 GetAdditionalLightsCount()가 0이 될 수 있나?

이건 “버그”가 아니라 계약입니다.

URP 17.3.0의 GetAdditionalLightsCount() 정의는 다음 의도를 명확히 드러냅니다(요약):

  • USE_CLUSTER_LIGHT_LOOP == 1이면 0을 반환
  • 이유: clustered에서 “개수 세기(counting)”는 bit list traversal이 필요하고, 사전에 필요하지 않기 때문

정의 위치(정확):

  • <URP>/ShaderLibrary/RealtimeLights.hlsl:271

따라서, Forward+에서 다음 코드는 실패(또는 0회 루프)할 수 있습니다.

HLSL
for (uint i = 0; i < GetAdditionalLightsCount(); ++i) { ... }

Forward+에서는 “count 기반 루프”가 아니라, URP가 제공하는 클러스터 루프 매크로를 사용해야 합니다.

7.7 LIGHT_LOOP_BEGIN/END: 같은 호출, 다른 구현(Forward vs Forward+)

URP는 LIGHT_LOOP_BEGIN(lightCount) ... LIGHT_LOOP_END 형태로 “라이트 루프의 본문(body)”만 작성하게 하고, Forward/Forward+에서 루프 구현은 매크로로 바꿉니다.

정의 위치(Generated xref 기준):

  • <URP>/ShaderLibrary/RealtimeLights.hlsl:28 (cluster 구현)
  • <URP>/ShaderLibrary/RealtimeLights.hlsl:36 (for-loop 구현)

Forward+에서는 LIGHT_LOOP_BEGIN(pixelLightCount)에 넘기는 pixelLightCount 값이 0이라도, 매크로가 ClusterInit/ClusterNext를 사용해 실제 라이트 리스트를 순회합니다.

7.8 Forward vs Forward+ 라이트 루프: 시퀀스 다이어그램(핵심만)

sequenceDiagram participant Frag as Fragment participant RL as RealtimeLights.hlsl participant CL as Clustering.hlsl Frag->>RL: Light main = GetMainLight(...) Frag->>RL: int count = GetAdditionalLightsCount() alt Forward (USE_CLUSTER_LIGHT_LOOP=0) Frag->>RL: for lightIndex in [0..count) Frag->>RL: GetAdditionalLight(lightIndex, ...) else Forward+ (USE_CLUSTER_LIGHT_LOOP=1) note over Frag,RL: count == 0 (계약) Frag->>RL: LIGHT_LOOP_BEGIN(count) RL->>CL: ClusterInit(screenUV, positionWS, ...) loop while ClusterNext(...) RL->>RL: GetAdditionalLight(lightIndex, ...) end Frag->>RL: LIGHT_LOOP_END end

7.9 실전 구현 패턴(권장): “URP 루프를 유지하고 BRDF만 교체”

가장 안전한 커스터마이즈는 다음 중 하나입니다.

  1. SurfaceData 생성만 바꾸기(알베도/노말/러프니스 등)
  2. BRDF 함수만 바꾸기(LightingPhysicallyBased 계열)

반대로, 아래를 “처음부터” 직접 구현하면 깨지기 쉽습니다.

  • 추가 라이트 루프(Forward/Forward+ 분기)
  • ShadowMask/AO/LightLayers/Decal/ProbeVolume 등 주변 기능 결합

“내 셰이더에서 추가 라이트를 직접 루프”해야 한다면, 최소한 이 형태를 유지하세요.

HLSL
uint count = GetAdditionalLightsCount();
LIGHT_LOOP_BEGIN(count)
    Light light = GetAdditionalLight(lightIndex, inputData, shadowMask, aoFactor);
    // ... accumulate with your BRDF
LIGHT_LOOP_END

7.10 검증 루틴(수용 기준): “Forward+에서 라이트가 0개”를 재현/해결

증상: Forward+ ON인데 추가 라이트가 적용되지 않음.

  1. 키워드 확인: 셰이더에 _CLUSTER_LIGHT_LOOP variant가 있는가?
    • URP Lit은 #pragma multi_compile _ _CLUSTER_LIGHT_LOOP를 사용합니다.
  2. 루프 구현 확인: GetAdditionalLightsCount 기반 for-loop만 있는가?
    • 그렇다면 Forward+에서 count==0으로 루프가 돌지 않을 수 있습니다.
  3. URP 제공 루프 사용: LIGHT_LOOP_BEGIN/END로 전환
  4. Frame Debugger: Forward 패스에서 “Additional Lights” 단계가 실제로 실행되는지 확인
  5. 정의 위치 추적: book/generated/urp-17.3.0/xref/lit-key-symbols.md에서 _CLUSTER_LIGHT_LOOP, USE_CLUSTER_LIGHT_LOOP, LIGHT_LOOP_BEGIN/END 정의를 확인

7.11 검색 레시피(로컬 소스/Generated를 같이 쓰기)

Generated 파일을 먼저 열면 “정의 위치”가 바로 나오고, 그 다음에 로컬 소스에서 맥락을 읽는 순서가 가장 빠릅니다.

A) 가장 빠른 길: generated 인덱스에서 찾기

B) 소스에서 직접 찾기(rg)

URP 프로젝트 루트에서:

POWERSHELL
rg -n "int GetAdditionalLightsCount\\(" Packages/com.unity.render-pipelines.universal/ShaderLibrary/RealtimeLights.hlsl
rg -n "LIGHT_LOOP_BEGIN" Packages/com.unity.render-pipelines.universal/ShaderLibrary/RealtimeLights.hlsl
rg -n "USE_CLUSTER_LIGHT_LOOP" Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl

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