all lessons / computer_vision_3d / 01 · the 3D problem lesson 1 / 13

The 3D problem and the representation zoo

The CV track's geometry arc ended at lesson 05 by surveying NeRF and Gaussian Splatting — naming them without deriving them. This track earns them. But before any method, one decision governs everything downstream: what object are we actually optimizing? A point cloud, a mesh, a grid of occupancy, a signed-distance function, a field of colored fog, a million fuzzy blobs? Each renders differently, optimizes differently, and fails differently. This lesson builds the loop that every later lesson is a move inside, and lays out the representation design space so the acronyms stop looking like a zoo.

The plan
Four moves. (1) State the inverse problem precisely — an image is render(scene), and 3D vision is inverting a lossy, many-to-one map — and draw the render→compare→backprop loop that is the spine of the whole track. (2) Lay out the representation zoo: the explicit family (points, voxels, meshes), the implicit family (occupancy, SDF, radiance field), and Gaussians as the deliberate hybrid. (3) Explain the curse of the third dimension — why the obvious representation (a voxel grid) is O(N³) and why that number decides real designs. (4) Pin down the one property that makes the loop close: the renderer must be differentiable. We finish with the rule the rest of the track keeps invoking: choose the representation from the constraint, not the fashion.

1 · The inverse problem, and the loop that solves it

Start from the CV track's central loss. A pinhole camera maps a 3D point (X,Y,Z) to a pixel by x = f·X/Z, y = f·Y/Z (lesson 04). The Z in the denominator is the whole story: scale the point along its ray to (tX, tY, tZ) and the t cancels — f·tX/tZ = f·X/Z — so every point on a ray lands on the same pixel. Projection is many-to-one. An image is a function of the scene, and that function threw depth away.

Write the forward direction honestly. A scene S (geometry + appearance) and a camera produce an image by a rendering operator R:

I = R(S, camera) + noise .

3D vision is the inverse problem: recover S given one or more images I. Because R is many-to-one, the inverse is ill-posed — many scenes explain the same image (a small near object and a large far one are indistinguishable; the CV track called this the monocular scale ambiguity). You cannot simply invert R. What you can do is search the space of scenes for one whose renderings match the observations. That search is the loop this entire track lives inside:

pick a representation of S ① │ ▼ render it: Î = R(S) ② forward — must be cheap AND differentiable │ ▼ compare to observations: ③ loss L = ‖Î − I‖ (photometric, geometric, …) L(Î, I) │ ▼ update S to reduce L ④ inverse — ∂L/∂S via backprop through R └──────── repeat ───────┘

Read the four steps as the table of contents for the track. Step ① is this lesson: which representation. Step ② is rendering — rasterization (lessons 06–07) or ray-marching (lesson 09). Step ③ is the loss — photometric for radiance fields, geometric (Chamfer, reprojection) for point clouds and depth. Step ④ is the inversion — and it only works if R is differentiable, the property we spend section 4 on. Classical geometry (CV 04–05: triangulation, SfM, ICP) is a special, hand-derived way of doing this loop for point representations; the modern era is about making step ② differentiable so a generic optimizer can do step ④.

The reframe
Don't ask "how do I reconstruct 3D?" Ask "what do I optimize, how does it render, and what supervises it?" Every method in this track is a specific answer to those three, and the first one — the representation — constrains the other two. That is why we start here.

2 · The representation zoo

There are only a handful of ways to write down 3D structure, and they split into two families by one question: do you store the shape, or a function that tests the shape?

Explicit representations store the geometry directly, as primitives with coordinates you can list:

Implicit representations store a function f:ℝ³→ℝ and define the shape as a level set of it — the surface is "where f crosses a threshold." You never list the surface; you query the function.

And the hybrid the field converged on for real-time work:

The whole zoo, on the axes that actually decide a design:

RepresentationFamilyMemory scales asNative renderingTopology / editingFirst appears
Point cloudexplicit#points (∝ surface)splat / rasterizefree; no surfacelesson 03
Voxel gridexplicitO(N³)march / rasterizefree; coarselesson 11
Meshexplicit#verts (∝ surface)rasterize (GPU-native)rigid — re-mesh to changelesson 05
Occupancy / SDFimplicitnetwork weightsray-march + extractfree; any topologylesson 05
Radiance fieldimplicitnetwork weightsvolumetric ray-marchfree; not a surfacelesson 09
3D Gaussianshybrid#Gaussiansrasterize (real-time)free; explicitlesson 10

The widget below makes the explicit/implicit split tangible in 2D, where a "voxel grid" is a pixel grid and a "surface" is a curve. Flip between the four representations of the same shape and watch what each one stores — and what happens to each when you change the shape's topology.

The representation zoo — one shape, four ways to store it
The same 2D shape (union of two disks — the 2D stand-in for a solid) under each representation. Points and mesh store only the boundary (∝ its length); the grid stores every cell (∝ area, which is here and in 3D); the implicit field stores one function. Drag the gap slider until the two disks merge — the topology changes. Notice which representations absorb that for free and which (the mesh) need their connectivity rebuilt.
Representation
voxel grid
Primitives stored
Memory in 3D
∝ N³
Topology change
free
Show the core JS
// signed distance to a union of two disks: min of the two disk SDFs
function sdf(px, py){
  const dA = Math.hypot(px - ax, py - ay) - r;   // <0 inside disk A
  const dB = Math.hypot(px - bx, py - by) - r;
  return Math.min(dA, dB);                        // union = min (valid SDF outside)
}
const inside = (px,py) => sdf(px,py) < 0;

