Introduction

Photography captures a frozen moment, but what if you could walk into it? That is the promise of radiance fields: data structures that encode how light fills a volume so a renderer can synthesize any viewpoint on demand.

The dominant approach for years was Neural Radiance Fields (NeRF), which trains a neural network to answer "how bright and opaque is this point in space?" Stunning results, but slow — a single frame can take seconds or minutes to render.

In 2023, Bernhard Kerbl and colleagues at INRIA published 3D Gaussian Splatting (3DGS): represent the scene as a set of 3-D Gaussians — fuzzy ellipsoids, each with a position, shape, opacity, and color — and project ("splat") them onto the screen. No rays, no network queries per pixel. The result: real-time, photorealistic rendering at 30–100 fps on consumer hardware.

The key insight is that a Gaussian is closed under projection: a 3-D Gaussian seen from any angle is still a 2-D Gaussian on the screen, cheap to rasterize. Blend millions of them back-to-front (alpha compositing) and you reconstruct the radiance field.

Splat and Blend

The demo below is a 2-D analogy of how Gaussian Splatting builds an image. Each splat is a 2-D Gaussian: a colored blob with a position, a radius, and an opacity. They are sorted back-to-front and composited with standard alpha blending — exactly the operation 3DGS performs after projecting 3-D Gaussians onto the screen.

<!-- Canvas + controls for the 2-D Gaussian splat playground -->
<div class="controls">
  <button id="addBtn" type="button">Add splat</button>
  <button id="clearBtn" type="button" class="ghost">Clear</button>
</div>
<canvas id="canvas" width="480" height="300" aria-label="2D Gaussian splat canvas"></canvas>
<div class="splat-controls" id="splatControls" style="display:none">
  <div class="ctrl-row">
    <label for="radiusSlider">Radius</label>
    <input id="radiusSlider" type="range" min="10" max="120" value="40">
    <span id="radiusVal">40</span>
  </div>
  <div class="ctrl-row">
    <label for="opacitySlider">Opacity</label>
    <input id="opacitySlider" type="range" min="5" max="100" value="70">
    <span id="opacityVal">70%</span>
  </div>
  <div class="ctrl-row">
    <label for="colorPicker">Color</label>
    <input id="colorPicker" type="color" value="#4a90d9">
  </div>
  <p class="formula">Alpha-compositing formula: C = &sum; &alpha;<sub>i</sub> c<sub>i</sub> &prod;<sub>j&lt;i</sub>(1&minus;&alpha;<sub>j</sub>)</p>
</div>
<p class="info" id="info">Click ’Add splat’ to place a Gaussian on the canvas.</p>
/* Styles for the Gaussian splatting demo canvas and controls */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; gap: .5rem; margin-bottom: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui; padding: .4rem .9rem; border: 1px solid #1d3557;
         background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:hover { opacity: .88; }
#canvas { display: block; border-radius: 10px; border: 1px solid #cdd9e3;
          background: #0a0f14; cursor: crosshair; max-width: 100%; }
.splat-controls { margin-top: .6rem; background: #f4f7fa; border-radius: 8px;
                  padding: .6rem .8rem; }
.ctrl-row { display: flex; align-items: center; gap: .5rem; margin-bottom: .35rem; }
.ctrl-row label { width: 70px; font-size: .85rem; color: #444; }
.ctrl-row input[type=range] { flex: 1; }
.ctrl-row span { width: 40px; font-size: .85rem; font-variant-numeric: tabular-nums; }
#colorPicker { width: 36px; height: 28px; border: none; padding: 0; cursor: pointer;
               border-radius: 4px; }
