TAI BUI
Back to curriculum
BuildPython

3D Gaussian Splatting from Scratch

A scene is a cloud of millions of 3D Gaussians. Each one carries a position, orientation, scale, opacity, and a colour that shifts with viewing direction. Rasterise them, backprop through the rasterisation, done.

TIME: ~90 minutesPREREQUISITES: Phase 4 Lesson 13 (3D Vision & NeRF), Phase 1 Lesson 12 (Tensor Operations), Phase 4 Lesson 10 (Diffusion basics optional)

Learning Objectives

  • Explain why 3D Gaussian Splatting replaced NeRF as the production default for photorealistic 3D reconstruction in 2026
  • State the six per-Gaussian parameters (position, rotation quaternion, scale, opacity, spherical harmonics colour, optional feature) and how many floats each contributes
  • Implement a 2D Gaussian splatting rasterizer from scratch using `alpha` compositing, then show how the 3D case projects to the same loop
  • Use `nerfstudio`, `gsplat`, or `SuperSplat` to reconstruct a scene from 20-50 photos and export to the `KHR_gaussian_splatting` glTF extension or the OpenUSD 26.03 `UsdVolParticleField3DGaussianSplat` schema

The Problem

A NeRF stores a scene as the weights of an MLP. Every rendered pixel costs hundreds of MLP queries along a ray, so training takes hours and rendering takes seconds. Worse, the weights aren't editable: move a chair inside the scene and you have to retrain from scratch.

3D Gaussian Splatting (Kerbl, Kopanas, Leimkühler, Drettakis, SIGGRAPH 2023) replaced all of that. A scene becomes an explicit set of 3D Gaussians. Rendering is GPU rasterisation at 100+ fps. Training takes minutes. Editing is direct: translate a subset of Gaussians and you've moved the chair. By 2026 the Khronos Group has ratified a glTF extension for Gaussian splats, OpenUSD 26.03 ships a Gaussian splat schema, Zillow and Apartments.com render real estate with them, and most new research papers on 3D reconstruction are variants on the core 3DGS idea.

The mental model is simple. The math has enough moving parts that most introductions start at rasterisation and skip past the projections and spherical harmonics entirely. This lesson builds the whole thing: a 2D version first, then the 3D extension.

Warm-up0/4
1. A NeRF renders each pixel by querying a neural network hundreds of times along that pixel's camera ray. Based on that alone, would you expect NeRF rendering to be faster or slower than a GPU rasterising explicit geometry (points or triangles) directly?Warm-up
A camera ray with seven dots marking hundreds of MLP queries along it, next to a GPU drawing a triangle's geometry directly.
The two things being compared: querying an MLP hundreds of times per ray, and a GPU drawing geometry directly.
2. You draw several semi-transparent coloured shapes on top of each other. What do you need to know to get the final colour at a pixel right?Warm-up
Three semi-transparent shapes, a circle, a square and a triangle, overlapping so the colours blend where they cover each other, with a legend naming them shape A, B and C.
Several semi-transparent shapes stacked on top of each other, with no order or opacity values stated.
3. To train scene parameters by gradient descent on an image loss, what must be true of the computation that turns those parameters into a rendered image?Warm-up
A forward chain of boxes, parameters, render, image, loss, with a dashed curved arrow looping back from loss to parameters labelled gradient back to the parameters, a question mark.
The forward chain from parameters to a loss, with the backward path left open.
4. A 2D Gaussian (bell curve) density is centred on a point. How does its value change as you move away from that centre?Warm-up
A centre point with three dashed distance rings labelled d equals 1, 2 and 3, next to an empty plot of density against distance from the centre holding a question mark.
Where a Gaussian's density is measured from, with the curve itself left blank.
Check: The Problem0/3
1. Why did 3D Gaussian Splatting largely replace NeRF as the production default for photorealistic scene reconstruction by 2026?Explain
Two boxes side by side, NeRF marked the earlier default and 3D Gaussian Splatting marked the 2026 default, above a dashed box standing for why the swap happened.
The two approaches the question compares, with no claim about either drawn in.
2. Why can you move a chair in a 3DGS scene directly, while a NeRF scene would have to be retrained?Explain
A scanned room with one chair marked in it, next to two stores, a NeRF file holding MLP weights and a 3DGS file holding per-Gaussian values, above a dashed box standing for why editing differs.
One room stored two ways, with why moving the chair differs between them left open.
3. A real-estate app needs fast browser rendering of rooms and also needs staff to remove a piece of furniture from a finished scan. Which representation and workflow fits?Design
A scanned room icon, with two listed needs, fast browser rendering and removing one piece of furniture, next to a dashed box asking for the representation and workflow.
A scanned room with its two stated needs, and the choice of representation left open.

