Clustered Shading - Light Clustering & Debug

2025. 1. 29. 21:57WIP/Igniter

요약

  1. Clustered Shading에 필요한 Light Clustering 알고리즘 구현 (Compute Shader / Tile+Depth Bin)
  2. 아직 렌더링 파이프라인의 구현은 완료하지 않아서 영상은 Light Cluster를 시각화함(파랑->초록->노랑->빨강 순으로 밀도가 높음/QHD에서 8x8 타일로 320x180 타일 해상도 및 깊이는 32구간으로 선형적으로 분할함 및 최대 광원 수는 8192개)
  3. 대략적인 광원의 위치를 알기 위해 구 형태의 메쉬 사용
  4. 영상의 경우 광원의 반지름은 2, 총 광원 수는 8000개, 처리 시간은 버퍼 클리어(41 us) + 클러스터링 알고리즘(23.552 us~540us) = 64.552us ~ 581us = 0.0645ms ~ 0.581ms (최선: 광원이 화면 상 에서 적은 영역을 차지하는 경우, 최악: 광원이 화면 상 에서 많은 영역을 차지하는 경우)
  5. 메모리 사용량은 Tile 버퍼에 56.25MB, Depth Bin에 256B (GPU 상에서 최대 2개 프레임이 동시에 렌더링 되기때문에 이들의 두 배만큼의 메모리 공간 사용)

본문

픽셀에서 광원을 평가할 때 Forward Shading이든 Deferred Shading이든 픽셀에 영향을 끼치지 않는 광원까지 평가해야 한다면 조명의 수가 늘어남에 따라 더 많은 계산이 필요로 하게 됨.

그걸 최소화 하기 위해서 고안 된 방법이 Tiled Shading, 화면을 일정한 크기의 격자로 나누고 격자 내의 구분되는 구역을 타일 이라고 부르는데. 이 때 광원이 특정 범위를 가지는 구체라고 가정했을 때, 이 구체를 화면 공간(Screen Space)으로 투영한다면 쉽게 어떤 타일에 어떤 광원 들 이 속해있는지 알아 낼 수 있음.

다만, Tiled Shading의 큰 단점은 렌더링 할 공간이 3D 공간이란 점임. 만약 렌더링 시 넓은 화면 깊이(Depth)에 걸쳐서 많은 광원이 존재한다면 여전히 상대적으로 비효율적인 상황임을 알 수 있음.

예를 들어, 하나의 타일에 속하는 픽셀이지만 1번 픽셀은 깊이가 1000이고 2번 픽셀은 깊이가 0.1인 경우, 오픈 월드같이 넓은 공간을 렌더링 할 때 풀과 저 멀리 있는 산이 같이 있는 경우를 상상해보면, 이 둘 이 속하는 타일에 풀~멀리있는 산 사이에 있는 광원이 모두 포함되고, 얘 둘에 대한 광원을 평가할 때 엄청나게 많은 광원을 순회해야 할 수도 있다는 것임.

위와 같은 문제를 'Depth Discontinuity(깊이 불연속)'이라고 함. 이걸 해결하기 위해 Z Pre-Pass를 통해 화면의 깊이를 먼저 얻어낸 다음, 타일에서 완전히 가려지는 광원을 사전에 제외 시키는 방법 또한 있지만, Z Pre-Pass를 수행하는 비용도 공짜가 아닐 뿐더러 여전히 가려지지 않는 부분에 대한 광원들이 타일에 포함되어 완벽한 해결책은 아님.

이쯤 되면 당연히 예상하겠지만 여기서 더 나아간 해결 방법은 이제 화면 공간을 Z 축에 대해서도 자르는 방법이 있음. 이제 이런 방법들을 Clustered Shading 이라고 함. 문제는 우리의 전통적인 법칙인 "알고리즘이 빠르다 == 메모리를 많이 쓴다!" 의 법칙에 따라서 더 잘게 쪼개진 공간은 광원 평가 시 매우 효율적일 수 있으나, 그 만큼 더 많은 메모리를 사용한다는 거임.

만약 렌더링 엔진이 최대 2^16개 (65,536개)의 광원을 지원한다고 하면. 클러스터 하나가 광원에 대한 정보를 저장하기 위해 최소 8KB(2^16 비트)의 메모리 공간이 필요로 함. 이때 가시 카메라 영역(View Frustum 내부)을 320 * 180 * 32 (QHD 해상도에 대해 8x8 크기의 타일을 사용하고, 깊이 공간을 32개의 영역으로 나누었을 때)으로 나눈다고 가정한다면 대략 14GB 크기의 GPU 버퍼가 필요 하단 사실을 알 수 있음.

보통 모던 그래픽스 API를 사용하는 렌더러의 경우 GPU 사용률을 높이기 위해 2개 이상의 프레임을 GPU상에서 동시에 처리하는 경우도 있기에 이런 경우에 최소 28GB의 GPU 메모리가 필요하단 것을 알 수 있음. RTX 4090의 VRAM이 24GB인데 이미 상상을 초월하는 크기의 메모리가 필요해져 버림..

이제 이걸 해결하기 위해 정말 천재적인 발상이 하나 제시되었는데,

  1. 광원의 인덱스를 광원의 View space 에서의 z를 기준으로 오름차순으로 정렬한다.
  2. Depth를 나누는데 각 구간을 광원의 인덱스를 저장하고 있는 리스트의 인덱스의 최소, 최대 인덱스 2개를 통해 표현 한다(Depth Bin)
  3. 광원을 평가 할 때, 현재 픽셀의 Z 값에 해당하는 Depth Bin을 읽어서 최소~최대 인덱스 사이에 속하는 모든 인덱스들이, 해당 타일에 속한다면, 그 인덱스가 가르키는 광원은 현재 픽셀에 영향을 끼친다.

이렇게 320 * 180 * 32에서 320 * 180 + 32로 필요한 데이터의 차원을 떨궈버리면서도 여전히 원하는 효과를 얻을 수 있게 되었음.

물론 추가적으로 광원 정렬이나 광원의 인덱스를 저장하는 버퍼 등 추가적인 비용은 들어가겠지만 충분히 감수할만한 트레이드-오프라고 생각함.

(동일 조건에서 450MB + 128B 만큼의 메모리 공간 만을 요구)

아마 대부분의 게임 엔진에서 구현하는 Clustered Shading이 대부분 이런 방식을 통해서 구현 될 것임. 아마 Clustered Shading은 못들어 봤어도 'Forward+ Rendering Pipeline' 같은 용어는 렌더링에 관심 있는 사람들은 한번 쯤 본적이 있을 거임. 바로 이게 Forward Shading에 Clustered Shading을 합친 형태를 말함.

Clustered Shading은 별도의 작업 의존성(Tiled는 Z Pre-Pass를 필요로 했던 것과 달리)을 가지지 않기 때문에 Forward/Deferred 모두 결합해서 사용 할 수 있다는 장점이 존재함.

참고

구현에는 Siggraph 2017에서 인피니티 워드-액티비전에서 발표한 아래 자료를 대부분 참고했음

Rendering of COD:IW

'WIP > Igniter' 카테고리의 다른 글

Meshlet 렌더링 파이프라인의 구현  (0) 2025.02.16
Forward+ Rendering Prototype  (0) 2025.02.03
GPU Driven Dynamic LOD 선택 구현  (0) 2025.01.20
GPU Driven Rednering 구현 시작  (0) 2025.01.17
에셋 선택 팝업  (1) 2024.12.17