2025년 2월 12일 문장 다시 정리.
약 5년 전, 중국의 게임 개발사들은 모바일에서 디퍼드 렌더링보다는 포워드 렌더링을 기반으로 아티스트들의 'Many Lights' 요구사항을 해결하는 데 집중했습니다.
당시 유니티에는 클러스터드 기반의 포워드 플러스 렌더링이 없었기 때문에 대부분의 개발사가 직접 구현했습니다.
거인 네트워크, 신동 네트워크, 퍼펙트 월드, 그리고 텐센트 등이 이에 해당합니다.
아래 기사는 제가 중국에 있을 당시 Z 깊이에 대한 클러스터링 데이터로 공간 셀을 형성하고, TBDR의 2차원 타일 환경에서 라이트 인덱스를 관리했던 방법을 간략히 소개합니다.
2019년 10월.
아래 글은 예전에 중국어로 작성된 토픽을 한국어로 번역한 것입니다. ㅜㅜ
본 기사는 해결 된 문제, 실현 원리, 실천의 세 부분으로 나뉜다.
문제 해결
조명을 할당하는 방법
즉, 프래그먼트 셰이더가 조명 계산을 수행할 때 어떤 조명을 계산해야 하는지 어떻게 판단할 수 있을까요?
포워드
전통적인 포워드 렌더링에서는 각 객체별로 적용할 조명을 판단하여 계산합니다. 하지만 이러한 객체별 조명 컬링 방식은 두 가지 문제를 발생시킵니다.
- 큰 물체가 작은 조명의 영향을 받을 때, 실제로 영향을 받지 않는 부분까지 N개의 조명에 대해 계산해야 하므로 중복 계산이 발생합니다. (일반적으로 많은 객체를 렌더링해야 하므로 이러한 중복성이 더욱 증가합니다)
- 많은 작은 물체들이 하나의 헤드라이트의 영향을 받을 때 많은 교차 연산이 필요합니다. 이처럼 조명 처리는 장면의 복잡도와 밀접한 관련이 있습니다.
조명 수가 적을 때는 이러한 문제가 크게 드러나지 않아 LWRP에서는 장면의 최대 조명 수를 제한합니다. 더 많은 조명 효과를 원할 경우 심각한 성능 문제가 발생할 수 있습니다.
디퍼드
디퍼드 렌더링에서는 각 조명을 래스터화하여 타일이 조명의 영향을 받도록 하고, 해당 타일에서 조명 계산을 수행합니다.
이러한 조명 컬링 방식의 장점은 장면의 복잡도와 무관하며 중복 계산이 없다는 것입니다.
그러나 조명 계산을 수행할 때마다 G-buffer를 읽고 써야 하므로 조명이 많을 때 큰 대역폭 부하가 발생한다는 단점이 있습니다. 이러한 대역폭 문제는 점차 모바일 기기의 주요 성능 병목 현상이 되고 있습니다.
클러스터의 원리
디퍼드 렌더링은 조명 처리를 위한 좋은 접근 방식입니다. 스크린 공간에서의 조명 처리는 장면의 복잡도와 분리되어 중복 계산 문제를 해결합니다.
대역폭 부하는 조명을 순회할 때마다 G-buffer를 읽고 써야 하기 때문에 발생합니다. 순회 순서를 수정하고 프래그먼트의 조명을 순회하면 아래와 같이 G-buffer를 한 번만 읽고 쓸 수 있습니다.
그렇다면 처음 질문으로 돌아가서, 각 프래그먼트에 대해 어떤 조명을 계산해야 하는지 효율적으로 판단하려면 어떻게 해야 할까요?
- 모든 조명을 사용하고 조명 컬링을 하지 않는 방법 (계산 중복과 성능 저하 발생)
- 조명 목록을 픽셀별로 저장하는 방법 (조명 계산이 너무 복잡하고 메모리 사용량이 높음)
인접한 픽셀들은 동일한 조명의 영향을 받기 쉽다는 점에 착안하여 픽셀 그룹 단위로 접근하는 방법이 제안되었습니다. 컬링은 그룹 단위로 수행되어 해당 그룹에 영향을 주는 조명 목록을 얻고, 프래그먼트는 이 그룹의 조명 목록을 사용하여 계산을 수행합니다.
클러스터
클러스터는 뷰 프러스텀을 XYZ 3차원으로 분할한 그룹으로, 각각의 그룹이 하나의 클러스터가 됩니다.
각 클러스터는 조명들과의 교차 검사를 통해 영향을 받는 조명 목록을 얻습니다. 프래그먼트에서는 자신이 속한 클러스터에 따라 조명 계산을 수행합니다.
이러한 클러스터 기반 조명 분배 방식은 디퍼드 렌더링뿐만 아니라 포워드 렌더링에도 적합합니다.
라이트리스트 저장 구조
각 클러스터의 컬링 결과로 얻은 조명 목록은 어떻게 저장할까요?
3 층 구조
- 라이트리스트는 모든 조명 데이터를 저장합니다
- Light Index List는 각 클러스터의 조명 인덱스를 저장합니다
- Light Grid는 Light Index List에서 각 클러스터의 시작과 끝 위치를 저장합니다
2 층 구조
- 라이트리스트는 모든 조명 데이터를 저장합니다
- Light Grid는 각 클러스터의 조명 수와 인덱스를 저장합니다 (고정 길이 저장 방식으로 인해 일부 공간이 낭비됨)
구조 선택
3층 구조는 메모리 낭비가 없지만 추가적인 샘플링이 필요합니다.
다음은 라이트리스트를 위한 2층 구조입니다.
실습
Unity의 LWRP 렌더링 파이프라인은 사용자 정의가 가능하고 수정이 쉽습니다. 다음은 LWRP의 포워드 렌더링에 클러스터 조명 분배 시스템을 추가하여 다수의 동적 조명을 지원하는 방법을 설명합니다. 주요 단계는 다음과 같습니다.
- 프로젝트의 기본 객체별 조명 컬링을 비활성화하고 기본 조명 설정을 해제합니다.
- Cluster Light Distribution Scheme을 사용하여 각 클러스터의 조명 목록을 얻습니다.
- 프래그먼트 조명 계산 시 자신이 속한 클러스터를 계산하고 해당 조명 목록을 사용합니다.
1번과 3번은 조명 분배와 직접적인 관련이 없으므로 자세히 다루지 않겠습니다.
GPU 조명 분배
먼저 Compute Shader로 구현해보겠습니다. 각 클러스터는 하나의 스레드에 할당되며 조명은 병렬로 컬링됩니다.
각 클러스터는 작은 육면체로, 모든 조명과의 교차 검사를 통해 영향을 받는 조명 목록을 얻습니다.
테스트 결과, Compute Shader의 조명 할당 오버헤드는 크지 않아 이 부분의 과도한 최적화는 불필요했습니다. 주요 성능 병목은 조명이 많을 때의 조명 계산에 있었으며, PBR 대신 Lambert 모델을 사용하는 등 조명 모델을 단순화하여 개선했습니다.
StructBuffer 대 텍스처
조명 목록 정보 저장에 StructuredBuffer와 Texture를 각각 사용하여 성능을 테스트한 결과, Texture 사용 시 성능이 더 좋았습니다.
또한 모바일 기기는 Texture를 더 잘 지원하며, 일부 기기는 StructuredBuffer를 제대로 지원하지 않습니다.
GPU 솔루션 테스트 요약
- GPU 조명 분배 시스템의 성능 병목은 GPU에 있으며, 다음과 같은 방법으로 성능을 개선할 수 있습니다.
- 비표준 광원의 조명 계산을 단순화합니다.
- 데이터 입출력에 텍스처를 사용합니다.
- 클러스터 컬링 정밀도를 적절히 높여 컬링 정확도를 개선하고 조명 계산의 중복을 줄입니다.
발생한 문제
- Compute Shader의 numthreads (그룹 스레드)는 가능한 크고 빠릅니다 (하나의 설명은 하드웨어에 최소 스레드 수가 있다는 것입니다.이 수보다 낮은 경우 GPU를 완전히 사용할 수 없습니다. 다른 그룹에있는 스레드가 많을수록 더 많은 메모리 히트가 발생합니다. 높음).
- Huawei Mali의 GPU 성능이 약하고 (권장 CPU 솔루션) Compute Shader에서 numthreads가 지원하는 최대 X * Y * Z가 더 낮습니다 Mali-G76에서는 32 * 16 = 512가 지원되지 않습니다 (Qualcomm의 GPU는 모두 지원됨). 지원), 16 * 8 = 128 수 있습니다.
- 장치가 CS (OpenGL ES 3.1)를 지원하는지 여부 및 Float 유형 텍스처 형식 (Huawei Mate7에서 지원되지 않음)을 지원하는지 확인합니다.
- GPU의 성능을 테스트 할 때는 GPU 하단의 셰이더 계산 최적화에주의를 기울여야합니다 (예 : 반복 계산 또는 사용되지 않은 계산이 최적화 됨). CPU에서 작성한 코드가 실행되는 동안에는 그렇지 않습니다. 잘못된 결론은 시험 방향에 영향을 미칩니다.
CPU 배광
조명 분배 계산에 멀티스레딩을 적용할 때는 Unity의 Job System을 사용하며, Burst 컴파일러로 성능을 향상시킵니다.
Job System은 멀티스레드 작업을 캡슐화하여, 실행 큐에 작업만 추가하면 엔진이 스케줄링, 리소스 경합 처리, 멀티스레드 할당을 담당합니다.
Burst 컴파일러는 C# 코드를 대상 플랫폼에 최적화된 기계어 코드로 변환합니다. 단, Job에서는 관리되는 힙 메모리를 할당할 수 없습니다.
알고리즘 최적화
CPU 조명 분배의 성능 병목이 조명 분배 알고리즘에 있어 다양한 최적화를 시도했습니다.
- 클러스터 분할 수를 32 * 16 * 32에서 16 * 8 * 32로 줄였습니다. 성능은 향상되었지만 컬링 정밀도가 낮아져 GPU 성능이 저하되는 경우도 있습니다.
- 교차 검사 알고리즘 단순화: 육면체 대신 구형 경계 상자를 사용하여 컬링 정밀도를 낮추는 대신 특정 상황에서의 성능을 개선했습니다.
- 여러 클러스터를 통합하고, 조명 순회 횟수를 줄이며, 거친 컬링을 적용하여 알고리즘 효율을 개선한 결과 확실한 성능 향상을 얻었습니다.
요약: 1번과 2번 방법은 모두 컬링 정밀도를 낮추어 CPU 부하를 줄이지만 때로는 GPU 부하가 증가하므로 적절히 사용해야 합니다. 클러스터 통합과 거친 컬링은 성능 개선에 이상적입니다.
발생한 문제
- 데이터 경합을 방지하기 위해 Job System은 Blittable 타입 데이터만 접근 가능합니다(IJobParallel은 NativeArray 데이터 쓰기 제한). 데이터 구조는 struct만 사용할 수 있으며, 데이터가 많을 경우 struct 복사가 큰 성능 오버헤드를 발생시키므로 멤버 변수에 직접 접근합니다. 이는 데이터 복사를 줄이지만 코드 가독성이 떨어집니다.
- NativeArray는 읽기 속도가 매우 느리므로 일반 배열로 변환하면 성능이 크게 향상될 수 있습니다.
요약
- 조명 분배 방식은 프로젝트의 성능 병목에 따라 CPU 또는 GPU 방식을 선택할 수 있습니다(화웨이 Mali GPU는 성능이 낮아 CPU 방식 권장).
- 조명 데이터 읽기에는 텍스처를 사용하세요.
- 조명 계산에서는 조명 모델을 단순화하세요.
모바일 기기 테스트 결과표.
디버그 패널
- 클러스터 컬링 결과를 시각화하고 클러스터의 조명 수에 따라 색상을 지정합니다.
- 클러스터에 해당하는 그리드를 클릭하면 조명 정보가 표시됩니다.
추가
- 조명이 변하지 않으면 재계산하지 않습니다.
- 3층 구조는 조명 목록을 저장하여 메모리 사용량을 줄이고 추가 샘플링이 필요하며, 성능 테스트에 사용됩니다.
'UNITY3D' 카테고리의 다른 글
Custom Shadow Attenuation Tweak Example (0) | 2024.09.25 |
---|---|
Unity6 Adaptive Light Probe Debugmode. (0) | 2024.09.24 |
RCAS ( Robust Contrast-Adaptive Sharpening ) (0) | 2024.09.20 |
Unity Rendering Graph System. (2) | 2024.09.18 |
[UNITY6] For GPU Driven Rendering. BRG | GRD (0) | 2024.08.27 |