Skip to content

Gaussian Splat

Technical Module · Runtime

Gaussian
Splat

해석적 브러쉬 페인팅 기반 가우시안 필드 생성 & 렌더링 모듈.
글로벌 depth sort 없이 타일 클러스터 + Beer-Lambert OIT로 실시간 렌더링.

16×16 Tile ClusterBeer-Lambert OITAnalytical BrushSpring-Mass Physics
렌더링 페인팅 물리

TECHNICAL MODULE

Gaussian Splat

해석적 브러쉬 페인팅 기반 가우시안 필드 생성 & 렌더링 모듈

이 프로젝트의 아트 에셋은 메쉬나 2D 텍스처 대신 Gaussian Splat(GS)을 기본 단위로 사용한다. SSIM 학습 기반 재구성이 아닌, 브러쉬 스트로크를 기하학적으로 해석하여 가우시안을 스폰·병합·삭제하는 방식으로 GS 필드를 직접 저작한다. 렌더링은 타일 클러스터 기반 OIT 파이프라인을 사용한다.

16×16 Tile Cluster Beer-Lambert OIT Analytical Brush Spring-Mass Physics

Gaussian Splat 렌더링

GS 필드를 실시간으로 렌더링하기 위한 파이프라인. 전통적인 3DGS 파이프라인이 요구하는 글로벌 depth sort(Radix Sort)를 제거하고, 타일 기반 클러스터링과 Beer-Lambert 밀도 누적으로 OIT를 처리한다.

1.1 타일 클러스터 렌더링

화면을 16×16 픽셀 단위의 타일로 분할한다. 각 GS는 스크린스페이스 투영 후 자신의 2D 범위(AABB)가 겹치는 타일 목록에 등록된다. 이후 렌더링은 타일 단위로 독립적으로 처리된다.

클러스터링 기준은 순수 스크린스페이스이며, 별도의 뎁스 구간 분류나 Z-버킷을 사용하지 않는다. 이로 인해 전체 GS 집합에 대한 글로벌 정렬 패스가 불필요하다.

💡
왜 타일 클러스터인가

전통적인 3DGS는 올바른 alpha compositing을 위해 카메라 기준 뎁스 순으로 모든 GS를 정렬(Radix Sort)해야 한다. 그러나 이 프로젝트는 Beer-Lambert OIT를 사용하므로 정렬이 필요 없고, 타일 단위 병렬 처리만으로 GPU 파이프라인을 구성할 수 있다.

처리 흐름

  1. Projection Pass — 각 GS를 스크린스페이스로 투영, 2D AABB 계산
  2. Binning Pass — AABB 기준으로 겹치는 16×16 타일에 GS ID 등록
  3. Tile Shade Pass — 타일별로 등록된 GS 목록을 순회하며 Beer-Lambert 누적
  4. Resolve Pass — 타일 결과를 프레임버퍼에 합성

1.2 Beer-Lambert 밀도 누적 OIT

각 픽셀에서 GS들의 기여를 합산할 때 Beer-Lambert 법칙에 따른 밀도 누적을 사용한다. 이 방식은 기여 순서에 무관하게(order-independent) 동일한 결과를 보장하며, 레이어 수 제한도 없다.

Beer-Lambert 투과율
T(x) = exp(−∫₀ˣ σ(s) ds)
연속형 — σ(s): 위치 s에서의 소광 계수(밀도)
이산화 — 픽셀당 GS 누적
σᵢ = densityi × 𝒢(p, μᵢ, Σᵢ)
Tᵢ = exp(−σᵢ)
C += Taccum × colorᵢ × (1 − Tᵢ)
Taccum ×= Tᵢ
𝒢: 픽셀 좌표 p에서의 2D Gaussian 가중치
🔑
순서 독립성의 근거

전체 투과율 exp(−Σσᵢ)는 곱셈 교환 법칙에 의해 순서에 무관하다: exp(−σ₁)·exp(−σ₂) = exp(−σ₂)·exp(−σ₁). 색상 누적 역시 T_accum을 가중치로 사용하는 구조가 동일한 교환 가능성을 보장한다. 따라서 Radix Sort 없이 타일 내 GS를 임의 순서로 처리할 수 있다.

방식 비교
전통 3DGS
  • ✗ 글로벌 Radix Sort 필수
  • ✗ 카메라 이동 시 매 프레임 재정렬
  • ✗ GPU↔CPU 동기화 비용
  • ✓ 정확한 alpha compositing
