3D Gaussian Splatting을 실시간 배경 엔진 에셋으로 이식하고 최적화하는 방법
3D Gaussian Splatting 데이터를 게임 엔진의 실시간 배경으로 전환하는 파이프라인을 정리한다. 가우시안 축소, 타일 컬링, 정렬, 셰이더 렌더링, 메모리·모바일 최적화 기준까지 단계별로 다룬다.

핵심 요약
3D Gaussian Splatting(3DGS)은 사진 기반 장면을 많은 3D 가우시안 타원체와 SH(Spherical Harmonics) 색상 계수로 표현해 빠르게 재구성한다. 이를 게임 배경으로 쓰려면 원본 데이터를 그대로 그리기보다 기여도가 낮은 splat 제거, 화면 타일 단위 컬링, 깊이 정렬, 압축된 GPU 버퍼를 결합해야 한다. 가장 실용적인 구조는 CPU가 카메라·LOD 상태만 갱신하고 Compute Shader가 가시 splat 목록과 정렬 키를 만들며 인스턴스드 쿼드 드로우가 최종 블렌딩을 수행하는 방식이다.
3D Gaussian Splatting을 배경 에셋으로 이식할 때 무엇이 달라지는가?
연구·캡처 도구의 3DGS 데이터는 자유 시점 품질을 우선한다. 반면 게임 런타임은 프레임 시간, VRAM, 카메라 이동 범위, 라이팅 연동, 충돌과 네비게이션처럼 다른 제약을 갖는다. 따라서 3DGS를 메시의 완전한 대체재로 보기보다 멀리 있는 고정 배경이나 접근 불가능한 풍경을 위한 뷰 의존 렌더링 에셋으로 배치하는 편이 안전하다.
| 항목 | 캡처·연구용 3DGS | 실시간 배경용 3DGS |
|---|---|---|
| 우선순위 | 재구성 품질 | 고정 프레임 시간 |
| 데이터 규모 | 수백만 splat도 허용 | 장면·플랫폼별 예산 필요 |
| 카메라 | 넓은 자유 이동 | 플레이 영역과 시야 범위 제한 |
| 조명 | 캡처된 외형 보존 | 대체로 베이크드 배경 조명 |
| 상호작용 | 제한적 | 충돌·그림자는 별도 프록시 필요 |
가우시안 하나는 중심 위치 , 회전 , 축별 스케일 , 불투명도 , 색상 또는 SH 계수로 구성한다. 공분산을 카메라 좌표계로 변환한 뒤 투영 야코비안 를 사용하면 화면 공간 공분산을 다음처럼 근사할 수 있다. 여기서 은 가우시안 회전을 카메라 좌표계에서 표현한 값이다.
이 공분산으로 화면에서 타원형 footprint를 만들고 중심에서 멀어질수록 감소하는 가중치로 알파 블렌딩한다.

