2025. 1. 29. 21:57ㆍWIP/Igniter
요약
- Clustered Shading에 필요한 Light Clustering 알고리즘 구현 (Compute Shader / Tile+Depth Bin)
- 아직 렌더링 파이프라인의 구현은 완료하지 않아서 영상은 Light Cluster를 시각화함(파랑->초록->노랑->빨강 순으로 밀도가 높음/QHD에서 8x8 타일로 320x180 타일 해상도 및 깊이는 32구간으로 선형적으로 분할함 및 최대 광원 수는 8192개)
- 대략적인 광원의 위치를 알기 위해 구 형태의 메쉬 사용
- 영상의 경우 광원의 반지름은 2, 총 광원 수는 8000개, 처리 시간은 버퍼 클리어(41 us) + 클러스터링 알고리즘(23.552 us~540us) = 64.552us ~ 581us = 0.0645ms ~ 0.581ms (최선: 광원이 화면 상 에서 적은 영역을 차지하는 경우, 최악: 광원이 화면 상 에서 많은 영역을 차지하는 경우)
- 메모리 사용량은 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인데 이미 상상을 초월하는 크기의 메모리가 필요해져 버림..
이제 이걸 해결하기 위해 정말 천재적인 발상이 하나 제시되었는데,
- 광원의 인덱스를 광원의 View space 에서의 z를 기준으로 오름차순으로 정렬한다.
- Depth를 나누는데 각 구간을 광원의 인덱스를 저장하고 있는 리스트의 인덱스의 최소, 최대 인덱스 2개를 통해 표현 한다(Depth Bin)
- 광원을 평가 할 때, 현재 픽셀의 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에서 인피니티 워드-액티비전에서 발표한 아래 자료를 대부분 참고했음
'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 |