Rendering a black hole, the Interstellar way

The renderer draws Gargantua's three signature features — the dark shadow, the thin photon ring hugging it, and the accretion disk that seems to arc over the hole — without modeling any of them directly. All three fall out of one thing: tracing light rays through curved spacetime, per pixel, on the GPU. This page walks through the math it actually runs.

1 — The spacetime

A non-rotating black hole of mass M is described by the Schwarzschild metric:

ds² = −(1 − rs/r) c²dt² + dr²/(1 − rs/r) + r²dΩ²

where rs = 2GM/c² is the Schwarzschild radius — the event horizon. The renderer works in units where rs = 1 and c = 1 (so M = ½), which makes every formula below pleasantly small.

2 — How light bends

Photons follow null geodesics of that metric.

New to this? What's a geodesic, and why "null"?

A geodesic is the straightest possible path through a space. On a flat sheet of paper that's an ordinary straight line. But on a curved surface, "go straight ahead and never turn" produces something that looks bent from the outside. This is why long flights take those strange-looking arcs on a flat map: the great-circle route over the globe is the actual straight line; the map is what's lying.

Here is the part that makes gravity click. Imagine two ants on a ball, starting a little apart on the equator, both walking perfectly straight "north" — neither ever steers. They start out parallel, yet they drift together and collide at the pole:

Two perfectly straight paths on a curved surface converge — no force pulled the ants together. The shape of the surface did it.

Einstein's idea was that gravity works the same way. A mass like a black hole curves the space (and time) around it, and everything — planets, spaceships, light — just keeps going "straight ahead" through that curved geometry. Nothing tugs on a photon; the photon never turns. The road itself is bent:

