Blog iconJaeho Yi
Radiance Cascades

개요

notion image
💡
기간: 2026. 07. ~ 2026. 08.
사용 기술: C++, Direct3D 11, Win32, HLSL, Dear ImGui 등
Radiance Cascades는 많은 광원이 있는 2D 씬에서 실시간 Global Illumination을 구현하기 위한 접근법입니다. C++20, Direct 3D 11, HLSL을 사용해 Radiance Cascades의 핵심 원리와 간단한 최적화 기법들을 구현하는 프로젝트입니다. 맵을 실시간으로 편집할 수 있는 간단한 에디터 기능도 구현했습니다. D3D11 API와 최신 기법을 배우고 구현하는 것을 목표로 진행한 프로젝트입니다.

렌더링 파이프라인

전체적인 흐름은 다음과 같습니다.
  1. Scene은 장애물과 광원 위치에 대한 데이터입니다. 현재는 이후 텍스처를 생성하기 편하도록 뷰포트와 같은 크기의 컨테이너로 되어있습니다.
  1. 이 데이터를 통해서 각 픽셀마다 장애물, 광원 정보를 담는 뷰포트 크기의 텍스처를 생성합니다. 이 텍스처는 Scene 데이터가 변경됐을 때 변경된 바운딩 박스 영역만 업데이트하여 CPU-GPU 데이터 교환을 최소화합니다.
  1. 추후 Ray Marching에 사용할 Distance Field 텍스처를 생성합니다. 각 픽셀은 가장 가까운 장애물과의 거리를 저장합니다. JFA로 계산되기 때문에 정확한 값이 아닌 근사값으로 계산됩니다.
  1. Radiance Cascade 패스로, N단계에서 0단계까지 프로브 개수와 광선 개수를 조정하면서 반복하여 최종 프로브 Radiance 텍스처를 생성합니다.
  1. 이 Radince 값을 보간하여 최종적으로 각 픽셀의 조명 정보를 계산합니다.

Radiance Cascades 설계

Radiance Cascades는 Penumbra 가설을 기반으로 하는 알고리즘입니다. 가까운 거리에는 높은 공간 해상도가, 먼 거리에는 높은 각도 해상도가 필요하다는 가설입니다. 다시 말해서, 가까운 거리에 대한 조명은 공간적인 밀도가 높은 프로브를, 먼 거리에 대한 조명은 광선 수가 많은 프로브를 사용해서 빛을 수집해야 합니다.
그래서 Radiance Cascade는 Cascade 계층을 나눕니다. 낮은 계층에는 광선수가 적은 프로브를 촘촘히 배치합니다. 높은 계층에는 광선 수가 많은 프로브를 듬성히 배치합니다.
Cascade 계층은 N 계층에서 0 계층으로 내려오면서 계산합니다. 이 순서로 계산하면 Cascade 계층을 따로 저장해둘 필요 없이 더 먼 구간의 Radiance가 현재 구간의 장애물에 가려지는지를 자연스럽게 처리할 수 있습니다.
계층마다 바뀌는 프로브 거리, 광선 수나 각 계층이 담당하는 구간이 겹치는 정도에 따라서 결과물이 달라질 수 있습니다. 이런 파라미터들을 디버그 UI에 노출해서 결과를 비교해볼 수 있도록 구현했습니다.

최적화 기법

Ray Marching과 Distance Field

Radiance Cascade에서 성능상 큰 병목이 되는 부분은 Ray Marching입니다. 따라서 Ray Marching의 비용을 줄이기 위해서 Distance Field를 사용했습니다. 광선은 각 스텝마다 적어도 가장 가까운 장애물까지의 거리만큼은 이동할 수 있습니다. 이 거리를 저장하는 것이 바로 Distance Field입니다.
그러나 현재 사용하는 Distance Field는 정확한 값이 아닌 근사값이 저장되어 있습니다. 장애물을 지나치는 경우를 방지하기 위해서 Safety Margin을 두었고, 또 너무 근처에 장애물이 있어서 광선이 나아가지 못하는 경우를 방지하기 위해서 Minimum Step을 두었습니다.
Distance Field에 정확한 값이 아닌 근사값이 계산되는 이유는 Jump Flooding 알고리즘을 사용하기 때문입니다. 이 알고리즘은 비교하는 이웃까지의 간격을 줄여가면서, 매 반복마다 이웃 픽셀과 가장 가까운 장애물을 후보로 하여 값을 갱신합니다. 모든 픽셀이 가장 가까운 장애물을 찾는다는 보장은 없지만, 실제로 모든 픽셀을 검사하는 것보다 훨씬 효율적입니다.
추가적인 최적화로 x, y 좌표를 32비트 값 하나로 패킹하면서 텍스처 메모리를 줄였습니다.

텍스처 핑퐁, Pre-averaged Ray Storage

텍스처 메모리 사용량을 줄이기 위한 기법들입니다. Top-Down으로 Cascade 계층을 합쳐나가기 때문에 최종 Radiance만 있으면 조명을 계산할 수 있습니다. 또한 프로브 수가 늘어나는 비율과 광선 수가 줄어드는 비율을 일치시켰기 때문에 각 계층에 필요한 텍스처의 크기는 비슷합니다. 따라서 계층 수만큼 텍스처를 생성하지 않고 2개만 생성하여 셰이더 리소스와 렌더 타겟을 번갈아 바꿔가면서 계산했습니다. 일반적인 설정에서 계층 수가 8 이상이기 때문에 메모리 사용량을 1/4 이상으로 줄일 수 있었습니다.
그리고 Pre-averaing을 사용하여 메모리를 이 이상으로 줄일 수 있었습니다. 프로브의 특정 방향의 Radiance를 계산할 때는 그 방향과 인접한 4개 광선의 Radiance를 평균내어 계산합니다. 이 값들은 그 프로브의 계산에만 사용되기 때문에, 그 값들을 모두 저장해두지 않고 미리 평균낸 값을 합쳐서 저장해둘 수 있습니다.
예를 들어 8개의 Cascade 텍스처를 각각 저장하던 초기 구조와 비교하면 핑퐁으로 2개만 유지해 약 1/4, Pre-averaging으로 각 텍스처 면적을 다시 1/4로 줄여 텍스처 메모리를 약 1/16까지 줄일 수 있습니다.

추가 기능

리사이징, 자동 Cascade Count

창 크기가 바뀌는 상황을 처리할 수 있도록 구현했습니다. 화면의 크기가 바뀔 때 렌더러에 리사이징 요청을 보내고, 적절한 시점에 Swap Chain이나 Cascade 자원들을 다시 계산합니다. Cascade의 계층 수도 뷰포트와 씬 전체를 덮을 수 있는 값으로 자동으로 설정합니다.