TECHARTNOMAD | TECHARTFLOWIO.COM

TECH.ART.FLOW.IO

[발표 번역] MEGALIGHTS: STOCHASTIC DIRECT LIGHTING IN UNREAL ENGINE 5

jplee 2025. 12. 30. 13:21

역자의 말: 2025년을 맞이해서 레이트레이싱과 다수의 라이트 처리에 대한 진보된 시도들이 무척 눈에 띄고 있습니다. 게임 출시에 바빠서 최신 언리얼 엔진 소스코드 조차 살펴보지 못했지만 늦은 시각에 빨래를 건조까지 돌려놨더니 시간 여유가 생겨서 시그라프 2025 어드벤스드 렌더링 세션에서 발표 된 애픽의 메가라이트 확률적 직접 조명 발표 내용을 살펴보게 되었네요.


오늘 영상 공개 됬네요.
https://youtu.be/dmmN8_c8Tb0?si=ltyCrNSO0DRAvF1f

SIGGRAPH 2025 Advances in RT Rendering: Megalights: Stochastic Direct Lighting in Unreal Engine 5

https://advances.realtimerendering.com/s2025/index.htmlABSTRACT: MegaLights, Unreal Engine 5’s new stochastic direct lighting path, enables artists to place ...

www.youtube.com

초록(Abstract): MegaLights는 Unreal Engine 5의 새로운 확률적 직접 조명(stochastic direct lighting) 경로로, 아티스트가 이전보다 훨씬 더 많은 동적(area) 라이트와 그림자 처리된 에어리어 라이트를 배치할 수 있게 해 준다. 이 기법은 현 세대 콘솔을 타깃으로 설계되었으며, 다양한 유형의 에어리어 라이트에서 현실적인 소프트 섀도를 얻기 위해 레이 트레이싱을 적극적으로 활용한다.
우리는 라이트 계층 구조(light hierarchies)나 리저버 리샘플링(reservoir resampling) 기반 기법 등 기존의 명시적(explicit) 라이트 샘플링 기법들을 조사했지만, 현 세대 콘솔에 스케일시키기에는 한계를 확인했다. 본 발표에서는 이러한 한계를 어떻게 극복했는지 다룬다. 구체적으로는 완전한 직접 조명 솔루션을 구성하는 모든 요소를 설명한다. 라이트를 어떻게 샘플링하는지, 레이를 어떻게 가이드하는지, 어떤 레이 트레이싱 파이프라인을 사용하는지, 레이 트레이싱 지오메트리 불일치 문제를 어떻게 처리하는지, 스케일링 전략, 반투명 및 볼류메트릭 효과 처리, 샘플 셰이딩 및 디노이징까지 모두 설명하면서 목표 하드웨어 제약 안에 이 모든 것을 어떻게 담아냈는지 공유한다.
발표자 소개(Speakers Bio):

Krzysztof Narkowicz는 Epic Games 그래픽스 엔지니어링 펠로우(Engineering Fellow in Graphics)로, Lumen을 공동으로 설계했으며 주로 라이팅 관련 작업을 담당하고 있다. 그 이전에는 11 Bit Studios와 Flying Wild Hog에서 10년 넘게 다양한 게임 타이틀을 개발했다. 아티스트와 함께 일하는 것, 아름다운 픽셀을 만드는 것, 그리고 하드웨어에 가깝게("close to the metal") 코드를 작성하는 일을 좋아한다.

Tiago Costa는 Epic Games의 프린시플 렌더링 프로그래머(Principal Rendering Programmer)로, 레이 트레이싱과 기타 렌더링 기능을 담당하고 있다. 이전에는 Rockstar North, Apple, Meta Reality Labs에서 근무했다.

안녕하세요, Krzysztof입니다. 오늘은 Tiago Costa와 함께 Unreal Engine 5의 새로운 확률적 직접 조명 기법인 Stochastic Direct Lighting을 소개하려 합니다.

게임은 항상 품질의 상한선을 끌어올리고자 합니다. 더 많은 라이트를 쓰고 싶어 하고, 각 라이트가 동적이어야 하며, 모두 그림자를 가져야 하고, 현실적인 소프트 섀도를 갖는 에어리어 라이트를 원합니다. 여기에 복잡한 BRDF, 복잡한 라이트 소스, 여러 BRDF 평가가 필요한 레이어드 머티리얼까지 더해지면, 단순한 언샤도우드(unshadowed) 라이트 평가만으로도 너무 비싸지는 상황이 자주 발생합니다.
동시에 게임의 전체 콘텐츠 규모와 복잡성도 급격히 증가하고 있어서, 이제는 "워크플로우" 자체가 그래픽 품질을 제한하는 주요 요인이 되었다고 생각합니다. 이상적인 워크플로우에서는, 아티스트가 이 스크린샷처럼 제약 없이, 장난치듯(playfully) 마음껏 작업할 수 있어야 합니다. 이 예시는 다소 과한 면도 있지만, 아티스트가 절차적으로 라이트를 배치하는 방법을 이해하고 나면 실제로 이런 식의 장면이 쉽게 만들어집니다.
또 하나 중요한 점은, 이 기법이 "기본(baseline) 라이팅"이어야 한다는 것입니다. 아티스트가 제작 과정 내내 메인 비주얼 타깃으로 사용할 수 있는 조명 기법이어야 합니다. 추가적인 기법이라서 아티스트에게 한 번 더 리라이팅을 요구하거나, 워크플로우를 복잡하게 만드는 방식이어서는 안 됩니다. 최소한 콘솔과 동일 급 PC GPU에서 기본값처럼 돌아가야 합니다.

직접 조명을 처리하는 방법에는 여러 가지가 있습니다.
게임에서 흔히 쓰는 방식은 포워드 혹은 디퍼드 라이팅입니다. 화면에 보이는 각 라이트에 대해 먼저 섀도우 맵(또는 레이 트레이싱 기반 섀도우)을 계산하고, 그 결과를 디노이즈해서 섀도우 마스크로 저장합니다. 그리고 별도 패스에서 미리 적분된(Pre-integrated) 조도(irradiance)에 이 섀도우 마스크를 곱해, 라이트 하나씩 화면에 적용합니다.
이 방식은 라이트 개수가 적을 때는 매우 잘 동작합니다. 하지만 라이트 수가 늘어나면 라이트별로 수행하는 작업량이 너무 커져서 비용이 기하급수적으로 증가합니다.
또 다른 접근은, 오프라인 렌더링에서 더 흔하게 볼 수 있는 방식입니다. BRDF 기반으로 혹은 라이트들의 확률적 부분집합을 향해 고정 개수의 레이를 쏘고, 레이가 라이트에 적중하면 그 에너지를 누적합니다. 이 경우 성능은 라이트 개수와 무관해지고, 픽셀당 고정된 개수의 레이만 추적하면 되므로 라이팅 복잡도에 거의 영향을 받지 않습니다.

디퍼드 라이팅은 게임에서 워낙 널리 쓰이기 때문에, 우리의 첫 번째 실험 대상이 되었습니다. 레이 트레이스 섀도우를 빠르게 만드는 방법, 라이트 평가를 가속하는 방법 등을 다양하게 시도해 봤지만, 원하는 수준의 라이팅 복잡도로는 잘 스케일되지 않았습니다.
콘텐츠를 세심하게 들여다보니, 왜 목표를 달성하지 못하는지 이유가 분명해졌습니다. 예를 들어, 이 한 픽셀은 감쇠(attenuation) 범위 안에 50개의 라이트가 있습니다. 각 라이트에 대해 우리가 해야 할 일은 섀도우 맵을 만들거나, 레이 트레이스를 하고, 그 결과를 디노이즈해 섀도우 마스크를 만드는 것입니다. 하지만 실제로 해당 픽셀에 눈에 띄는 영향을 주는 라이트는 15개뿐입니다. 즉, 나머지에 대해서는 상당한 양의 연산이 그냥 낭비되고 있습니다.
일부 비용은 섀도우 맵 캐싱으로 숨길 수 있지만, 근본적인 스케일 문제는 크게 달라지지 않습니다. 라이트·카메라·오브젝트가 움직이기 시작하면 섀도우 캐시 무효화 때문에 성능이 무너집니다. MegaLights 데모에서는 수백 개의 이동 라이트(예: 날아다니는 봇들)가 등장하는데, 이런 콘텐츠는 대부분의 캐싱 전략을 무력화합니다.
모든 것이 정적이라고 가정하더라도, 섀도우 맵 페이지를 캐시하려면 비현실적인 수준의 메모리가 필요합니다. MegaLights 데모에서 VSM(Virtual Shadow Maps)을 켜면, 섀도우 맵 가상 페이지 캐시의 4GB 최대 할당량이 꽉 차고, 그럼에도 필요한 모든 라이트를 커버하지 못해서 다양한 아티팩트가 발생합니다.
레이 트레이스 섀도우는 적응형 트레이싱(픽셀·타일 단위로 트레이스 횟수 조절)과 스파스 섀도우 마스크를 활용해 일부 비용을 줄일 수 있지만, 역시 본질적인 스케일 문제는 변하지 않습니다. 레이 트레이싱과 디노이징 자체가 꽤 비싸고, 라이트별로 이 과정을 수행하는 순간 사용할 수 있는 라이트 개수에 매우 낮은 상한이 생겨 버립니다.
설령 섀도잉 비용이 공짜라고 가정하더라도, 픽셀마다 수많은 라이트를 평가해야 하는 문제가 남습니다. 이 예시에서는 이론적으로 15개의 라이트만 실제로 보이지만, 이 라이트들은 모두 복잡한 타입이고, 머티리얼도 복잡합니다. 콘솔에서는 언샤도우드 상태라고 해도 이 정도 개수의 복잡한 라이트를 평가하기엔 여전히 부담스럽습니다.
이 15개의 보이는 라이트를 다시 보면, 전체 에너지의 80%가 단 하나의 라이트에서 나오고 있습니다. 그렇다면 왜 50개의 라이트를 브루트 포스로 모두 계산하려고 할까요? 그 강한 한 개 라이트만 고품질로 정확히 계산하고, 나머지는 근사해도 되지 않을까요?

다른 접근법은 BRDF 샘플링, 즉 GI(Global Illumination) 문제로 보는 것입니다.
UE5에서는 흔히 쓰이는 워크어라운드로, 아티스트가 장면에 에미시브(emissive) 메시를 배치한 뒤 메인 뷰에서는 숨기고, 이 메시들을 소프트 에어리어 라이트처럼 사용하는 방식이 있습니다.
GI는 어차피 계산해야 하므로 이런 방법은 사실상 공짜에 가깝지만, 품질 측면에서 여러 문제가 있습니다. 작은 에미시브 표면이나 화면에서 멀리 떨어진 에미시브 표면은 BRDF 샘플링만으로는 찾기가 매우 어렵습니다. Lumen(UE5의 GI/리플렉션 시스템)의 레이 가이딩을 보조로 사용하더라도 마찬가지입니다.
또 다른 문제는, 실시간 GI가 종종 직접 조명의 도움을 필요로 한다는 점입니다. 오프라인 렌더링에서도 컨버전스를 빠르게 하기 위해 창가에 에어리어 라이트를 배치하는 식의 트릭을 자주 사용합니다. 우리는 지금 콘솔에서 60Hz 실시간 GI를 목표로 하고 있습니다. 이런 상황에서 분석적인(analytical) 라이트로 GI를 도와야 하는데, 오히려 GI가 직접 조명까지 떠안고 있는 구조가 되어 버리면 문제가 됩니다.

