No install, no account. It needs WebGL2, which every current desktop browser has.
An interactive black hole visualizer that ray-traces real photon geodesics on the GPU, per pixel, every frame, in Schwarzschild or in the exact Kerr metric of a spinning hole. It renders the event horizon shadow, the photon ring, a Doppler-beamed accretion disc bent over and under the hole (the Interstellar look), and a gravitationally lensed starfield. You can launch photons and watch their true paths, drop a planet or star and watch it be tidally shredded, and place a second black hole to watch a gravitational-wave inspiral end in a merger, complete with a LIGO-style audio chirp.
Everything runs in the browser. No backend, no textures, no libraries beyond Three.js: the stars, disc, physics, and sound are all procedural.
A disruption in progress. The stream is debris on real orbits, the arcs around the shadow are background galaxies lensed by the same geodesics, and the whole sky is generated at boot.
The curvature grid (G): Flamm's paraboloid, the exact embedding of a Schwarzschild slice. During an inspiral the binary's gravitational waves ripple outward across it.
- Real GR light bending: each pixel integrates the Schwarzschild null-geodesic equation with RK4 and adaptive stepping. The shadow, photon ring, Einstein-ring star smearing, and the disc's secondary images emerge from the math, not from textures.
- Accretion disc: Shakura–Sunyaev temperature profile, differential-rotation noise that shears into trailing spirals, relativistic Doppler + gravitational redshift using the bent photon direction (correct even for the lensed secondary image).
-
Spin (Kerr): turn the hole up to the Thorne limit,
$a/M = 0.998$ , and the exact Kerr metric is ray traced in Cartesian Kerr-Schild form. Everything follows from the one number: the horizon shrinks to$0.53,r_s$ , frame dragging pulls the approaching edge of the shadow in to$1.06,r_s$ while the receding edge swings out to$3.50,r_s$ (the D shape), the photon sphere splits into prograde and retrograde radii, and the disc's inner edge follows the last stable orbit in from$3,r_s$ to$0.62,r_s$ . Spin 0 reproduces the Schwarzschild image exactly. - Interactive photon trajectories: click to launch fans of photons; escaped, captured, and near-critical rays are drawn as glowing curves computed by the same integrator the shader uses.
- Image order overlay: colour the disc by how many half turns the light made before it reached you, a diagnostic that separates the direct image, the first lensed image and the photon ring.
- Relativistic camera: during a scripted flight the star field aberrates toward the direction of travel and the sky ahead blueshifts and brightens while the sky behind reddens, computed from the physical speed of the trajectory the flight stands for. Exactly off when the camera is at rest.
- Tidal disruption: place a planet or star. Cinematic mode gives a directable inward spiral with spaghettification and a debris stream that feeds and brightens the disc. Realistic TDE mode launches a true zero-energy parabolic plunge, one violent shredding at pericenter, and a physically motivated bound/unbound debris split (roughly half the debris escapes, as in real TDEs).
- Infalling beacon: drop a probe from rest and watch it freeze. It reddens, dims by nine decades and stalls just outside the shadow, because the coordinate time to reach the horizon diverges and the gap decays as e^(-t/rₛ). Exact Schwarzschild radial free fall, not the pseudo-Newtonian potential the debris uses, since that one has no such divergence. The readout shows both clocks: yours running on forever, and the probe's, which crosses in a finite 28 rₛ/c and notices nothing.
- Black hole merger: place a second hole. Its orbit decays by the actual Peters (1964) gravitational-wave equations (the trajectory shape is exact; only wall-clock time is compressed). Both holes lens light. At contact the shadow swells to the merged mass minus the radiated gravitational-wave energy, with a ringdown wobble.
- Deep sky: the background is a procedurally baked HDR cubemap. Five layers of stars on a stellar-temperature colour sequence (the bright ones get diffraction spikes), a warped galactic band with dust lanes that actually extinguish the stars behind them, emission nebulae in hydrogen red and doubly-ionised-oxygen teal, globular clusters that crowd extra stars into their cores, and distant galaxies with dust lanes and spiral arms. All of it is lensed by the same geodesics, so it smears into Einstein arcs near the shadow.
- Feeding outflow: when a disruption dumps matter on the disc it goes super-Eddington and drives a broad, ragged wind out of the poles. Wide and un-collimated, unlike the jet, it appears and fades with the feeding itself.
- Relativistic jet: optional twin polar beams with braided filaments, integrated inside the raymarch so the jet bends with the light near the hole. The brightness difference between the two cones is the real Doppler boost, δ³.
-
Curvature grid (
G): Flamm's paraboloid, the exact Schwarzschild embedding diagram, drawn as a wireframe funnel. During an inspiral the binary's quadrupole gravitational wave ripples outward across it: two crests per orbit, wound into a trailing spiral, growing as the pair tightens. - Presets: one-click scenes. A star being devoured, slow spaghettification, a binary merger with waves, bare curved spacetime, a jetted quasar, a probe frozen at the horizon, and a clean wallpaper frame.
- Camera flights: plunge into the horizon, fly past, or circle the hole.
-
Cinematic mode (
H): fades the panel and readout for a wallpaper-clean frame. - Procedural audio: a Perseus-cluster-style deep drone, matter-rush noise when the disc is feeding, a gravitational-wave chirp that tracks the real orbital frequency, and a merger thump + ringdown tone whose pitch falls with the merged mass.
- Performance: quality presets, half-resolution raymarch upscaling, temporal accumulation when idle (free anti-aliasing), auto-degrade on slow machines.
You need two things: Node.js (version 20 or newer) and this repository. That's it. No accounts, no API keys, no GPU drivers to install. Follow the section for your operating system.
- Open a terminal: press the Windows key, type
powershell, press Enter. - Install Node.js (pick one):
- Easiest: download the LTS installer from https://nodejs.org, run it, click Next through the defaults (keep "Add to PATH" checked).
- Or with winget, in PowerShell:
winget install OpenJS.NodeJS.LTS
- Close and reopen PowerShell (so it picks up the new PATH), then verify:
You should see something like
node --versionv22.x.x. Any number 20 or higher is fine. - Get the code (pick one):
- With git:
git clone https://github.com/algometrix/blackhole-sim.gitthencd blackhole-sim - No git: on this GitHub page click the green Code button → Download ZIP, right-click the ZIP → Extract All. Open the extracted folder in Explorer, click the address bar, type
powershell, press Enter.
- With git:
- Install and run:
npm install npm run dev
- Open the address it prints (normally
http://localhost:5173/) in Chrome or Edge. You should see a black hole with a glowing disc. To stop: pressCtrl+Cin the terminal.
- Open a terminal: press
Cmd+Space, typeterminal, press Enter. - Install Node.js (pick one):
- Easiest: download the LTS installer from https://nodejs.org and run it.
- Or with Homebrew:
brew install node
- Verify (reopen the terminal if you just installed):
Any version 20 or higher is fine.
node --version
- Get the code (pick one):
- With git (already on every Mac):
git clone https://github.com/algometrix/blackhole-sim.git && cd blackhole-sim - No git: green Code button on this page → Download ZIP → double-click to unzip → in Terminal type
cd(with a trailing space), drag the unzipped folder onto the Terminal window, press Enter.
- With git (already on every Mac):
- Install and run:
npm install npm run dev
- Open the printed address (normally
http://localhost:5173/) in Safari or Chrome. To stop:Ctrl+C.
- Open your terminal.
- Install Node.js 20+:
- Debian / Ubuntu / Mint:
sudo apt update && sudo apt install -y nodejs npm, then checknode --version; if it prints something below 20, install via nvm instead (next line). - Any distro, always current (nvm):
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash, reopen the terminal, thennvm install --lts - Fedora:
sudo dnf install -y nodejs npm· Arch:sudo pacman -S nodejs npm
- Debian / Ubuntu / Mint:
- Verify:
node --version→ 20 or higher. - Get the code:
(No git?
git clone https://github.com/algometrix/blackhole-sim.git cd blackhole-simsudo apt install git, or download the ZIP from the green Code button andunzipit.) - Install and run:
npm install npm run dev
- Open the printed address (normally
http://localhost:5173/) in Chrome or Firefox. To stop:Ctrl+C.
| Symptom | Fix |
|---|---|
node: command not found |
Node.js isn't installed (or the terminal was open before installing; close and reopen it). Redo step 1. |
npm install fails with engine/version errors |
Your Node is too old. Install the LTS from nodejs.org and reopen the terminal. |
| Blank page saying WebGL2 is required | Your browser or GPU driver blocks WebGL2. Try Chrome, and check chrome://gpu. |
| Port 5173 already in use | Run npm run dev -- --port 5200 and open that port instead. |
| It runs but is choppy | Set Render → Quality to low in the control panel; the app also auto-lowers quality after a few slow seconds. |
npm test # run the 100-test physics/simulation suite
npm run typecheck # strict TypeScript check
npm run build # production build into dist/
npm run preview # serve the production build| Action | How |
|---|---|
| Orbit / zoom | drag / scroll |
| Try a canned scene | Presets folder (top): star devoured, spaghettification, merger, curvature, quasar, wallpaper |
| Hide the interface | H (cinematic mode) |
| Show curved spacetime | G (Flamm funnel + gravitational waves) |
| Place a planet or star | Place folder → button → click on the disc plane (between the guide rings) |
| Choose disruption physics | Place → Disruption mode: cinematic spiral vs realistic one-pass TDE |
| Place a second black hole | Place → Place black hole → click; watch the inspiral chirp in the HUD |
| Speed up / slow the inspiral | Simulation → GW time × |
| Spin the hole | Black hole → Spin (a/M); the readout reports the horizon, ISCO and both photon-ring radii as you drag |
| Launch photons | Light paths → Enabled, then click anywhere; rays per launch and spread are sliders |
| See which lensed image is which | Light rays → Tint image orders (diagnostic; off by default) |
| Camera flights | Camera folder: fly in (plunge), fly past, circle; Esc stops |
| Relativistic optics on a flight | Camera → Relativistic view, with a flight-speed slider next to it |
| Sound | Sound → Enabled (browsers require one click on the page first) |
| Change the sky | Deep sky folder: star density, nebulae, galaxies, or reseed the whole thing |
| Art-direction knobs | append ?debug=1 to the URL for the hidden tuning folder |
| On a phone | the panel is a bottom sheet: tap its bar to open it, and the eye button hides the whole interface. Quality starts at low and the render resolution is capped, because a phone GPU is roughly a tenth of a desktop card at three times the pixel density. |
Every control carries a hover description, and each folder has a reset button.
New to any of this? docs/THEORY.md derives every equation
below from scratch, starting at F = ma and ending at Flamm's paraboloid, with
diagrams and no assumed background. The section here is the short version.
Geometric units throughout:
Each pixel's ray integrates the 3D vector form of the Schwarzschild photon equation of motion,
equivalent to the Binet form
fall in (this sets the apparent shadow radius); the photon sphere sits at
Temperature follows the Shakura–Sunyaev profile
with the inner edge at the ISCO,
applied to brightness as
Planets, stars, and debris move in the Paczyński–Wiita potential
the standard pseudo-Newtonian stand-in that reproduces the correct ISCO at
and the sign of
A body is disrupted where the hole's tide beats its self-gravity, at the tidal radius
Four decisions do all the work, and each one was arrived at by watching the stream fail without it:
-
Realistic launches carry no drag. They are zero-energy (parabolic) orbits with pericenter at a chosen fraction of
$r_T$ . Bleeding velocity on the way in is not a small correction: it destroys the angular momentum that sets the pericenter, and the "plunge" collapses into the hole. -
Mass loss starts before disruption does. Once the body crosses
$r_T$ it spills at a small rate that ramps with depth, so a still-intact star is already connected to the hole by a thin leader before it comes apart. -
Debris is launched onto the body's orbit, not with the body's velocity vector. A particle offset a body-length inward along the strand shares the orbit's
$\varepsilon$ and$L$ , so its tangential speed is$L/r_p$ and the rest is radial. Copying the velocity vector instead hands that particle the same speed with less angular momentum, and the inner half of every stream dives into the hole. -
The tidal energy spread is applied radially. A radial kick changes
$\varepsilon$ while leaving$L$ untouched, so the debris keeps the star's pericenter and swings back out. While the star is falling inward ($\mathbf v\cdot\hat r<0$ ) an outward kick cancels part of that infall and lowers the energy, so the near tip is bound hardest and returns first while the far tip escapes. The split lands near the 50/50 that real disruptions show, and it is read back out of$\varepsilon$ rather than assumed.
Debris then joins the disc when it has circularized, inside the inner edge its speed must fall below src/sim/__tests__/stream.test.ts pin the resulting shape: the stream survives its first passage, spreads over a wide range of radii, wraps more than 90° around the hole, and is still feeding hundreds of seconds later.
The shredded body itself is drawn as a teardrop rather than an ellipsoid, a bright head with a thin tail streaming toward the hole. That is not a quadric, so the raymarcher solves the bounding ellipsoid analytically and then marches the teardrop field inside it.
Two different things come out of the poles, and they are not the same physics.
The jet is optically thin, narrow, and relativistic. It is integrated inside the geodesic march, so it bends with the light near the hole. The brightness difference between the approaching and receding cone is the real Doppler boost for an optically thin source,
which is why one side of a real AGN jet is bright and the other is nearly invisible.
The outflow is thermal: wide, ragged, un-collimated, and slow. When a disruption dumps matter on the disc the accretion rate goes super-Eddington and radiation pressure drives a wind, so its strength here is tied to the disc's feed rate, it swells during fallback and dies with it.
The secondary's separation decays by the circular-orbit Peters equation,
with Keplerian phase advance
normalized to GW150914's ~4.6% at equal mass. The audio chirp frequency tracks
The funnel is Flamm's paraboloid, the exact embedding of a Schwarzschild equatorial slice,
drawn with a vertical exaggeration and shifted so its rim sits at the disc
plane. The ripple on it is the quadrupole strain pattern of the binary: two
crests per orbit, wound into a trailing spiral by the retarded phase, with
amplitude growing as
The binary inspiral and the tidal disruption both run on compressed clocks,
because a circular orbit at stepWorld walks it in
substeps of at most 0.1 time units, a unit test asserts the body lands in the
same place at ×1 over 1200 ticks and at ×20 over 60. The accretion disc keeps
the uncompressed clock; it is a background, and shearing it twenty times faster
would turn it into a pinwheel.
The background is baked once into an HDR cubemap and then lensed by the same geodesics as everything else. It is invented, but it follows the structure that makes a real deep field look the way it does:
- a luminosity function skewed hard to faint stars (
$b \propto u^{9}$ on a uniform draw), with the hot end of the main sequence also the bright end, so blue stars are rare and dominant; - dust that both glows and blocks, the galactic band's dark lanes multiply the light of everything behind them, and the far, faint star layers are extinguished more than the near, bright ones;
- emission regions in hydrogen red and doubly-ionised-oxygen teal, masked to sparse patches and structured by the same domain-warped field as the gas they sit in;
- clustering: stars crowd into the galactic plane and into globular-cluster cores rather than being scattered uniformly;
- galaxies with bulges, spiral arms, and a dust lane across the inclined ones.
Every noise lookup is rotated by a fixed non-axis-aligned frame, because value noise lives on a cubic lattice and a cubemap face is aligned with it, sampled straight, the lattice shows up as angular patches in the sky.
- The full spacetime of two holes requires numerical relativity; this app superposes two Schwarzschild deflections, which is qualitatively right (double shadows, eyebrow images) but not exact in the final strong-field moments. The ringdown "breathing" of the shadow is art-directed shorthand for quasi-normal ringing.
- The cinematic disruption mode is deliberately directable (drag-driven inspiral, fixed tidal radii): physics-inspired theater, not a simulation. Realistic mode is the honest one.
- Debris particles are occluded by the horizon and approximately deflected, but not fully ray-traced.
- Spin is exact for the light, but not for the matter: debris and bodies stay Paczyński–Wiita (a Schwarzschild pseudo-potential), and only their boundaries (the disc's inner edge, the kill radius) move with the spin. Kerr is a one-hole solution, so placing a second hole forces the spin to 0, which also means a merger leaves a non-spinning remnant where a real equal-mass merger leaves
$a/M \approx 0.69$ . - No light travel-time delay: you see the whole disc at one instant rather than each part as it was when its light left.
- The camera's relativistic colour shift is a per-channel RGB gain, not a spectral shift, and the overlay pass (debris, drawn rays, curvature grid) is not aberrated, so occlusion near the shadow edge is slightly wrong during a fast flight.
- The disrupted body is drawn as a teardrop, the tidal silhouette, not a hydrodynamic result, and the feeding outflow is likewise a shape and a colour, driven by the disc's feed rate rather than by radiation transport.
- The jet is art direction, not magnetohydrodynamics: Blandford–Znajek needs a magnetic field as well as spin, so the jet does not strengthen when you spin the hole up or fade when you spin it down. Its geometry, filaments and plasma speed are chosen; the Doppler beaming between the two cones and the lensing of the beams are computed.
- The curvature funnel is an embedding diagram of one spatial slice, the standard picture, not a picture of "gravity pulling down". The wave ripple shows the strain pattern, exaggerated far beyond any real
$h \sim 10^{-21}$ . - The deep sky is invented. It follows real structure (a luminosity function skewed to faint stars, hotter stars bluer and brighter, dust that reddens and extinguishes, clusters, galaxies) but it is not a star catalogue.
src/physics/ constants, Kerr closed forms, CPU null-geodesic integrator (Schwarzschild
superposition and exact Kerr), observer aberration, image order:
all shared, line for line, with the shader
src/sim/ pure simulation core: PW gravity, tidal phase machine, debris pool,
Peters binary inspiral, quadrupole wave state, zero WebGL/DOM,
fully unit-tested (vitest)
src/render/ GPU side: geodesic raymarch pass (disc, jet, shredded body), deep-sky
cubemap baker, spacetime wireframe, bloom, composite, horizon-mask
occlusion, photon-path tubes, camera tours
src/audio/ procedural WebAudio engine (drone, disc rush, GW chirp, merger)
src/ui/ lil-gui panel, scene presets, cinematic mode, click-to-place
src/main.ts fixed-timestep sim loop + per-frame GPU/audio/HUD sync
The renderer's key trick: everything that must be truly lensed (sky, disc, the stretched body, both holes) lives inside one full-screen raymarch shader; everything that is many-small-glowing-things (debris, photon paths, gizmos) renders as ordinary additive geometry occluded by the shader's horizon mask. The shader and CPU share one source of truth for the physics constants.
- J.-P. Luminet, Image of a spherical black hole with thin accretion disk, A&A 75, 228 (1979), the first computed image of what this app draws.
- O. James, E. von Tunzelmann, P. Franklin, K. S. Thorne, Gravitational lensing by spinning black holes in astrophysics, and in the movie Interstellar, Class. Quantum Grav. 32, 065001 (2015).
- R. P. Kerr, Gravitational field of a spinning mass as an example of algebraically special metrics, Phys. Rev. Lett. 11, 237 (1963), the spinning solution and its Kerr-Schild form.
- J. M. Bardeen, W. H. Press, S. A. Teukolsky, Rotating black holes: locally nonrotating frames, energy extraction, and scalar synchrotron radiation, ApJ 178, 347 (1972), the Kerr ISCO and photon orbits.
- K. S. Thorne, Disk-accretion onto a black hole. II, ApJ 191, 507 (1974), the a/M = 0.998 accretion limit.
- P. C. Peters, Gravitational Radiation and the Motion of Two Point Masses, Phys. Rev. 136, B1224 (1964), the inspiral equations.
- N. I. Shakura, R. A. Sunyaev, Black holes in binary systems. Observational appearance, A&A 24, 337 (1973), the disc temperature profile.
- B. Paczyński, P. J. Wiita, Thick accretion disks and supercritical luminosities, A&A 88, 23 (1980), the pseudo-Newtonian potential.
- M. J. Rees, Tidal disruption of stars by black holes of 10⁶–10⁸ solar masses in nearby galaxies, Nature 333, 523 (1988), the bound/unbound debris split.
- LIGO Scientific Collaboration & Virgo Collaboration, Observation of Gravitational Waves from a Binary Black Hole Merger (GW150914), Phys. Rev. Lett. 116, 061102 (2016), the chirp, the radiated-mass numbers.
- NASA Chandra sonifications, Perseus cluster black hole (2022), the inspiration for the drone.

