Starting…
drag · scroll

Black Hole, in your browser

A real-time raymarcher that integrates the Schwarzschild geodesic ODE per pixel — straight through a WebGPU fragment shader. No textures baked, no precomputed light maps. Drag to orbit, scroll to zoom.

resolution
window × DPR
integrator
RK4, adaptive Δφ
steps / ray
up to 300 × 8

How it works

Light rays follow null geodesics around the black hole. In Schwarzschild coordinates, the equation for the inverse radius u = 1/r as a function of orbital angle φ reduces to a clean second-order ODE:

d²u/dφ² = (3/2) · r_s · u² − u

For each pixel, the fragment shader sets up an orbital plane from the camera position and ray direction, then RK4-integrates that ODE with an adaptive step size — coarser when the ray is far from the hole, denser near the photon sphere where bending is steep.

Whenever a step crosses the equatorial plane inside the disk band, the integrator switches to volumetric marching: sampling a tiled noise texture warped by Keplerian angular velocity ω ∝ r^(-3/2), with a Gaussian vertical profile and a radial falloff for the disk density. Beer's law accumulates opacity along the way.

Rays that cross the event horizon are swallowed. Rays that escape the universe radius (60 r_s) hit an HDR star map (lat/long sampled), giving the characteristic Einstein-ring distortion of the background as light bends around the hole.

WebGPU pipeline

The whole thing runs as a single fullscreen draw — a 3-vertex triangle covering the clip space, with all the heavy work in the fragment shader. Uniforms hold the camera frame in viewport form (pixel00 origin + ΔW, ΔH per-pixel basis vectors), so the shader reconstructs one ray per fragment without ever touching a matrix.

  • Shader: WGSL, ~380 lines (source on github)
  • Bindings: uniform buffer, stars texture, noise texture, two samplers
  • Camera: orbit camera in TypeScript with latitude/longitude/distance

Why I built this

I've been working with CUDA kernels professionally for years but had never written a raymarcher from first principles. This project was an excuse to: (1) pick up WebGPU and WGSL properly, (2) work through general relativity to the point I could actually re-derive a ray-tracing ODE, and (3) build something that looks great as a portfolio piece — physics-correct enough to satisfy me, fast enough to ship as a webpage.

WebGPUWGSLTypeScriptRK4RaymarchingSchwarzschild metricVolumetric rendering