.formula { font-size: .82rem; color: #555; margin: .3rem 0 0; font-style: italic; }
.info { font-size: .85rem; color: #666; margin: .4rem 0 0; min-height: 1.2em; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// 2-D Gaussian Splatting demo
// Each splat is a radial gradient (Gaussian approximation) composited with globalAlpha.
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const splats = []; // array of splat objects: { x, y, radius, opacity, color, z }
let selected = null; // index of the currently selected splat, or null

const radiusSlider = document.getElementById('radiusSlider');
const opacitySlider = document.getElementById('opacitySlider');
const colorPicker = document.getElementById('colorPicker');
const radiusVal = document.getElementById('radiusVal');
const opacityVal = document.getElementById('opacityVal');
const splatControls = document.getElementById('splatControls');
const infoEl = document.getElementById('info');

// Convert hex color string to { r, g, b } object
function hexToRgb(hex) {
  const r = parseInt(hex.slice(1,3),16);
  const g = parseInt(hex.slice(3,5),16);
  const b = parseInt(hex.slice(5,7),16);
  return { r, g, b };
}

// Draw a single splat as a radial gradient on the canvas.
// The gradient goes from full color at the center to transparent at the edge, mimicking a 2-D Gaussian.
function drawSplat(s) {
  const { r, g, b } = hexToRgb(s.color);
  const grad = ctx.createRadialGradient(s.x, s.y, 0, s.x, s.y, s.radius);
  grad.addColorStop(0, `rgba(${r},${g},${b},${s.opacity})`);
  grad.addColorStop(1, `rgba(${r},${g},${b},0)`);
  ctx.fillStyle = grad;
  ctx.beginPath();
  ctx.arc(s.x, s.y, s.radius, 0, Math.PI * 2);
  ctx.fill();
}

// Re-render all splats.
// Sort by ascending z (back-to-front) then draw each splat — this is the depth-sorted alpha compositing at the heart of 3DGS.
function render() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  const sorted = [...splats].sort((a, b) => a.z - b.z); // back-to-front depth sort
  for (const s of sorted) drawSplat(s);
  // draw dashed selection ring around the active splat
  if (selected !== null) {
    const s = splats[selected];
    ctx.strokeStyle = '#fff';
    ctx.lineWidth = 1.5;
    ctx.setLineDash([4, 3]);
    ctx.beginPath();
    ctx.arc(s.x, s.y, s.radius, 0, Math.PI * 2);
    ctx.stroke();
    ctx.setLineDash([]);
  }
  updateInfo();
}

function updateInfo() {
  if (splats.length === 0) {
    infoEl.textContent = 'Click ’Add splat’ to place a Gaussian on the canvas.';
  } else if (selected !== null) {
    infoEl.textContent = fmt('Splat %selected%%v% of %length% selected — adjust radius, opacity, or color.', { selected: selected, v: 1, length: splats.length });
  } else {
    infoEl.textContent = fmt('%length% splats — click a splat to select it.', { length: splats.length });
  }
}

// Add a new splat at a random position with a random color and size.
function addSplat() {
  const margin = 60;
  const x = margin + Math.random() * (canvas.width - 2 * margin);
  const y = margin + Math.random() * (canvas.height - 2 * margin);
  const colors = ['#e63946','#457b9d','#2a9d8f','#e9c46a','#f4a261','#c77dff','#90e0ef'];
  const color = colors[Math.floor(Math.random() * colors.length)];
  const splat = { x, y, radius: 40 + Math.random() * 30, opacity: 0.4 + Math.random() * 0.4,
                  color, z: Math.random() };
  splats.push(splat);
  selected = splats.length - 1;
  syncControls();
  splatControls.style.display = 'block';
  render();
}

// Update the slider/picker UI to reflect the selected splat’s current values.
function syncControls() {
  if (selected === null) return;
  const s = splats[selected];
  radiusSlider.value = Math.round(s.radius);
  radiusVal.textContent = Math.round(s.radius);
  const pct = Math.round(s.opacity * 100);
  opacitySlider.value = pct;
  opacityVal.textContent = pct + '%';
  colorPicker.value = s.color;
}

document.getElementById('addBtn').addEventListener('click', addSplat);
document.getElementById('clearBtn').addEventListener('click', () => {
  splats.length = 0;
  selected = null;
  splatControls.style.display = 'none';
  render();
});

// Click on the canvas to select the topmost splat under the cursor.
canvas.addEventListener('click', e => {
  const rect = canvas.getBoundingClientRect();
  const scaleX = canvas.width / rect.width;
  const scaleY = canvas.height / rect.height;
  const mx = (e.clientX - rect.left) * scaleX;
  const my = (e.clientY - rect.top) * scaleY;
  // Test from front to back; first hit wins
  let hit = -1;
  for (let i = splats.length - 1; i >= 0; i--) {
    const s = splats[i];
    const dx = mx - s.x, dy = my - s.y;
    if (dx * dx + dy * dy <= s.radius * s.radius) { hit = i; break; }
  }
  selected = (hit === -1) ? null : hit;
  if (selected !== null) { syncControls(); splatControls.style.display = 'block'; }
  render();
});

radiusSlider.addEventListener('input', () => {
  if (selected === null) return;
  splats[selected].radius = +radiusSlider.value;
  radiusVal.textContent = radiusSlider.value;
  render();
});
opacitySlider.addEventListener('input', () => {
  if (selected === null) return;
  splats[selected].opacity = +opacitySlider.value / 100;
  opacityVal.textContent = opacitySlider.value + '%';
  render();
});
colorPicker.addEventListener('input', () => {
  if (selected === null) return;
  splats[selected].color = colorPicker.value;
  render();
});

render();
Result
Console

Click Add splat to drop a new Gaussian at a random position. Adjust its radius and opacity sliders, then pick a color. Notice how overlapping splats blend smoothly — no hard edges, no polygons. Clear removes everything and lets you start over. The transparency math shown is the same α\alpha-compositing formula used in the real algorithm: C=∑iαici∏j<i(1−αj)C = \sum_i \alpha_i c_i \prod_{j<i}(1-\alpha_j).

Rendering Complexity

How expensive is Gaussian Splatting compared with its predecessors?

  • Triangle rasterization (classic games): O(T)O(T) per frame where TT is triangle count. Fast but hard to capture volumetric, fuzzy effects like hair or fog.
  • NeRF: shoots RR rays, each sampled at SS points, each querying a network — roughly O(R⋅S⋅network)O(R \cdot S \cdot \text{network}). Photorealistic but painfully slow at inference.
  • Gaussian Splatting: NN splats are sorted by depth — O(Nlog⁡N)O(N \log N) — then rasterized front-to-back in parallel on the GPU. With tiled rendering (the screen is split into 16×1616 \times 16 tiles; each tile independently composites only the splats that touch it), the practical cost per frame is close to O(Nvisible/tiles)O(N_{\text{visible}} / \text{tiles}).

A typical outdoor scene uses 3–6 million Gaussians. Sorting costs dominate setup, but rasterization is massively parallel, so modern GPUs hit 60+ fps. The memory footprint is large (several GB for a complex scene) but training takes only minutes to hours on a single GPU — far faster than NeRF.

The dimensionality reduction analogy is apt: Gaussian Splatting compresses the infinite-dimensional light field into a finite, differentiable set of parameters optimized end-to-end by minimizing the difference between rendered and ground-truth images.

Where It Matters

Real-time photorealistic rendering is not a toy problem. Gaussian Splatting has already found its way into:

  • VR/AR telepresence: record a person with multiple cameras, fit a Gaussian scene in minutes, stream it to a headset — the viewer walks around a life-size volumetric clone.
  • Game-asset creation: artists photograph a real object, reconstruct it as a Gaussian scene, and export it — faster than hand-sculpting and more realistic than photogrammetry meshes.
  • Autonomous driving simulation: synthetic sensor data for training self-driving models requires photorealistic, real-time scenes. Gaussian Splatting delivers both.
  • Medical imaging: CT and MRI volumes can be approximated as Gaussian fields, enabling interactive visualization of complex anatomy at surgical-planning speeds.
  • Cultural heritage: scanning fragile artifacts with multi-view cameras and splatting them preserves every detail at sub-millimeter accuracy.

The technique also inspired research into dynamic Gaussian Splatting (scenes with moving objects) and compressed Gaussian representations that cut memory use by an order of magnitude — an active frontier in computer graphics and neural network training.

Conclusion

3D Gaussian Splatting is an elegant inversion of intuition: instead of tracing how light travels, it asks where does the light come from and answers with a cloud of glowing blobs. Each blob is just a position, a shape, a color, and an opacity — yet millions together reconstruct photographic reality at interactive speeds.

The mathematics is classical (Gaussians, alpha compositing, gradient descent), the engineering is clever (tiled rasterization, CUDA radix sort), and the result is a step change in what a single GPU can render. Whether you care about virtual worlds, surgical planning, or the future of digital twins, the blob revolution has already arrived.

Share this article

Pick a channel — or use your device's native share sheet.

Comments

Loading comments...

https://www.kipuhub.com/en/article/gaussian-splatting/Content licensed under CC BY-NC 4.0.