A real-time 3D Gaussian Splatting (3DGS) viewer built with WebGPU and TypeScript. This viewer renders 3D scenes represented as collections of Gaussian primitives, enabling high-quality novel view synthesis directly in the browser.
splat.mp4
3D Gaussian Splatting is a novel rendering technique that represents scenes as millions of 3D Gaussian primitives. Each Gaussian is defined by:
- Position (x, y, z): Center point in world space
- Covariance (3x3 matrix): Defines the ellipsoid shape and orientation
- Color (RGB): Stored as spherical harmonic coefficients or direct RGB
- Opacity (alpha): Transparency of the Gaussian
This viewer implements the full rendering pipeline described in "3D Gaussian Splatting for Real-Time Radiance Field Rendering" (Kerbl et al., 2023).
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Main Thread β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β βββββββββββββββ βββββββββββββββ βββββββββββββββ βββββββββββββββ β
β β Camera βββββΆβ Controls βββββΆβ Renderer βββββΆβ GUI β β
β β (camera.ts)β β(controls.ts)β β(renderer- β β (gui.ts) β β
β β β β β β webgpu.ts) β β β β
β βββββββββββββββ βββββββββββββββ βββββββββββββββ βββββββββββββββ β
β β β² β
β βΌ β β
β βββββββββββββββββββββββββββββββββββββββββββββββ΄ββββββββββββββββββββββββ β
β β Sort Worker Bridge β β
β β (sort-worker.ts) β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββ
β postMessage
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Web Worker β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Sort Worker β β
β β (sort-worker.worker.ts) β β
β β β’ Depth sorting (counting sort, O(n)) β β
β β β’ PLY parsing and conversion β β
β β β’ Raw .splat / converted .ply transfer β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
| Module | Description |
|---|---|
main.ts |
Application entry point, orchestrates subsystems |
renderer-webgpu.ts |
WebGPU rendering pipeline with instanced drawing |
camera.ts |
Matrix math for view/projection transforms |
controls.ts |
Orbit, pan, zoom, roll camera controls with inertia |
sort-worker.worker.ts |
Off-thread depth sorting and PLY parsing |
loader.ts |
Streaming .splat file loader |
shaders/*.wgsl |
WGSL vertex and fragment shaders |
The rendering pipeline follows these steps each frame:
ββββββββββββββββββ ββββββββββββββββββ ββββββββββββββββββ
β Camera Update ββββββΆβ View Matrix ββββββΆβ Sort Worker β
β (user input) β β (controls) β β (depth order) β
ββββββββββββββββββ ββββββββββββββββββ βββββββββ¬βββββββββ
β
ββββββββββββββββββ ββββββββββββββββββ β
β Projection ββββββΆβ GPU Render βββββββββββββββ
β Matrix β β (instanced) β sorted indices
ββββββββββββββββββ βββββββββ¬βββββββββ
β
βββββββββΌβββββββββ
β Alpha-Blended β
β Output β
ββββββββββββββββββ
Splat Buffer (32 bytes per Gaussian):
| Offset | Size | Type | Description |
|---|---|---|---|
| 0-11 | 12 | float32Γ3 | Position (x, y, z) |
| 12-23 | 12 | float32Γ3 | Scale (sx, sy, sz) |
| 24-27 | 4 | uint8Γ4 | Color (R, G, B, A) |
| 28-31 | 4 | uint8Γ4 | Rotation quaternion (quantized qw, qx, qy, qz) |
Sorted Index Buffer: Maps draw instance index to Gaussian index for back-to-front rendering.
Correct alpha blending requires back-to-front draw order. The sort worker uses 16-bit counting sort for O(n) performance:
- Compute depth: For each Gaussian, calculate
depth = dot(viewDir, position) - Find range: Track min/max depth values
- Quantize: Map depths to 16-bit integers (65,536 buckets)
- Count: Tally occurrences in each bucket
- Prefix sum: Compute cumulative counts for bucket positions
- Place: Output Gaussian indices in sorted order
This approach handles millions of Gaussians in 5-30ms, compared to ~100ms for comparison-based sorts.
The key mathematical insight of 3DGS is that a 3D Gaussian projects to a 2D Gaussian analytically. In this viewer, the GPU reconstructs the 3D covariance from:
- Scale and rotation stored in the raw
.splatrow - View matrix V
- Projection focal lengths (fx, fy)
The 2D covariance is computed as:
Ξ£βββ = J Γ V Γ Ξ£βββ Γ Vα΅ Γ Jα΅
Where J is the Jacobian of perspective projection:
J = | fx/z 0 -fxΒ·x/zΒ² |
| 0 fy/z -fyΒ·y/zΒ² |
The 2D covariance defines an ellipse. We extract its axes via eigendecomposition and render each Gaussian as a screen-aligned quad scaled along these axes. Moving this covariance reconstruction into the vertex shader keeps .splat load times low because the worker no longer precomputes packed covariance on the CPU.
The viewer uses "one-minus-dst-alpha" blending for correct transparent compositing:
dst.rgb = src.rgb Γ (1 - dst.a) + dst.rgb
dst.a = src.a Γ (1 - dst.a) + dst.a
Combined with back-to-front sorted draw order, this produces:
final = Ξ£α΅’ (colorα΅’ Γ Ξ±α΅’ Γ ββ±Ό<α΅’(1 - Ξ±β±Ό))
The fragment shader outputs premultiplied alpha: (color Γ alpha, alpha).
Binary format with 32 bytes per Gaussian:
| Offset | Size | Type | Description |
|---|---|---|---|
| 0-11 | 12 | float32Γ3 | Position (x, y, z) |
| 12-23 | 12 | float32Γ3 | Scale (sx, sy, sz) |
| 24-27 | 4 | uint8Γ4 | Color (R, G, B, A) |
| 28-31 | 4 | uint8Γ4 | Rotation quaternion (quantized) |
The viewer supports both standard and compressed PLY formats from 3DGS training tools. Attributes parsed:
x, y, z: Positionscale_0, scale_1, scale_2: Log-space scalerot_0, rot_1, rot_2, rot_3: Rotation quaternionf_dc_0, f_dc_1, f_dc_2: DC spherical harmonic (color)opacity: Logit-space opacity
- Gaussian Splatting: Full ellipse rendering with soft falloff
- Point Cloud: Simple dot rendering for debugging
- Crossfade: Smooth transition between modes
- Anaglyph: Red/cyan stereoscopic output
- Side-by-Side: VR headset compatible output
| Input | Action |
|---|---|
| Left drag | Orbit (rotate around focus point) |
| Right drag / Ctrl+drag | Pan (translate parallel to view) |
| Shift+drag | Roll (rotate around view axis) |
| Scroll wheel | Zoom (move forward/backward) |
| Arrow keys | Translate camera |
| WASD | Rotate camera |
All controls feature inertia for smooth, natural-feeling interaction.
Real-time adjustments applied in the fragment shader:
- Brightness, contrast, gamma
- Black/white level (shadow/highlight lift)
- Saturation and vibrance
- Color temperature and tint
- Global alpha
The renderer automatically reduces resolution during camera motion to maintain smooth frame rates, then restores full quality when the camera stops.
- Instanced Rendering: Single draw call renders all Gaussians
- Raw
.splatUpload Path: Avoids CPU-side covariance packing during load - Worker Thread Sorting: Keeps main thread responsive
- Counting Sort: O(n) complexity for millions of primitives
- Invalidation-Driven Rendering: Stops the RAF loop while the view is static
- Adaptive Resolution: Reduces internal resolution during motion
- Frustum Culling: Discard Gaussians outside view
- Fragment Discard: Skip pixels with negligible contribution
- WebGPU: Required for rendering (Chrome 113+, Edge 113+, Firefox Nightly)
- Web Workers: Required for off-thread sorting
- ES Modules: Modern JavaScript module support
src/
βββ main.ts # Application entry point
βββ style.css # UI styles
βββ viewer/
βββ camera.ts # Matrix math and camera poses
βββ camera-utils.ts # Camera centering utilities
βββ controls.ts # Interactive camera controls
βββ dom.ts # DOM element management
βββ gui.ts # lil-gui parameter panel
βββ input.ts # Keyboard and drag-drop handlers
βββ loader.ts # .splat file streaming
βββ renderer-webgpu.ts # WebGPU rendering pipeline
βββ sort-worker.ts # Worker bridge (main thread)
βββ sort-worker.worker.ts # Sorting and PLY parsing
βββ types.ts # Shared types and defaults
βββ utils.ts # Helper functions
βββ shaders/
βββ splat.vert.wgsl # Vertex shader
βββ splat.frag.wgsl # Fragment shader
βββ anaglyph-composite.wgsl # Stereo compositing
βββ crossfade-composite.wgsl # Mode transition
From rotation quaternion q = (w, x, y, z) and scale s = (sx, sy, sz):
- Convert quaternion to rotation matrix R
- Create scale matrix S = diag(sx, sy, sz)
- Compute M = R Γ S (scale-rotation matrix)
- Compute Ξ£ = M Γ Mα΅ (covariance)
The covariance is symmetric, so only 6 unique values are needed. In the current implementation this reconstruction happens in the vertex shader instead of being prepacked on the CPU.
In the fragment shader, each pixel evaluates:
G(p) = exp(-rΒ²)
where r = length(p) and p is the local quad coordinate. The quad ranges from -2 to 2, capturing ~95% of the Gaussian's visible contribution.
Rotation quaternions are stored as 4 bytes with values mapped from [-1, 1] to [0, 255]. Compressed PLY uses 2+10+10+10 bit packing with the largest component omitted and reconstructed from unit-length constraint.
MIT License - see LICENSE for details.