출처: PDF 문서 (course_notes_moving_frostbite_to_pbr_v32.pdf)
4.4 점광원(Punctual lights)
Frostbite는 점광원(point light)과 스포트라이트(spot light) 두 가지 유형의 점광원만 지원합니다. 물리적으로 정확한 점광원이 되려면, 소위 역제곱 법칙(inverse square law) [Wikf]을 따라야 합니다. 이는 그림 22에서 확인할 수 있습니다. 일정한 광도(luminosity)를 가진 광원에서 관찰되는 빛의 강도는 물체로부터의 거리 제곱에 반비례하여 감소합니다. 그림 22에서 화살표는 광원에서 방출되는 광속(flux)을 나타냅니다. 주황색 사각형으로 표시된 단위 면적당 화살표의 밀도, 즉 광속 밀도는 거리의 제곱에 따라 감소하는 것을 볼 수 있습니다.

이 역제곱 법칙은 오직 점광원에만 유효하며, 다음 유형의 광원에는 적용되지 않습니다.
1. 빛 빔이 고도로 평행하게 정렬된(collimated) 좁은 분포를 가진 투광등(floodlights)이나 서치라이트(searchlights).
2. 영역광(area lights) 또는 프레넬 렌즈(Fresnel lens)와 같은 특정 광원.
광속(Luminous power): 역제곱 법칙을 방정식으로 표현하면 다음과 같습니다.
E = I / distance^2
여기서 E는 조도(illuminance), I는 광도(luminous intensity), distance는 광원으로부터의 거리입니다. 이 공식은 빛의 강도가 거리의 제곱에 따라 어떻게 약해지는지를 명확하게 보여줍니다.
우리는 광도 I(luminous intensity)와 조도 E(illuminance)를 사용하여 빛의 감쇠를 계산합니다 ( 주: 광도(I)를 바탕으로, 거리가 멀어짐에 따라 물체 표면에 도달해 맺히는 빛의 양인 조도(E)가 어떻게 줄어드는지를 계산한다는 의미 ). 이 계산식에서는 거리 단위가 미터(m), 센티미터(cm), 밀리미터(mm) 등으로 통일되어야 합니다. 이론적으로 거리가 0에 가까워지면 값이 무한대로 발산할 수 있지만, 실제로는 광원이 항상 크기를 가지므로 이런 일은 발생하지 않습니다. 하지만 실시간 그래픽스에서는 수치적인 문제를 피하기 위해 작은 바이어스(bias) 값을 더하는 것이 일반적입니다. 경험상 점 광원(punctual light)은 최소한의 크기를 가진다고 가정하고, 오브젝트가 광원 내부로 침투하지 않도록 처리하는 것이 좋습니다.
프로스트바이트(Frostbite)에서는 1 유닛(unit)이 1 미터(meter)에 해당하며, 점 광원의 최소 크기를 1cm로 정의합니다. 이를 반영한 조도 E 계산식은 다음과 같습니다 (이 문서의 나머지 부분에서는 표기 단순화를 위해 max 연산자를 생략하겠습니다):
E = I / max(distance^2, 0.01^2) (14)
여기서 I는 광원의 광도이며, distance는 광원과 픽셀 사이의 거리입니다. max(distance^2, 0.01^2)는 거리가 0에 가까워질 때 발생하는 0으로 나누는 오류를 방지하고, 광원이 최소한 1cm의 크기를 가진다고 가정하여 0.01m의 제곱 값을 최소 거리 제곱으로 사용합니다.
프로스트바이트에서 아티스트는 광량(luminous power) 단위 또는 광도(luminous intensity) 단위로 점 광원의 강도를 제어할 수 있습니다. 포토메트릭 프로파일(photometric profile)에 대한 자세한 내용은 Section 4.5를 참조하세요. 광량은 항상 광도 값으로 변환되어 라이팅 계산에 사용됩니다. 광량은 광원의 입체각(solid angle)에 대해 광도를 적분하여 계산할 수 있습니다.
- 점 광원(Point light):
점 광원의 광량 φ는 전 방향으로 빛을 방출하므로, 전체 구형 입체각(4π 스테라디안(주: 빛이 사방 360도로 완전한 공 모양으로 다 퍼져나갈 때의 총 3차원 각도 ))에 대해 광도를 적분하여 계산합니다.

