all lessons / computer_vision_3d / 09 · volume rendering & NeRF lesson 9 / 13

Volumetric rendering and NeRF

Lessons 05–08 built hard geometry and the full forward rasterizer that draws it — all the way to photorealistic PBR, shadows, and global illumination (lesson 08) — yet it stayed hand-authored and non-differentiable: the rasterizer's discontinuous coverage test has no useful gradient (lesson 01 §4), so you cannot invert it. But suppose all you have is a stack of photos and you want to recover the scene's appearance, not just its shape — you need a representation and a renderer that are differentiable end-to-end. NeRF's answer inverts the hard-surface intuition: instead of a crisp surface, represent the scene as a continuous volume of colored, semi-transparent fog, and render it by an integral you can backpropagate through. This lesson derives that integral from first principles — the derivation the CV track (CV lesson 05) only surveyed.

The plan
Five moves. (1) The representation: a radiance field F:(x,y,z,\mathbf d) → (\mathbf c, σ) — an MLP whose density depends on position only (so geometry is multiview-consistent) and whose color also depends on view direction (so highlights can move). (2) Derive the volume rendering integral from the physics of an absorbing/emitting medium: transmittance obeys an ODE, and the pixel color is an expectation over where the ray terminates. (3) Discretize that integral and watch it collapse into plain front-to-back alpha compositing — the very equation lesson 10 reuses. (4) Make the MLP actually work: positional encoding to beat spectral bias, the position/direction split, hierarchical sampling, and training by photometric loss with no 3D supervision. (5) Failure modes and speedups — why NeRF is slow and per-scene, and how hash grids and factorized fields fix it, setting up lesson 10's fully explicit alternative.

1 · The representation — a radiance field

Drop the idea of a surface entirely. A radiance field is a function that, for every point in space and every viewing direction, returns how much light is emitted there and how opaque the point is:

F:\ (x,y,z,\ \mathbf d) \ \longrightarrow\ (\mathbf c,\ σ), \qquad \mathbf c = (r,g,b),\ \ σ ≥ 0 .

Here (x,y,z) is a 3D position, \mathbf d is a unit view direction, \mathbf c is an emitted RGB color, and σ is a volume density — an absorption rate per unit length, with units of inverse distance (σ\,dt is a dimensionless probability of interaction over a step dt). NeRF parameterizes F as a small multilayer perceptron: feed in the coordinate, read out color and density. The scene is the weights.

One design choice carries most of the method's correctness, and it is deliberate: density depends on position only, while color depends on position and direction:

σ = σ(x,y,z), \qquad \mathbf c = \mathbf c(x,y,z,\ \mathbf d) .

The reasoning is physical. Where the scene is opaque is a property of geometry — a table leg blocks light from every viewpoint — so density must be a function of position alone; letting it vary with direction would let the geometry look solid from one camera and empty from another, destroying multiview consistency. But the color a surface sends toward you legitimately changes with viewpoint: a specular highlight on that table leg slides across it as you move. Splitting the two lets one field explain both view-independent geometry and view-dependent shading. We return to this in §4 as an inductive bias; for now note it is the reason a NeRF trained on many views agrees with itself in 3D.

Not a surface — and that is the point
This is emphatically not the hard SDF surface of lesson 05, where a point is either inside or outside and the boundary is a razor-thin level set. It is semi-transparent emissive fog: every point has a density and glows a little. That softness is not a modeling compromise — it is precisely what makes rendering differentiable. Contrast the hard rasterizer of lesson 01 §4, where a vertex nudge flips a pixel's triangle with a zero/undefined derivative; here, nudging the field changes density smoothly, so the pixel color changes smoothly, and gradients flow. The fog is the trick.

2 · The volume rendering integral

We derive the color of one pixel. A pixel corresponds to a camera ray

\mathbf r(t) = \mathbf o + t\,\mathbf d, \qquad t \in [t_{\text{near}}, t_{\text{far}}] ,

starting at the camera center \mathbf o and marching outward along direction \mathbf d. The ray passes through the participating medium (the fog), which both absorbs light and emits it. We want the total light that arrives back at the camera along this ray.

