Rendering a black hole that spins
The first renderer assumed a non-rotating black hole. Real ones spin — and Interstellar's Gargantua spun at nearly the maximum rate, which is responsible for details the Schwarzschild picture can't produce: a shadow that is flattened on one side and pushed off-center, a disk that reaches far closer in, and light that gets dragged around the hole. The spinning demo renders all of this; use its slider to sweep the spin from 0 to nearly 1 and watch the features appear.
1 — The Kerr metric
A rotating black hole is described by the Kerr metric, with two parameters: the mass M and the spin a = J/M (angular momentum per unit mass), which runs from 0 (Schwarzschild) to M (extremal). This page uses units where G = c = M = 1. The event horizon shrinks as spin grows:
r+ = M + √(M² − a²)
— from 2M at zero spin toward M at maximal spin. The renderer works in Kerr-Schild coordinates, a Cartesian form of the metric with no coordinate trouble at the poles or the horizon — much friendlier for a shader than the usual textbook (Boyer-Lindquist) coordinates.
New to this? What "frame dragging" means
Picture water circling a drain. A toy boat dropped in — even one rowing hard to stay put — gets carried around, because the water itself is moving. A spinning mass does this to spacetime: it doesn't just curve the space around it, it swirls it. Close to a fast-spinning black hole the swirl is so strong that inside a region called the ergosphere, staying still is not merely hard — it is impossible. You would need to move faster than light to hover at a fixed point in the sky. Everything there orbits, willingly or not.
And just like the current carries the boat, the swirl carries light. A photon skimming the hole in the spin direction gets a tailwind; one fighting the spin gets a headwind. That one asymmetry is behind everything else on this page.
2 — Frame dragging and the ergosphere
The dragging rate — how fast a freely hovering observer is swept around, as seen from far away — is ω(r) = 2Mar/A, which falls off as 1/r³: three powers of distance, like a magnetic dipole, which is why it went unmeasured around Earth until Gravity Probe B. At the ergosphere it is overwhelming:
3 — Ray marching without a force law
The Schwarzschild renderer leaned on a lucky exactness: photon paths there are reproduced by a simple inverse-fourth-power force in flat space. Spin breaks that luck — frame dragging has no central-force equivalent — so this renderer integrates the real geodesic equations. The cleanest way is Hamiltonian form. One scalar function of position and momentum,
H(x, p) = ½ [ −pt² + |p|² − f (l·p)² ], f = 2Mr³/(r⁴ + a²z²)
generates the equations of motion by differentiation:
dx/dλ = ∂H/∂p // analytic — a few dot products
dp/dλ = −∂H/∂x // central differences: 6 evaluations of H
// one RK4 step of both, ~300 steps per pixel
For programmers: a Hamiltonian is a cost function for motion
If the phrase "Hamiltonian form" sounds heavier than it is: H is just one scalar function, and the rules say positions move along the momentum-gradient, momenta move down the position-gradient. It's the same shape of idea as gradient descent — a single scalar (loss / energy) whose derivatives drive the update — except here the two gradients feed each other instead of converging, which is what makes it orbit instead of settle.
The position-derivative of H is messy to write out by hand, so the shader
does what you'd do to a loss function you can't differentiate: numerical gradients,
(H(x+ε) − H(x−ε)) / 2ε, three axes, six evaluations. H itself is
~15 lines of dot products. That's the entire integrator.
One subtlety unique to spin: camera rays are traced backwards, and in Kerr you can't just flip the spatial direction — frame dragging is not time-symmetric (a movie of the swirl played in reverse swirls the wrong way). The correct reversal flips the whole 4-momentum, including its time component. Get this wrong and the hole appears to drag light the wrong way.
The integrator was validated before going into the shader:
scripts/validate-kerr.mjs fires photons at the hole and recovers the
critical impact parameters 2.5822 M (prograde) and 6.9164 M (retrograde) at a = 0.95 —
matching Bardeen's exact formulas to four decimal places.
Here is that integrator running in this page (the same physics, in 2D): photons thrown past the hole with the spin and against it, at the same range of distances:
4 — The shadow stops being round
For Schwarzschild there was a single photon sphere at 1.5 rs. With spin it splits into a whole family of unstable photon orbits, from a tight prograde one out to a distant retrograde one, and the shadow rim maps that family onto the sky. Bardeen worked out the exact rim in 1973 — the renderer's shadow matches it, and this figure draws it directly:
Note what does not change much: the overall size. The shadow stays roughly 5 rs wide at every spin — spin reshapes it more than it shrinks it.
5 — The disk moves in
The disk's inner edge is the innermost stable circular orbit, and for matter orbiting with the spin the dragging acts like a stabilizer, letting the gas live far deeper in the well:
Gas at the ISCO of a fast-spinning hole moves at a healthy fraction of light speed, with angular velocity Ω = M½/(r3/2 + a M½). The redshift the renderer applies per disk sample is the exact result for a circular equatorial emitter:
g = 1 / [ ut (1 − Ω λ) ], ut = 1/√(1 − 3M/r + 2a√M/r3/2), λ = Lz/E of the photon
λ comes free during integration (it's the conserved angular momentum of the traced ray), so no approximation is needed — Doppler boost, gravitational redshift, and the dragging correction all live inside that one factor, raised to the third power for beamed intensity.
6 — Everything else carries over
The volumetric disk texture, blackbody coloring, lensed starfield, and ACES tonemap are identical to the non-rotating renderer, which covers them in detail. The only genuinely new ingredients on this page are the metric, the integrator, and the geography of orbits around it.
The figures on this page are dynamically generated SVG, computed from the exact formulas as it loads — the ray fan runs the same Hamiltonian integrator the shader uses, just in JavaScript.
← back to the demo · all experiments · the non-rotating tutorial