A ray tracer that renders a spinning black hole with an accretion disk, gravitational lensing, Doppler beaming, and a star field background. Written in Rust. Inspired by the black hole from Interstellar.
This started as a weekend project to see if I could get a physically plausible black hole render without reaching for a GPU. Turns out, Rust + Rayon make CPU ray tracing surprisingly fast.
The program fires ~2 million rays from a virtual camera, and for each one, numerically integrates the photon's path through the curved spacetime of a Kerr (spinning) black hole. Depending on where the ray ends up, you get one of three outcomes:
- ⚫ Captured — the photon crosses the event horizon. Pixel goes black.
- 🟠 Disk hit — the photon crosses the equatorial plane within the accretion disk's bounds. It gets colored based on blackbody temperature and Doppler shift.
- ✨ Escaped — the photon flies off to infinity. It gets matched against a procedural star field.
After all rays are traced, a simple bloom pass adds a soft glow around bright pixels for that cinematic look.
The gravitational acceleration on a photon at position p with velocity v is:
a = -(M / r^3) * p * (1 + 3 * h^2 / r^2)
Mis the black hole massris the distance from the singularityh^2 = |p x v|^2is the squared angular momentum of the photon
That extra (1 + 3h^2/r^2) term is what makes it general-relativistic rather than Newtonian — it's responsible for producing the photon sphere at r = 3M, which is where light can actually orbit the black hole. Without it, you'd just get boring straight-line deflection.
Integration is done with RK4 (4th-order Runge-Kutta) and adaptive time-stepping. Steps get very small near the horizon to maintain accuracy, and larger out in flat space where precision matters less.
The disk isn't just a flat color. Each point on the disk has a temperature based on its radius — hotter toward the inner edge (~11,000 K, bluish-white) and cooler at the outer edge (~1,800 K, deep red). These temperatures get converted to RGB using a blackbody approximation.
On top of that, Keplerian orbital motion means one side of the disk is moving toward you (blue-shifted, brighter) and the other side is moving away (red-shifted, dimmer). The brightness boost follows a D^3 law, where D is the Doppler factor. This asymmetry is one of the most visually striking features of a real black hole.
You need Rust installed — grab it from rustup.rs if you don't have it.
git clone https://github.com/your-username/blackhole-sim.git
cd blackhole-sim
cargo run --releaseUse --release. Seriously. The release profile has opt-level = 3 and LTO enabled, so it's dramatically faster than a debug build. The render takes a minute or two in release mode; in debug mode, you'll be waiting a while.
Output goes to blackhole.png in the project root. 🖼️
Everything is configured through constants at the top of src/main.rs. No config files, no CLI args — just edit the values and rebuild. Here's what you can play with:
Black hole
| Constant | Default | What it controls |
|---|---|---|
BH_MASS |
1.5 | Shadow size. Bigger mass = bigger shadow |
BH_SPIN |
0.97 | Spin. 0 = non-spinning (Schwarzschild), 1 = maximum spin |
Camera
| Constant | Default | What it controls |
|---|---|---|
CAM_R |
35.0 | How far the camera is from the black hole |
CAM_INCL |
80 degrees | Viewing angle. 90 = edge-on, 0 = looking down the spin axis |
CAM_FOV |
28 degrees | Field of view |
Disk and rendering
| Constant | Default | What it controls |
|---|---|---|
DISK_INNER |
3.5 | Inner edge of the accretion disk |
DISK_OUTER |
16.0 | Outer edge of the accretion disk |
NUM_STARS |
5000 | Background stars |
MAX_STEPS |
4000 | Max integration steps per ray (increase if rays aren't converging) |
WIDTH / HEIGHT |
1920 x 1080 | Output resolution |
- Schwarzschild: Set
BH_SPIN = 0.0— no spin, perfectly symmetric shadow - Polar view: Set
CAM_INCL = 15.0— looking almost straight down. You'll see the disk as a near-perfect ring - Edge-on: Set
CAM_INCL = 89.0— extreme equatorial view, lots of warped lensing arcs - Higher res: Bump
WIDTH/HEIGHTup if you want a wallpaper. Render time scales linearly
The whole thing is a single file (src/main.rs). It's about 300 lines. Here's the rough layout:
V3 — basic 3D vector type (dot, cross, normalize, etc.)
accel() — the GR acceleration formula
step() — one RK4 timestep
make_ray() — maps a pixel coordinate to a ray direction
trace() — integrates a ray until it hits something or escapes
disk_rgb() — temperature + Doppler -> RGB for disk hits
bb_rgb() — converts a Kelvin temperature to an RGB color
gen_stars() — generates a random star field (seeded, so it's deterministic)
sky_rgb() — looks up the nearest star for escaped rays
bloom() — post-processing glow
main() — ties everything together
Just three crates, nothing exotic:
- image (0.25) — for writing the output PNG
- rayon (1.10) — for parallelizing the ray tracing across all CPU cores
- rand (0.8) — for generating the star field (uses a fixed seed, so renders are reproducible)
- The star field uses a fixed RNG seed (42), so you'll get the same stars every time. Change the seed in
gen_stars()if you want a different sky. - The event horizon radius is computed as
r+ = M + sqrt(M^2 - a^2)wherea = spin * M. For the default spin of 0.97, the horizon is quite small. - Bloom radius is 3 pixels with an exponential falloff. It's subtle but makes the hot inner disk glow nicely.
MIT — do whatever you want with it. 🤙