세 번째 접근은 완전히 새로운 시스템을 만드는 것입니다. 픽셀마다 고정된 개수의 라이트 부분집합을 확률적으로 선택하고, 그쪽으로 레이를 쏴 보는 겁니다. 레이가 라이트에 맞으면 그 지점을 셰이딩하고, 해당 라이트 에너지를 하나의 렌더 타깃에 누적합니다. 모든 레이 트레이싱이 끝나면 이 단일 렌더 타깃을 디노이즈합니다.
이 방식의 장점은, 장면에 라이트가 얼마나 많든 간에 픽셀당 수행하는 작업량이 고정된다는 점입니다.
물론 실제 구현은 그리 단순하지 않습니다. 콘솔에서는 픽셀당 1개 레이가 한계인 경우가 많습니다. 이 경우, 각 레이가 최대한 가치 있는 곳에 쓰이도록 라이트 선택(light selection)이 극도로 중요해집니다. 오른쪽 다이어그램처럼, 셰이딩 포인트 바로 근처의, 겉보기에는 아주 중요해 보이는 라이트 두 개를 향해 2개 레이를 쐈는데 둘 다 가려져 있다면, 우리는 예산 전체를 허비한 셈입니다.
라이트 선택을 위한 다양한 기법이 존재합니다.
가장 널리 쓰이는 것 중 하나는 라이트 계층 구조(light hierarchies)를 사용하는 것입니다. 먼저 라이트들에 대한 계층 구조를 만들고, 그 중 가장 중요한 클러스터를 고르는 방식입니다. 미리 만들어 둔 계층 구조는 라이트 샘플링을 빠르게 할 수 있게 해 주지만, 가시성(visibility)을 반영하지 못한다는 치명적인 한계가 있습니다. 다이어그램에서 볼 수 있듯, 가시성은 라이트 선택에서 매우 결정적인 요소입니다. 계층 구조를 활용해 라이트를 병합하는 LOD 형태의 기법도 가능하지만, 실제로는 잘못된 라이팅, 빛샘(leaking) 등 문제가 많았습니다. 또한 매 프레임 라이트 계층을 만드는 연산 자체가 GPU에 잘 맞지 않아, 상당히 비싼 작업이 됩니다.
또 다른 인기 있는 기법은 ReSTIR입니다. 하이엔드 PC에서는 놀라운 결과를 보여 주지만, 이걸 콘솔 수준까지 스케일 다운해서 기본 직접 조명 기법으로 사용할 수 있을까요?

이 질문에 답하려면 먼저 ReSTIR이 실제로 무엇을 하는지 살펴봐야 합니다.
ReSTIR은 확률적 직접 조명 파이프라인에 주황색으로 표시된 몇 개의 블록을 추가합니다. 이 블록들은 히스토리와 이웃 픽셀에서 일부 샘플을 가져와 레이를 쏴 가시성을 확인한 뒤, 새로 생성한 후보 샘플과 함께 다시 확률적으로 조합합니다.
핵심 아이디어는, 현재 프레임에서 좋은 후보를 뽑지 못하더라도, 예를 들어 모든 샘플이 가려졌더라도, 과거 히스토리나 이웃 픽셀에는 더 나은 샘플이 있을 가능성이 크다는 점을 활용하는 것입니다.
ReSTIR은 이런 재사용 덕분에 품질을 크게 끌어올릴 수 있지만, 그만큼 높은 상수 비용이 붙습니다. 재사용 자체가 비쌀 뿐 아니라, 가장 큰 비용은 재사용되는 각 샘플의 가시성을 확인하기 위해 레이를 한 번씩 더 쏴야 한다는 점입니다.
실용적인 품질을 얻으려면 픽셀당 최소 1 SPP(샘플/픽셀)가 필요하고, 이를 위해 픽셀당 2~3회의 레이 트레이스가 요구됩니다. 하이엔드 PC에서는 크게 문제가 되지 않지만, 콘솔에서는 픽셀당 1개 레이도 버거운 상황이라 적용이 어렵습니다.

또 다른 문제는 후보 샘플링(candidate sampling)입니다.
이론적으로는 픽셀마다 완전히 랜덤한 라이트를 고를 수도 있습니다. 하지만 실제 게임에서 출하 가능한 품질을 내려면, 어떤 라이트를 후보로 뽑을지 매우 신중하게 골라야 합니다. 각 라이트를 BRDF로 가중해야 하고, 픽셀당 최소한 전체 라이트 목록의 20% 정도는 평가할 수 있어야 합니다. 하이엔드 PC라면 괜찮지만, 콘솔에서는 이것만으로도 사실상 언샤도우드 라이트 다수를 매 프레임 평가하는 것과 같아서, 비용이 금방 한계에 도달합니다.
게다가 BRDF 가중치에는 섀도잉 정보가 전혀 반영되지 않습니다. 강한, 그러나 가려진 라이트는 샘플 가중치가 높게 나와서 샘플을 과도하게 배정하게 되고, 실제로 픽셀에 기여하는 좋은 라이트를 찾는 일을 더 어렵게 만듭니다.
정리하자면, 이미 감당하기 힘든 높은 상수 비용을 가진 상태에서, 여전히 이전과 거의 비슷한 라이트 샘플링 문제를 풀어야 한다는 뜻입니다. 단지 상수 계수만 바뀐 셈입니다.

그렇다면 "ReSTIR보다 더 나은 무언가"를 만들 수 있을까요?
확률적 라이트 샘플링 실험을 하다 보면, 오히려 아주 단순한 샘플링이 ReSTIR보다 나아 보이는 경우가 있습니다. 이유는 상당히 직관적입니다. 초기 샘플은 이후 프레임에서도 반복해서 등장할 확률이 더 높습니다. 매 프레임, 현재 샘플과 과거 샘플을 연결하는 경로의 수가 기하급수적으로 증가하기 때문입니다. 이 때문에 실제 구현에서는 M(히스토리에 유지하는 프레임 수)을 클램핑하지만, 그럼에도 샘플 뭉침(clumping) 현상은 여전히 뚜렷하게 나타납니다. 이를 완전히 해결하려면 M을 1로 줄여야 하는데, 그러면 재사용 자체가 사실상 사라지고 단순 확률 샘플링으로 되돌아가게 됩니다.
이렇게 뭉친 샘플들은 ReSTIR 결과를 덜 노이즈처럼 보이게 만들 수는 있지만, 동시에 디노이저 효율을 크게 떨어뜨립니다. 디노이저가 최적으로 동작하려면, 라이트가 픽셀마다 상관성이 적고(non-correlated), 서로 번갈아 가며 선택되는(alternating) 패턴을 갖는 것이 중요하기 때문입니다.
다른 관점에서 보면, ReSTIR과 디노이저는 모두 스크린 공간에서 시공간(spatio-temporal) 필터링을 수행한다는 공통점이 있습니다. 차이점이라면, ReSTIR은 이미 셰이드된 결과를 재사용하는 대신, 재사용할 샘플을 다시 셰이딩하고, 더 긴 히스토리를 유지하며(보통 디노이저보다 2배 길게), 히스토리 보정(history rectification)을 거치지 않는다는 점 정도입니다. 이 덕분에 ReSTIR은 더 많은 프레임을 누적하고 이웃 픽셀도 더 공격적으로 재사용할 수 있지만, 동시에 샘플링 패턴을 뒤섞어 버려서 경우에 따라 전체 퀄리티를 떨어뜨리기도 합니다.
그렇다면 굳이 그 시공간 재사용 단계를 거치지 않고, 처음부터 디노이저에 최적화된 샘플링을 설계하면 어떨까요? 그렇게 하면 비슷한 수준의 품질을 훨씬 저렴한 비용으로, 그리고 콘솔 수준의 예산 안에서도 달성할 수 있을 것입니다.

이상적으로는, 픽셀마다 "보이는 라이트 목록(visible light list)"만 샘플하면 좋겠습니다.
이런 목록이 있다면, 매 프레임 서로 다른 라이트를 확률적으로 선택해 디노이저에 이상적인 패턴을 만들어 줄 수 있습니다.
또한 보이는 라이트만 샘플한다면, 최종 픽셀에 기여하지도 않는 숨겨진 라이트를 향해 레이를 쏘느라 예산을 낭비하지 않게 되고, 그만큼 노이즈도 줄어듭니다.

히스토리는 이런 보이는 라이트 목록을 만드는 데 훌륭한 재료가 될 수 있습니다. 이전 프레임에서 보였던 라이트 샘플은, 다음 프레임에서도 보일 확률이 높기 때문입니다.
픽셀마다 개별적인 라이트 리스트를 두기에는 메모리 오버헤드가 너무 크지만, 예를 들어 8×8 스크린 타일 단위라면 현실적인 수준입니다. 인접한 픽셀들은 비슷한 가시성을 공유할 가능성이 높기 때문입니다.
이를 위해 우리는 트레이싱 패스가 끝난 뒤, 보이는 라이트 샘플들을 모아서 WaveActiveMin 같은 연산을 활용해 8×8 타일마다 정렬된 라이트 목록을 만듭니다. 다음 프레임 샘플링 패스에서는, 픽셀을 재투영(reproject)해 적절한 타일을 찾고, 거기서 보이는 라이트를 선택합니다.
우리는 이진 가시성 대신, 히트/미스 비율에 따라 가중치를 조절하는 여러 변형도 실험해 봤습니다. 하지만 실제로는 덜 효과적이었습니다. 이런 기법은 페넘브라 영역에서 레이 수를 줄이는 경향이 있는데, 페넘브라는 본질적으로 노이즈가 많기 때문에 오히려 더 많은 샘플을 할당해 주는 것이 바람직한 경우가 많습니다.
8×8 타일은 꽤 크기 때문에, 때로는 눈에 띄는 경계가 보이기도 합니다. 사실상 타일 경계에서 한 라이트 리스트에서 다른 리스트로 "갑자기" 전환되기 때문입니다. 이를 완화하기 위해, 우리는 스토캐스틱 바이리니어(stochastic bilinear) 룩업을 사용합니다. 인접한 4개 리스트 사이에서 확률적으로 보간하여, 경계를 부드럽게 만들고 새로운 보이는 라이트가 화면 전체에 더 빠르게 퍼지도록 합니다.
8×8 타일은 깊이 불연속 지점에서 샘플 가이딩의 효율을 다소 떨어뜨리지만, 실전에서는 큰 문제가 되지 않았습니다. 이 방식이 여전히 픽셀 단위로 라이트 가시성을 누적하기 때문입니다.
초기에는 블룸 필터(bloom filter)를 사용했는데, 블룸 필터는 매우 흥미로운 자료구조이면서도 실시간 렌더링에서는 드문 편입니다. 이후에는 명시적 리스트로 전환했습니다. 명시적 리스트는 각 라이트에 추가 페이로드를 붙일 수 있고, 라이트 그리드 셀 안에 있는 모든 라이트를 순회하지 않고도 리스트 자체를 직접 샘플링할 수 있다는 장점이 있습니다.
이 리스트의 길이는 작게 고정해도 됩니다. 근본적으로는 8×8 타일당 보이는 샘플 개수에 의해 상한이 결정되기 때문입니다. 픽셀당 1 SPP로 셰이딩한다면, 디노이저의 시간 누적까지 감안하더라도, 한 타일에서 실제로 누적 가능한 라이트 개수는 엄청나게 많지 않습니다.