왜 원본 PLY를 그대로 렌더링하면 프레임이 무너지는가?
문제는 단순한 점 개수가 아니다. splat마다 투영, 화면 영역 계산, 가시성 판단, 반투명 블렌딩 순서가 필요하다. 큰 splat은 여러 화면 타일을 덮고 카메라가 움직일 때마다 깊이 순서도 달라진다.
특히 일반적인 알파 합성은 뒤에서 앞으로 그려야 한다.
깊이 정렬 없이 그리면 반투명 경계가 뒤집히거나 배경이 탁해진다. 모든 splat을 전역 정렬하면 품질은 좋지만 수십만 개 이상에서는 정렬과 overdraw가 병목이 된다.
먼저 정해야 할 런타임 예산
플랫폼별 목표를 먼저 정하고 데이터 축소 기준을 그 예산에 맞춘다.
| 목표 플랫폼 예시 | 시험할 가시 splat 수 예시 | GPU 프레임 예산 예시 | 검토할 전략 |
|---|---|---|---|
| 고사양 PC·콘솔 | 300,000~1,000,000 | 4~8 ms | 타일 컬링, GPU 정렬, SH 1~3차 선택 |
| Steam Deck급 | 100,000~300,000 | 3~5 ms | 강한 LOD, SH 0차 또는 1차 |
| 모바일 | 30,000~100,000 | 2~4 ms | 구역 분할, 저차 색상, 제한된 카메라 |
표의 수치는 특정 벤치마크에서 검증된 권장값이 아니라 테스트 계획을 세울 때 사용할 수 있는 예시다. 실제 기준은 목표 해상도, splat의 평균 화면 면적, 투명 배경과의 중첩도, GPU 아키텍처에 따라 측정해야 한다.
실시간 3DGS 배경 에셋은 어떻게 만들까?
1. 캡처 장면을 플레이 가능한 영역 기준으로 자른다
학습 결과의 모든 영역이 게임에 필요하지는 않다. 플레이어가 볼 수 없는 뒷면, 하늘이나 바닥의 노이즈, 이동 불가능한 구역은 오프라인 단계에서 제거한다. 배경의 근거리·중거리·원경을 셀 또는 구역으로 나누면 스트리밍과 LOD 기준도 단순해진다.
- 카메라가 접근하지 않는 원경은 별도 3DGS 셀로 분리한다.
- 충돌이 필요한 물체는 저폴리 메시 또는 단순 콜라이더를 별도로 만든다.
- 그림자 캐스터가 필요한 실루엣은 메시 프록시를 둔다.
- 움직이는 오브젝트와 접하는 구역은 3DGS 대신 메시 품질을 우선한다.
2. 오프라인에서 splat을 줄이고 패킹한다
가우시안 축소는 화면 기여도를 기준으로 한다. 불투명도가 낮고 매우 작고 이웃 splat과 색·위치가 유사한 데이터부터 병합하거나 제거한다. 단순 거리 기반 제거만 하면 가까운 얇은 구조가 무너질 수 있으므로 대표 카메라 세트에서의 투영 오차를 함께 확인한다.
권장 패킹 예시는 다음과 같다.
| 속성 | 런타임 형식 예시 | 비고 |
|---|---|---|
| 위치 | float3 | 큰 월드는 셀 로컬 좌표 사용 |
| 회전 | snorm16x4 쿼터니언 | 셰이더에서 정규화 |
| log scale | half3 | 원본이 로그 스케일이면 유지 |
| opacity | unorm8 또는 half | 페이드 품질에 따라 선택 |
| 색상 | unorm8x4 | SH 0차만 쓸 때 |
| SH 계수 | half 배열 | 거리 LOD별 차수 축소 |
float 중심 데이터를 무조건 절반 정밀도로 바꾸기보다 셀 원점 기준의 로컬 좌표로 변환한 뒤 half 사용 가능 범위를 검증한다. 넓은 월드에서 바로 half3를 쓰면 위치 지터가 생길 수 있다.
3. Compute Shader에서 컬링과 정렬 키를 만든다
매 프레임 모든 splat을 드로우 호출로 제출하지 않는다. Compute Shader가 frustum과 거리 LOD를 통과한 splat에 원자적 인덱스를 할당하고, 같은 인덱스에 visible index와 카메라 공간 깊이 키를 기록한다. 이후 GPU radix sort 또는 깊이 버킷 정렬을 적용한다.
flowchart LR
A[압축된 Splat Buffer] --> B[Compute: Frustum·LOD 컬링]
B --> C[Visible Index Buffer]
C --> D[Compute: Depth Key 생성]
D --> E[GPU 정렬 또는 Depth Bucket]
E --> F[Instanced Quad Draw]
F --> G[Gaussian Fragment Blend]
전역 정렬 비용이 높다면 깊이 버킷을 먼저 시도할 수 있다. 카메라 공간 깊이를 64~256개 버킷으로 양자화한 후 버킷을 뒤에서 앞으로 렌더링한다. 교차하는 얇은 구조에서 정확도는 낮아지지만 원경 배경에서는 비용 대비 품질이 좋은 경우가 많다.
RWStructuredBuffer<uint> _Visible;
RWStructuredBuffer<uint> _DepthKeys;
RWStructuredBuffer<uint> _VisibleCount;
uint _VisibleCapacity;
uint _SplatCount;
[numthreads(64, 1, 1)]
void CullSplats(uint3 dispatchId : SV_DispatchThreadID)
{
uint id = dispatchId.x;
if (id >= _SplatCount) return;
Splat splat = _Splats[id];
if (!IntersectsFrustum(splat.position, splat.radius)) return;
float depth = -mul(_View, float4(splat.position, 1)).z;
if (depth <= 0 || depth > _MaxDistance) return;
uint visibleIndex;
InterlockedAdd(_VisibleCount[0], 1, visibleIndex);
if (visibleIndex >= _VisibleCapacity) return;
_Visible[visibleIndex] = splat.index;
_DepthKeys[visibleIndex] = asuint(depth);
}
이 예제는 _VisibleCount[0]을 디스패치 전에 0으로 초기화하고, _Visible과 _DepthKeys에 같은 예약 인덱스를 사용한다. 카운터 값은 용량을 넘을 수 있으므로 후속 패스의 처리 개수를 _VisibleCapacity 이하로 제한한 뒤 정렬과 indirect draw 인수를 갱신한다. InterlockedAdd는 원자적으로 인덱스를 예약하므로 두 버퍼의 visible index와 depth key가 서로 어긋나지 않는다.
가우시안 splat 셰이더는 어떤 계산을 해야 하는가?
버텍스 단계는 카메라를 향한 쿼드의 각 정점을 화면 공간 타원으로 변환한다. 프래그먼트 단계는 타원 내부의 로컬 좌표 로부터 가우시안 감쇠값을 계산하고 충분히 작아진 알파를 버린다.
아래 셰이더는 원형으로 정규화한 쿼드 좌표를 사용하는 단순화 예시다. 전체 3DGS 구현에서는 투영된 공분산에 맞춰 타원 footprint 좌표를 계산해야 한다.
float4 Fragment(Varyings input) : SV_Target
{
float r2 = dot(input.localPos, input.localPos);
if (r2 > 1.0) discard;
float gaussian = exp(-0.5 * r2 * _Falloff);
float alpha = input.opacity * gaussian;
if (alpha < _AlphaCutoff) discard;
return float4(input.color.rgb * alpha, alpha);
}
프리멀티플라이드 알파를 사용한다면 블렌드 상태와 출력 색상을 맞춰야 한다. 예를 들어 Blend One OneMinusSrcAlpha에서는 RGB에 이미 알파를 곱해 출력한다. 이 규칙이 어긋나면 가장자리가 밝아지거나 어두운 테두리가 생긴다.

