3D Gaussian Splatting을 실시간 게임 배경 에셋으로 변환하는 방법: 메모리 최적화까지
3D Gaussian Splatting 캡처 데이터를 실시간 게임 배경으로 전환하는 파이프라인을 정리한다. 가우시안 정리, 청크 스트리밍, SH 축소, 압축과 LOD 설계로 VRAM·대역폭 병목을 줄이는 실전 기준을 제시한다.

핵심 요약
3D Gaussian Splatting(3DGS)은 포인트 클라우드보다 풍부한 외관 정보를 빠르게 그릴 수 있지만 원본 학습 결과를 그대로 게임에 넣으면 수백만 개 가우시안과 높은 셰이더 속성 비용 때문에 VRAM·메모리·드로우 대역폭이 빠르게 한계에 닿는다. 해결책은 배경을 공간 청크로 분할하고 카메라 거리별 LOD를 만들며 SH 계수와 속성 정밀도를 줄인 뒤 GPU 친화적인 압축 포맷으로 스트리밍하는 것이다.
이 글은 특정 엔진 기능에 의존하지 않는 공통 구조를 기준으로 설명한다. Unity나 Unreal Engine에서는 Compute Shader 또는 GPU Driven Rendering 경로에 같은 원칙을 적용할 수 있다.
3D Gaussian Splatting 데이터는 무엇으로 구성되는가?
3DGS의 가우시안 하나는 화면에서 타원형 스플랫으로 투영된다. 일반적으로 위치, 회전, 크기, 불투명도, 시점 의존 색상 정보를 저장한다.
여기서 (\mu)는 가우시안 중심이며 (\Sigma)는 회전과 스케일로 구성한 3차원 공분산을 뜻한다. 런타임에서는 이를 카메라 공간으로 변환해 2차원 타원으로 투영하고 깊이순으로 알파 합성한다.
| 속성 | 일반적인 원본 표현 | 게임 런타임에서의 비용 | 우선 최적화 대상 |
|---|---|---|---|
| 위치 | float32 × 3 | 비교적 필수 | 청크 기준 양자화 |
| 회전 | float32 × 4 quaternion | 개수에 비례해 큼 | 10:10:10:2 또는 소형 quaternion |
| 스케일 | float32 × 3 | 필수 | log scale 양자화 |
| 불투명도 | float32 | 작지만 누적됨 | UNORM8 |
| 색상/DC | float32 × 3 | 필수 | RGB10A2 또는 FP16 |
| SH 계수 | 계수 수 × RGB | 가장 큰 메모리 요인 | 차수 축소·정밀도 축소 |
가령 3차 구면 조화 함수(Spherical Harmonics, SH)는 RGB당 16개 계수를 쓴다. 위치·회전·스케일·불투명도와 함께 float32로 유지하면 가우시안당 수백 바이트에 이르며 200만 개 데이터는 GPU에서 수백 MB 이상을 점유할 수 있다. 실제 레이아웃은 구현마다 다르지만 병목의 방향은 대체로 SH 속성과 대량의 개별 속성 읽기에서 발생한다.