물론 보이는 라이트 리스트만 샘플할 수는 없습니다. 장면은 동적이며, 숨겨져 있던 라이트가 시야에 들어올 수도 있습니다. 새로운 보이는 라이트를 발견하는 데도 일정 비율의 샘플 예산을 써야 합니다.
우리는 보이는 라이트 리스트와 숨겨진 라이트 리스트, 두 쪽 모두에서 샘플을 뽑은 뒤, 숨겨진 라이트 리스트의 가중치 합을 전체 가중치 합의 20%로 클램프합니다. 이렇게 하면 숨겨진 라이트를 탐색하는 데 항상 고정된 비율의 샘플을 할당하게 됩니다.
구현 방식은 간단합니다. 보이는 라이트는 한 리저버에, 숨겨진 라이트는 다른 리저버에 모으고, 마지막에 두 리저버를 병합하기 직전에 숨겨진 리저버의 가중치를 클램프합니다. 두 리저버는 서로 다른 난수를 사용해 상관성을 줄입니다. 또, 보이는 라이트 리스트의 난수를 기준으로 숨겨진 라이트 리저버를 추가로 샘플링하여, 보이는 라이트 리스트 난수의 노이즈 특성을 유지합니다.
20%라는 값은 물론 튜닝 가능한 파라미터입니다. 수렴 후 품질과 디소클루전(disocclusion) 시의 반응 속도 사이의 트레이드오프를 조절하는 역할을 합니다.
간혹 보이는 라이트들이 매우 어둡거나, 아예 로컬 라이트가 없는 경우도 있습니다. 이때는 20% 클램프를 완화해 숨겨진 라이트에 더 많은 레이를 할당하고, 장면 변화나 디소클루전에 더 빠르게 반응하도록 합니다.
숨겨진 라이트 예산 방식은, 단순히 숨겨진 라이트 가중치를 줄이는 것보다 효과적입니다. 아무리 강한 숨겨진 라이트라도 전체 샘플 예산의 20% 이상을 가져가지 못하기 때문에, 샘플링을 독점해서 노이즈를 유발하는 상황을 방지할 수 있습니다.
히스토리 재투영이 오브젝트 움직임이나 화면 밖 샘플링 때문에 실패하는 경우에는, 여전히 가장 가까운 타일의 라이트 리스트를 재사용합니다. 이 리스트에도 유용한 라이트들이 있을 가능성이 높기 때문입니다. 동시에, 새로운 보이는 라이트를 더 빠르게 찾기 위해 숨겨진 라이트 쪽 샘플 비율도 올립니다.

이제 보이는 라이트 리스트를 손에 쥐었으니, 전체 파이프라인을 다시 살펴보겠습니다. 구조는 앞에서 본 것과 거의 동일하지만, 여기에 보이는 라이트 리스트를 만드는 주황색 블록이 하나 추가되었다고 보면 됩니다. 다음 프레임에는 이 리스트를 이용해 라이트를 샘플하고, 아주 작은 추가 비용만으로 샘플을 효과적으로 가이드합니다.
우리는 숨겨진 라이트를 검사하는 데도 일정 비율의 레이를 할당하지만, 그 비중은 상대적으로 작습니다. 대부분의 레이는 보이는 라이트를 향해 쏘아 실제 픽셀에 기여합니다.
또한 이 리스트 덕분에 디노이저를 위한 좋은 샘플 패턴을 만들어 낼 수 있습니다.

이제 이 라이트 리스트를 어떻게 활용하는지 살펴보겠습니다.
우리는 가중 리저버 샘플링(weighted reservoir sampling)을 사용합니다. 아래쪽 코드에서 볼 수 있듯, 이 방식은 모든 라이트를 한 번씩만 순회하면서, 각 라이트의 가중치 비율에 따라 확률적으로 하나를 선택하는 매우 단순한 알고리즘입니다.
각 라이트의 가중치는 BRDF 휘도(luminance)에 로그 기반의 지각적(perceptual) 가중을 적용해 계산합니다. 지각적 가중 자체가 전반적인 품질 향상에 도움이 되지만, 특히 톤매핑 이후 실제 픽셀에 미치는 영향이 그리 크지 않은, 매우 강한 라이트의 상대적 비중을 내려 주는 데 효과적입니다. 이 덕분에 강한, 그러나 가려진 라이트에 샘플이 몰리는 현상이 줄어듭니다.
Spatio-Temporal Blue Noise(STBN)는 디노이징에 최적화된 노이즈 패턴을 제공하는 룩업 텍스처입니다. 이 패턴은 품질 대비 비용이 매우 좋기 때문에, 화면 공간에서 발생하는 다양한 확률적 연산에 널리 사용하고 있습니다.
여기서 작은 문제가 하나 있습니다. STBN 텍스처는 픽셀마다 단 하나의 난수만 제공하도록 설계되어 있습니다. 하지만 아래에 보이듯, 우리는 샘플 선택 루프의 각 반복마다 다른 난수가 필요합니다. STBN 텍스처를 여러 번 읽어 버리면, 원래 의도했던 노이즈 패턴 특성이 망가지게 됩니다.

운 좋게도, 단일 난수를 여러 번 재사용하면서도 원래의 노이즈 특성을 유지할 수 있는 몇 가지 트릭이 있습니다.
단일 샘플을 선택하는 경우, 각 단계에서 선택 구간을 다시 0-1 범위로 재매핑하는 방식으로 난수를 재사용할 수 있습니다.
픽셀당 여러 레이를 쏘고 싶다면, 디더링된 샘플링(dithered sampling)을 사용해 난수 하나를 여러 구간으로 나눌 수 있습니다.
어떤 재매핑이든 난수의 비트 정밀도를 조금씩 소모한다는 단점이 있지만, 실제로는 문제가 되지 않았습니다. 우리는 두 개의 난수를 사용하는데, 하나는 보이는 라이트 리스트용, 다른 하나는 숨겨진 라이트 리스트용입니다. 이 두 난수에 정밀도 손실을 나눠 담으면 되고, 어차피 성능 상의 이유로 리스트 안의 모든 라이트를 다 순회하지 못하는 한계가 있어서, 실질적인 영향은 미미합니다.

이 모든 트릭 덕분에, 우리는 단 하나의 텍스처 룩업만으로도 빠르게 샘플을 선택할 수 있고, 무엇보다 원래 STBN이 가진 좋은 노이즈 특성을 그대로 유지할 수 있습니다. 품질 측면에서 상당한 이득을 주는 개선이지만, 연산이나 레이 트레이스 비용은 거의 늘어나지 않습니다.

이제 라이트를 어떻게 고르는지는 알게 되었지만, 실제 게임 장면에서는 하나의 라이트 그리드 셀에 수백 개의 라이트가 들어갈 수 있습니다. 이런 상황에서는 그리드 셀의 모든 라이트를 순회하는 것만으로도 큰 비용이 됩니다.
우리는 보이는 라이트 리스트에 있는 라이트는 전부 평가하고, 그 뒤에 라이트 그리드에서 일부 라이트만 추가로 평가합니다. 이때 이미 보이는 리스트에서 처리한 라이트는 건너뜁니다. 픽셀마다 서로 다른 확률적 부분집합을 평가하기 때문에, 코히어런시는 떨어지지만, 장면 변화에 매우 빠르게 반응할 수 있다는 장점이 있습니다. 히스토리를 재투영했을 때 실패하는 경우에는, 라이트 그리드에서 평가하는 라이트 수를 늘려서 변화에 더 빨리 따라잡도록 합니다.
알고리즘 자체는, 서로 다른 스텝을 가진 두 개의 정렬된 라이트 리스트를 순회하면서, 인덱스가 더 작은 라이트를 선택하는 형태로 단순화할 수 있습니다.
또 하나의 최적화 포인트는, 우리가 실제로 필요한 것은 BRDF 전체가 아니라 그 휘도라는 점입니다. 입력을 전부 그레이스케일로 변환하는 완전한 그레이스케일 BRDF 계산은 노이즈를 너무 많이 유발해서 쓸 수 없었지만, 라이트 컴포넌트 누적기(light component accumulator) 같은 일부 코드만 그레이스케일로 바꿔 VGPR 사용량을 줄이는 식의 타협을 찾았습니다.
또한 노출 기준으로 아주 작은 라이트 샘플은 일정 임계값 아래에서 잘라 버립니다. 이 임계값은 거의 에너지 손실이 발생하지 않을 정도로 낮게 설정되어 있습니다. 이는 성능 최적화라기보다 품질을 위한 조치입니다. 실제 픽셀에 거의 영향을 주지 못할 라이트에 레이를 할당하느니, 그 레이를 더 중요한 곳에 쓰는 편이 낫기 때문입니다. 매우 드문, 어쩌다 한 번 나오는 희박한(dim) 라이트 샘플들이 높은 가중치를 받으면서 생기는 "파이어플라이" 아티팩트도 이 방식으로 줄일 수 있습니다.

비슷한 노멀과 깊이를 가진 픽셀들 사이에서는, 라이트 가중치도 대체로 비슷합니다. 이 특징을 활용해 샘플링 해상도를 낮출 수 있습니다. 예를 들어, 픽셀마다 샘플 하나를 선택하는 대신, 4개 픽셀마다 4개 샘플을 뽑고 BRDF 계산 결과를 재사용하는 식입니다.
여기서 중요한 점은, 좋은 샘플링 패턴을 써야 한다는 것입니다. 체커보드나 4-rooks와 같은 패턴을 사용하고, 공간적·시간적으로 패턴에 지터를 주어야 합니다. 이렇게 해야 최종 픽셀을 제대로 재구성할 확률이 크게 올라갑니다.
샘플링이 끝난 후에는 깊이와 노멀을 기반으로 한 스토캐스틱 바이리니어 업샘플을 수행합니다. 이상적인 경우, 시간이 지남에 따라 결국 올바른 결과로 수렴합니다.
아주 드물지만, 업샘플에 사용할 유효 픽셀이 하나도 없는 경우가 있습니다. 예를 들어, 아주 얇은 와이어 같은 지오메트리에서는 샘플링 패턴이 한 프레임 동안 그 픽셀을 완전히 건너뛰고 지나칠 수 있습니다. 이런 경우에는 이웃 업샘플 대신, 히스토리를 아무 보정 없이 그대로 재사용합니다. 네이버후드 클램프와 같은 보정도 하지 않습니다.
히스토리 보정과 업샘플링 때문에, 이 방식은 풀 해상도 샘플링과 완전히 같은 결과로 수렴하지는 않습니다. 대신 그림자가 풀 해상도와 하프 해상도 사이 정도로 조금 더 부드러워지는 경향이 있습니다. 하지만 샘플링 비용을 거의 4배 가까이 줄일 수 있기 때문에, 실전에서는 상당히 괜찮은 트레이드오프로 작용합니다.
현재는 풀 해상도와 저해상도 샘플링 사이에서 품질과 성능을 더 잘 조절할 수 있도록, 적응형 샘플링 스킴으로 바꾸는 작업을 진행 중입니다.

방향성 라이트(directional light)는 확률적 라이트 샘플링 관점에서 여러 문제를 일으킬 수 있습니다.
방향성 라이트는 보통 장면에서 가장 강한 라이트입니다. 다른 어떤 라이트보다 수천 배 강한 경우도 흔합니다. BRDF 기반으로 라이트를 샘플링하면, 이런 라이트의 가중치가 매우 높게 나와서, 전체 레이 대부분을 방향성 라이트에만 보내게 됩니다. 그러면 인테리어 장면에서 로컬 라이트를 통해 새로운 보이는 라이트를 찾아야 할 때 문제가 됩니다.
일반적인 해결책은 방향성 라이트를 별도 패스로 처리하는 것입니다. 전용 레이 트레이스 섀도우 패스를 돌려 방향성 라이트만 따로 그림자를 계산하는 식입니다. 이 경우, 방향성 라이트에 대해서는 셰이딩과 디노이징을 별도로 수행할 수 있기 때문에, 품질 측면에서는 유리합니다. 하지만 그만큼 픽셀당 레이가 늘어나고, 셰이딩/디노이징 패스도 하나 더 돌려야 해서, 리소스가 제한적인 게임 타이틀에는 부담이 큽니다.
우리는 이 대신, 방향성 라이트가 가져갈 수 있는 샘플 예산 비율을 제한하는 방식을 사용합니다. 이는 숨겨진 라이트에 대한 20% 예산과 아주 비슷한 개념입니다. 로컬 라이트의 가중치 합이 일정 임계값 이상일 때만, 방향성 라이트 가중치를 로컬 라이트 합의 특정 퍼센트로 클램프합니다. 로컬 라이트가 없거나 매우 어두운 경우에는 이 제한을 완화하여, 방향성 라이트에 더 많은 샘플을 할당합니다.
이 방식은 사실상 "소프트"한 레이 예산처럼 동작합니다. 픽셀마다 로컬 라이트 상황에 따라 방향성 라이트에 쓰이는 레이 개수가 유동적으로 변합니다.
이론적으로는 방향성 라이트가 아닌 다른 라이트도 비슷한 문제를 일으킬 수 있습니다. 예를 들어, 누군가가 태양을 흉내 내기 위해 매우 강한 스포트라이트를 사용할 수도 있습니다. 하지만 실제 콘텐츠에서는 이런 경우가 드뭅니다. 태양만큼 밝지 않은 라이트는 지각적 가중과 숨겨진 라이트 20% 예산만으로도 대부분 잘 처리됩니다. 필요하다면 이런 매우 강한 라이트를 별도의 리스트로 묶고, 이들에 대해 별도의 예산을 배정하는 방식으로 확장할 수도 있습니다.