빛이 퍼져나가는 사방(360도 공 모양의 전체 영역 S)을 잘게 쪼갠 각도(미소 입체각 dθdφ)마다 빛의 세기(광도 I)를 다 더하면, 구 전체 크기(4π)만큼이 곱해져 '총 광량 = 광도 I × 4π'가 됩니다.
(주: 구 전체 크기 4파이 (4π) 스테라디안은 사실 좀 오해의 여지가 있습니다. 정확히는 구 전체의 면적을 나타내는 3차원 입체각도입니다 . 그래서 각 각도마다의 빛의 세기와 전체 면적을 나타내는 3차원 입체각도를 곱하면 전체 광량이 되지요)
- 스포트라이트(Spot light) (개방각 0 ≤ θouter ≤ π인 경우):
스포트라이트의 광량 φ는 특정 원뿔형 입체각 Ω 내에서만 빛을 방출하므로, 해당 입체각에 대해 광도를 적분합니다.

수식 16은 원뿔의 정확한 입체각을 나타냅니다. 만약 θouter가 반각(half angle)이라면, 적분 결과는 I 2π(1 − cos θhalfouter)가 됩니다. 이러한 공식화의 결과로, 동일한 광도 값에 대해 원뿔 각도가 작을수록 빛이 더 집중되어 조명 수준이 더 밝게 보입니다 (그림 23 참조). 하지만 이러한 광원 집중도(focused light coupling)와 광도 간의 연동은 아티스트가 스포트라이트를 조작하기 어렵게 만들고, 최적화 과정에서 문제를 일으킵니다 ( 아티스트가 광원의 밝기(강도)를 바꾸지 않으면서, 원뿔 감쇠 범위를 좁혀 빛의 영향을 받는 픽셀 수를 줄이는 것이 불가능합니다 ).
프로스트바이트에서는 이러한 광원 집중도 연동을 제거하고, 스포트라이트 반사경이 빛을 흡수하여 단순히 마스킹(masking)하는 것처럼 동작하도록 결정했습니다. 이러한 고려 사항과 디스크 영역 광원(disk area light)과의 부드러운 전환을 위해, 프로스트바이트에서 스포트라이트의 광량은 다음과 같이 정의합니다: (주: 광량을 유지한체 빛 각도를 모으면 빛이 너무 세지니까, 빛 각도를 모으는 방식이 아니라 그냥 각도로 짤라버리는 (마스킹) 방식으로 처리해서 빛 각도를 모아도 빛이 안 강해지게 했다는 뜻)
φ = πI (17)
점 광원의 광량에서 광도로의 변환은 표 9에 요약되어 있습니다. 참고로, 프러스텀 라이트(frustum light) [Wiki] (즉, 직사각형 피라미드 형태의 광원)에 대한 변환도 추가했습니다. 하지만 프러스텀 라이트와 라인 라이트(line light)는 프로스트바이트에서 영역 광원(area light)으로 간주되므로, 이 공식들을 사용하지 않습니다 . 이들은 가끔 사용되지만, 점 광원이나 스포트라이트처럼 효율적인 라이팅 경로를 공유할 수는 없습니다.

라이팅 계산(Light calculation): 이러한 광원들에 대한 라이팅 계산은 다음과 같이 표현할 수 있습니다.
- 점 광원(Point light): 점 광원의 라이팅 평가는 다음과 같이 표현됩니다.