이 파이프라인
  • ✓ 정렬 불필요
  • ✓ 완전 GPU-side 처리
  • ✓ 레이어 제한 없음
  • △ 물리적 근사 (시각 차이 미미)

Gaussian Splat 페인팅

GS 필드를 직접 저작하는 웹 기반 페인팅 툴. 브러쉬 스트로크를 기하학적으로 해석하여 GS를 스폰·병합·삭제한다. SSIM 학습 없이 아티스트가 실시간으로 GS 필드를 조각한다.

2.1 툴 인터페이스 & 데이터 스펙

🌐
Web-Based Editor
브라우저 기반 Canvas / WebGPU 렌더 뷰포트. Electron 래핑 여부와 무관하게 웹 표준 API 기준으로 구현.
🖌️ 브러쉬 사이즈 · 밀도 · 색상 슬라이더
👁️ 실시간 GS 렌더 프리뷰 (Beer-Lambert)
🗂️ 레이어 패널 (GS 서브셋 단위 관리)
⚙️ 병합 임계값 설정 패널

GS 데이터 구조

단일 GS는 아래 필드로 구성된다. CPU(Unity Job System)와 GPU(HLSL/WGSL) 간 레이아웃이 다르므로 주의한다.

CPU 레이아웃 — 패딩 없음
Unity Job System / C# unmanaged struct
struct GaussianSplat {
  float3 pos;        // 12 B  위치
  float4 quaternion; // 16 B  회전
  float3 extents;    // 12 B  반축 길이 (σ)
  float  density;    //  4 B  소광 계수
  float3 color;      // 12 B  선형 RGB
}  // 총 56 B — 패딩 없음
pos
12B
quat
16B
ext
12B
dens
4B
color
12B
GPU 레이아웃 — 16B 정렬
HLSL / WGSL StructuredBuffer
struct GaussianSplatGPU {
  float3 pos;        // 12 B
  float  _pad0;      //  4 B  ← 패딩 (16B 맞춤)
  float4 quaternion; // 16 B
  float3 extents;    // 12 B
  float  density;    //  4 B  ← extents+density = 16B
  float3 color;      // 12 B
  float  _pad1;      //  4 B  ← 패딩 (16B 맞춤)
}  // 총 64 B — 4 × 16B 블록
pos
12B
pad
4B
quat
16B
ext
12B
dens
4B
color
12B
pad
4B
⚠️
CPU → GPU 업로드 시 주의

CPU 버퍼(56B)를 GPU 버퍼(64B)로 업로드할 때 직접 memcpy 불가. pos 이후 4B 패딩 삽입이 필요하다. 업로드 루틴에서 필드별 복사 또는 전용 변환 Job으로 처리한다.

2.2 브러쉬 스트로크 해석적 처리

브러쉬 스트로크는 연결된 선분(connected segments)의 폴리라인으로 정의된다. 이 폴리라인을 기반으로 GS 스폰 공간을 결정하고, 기존 GS와의 병합/삭제를 처리한다.

스폰 규칙

각 폴리라인 점에서 선분까지의 SDF(Point-to-Segment Signed Distance)를 계산하고, 선분 집합에 대한 보로노이 분할을 수행한다. 각 보로노이 셀은 하나의 선분에 귀속되며, 셀 내부에서 GS를 스폰한다.

1
SDF 계산 — 임의의 공간 점 p에서 각 선분 segᵢ까지의 거리 d(p, segᵢ)를 계산. d = |p − closest_point_on_seg(p, segᵢ)|
2
보로노이 분류 — 각 셀 포인트를 가장 가까운 선분에 귀속. 브러쉬 반경 내부(d < r_brush)인 영역만 유효 셀.
3
노이즈 오프셋 스폰 — 각 셀 중심에 소량의 노이즈 오프셋을 더해 GS를 배치. 균등 분포 방지를 위해 미리 샘플링된 포아송 디스크 패턴을 선분 로컬 좌표계로 변환하여 사용.
4
방향 초기화 — 신규 GS의 주축(quaternion)은 귀속 선분의 방향 벡터를 기준으로 설정. Extents는 브러쉬 사이즈 파라미터에서 결정.
🔧
구현 노트

런타임 스폰 시 포아송 디스크를 매번 생성하는 것은 비용이 높다. 다양한 반경·밀도 조합에 대한 포아송 디스크 패턴을 사전 생성 후 테이블로 보관하고, 스트로크 시 해당 테이블에서 참조하는 방식을 권장한다.