// VOXEL GRID: fill every cell whose centre is inside → cost is N×N cells (N×N×N in 3D)
for (let i=0;i<N;i++) for (let j=0;j<N;j++)
  if (inside(cellCx(i), cellCy(j))) fill(i,j);    // stored ∝ N²  →  the curse of dimension

// IMPLICIT FIELD: store nothing but the function; the "surface" is the level set {sdf=0}
//   render by colouring each pixel by sdf value and drawing the zero contour.

// POINT CLOUD / MESH: sample the BOUNDARY only (∝ its length). A point on disk A is on
//   the union boundary iff it is outside disk B:
for (const t of angles) {
  const p = onCircle(A, t);
  if (dist(p, B) > r) boundaryPts.push(p);        // stored ∝ perimeter, not area
}
// mesh = connect consecutive boundary points into segments (edges). Merging the disks
//   changes which points are on the boundary → the mesh must be re-connected; the grid,
//   field, and point cloud just... change, with no bookkeeping.

3 · The curse of the third dimension

Why not just use a voxel grid? It is the honest 3D analog of an image — a pixel image is a 2D grid of samples, so a 3D grid of samples should be the natural next step. The problem is arithmetic. An image at resolution N per side has pixels; a volume has voxels. Resolution you take for granted in 2D becomes ruinous in 3D:

Resolution N2D image 3D volume 3D at 4 bytes/voxel
12816 K2.1 M8 MB
512262 K134 M537 MB
10241 M1.07 billion4.3 GB

A 1024³ dense grid of floats does not fit in GPU memory, and a 3D convolution over it costs O(N³) multiply-adds per channel — the reason a naïve 3D CNN (lesson 11) is a non-starter at high resolution. Two escapes, and both drive real architectures:

Implicit representations sidestep the grid entirely: an MLP that maps (x,y,z)→\text{value} has a fixed memory cost (its weights) and is queried at continuous coordinates, so there is no resolution to blow up. You trade the storage explosion for a per-query network evaluation — cheap to store, expensive to render. That trade is the single most important axis in the whole zoo, and it recurs in every lesson from here.

4 · Differentiability — the property that closes the loop

Return to step ④ of the loop. To update the representation S from the image error, we need the gradient ∂L/∂S, and by the chain rule that means differentiating through the renderer: ∂L/∂S = (∂L/∂Î)(∂Î/∂S). If ∂Î/∂S doesn't exist or is zero almost everywhere, gradient descent is dead in the water. This is the requirement that shapes modern 3D more than any other.

Where it bites: the standard graphics rasterizer (built in full in lessons 06–07) is not differentiable. Deciding which triangle covers a pixel is a discrete argmax with a hard edge — nudge a vertex and the pixel either flips to a new triangle or doesn't; the derivative is zero in between and undefined at the jump. So you cannot naively backprop an image loss into mesh vertices. The field's responses to this discontinuity define its landmarks:

The trap that motivates half the track
"Just optimize the mesh to match the photos" fails not because meshes are bad but because the standard renderer's coverage test has no useful gradient. Recognizing that a discontinuous renderer breaks step ④ is the insight that produced differentiable rendering, NeRF, and Gaussian Splatting. When you see a soft/volumetric formulation where a hard one seems simpler, this is usually why.

Where this points next

We now have the frame: 3D vision is inverse rendering, run as a represent→render→compare→backprop loop; the representation you pick (explicit vs implicit) sets the memory cost, the rendering method, and whether the loop is even differentiable. But there is a prerequisite we quietly assumed all through step ①: to place a camera, align a scan, or move an object, we need to transform things in 3D — translate and, worse, rotate them. Translation is easy; rotation is not a vector space, and naïvely optimizing rotations breaks in ways that sink real systems. Lesson 02 builds rigid-body motion — SO(3), SE(3), quaternions, and the Lie-algebra trick that lets you do gradient descent on rotations without ever leaving the space of valid ones. Every later lesson that places a camera (registration, NeRF poses, Gaussian covariances, bundle adjustment) stands on it.

Takeaway
An image is render(scene), and projection is many-to-one, so recovering 3D is an ill-posed inverse problem solved by a loop: pick a representation, render it, compare to observations, backprop the error into the representation. Representations split into explicit (points, voxels, meshes, Gaussians — store the geometry, render fast, optimize with difficulty) and implicit (occupancy, SDF, radiance fields — store a function, optimize easily, render slowly). The curse of dimension makes dense voxels O(N³), so real methods store surfaces or exploit sparsity. And the loop only closes if the renderer is differentiable — the discontinuity of hard rasterization is the reason NeRF and Gaussian Splatting exist. The governing rule: choose the representation from the product constraint, not the fashion.

Interview prompts