다음으로는 에어리어 라이트를 살펴보겠습니다.
스크린샷에서 보듯, 에어리어 라이트는 상당히 큰 면적을 가질 수 있고, 단순한 이진 가시성만으로는 부족합니다. 라이트의 큰 부분이 가려져 있을 수 있고, 그 영역까지 포함해 샘플을 쏘면 예산을 낭비하게 됩니다.
이를 개선하기 위해, 우리는 각 라이트에 대해 2×2 비트마스크를 추가로 유지합니다. 이 비트마스크는 라이트의 어떤 부분이 보이는지를 표시하는 정보로, 보이는 라이트 리스트와 함께 구축됩니다.
좀 더 구체적으로 말하면, 이 2×2 비트마스크는 에어리어 라이트 샘플링에 사용되는 2D 난수 공간을 2×2 구간으로 나눈 것입니다. 에어리어 라이트 샘플러는 이 2D 난수를 [0,1]×[0,1] 정사각형에서 라이트 표면의 실제 영역으로 면적 보존(remap with area preservation)을 유지하며 매핑합니다. 이런 설계 덕분에 캡슐, 스피어 등 다양한 에어리어 라이트 타입에서도 품질 좋은 샘플을 얻을 수 있습니다.
하드코딩된 2×2 분할은 모든 상황에서 최적은 아닙니다. 어떤 라이트는 너무 커서 더 많은 분할이 필요하고, 라인 라이트처럼 4×1 같은 다른 비율이 더 어울리는 경우도 있습니다. 현재는 이를 보다 적응형·계층형(hierarchical) 구조로 바꾸는 작업을 진행 중입니다.

샘플링 단계에서는 먼저 픽셀당 N개의 라이트를 선택한 뒤, 각 라이트의 표면에서 실제로 샘플할 지점을 하나 더 뽑아야 합니다.
이 지점 선택에서 가려진 영역은 가중치를 줄이고, 보이는 영역에는 더 많은 샘플을 배정합니다. 이렇게 하면 라이트의 가시 영역을 향한 레이 비율이 늘어나, 노이즈를 줄일 수 있습니다.
각 에어리어 라이트는 2D STBN 노이즈와 2D 샘플 워핑(sample warping)을 사용해 샘플링합니다. 워핑 덕분에 숨겨진 영역을 다운웨이트하는 과정 이후에도, STBN이 가진 좋은 노이즈 특성을 유지할 수 있습니다. 이때 사용하는 STBN과 워핑은 앞서 봤던 스칼라 형태와는 다소 다른 변형으로, 2D 포인트를 다루는 데 특화되어 있습니다.

확률적 샘플링의 특성상, 때로는 레이가 중복되는 경우도 있습니다.
예를 들어, 표면 근처에 매우 강한 포인트 라이트가 있고, 같은 픽셀에서 이 라이트를 향해 완전히 동일한 레이 4개를 쏜 상황을 생각해 보겠습니다. 이 경우 실제로 필요한 것은 레이 1개뿐이고, 나머지 3개는 첫 번째 레이의 가시성 결과를 재사용하면 됩니다.
아주 멀리 떨어진, 아주 작은 에어리어 라이트에서도 비슷한 상황이 발생할 수 있습니다. 이 경우에는 페넘브라가 거의 없기 때문에, 레이들이 사실상 같은 경로를 따라가게 됩니다.
콘텐츠에서 이런 완전한 중복은 흔하지 않습니다. 아티스트는 보통 큰 에어리어 라이트를 선호하기 때문이죠. 하지만 이 최적화는 약간의 성능 이득을 제공하면서도, 품질에 부정적인 영향을 주지 않습니다.
또한 이 기법은 전체 페넘브라 크기를 줄이는 스케일 팩터로도 활용할 수 있습니다. 이 스케일 팩터를 줄이면, 에어리어 라이트의 실제 크기가 줄어드는 대신 더 많은 레이가 중복되어 스킵되므로, 트레이싱 비용이 감소합니다. 스팀 덱이나 상위 모바일 플랫폼처럼, 콘솔보다 훨씬 느리지만 같은 콘텐츠를 별도의 라이팅 작업 없이 그대로 돌리고 싶은 플랫폼에 좋은 타협점이 될 수 있습니다.

콘솔보다 더 낮은 성능 타깃으로 스케일 다운하는 또 다른 방법은, 픽셀 사이에서 레이를 재사용하는 것입니다.
픽셀 중앙에서만 셰이딩하는 대신, 주변 픽셀 몇 개를 찾아 그들이 쏜 레이를 재사용할 수 있는지 검사합니다.
우리는 픽셀까지의 거리와 레이 히트 거리 등을 기반으로 스크린 공간 커널 크기를 결정하고, 깊이 차이 같은 여러 휴리스틱을 추가로 사용합니다.
이렇게 이웃 픽셀에서 가져온 라이트는, 원래 픽셀에서 뽑혔을 확률과 다르기 때문에, 최종 결과를 다시 리웨이트(reweight)해야 합니다.
이 방식은 접촉 그림자를 약간 블러 처리하지만, 픽셀당 레이 수를 줄이면서도 라이트가 매우 안정적이고 날카롭게 유지되도록 해 줍니다.

이제 보이는 샘플들을 실제로 셰이딩할 준비가 되었습니다.
먼저 해당 픽셀에 기여하는 보이는 샘플들의 가중치를 누적합니다. 그다음, 누적된 가중치를 미리 적분된(pre-integrated) 라이트 조도(analytic/precomputed light illumination)에 곱해 줍니다.
미리 적분된 조도를 사용한다는 것은, 섀도잉과 셰이딩을 분리할 수 있다고 가정하는 셈입니다. 이는 엄밀히 말해 정확하지 않은 근사이며, [Heitz et al. 2018]의 도입부에서도 이 근사의 한계가 잘 정리되어 있습니다. 그럼에도 이 근사를 사용하면 특정 연산이 더 빠르고 덜 노이즈하게 됩니다.
무엇보다도 이 근사는 기존 조명 파이프라인과의 하위 호환성을 제공해 줍니다. 확률적 조명을 적용하더라도, 시각적으로는 기존 비확률적(non-stochastic) 라이트와 거의 동일하게 보입니다. 하이엔드 PC에서 ReSTIR를 사용하는 구현들이 이 전략을 채택하는 이유도 동일합니다.
물론 이는 장기적으로 개선하고 싶은 부분이기도 합니다. 특히 비게임(non-game) 용도에서는 섀도잉과 셰이딩을 더 정확하게 통합한 모델을 요구하는 경우가 많습니다.

셰이딩이 끝나면 디노이징 단계로 넘어갑니다.
TSR만으로도 꽤 괜찮은 결과를 얻을 수 있지만, 몇몇 영역에서는 전용 디노이저가 꼭 필요합니다.
우리는 모든 라이트를 합친 결과(조명은 2개의 렌더 타깃에 누적)를 대상으로 한 번의 디노이징 패스를 수행합니다. 이때 디퓨즈와 스페큘러 신호는 별도 채널로 나누어 디노이징합니다.
디노이징 전에 먼저 신호를 디모듈레이션(demodulate)합니다. 이는 머티리얼의 비확률적(non-stochastic) 부분을 제거하는 과정입니다. 알베도 같은 머티리얼 데이터는 블러되면 안 되기 때문입니다.
디노이저 핵심 아이디어는 SVGF에서 소개된 시간적 분산(temporal variance) 기반 필터링입니다. 우리는 픽셀별로 시간적 분산을 추적하고, 이를 신호의 "노이즈 정도"를 나타내는 지표로 사용합니다. 이 값에 따라 픽셀마다 공간 필터 강도를 다르게 적용해, 노이즈가 큰 영역만 더 강하게 블러합니다.

시간 필터 측면에서는, 먼저 히스토리를 재투영한 뒤 조명 값과 조명 휘도의 1차·2차 모멘트를 누적합니다.
조명은 32비트 렌더 타깃 두 개에 저장합니다. 하나는 디퓨즈, 다른 하나는 스페큘러 신호용입니다. 스토캐스틱 플로트 양자화(stochastic float quantization)를 사용해 밴딩이나 색상 쉬프트 같은 아티팩트를 방지한 덕분에, 64비트 레퍼런스와 비교했을 때도 크게 차이가 나지 않습니다.
휘도 모멘트(1차·2차)도 디퓨즈와 스페큘러 각각 별도로 저장합니다.
Float11, Float16 포맷은 꽤 넓은 표현 범위(-2^16 ~ 2^16)를 가지지만, 극단적인 상황에서는 이 범위를 넘어설 수 있습니다. 클램핑으로 인한 문제를 막기 위해, 우리는 모든 조명 값을 프리-익스포즈(pre-exposed) 공간에 저장합니다.
히스토리 보정을 위해 5×5 네이버후드 클램프를 사용합니다. 커널이 꽤 크기 때문에, 먼저 주변 값을 그룹 공유 메모리(groupshared)에 로드하고 패킹합니다.
히스토리 클램프는 YCoCg 색 공간에서 수행합니다. 이 색 공간을 사용하면, 히스토리를 보정할 때 발생할 수 있는 색상 쉬프트를 줄일 수 있습니다.
흥미로운 트릭 하나는, 새로운 데이터가 네이버후드 경계 밖에 있을 때, 그 경계로부터의 거리만큼 히스토리 속도를 높이는 것입니다. 이렇게 하면 고스팅(ghosting)을 크게 줄일 수 있습니다. 아이디어는 간단합니다. 새 데이터가 기존 히스토리와 아주 멀리 떨어져 있다면, 그 히스토리를 어떻게든 유지하려 하기보다 빠르게 버리는 편이 더 자연스럽다는 것입니다. 코드에서는 이런 식으로 구현합니다:

float3 ClampedHistoryDiffuseLighting = clamp(HistoryDiffuseLighting, DiffuseNeighborhood.Center - 
DiffuseNeighborhood.Extent, DiffuseNeighborhood.Center + DiffuseNeighborhood.Extent);
float NormalizedDistanceToNeighborhood = length(abs(ClampedHistoryDiffuseLighting - 
HistoryDiffuseLighting) / max(DiffuseNeighborhood.Extent, 0.1f));
float DiffuseConfidence = saturate(1.0f - NormalizedDistanceToNeighborhood);

이 DiffuseConfidence 값은 이후 히스토리에 누적할 수 있는 최대 프레임 수(즉, 히스토리의 가중치)를 줄이는 데 사용됩니다. 다만, 이 값이 0까지 떨어지지는 않도록 해서, 최소한 몇 프레임 정도는 항상 히스토리를 섞어 노이즈를 완화하도록 합니다.
또 하나는 다운샘플링을 사용할 때의 처리입니다. 재구성된 픽셀은 상대적으로 신뢰도가 낮기 때문에, 네이버후드 클램프를 더 느슨하게 적용해 줍니다. 이는 아주 작은 메탈 그레이팅 같은 고주파 지오메트리에서, 각 프레임마다 입력이 완전히 달라지는 문제를 완화해 줍니다. 이런 경우에는 디테일이 너무 세밀해서 서브픽셀 지터로 인해 프레임마다 전혀 다른 패턴이 나오곤 합니다.

