Part XIV — 3D Vision, Neural Fields & Gaussian Splatting · Chapter 4

Instant-NGP & spatial multiresolution hash grids

Hook

NeRF's MLP has to be big to memorize fine detail, and a big MLP evaluated millions of times per image is slow. What if the fine detail lived in a lookup table instead, and the MLP itself could stay tiny?

Intuition
coarse: dense, 1 slot per vertex — resolution 2², table size 4
2
3
0
1
fine: hashed, slots reused — resolution 4², table size 8
5
4
7
6
6
7
4
5
7
6
5
4
0
1
2
3
Same-colored chips = same hash slot. The three ringed vertices on the fine grid all hash to slot 6 — they share one learned feature whether they "want" to or not.

Same idea, two grids. The coarse grid is small enough that every vertex gets its own table slot — dense, one color per chip, no repeats. The fine grid has 16 vertices but only 8 slots: three vertices, spread all over the grid, are forced to share slot 6 (ringed, same color). Instant-NGP just accepts that — it bets that gradient descent can still learn something useful per location, as long as most vertices land somewhere of their own.

Formalize

Several grids, coarse to fine, each cover the same space; each vertex owns a learned feature. A resolution-RR grid has R2R^2 vertices — too many to store directly once RR gets large, so each level keeps a fixed-size hash table of size TT instead, and a vertex's feature lives at:

slot(ix,iy)={iyR+ixR2T (dense: one slot per vertex)(ix7iy)modTR2>T (hashed: collisions possible)\text{slot}(ix, iy) = \begin{cases} iy \cdot R + ix & R^2 \le T \text{ (dense: one slot per vertex)} \\ (ix \oplus 7\,iy) \bmod T & R^2 > T \text{ (hashed: collisions possible)} \end{cases}

Querying a point bilinearly interpolates the 4 surrounding vertices at each level, then concatenates every level's result into one encoding:

value(q)=corner kwkfeature(cornerk),wk=(1±fx)(1±fy)\text{value}(q) = \sum_{\text{corner } k} w_k \cdot \text{feature}(\text{corner}_k), \qquad w_k = (1 \pm f_x)(1 \pm f_y)
  • RR — grid resolution (vertices per axis); TT — the level's fixed hash table size.
  • (ix,iy)(ix, iy) — a grid vertex's integer coordinates; \oplus — bitwise XOR.
  • fx,fyf_x, f_y — the query point's fractional position between its 4 surrounding vertices.
  • wkw_k — a corner's bilinear weight; all 4 weights always sum to exactly 1.
  1. Coarse level: 2² = 4 vertices, table size 4 — dense

    Query point (0.3,0.7)(0.3, 0.7) sits fx=0.3f_x=0.3 of the way across, fy=0.7f_y=0.7 up. Each corner's bilinear weight:

    • (1fx)(1fy)=(0.7)(0.3)=0.21(1-f_x)(1-f_y) = (0.7)(0.3) = 0.21
    • fx(1fy)=(0.3)(0.3)=0.09f_x(1-f_y) = (0.3)(0.3) = 0.09
    • (1fx)fy=(0.7)(0.7)=0.49(1-f_x)f_y = (0.7)(0.7) = 0.49
    • fxfy=(0.3)(0.7)=0.21f_xf_y = (0.3)(0.7) = 0.21

    Against features 1,2,3,41, 2, 3, 4: value =0.21(1)+0.09(2)+0.49(3)+0.21(4)=2.70= 0.21(1) + 0.09(2) + 0.49(3) + 0.21(4) = 2.70.

  2. Fine level: 4² = 16 vertices, table size 8 — hashed

    Same query point, now at (0.9,2.1)(0.9, 2.1) in grid units — that's fx=0.9f_x=0.9 across its cell and fy=0.1f_y=0.1 up (the fractional part of 2.12.1). Each corner's weight:

    • (1fx)(1fy)=(0.1)(0.9)=0.09(1-f_x)(1-f_y) = (0.1)(0.9) = 0.09
    • fx(1fy)=(0.9)(0.9)=0.81f_x(1-f_y) = (0.9)(0.9) = 0.81
    • (1fx)fy=(0.1)(0.1)=0.01(1-f_x)f_y = (0.1)(0.1) = 0.01
    • fxfy=(0.9)(0.1)=0.09f_xf_y = (0.9)(0.1) = 0.09

    Against hashed features 70,80,60,5070, 80, 60, 50: value =0.09(70)+0.81(80)+0.01(60)+0.09(50)=76.2= 0.09(70) + 0.81(80) + 0.01(60) + 0.09(50) = 76.2.

  3. Concatenate

    The full encoding at this point is [2.70, 76.2][2.70,\ 76.2] — one number per level, handed to the (now tiny) MLP as input.

Play
fine level — active bilinear stencil — resolution 4², table size 8
5
4
7
6
6
7
4
5
7
6
5
4
0
1
2
3

Drag the query point across the fine grid and watch exactly 4 vertices light up at a time — the bilinear stencil never touches a 5th. Push the point right onto a vertex and one weight goes to 1 while the other 3 vanish: the "interpolation" collapses to a plain lookup.

Worked example

Two more query points on the fine level:

  1. q = (0, 0) — exactly on a vertex

    All weight (1.01.0) lands on corner (0,0)(0,0), which hashes to slot 00: value =10= 10 exactly, with zero contribution from the other 3 corners.

  2. q = (1, 1) — the opposite corner

    All weight lands on vertex (3,3)(3,3) — the same vertex this chapter's Intuition beat showed colliding into slot 6 with two others. Value =70= 70, same as whatever else shares that slot.

Checkpoint

Drag the query point until the fine level's interpolated feature reaches 50 (hint: land close to a single vertex — set y to 1 and x near 1/3).

fine level — resolution 4², table size 8
5
4
7
6
6
7
4
5
7
6
5
4
0
1
2
3
interpolated value = 65.00
Drag the sliders to try it
Summary
slot=(ix7iy)modT,value(q)=kwkfeature(cornerk)\text{slot} = (ix \oplus 7\,iy) \bmod T, \qquad \text{value}(q) = \sum_k w_k \cdot \text{feature}(\text{corner}_k)

Instant-NGP trades exact per-vertex storage for a fixed memory budget: as resolution climbs, collisions become unavoidable, but the MLP that consumes these features stays small and fast regardless. The next chapter drops the neural network from the picture almost entirely — instead of querying a representation at each ray sample, it stores the scene as an explicit pile of Gaussian blobs you can draw directly.