원본 3DGS를 그대로 게임에 넣으면 왜 느려지는가?
투명도 정렬 비용이 프레임 시간을 지배한다
스플랫은 보통 알파 블렌딩을 사용하므로 뒤에서 앞으로 정렬하거나 근사적인 버킷 정렬을 수행해야 한다. 수백만 개를 CPU에서 정렬하면 프레임 유지가 어렵고 GPU 정렬도 메모리 대역폭과 동기화 비용을 유발한다.
완전한 전역 정렬이 항상 정답은 아니다. 게임 배경은 시점 이동이 연속적이므로 월드 청크 단위의 대략적 정렬과 청크 내부의 깊이 버킷을 조합하면 품질과 비용의 균형을 맞추기 쉽다.
보이지 않는 가우시안도 읽으면 대역폭을 낭비한다
화면 밖, 프러스텀 뒤 다른 지오메트리에 가려진 가우시안까지 속성을 읽어 버텍스 확장을 수행하면 연산보다 메모리 읽기가 먼저 포화된다. 3DGS 렌더러는 일반 메시 렌더링보다 컬링 이후 살아남은 인스턴스 수가 성능을 더 직접적으로 좌우하는 경우가 많다.
시점 의존 색상은 품질을 높이지만 저장 비용이 크다
SH는 반사광과 관찰 방향에 따른 색 변화를 표현하지만 정적인 원경 배경까지 높은 차수가 필요한 것은 아니다. 가까운 시야 중심부에는 낮은 차수 SH를 유지하고 중·원거리에는 DC 색상 또는 1차 SH만 남기는 설계가 효과적이다.
게임 배경용 3DGS 변환 파이프라인은 어떻게 설계할까?
1. 캡처 영역을 플레이 가능한 배경 단위로 정리한다
학습 전 또는 학습 후에 플레이 영역 밖의 데이터, 촬영 장비, 이동 인물, 반사 오류처럼 게임 배경에 불필요한 가우시안을 제거한다. 바닥이나 벽처럼 충돌이 필요한 표면은 3DGS와 별개로 단순 콜라이더 또는 프록시 메시를 만든다. 가우시안 스플랫은 시각 표현이지 물리 지오메트리가 아니다.
권장 기준은 다음과 같다.
- 플레이어가 접근 가능한 범위를 기준으로 배경을 분리한다.
- 충돌, 내비게이션, 그림자 수신이 필요한 구조물은 프록시 메시를 만든다.
- 카메라가 절대 도달하지 않는 촬영 뒤편과 실패한 재구성 영역을 삭제한다.
2. 월드를 고정 크기 청크로 분할한다
월드 좌표계에서 예를 들어 16 m 또는 32 m 정육면체 청크를 만든 뒤 각 가우시안을 하나의 청크에 배정한다. 청크 헤더에는 AABB, 가우시안 수, 각 LOD 오프셋, 압축 버퍼 크기를 둔다.
| 청크 데이터 | 용도 |
|---|---|
| AABB | 프러스텀 및 거리 컬링 |
| LOD별 가우시안 범위 | 거리별 버퍼 선택 |
| 위치 양자화 기준점·범위 | uint16 위치 복원 |
| 최대 불투명도·화면 오차 | 로딩 및 LOD 판정 |
| 파일 오프셋 | 비동기 스트리밍 |
청크 내부 위치는 월드 float32가 아니라 청크 AABB에 대해 uint16으로 양자화할 수 있다.
셰이더에서 (q)를 다시 청크 범위로 복원하면 된다. 작은 청크일수록 위치 오차는 줄지만 헤더와 스트리밍 요청 수는 늘어난다. 옥외 배경은 16~32 m, 실내는 방 또는 복도 단위에서 시작해 측정으로 조정하는 편이 안전하다.
3. LOD는 단순 삭제가 아니라 화면 오차로 만든다
가우시안 중요도는 불투명도, 화면 투영 면적, 색상 변화량, 카메라 거리로 평가한다. 원거리 LOD는 작은 가우시안을 무작정 지우는 것보다 이웃한 가우시안을 병합하거나 대표 가우시안으로 재표본화할 때 빈틈과 깜빡임이 줄어든다.
실무에서는 다음과 같은 3단계 구성이 출발점으로 적합하다.
| LOD | 사용 거리 예시 | 데이터 정책 |
|---|---|---|
| LOD0 | 0~20 m | 위치·회전·스케일, 1차 또는 고차 SH 유지 |
| LOD1 | 20~60 m | 가우시안 수 축소, DC 또는 1차 SH, 16비트 양자화 |
| LOD2 | 60 m 이상 | 대표 스플랫 또는 메시·임포스터 전환 |
거리 경계에서 갑자기 교체하면 팝핑이 발생한다. 5~10% 폭의 전환 구간에서 두 LOD의 가중치를 교차 페이드하거나 카메라 속도와 화면 점유율을 함께 고려해 전환 시점을 안정화한다.