여기서 Lout은 출력 복사 휘도(outgoing radiance),( 표면에서 반사되어 관찰자(카메라) 쪽으로 나가는 빛의 휘도)
f(v, l)은 BRDF(Bidirectional Reflectance Distribution Function),
E는 조도(illuminance),( 표면 단위 면적에 도달하는 총 빛의 양)
Lin은 입사 복사 휘도(incoming radiance)를 나타냅니다.
⟨n·l⟩는 표면 법선 n과 광원 방향 l의 내적(dot product)으로, 코사인 감쇠(cosine attenuation)를 의미합니다. 이 식은 조도 E를 I / distance^2로 대체하고, 다시 광도 I를 φ / 4π로 대체하여 광량 φ를 기준으로 표현한 것입니다.
(최종 계산 결과 = 재질 반사율 * 표면에 닿은 빛 = 재질반사율 * 들어온 빛 세기 * 비스듬한 각도 감쇄 = 재질 반사율 × (광도 / 거리제곱) × 비스듬한 각도 감쇠 = 재질 반사율 × (루멘 / (4π × 거리제곱)) × 비스듬한 각도 감쇠 로 유도되어 가는 식)

광원을 정면으로 똑바로 향하고 있는 표면 법선(surface normal)을 고려할 때, 역제곱 법칙에 따라 산출되는 조도(illuminance)는 다음과 같습니다 (주: 최대 조도-빛을 받는 표면 물리량- 상태)

주: 조도 = 전체 구 표면에 떨어지는 빛의 양 (광량 ) / (1m²(1m × 1m) 면적에 떨어지는 빛의 양 * 거리제곱)
- 스포트라이트 ( Spot light ): 스포트 라이트의 라이팅 평가는 다음과 같이 표현됩니다.

( 주: 최종 계산 결과 Lout = 재질 반사율 × (광도 / 거리제곱) × 비스듬한 각도 감쇠 = 재질 반사율 × (루멘 / (π × 거리제곱)) × 비스듬한 각도 감쇠 × 원뿔 각도 감쇠)
이에 따른 결과 조도는 역제곱 법칙을 따르며, 광원을 정면으로 향하고 있는 표면 법선을 고려할 때 (프로스트바이트 규칙에 따라) 다음과 같습니다

측정(Measurement):
이러한 수식 관계를 검증하기 위해 입사광 측정기(조도계)로 실제 빛을 측정해 보았습니다(그림 24 참조). 표 10의 측정 결과를 보면, 엔진에서 처리한 광속(루멘) 단위 계산과 '일반 전구를 점광원으로 가정한 방식'이 잘 맞아떨어지는 것을 알 수 있습니다. 루멘 값이 두 배가 되면 표면에 맺히는 조도 역시 정확히 두 배가 되었습니다. 가장 가까운 거리의 측정은 전구 표면에 측정기를 직접 대고 진행했는데, 여기서 측정된 최대 조도 값은 전구 자체의 물리적 크기에 따라 달라집니다.
반면 스포트라이트는 측정 환경 구성이 까다로울 뿐만 아니라 프로스트바이트의 간소화된 스포트라이트 정의와도 맞지 않아 따로 실측하지 않았습니다. 프로스트바이트에서는 실제 측정값과 잘 일치하는 점광원을 모든 조명 세기 보정(Calibration)의 기준 광원으로 사용하므로, 이를 바탕으로 한 라이팅 수치는 충분히 신뢰할 수 있습니다.
감쇠(Attenuation): 역제곱 법칙의 치명적인 단점은 거리가 아무리 멀어져도 빛의 세기가 완전히 0이 되지 않는다는 점입니다. 렌더링 성능을 확보하려면 빛이 닿지 않는 영역을 잘라내는 광원 컬링(Light culling)을 적용해야 하므로, 빛의 영향 반경을 유한하게 제한해야 합니다. 즉, 설정한 최대 반경 끝자락에서는 조도가 부드럽게 0으로 떨어져야 합니다. 이를 해결하는 대표적인 방법이 기본 역제곱 곡선의 형태는 최대한 유지하면서 끝부분만 0으로 닫아주는 윈도잉(Windowing) 기법입니다[Kar13]. 우선 가장 단순한 접근법으로, 거리에 따라 조도를 0으로 선형 보간(Lerp)하는 방식부터 살펴보겠습니다.