마지막으로 공간 필터(spatial filter)를 한 번 더 돌려, 시간 필터 이후에도 남아 있는 노이즈를 정리합니다. 이 필터는 시간 히스토리 누적 이후에만 한 번 적용되며(히스토리 상에서 반복적으로 블러하지 않음), 덕분에 출력 결과를 더 날카롭게 유지하면서도 디소클루전 이후에 빠르게 수렴할 수 있습니다.
공간 필터는 표준 A-Trous 필터와는 조금 다릅니다. 여러 번의 무거운 A-Trous 패스를 거치는 대신, 한 번의 희소(sparse) 커널만 사용합니다. 대신 이 커널은 픽셀마다 회전되고, TSR(Temporal Super Resolution, UE의 TAA + 업스케일러)이 필터 홀(filter hole)을 메워 줍니다.
또한 상대적인 시간 분산이 높은 픽셀에만 이 공간 필터를 적용해서, 속도와 선명도를 동시에 챙깁니다. 노이즈가 거의 없는 영역은 굳이 완벽하게 깨끗할 필요가 없고, 남은 약간의 노이즈는 TSR이 처리해 줍니다.
강한 스페큘러 라이팅 같은 경우, 아주 작은 오차만으로도 수치적인 이유로 큰 절대 분산이 나타나, 실제로는 노이즈가 크지 않은데도 시간이 많이 흔들리는 것처럼 보일 수 있습니다. 이런 경우를 제대로 처리하기 위해, 우리는 절대 분산이 아니라 상대적인 분산을 사용합니다.
디소클루전 영역을 처리할 때는, 입력 픽셀이 일정한 시간 분산을 가진다고 가정합니다(별도로 추정하는 비용이 크기 때문입니다). 이 영역에서는 공간 샘플 수를 늘리고, 톤매핑된 공간에서 누적해서 파이어플라이를 제거합니다. 프레임이 누적되면서 이 가정들은 서서히 약해지고, 4프레임 정도의 히스토리를 쌓으면 디소클루전용 특수 처리는 완전히 꺼집니다.

디노이징에는 하나 더 중요한 문제가 있습니다. 디노이저는 보통 렌더링 해상도보다 낮은 해상도에서 동작하고, 그 결과를 업샘플한다는 점입니다.
이는 디노이저 히스토리의 한 픽셀이, 업샘플링 이후에는 여러 출력 픽셀에 대응된다는 뜻입니다. 이 경우, 서로 상관없는 픽셀들을 섞어 누적하면 라이팅이 퍼지면서 블러가 생기고, 반대로 이를 막으려면 매 프레임 히스토리를 급격히 버려야 해서 노이즈가 커집니다.
이 문제는 라이트별 디노이저(per-light denoiser)에서는 덜 두드러집니다. 이들은 일반적으로 섀도우 마스크나 레이셔 에스티메이터(ratio estimator)만 디노이징하고, 라이트 평가 자체는 매 프레임 비확률적으로 수행하기 때문입니다. 이 경우 문제는 대부분 페넘브라 주변에만 나타납니다. 하지만 여러 라이트를 합친 결과를 한 번에 디노이징하는 경우, 이 블러가 모든 라이팅에 영향을 줄 수 있고, 특히 얼굴처럼 사람이 민감하게 보는 영역에서 스페큘러 디테일이 쉽게 사라진다는 문제가 있습니다.
우리는 이를 완화하기 위해, 픽셀마다 가장 중요한 라이트를 찾아내어(per-pixel most important light), 그 라이트를 따로 분리하는, 라이트별 레이셔 에스티메이터와 유사한 기법도 실험했습니다. 하지만 실제로는 잘 동작하지 않았습니다. 라이트 선택 자체가 확률적이기 때문에, 이 선택 노이즈가 복잡한 라이팅 상황에서 그대로 화면에 드러나 버립니다. BRDF 자체를 분리해 내는 방식도 그리 좋지 않았습니다. 이 경우 시간·공간 필터가 전부 잘못된 값 위에서 동작하게 되기 때문입니다. 픽셀마다 "가장 중요한 라이트"가 다르고, 인접 픽셀 사이에서 그 선택이 불연속적으로 바뀌는 지점에서는 아주 이상한 아티팩트가 발생합니다.
이상적인 해결책은, 디노이저의 히스토리도 디스플레이 해상도에 맞추는 것입니다. 이렇게 하면 앞에서 말한 문제를 근본적으로 제거할 수 있습니다. 하지만 콘솔에서는 비용이 너무 큽니다. 실제 게임들에서는 1080p로 렌더링하고 4K로 업스케일하는 경우가 흔한데, 이 경우 디노이저에서 4K 해상도 전체에 대해 시간 필터를 돌려야 해서, 관련 패스 비용이 약 4배로 늘어납니다. 그래도 하이엔드 PC 같은 상위 플랫폼을 위한 스케일업 옵션으로는 충분히 연구할 가치가 있습니다.

앞서 살펴본 것처럼, 보통 한 픽셀의 실제 에너지는 소수의 중요한 라이트에서 대부분 나옵니다. 만약 우리가 한 픽셀의 전체 에너지 중 80% 정도를 직접 샘플링으로 잡을 수 있다면, 디노이징 강도를 줄이거나, 경우에 따라 TSR에 거의 원신호를 넘겨 줄 수도 있습니다.
이를 위한 꽤 좋은 휴리스틱이 있습니다. 보이는 라이트 리스트는, 특정 픽셀에 기여할 수 있는 전체 에너지의 합을 알려 줍니다. 그중 이번 프레임에 실제로 샘플링해 셰이딩한 라이트가 어느 정도인지 알 수 있다면, "충분히 많은" 에너지를 직접 샘플링했다는 판단을 내릴 수 있습니다. 예를 들어, 강한 스페큘러 하이라이트가 하나 있고, 이 라이트가 픽셀 에너지의 80% 이상을 차지하면서, 이번 프레임에 우리가 이 라이트를 샘플링했다면, 굳이 디노이저를 돌리지 않고 이 값을 그대로 TSR에 넘겨도 됩니다.
이 휴리스틱은 시간적으로 꽤 요동칠 수 있기 때문에, 약간의 시간 안정화(temporal accumulation)가 필요합니다. 어떤 프레임에는 중요한 라이트가 샘플링되고, 다른 프레임에는 그렇지 않을 수 있기 때문입니다. 하지만 전체적으로는 실전에서 잘 동작합니다. 이 기법은 출력 이미지를 더 날카롭게 만들 뿐 아니라, 스페큘러 하이라이트의 고스팅이나 블러 같은 흔한 디노이징 문제도 완화해 줍니다. 특히 평면이 아닌 표면에서 이런 문제를 제대로 재투영하기가 매우 어렵습니다.
슬라이드 상에서는 차이가 다소 미묘하게 보일 수 있지만, 실제로는 오른쪽처럼 잘린 이미지를 자세히 보면 상당히 큰 차이가 느껴집니다.
이제 발표를 Tiago에게 넘기겠습니다.

우리가 라이트 샘플의 가시성을 계산하는 기본 방법은 하드웨어 레이 트레이싱(HWRT)입니다. 이를 통해 프레임마다 수많은 섀도우 맵을 렌더링·관리해야 하는 부담을 줄일 수 있습니다. 특히 수백, 수천 개의 동적 라이트를 처리하는 경우에 큰 장점이 있습니다.
섀도우 퀄리티는 매우 중요하지만, 메인 뷰 렌더링에서 사용하는 모든 디테일을 그대로 BVH에 담을 수는 없습니다. 정점당 메모리 오버헤드도 크고, Nanite를 사용하는 UE 프로젝트는 기본적으로 지오메트리 디테일이 매우 높습니다. 그만큼 BVH 빌드와 트레이싱 비용도 커집니다. 여기에 키트배시(kitbash)로 구성된 환경은 인스턴스가 지나치게 많이 겹치는 경향이 있어서, 레이 트래버설 비용을 더 끌어올립니다.
그래서 우리는 단순화된 프록시 메시(proxy mesh), 공격적인 인스턴스 컬링, 집합적 표현(aggregate representation) 등을 사용해, 레이 트레이싱용 레벨 표현의 복잡도를 크게 줄일 필요가 있습니다.

우리는 레벨에 대해 두 가지 레이 트레이싱 표현을 사용합니다. ● 플레이어 카메라 주변 150m 반경의 근접 영역(near field)에는 프록시 메쉬를 사용합니다. ○ 원본 지오메트리를 적절히 단순화한 로우 폴리곤 메시로, 근거리 트레이싱에서 성능과 정확도 사이의 밸런스를 맞추는 역할을 합니다. ● 레이가 근접 영역 메시를 맞추지 못하고 반경 밖으로 나가면, 레벨의 더 공격적으로 단순화된 표현(far field representation)을 추가로 트레이스합니다. ○ 이 표현에서는 인스턴스를 병합하고, 삼각형 개수를 크게 줄여 레이 트래버설 비용을 낮춥니다. ○ 원거리 표현은 별도의 TLAS에 저장되기 때문에, BVH 복잡도가 낮아 그만큼 트레이스가 빠릅니다.
콘솔 세대에서는 인라인 레이 트레이싱이 더 좋은 성능을 보여 주기 때문에, 이를 기본 모드로 사용합니다.

프록시 메쉬를 레이 트레이싱에 사용하면, 가장 큰 단점은 프록시와 실제 래스터라이즈 지오메트리 사이의 불일치(mismatch)입니다.

이런 불일치는 잘못된 자기 그림자(self-shadowing)를 유발합니다. 레이는 래스터라이즈된 표면에서 시작하지만, 곧바로 프록시 메쉬의 삼각형에 맞을 수 있기 때문입니다.
이 스크린샷에서 보면, 플레이어 앞 벽에 잘못된 그림자가 나타납니다. 이 메시는 테셀레이션을 사용하고 있는데, 이를 레이 트레이싱에서 효율적으로 표현하기 어렵기 때문입니다. 바닥 역시 프록시와 실제 지오메트리의 차이로 인해 과도한 자기 그림자가 생깁니다. 플레이어 캐릭터 역시 마찬가지입니다. 레이 트레이싱 표현이 부정확한 시뮬레이션 천(cloth) 메쉬로 인해 잘못된 그림자가 발생합니다.

애니메이션 지오메트리나 알파 마스크 지오메트리 역시 비슷한 문제를 유발합니다. 이런 지오메트리는 특히 정확하게 표현하고 레이를 쏘는 데 비용이 많이 듭니다.

안타깝게도, 단순히 레이에서 프론트 혹은 백 페이스 컬링을 사용한다고 해서 이런 불일치를 모두 해결할 수는 없습니다. 왼쪽 다이어그램에서는 백 페이스 컬링만으로 잘못된 오클루전을 막을 수 있지만, 오른쪽처럼 프록시의 프론트 페이스에 레이가 맞는 경우에는 여전히 잘못된 오클루전이 발생합니다.
게다가 프론트/백 페이스 컬링을 사용하면, 레이 트래버설 중의 조기 종료(early exit) 기회를 줄여 약 10% 정도의 오버헤드가 추가됩니다.
결국 프록시 메쉬에만 의존할 수는 없고, 이런 문제를 완화하기 위한 폴백 메커니즘이 필요합니다.

우리가 잘못된 자기 그림자를 피하기 위한 핵심 방법은 스크린 공간 레이 트레이싱(screen space ray tracing)입니다. 스크린 레이는 래스터라이즈 지오메트리의 깊이 버퍼를 기준으로 트레이싱되기 때문에, 프록시 지오메트리와 상관없이 실제 표면에서 멀어지는 방향으로 움직일 수 있습니다. 그런 다음, 다이어그램처럼 스크린 레이가 도달한 지점을 기준으로 월드 레이의 시작점을 오프셋하면, 대부분의 자기 그림자 문제를 피할 수 있습니다.
추가 보너스로, BVH에 충분히 표현되지 않는 세밀한 컨택트 섀도우도 얻을 수 있습니다. 다만 일반적인 컨택트 섀도우와 달리, 여기서는 픽셀 단위로 매우 정확한 트레이싱이 필요합니다. 보통 컨택트 섀도우는 짧은 거리와 소수의 샘플만으로도 충분한 경우가 많지만, 여기서는 레이 길이가 월드 공간에서 정의되기 때문에, 상황에 따라 화면 전체를 가로질러야 할 수도 있습니다. 이런 경우에는 고정 스텝만으로는 너무 비싸집니다.