The Concept

What a Gaussian carries

One 3D Gaussian is a parametric blob in space with these attributes:

position         mu         (3,)    centre in world coordinates
rotation         q          (4,)    unit quaternion (a 4-number rotation encoding that avoids gimbal lock) for orientation
scale            s          (3,)    log-scales per axis (exponentiated at render time)
opacity          alpha      (1,)    post-sigmoid opacity [0, 1]
SH coefficients  c_lm       (3 * (L+1)^2,)   view-dependent colour
feature (opt.)   f          (D,)    optional per-Gaussian embedding (e.g. a distilled CLIP/language feature for tasks like LangSplat or Gaussian Grouping); D = 0 in the base model

Rotation and scale combine into a 3x3 covariance: Sigma = R S S^T R^T, the shape of the Gaussian in 3D. Spherical harmonics let the colour change with viewing direction (specular highlights, subtle sheen, view-dependent glow) without storing per-view textures. With SH degree 3 you get 16 coefficients per colour channel: 48 floats per Gaussian for colour alone.

A scene typically has 1-5 million Gaussians. Each stores roughly 60 floats: 3 for position, 4 for rotation, 3 for scale, 1 for opacity, 48 for SH colour, and the optional feature vector, which is zero-length unless a task adds one. At 4 bytes per float, that works out to about 1.2 GB for a five-million-Gaussian scene (5M x 60 x 4 bytes). It is far bigger than a NeRF's few megabytes of MLP weights, and that is the price of an explicit representation that renders in real time with no network call per pixel.

Rasterisation, not ray marching

Mermaid diagram

Five steps, all GPU-friendly. No MLP query per pixel. A single RTX 3080 Ti renders 6 million splats at 147 fps.

The projection step

The 3D Gaussian at world position mu with 3D covariance Sigma projects to a 2D Gaussian at screen position mu' with 2D covariance Sigma':

mu' = project(mu)
Sigma' = J W Sigma W^T J^T          (2 x 2)

W = viewing transform (rotation + translation of camera)
J = Jacobian (matrix of partial derivatives) of the perspective projection at mu'

The 2D Gaussian's footprint is an ellipse whose axes are the eigenvectors of Sigma'. Every pixel inside that ellipse receives the Gaussian's contribution, weighted by exp(-0.5 * (p - mu')^T Sigma'^-1 (p - mu')).

The alpha-compositing rule

For one pixel, the Gaussians that cover it are sorted front-to-back, nearest first, which is what the transmittance term below assumes (back-to-front gives the same image with a different recurrence). Colour is composited with the same equation as every semi-transparent rasteriser since the 1980s:

C_pixel = sum_i alpha_i * T_i * c_i