광원 감쇠(light attenuation)를 모델링하는 한 가지 방법은 다음 식 22와 같이 lerp 함수를 사용하는 것입니다.

여기서 E는 조도(illuminance), I는 광원의 강도(intensity), distance는 광원으로부터의 거리, lightRadius는 광원이 완전히 감쇠하여 0이 되는 최대 반경을 의미합니다. 이 식은 distance가 lightRadius에 가까워질수록 I / distance^2 값이 0으로 선형 보간(linear interpolation)되도록 합니다.
(주: 최종 계산 결과 E = lerp( 원래 물리 공식에 따른 역제곱 조도,0, distance / lightRadius (현재 거리가 최대 반경 대비 몇 %나 도달했는지를 나타내는 0~1 사이 비율 ) = (광도 / 거리제곱) × (1 - (거리 / 최대반경))
즉 최대 거리(lightRadius)에 다다를수록 빛의 세기를 1에서 0으로 점차 깎아내어 0으로 수렴시키는 식)
하지만 이 함수를 그대로 사용하지 않고, 거리 기준을 다음과 같이 조정하여 식 23과 같은 E_window2 함수를 만들 수 있습니다.

(주: 최종 계산 결과 E_window2 = (광도 / 거리제곱) × (1 - (거리 / 최대반경)^20) = 평소에는 빛의 세기를 100% 온전히 유지하다가, 반경 끝자락(lightRadius)에 도달하는 순간에만 급격하게 0으로 뚝 떨어뜨리려고 지수를 20승이나 높여 강제로 버티게 만든 식)
앞선 20제곱 방식은 끝자락에서 빛이 칼로 자른 듯 뚝 끊겨 보이는 하드 컷오프(hard cutoff) 문제가 있습니다.
이를 해결하기 위해 [Mad11]에서 제안한 두 번째 접근법은 임계값(Threshold)을 활용한 스케일-바이어스(Scale-Bias) 방식입니다.
원리는 단순합니다. 최대 반경(lightRadius)에 도달했을 때 원래 나와야 할 아주 미세한 역제곱 빛의 양을 바닥 기준점(임계값)으로 정합니다. 그리고 전체 조도 곡선에서 이 기준점만큼을 통째로 아래로 끌어내려(Bias), 최대 반경 지점에서 빛이 정확히 0에 닿도록 만듭니다. 그 후 아래로 내려앉으면서 줄어든 전체적인 빛의 스케일을 다시 원래 밝기(0~1 범위)로 늘려 맞춰주는(Scale) 방식입니다.
이를 위해 먼저 최대 반경 지점에서의 기준 빛 세기(임계값)를 식 24와 같이 정의합니다.

그리고 이 임계값을 사용하여 식 25와 같이 E_scaleBias 함수를 계산합니다.

이 방식은 이전보다 훨씬 자연스럽지만, 빛이 0으로 끝나는 최대 반경(lightRadius) 경계면에 도달할 때 곡선의 기울기가 수평(0)으로 완만하게 눕지 않고 각진 채로 닿는다는 단점이 있습니다. 이 때문에 렌더링 화면을 보면 빛이 끝나는 경계선 테두리가 미세하게 눈에 띄게 됩니다.
따라서 더 좋은 방법은 끝부분에 도달했을 때의 기울기가 완벽하게 0이 되도록 만들어주는 윈도잉 함수(windowing function)를 곱하는 것입니다. 이는 감쇠 곡선에 거듭제곱(제곱수)을 적절히 적용하여 끝자락을 완만한 S자 형태로 부드럽게 눕힘으로써 해결할 수 있으며, 이에 대한 비교는 아래 그림 25에서 확인할 수 있습니다.

- 왼쪽 그래프: 감쇠 곡선 모양 비교. Window2(20제곱)는 끝자락에서 절벽처럼 뚝 떨어지는 반면, Window1과 Window3은 바닥에 닿을 때 완만하게 누워 기울기 0(수평)으로 안착합니다.
- 가운데 그래프 (반경 10): 물리적 기준선(Reference)은 0이 되지 않고 떠 있습니다. Scale & Bias는 0에 닿기는 하지만 빗각 형태로 툭 부딪히는 반면, Window1은 바닥에 미끄러지듯 부드럽게 닿습니다.
- 오른쪽 그래프 (반경 40): 반경이 커질수록 원래 물리 법칙의 빛 형태(Reference)를 온전히 유지하면서 끝자락만 부드럽게 0으로 닫아주는 효과가 더욱 정교해집니다.)

