개요

기간: 2026. 07. ~ 2026. 08.
사용 기술: C++, Direct3D 11, Win32, HLSL, Dear ImGui 등
Radiance Cascades는 많은 광원이 있는 2D 씬에서 실시간 Global Illumination을 구현하기 위한 기법입니다. Path of Exile 2에 적용된 기술에 대한 소개 영상을 보고 직접 구현해보겠다는 목표로 시작했습니다.
C++20, Direct 3D 11, HLSL을 사용해 Radiance Cascades의 핵심 원리와 간단한 최적화 기법들을 구현했습니다. 실시간 반영을 확인하기 위해서 맵을 실시간으로 편집할 수 있는 간단한 에디터 기능도 구현했습니다.
파이프라인
전체적인 흐름은 다음과 같습니다.
Scene은 장애물과 광원의 위치 데이터입니다. 뷰포트와 같은 크기의 배열로 되어있습니다.
- 이 데이터를 통해서 각 픽셀마다 장애물, 광원 정보를 담는 뷰포트 크기의 텍스처를 생성합니다. CPU-GPU 데이터 교환을 최소화하기 위해서
Scene데이터가 변경될 때 변경된 바운딩 박스 영역만 텍스처를 업데이트하도록 만들었습니다.
- 이후 Ray Marching에 사용할 Distance Field 텍스처를 생성합니다. 각 픽셀은 가장 가까운 장애물과의 거리를 저장합니다. 계산량을 줄이기 위해서 JFA로 계산되기 때문에 정확한 값이 아닌 근사값으로 계산됩니다.
- Radiance Cascade 패스로, N단계에서 0단계까지 프로브 개수와 광선 개수를 조정하면서 Cascade 계산을 반복하여 최종 프로브 Radiance 텍스처를 생성합니다.
- 최종적으로 이 Radince 값을 보간하여 각 픽셀의 조명 정보를 계산합니다.
Radiance Cascades
Radiance Cascades는 Penumbra 가설을 기반으로 하는 알고리즘입니다. Penumbra 가설은 가까운 거리에는 높은 공간 해상도가, 먼 거리에는 높은 각도 해상도가 필요하다는 가설입니다. 다시 말해서 가까운 거리에 대한 조명은 프로브를 공간적으로 빼곡하게 배치하고, 먼 거리에 대한 조명은 광선 수가 많은 프로브를 사용해서 빛을 수집해야 합니다.
그래서 Radiance Cascade는 거리를 기준으로 Cascade 계층을 나눕니다. 낮은 계층에는 광선수가 적은 프로브를 촘촘히 배치합니다. 높은 계층에는 광선 수가 많은 프로브를 듬성히 배치합니다.
Cascade 계층은 N 계층에서 0 계층으로 내려오면서 계산합니다. 이 순서로 계산하면 Cascade 계층을 따로 저장해둘 필요 없이 더 먼 구간의 Radiance가 현재 구간의 장애물에 가려지는지를 자연스럽게 처리할 수 있습니다.
계층마다 바뀌는 프로브 거리, 광선 수나 각 계층이 담당하는 구간이 겹치는 정도에 따라서 결과물이 달라질 수 있습니다. 이런 파라미터들을 디버그 UI에 노출해서 결과를 비교해볼 수 있도록 구현했습니다.
최적화 기법
Ray Marching과 Distance Field
Radiance Cascades에서는 Ray Marching 계산이 큰 병목이 됩니다. 따라서 Ray Marching의 비용을 줄이기 위해서 Distance Field를 사용했습니다. 가장 가까운 장애물까지의 거리를 미리 알고 있다면, 광선은 그 거리만큼은 건너뛰어 이동할 수 있습니다. 이 거리를 저장하는 것이 Distance Field입니다.
그런데 이 Distance Field를 생성하는 것도 많은 계산량이 필요합니다. Distance Field를 빠르게 계산할 수 있는 알고리즘으로 Jump Flooding 알고리즘을 선택했습니다. 이 알고리즘은 비교하는 이웃까지의 간격을 줄여가면서, 매 반복마다 이웃 픽셀과 가장 가까운 장애물을 후보로 하여 값을 갱신합니다. 모든 픽셀이 가장 가까운 장애물을 찾는다는 보장은 없기 때문에 항상 최소 거리가 저장되는 것이 아니라 근사값이 저장됩니다.
따라서 이 Distance Field에 정확한 최소 거리가 저장되어있는 것이 아니기 때문에, 장애물을 지나치는 경우를 방지하기 위해서 Safety Margin을 설정했고, 또 너무 근처에 장애물이 있어서 광선이 나아가지 못하는 경우를 방지하기 위해서 Minimum Step을 설정했습니다.
텍스처 더블 버퍼링, Pre-averaged Ray Storage
텍스처 메모리 사용량을 줄이기 위한 기법들입니다. Top-Down으로 내려가면서 각 계층마다 이전 계층의 값까지 합쳐서 저장을 한다면 이전 계층의 값만 가지고 최종 Radiance를 계산할 수 있습니다. 또한 프로브 수가 늘어나는 비율과 광선 수가 줄어드는 비율이 일치하기 때문에 각 계층에 필요한 텍스처의 크기는 비슷합니다. 따라서 계층 수만큼 텍스처를 생성하지 않고 2개만 생성하여 셰이더 리소스와 렌더 타겟을 번갈아 바꿔가면서 계산했습니다. 계층 수가 8 이상인 일반적인 상황에서 메모리 사용량을 1/4 이상으로 줄일 수 있었습니다.
그리고 이에 더해 Pre-averaing을 사용하여 사용 메모리를 더욱 줄일 수 있었습니다. 프로브의 특정 방향의 Radiance를 계산할 때는 그 방향과 인접한 4개 광선의 Radiance를 평균내어 계산합니다. 이 값들은 그 프로브의 계산에만 사용되기 때문에, 그 값들을 모두 저장해둘 필요 없이 미리 평균낸 값을 합쳐서 저장해둘 수 있습니다.
8개의 Cascade 텍스처를 각각 저장하던 초기 구조와 비교하면 더블 버퍼링으로 2개만 유지해 약 1/4, Pre-averaging으로 각 텍스처 면적을 다시 1/4로 줄여 텍스처 메모리를 약 1/16까지 줄일 수 있습니다.