T_i = prod_{j < i} (1 - alpha_j)       transmittance up to i
alpha_i = opacity_i * exp(-0.5 * d^T Sigma'^-1 d)   local contribution
c_i = eval_SH(SH_i, view_direction)    view-dependent colour

This is the same equation as NeRF's volumetric render, just over an explicit sparse set of Gaussians instead of dense samples along a ray. That identity is why rendered quality matches NeRF: both integrate the same radiance-field equation.

Why this is differentiable

Every step (projection, tile assignment, alpha compositing, SH evaluation) is differentiable with respect to the Gaussian parameters. Given a ground-truth image, compute the rendered pixel loss, backprop through the rasteriser, and update every (mu, q, s, alpha, c_lm) by gradient descent. Over roughly 30,000 iterations the Gaussians settle into the right positions, scales, and colours.

Densification and pruning

A fixed set of Gaussians cannot cover a complex scene, so training includes two adaptive mechanisms:

  • Clone a Gaussian at its current position when its gradient magnitude is high but its scale is small: the reconstruction needs more detail here.
  • Split a large-scale Gaussian into two smaller ones when its gradient is high: one big Gaussian is too smooth to fit the region.
  • Prune Gaussians whose opacity drops below a threshold: they aren't contributing anything.

Densification runs every N iterations. A scene typically grows from roughly 100k initial Gaussians (seeded from SfM points) to 1-5M by the end of training.

Spherical harmonics in one paragraph

View-dependent colour is a function c(direction) on the unit sphere. Spherical harmonics are the sphere's Fourier basis. Truncate at degree L and you get (L+1)^2 basis functions per channel. Evaluating the colour for a new view is a dot product between the learned SH coefficients and the basis evaluated at the viewing direction. Degree 0 = one coefficient = constant colour. Degree 3 = 16 coefficients = enough to capture Lambertian shading, specular, and mild reflection. 3D Gaussian Splatting papers use degree 3 by default.

The 2026 production stack

1. Capture         smartphone / DJI drone / handheld scanner
2. SfM / MVS       structure-from-motion / multi-view stereo (COLMAP or GLOMAP) derives camera poses + sparse points
3. Train 3DGS      nerfstudio / gsplat / inria official / PostShot (~10-30 min on RTX 4090)
4. Edit            SuperSplat / SplatForge (clean floaters, segment)
5. Export          .ply -> glTF KHR_gaussian_splatting or .usd (OpenUSD 26.03)
6. View            Cesium / Unreal / Babylon.js / Three.js / Vision Pro

4D and generative variants

  • 4D Gaussian Splatting: Gaussians as functions of time, used for volumetric video (Superman 2026, A$AP Rocky's "Helicopter").
  • Generative splats: text-to-splat models (Marble by World Labs) that hallucinate entire scenes.
  • 3D Gaussian Unscented Transform: NVIDIA NuRec's variant for autonomous driving simulation.
Check: The Concept0/22
1. What is 3D Gaussian Splatting (3DGS)?Define
A dense cluster of overlapping translucent blobs, an arrow to one of them enlarged, and a dashed box standing for what that one blob stores.
One scene drawn up close as a cluster of overlapping blobs, with what a single blob stores left open.
2. In 3DGS, what does the covariance matrix of a Gaussian describe?Define
The formula Sigma equals R S S transpose R transpose, a three by three Sigma computed with numpy from illustrative R and S, and a dashed box standing for what Sigma describes.
The lesson's Sigma formula and one worked three by three matrix, with what the matrix describes left open.
3. What is alpha compositing in a Gaussian splatting renderer?Define
Three circles labelled nearest, middle and farthest on one dashed sight line, a note that each carries its own colour and opacity, and a dashed box standing for the resulting pixel colour.
Three Gaussians covering one pixel at different depths, with how they turn into one colour left open.
4. What is densification in 3DGS training?Define
A region holding three large translucent blobs labelled 3 large Gaussians, high gradient here, an arrow into a dashed box standing for what densification does there.
One region of the scene mid-training, with what densification does there left open.
5. What does pruning do during 3DGS training?Define
A table of four Gaussians with illustrative opacity and scale values, next to a dashed box standing for what pruning does to them.
Four Gaussians partway through training, with what pruning does to them left open.
6. Besides position, rotation, scale and opacity, what does a 3D Gaussian store so that its colour can change with the viewing direction?Define
A table of a Gaussian's attributes, position, rotation, scale and opacity each with a value, and one more row whose attribute name is a question mark, described only as lets colour change with view.
A Gaussian's known attributes, with the one that lets colour vary by viewing direction left as a question mark.
7. What is the optional per-Gaussian feature vector for?Define
The lesson's own per-Gaussian parameter table, position through SH colour filled in with their sizes and what they hold, and the feature row's 'holds' column left as a question mark.
The lesson's parameter list for one Gaussian, with the optional feature row's purpose left blank.
8. In the lesson's 2026 production stack, what does the SfM/MVS step produce, and roughly how long does splatfacto training take on an RTX 4090?Define
The first three stages of the 2026 stack, capture, SfM / MVS, train 3DGS, with two dashed boxes below asking what SfM/MVS produces and how long training takes on an RTX 4090.
The stack's first three stages, with the SfM/MVS output and the training time both left open.
9. The colour equation for one pixel in both NeRF and 3DGS is `C = sum_i alpha_i * T_i * c_i` where `T_i = prod_{j<i}(1 - alpha_j)`. What does this shared equation say about the two methods?Explain
The shared formula C equals the sum of alpha_i times T_i times c_i above two boxes, NeRF and 3DGS, and a dashed box standing for what sharing the equation means.
The equation the stem quotes, over the two methods that both write it, with the reading left open.
10. Why can a splat renderer reach 100+ fps on a single consumer GPU, as with 6 million splats at 147 fps on an RTX 3080 Ti?Explain
A frame packed with translucent splats labelled 6 million splats, a badge reading RTX 3080 Ti at 147 fps, and a dashed box standing for what makes that rate possible.
The stem's own frame rate and splat count, with the reason behind them left open.
11. When projecting a 3D Gaussian to screen space, the lesson gives `Sigma' = J W Sigma W^T J^T`. What do `W` and `J` represent?Explain
The formula Sigma prime equals J W Sigma W transpose J transpose, with W and J each drawn as a box holding a question mark, and a box for mu prime.
The projection formula with W and J left unresolved.
12. During 3DGS training, densification includes 'clone' and 'split' operations. What triggers each?Explain
A small blob labelled small scale and a large ellipse labelled large scale, above a dashed box standing for which of clone and split applies to which, and under what condition.
A small Gaussian and a large one mid-training, with which operation fits each left open.
13. Why does evaluating spherical harmonics let a Gaussian's colour vary with view without storing per-view textures?Explain
A listing of what the renderer holds at one pixel, a Gaussian's sixteen by three SH coefficients and a three-element view direction, with an arrow into a dashed box standing for the colour.
What the renderer has on hand for one Gaussian and one view, with how it reaches a colour left open.
14. What is the difference between '4D Gaussian Splatting' and a 'generative splat' model like Marble?Explain
Two boxes, 4D Gaussian Splatting and Marble, each above a dashed box asking whether it adds a time axis or generates a scene from text.
The two named approaches, with which one does which left unresolved.
15. Using the lesson's per-Gaussian layout (3 position, 4 rotation, 3 scale, 1 opacity, 3*(L+1)^2 SH, no feature), how many floats does one Gaussian need at SH degree 2?Apply
The fixed 11 floats worked out, the SH formula given with L set to 2, and the total left as a question mark.
The layout's fixed floats added up, with the SH term and the grand total left open.
16. A reconstruction shows floaters and blotchy artifacts, and training for twice as long does not help. The photos were blurry, with uneven exposure and thin coverage around occluded edges. What is the most likely root cause?Analyze
A table pairing capture problems, blurry frames, uneven exposure and thin coverage at occluded edges, with their results, floaters, blotchy artifacts and no change after twice the training, above a dashed box standing for the root cause.
The capture problems and the training outcome the stem describes, with the root cause left open.
17. Late in training, a region stays blurry. The Gaussians there are large and have high gradient magnitude. Which densification step should fix it, and why?Analyze
One large ellipse labelled large scale, high gradient, next to a dashed box asking which densification step applies and why.
A large, high-gradient Gaussian in a blurry region, with the fix left open.
18. A training run keeps adding Gaussians. The count grows far beyond what the scene needs, and many of them have near-zero opacity. Which mechanism is not doing its job?Analyze
A rising curve of Gaussian count over training iterations, climbing far beyond what the scene needs, with a note that many have near-zero opacity, and a dashed box standing for the mechanism that is not doing its job.
The Gaussian count climbing with many near-zero-opacity Gaussians, with the broken mechanism left open.
19. A colleague says 3DGS matches NeRF's image quality because Gaussian splatting is simply a more accurate renderer. What is wrong with that reasoning?Analyze
A quote box reading, 3DGS matches NeRF because it is simply a more accurate renderer, attributed to a colleague, above a dashed box standing for what is wrong with the reasoning.
A colleague's claim about why 3DGS and NeRF quality match, with what is wrong with it left open.
20. After training with SH degree 0 only, specular highlights no longer move as the camera moves. Why?Analyze
A badge reading SH degree 0, one Gaussian drawn between two camera icons at different positions, and a dashed box standing for whether the highlight moves between the views.
A degree-0 Gaussian seen from two camera positions, with whether its highlight moves left open.
21. You must turn a recorded performance into volumetric video that a viewer can scrub through in time. Which extension of the lesson's idea fits?Design
A box reading recorded performance, a note that the viewer scrubs through time, and a dashed box asking which extension fits.
A recorded performance that needs to be scrubbed in time, with the fitting extension left open.
22. You will capture a living room with 20-50 photos for a splat. Which plan best uses the lesson's advice before you tune any training hyperparameters?Design
A living room icon labelled a living room, 20-50 photos, with a note about to tune splatfacto's hyperparameters, next to a dashed box asking what to do first.
The capture ahead of you and the hyperparameter tuning you have not started yet, with the plan left open.

Build It

Step 1: A 2D Gaussian

We first build a 2D rasteriser. The 3D case reduces to it after projection.

python
import torch
import torch.nn as nn
import torch.nn.functional as F
 
 
def eval_2d_gaussian(means, covs, points):
    """
    means:  (G, 2)      centres
    covs:   (G, 2, 2)   covariance matrices
    points: (H, W, 2)   pixel coordinates
    returns: (G, H, W)  density at every pixel for every Gaussian
    """
    G = means.size(0)
    H, W, _ = points.shape
    flat = points.view(-1, 2)
    inv = torch.linalg.inv(covs)
    diff = flat[None, :, :] - means[:, None, :]
    d = torch.einsum("gpi,gij,gpj->gp", diff, inv, diff)
    density = torch.exp(-0.5 * d)
    return density.view(G, H, W)

einsum does the quadratic form diff^T Sigma^-1 diff for every (Gaussian, pixel) pair.

Step 2: 2D splatting rasteriser

Alpha-compositing front-to-back. Depth in 2D is meaningless, so we use a learned per-Gaussian scalar for order.

python
def rasterise_2d(means, covs, colours, opacities, depths, image_size):
    """
    means:     (G, 2)
    covs:      (G, 2, 2)
    colours:   (G, 3)
    opacities: (G,)     in [0, 1]
    depths:    (G,)     per-Gaussian scalar used for ordering
    image_size: (H, W)
    returns:   (H, W, 3) rendered image
    """
    H, W = image_size
    yy, xx = torch.meshgrid(
        torch.arange(H, dtype=torch.float32, device=means.device),
        torch.arange(W, dtype=torch.float32, device=means.device),
        indexing="ij",
    )
    points = torch.stack([xx, yy], dim=-1)
 
    densities = eval_2d_gaussian(means, covs, points)
    alphas = opacities[:, None, None] * densities
    alphas = alphas.clamp(0.0, 0.99)
 
    order = torch.argsort(depths)
    alphas = alphas[order]
    colours_sorted = colours[order]
 
    T = torch.ones(H, W, device=means.device)
    out = torch.zeros(H, W, 3, device=means.device)
    for i in range(means.size(0)):
        a = alphas[i]
        out += (T * a)[..., None] * colours_sorted[i][None, None, :]
        T = T * (1.0 - a)
    return out

This isn't fast (a real implementation uses tile-based CUDA kernels), but it's exactly the right math, and it's fully differentiable.

Step 3: A trainable 2D splat scene

python
class Splats2D(nn.Module):
    def __init__(self, num_splats=128, image_size=64, seed=0):
        super().__init__()
        g = torch.Generator().manual_seed(seed)
        H, W = image_size, image_size
        self.means = nn.Parameter(torch.rand(num_splats, 2, generator=g) * torch.tensor([W, H]))
        self.log_scale = nn.Parameter(torch.ones(num_splats, 2) * math.log(2.0))
        self.rot = nn.Parameter(torch.zeros(num_splats))  # single angle in 2D
        self.colour_logits = nn.Parameter(torch.randn(num_splats, 3, generator=g) * 0.5)
        self.opacity_logit = nn.Parameter(torch.zeros(num_splats))
        self.depth = nn.Parameter(torch.rand(num_splats, generator=g))
 
    def covs(self):
        s = torch.exp(self.log_scale)
        c, si = torch.cos(self.rot), torch.sin(self.rot)
        R = torch.stack([
            torch.stack([c, -si], dim=-1),
            torch.stack([si, c], dim=-1),
        ], dim=-2)
        S = torch.diag_embed(s ** 2)
        return R @ S @ R.transpose(-1, -2)
 
    def forward(self, image_size):
        covs = self.covs()
        colours = torch.sigmoid(self.colour_logits)
        opacities = torch.sigmoid(self.opacity_logit)
        return rasterise_2d(self.means, covs, colours, opacities, self.depth, image_size)

log_scale, opacity_logit, and colour_logits are all unconstrained parameters mapped through the right activation at render time. This is the standard pattern for every 3DGS implementation.

Step 4: Fit 2D Gaussians to a target image

python
import math
import numpy as np
 
def make_target(size=64):
    yy, xx = np.meshgrid(np.arange(size), np.arange(size), indexing="ij")
    img = np.zeros((size, size, 3), dtype=np.float32)
    # Red circle
    mask = (xx - 20) ** 2 + (yy - 20) ** 2 < 10 ** 2
    img[mask] = [1.0, 0.2, 0.2]
    # Blue square
    mask = (np.abs(xx - 45) < 8) & (np.abs(yy - 40) < 8)
    img[mask] = [0.2, 0.3, 1.0]
    return torch.from_numpy(img)
 
 
target = make_target(64)
model = Splats2D(num_splats=64, image_size=64)
opt = torch.optim.Adam(model.parameters(), lr=0.05)
 
for step in range(200):
    pred = model((64, 64))
    loss = F.mse_loss(pred, target)
    opt.zero_grad(); loss.backward(); opt.step()
    if step % 40 == 0:
        print(f"step {step:3d}  mse {loss.item():.4f}")

Over 200 steps the 64 Gaussians settle into the two shapes. That's the entire idea: gradient descent on explicit geometric primitives.

Step 5: From 2D to 3D

The 3D extension keeps the same loop. The additions:

  1. Per-Gaussian rotation is a quaternion instead of a single angle.
  2. Covariance is R S S^T R^T with R built from the quaternion and S = diag(exp(log_scale)).
  3. Projection (mu, Sigma) -> (mu', Sigma') uses the camera extrinsics and the Jacobian of the perspective projection at mu.
  4. Colour becomes a spherical-harmonics expansion; evaluate it at the viewing direction.
  5. Depth-sort is from actual camera-space z instead of a learned scalar.

Every production implementation (gsplat, inria/gaussian-splatting, nerfstudio) does exactly this on the GPU with tile-based CUDA kernels.

Step 6: Spherical harmonics evaluation

The SH basis up to degree 3 has 16 terms per channel. Evaluation:

python
def eval_sh_degree_3(sh_coeffs, dirs):
    """
    sh_coeffs: (..., 16, 3)   last dim is RGB channels
    dirs:      (..., 3)       unit vectors
    returns:   (..., 3)
    """
    C0 = 0.282094791773878
    C1 = 0.488602511902920
    C2 = [1.092548430592079, 1.092548430592079,
          0.315391565252520, 1.092548430592079,
          0.546274215296039]
    x, y, z = dirs[..., 0], dirs[..., 1], dirs[..., 2]
    x2, y2, z2 = x * x, y * y, z * z
    xy, yz, xz = x * y, y * z, x * z
 
    result = C0 * sh_coeffs[..., 0, :]
    result = result - C1 * y[..., None] * sh_coeffs[..., 1, :]
    result = result + C1 * z[..., None] * sh_coeffs[..., 2, :]
    result = result - C1 * x[..., None] * sh_coeffs[..., 3, :]
 
    result = result + C2[0] * xy[..., None] * sh_coeffs[..., 4, :]
    result = result + C2[1] * yz[..., None] * sh_coeffs[..., 5, :]
    result = result + C2[2] * (2.0 * z2 - x2 - y2)[..., None] * sh_coeffs[..., 6, :]
    result = result + C2[3] * xz[..., None] * sh_coeffs[..., 7, :]
    result = result + C2[4] * (x2 - y2)[..., None] * sh_coeffs[..., 8, :]
 
    # degree 3 terms omitted here for brevity; full 16-coefficient version in the code file
    return result

Learned sh_coeffs store the "colour in every direction" for that Gaussian. At render time you evaluate against the current view direction and get a 3-vector RGB.

Check: Build It0/9
1. In the lesson's 2D splat model, scale, opacity and colour are stored as `log_scale`, `opacity_logit` and `colour_logits`. Why?Explain
The lesson's own Splats2D lines, the three parameters as they are declared and the exp and sigmoid calls that read them at render time, above a dashed box standing for why they are stored this way.
The lesson's own code for these three parameters, with the reason for the form left open.
2. Why does the lesson build a 2D rasteriser first and then say the 3D case follows the same loop?Explain
The lesson's Step 2 rasteriser signature above its Step 5 list of the five things the 3D case adds, with a dashed box standing for why one loop covers both.
What the 3D case adds to the 2D rasteriser, with why the loop carries over left open.
3. In the lesson's `rasterise_2d` function, in what order are the Gaussians composited, and what determines that order?Apply
The rasterise_2d signature above a table of three Gaussians with illustrative depth and opacity values, and a dashed box standing for which one gets blended first.
The inputs rasterise_2d is handed, with the order it blends them in left open.
4. `eval_2d_gaussian` is called with 3 Gaussians and a `points` tensor for a 32 by 48 image (shape (32, 48, 2)). What shape does it return?Apply
The function's own argument shapes with G set to 3, H to 32 and W to 48, its return shape left as a question mark, above a dashed box standing for the returned shape.
The function's declared argument shapes, with G, H and W filled in and the output shape left open.
5. In `rasterise_2d`, a Gaussian has opacity 0.6 and a pixel lies exactly at its mean. What is that Gaussian's alpha at that pixel?Apply
The alpha formula with this Gaussian's opacity of 0.6 and a pixel placed exactly at the mean, with the value of alpha left as a question mark.
The alpha formula for this Gaussian, with the arithmetic left undone.
6. Two Gaussians cover one pixel with alpha 0.5 each. The nearer is red (1, 0, 0) and the farther is blue (0, 0, 1). Using the lesson's compositing loop, what is the pixel colour?Apply
A red circle labelled nearer, alpha 0.5, and a blue circle labelled farther, alpha 0.5, above a line reading pixel colour equals a question mark.
The two Gaussians covering the pixel, each with its own colour and alpha, with the composited colour left open.
7. A Gaussian with opacity 1.0 sits exactly at a pixel in `rasterise_2d`. After it is composited, how much transmittance T remains for the Gaussians behind it?Apply
The stem's setup written out, an opacity of 1.0, a pixel at the Gaussian's mean and a transmittance of 1.0 before it, with the transmittance after it left as a question mark.
The transmittance update for this Gaussian, with the result left open.
8. In `Splats2D`, one splat starts with `rot = 0` and `log_scale = [log 2, log 2]`. What is its covariance returned by `covs()`?Apply
The covs() lines written out for a splat with rot 0 and log_scale log 2, every step left symbolic, with the resulting covariance a question mark.
The covariance formula for this splat, with the arithmetic left undone.
9. In `eval_sh_degree_3`, only coefficient 0 of a Gaussian is non-zero (the others are all zero). What happens to the evaluated colour as the viewing direction changes?Apply
One Gaussian's SH coefficients, the first set to an illustrative non-zero value and every other one zero, above a dashed box standing for the colour as the view direction changes.
One Gaussian's SH coefficients with only the first non-zero, and what its colour does across views left open.

Use It

For real 3DGS work, use gsplat (Meta) or nerfstudio:

bash
pip install nerfstudio gsplat
ns-download-data example
ns-train splatfacto --data path/to/data

splatfacto is nerfstudio's 3DGS trainer. The run takes 10-30 minutes on an RTX 4090 for a typical scene.

Export options that matter in 2026:

  • .ply: raw Gaussian cloud (portable, largest file).
  • .splat: PlayCanvas / SuperSplat quantised format.
  • glTF KHR_gaussian_splatting: Khronos standard, portable across viewers (Feb 2026 RC).
  • OpenUSD UsdVolParticleField3DGaussianSplat: USD-native, for NVIDIA Omniverse and Vision Pro pipelines.

For 4D / dynamic scenes, 4DGS and Deformable-3DGS extend the same machinery with time-varying means and opacities.

Check: Use It0/4
1. What is splatfacto?Define
A shell window holding the lesson's three commands, pip install nerfstudio gsplat, ns-download-data example, and ns-train splatfacto with a data path.
The lesson's own commands, where this name appears on the command line.
2. What is `KHR_gaussian_splatting`?Define
A short glTF file excerpt whose extensionsUsed list holds the name KHR_gaussian_splatting, above a dashed box standing for what kind of thing that name is.
The name as it appears inside a file, with what it actually is left open.
3. How do the `.ply` and `.splat` export options differ?Explain
Two boxes, .ply and .splat, each above a dashed box asking whether it is raw, quantised, or the largest format.
The two export formats the question compares, with their properties left open.
4. You want to ship a 3DGS scene across Unreal Engine, Vision Pro, Babylon.js and a Three.js web viewer in 2026. Which export choice is the safest bet?Design
Four boxes, Unreal, Vision Pro, Babylon.js and Three.js, above a dashed box asking for the safest single export choice across all of them.
The four shipping targets named in the stem, with the export choice left open.

Ship It

This lesson produces:

  • outputs/prompt-3dgs-capture-planner.md: a prompt that plans a capture session (number of photos, camera path, lighting) for a given scene type.
  • outputs/skill-3dgs-export-router.md: a skill that picks the right export format (.ply / .splat / glTF / USD) given the downstream viewer or engine.
Author's Note

The part of this pipeline nobody warns you about is capture quality. Once you've reconstructed one clean scene, it's tempting to assume the model did the work. It didn't: blurry frames, inconsistent exposure between shots, and thin coverage around occluded edges show up later as floaters and blotchy artifacts that no amount of extra training time fixes, because the optimizer is only as good as the SfM points and camera poses it started from.

Before touching splatfacto's hyperparameters, walk the capture path twice: once for coverage, once for overlap. That habit fixes more scenes than any densification tuning I've ever done.

Exercises

  1. (Easy) Run the 2D splat trainer above on a different synthetic image. Vary num_splats in [16, 64, 256] and plot MSE vs step for each. Identify the point of diminishing returns.
  2. (Medium) Extend the 2D rasteriser to support per-Gaussian RGB colours that depend on a scalar "view angle" through a degree-2 harmonic. Train on a pair of target images and verify the model reconstructs both.
  3. (Hard) Clone nerfstudio and train splatfacto on a 20-photo capture of any scene you have (desk, plant, face, room). Export to glTF KHR_gaussian_splatting and open it in a viewer (Three.js GaussianSplats3D, SuperSplat, Babylon.js V9). Report training time, number of Gaussians, and rendered fps.

Key Terms

TermWhat people sayWhat it actually means
3DGS"Gaussian splats"Explicit scene representation as millions of 3D Gaussians with per-Gaussian position, rotation, scale, opacity, SH colour
Covariance"Shape of the Gaussian"Sigma = R S S^T R^T; orientation and anisotropic scale of one Gaussian
Alpha compositing"Layered blend"Same equation as NeRF's volumetric render, now over an explicit sparse set
Densification"Clone and split"Adaptive addition of new Gaussians where reconstruction is under-fit
Pruning"Delete low-opacity"Remove Gaussians that have collapsed to near-zero opacity during training
Spherical harmonics"View-dependent colour"Fourier basis on the sphere; stores colour as a function of viewing direction
Splatfacto"nerfstudio's 3DGS"The easiest path to training 3DGS in 2026
KHR_gaussian_splatting"glTF standard"Khronos 2026 extension that makes 3DGS portable across viewers and engines

Further Reading