The same ray of light with identical "go straight ahead" instructions, in flat space (dashed) and in the curved space around a black hole (solid, computed with this page's integrator). The warped grid is an artist's sketch of how distances stretch near the mass.

And "null"? In relativity, the "distance" between two events mixes space and time, and it has a quirk: along anything moving at exactly the speed of light, that spacetime distance works out to zero. Physicists call such paths null. So "null geodesic" is just compact jargon for: the straightest possible path, traveled at the speed of light — in other words, the route a ray of light takes. That's all the renderer computes, once per pixel.

Every geodesic stays in a single plane, and in that plane, writing u = 1/r as a function of the angle φ, the orbit equation (the relativistic Binet equation) is:

d²u/dφ² + u = (3/2) rs

Without the right-hand side this is a straight line in polar coordinates. The (3/2)rsu² term is the entire general-relativistic correction — negligible far away, dominant near the hole.

A polar ODE per ray plane is awkward in a shader, so the renderer uses an equivalent 3D form. Any planar orbit with conserved angular momentum h = |x × v| can be rewritten as motion under a central acceleration, and matching it to the Binet equation gives an inverse-fourth-power attraction:

a = −(3/2) rsx / r⁵

This is exact for Schwarzschild photons, not a weak-field approximation. In the fragment shader it is two lines, with h² computed once per ray since it is conserved:

vec3 hv = cross(p, v);
float h2 = dot(hv, hv);            // conserved along the ray
...
vec3 a = -1.5 * h2 * p / (r2 * r2 * r);   // r5 = r2 * r2 * r
For programmers: where did the curved-space math go? This looks like a particle sim

Because it is one. If you have ever written an orbit toy — planets attracting each other with pos, vel, acc and a little integrator — the shader loop is exactly that program. Compare:

// Newtonian gravity (a planet in a game):
acc = -G * M * pos / pow(r, 3.0);    // pull toward center, strength ∝ 1/r²

// Schwarzschild photon (this renderer):
acc = -1.5 * h2 * pos / pow(r, 5.0); // pull toward center, strength ∝ 1/r⁴

Same loop, same integrator, same cross and dot products. The only difference is the exponent and the per-ray constant h². So where did all the geodesic machinery go? It got compiled away, in three steps:

Step 1 — geodesic equation. "Straightest path through the metric" is, concretely, a differential equation: it tells you how a path's coordinates (t, r, φ) accelerate as you move along it. Written out for Schwarzschild it's a messy coupled system — nothing you'd want in a shader.

Step 2 — conservation laws shrink it. The metric doesn't change with time or with angle, and each symmetry hands you a conserved quantity (energy, angular momentum) — the same reason cross(pos, vel) stays constant in your orbit toy: a pull pointing straight at the center has no lever arm to apply torque. Using the conserved quantities to eliminate variables, the messy system collapses to one ODE for the orbit's shape, r as a function of φ. The u = 1/r substitution is nothing deep — just a change of variables that makes the ODE almost linear, the same instinct as switching to log-space when it makes the math cleaner. That one-liner is the Binet equation above.

Step 3 — reverse-engineer a force. Now ask a programmer's question: "what acceleration field acc(pos), in ordinary flat xyz space, would make particles trace exactly these curves?" For any central pull there's a standard recipe linking the force law to the orbit ODE, and running it backwards on the Binet equation spits out the inverse-fourth-power law. For Schwarzschild photons the match is exact — not an approximation. All of the curvature survives as that one strange exponent.

The dictionary between the two worlds:

geodesic              →  the polyline your integrator traces
affine parameter λ    →  your loop's accumulated dt (a step counter, not clock time)
conserved E, L        →  the constants you'd factor out of any orbit sim
h = |x × v|           →  cross(pos, vel) — computed once, never changes
metric / curvature    →  baked into the force law's exponent
"null"                →  it's light: only vel's direction matters, |vel| is meaningless

Two honest caveats. First, the parameter you march along is not time — photons don't experience time — it's just "distance along the loop," which is why the renderer can pick whatever step size it likes. Second, this flat-space-force trick is a lucky special case, not how GR is done in general: photons around a non-rotating hole happen to admit an exact equivalent force. A spinning (Kerr) black hole — the kind Interstellar actually modeled — has no such shortcut, and you'd integrate the full geodesic equations instead.

3 — The photon sphere and the shadow

Set d²u/dφ² = 0 in the Binet equation and you get a circular light orbit at r = (3/2) rs: the photon sphere. It is unstable — a ray arriving slightly inside spirals into the hole, slightly outside escapes after wrapping around. Whether a ray makes it is decided by its impact parameter b (the perpendicular offset of the incoming ray from the hole). The critical value is:

bc = (3√3 / 2) rs ≈ 2.598 rs

Drag the slider through bc and watch the ray wrap more and more before the outcome flips:

Computed photon paths (same integrator as the shader). Solid disk: horizon (r = rs). Dashed: photon sphere (1.5 rs). The white ray is the slider's; red rays fall in, the rest escape.
Why bc exists: radial motion obeys (dr/dλ)² = 1/b² − V(r) with effective potential V(r) = (1 − rs/r)/r². If the 1/b² line clears the peak (which sits exactly at r = 1.5 rs), the photon falls in; otherwise it reflects off the potential at the marked turning point.

This is also why the shadow looks bigger than the hole: a distant observer sees darkness out to impact parameter bc ≈ 2.6 rs, not rs. And rays with b just above critical circle the hole once or more before escaping, which is what builds the bright, razor-thin photon ring at the shadow's edge.

4 — Marching the rays

Each pixel fires one ray from the camera and integrates the acceleration law with velocity Verlet. The step size adapts: proportional to r (curvature grows near the hole), clamped finer inside the disk slab so the volumetric texture resolves.

float dt = clamp(0.12 * r, 0.02, 0.7);
if (insideDiskSlab) dt = min(dt, 0.085);

vec3 a1 = -1.5 * h2 * p / (r2 * r2 * r);
vec3 pn = p + v * dt + 0.5 * a1 * dt * dt;
vec3 a2 = accel(pn);
v += 0.5 * (a1 + a2) * dt;
p = pn;

Three exits: fall through the horizon (pixel stays black — that is the shadow), escape past r = 40 (sample the starfield with the bent direction, which is all gravitational lensing of the background is), or run out of steps. Along the way the ray accumulates disk emission with standard volumetric compositing:

color += transmittance * emission * dt;
transmittance *= exp(-absorption * dt);

5 — The accretion disk

The disk is a thin slab of glowing gas in the equatorial plane on circular orbits. Stable circular orbits for matter only exist down to the ISCO (innermost stable circular orbit) at r = 3 rs, so that is the inner edge. The local orbital speed, as measured by a hovering observer, is

v = √( M / (r − 2M) ) · c   ⇒   v(ISCO) = c/2

Gas temperature follows the Shakura–Sunyaev thin-disk profile Tr−3/4, and each sample emits blackbody light at its local temperature. The streaky texture is fractal noise sheared by differential rotation: inner gas orbits faster (Ω ∝ r−3/2), so any blob stretches into a trailing spiral on its own.

The iconic look — the disk visible above and below the shadow even though it is flat and edge-on — is nothing but lensing. Rays leaving the camera slightly upward bend over the hole and land on the top of the disk behind it; rays going under wrap around and see its underside:

Side view, computed paths. The camera sits just above the disk plane (right). Orange dots: rays landing on the disk directly. Blue dots: rays that bent over or under the hole and hit the far side — these form the arcs above and below the shadow.

6 — Redshift, Doppler, beaming

New to this? Sirens, headlights, and why moving gas glows brighter

You already know the main effect here — by ear. An ambulance siren sounds higher-pitched rushing toward you and lower-pitched driving away. The siren never changes; the waves do. A source moving toward you partly chases the waves it just emitted, so they arrive bunched together. Moving away, it leaves them stretched out. Light works exactly the same way: bunched light waves look bluer, stretched ones look redder.

Wavefronts from a source moving at half the speed of light (positions computed, not sketched). Every circle was emitted by the same source at an earlier moment.

Speed does a second, stranger thing. Think of running through rain that's falling straight down: to you, it seems to slant into your face. Light aims the same way — for a glowing blob moving near light speed, even the light it emits sideways gets tilted forward, concentrating its glow into a cone pointing the way it moves. Physicists call it the headlight effect. If the blob is heading toward you, far more of its light reaches your eye, so it looks dramatically brighter; receding, dimmer.

The same blob, emitting light evenly in every direction in its own frame. Right: the identical rays after relativistic aberration at half light speed (computed with the exact formula) — they crowd toward the direction of motion, and each arrow's length shows how strongly that ray is boosted.

Gravity adds a third shift: light climbing out of the hole's gravity well loses energy. It can't slow down — light always travels at c — so it pays in color instead, sliding toward red. (This is real and measurable: light sent between the floors of a building on Earth shifts by a tiny, detectable amount.)

Now put all three together for the accretion disk. The gas orbits at half the speed of light, so the side swinging toward the camera is blueshifted and beamed brighter, while the receding side is reddened and dimmed — and everything pays the gravity toll on the way out. The math below bundles all of it into a single factor g, and that's the whole story of the lopsided ring in the last figure of this section.

Light leaving the disk is shifted twice: climbing out of the gravity well, and by the orbital motion of the gas. The combined factor between emitted and observed frequency is

g = νobsem = √(1 − rs/r) · 1 / [ γ (1 − β·) ]

with β the gas velocity and the photon's direction toward the camera. Because Iν/ν³ is a relativistic invariant, observed intensity scales as g³ — relativistic beaming. At v = 0.5c the approaching side outshines the receding side by a factor of ~25, and its blackbody spectrum shifts hotter (the shader literally evaluates the Planck-locus color at g·T):

Top view of the disk with the camera below. The side rotating toward the camera is beamed brighter and bluer by g³; the receding side is dimmed and reddened. Interstellar rendered this too — then dialed it down because Nolan found the asymmetry confusing. This renderer keeps it.

7 — Finishing

Escaped rays sample a procedural starfield and a faint galactic band using their final bent direction, so background stars smear into arcs near the shadow for free. The accumulated radiance — which spans a huge dynamic range thanks to g³ and T4-ish emission — goes through an ACES tonemap and gamma. That's the whole pipeline: one fragment shader, ~380 integration steps per pixel, no textures, no geometry.

Everything in the figures above was integrated live on this page with the same equation the shader uses — view source, it's ~200 lines.

← back to the renderer · all experiments