Transmittance and its ODE. Define transmittance T(t) as the probability that a photon travels from the near bound to t without being absorbed — equivalently, the fraction of light that survives the trip to t. Consider an infinitesimal step from t to t+dt. By definition of density as an absorption rate, a fraction σ(t)\,dt of the light present is absorbed over that step. So the surviving fraction is multiplied by (1 - σ(t)\,dt):

T(t+dt) = T(t)\,\big(1 - σ(t)\,dt\big) \ \Longrightarrow\ \frac{dT}{dt} = -σ(t)\,T(t) .

This is the same first-order linear ODE that governs radioactive decay and Beer–Lambert attenuation. With the boundary condition T(t_{\text{near}}) = 1 (nothing has been absorbed at the start), it integrates to an exponential of accumulated density:

T(t) = \exp\!\Big(-\!\int_{t_{\text{near}}}^{t} σ(s)\,ds\Big) .

The color integral. Now assemble the light reaching the camera. At each point t along the ray, the medium emits color \mathbf c(t) at a rate proportional to its density σ(t) (denser fog emits more), and that emitted light must then survive the remaining trip back to the camera — it is attenuated by exactly T(t). Integrating this emitted-and-surviving contribution over the whole ray:

\mathbf C(\mathbf r) = \int_{t_{\text{near}}}^{t_{\text{far}}} T(t)\,σ(t)\,\mathbf c(t)\,dt , \qquad T(t) = \exp\!\Big(-\!\int_{t_{\text{near}}}^{t} σ(s)\,ds\Big) .

This is the volume rendering integral. It is worth reading the three factors under the integral as a probability statement, because that reading makes the whole thing intuitive and makes §3 obvious:

So \mathbf C is nothing but the expected color at the point where the ray terminates — a weighted average of the emitted colors \mathbf c(t) along the ray, weighted by the probability the ray ends there. A hard opaque surface is the limit where all that termination probability piles up at a single depth; fog spreads it out. (One can verify the weights integrate to the "opacity" of the ray: \int T(t)σ(t)\,dt = 1 - T(t_{\text{far}}), which equals 1 for a fully opaque ray and 0 for empty space.)

3 · Discretize → alpha compositing

The integral has no closed form for an arbitrary MLP σ,\mathbf c, so we evaluate it by quadrature. Sample N points t_1 < t_2 < \dots < t_N along the ray with spacing δ_i = t_{i+1} - t_i, and assume density is piecewise-constant on each segment (equal to its sampled value σ_i).

Segment opacity. Over a segment of length δ_i with constant density σ_i, the transmittance across that one segment is \exp(-σ_iδ_i) by the ODE solution above. The fraction absorbed within the segment — its opacity — is one minus that:

α_i = 1 - \exp(-σ_i\,δ_i) \ \in [0,1) .

This is the local alpha of sample i: σ_iδ_i → 0 gives α_i → 0 (transparent), σ_iδ_i → ∞ gives α_i → 1 (fully opaque).

Accumulated transmittance. The probability of surviving up to sample i is the product of surviving every earlier segment:

T_i = \prod_{j

Sample weight and color. The discrete analog of the termination density T(t)σ(t) is the weight

w_i = T_i\,α_i, \qquad \mathbf C \approx \sum_{i=1}^{N} w_i\,\mathbf c_i = \sum_{i=1}^{N} T_i\,α_i\,\mathbf c_i .

State plainly what this is: it is exactly front-to-back alpha compositing — the "over" operator, the alpha-blend the CV survey named without deriving. Walk the ray from the camera outward; at each sample you deposit its color scaled by how opaque it is (α_i) and by how much light is left to deposit it (T_i), then you attenuate the remaining budget by (1-α_i). Each T_i = \prod_{j is precisely the "remaining transparency" a painter tracks when layering translucent glazes front to back.

The unification to say out loud in an interview
This compositing sum \mathbf C = \sum_i T_i α_i \mathbf c_i is the same equation lesson 10 (3D Gaussian Splatting) uses to render. The only difference is what is being composited: NeRF sums over MLP samples marched along a single ray, whereas Gaussian Splatting sums over explicit primitives sorted by depth and projected to the pixel. Same alpha-over operator, same T_i = \prod(1-α_j); different generator of the (α_i, \mathbf c_i) list. If you understand this section, you have already understood the compositing half of splatting.

Two free readouts. Because the weights w_i are a distribution over termination depth, the same sum gives geometry for free. The expected depth (a depth map straight out of the field) and the accumulated opacity (how much the ray hit anything at all) are

\hat t = \sum_i w_i\,t_i, \qquad \text{opacity} = \sum_i w_i = 1 - T_{\text{final}} .

Opacity near 0 means the ray sailed through empty space (useful for compositing NeRFs onto backgrounds); the expected depth is what the widget below plots as a vertical marker. All of this — color, depth, opacity — is differentiable in σ_i and \mathbf c_i, hence in the MLP weights, which is the entire reason we chose fog.

4 · Making the MLP actually work

The integral is correct but a naïve MLP renders blurry mush and cannot represent view-dependent effects. Four ideas turn the recipe into NeRF.

(a) Positional encoding — beating spectral bias

Feed raw coordinates (x,y,z) to an MLP and the output is smooth and blurry, missing all fine texture. The cause is spectral bias: MLPs preferentially fit low frequencies first and are strongly reluctant to represent high-frequency detail — the same frequency-domain intuition developed in CV lesson 02, now biting the network rather than a convolution. The fix is to lift each scalar input through a bank of sinusoids at geometrically increasing frequencies before the MLP sees it:

γ(p) = \big(\sin(2^{0}πp),\ \cos(2^{0}πp),\ \dots,\ \sin(2^{L-1}πp),\ \cos(2^{L-1}πp)\big) .

Applied to each of x,y,z (and, at lower L, to the direction), this injects high-frequency content into the input so that a smooth function of γ(p) can be a high-frequency function of p. Fine detail — bark, text, thin structures — becomes representable. Concretely with L=10 for position, a 3-vector becomes a 3 × 2 × 10 = 60-dimensional encoded input.

(b) View dependence — a deliberate inductive bias

Reiterating §1 now that we see it operationally: the MLP first maps encoded position to density σ and a feature vector; only then is the encoded direction concatenated to produce color \mathbf c. Density literally cannot see the direction. This architectural split is an inductive bias — it forces multiview-consistent geometry (one density field for all cameras) while still permitting view-dependent shading (color is free to swing with \mathbf d). It is the network-level enforcement of the physical argument from §1.

(c) Hierarchical sampling — spend samples where the scene is

Uniform samples along a ray waste almost all their budget in empty space, since a scene is mostly air with a thin shell of matter (the O(N^2)-surface-in-O(N^3)-volume observation from lesson 01 §3). NeRF trains two networks. A coarse network is queried at uniform samples and produces weights w_i. Normalize those weights into a piecewise-constant pdf \hat w_i = w_i / \sum_j w_j along the ray, then importance-sample a second, fine network from that pdf (inverse-CDF sampling), concentrating new samples exactly where the coarse pass thinks a surface is. The fine render uses the union of coarse and fine samples. Same integral, far fewer wasted evaluations.

(d) Training — geometry emerges from photometric consistency

The supervision is startlingly simple. From a set of posed images — camera intrinsics and SE(3) extrinsics recovered by SfM (CV lesson 05), the pose machinery of lesson 02 — cast a ray through each training pixel, render its color \hat{\mathbf C} by the sum in §3, and minimize the photometric mean-squared error against the observed pixel color:

\mathcal L = \sum_{\text{rays } \mathbf r} \big\| \hat{\mathbf C}(\mathbf r) - \mathbf C_{\text{gt}}(\mathbf r) \big\|^2 .

There is no 3D supervision — no depth, no mesh, no occupancy labels. The density field is never told where surfaces are. Geometry emerges as the only configuration of fog that, composited by the volume rendering integral, reproduces all the training views simultaneously: a surface must sit where many rays from different cameras agree on color, because only a consistent opaque structure explains that agreement. This is multiview photometric consistency doing the work classical stereo did, but end-to-end differentiable. Optimization is per scene (the weights encode one scene) and takes minutes to hours on a GPU.

5 · Failure modes and speedups

NeRF is a landmark, not a finish line. Its limitations map directly onto later lessons:

LimitationWhyAddressed in
Needs many, accurately-posed viewsphotometric consistency needs overlap; bad poses blur or ghost the fieldpose refinement (lesson 02 / SfM)
Per-scene; does not generalizeweights are one scene — a new scene means retraining from scratchlearned priors, lesson 11
Slow to renderhundreds of MLP queries per ray × millions of rays → seconds per framegrids/splatting (below, lesson 10)
"Floaters" & background hazedensity in empty space that happens to fit a few views (an ambiguity)regularizers; explicit occupancy
Static-scene assumptionthe field has no time axis; anything that moves is inconsistentdynamic NeRF, lesson 13

The floater deserves a look because the widget makes it visible: a spurious blob of density floating in front of the true surface. The volume rendering integral handles it correctly in the sense that the front blob occludes what is behind it — transmittance T drops after the blob, so the true surface receives less weight and the expected depth is pulled forward. That is the mechanism, and it is also why floaters are pernicious: once one forms, it steals weight from the real geometry.

Speedups. The slowness is almost entirely the "hundreds of MLP queries per ray" cost, so the fixes attack the MLP:

  • Instant-NGP — multiresolution hash grid. Replace most of the MLP's job with learned features stored on a grid, indexed at multiple resolutions through a spatial hash, feeding a tiny MLP. Because most cells are empty, the hash lets many grid coordinates share table slots without an explicit octree — the sparsity trick of lesson 01 §3 applied to a radiance field. Training drops from hours to seconds.
  • Factorized fields (TensoRF). Represent the volume as a sum of low-rank outer products of vectors/matrices along the axes, turning a dense 3D grid into a few 1D/2D factors — compact and fast.
  • Baking to real-time structures. Distill a trained NeRF into an explicit format (sparse voxels, textured meshes, or precomputed samples) that renders in real time.

Each of these keeps the volume rendering integral of §2–§3 and speeds up the field query. Lesson 10 takes the more radical step: abandon the implicit field and the per-ray march entirely, keep only the compositing equation, and composite millions of explicit Gaussian primitives that a GPU rasterizes in real time.

1D volume rendering along a ray
The ray runs left → right (camera on the left, at t=0). Shape the density profile σ(t) with two "bumps" — two candidate surfaces, one orange, one teal — using their position and height sliders, and set how sharp each bump is (sharp = a hard surface, wide = fog). We sample ~200 points, compute α_i = 1-e^{-σ_iδ}, T_i = \prod_{j, and the weight w_i = T_iα_i. Watch how the weight (filled) peaks where a surface is and the ray still has transmittance left. Try: one sharp tall bump → weight spikes at one depth (crisp surface); a wide low bump → weight smears out (foggy, blurry depth); two bumps → the front one occludes the back, so T collapses behind it and the expected depth is pulled forward — that is a floater.
Composited color C
Accumulated opacity Σw
Expected depth Σ w·t
Peak weight (surface-ness)
Show the core JS
// two Gaussian density bumps along the ray; `sharp` narrows them (hard surface) vs wide (fog)
var sig = new Float64Array(N), col = [];       // density and emitted colour per sample
for (var i=0;i<N;i++){
  var t  = i/(N-1);                            // normalised depth in [0,1]
  var g1 = h1*Math.exp(-0.5*Math.pow((t-p1)/wdt,2));   // orange bump
  var g2 = h2*Math.exp(-0.5*Math.pow((t-p2)/wdt,2));   // teal   bump
  sig[i] = g1 + g2;
  col[i] = blend(ORANGE, g1, TEAL, g2);        // colour = density-weighted mix of the two
}

// the discrete volume-rendering / alpha-compositing sum (front to back)
var T = 1, C = [0,0,0], opac = 0, depth = 0, peak = 0;
for (var i=0;i<N;i++){
  var a = 1 - Math.exp(-sig[i]*d);             // α_i = 1 − e^(−σ_i·δ)
  var w = T * a;                               // w_i = T_i · α_i   (T_i = Π_{j<i}(1−α_j))
  C[0]+=w*col[i][0]; C[1]+=w*col[i][1]; C[2]+=w*col[i][2];
  opac += w;  depth += w * (i/(N-1));  peak = Math.max(peak, w);
  T *= (1 - a);                                // attenuate remaining light
}
// opac = Σ w_i = 1 − T_final ;  depth = Σ w_i t_i (expected termination depth)

Where this points next

We now have the derivation the CV track only surveyed: a scene is a continuous radiance field, a pixel is the expectation of emitted color over where its ray terminates, and evaluating that expectation is front-to-back alpha compositing — differentiable, so plain photometric gradient descent recovers geometry with no 3D labels. The one dissatisfaction left is speed: hundreds of MLP queries per ray is inherently slow, and the fixes in §5 chip at it without changing the shape of the computation. Lesson 10 keeps the compositing equation of §3 verbatim but throws out the implicit field and the per-ray march: it represents the scene as millions of explicit 3D Gaussians — the hybrid primitive introduced in lesson 01 — projects them to the image, sorts them by depth, and composites them with a GPU rasterizer, reaching real-time rendering while staying differentiable. NeRF taught us what to composite and why it trains; splatting changes only who is on the composite list.

Takeaway
NeRF represents a scene as a radiance field F:(x,y,z,\mathbf d)→(\mathbf c,σ) — an MLP where density is position-only (multiview-consistent geometry) and color is view-dependent (moving highlights) — i.e. semi-transparent fog, whose softness is exactly what makes rendering differentiable. Transmittance obeys dT/dt = -σT, giving T(t)=\exp(-\!\int σ), and a pixel is the expected emitted color at the ray's termination point: \mathbf C = \int T(t)σ(t)\mathbf c(t)\,dt. Discretized with α_i = 1-e^{-σ_iδ_i}, T_i = \prod_{j, w_i=T_iα_i, this is front-to-back alpha compositing \mathbf C=\sum_i w_i\mathbf c_i — the same equation lesson 10 uses. Positional encoding defeats MLP spectral bias; hierarchical sampling spends samples on surfaces; training is a plain photometric MSE from posed images with no 3D supervision — geometry emerges. It is slow and per-scene; hash grids (Instant-NGP), factorized fields (TensoRF), and baking are the speedups, and explicit Gaussian Splatting is the next step.

Interview prompts

  • Derive the volume rendering integral from the transmittance ODE, and interpret T(t)σ(t) probabilistically. (§2 — dT/dt=-σT ⇒ T=\exp(-\!\int σ); T(t)σ(t)\,dt is the probability the ray terminates in [t,t+dt), so \mathbf C is the expected color at termination.)
  • Show the discretized integral equals front-to-back alpha compositing. (§3 — α_i=1-e^{-σ_iδ_i}, T_i=\prod_{j, \mathbf C=\sum_i T_iα_i\mathbf c_i; it is the "over" operator, identical to lesson 10's compositing.)
  • Why does density depend on position only while color depends on position and direction? (§1, §4 — position-only density enforces multiview-consistent geometry; view-dependent color captures specular highlights. It is a deliberate inductive bias.)
  • Why is positional encoding necessary, and what does it fix? (§4 — MLPs have spectral bias and fit low frequencies first; γ(p) injects high-frequency sinusoids so fine detail becomes representable — the CV lesson 02 frequency view.)
  • How does NeRF recover geometry with no 3D supervision? (§4 — photometric MSE between rendered and observed pixels from posed (SfM) images; a consistent density field is the only fog that explains all views, so geometry emerges.)
  • Why is vanilla NeRF slow, and how do Instant-NGP / TensoRF / splatting speed it up? (§5 — hundreds of MLP queries per ray; hash grids + tiny MLP, low-rank factorization, or baking; splatting drops the field and per-ray march, keeping only compositing.)