3DGS 메모리는 어떤 순서로 줄여야 할까?
가장 먼저 가우시안 수와 SH 데이터를 줄이고 다음으로 속성 포맷을 압축한다. 모든 속성을 한 번에 저정밀도로 바꾸면 특정 조명 조건이나 가까운 시점에서 오류 원인을 찾기 어려워진다.
권장 압축 우선순위
- 불필요한 가우시안 제거: 가려진 내부 데이터, 플레이 영역 밖 데이터, 낮은 불투명도 노이즈를 정리한다.
- SH 차수 축소: 원경 또는 저중요 청크에서 3차 SH를 1차 SH나 DC 색상으로 낮춘다.
- 청크 로컬 양자화: 위치와 log scale을 uint16으로 저장한다.
- 회전·색상 패킹: 회전은 snorm 패킹, 색상은 UNORM8·RGB10A2·FP16 중 아트 검수 결과에 맞춰 선택한다.
- 가시 청크만 상주: 비동기 I/O로 인접 청크를 미리 읽고 멀어진 청크는 GPU 버퍼에서 해제한다.
속성 레이아웃 예시
아래는 구조화 버퍼에 넣기 위한 개념적 HLSL 예시다. 실제 정렬 규칙은 사용하는 그래픽 API와 버퍼 형식에 맞춰 확인해야 한다.
struct PackedGaussian
{
uint2 positionQ; // qx | qy << 16, qz는 두 번째 uint의 하위 16비트
uint rotationPacked; // 압축 quaternion 또는 10:10:10:2
uint2 scaleOpacity; // log scale 3개 + opacity 8비트 패킹
uint colorDC; // RGB10A2 또는 RGBA8
uint sh01; // 선택적: 저차 SH 계수 패킹
};
float3 DecodePosition(uint2 packed, float3 boundsMin, float3 boundsSize)
{
uint3 q = uint3(packed.x & 0xFFFF, packed.x >> 16, packed.y & 0xFFFF);
return boundsMin + (float3(q) / 65535.0) * boundsSize;
}
중요한 점은 PackedGaussian의 크기만 줄이는 것이 아니다. LOD마다 필요한 속성만 읽도록 버퍼를 분리해야 한다. 원거리 LOD가 SH를 쓰지 않는다면 SH 버퍼 자체를 바인딩하거나 읽지 않게 설계해야 실제 대역폭 절감으로 이어진다.
GPU 컬링과 정렬은 어떻게 구현할까?
Step 1. CPU는 청크만 고른다
CPU는 카메라 프러스텀과 스트리밍 반경으로 청크 AABB를 선별한다. 개별 가우시안까지 CPU가 검사하지 않는다. 선택된 청크 목록과 카메라 행렬을 GPU에 전달한다.
Step 2. Compute Shader에서 가우시안을 컬링한다
Compute Shader는 가우시안 중심과 최대 반경을 사용해 프러스텀 밖 항목을 제외한다. 가능하면 Hi-Z 깊이 피라미드로 화면에 완전히 가려진 청크 또는 스플랫을 추가 제거한다. 다만 반투명 스플랫은 깊이 테스트 방식에 따라 과도하게 제거될 수 있으므로 보수적인 오클루전 판정을 사용해야 한다.
Step 3. 깊이 키를 만들고 간소화된 정렬을 적용한다
살아남은 가우시안에 대해 뷰 공간 깊이를 키로 만들고 GPU radix sort 또는 깊이 버킷 분류를 적용한다. 배경처럼 겹침이 적은 장면에서는 청크 단위 정렬과 64~256개 깊이 버킷만으로도 충분한 경우가 많다.
float viewDepth = mul(_ViewMatrix, float4(centerWS, 1.0)).z;
uint bucket = (uint)clamp((-viewDepth - _Near) * _BucketScale, 0.0, 255.0);
InterlockedAdd(_BucketCounts[bucket], 1);
정렬 품질은 반드시 실제 카메라 이동과 알파 누적 장면에서 확인한다. 풀 소트가 시각적으로 꼭 필요한지 먼저 검증해야 한다. 항상 완전 정렬을 강제하면 프레임 시간의 상당 부분을 정렬에 사용하게 된다.
셰이더에서 화면 크기와 알파를 안정화하는 방법
카메라에 가까운 가우시안의 화면 타원이 지나치게 커지면 과도한 필레이트와 뭉개짐이 생긴다. 반대로 너무 작으면 깜빡임이 발생한다. 투영된 공분산의 고유값으로 스플랫 반경을 구한 뒤 최소·최대 픽셀 반경을 제한하는 방식을 사용한다.
여기서 (\lambda_1,\lambda_2)는 화면 공간 공분산의 고유값이고 (k)는 가우시안 절단 범위를 정하는 상수다. 절단 범위와 알파 임계값은 아트 품질과 오버드로우의 균형점이다.
float alpha = saturate(decodedOpacity * exp(-0.5 * mahalanobisDistance));
clip(alpha - _AlphaCutoff);
clip은 낮은 기여도의 픽셀을 빠르게 버릴 수 있지만 너무 높은 _AlphaCutoff은 입자감과 깜빡임을 만든다. 가까운 LOD와 원거리 LOD에 같은 값을 쓰지 말고 LOD별로 임계값을 조정하는 편이 낫다.
성능 검증은 어떤 지표로 해야 할까?
3DGS 최적화는 파일 크기 하나만 보고 판단하면 안 된다. 다음 지표를 같은 카메라 경로에서 전후 비교한다.
| 지표 | 확인할 문제 |
|---|---|
| 평균·P99 GPU frame time | 순간적인 정렬·스트리밍 스파이크 |
| 상주 GPU 메모리 | 청크 교체 시 VRAM 예산 초과 |
| 프레임당 활성 가우시안 수 | 컬링·LOD 효과 |
| 픽셀당 평균 오버드로우 | 알파 블렌딩·필레이트 병목 |
| 디스크 읽기량과 업로드량 | 이동 중 스트리밍 끊김 |
| LOD 전환 프레임 캡처 | 팝핑·노출 변화·깜빡임 |
목표 예산은 플랫폼별로 먼저 고정한다. 예를 들어 특정 플랫폼에서 배경 3DGS에 VRAM 300 MB, 프레임당 활성 가우시안 50만 개, GPU 시간 2 ms라는 예산을 잡았다면 모든 콘텐츠 청크가 이 범위에 들어오는지 자동 검증하는 것이 좋다. 숫자는 프로젝트의 해상도, 목표 FPS, 다른 렌더링 비용에 맞춰 바꿔야 한다.
자주 묻는 질문 (FAQ)
3D Gaussian Splatting은 메시를 완전히 대체할 수 있는가?
아니다. 충돌, 내비게이션, 정밀 그림자, 상호작용, 실루엣이 중요한 오브젝트에는 메시가 여전히 필요하다. 3DGS는 현실 캡처 기반의 정적 배경이나 접근 불가능한 원경에서 특히 유리하다.
원본 PLY를 런타임에서 바로 읽어도 되는가?
프로토타입에는 가능하지만 출시용으로는 권장하지 않는다. PLY는 런타임 스트리밍과 GPU 직접 업로드에 맞춰진 포맷이 아니므로 청크·LOD·양자화 정보를 포함한 바이너리 에셋으로 변환하는 편이 낫다.
SH를 제거하면 품질이 얼마나 떨어지는가?
광택, 반사, 관찰 방향에 따른 색 변화가 큰 표면에서 차이가 난다. 하지만 원거리 배경이나 확산 반사가 대부분인 영역은 DC 색상만으로도 허용 가능한 결과가 나올 수 있다. 대표 카메라 경로에서 LOD별 스크린샷과 동영상으로 판단해야 한다.
정리
실시간 게임용 3DGS의 핵심은 원본 품질을 무조건 보존하는 데 있지 않다. 플레이어가 실제로 보는 범위와 거리에서 필요한 정보를 남기는 데 있다. 청크 분할 → LOD 재표본화 → SH·속성 압축 → GPU 컬링·정렬 → 상주 메모리 검증 순서로 구축하면 캡처 기반 배경의 장점을 유지하면서도 런타임 예산 안에서 관리할 수 있다.
처음에는 한 개의 대표 구역으로 가우시안 수, SH 차수, 청크 크기, 정렬 방식을 계측해 기준값을 잡고 그 기준을 전체 환경 에셋의 빌드 규칙으로 확장하는 것이 가장 안정적이다.