우리는 HZB(Hierarchical Z-Buffer) 트레이싱을 사용해, 모든 픽셀을 일일이 검사하지 않고도 빈 공간을 빠르게 건너뛰면서 정확한 스크린 레이를 구현합니다.
또한 흔한 스크린 공간 아티팩트를 최대한 피하기 위한 몇 가지 전략을 사용합니다. ● 기본적으로 매우 보수적인, 낮은 표면 두께 값을 가정합니다. 대부분의 경우, 레이가 표면 뒤로 가면 교차가 있었다고 가정하지 않고, 하드웨어 레이 트레이싱으로 폴백합니다. ● 화면 전체를 가로지르는 긴 스크린 레이는, 깊이 버퍼 앨리어싱으로 인한 아티팩트를 유발할 수 있습니다. 실전에서는 이를 피하기 위해 레이 길이에 제한을 둡니다. ● 깊이 버퍼를 기준으로 레이를 쏘면, 그 자체로 새로운 자기 그림자 이슈를 만들기도 합니다. 이를 막기 위해 포인트 샘플링과 바이리니어 샘플링을 혼합해 깊이 버퍼를 참조합니다.
추가로, 인스턴스가 레이 트레이싱 표현에 포함되는지 여부를 스텐실 비트로 트래킹합니다. 레이가 그런 인스턴스 뒤로 넘어가면, 추정 표면 두께를 늘려서 그 인스턴스들이 컨택트 섀도우에 조금 더 기여할 수 있도록 합니다.

GI나 리플렉션에서는, 삼각형을 축소하거나 알파 마스크 기준으로 컬링하는 등 다양한 트릭을 BVH 빌드에 적용해 오클루전을 싸게 근사할 수 있습니다. 하지만 직접 그림자(direct shadow)에는 이런 기법들이 잘 맞지 않습니다.
이상적인 해결책은, 인라인 레이 트레이싱에서도 직접 사용할 수 있는 "고정 기능" 알파 마스크를 지원하는 것입니다. 하지만 아티스트가 머티리얼 그래프를 통해 알파 마스크를 자유롭게 구성할 수 있기 때문에, 실제로는 실용적이지 않습니다. 결국 우리는 다른 선택지들로 눈을 돌려야 합니다. ● 모든 레이를 Any-hit 평가로 트레이싱할 수도 있습니다. 이렇게 하면 정확한 결과를 얻을 수 있지만, 인라인 레이 트레이싱의 장점을 전혀 활용할 수 없어서 콘솔에서는 너무 비싸집니다. ● 두 번째 선택지는, 먼저 가벼운 인라인 레이를 트레이싱한 뒤, 머티리얼 평가가 필요한 인스턴스를 맞았을 때에만 Any-hit 평가가 활성화된 "연속 레이(continuation ray)"를 다시 쏘는 것입니다. 이렇게 하면 인라인 트레이싱의 성능 이득을 유지하면서, 필요한 부분에서만 정확한 알파 마스크 처리를 할 수 있습니다.

그래도 여전히, 현재 세대 하드웨어에서는 하드웨어 레이 트레이싱이 실용적이지 않은 콘텐츠와 상황이 존재합니다. ● Any-hit 셰이더를 많이 돌려야 하는 경우, 특히 조밀한 수풀이 겹겹이 쌓여 있는 상황에서는, 오버헤드가 너무 큽니다. ● 혹은 애니메이션 인스턴스를 대량으로 사용하는 콘텐츠에서 BVH 빌드에 필요한 메모리와 시간 자체가 감당하기 어렵습니다.
이런 경우를 위한 폴백으로, 우리는 특정 라이트에 대해 하드웨어 레이 트레이싱 대신 VSM(Virtual Shadow Maps)을 사용할 수 있도록 했습니다.
VSM은 우리 목적에 잘 맞는 기능을 제공합니다. ● 그 자체로 그럴듯한 소프트 섀도를 생성할 수 있어, 다른 라이트에서 온 레이 트레이스 섀도우와도 자연스럽게 섞입니다. ● 라이트 샘플링 패스에서는, 실제로 샘플된 페이지에만 표시를 남겨, 섀도우 맵 렌더링 시 불필요한 작업을 줄일 수 있습니다.
이 폴백 메서드는 이진 선택일 필요는 없습니다. 아티스트가 특정 인스턴스를 섀도우 맵에만 렌더링하도록 태그하고, 레이 트레이싱에 잘 표현되는 인스턴스는 HWRT에 맡기는 식의 하이브리드 구성이 가능합니다.
다만, 이 폴백은 섀도우 맵의 스케일 문제를 해결해 주지는 않는다는 점이 중요합니다. BVH는 어차피 Lumen 때문에 한 번은 구축해야 하고, 이후 라이트별 추가 오버헤드는 거의 없습니다. 반면 VSM에서는 라이트마다 별도의 섀도우 맵 준비 비용이 붙고, 아무리 복잡한 캐싱 스킴을 사용하더라도, 많은 라이트가 있으면 60Hz 예산 바깥으로 밀려나기 쉽습니다. 그래서 실제로는 방향성 라이트에서만 주로 사용합니다. 이 라이트는 멀리까지 날카로운 섀도를 요구하는 경우가 많고, 이를 BVH 하나로 모두 커버하려면 매우 비싸기 때문입니다.

지금까지의 내용을 하나의 다이어그램으로 정리하면 다음과 같습니다. ● 트레이싱 파이프라인의 코어는 스크린 트레이스입니다. 항상 스크린 트레이스를 먼저 수행해 잘못된 자기 그림자를 피하고, 이후 대부분의 레이는 근접 영역 트레이스로 처리합니다. ● 레이가 알파 마스크 머티리얼이 적용된 인스턴스를 맞으면, 필요 시 머티리얼 평가가 활성화된 연속 레이를 다시 쏩니다. ● 레이가 근접 영역 반경 밖으로 나가면, 원거리 표현(far field)에 대해 다시 트레이스를 수행합니다. ● 마지막으로, VSM을 사용하는 라이트에 대해서는 섀도우 맵을 샘플링합니다.
각 단계 사이에서는 항상 컴팩션(compaction)을 수행해, 워크로드의 코히어런시를 유지합니다. 이 컴팩션 패스는 16×16 트레이싱 타일 단위로 그룹을 구성해, 최대한 효율을 끌어냅니다.

실제로는 수천 개의 라이트를 쓰는 게임이 그리 많지는 않습니다. 그래서 UE처럼 성숙한 엔진에서도, 라이트 수를 극단적으로 늘려 보기 전까지는 남아 있는 성능 여유가 꽤 많았습니다.
예를 들어, 라이트 수를 시각화하는 히트맵의 기본 상한이 8개였는데, 이 값만 봐도 당시 엔진이 상정하던 라이트 요구 사항을 짐작할 수 있습니다. 또한 아티스트들이 점점 더 복잡한 라이트 타입(예: 직사각(rect) 라이트, 텍스처드 라이트)을 사용하기 시작하면서, 엔진 전체가 이런 라이트를 효율적으로 처리할 수 있어야 했습니다.

우리가 기존에 쓰던 라이트 그리드(light grid) 구현은, 수천 개의 라이트 규모로 스케일 업했을 때 압박을 심하게 받기 시작했습니다. 그래서 라이트 그리드 빌드 로직을 개선해, 대량의 라이트를 더 잘 처리하도록 했습니다.
슬라이드에 요약된 새 구현 덕분에, MegaLights 데모에서 라이트 그리드 빌드 시간은 0.6ms 이상에서 0.2ms 이하로 감소했습니다. 이 분야에는 지난 10년 동안 이미 매우 효율적인 기법들이 다수 발표되었기 때문에, 원한다면 더 최적화할 여지도 충분합니다.
우리는 기존의 프로젤(froxel) 기반 구조를 유지하면서도, 라이트 인젝션을 두 개의 패스로 나누었습니다. 첫 번째 패스에서는 코스 그리드(coarse grid)에 라이트를 컬링합니다. 이때는 셀당 하나의 스레드 그룹을 사용합니다. 모든 라이트를 단일 스레드로 순회하는 것은 너무 느리기 때문입니다. 두 번째 패스에서는, 코스 그리드 결과를 바탕으로 실제 렌더링에 사용하는 메인 그리드에 라이트를 컬링합니다. 이 단계에서는 각 셀에서 확인해야 할 라이트 개수가 훨씬 줄어들기 때문에, 셀당 한 개 스레드로도 충분한 경우가 많습니다. 플랫폼에서 32레인 웨이브를 지원한다면, 셀당 전체 스레드 그룹과 단일 스레드 사이에서 중간 지점 역할을 할 수도 있습니다.
라이트 그리드 셀마다 최악의 경우를 가정해 메모리를 할당하면 낭비가 크기 때문에, 우리는 각 셀의 라이트 리스트를 촘촘하게(packed) 저장해야 합니다. 이를 위해서는 셀마다 관련 라이트 수를 먼저 세고, 버퍼에서 공간을 할당한 뒤, 라이트 인덱스를 기록해야 합니다. 전역 라이트 리스트를 두 번 순회(한 번은 개수 세기, 한 번은 기록)를 피하기 위해, 우리는 먼저 각 셀에서 관련 라이트를 LDS에 기록하고, 오버플로는 전역 메모리에 있는 연결 리스트로 처리합니다. 그 뒤, 그리드 버퍼에 공간을 할당하고, LDS + 오버플로 리스트의 내용을 실제 버퍼로 복사합니다.
실제 장면에서는 대부분의 라이트 그리드 셀이 가려져 있기 때문에, HZB 컬링을 활용해 라이트 그리드 빌드 비용을 크게 줄입니다. 프로젤 그리드는 비선형 구조이기 때문에, 카메라에서 멀어질수록 셀 크기가 커지고, 멀리 있는 셀일수록 많은 라이트에 영향을 받게 됩니다. 그 결과 원거리 셀은 메모리 대역폭이 병목이 되기 쉬운데, HZB 컬링을 통해 이런 셀에 대한 작업을 많이 줄일 수 있습니다.

타이트한 컬링 바운드를 갖는 것은 성능 측면에서 매우 중요합니다. 이 분야 역시 지난 수년간 많은 연구가 이루어졌고, 다양한 측면을 다루는 논문들이 발표되었습니다. 다만 그 과정에서 직사각 라이트(rect light)는 상대적으로 덜 주목받는 경향이 있었던 것 같습니다. 게임에서의 사용 빈도가 낮았기 때문일 것입니다. 우리는 바 도어(barn door)를 고려해 컬링 바운드를 더 조밀하게 만들면, 이 라이트들이 영향을 미치는 셀 개수를 크게 줄일 수 있다는 것을 확인했습니다.

셰이더의 레지스터 압박과 점유율(occupancy)도 항상 신경 써야 하는 요소입니다. 하나의 셰이더가 다양한 머티리얼 타입과 라이트 기능을 모두 처리해야 하기 때문입니다. 이 문제를 완화하기 위해, 우리는 타일을 머티리얼 타입과 해당 타일에 영향을 주는 라이트 타입, 두 기준으로 분류합니다. 이렇게 하면 최악의 VGPR 사용량을 감수해야 하는 영역을 최소화할 수 있습니다. 특히 직사각 라이트와 텍스처드 라이트를 지원하는 로직은 오버헤드가 상당히 크기 때문에, 이들을 위한 전용 셰이더 퍼뮤테이션을 따로 두었습니다. MegaLights 데모에서는, 이런 라이트 타입 기반 분류를 추가한 것만으로도, 비교적 작은 오버헤드로 점유율을 약 20% 향상시킬 수 있었습니다. 라이트 그리드 빌드 단계에서, 각 셀에 어떤 라이트 타입이 영향을 미치는지 이미 추적하고 있었기 때문에, 이 정보를 그대로 재활용하면 됩니다.
여기까지가 불투명(opaque) 파이프라인에 대한 내용이고, 이제 반투명(translucency)과 볼류메트릭 포그(volumetric fog)로 넘어가겠습니다.

