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 핵심 심볼 xref(정의/대표 참조):
book/generated/urp-17.3.0/xref/lit-key-symbols.md - 함수 시그니처 인덱스(반환형/파라미터/정의 파일/라인):
book/generated/urp-17.3.0/symbols/functions.md - 구조체 인덱스(필드 목록/정의 파일/라인):
book/generated/urp-17.3.0/symbols/structs.md
추가로 “Lit.shader가 실제로 어떤 Pass를 가지는지”는 여기서 고정합니다.
- Lit Pass/Include 맵:
book/generated/urp-lit-map.md
7.1 Forward와 Forward+의 차이: “라이트 선택(Select)”의 위치
Forward 계열은 기본적으로 “빛을 누적(accumulate)”하지만, 어떤 빛을 대상 픽셀에서 계산할지(selection)는 구현에 따라 달라집니다.
- Forward(고전 추가 라이트 루프): “픽셀에서 추가 라이트 인덱스 0..N-1를 돈다”
- Forward+(Clustered Forward): “픽셀의 screen/WS 위치로 클러스터를 조회해, 그 클러스터에 속한 라이트만 돈다”
즉, Forward+는 “루프 대상(light list)”이 픽셀마다 다르게 결정되며, 이 때문에 아래 두 가지가 매우 중요해집니다.
- Shader keyword:
_CLUSTER_LIGHT_LOOP(컴파일 변형/경로 선택) - 코드 경로:
USE_CLUSTER_LIGHT_LOOP/LIGHT_LOOP_BEGIN/END(실제 루프 구현)
7.2 Light 구조체: 셰이더가 소비하는 “조명 계약”
URP의 라이트 함수들은 결국 Light 구조체를 반환(또는 채움)합니다. 이 구조체가 “뭘 갖고 있나”를 모르고 BRDF만 손대면, 디버깅이 매우 힘들어집니다.
URP 17.3.0의 Light 구조체(정확 정의 위치 포함):
- 정의:
<URP>/ShaderLibrary/RealtimeLights.hlsl:12(book/generated/urp-17.3.0/symbols/structs.md참고)
| 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(비-클러스터) 경로의 기본 패턴은 다음입니다.
int count = GetAdditionalLightsCount();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:20USE_CLUSTER_LIGHT_LOOP관련:<URP>/ShaderLibrary/Core.hlsl:14/:16
요지는 아래처럼 요약할 수 있습니다(개념 형태):
// <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회 루프)할 수 있습니다.
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+ 라이트 루프: 시퀀스 다이어그램(핵심만)
7.9 실전 구현 패턴(권장): “URP 루프를 유지하고 BRDF만 교체”
가장 안전한 커스터마이즈는 다음 중 하나입니다.
- SurfaceData 생성만 바꾸기(알베도/노말/러프니스 등)
- BRDF 함수만 바꾸기(LightingPhysicallyBased 계열)
반대로, 아래를 “처음부터” 직접 구현하면 깨지기 쉽습니다.
- 추가 라이트 루프(Forward/Forward+ 분기)
- ShadowMask/AO/LightLayers/Decal/ProbeVolume 등 주변 기능 결합
“내 셰이더에서 추가 라이트를 직접 루프”해야 한다면, 최소한 이 형태를 유지하세요.
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인데 추가 라이트가 적용되지 않음.
- 키워드 확인: 셰이더에
_CLUSTER_LIGHT_LOOPvariant가 있는가?- URP Lit은
#pragma multi_compile _ _CLUSTER_LIGHT_LOOP를 사용합니다.
- URP Lit은
- 루프 구현 확인:
GetAdditionalLightsCount기반 for-loop만 있는가?- 그렇다면 Forward+에서 count==0으로 루프가 돌지 않을 수 있습니다.
- URP 제공 루프 사용:
LIGHT_LOOP_BEGIN/END로 전환 - Frame Debugger: Forward 패스에서 “Additional Lights” 단계가 실제로 실행되는지 확인
- 정의 위치 추적:
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 인덱스에서 찾기
- 함수:
book/generated/urp-17.3.0/symbols/functions.md에서 함수명 검색 - 구조체:
book/generated/urp-17.3.0/symbols/structs.md에서 구조체명 검색 - 키 심볼 xref:
book/generated/urp-17.3.0/xref/lit-key-symbols.md
B) 소스에서 직접 찾기(rg)
URP 프로젝트 루트에서:
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
추가 읽을거리(공식/권위 자료)
- Forward+ 추가 라이트(개념/설정): https://docs.unity3d.com/6000.3/Documentation/Manual/urp/rendering/additional-lights-fplus.html