A terminal-based, real-time renderer of a non-rotating (Schwarzschild) black hole, complete with a glowing accretion disk and gravitational lensing of the background — written entirely in C.
Light rays are integrated step-by-step through a gravitational field that approximates the geodesic equation of a Schwarzschild photon. The result is the iconic "Interstellar" view — a dark event horizon ringed by a disk whose far side is warped above and below the singularity by the curvature of spacetime itself.
No graphics library. No GPU. Just photons, vectors, and stdout.
This project focuses on learning:
- ray tracing in curved spacetime
- numerical integration of photon trajectories
- accretion disk rendering
- gravitational lensing
- ANSI 256-color terminal rendering
- real-time rendering in C
- Schwarzschild black hole with event horizon
- Glowing accretion disk in the equatorial plane
- Hot-to-cool color gradient on the disk (white-yellow inner -> deep red outer)
- Gentle angular swirl pattern
- Gravitational lensing of the background star field
- Star sprinkle from a directional hash
- Orbiting camera
- Gamma-corrected ANSI 256-color output
- Pure terminal rendering
- Written entirely in C
- Standard libraries only
For every pixel, a photon is shot from the camera through that pixel and integrated backwards in time. At each step the photon is deflected by an approximation of the gravitational pull of the black hole. The integration stops when one of three things happens:
- the photon falls past the event horizon -> the pixel is black
- the photon escapes to infinity -> the pixel samples the sky
- the photon's trajectory crosses the accretion-disk plane between the inner and outer disk radii -> the pixel takes the disk color at that radius
Because the gravitational pull bends the photon's trajectory, you can see behind the black hole — the back half of the disk appears above and below the silhouette, ringed by the photon sphere.
The defining length scale of a black hole is the Schwarzschild radius:
r_s = 2 G M / c^2
In this program we set r_s = 1 and work in dimensionless units. The event horizon is at r = r_s, the photon sphere at r = 1.5 r_s, and the innermost stable circular orbit (ISCO) at r = 3 r_s — which is where we place the inner edge of the accretion disk.
For Schwarzschild geodesics, the bending angle of a light ray passing at impact parameter b is approximately:
delta phi ≈ 4 G M / (c^2 b)
That is exactly twice the Newtonian deflection. To approximate this in our simulation we apply a 1.5 * r_s / r^3 acceleration on the photon (pointing toward the black hole). This gives the right qualitative lensing while keeping the integrator extremely simple:
accel = -1.5 * r_s / r^3 * pos
dir = normalize(dir + accel * dt)
pos = pos + dir * dt
Every step. For every pixel. Every frame.
The disk lives in the y = 0 plane between R_INNER and R_OUTER. We detect disk crossings by tracking the photon's previous and current y and looking for a sign change:
if (prev_y * pos.y < 0) {
t = prev_y / (prev_y - pos.y);
crossing = lerp(prev, pos, t);
r = sqrt(cross.x^2 + cross.z^2);
if R_INNER <= r <= R_OUTER:
return disk_color(r, atan2(cross.z, cross.x));
}
Colors are temperature-like: brilliant white-yellow at the inner edge (where the gas orbits fastest and shines hottest), fading through orange to deep red at the outer edge. A sin(phi*6 + r*2.5) factor adds a gentle swirl.
When a photon escapes to r > ESCAPE_R we treat its current direction as a ray into the background. The background is a deep purple/blue gradient with deterministically-placed "stars" added by hashing the ray direction:
h = sin(dir.x * 91.3 + dir.y * 47.8 + dir.z * 73.1) * 4321.7;
h -= floor(h);
if (h > 0.998) -> star
Because the stars are computed from the photon's final direction after lensing, you see the star field warped — a smoking-gun signature of gravitational lensing.
Every frame the camera orbits the black hole at a fixed radius, slightly above the disk plane:
cam = (R * cos(angle), height, R * sin(angle));
A standard camera basis (fwd, right, up) is constructed and rays are launched from each pixel as in a normal raytracer.
Each photon's final RGB color is gamma-corrected and quantized to the 6x6x6 RGB cube of ANSI 256 colors:
index = 16 + 36 * R + 6 * G + B (with R, G, B in [0, 5])
The pixel is printed as a space with that color as its background:
printf("\x1b[48;5;%dm ", index);
Adjacent pixels with the same color share one escape code to keep output fast.
Compile using:
gcc horizon.c -o horizon -lm
Or just:
make
The -lm flag links the math library.
./horizon
Press Ctrl+C to exit.
For best results, run in a 256-color terminal at roughly 80 columns × 32 rows.
Edit the constants near the top of horizon.c:
RS,EVENT_HORIZON— black hole geometryR_INNER,R_OUTER— accretion disk extentMAX_STEPS,STEP— integration accuracy vs. speed- Disk color palette —
disk_color() - Camera radius, height, FOV —
main()
Try tilting the camera further to view the disk edge-on, or reducing the disk thickness to make a sharper photon ring.
- Ray tracing in curved spacetime
- Numerical integration of photon trajectories
- General relativity intuition (Schwarzschild)
- Accretion disk physics (simplified)
- Gravitational lensing
- Vector math in 3D
- Camera basis construction
- ANSI 256-color rendering
- Gamma correction
- Real-time rendering loops
- Terminal graphics
Standard C libraries only:
stdio.hstdlib.hstring.hmath.hunistd.h
No graphics engine required.
Heavily inspired by the gravitational-lensing renderer Kip Thorne and the Interstellar VFX team built for the film's depiction of Gargantua — the same physics, but reduced to a few hundred lines of C and a terminal.
A black hole is the ultimate visual demonstration of general relativity. Every pixel in this program is, in a small way, integrating one of Einstein's field equations.