3D Gaussian Splatting의 VRAM과 overdraw는 어떻게 최적화할까?
VRAM 절감 우선순위
- 보이지 않는 데이터 제거: 오프라인 클린업이 가장 큰 절감 효과를 낸다.
- SH 차수 LOD: 원거리에는 DC 성분 또는 1차 SH만 사용한다.
- 속성 패킹: opacity·색상·회전은 필요한 정밀도만 쓴다.
- 셀 스트리밍: 현재 카메라 주변 셀만 GPU에 올린다.
- 중복 버퍼 제거: 원본 구조체와 렌더 구조체를 동시에 상주시킬 이유가 없는지 확인한다.
overdraw 절감 우선순위
- 프러스텀 컬링과 최대 거리 제한을 적용한다.
- 투영 크기가 1~2픽셀보다 작은 splat은 병합 LOD 또는 제거 대상으로 보낸다.
- 큰 splat에 화면 면적 상한을 두고 임계값을 넘으면 낮은 해상도 LOD로 전환한다.
- 배경 렌더 패스를 불투명 지오메트리 뒤에 배치해 깊이 버퍼를 활용한다.
- 가능한 구역에는 메시·스카이박스·임포스터를 혼합한다.
화면 면적이 넓은 splat 몇 개가 작은 splat 수만 개보다 비쌀 수 있다. 따라서 프로파일링에서는 단순 visible count뿐 아니라 fragment 수, early-z 효율, 타일별 overdraw를 함께 본다.
Unity와 Unreal Engine에서 어떤 렌더링 경로가 적합한가?
| 엔진 | 권장 경로 | 확인할 점 |
|---|---|---|
| Unity 6 URP | ComputeShader + Graphics.RenderPrimitivesIndirect 또는 Render Graph 연동 | URP Renderer Feature 순서, depth texture, 모바일 API |
| Unity 6 HDRP | Custom Pass 또는 Render Graph 패스 | HDRP 노출·TAA·볼류메트릭과의 합성 |
| Unreal Engine 5 | RDG(Render Dependency Graph) 기반 compute pass + indirect draw | Nanite 대체가 아니라 배경 렌더 패스로 운용 |
엔진별 API는 달라도 핵심 데이터 흐름은 같다. CPU는 셀 로딩과 카메라 파라미터를 전달하고 GPU는 가시성 판단·정렬·인스턴스 생성을 처리한다. 매 프레임 CPU에서 visible list를 읽어 오거나 splat별 draw call을 발행하면 이 구조의 이점이 사라진다.
자주 묻는 질문 (FAQ)
3D Gaussian Splatting은 Nanite나 일반 메시를 완전히 대체할 수 있는가?
아니다. 3DGS는 캡처된 외형을 빠르게 보여 주는 데 강하지만 정밀 충돌, UV 기반 머티리얼, 동적 변형, 정확한 그림자에는 일반 메시가 더 적합하다. 원경 배경과 스캔 기반 정적 환경에 혼합하는 방식이 현실적이다.
투명도 정렬을 생략해도 되는 경우가 있는가?
카메라 이동이 작고 원경 위주인 배경이라면 깊이 버킷 정렬로 충분할 수 있다. 다만 가까운 얇은 오브젝트, 강한 패럴랙스, 반투명 splat의 중첩이 많은 장면은 GPU 정렬 품질을 비교해야 한다.
모바일에서 SH를 유지해야 하는가?
필수는 아니다. 먼 배경이라면 SH 0차 색상만으로 시작하고 시점에 따른 색 변화가 눈에 띄는 구역에만 1차 SH를 적용하는 편이 비용 대비 효율적이다.
마무리
실시간 3DGS 배경의 핵심은 고품질 원본을 그대로 재생하는 일이 아니라 플레이어가 실제로 보는 범위에 맞춰 데이터를 다시 설계하는 일이다. 먼저 메시와 3DGS의 역할을 나누고 오프라인 축소와 GPU 컬링을 적용한 뒤 정렬 정확도와 overdraw를 프로파일링으로 조정하자. 이 순서라면 사진 기반 장면의 밀도를 유지하면서도 게임 프레임 예산 안에 배경 에셋을 넣을 수 있다.