UE에서는 볼륨 라이팅을 두 가지 구조에 저장합니다.
● 볼류메트릭 포그는 카메라 정렬 프로젤 그리드(camera-aligned froxel grid)에 저장합니다.
● 파티클 효과용 볼륨은 카메라 주변 월드 공간 그리드에 저장되며, 각 보xel에 2밴드 구면 조화(SH)를 기록합니다.
이 구조들은 매 프레임 업데이트되며, 기존에는 주로 섀도우 맵을 이용해 라이트를 인젝션했습니다. 그러나 레이 트레이스 섀도우에 주로 의존하기 시작하면, 사용할 수 있는 섀도우 맵이 없고, 수많은 라이트를 이 구조에 직접 인젝션하는 것도 매우 비싸집니다. 그래서 우리는 이 볼륨 구조들이 MegaLights 시스템과 연동되도록 확장했습니다.

불투명 픽셀에서 사용한 일반적 접근법은 볼륨에도 적용할 수 있습니다.
● 반 해상도에서 샘플링을 수행하고, 보xel마다 N개의 라이트 샘플을 선택합니다.
● 섀도우 레이를 트레이싱합니다.
● 볼류메트릭 포그에는 위상 함수(phase function), 반투명 볼륨에는 디퓨즈 BRDF를 사용해 보이는 샘플을 셰이딩합니다.
● 마지막으로, 다음 프레임을 위한 보이는 라이트 리스트를 구축합니다. 이 과정은 각 볼륨에 대해 한 번씩, 총 두 번 수행해야 합니다.

하지만 이렇게 단순하게 적용하면 몇 가지 문제가 생깁니다.
● 우선, 특히 월드 공간 그리드의 경우, 프로브가 상당히 드문 편입니다. 그 결과 인접 보xel에서 가시성을 모아 가이딩을 하려 하면, 섀도우가 고주파인 영역에서는 가이딩 효율이 크게 떨어집니다.
● 두 번째 문제는, 두 볼륨 사이의 중복 작업입니다.
○ 우리는 이미 월드 곳곳의 프로브에서 중요한 라이트를 뽑고, 섀도우 레이를 트레이싱하고 있습니다.
○ 스크린샷에서 보듯, 두 볼륨의 프로브 커버리지는 상당 부분 겹칩니다.

한 가지 아이디어는, 프로젤 그리드 구조 자체에 SH를 추가해, 파티클과 반투명 오브젝트도 이 그리드를 통해 라이팅을 받게 하는 것입니다. 이렇게 하면 두 볼륨 사이의 중복 작업을 줄이고, 더 높은 밀도의 프로젝션 덕분에 반투명 라이팅 가이딩 효율도 높일 수 있을 것처럼 보입니다.
하지만 실제로 해 보니, 몇 가지 눈에 띄는 아티팩트가 생겼습니다.
● 무엇보다도 프로젤 그리드의 구조 자체가 너무 드러납니다. 특히 카메라가 움직일 때 이런 구조적인 패턴이 매우 거슬립니다.
○ 월드 공간 그리드에서도 비슷한 문제가 생기긴 하지만, 그리드는 카메라와 무관하게 고정되어 있기 때문에, 시각적으로 덜 방해가 됩니다.
● 시간 필터링(temporal filtering)도 필요하지만, 프로젝션 오류로 인해 재투영에서 완전히 피할 수 없는 오차가 누적됩니다.

그래서 우리는 두 볼륨에서 샘플링과 트레이싱만 공유하는 하이브리드 솔루션을 택했습니다.
샘플링은 프로젤 위치를 기준으로 수행합니다. 프로젤은 상대적으로 촘촘하기 때문에, 인접 프로브 사이에서 가시성을 공유하더라도 가이딩 효율이 크게 떨어지지 않습니다. 그 뒤, 이 샘플들에 대해 레이를 트레이싱하고, 마지막에는 두 개의 별도 셰이딩 패스를 돌립니다. 하나는 볼류메트릭 포그, 다른 하나는 반투명 볼륨용입니다. 각 볼륨은 가장 가까운 샘플링 프로브에서 확률적으로 샘플을 가져와 셰이딩합니다.
이 방식 덕분에, 샘플링과 트레이싱을 두 번 돌릴 필요가 없어져, 볼륨 빌드에 드는 시간을 약 25% 절감할 수 있었습니다.

앞서 불투명 파이프라인을 설명할 때, 보통 한 픽셀의 에너지 중 80% 정도가 소수 라이트에서 나온다고 언급했습니다.
불투명 표면에서는, 샘플링 단계에서 이미 노멀과 머티리얼 속성을 알고 있기 때문에, 픽셀당 적은 샘플만으로도 충분히 좋은 결과를 얻을 수 있습니다. 하지만 반투명 볼륨에서는 상황이 좀 더 까다롭습니다.
우리는 라이트를 SH 형태로 보xel에 주입하고 있고, 어떤 표면이 나중에 어떤 보xel을 참조할지는 미리 알 수 없습니다. 프로젤은 픽셀에 비해 상당히 크기 때문에, 이웃 보xel에서 확률적으로 샘플을 가져오는 업샘플링은 쉽게 불안정해지거나, 안정성을 위해 더 넓은 필터링이 필요합니다. 샘플링은 두 볼륨을 통합한 공간에서 수행되기 때문에, 위상 함수와 반투명 BRDF를 모두 고려해야 하고, 그 결과 라이트 가중치가 불투명 픽셀에 비해 더 균일해지는 경향이 있습니다.
이 모든 요인이 겹치면서, 불투명 픽셀에 비해 보xel당 더 많은 샘플을 셰이딩해야, 안정적이고 노이즈가 적은 결과를 얻을 수 있습니다.

간단한 해결책은 보xel당 샘플 수를 늘리는 것입니다. 하지만 이렇게 하면 파이프라인의 모든 단계 비용이 동시에 증가합니다.
대안으로, 우리는 이미 불투명 셰이딩에서 사용 중인 확률적 업샘플링을 확장했습니다. 단일 이웃에서만 샘플을 가져오는 대신, 여러 이웃 보xel에서 샘플을 모아 셰이딩하는 방식입니다.
이 접근법은 샘플링과 트레이싱에 추가 오버헤드를 만들지 않고도, 더 많은 샘플을 활용할 수 있게 해 줍니다. 대신, 보이는 영역의 섀도잉 디테일이 다소 흐려지는 트레이드오프가 있습니다.
마지막으로, 볼륨에 대해서도 시간·공간 필터링을 적용해 안정성을 높입니다.

유리 같은 일부 반투명 재질은 높은 품질의 스페큘러 라이팅을 요구합니다.
가장 직관적인 방법은, 앞쪽 레이어를 가장 중요한 표면으로 간주하고, 그 표면에 대해 MegaLights 파이프라인 전체를 한 번 더 돌려 정확한 조명을 계산하는 것입니다. 하지만 이는 실전에서는 너무 비쌉니다. 플레이어가 유리 표면 가까이에 가기만 해도, 전체 직접 조명 비용이 거의 두 배로 뛰어버릴 수 있기 때문입니다.
동시에 우리는 여러 레이어를 동시에 셰이딩해야 하는 요구 사항도 있습니다. 이를 보다 스케일 가능한 방식으로 해결해야 합니다.
현재는 저렴한 방법으로, 반투명 볼륨을 활용해 이런 표면들을 라이팅합니다. 이미 SH 형태로 저장된 라이팅을 기반으로 주요 방향(dominant direction)을 추출하고, 이를 스페큘러 조명을 근사하는 데 사용합니다. 단순한 경우에는 잘 동작하지만, 스크린샷에서 보듯, 하나의 스페큘러 하이라이트만 표현할 수 있다는 한계가 있고, SH 라이팅 자체가 충분히 안정적이지 않은 경우에는 스페큘러가 눈에 띄게 흔들릴 수 있습니다.
현재까지는 이 방법을 사용하고 있지만, 이런 종류의 반투명 표면에서 더 높은 조명 품질을 제공하는 것은 앞으로도 계속 연구할 영역입니다.

우리는 현재 얇은(thin) 표면을 대상으로 한 트랜스미션(transmission)도 지원하고 있으며, 이는 특히 수풀 같은 식생 라이팅 퀄리티를 크게 향상시킵니다.
트랜스미션은 때때로 메시의 뒷면에서 라이트를 향해 레이를 쏴야 하는데, 이 경우에는 몇 가지 추가 조정이 필요합니다. 먼저 스크린 공간 레이는, 지오메트리 뒤쪽으로 레이를 보낼 수 없기 때문에 비활성화합니다. 대신 바이어스를 조정해 레이를 라이트 방향으로 조금 이동시킵니다. 이렇게 하면 레이가 잎사귀 지오메트리에 바로 다시 맞는 대신, 그 뒤쪽으로 빠져나갈 수 있습니다.
메시 뒷면에서 레이 히트 지점까지의 거리는, 트랜스미션 항에 넣을 수 있는 두께 추정값을 제공합니다.

이제 전체 시스템이 런타임에서 어느 정도 성능을 내는지 살펴보겠습니다.
슬라이드에 있는 수치는 MegaLights 데모를 PlayStation 5에서 1080p 렌더 해상도, 픽셀당 1 샘플, 비동기 컴퓨트 비활성화 상태에서 실행한 결과입니다. 화면에는 900개가 넘는 라이트가 있으며, 픽셀당 20~80개 정도의 라이트가 영향을 줍니다.
직접 조명 전체(섀도잉 + 셰이딩)에 드는 총 비용은 약 5.5ms입니다.

흥미로운 점 중 하나는, 반 해상도로 돌렸음에도 불구하고, 불투명 표면의 샘플링 비용이 셰이딩보다 더 크다는 것입니다. 이는 샘플링 비용이 라이트 개수에 비례하기 때문입니다. 이 데모는 장면에 매우 많은 라이트를 사용하고 있고, 그 중 상당수가 비용이 큰 직사각 라이트입니다. 이 라이트들은 샘플링 비용도 높고, 셰이더의 점유율도 떨어뜨립니다. 반면 셰이딩은 픽셀당 셰이드하는 샘플 수에 상한을 두고 있기 때문에, 비교적 안정적인 비용을 유지합니다. 셰이딩 비용은 주로 타일 타입에 의해 결정됩니다.

또 하나 눈여겨볼 점은, 볼륨 반투명 셰이딩 비용이 불투명 셰이딩과 거의 비슷하다는 것입니다. 보xel 수는 화면 픽셀 수보다 훨씬 적음에도 말이죠. 이는 앞서 언급했듯, 안정성과 노이즈 감소를 위해 보xel당 더 많은 샘플을 셰이딩해야 하기 때문입니다.

전체적으로 보면, 아티스트가 MegaLights를 사용해 보기 시작하면, 얼마나 즐겁게 작업하는지 금방 느낄 수 있습니다.
MegaLights 데모 제작 과정에서도, 아티스트가 새로운 마인드를 익혀 가는 과정이 인상적이었습니다. 처음에는 조심스럽게, 여기저기 몇 개의 라이트만 배치하다가, 조금씩 자신감을 얻으면서 레벨 곳곳에 더 많은 라이트를 사용하기 시작했습니다.

곧이어, 이 스크린샷에서 보이듯, 직사각 라이트 사용도 급격히 늘어났습니다.

우리는 아티스트들이 메쉬를 활용해 라이트의 모양을 만드는 콘텐츠를 많이 사용하고 있다는 것도 발견했습니다. 이런 라이트는 샘플링과 해결(resolve)이 모두 어렵습니다.
이상적인 경우라면, 라이트 모양은 라이트 함수(light function)를 통해 정의하고(샘플링 패스에서 저렴하게 평가 가능), 실제 피스처 지오메트리는 레이 엔드 바이어스(ray end bias)를 라이트별로 지정해 스킵하는 편이 낫습니다.
실전에서는 이런 메쉬 기반 피스처도 큰 문제 없이 사용할 수 있었기 때문에, 데모는 그대로 이 콘텐츠를 포함한 채로 출하했습니다. 다만 아티스트에게 이런 지오메트리를 BVH에 수동으로 포함하도록 요청해, 거리에서 자동 컬링되면서 빛샘이 생기거나, 전체 라이팅 밸런스가 크게 변하지 않도록 했습니다.