식 26에서 거듭제곱 지수 n은 빛이 끝자락에서 얼마나 부드럽게 감쇠할지(transition smoothness)를 조절하는 역할을 합니다. 프로스트바이트(Frostbite)는 카리스[Kar13]가 발표한 방식을 따라 n = 4를 적용한 Window1 방식을 채택했습니다.
이 감쇠 함수는 점광원(Point light)과 스포트라이트(Spot light) 같은 모든 점 광원뿐만 아니라 모든 영역 광원(Area light)에도 일괄 적용되었습니다.
다만 이 방식의 선형 보간(lerp) 기준은 광원 중심으로부터 사방으로 퍼지는 구형 방사형(radial) 거리(x^2 / d^2)를 기반으로 작동하기 때문에, 튜브 라이트(tube light)나 직사각형 라이트(rectangular light)처럼 구형이 아닌 형태의 광원에는 물리적으로 완벽하게 맞아떨어지지 않는 한계가 있습니다.
하지만 연산 성능을 최우선으로 고려하여 이러한 트레이드오프(trade-off, 성능과 정확도의 타협)를 수용하기로 결정했습니다. 만약 시각적으로 더 높은 정확도가 필요한 상황이라면, 아티스트가 광원의 최대 반경(lightRadius)을 더 넉넉하게 키워서 이 오차를 해결할 수 있습니다.
이어지는 Listing 4는 지금까지 다룬 다양한 라이팅 및 감쇠 기법들을 실제로 구현한 최종 셰이더 코드입니다.
float smoothDistanceAtt ( float squaredDistance , float invSqrAttRadius )
{
float factor = squaredDistance * invSqrAttRadius ;
float smoothFactor = saturate (1.0 f - factor * factor );
return smoothFactor * smoothFactor ;
}
float getDistanceAtt ( float3 unormalizedLightVector , float invSqrAttRadius )
{
float sqrDist = dot ( unormalizedLightVector , unormalizedLightVector );
float attenuation = 1.0 / (max ( sqrDist , 0.01*0.01) );
attenuation *= smoothDistanceAtt ( sqrDist , invSqrAttRadius );
return attenuation ;
}
float getAngleAtt ( float3 normalizedLightVector , float3 lightDir ,
float lightAngleScale , float lightAngleOffset )
{
// On the CPU
// float lightAngleScale = 1.0 f / max (0.001f, ( cosInner - cosOuter ));
// float lightAngleOffset = -cosOuter * angleScale ;
float cd = dot ( lightDir , normalizedLightVector );
float attenuation = saturate ( cd * lightAngleScale + lightAngleOffset ) ;
// smooth the transition
attenuation *= attenuation ;
return attenuation ;
}
// Process punctual light
float3 unnormalizedLightVector = lightPos - worldPos ;
float3 L = normalize ( unnormalizedLightVector ) ;
float att = 1;
att *= getDistanceAtt ( unormalizedLightVector , lightInvSqrAttRadius );
att *= getAngleAtt (L , lightForward , lightAngleScale , lightAngleOffset );
// lightColor is the outgoing luminance of the light time the user light color
// i.e with point light and luminous power unit : lightColor = color * phi / (4 * PI)
float3 luminance = BSDF (...) * saturate ( dot (N , L)) * lightColor * att ;
댓글