병합 규칙

스폰된 GS가 기존 GS와 충분히 유사하다고 판단되면 두 GS를 하나로 병합한다. 세 가지 기준을 AND 조건으로 동시에 만족해야 병합이 발생한다.

📍
중심점 거리
|posᴬ − posᴮ| < τd
두 GS 중심 사이의 유클리드 거리. 공간적으로 근접한 경우에만 병합 후보. τd는 브러쉬 반경에 비례하여 동적 조정 가능.
🔄
회전 차이
|qᴬ · qᴮ| > cos(τr / 2)
두 quaternion의 내적으로 회전 각도 차이를 평가. 내적 절댓값이 임계치 이상이면 방향이 충분히 유사함. τr는 최대 허용 각도 차이(라디안).
📐
스케일 차이
max(eᴬ/eᴮ, eᴮ/eᴬ) < τs (축별)
extents 각 축의 비율로 크기 유사도 평가. 비율이 τs 미만이면 세 축 모두 통과 시 조건 충족. τs = 1.5 ~ 2.0 권장.
병합 결과 GS 계산
posmerged = (mᴬ · posᴬ + mᴮ · posᴮ) / (mᴬ + mᴮ) 질량 가중 평균
qmerged = slerp(qᴬ, qᴮ, mᴮ / (mᴬ + mᴮ)) 구면 선형 보간
extentsmerged = (mᴬ · extentsᴬ + mᴮ · extentsᴮ) / (mᴬ + mᴮ) 질량 가중 평균
densitymerged = max(densityᴬ, densityᴮ) 보수적 합산 (과포화 방지)
colormerged = (mᴬ · colorᴬ + mᴮ · colorᴮ) / (mᴬ + mᴮ) 질량 가중 평균

삭제 규칙

🗑️
브러쉬 직접 삭제 — Erase 브러쉬 반경 내 모든 GS 즉시 제거.
📉
밀도 감쇠 삭제 — density가 최소 임계값 τmin 이하로 내려가면 자동 삭제. 물리 시뮬레이션 중 감쇠 누적으로 발생 가능.
🔀
병합 후 소멸 — 병합이 완료된 소스 GS는 merged GS로 대체되어 제거.

2.4 공간 해싱 — NativeChunkedVolumeOctree

GS 필드에서 반경 r 이내의 이웃 GS를 찾는 공간 쿼리는 브러쉬 병합과 물리 시뮬레이션 모두에서 반복적으로 필요하다. 이를 위해 이미 구현된 NativeChunkedVolumeOctree 모듈을 재사용하여 공간 해싱 레이어를 구성한다.

모듈 개요

NativeChunkedVolumeOctree는 월드 공간을 고정 크기 청크로 분할하고, 각 청크 내부를 옥트리로 세분화하는 공간 가속 구조다. GS 필드 전체를 이 구조에 등록하면 다음 쿼리를 O(log n) 수준으로 처리할 수 있다.

🔍
Radius Query — 특정 위치 p 주변 반경 r 내 모든 GS 반환. 브러쉬 병합 후보 탐색(§2.2), KNN 패스(§3.2)에 공통 사용.
📦
AABB Query — 축 정렬 박스 내 GS 반환. 브러쉬 Erase 패스 및 Softbody 충돌 경계 처리에 활용.
🔄
갱신 시점 — GS 위치가 변경되는 두 경우, 즉 브러쉬 스트로크 후 스폰·병합·삭제 완료 시점과 물리 적분 틱 후 위치 갱신 완료 시점에 옥트리를 재구축한다. 청크 단위 부분 갱신이 가능하므로 매 틱 전체 재구축은 불필요하다.
🔧
구현 참조

NativeChunkedVolumeOctree는 기존 모듈로 이미 동작하는 코드가 존재한다. GS 시스템은 이 모듈을 직접 사용하며, 별도 공간 구조를 중복 구현하지 않는다. GS ID를 옥트리 leaf 데이터로 저장하고, 쿼리 결과로 GS 인덱스 배열을 반환하는 인터페이스를 사용한다.

Mass & 물리 처리

GS의 크기(extents)와 밀도(density)가 명시적으로 저장되어 있으므로, 각 GS의 질량은 별도 계산 없이 결정적으로 도출된다. 이 질량을 기반으로 Spring-Mass Softbody 시뮬레이션을 구성한다.

3.1 결정적 질량 도출