어느 시점부터 아티스트는 이 시스템에 완전히 익숙해졌고, 스플라인을 따라 자동으로 라이트를 배치하는 절차적 라이트 스포너(procedural light spawner) 같은 툴도 적극적으로 사용하기 시작했습니다.

전체 시스템은 훨씬 더 잘 스케일하며, 라이트 콘텐츠도 훨씬 관대하게 받아들입니다. 레벨을 살펴보다 보면, 벽 안쪽에 텍스처드 에어리어 라이트가 숨어 있다거나, 하나의 IES 프로파일 라이트 대신 여러 개의 라이트로 비슷한 효과를 만든 경우처럼, 이전 파이프라인에서는 치명적이었을 수 있는 콘텐츠도 발견됩니다.
물론 확률적 라이트 샘플링이 마법의 해결책은 아닙니다. 라이트는 여전히 샘플링 패스 성능을 깎아 먹거나, 장면 노이즈를 늘립니다. 감쇠 범위, 콘 앵글, 바 도어와 같은 콘텐츠 최적화는 여전히 중요합니다.

정리하자면:
레이 트레이싱은 콘솔처럼 리소스가 제한된 플랫폼에서도, 직접 조명을 해결하는 완전히 새로운 접근법을 가능하게 합니다.
물론 여전히 품질상의 한계는 존재합니다. ● 메쉬를 레이 트레이싱에서 얼마나 잘 표현할 수 있는지 ● 대부분의 경우 픽셀당 1 샘플로 제한된다는 점에서, 샘플링과 디노이징 설계가 매우 중요합니다.
또한 이 기법의 성능은, 픽셀당 샘플링하는 라이트 개수와 레이 트레이싱 장면 복잡도에 비례해 증가합니다.
제약 조건만 놓고 보면 꽤 무서워 보이고, 여전히 사용할 수 있는 라이트 수에는 제한이 있습니다. 하지만 실제로는 꽤 괜찮은 타협점이 되었습니다. 아티스트는 시스템이 깨지기 전까지 훨씬 자유롭게 라이트를 배치할 수 있게 되었고, 확률적 기법은 성능과 품질 사이를 연속적으로 조절하기에 탁월합니다. 덕분에 아티스트가 수동으로 라이트를 정리하거나, 여러 스케일러빌리티 레벨마다 별도 라이팅 세트를 유지해야 하는 부담이 크게 줄었습니다.
BVH의 한계는 여전히 부담스럽지만, 실제 콘텐츠에서 아티스트는 대개 에어리어 라이트를 사용하는 경향이 있고, 이 경우 레이는 주로 라이트 주변에서만 높은 디테일을 필요로 합니다. 레이 첫 구간(first segment)은 스크린 공간 트레이싱으로 꽤 잘 처리할 수 있기 때문에, 로컬 라이트에는 이 조합이 현실적인 솔루션이 됩니다. 방향성 라이트는, 아주 먼 거리까지 날카로운 섀도를 요구하는 경우가 많아 BVH 기반 솔루션만으로는 부담이 큽니다. 이때는 VSM으로 폴백하거나, 레이 트레이싱과 VSM을 혼합한 하이브리드 구성이 유용합니다.
빠른 카메라 이동 중의 시간 아티팩트도 모든 확률적 라이트 샘플링 기법에서 피하기 어려운 부분입니다. 이런 기법들은 기본적으로 여러 프레임에 걸쳐 데이터를 누적·재사용하는 전제를 깔고 있기 때문입니다. 라이트 수가 적고 라이트 소스 크기가 작다면, 픽셀마다 가장 중요한 라이트 하나만 샘플링해도 거의 즉각적인 응답을 얻을 수 있어 문제가 덜합니다. 하지만 라이트가 크거나, 한 픽셀에 중요한 라이트가 여러 개 있는 상황에서는 여러 프레임에 걸친 데이터가 필요하고, 빠른 움직임 중에는 그 데이터를 충분히 모으지 못합니다. 그럼에도 대부분의 게임에서는 이 정도 품질이면 충분하고, 보통 디노이저가 이런 아티팩트를 잘 숨겨 줍니다. 고급 GPU에서는 픽셀당 더 많은 레이를 쏠 수 있기 때문에, 이런 문제는 한층 덜 두드러집니다.

앞으로의 과제로는, MegaLights를 실제 프로덕션에서 더 잘 활용할 수 있도록 만드는 작업이 남아 있습니다. 아직 최적화해야 할 부분, 수정해야 할 이슈, 그리고 구현해야 할 기능들이 여럿 있습니다.
예를 들어, BRDF 샘플링과 명시적 샘플링을 함께 사용하는 방향을 연구 중입니다. 명시적 샘플링은 작거나 멀리 있는 라이트를 샘플링하는 데 강점이 있고, BRDF 샘플링은 큰 라이트와 반사 BRDF에서 유리합니다. 우리는 이미 Lumen 레이를 통해 BRDF 기반 레이를 가지고 있으므로, 이를 재활용해 직접 조명 품질을 높일 수 있습니다.
또한 포워드 셰이딩을 더 잘 지원하기 위한 개선도 진행 중입니다. GPU 기반 피드백 메커니즘을 사용해, 실제로 렌더링되는 포워드 셰이딩 표면을 기반으로 샘플 가이딩을 수행하려는 방향입니다. 동시에 이 기술을 실제로 사용 중인 라이선시들로부터 피드백을 수집해, MegaLights 디버깅과 최적화를 위한 실용적인 툴링을 준비하고 있습니다.
저사양(특히 모바일)으로의 스케일링은 여전히 열려 있는 문제입니다. 일부 타이틀은 매우 넓은 하드웨어 스펙을 타깃으로 삼고 있는데, 저렴한 모바일 기기부터 하이엔드 PC까지 동일한 콘텐츠를 제공하고 싶어 합니다. MegaLights를 기준으로 콘텐츠가 제작되고 나면, 아티스트의 수작업 없이 라이트 수를 줄이기가 매우 어렵고, 우리는 아티스트가 여러 플랫폼마다 별도의 라이팅 세트를 유지해야 하는 상황을 만들고 싶지 않습니다.
또 다른 중요한 영역은 디노이징과 업샘플링입니다. 현재 MegaLights는 내부 디노이저를 돌린 뒤, 그 출력을 TSR로 넘깁니다. 이 구조는 여러 가지 도전을 불러옵니다. 우리는 두 시스템을 더 잘 통합하는 방향을 연구 중입니다. 예를 들어 내부 디노이저에서 TSR로 추가 정보를 넘기거나, 아예 모든 디노이징을 TSR로 옮기는 식입니다. 이 분야에서는 이미 ML을 활용한 새로운 연구들이 많이 나오고 있으며, 매우 유망한 결과들을 보여 주고 있습니다.

미래를 향한 가장 큰 도전은, 하드웨어 레이 트레이싱에서의 메쉬 표현입니다.
지금까지는 비교적 저폴리 프록시 메쉬와 각종 트릭으로 버틸 수 있었습니다. 디퓨즈 GI나 거친 리플렉션은 상당히 관대하기 때문입니다. 하지만 직접 조명 섀도우는, 레이 트레이싱용 메쉬 표현의 단점을 아주 쉽게 드러냅니다.
동적 지오메트리는 여전히 거의 풀리지 않은 문제이며, 메모리와 성능 예산을 매우 빠르게 소진시킬 수 있습니다. 수많은 애니메이션 인스턴스를 정확하게 표현하는 것은 현재로서는 거의 불가능합니다.
키트배시가 많이 쓰인 콘텐츠나, 내부가 대부분 비어 있는 거대한 메쉬(예: 스카이박스, 단일 메쉬로 만든 동굴)는 하이엔드 GPU에서도 레이 트레이싱 성능 문제를 일으키기 쉽습니다. 이론적으로는 BVH 리브레이딩(rebraiding)으로 해결할 수 있지만, 리브레이딩은 비용이 크고, 메모리 요구량도 크게 늘립니다. 이미 BVH 빌드 타임과 BVH 자체의 메모리 오버헤드가 큰 부담인데, 이를 더 키우기는 어렵습니다.
Nanite(UE의 가상화 지오메트리 파이프라인)를 통해, 우리는 래스터라이제이션 측면에서는 대부분의 메쉬 최적화 문제를 감출 수 있었습니다. 하지만 레이 트레이싱 제약 조건은 이런 문제를 다시 전면으로 끌어올립니다. 아티스트가 BVH 표현을 위해 지오메트리를 신중하게 최적화해야 한다는 뜻입니다.
최근에는 레이 트레이싱 API에 Nanite 메쉬를 더 효율적으로 표현하기 위한 확장이 추가되고 있지만, 지오메트리 복잡도는 계속 증가하고 있습니다. 레이 트레이싱에서 표현하기 훨씬 더 까다로운 새로운 타입의 콘텐츠도 등장하고 있으며, 이들을 제대로 처리할 수 있어야만 순수 레이 트레이싱 기반 섀도잉으로 완전히 넘어갈 수 있을 것입니다.

 

References

Sampling
● Yuksel 2019 - “Stochastic Lightcutsˮ, HPG 2019
● Efraimidis et al. 2006 - “Weighted random sampling with a reservoirˮ, Information Processing Letters, 2006
● Donnely et al. 2024 - ˮFilter-Adapted Spatio-Temporal Sampling for Real-Time Renderingˮ, i3D 2024
● Ogaki 2021 - ˮVectorized Reservoir Samplingˮ, SIGGRAPH Asia 2021
● Georgiev et al. 2016 - “Blue-noise Dithered Samplingˮ, SIGGRAPH 2016
● Clarberg et al. 2016 - “Wavelet importance sampling: efficiently evaluating products of complex functionsˮ, SIGGRAPH 2005
ReSTIR
● Wyman et al. 2023 - “A Gentle Introduction to ReSTIR: Path Reuse in Real-timeˮ, SIGGRAPH 2023
● Kozlowski et al. 2023 - “ReSTIR Integration in Cyberpunk 2077ˮ, SIGGRAPH 2023
● Wyman et al. 2021 - “Rearchitecting spatiotemporal resampling for productionˮ, HPG 2021
● Panteleev et al. 2020 - “Rendering Games With Millions of Ray-Traced Lightsˮ, GTC 2020
● Bitterli 2022 - “Correlations and Reuse for Fast and Accurate Physically Based Light Transportˮ, Thesis 2022
● Knapik el al. 2024 - “The Evolution of the Real-Time Lighting Pipeline in Cyberpunk 2077ˮ, GPU Zen 3, 2024
Ray Tracing
● Netzel et al. 2022 - “Ray Tracing Open Worlds in Unreal Engine 5ˮ, SIGGRAPH 2022
● Stachowiak 2024 - “Rendering Tiny Glades With Entirely Too Much Ray Marchingˮ, GPC 2024
Shadow Maps
● VSMRT - “Soft Shadows with Shadow Map Ray Tracingˮ, UE documentation

Global Illumination
● Wright et al. 2022 - "Lumen: Real-time Global Illumination in Unreal Engine 5ˮ, SIGGRAPH 2022
Light Lists
● Sousa et al. 2016 - "The devil is in the details: idTech 666ˮ, SIGGRAPH, 2016
Denoising
● [Heitz et al. 2018 - "Combining Analytic Direct Illumination and Stochastic Shadowsˮ, i3D 2018
● Schied et al. 2016 - "Spatiotemporal Variance-Guided Filtering: Real-Time Reconstruction for Path-Traced Global Illuminationˮ, HPG 2017
● T. Zhuang et al. 2021 - "Real-time Denoising Using BRDF Pre-integration Factorizationˮ, CGF 2021
● Karis 2013 - "Tone Mappingˮ, Blog 2013
● Salvi 2016 - "An excursion in temporal supersamplingˮ, GDC 2016
● Karis 2014 - "High quality temporal supersamplingˮ, SIGGRAPH 2014