KERRSCOPE

one geodesic per pixel

Gravitational lensing here is not a screen-space effect. It is an integration.

A real-time Kerr black hole ray tracer. For every pixel on the screen the shader integrates Hamilton's equations backwards through curved spacetime until the photon lands on the disk, on an orbiting body, on the sky, or inside the hole. The shadow and the photon ring are not drawn. They fall out of the integration.

A Kerr black hole at spin a/M = 0.85. The accretion disk wraps over and under the shadow, brighter on the approaching side, and two orbiting bodies appear as thin white arcs above and below.
Fig. 1 Spin a/M = 0.85, disk outer edge 20 M, camera at 26 M. Rendered by the program, not illustrated. The bright side is the approaching side: specific intensity scales as g³.

What happens to one pixel

Five steps, about 163 of them per ray

Units are geometrised, G = c = 1, so every length below is in units of GM/c². The order matters — this is the sequence the shader runs, per pixel, per frame.

  1. Convert the camera position from pseudo-Cartesian coordinates to Boyer-Lindquist (r, θ, φ).

  2. Build the photon four-momentum in the local orthonormal frame of a zero angular momentum observer, normalised to unit local energy.

  3. Lower indices with the covariant Kerr metric to get the conserved E = −p_t and L = p_φ.

  4. Integrate Hamilton's equations with RK4, using analytic derivatives of the inverse metric. t and φ are cyclic, so only five quantities are carried.

  5. Accumulate emission at every equatorial crossing between the ISCO and the disk's outer edge; sample a procedural sky if the photon escapes to r = 1000 M.

dxⁱ/dλ =  ∂H/∂pᵢ        H = ½ g^{μν} p_μ p_ν   ( = 0 for a photon )
dpᵢ/dλ = −∂H/∂xⁱ

The emitting gas orbits prograde at Ω = √M / (r^{3/2} + a√M). Because the photon was normalised to unit energy in the camera frame, the whole redshift factor collapses to g = 1 / [ u^t (E − Ω L) ] — gravitational redshift, transverse and longitudinal Doppler and frame dragging in one number.

Measurement

The integrator is held to analytic results, not to taste

This is a physics calculation that happens to draw pictures, so it is checked the way a calculation is checked. A double-precision harness runs the same equations as the shader and compares them against values that are known in closed form. Every number below is asserted in CI on every push.

Critical impact parameter, camera at r = 26 M, production step policy
Spin a/M Numeric b Analytic b Rel. error p99 steps
0.005.1961535.1961528.2e−08126
0.504.0962674.0962671.4e−07131
0.853.0543373.0543361.6e−07149
0.952.5822132.5822122.0e−07163

b_crit is the strongest single check in the project, because it is the angular radius of the shadow. It only comes out right if the metric, the ZAMO tetrad, the constants of motion and the integrator are all correct at the same time. The row for a/M = 0.85 is the measurement labelled in Fig. 1.

Other checks
Null condition H = 0 along a ray, a/M = 0.9max |H| = 2e−06
Weak-field deflection vs 4M/b + 15πM²/4b²1.0e−04 at b = 320 M, falling as 1/b²
Frame dragging, radially aimed photon+2.15986 rad at a/M = 0.9; exactly 0 at a = 0
Fragment-shader port vs the original compute shader0 of 360000 pixels differ

Numbers are not the whole story, and the project says so. Cross-compiling the shader to a CPU renderer and looking at the output caught two bugs every numerical test had passed: a dark seam along the projected spin axis from the sin θ → 0 coordinate singularity, and stars visible inside the shadow, where RK4 stages stepped to r < r₊, Δ changed sign and photons were ejected back out.

Spin

Frame dragging is visible, and it is in the photon paths

Frame dragging enters through the g^{tφ} terms of the metric, so it bends the light itself rather than tinting what is behind it. Switch the spin and watch the shadow go off-centre and flatten on the approaching side.

The same scene at zero spin: the shadow is circular and centred, and the disk is symmetric about the axis. The same scene at spin a/M = 0.85: the shadow is off-centre and flattened on the brighter, approaching side.
Fig. 2 Spin a/M = 0.85. The orbiting bodies are on in this frame and off in the a = 0 one, so ignore the white arcs and watch the shadow.

Constraint

It runs on a laptop from 2012

The geodesic integration is in a fragment shader rather than a compute shader. Nothing here needs compute support, storage buffers or any extension: OpenGL 3.3 core is the floor, and it is a deliberate one.

Cost is roughly pixels × steps. The renderer traces at a fraction of the window resolution and then converges: each frame jitters the ray on a Halton sequence and averages into a float buffer as an exact running mean, so a still camera reaches a clean antialiased frame in about a second. That suits slow hardware far better than blurring would — the photon ring is a couple of pixels wide, and blur destroys exactly the feature you came to look at.

Work per frame, 1280 × 720 window
Render scaleTraced pixelsPer frame
0.15192 × 108~0.9 GFLOP
0.25 (default)320 × 180~2.4 GFLOP
0.35448 × 251~4.6 GFLOP
0.50640 × 360~9.4 GFLOP

What it does not do

The limits, stated rather than discovered

Spin is capped at a/M = 0.95
Near extremal, the outer horizon, the inner horizon and the prograde photon orbit crowd into a shell a few hundredths of M thick. Boyer-Lindquist ray tracing in 32-bit floats stops being trustworthy there: photons leak back out and speckle the shadow, measured at 3.6% of shadow pixels at a/M = 0.998 against 0% at 0.85. Fixing it properly needs horizon-penetrating coordinates.
About 0.4% of pixels exhaust the step budget
They render black. Raising the cap from 320 to 700 changes nothing, so those rays are genuinely trapped near the photon shell rather than merely slow. The visible effect is a thin dark arc exactly where the photon ring is brightest — cosmetic, but it is there.
The disk is a zero-thickness sheet
A Novikov-Thorne-like radial profile with procedural turbulence. It is not a radiative-transfer solution, and the turbulence is noise rather than magnetohydrodynamics. Light travel time is not accounted for in its rotation.
The disk colour is an artistic choice
diskTemperature defaults to 2000 K, and the documentation says plainly that this is a choice. A physically realistic accretion flow at 10⁴–10⁷ K renders as a white smear with no visible colour structure, because the normalised Planckian locus is almost colourless above about 5000 K. The physics is identical either way; only the colour lookup changes.

Get it

Download a build, or compile it in two commands

Releases carry a Linux binary and a Windows .exe, each with the shaders alongside. Unpack and run — there is nothing to install.

# needs a C++17 compiler, CMake 3.16+
# and an OpenGL 3.3 driver
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j
./build/kerr

GLFW and GLM are taken from the system if they are there and fetched if they are not. There is no third dependency: the GL loader is 44 entry points, vendored, and the PNG writer emits DEFLATE stored blocks rather than linking zlib.