GS의 질량은 density 필드와 extents로부터 결정적으로 계산된다. 부피는 가우시안 타원체를 정확하게 적분하지 않고, extents의 곱에 반비례하는 근사값을 사용한다.

질량 근사식
mass = density / (extents.x × extents.y × extents.z)
density: 소광 계수 (클수록 불투명·밀집) · extents: 각 축 반경
💡
물리적 직관

extents가 클수록(퍼진 스플랫) 동일한 density라도 공간에 희박하게 분포하므로 단위 질점으로서의 질량은 작아진다. 반대로 작고 밀집된 스플랫은 질량이 크며 물리 자극에 덜 반응한다. 이는 페인팅에서 세밀한 디테일 영역은 안정적이고, 넓게 칠한 영역은 유연하게 반응하는 직관적인 물리 거동을 만들어낸다.

질량이 GS 데이터에서 결정적으로 계산되기 때문에 별도의 물리 파라미터를 아티스트가 설정할 필요가 없다. 브러쉬 스트로크로 GS를 칠하는 행위 자체가 물리적 특성을 동시에 정의한다.

3.2 Spring-Mass Softbody 시뮬레이션

GS 필드를 질량-스프링 네트워크로 모델링하여 Softbody 물리 효과를 구현한다. 시뮬레이션 전 K-Nearest Neighbor(KNN) 패스를 실행하여 각 GS의 이웃 목록을 구성하고, 이웃 사이에 스프링을 배치한다.

K-Nearest Neighbor 패스

NativeChunkedVolumeOctree의 Radius Query를 이용해 각 GS의 이웃을 탐색한다. 이웃 수는 최대 6개로 제한한다. 6은 3D 정육면체 면 접촉 수에 대응하는 보수적 상한이며, GPU Job 버퍼를 고정 크기(GS당 6슬롯)로 유지할 수 있어 메모리 레이아웃이 단순해진다.

파라미터설명
K (최대 이웃 수)6GS당 스프링 연결 상한
탐색 반경r_spring브러쉬 평균 extents 기준 동적 설정
정렬 기준거리 오름차순가장 가까운 6개 선택
갱신 시점옥트리 재구축 후GS 추가·삭제·이동 시 재실행
스프링 힘
Fspring = −k · (|Δpos| − l₀) · n̂
k: 강성 계수, l₀: 자연 길이 (초기 거리), n̂: 연결 방향 단위벡터
감쇠 힘
Fdamp = −c · Δvel
c: 감쇠 계수, Δvel: 상대 속도. 진동 수렴 및 에너지 소산.
운동 적분
a = Ftotal / mass
vel += a · dt
pos += vel · dt
Semi-implicit Euler. mass는 GS 필드에서 결정적으로 계산.
🎨
아트 파이프라인과의 통합

물리 시뮬레이션은 런타임에 동적으로 활성화된다. 예를 들어 천, 연기, 수풀 등의 GS 레이어에 Softbody를 적용하면 외력(바람, 충돌)에 반응하는 GS 변형이 발생한다. GS의 extents와 quaternion이 실시간으로 갱신되므로 렌더링 파이프라인의 변경 없이 자연스러운 변형이 표현된다.

🐛
Known Issue — Volume Inversion (압력 제약 미해결)

여러 이웃 GS가 동시에 압축 방향으로 스프링 힘을 가할 때, 누적 압력이 extents를 음수로 만드는 Volume Inversion이 발생할 수 있다. GS의 부피가 음수가 되면 렌더링 시 가우시안이 뒤집혀 아티팩트가 생긴다.

현재 pressure constraint(반발 제약)가 구현되지 않아 인버전 발생을 막지 못한다. 임시 완화책으로 extents = max(extents, ε) 클램핑을 적용할 수 있으나 물리적으로 정확한 해결책은 아니며, 압력 제약 추가가 필요하다.

3.3 PBD 유체 시뮬레이션

💧
별도 시스템 — Softbody와 무관

Position-Based Dynamics(PBD) 는 Softbody 시뮬레이션과 독립된 별도 시스템으로, 유체 시뮬레이션에 사용된다. GS 필드 자체를 유체 입자로 해석하여 밀도 제약(density constraint)과 비압축성 조건(incompressibility)을 PBD 방식으로 처리한다. PBD 유체는 물, 용암, 연기 흐름 등 유동적인 GS 효과에 적용된다.

Spring-Mass Softbody와 PBD 유체는 적용 대상과 알고리즘 모두 다르며, 동일한 GS에 중첩 적용되지 않는다.