Around the Black Hole
No meshes, no textures, no special cases: every pixel follows one ray of light while gravity bends it. The black shadow, the thin ring of fire and the disk arching over the top all come out of a single line of physics — explained from first principles, with the real shader running right here in your browser.
The scene
The previous scene flew us into a spiral galaxy; this one arrives at the black hole at its core. It lasts 32 beats — 15.36 seconds at 125 BPM, cross-fading in from the galaxy over three beats and out into the credits over another three. The camera drifts slowly around the hole, a few degrees above the plane of its glowing disk, and closes in by 30 % over the scene.
What you see: a black disk — the shadow — edged by a razor-thin bright ring. Around it, a disk of hot gas, brighter and whiter on one side than on the other. And, strangest of all, the far half of that disk seems to rise up and arch over the top of the shadow, and to curl underneath it. Near the shadow the background stars are smeared into arcs. Small hot spots near the inner edge of the disk flare on the snare drum.
The core trick, in plain words: for every pixel the shader shoots a ray of light out of the camera and moves it forward in small steps. At each step it nudges the ray's direction toward the hole by an amount given by one formula. Where the bent path crosses the plane of the disk, the disk's light at that point is added. If the path falls into the hole, no background sky is added, but any disk or jet light already gathered remains. If it escapes, the sky along the ray's final direction is added through the remaining transparency. None of the effects above is drawn on purpose. The shadow, the ring, the arc and the distorted stars all emerge from bending the rays.
Live: the real shader
This is the actual black hole scene: the demo's fragment shader, ported to WebGL2 with only the #version line,
a precision qualifier and the #included helpers pasted in. The camera path and every uniform come from a JavaScript
mirror of the C++ render(), with the same defaults. Drag the time slider to scrub: the frame is a pure function
of time. The demo's full post-processing chain (bloom, chromatic aberration, ACES tone mapping, vignette, grain) is replaced
by a simple ACES tone mapping and glow.
sky.glsl are sized in pixels, so here they are sized for the demo's 1080-line frame instead of this smaller canvas (otherwise each star would cover several times more of the picture). If the frame rate is low, lower the render scale (the same knob as in the demo).Background
Nine ideas make up the whole scene. The first four are about light and gravity; the next four are about the glowing disk; the last one is about the sky behind it.
1 · Rays from the camera
Ray tracing runs backwards. Real light travels from the disk to your eye, and almost all of it misses the eye. The only
light that matters is the light that ends up in a pixel — so the shader starts at the eye and follows each pixel's ray out into
the scene. Light paths are reversible, even bent ones: the backward path is exactly the path the light took, walked the other way.
(One consequence you'll meet in §8: when the shader needs "the direction toward the camera" at a point on the disk, it uses
−rayDir.)
A pinhole camera. The camera is a position plus three perpendicular unit vectors: forward,
right and up. A pixel at (u, v), measured in screen heights from the center of the screen,
looks along
With focal = 1.6 the screen's height spans 2·atan(0.5 / 1.6) ≈ 35° of sky: a moderate telephoto.
Keeping the hole centered. The C++ builds the camera with Camera::lookAt(position, origin):
forward always points at the hole (which sits at the origin), right = forward × worldUp and
up = right × forward. However the camera moves, the hole stays in the middle of the frame. The motion itself is three
pure functions of the scene time t:
inclination = 9° + 2° · sin(0.2 t) // height above the disk plane, gently bobbing azimuth = −0.4 + 0.035 t // radians: a slow orbit, about 31° over the scene distance = 26 · (1 − 0.3 · smoothstep(0, 1, t / 15.36)) // closes in from 26 to 18.2 position = distance · (cos incl · sin az, sin incl, cos incl · cos az)
2 · Bending light, one step at a time
In empty space light goes straight. Near a mass its path curves. The shader does not solve for the curve in one go; it walks along it, like a hiker who looks at the compass after every few steps. Each step does three things:
- compute the pull at the current point: a vector pointing at the hole, of strength
1.5 h² / r⁴(r= distance to the hole;his explained below); - add
pull × dtto the ray's direction; - move the position by
direction × dt.
float dt = clamp((r - kHorizon) * 0.12, 0.012, 1.5) * uStepScale; vec3 previous = pos; dir += -1.5 * h2 * pos / pow(r, 5.0) * uLensing * dt; // pos / r⁵ = (unit vector) / r⁴ pos += dir * dt;
This is Euler's method — the simplest way to follow a differential equation, named after Leonhard Euler — in its semi-implicit form: the direction is updated first, and the new direction moves the position. That small ordering detail makes orbits behave much better than the naive version, which slowly gains energy and spirals outward.
What is h? It is the ray's angular momentum, h = |pos × dir|. For a ray that starts with a unit
direction, h equals the impact parameter b: the distance by which the ray would miss the hole if
it flew straight on. A pull that always points at the center exerts no twist (torque), so angular momentum never changes along
the path. The shader computes h² once per pixel (h2) and reuses it at every step.
Adaptive steps. Far from the hole the path is nearly straight, so steps can be long; near the horizon the path bends
sharply and steps must be short. dt = clamp(0.12 · (r − 1), 0.012, 1.5) does that: 1.5 units far away, shrinking
in proportion to the distance from the horizon, never below 0.012. A ray from the camera reaches the hole's neighborhood in a few
dozen steps. The quality knob multiplies the step budget (220 × quality) and divides the step size.
Three ways to stop. r < 1: the ray fell through the horizon — omit the background sky, retaining any disk or jet light already gathered.
r > cameraDistance + 20 while moving outward: it escaped — look up the sky along its final direction. Out of steps:
treated like an escape.
b. They are integrated with the
shader's exact loop. Cyan rays escape, red rays are captured. The dots are the integration steps: watch them
bunch up near the hole. The dashed circle is the photon sphere (§4) and the gold dashed lines mark the critical impact parameter
b = ±2.6.3 · Where −1.5 h² x / r⁵ comes from
Newton's gravity pulls with a strength that falls as 1/r². Light, though, is described by Einstein's general
relativity (1915), and around a non-spinning mass by the solution Karl Schwarzschild found in 1916. Write the path of a ray in
polar form — its angle φ around the hole and u = 1/r. The light path then obeys the
photon orbit equation:
where rs is the horizon radius (the Schwarzschild radius, 2GM/c² — about 3 km for
the mass of the Sun; 1 on this page). Without the right-hand side the solutions are straight lines: all the bending lives in that
one term.
Now the trick. Pretend the light is an ordinary particle pushed by some central force F(r). Newtonian mechanics
gives the shape of its path by Binet's equation: d²u/dφ² + u = −F / (h² u²). Choose the force so both
equations match: F = −(3/2) h² u⁴ = −1.5 h² / r⁴. As a vector pointing at the hole, that's
— the line in the shader. The shader comment calls it "the usual approximation of null geodesics" (a null geodesic is
the path of light in curved spacetime). It's worth knowing exactly what is approximate: the shape of the path, measured in
Schwarzschild's r and φ, is exact. What is approximate is (a) the finite step size, (b) treating
Schwarzschild's coordinates as ordinary flat 3D space when placing the camera and the disk, and (c) the speed along the path:
the "ray" speeds up near the hole like a falling stone — harmless, because only the path's shape is used. And the hole does not
spin: a spinning (Kerr) hole, like Interstellar's Gargantua, has a lopsided shadow.
How much does a ray bend? Far from the hole the bend is small. Einstein's weak-field result is
α = 2 rs / b radians — for the Sun, 1.75 arc-seconds at its edge, the deflection measured during the 1919
eclipse expeditions organized by Frank Dyson and Arthur Eddington. Treating light as a Newtonian particle gives exactly half of that (Johann von Soldner
published that value in 1804). Close to the hole, the true bend grows without limit:
2/b. Pink dashed: the Newtonian
value 1/b. As b grows, white and cyan converge (at b = 16 they still differ by about 10 %: the next correction
falls off as 1/b²); near b = 2.6 the bend shoots past 180°
(the ray comes back toward you) and beyond: the ray loops around the hole. Left of the gray line every ray is captured.4 · Horizon, photon sphere and shadow
Three radii organize everything you see:
- r = 1, the event horizon. Anything that crosses it — light included — never comes back. The shader's test is simply
r < kHorizon. - r = 1.5, the photon sphere. At exactly 1.5 horizon radii light can travel in a circle around the hole. The balance is unstable, like a pencil standing on its tip: the slightest nudge sends the light inward or outward.
- b = 3√3/2 ≈ 2.598, the critical impact parameter. A ray aimed with
bsmaller than this falls in; a largerbescapes. A ray aimed just above it skims the photon sphere and winds around it — the closer to 2.598, the more turns.
So the black disk on screen is not the horizon. It is the set of all directions whose rays are captured, and it appears with radius 2.6 — more than two and a half times the size of the horizon itself. That is why astronomers call it the shadow; the Event Horizon Telescope's 2019 image of the black hole in the galaxy M87 shows exactly such a shadow.
Rays aimed just outside the shadow edge circle the hole once, twice, more, before leaving — and on each loop they can cross the disk again. Each loop shows a thinner, fainter copy of the whole surroundings, squeezed against the shadow's edge: that is the photon ring, the thin bright line hugging the shadow in the capture at the top of the page.
The demo's own steps are coarse enough to matter a little here. With the default quality, the edge of the shadow comes out at
b ≈ 2.54 instead of 2.598: a few rays that should be captured escape, and the shadow is about 2 % smaller than the
exact one (at quality 4 the edge is at 2.58). Nobody can see the difference — which is what the quality knob trades.
5 · A thin disk, and the arc over the shadow
The accretion disk is gas orbiting the hole and slowly spiraling in. In the shader it is a flat sheet with no thickness:
the plane y = 0, between radius inner = 3 and outer = 13. Radius 3 is not arbitrary: it is the
innermost stable circular orbit (ISCO) of a Schwarzschild hole. Closer in no stable orbit exists, and gas plunges
straight in.
The shader never intersects the disk geometrically. After every step it simply checks whether the step crossed the
plane: previous.y and pos.y have opposite signs. It finds the crossing point by linear interpolation
along the step, and if that point lies between the two radii, it samples the disk there:
if (previous.y * pos.y < 0.0) { vec3 hit = mix(previous, pos, previous.y / (previous.y - pos.y)); // where y = 0 float rh = length(hit.xz); if (rh > uInner && rh < uOuter) { … diskSample(hit, dir) … } }
Because the ray keeps going after a hit, it can cross the plane again, and again. That is the whole secret of the famous look. A ray aimed just above the hole would, in flat space, sail over it and over the disk behind it into empty sky. Gravity bends it down; it crosses the plane behind the hole and sees the top of the far half of the disk. All those rays together paint the far half as an arc rising over the shadow. A ray aimed just below the hole slips through the gap between horizon and inner edge, is bent upward, and hits the far half from underneath: an arc curling under the shadow. Jean-Pierre Luminet computed the first image of this kind in 1979; the film Interstellar (2014) made it famous.
Layers, front to back. Each disk sample has a color and an opacity a. The shader composites them in the order
the ray meets them, nearest first: col += (1 − α) · color · a and α += (1 − α) · a, where
α is the opacity accumulated so far. Each new layer is seen through what lies in front of it — the "over"
operator of Porter and Duff (1984), run front to back. Even where the gas is densest its opacity is capped at 0.9, so the near half always lets at least 10 % through: in
the capture you can see the lower arc faintly through the front of the disk. Once α > 0.99 nothing behind could show,
and the loop stops early.
jets = 0; try them in the live demo) are not a
surface but a glowing volume: at every step the shader adds a little light, glow(pos) × dt, so a ray collects more
the longer it travels through the jet — a crude version of the volume integral used for clouds and nebulae.6 · How hot is the disk?
As gas spirals inward it rubs against itself (viscosity) and heats up. The standard thin-disk model of Nikolai Shakura
and Rashid Sunyaev (1973) gives the temperature at radius r:
The first factor makes the disk hotter inward. The second goes to zero at the inner edge, where the gas stops being held by
its neighbors and falls freely, so the hottest ring sits a little outside the edge — at r = (49/36) · rin ≈ 1.36
rin, where the expression (written with x = rin/r) equals 0.488. That is why the shader
divides by 0.488: its temperature peaks at exactly 1.
float x = uInner / r; float temperature = pow(x, 0.75) * pow(max(1.0 - sqrt(x), 0.0), 0.25) / 0.488;
Color. A hot body glows with a color set by its temperature (Max Planck's law of 1900): dull red, then orange, white and
finally blue-white. blackbody(t) is a hand-tuned four-color ramp that imitates that sequence; its input is
T · shift · temp (the shift comes in §8). Brightness grows as T² — gentler than a real blackbody,
whose total emission grows as T⁴ (the Stefan–Boltzmann law): an artistic choice that keeps the outer disk visible.
Both edges fade out smoothly: smoothstep(inner, 1.12·inner, r) · smoothstep(outer, 0.65·outer, r).
T(r). Pink: the brightness factor T². Violet
dashed: the edge fade. The strip shows the resulting color along a radius, with average turbulence and no Doppler
shift, tone-mapped like the live demo. Move the inner radius: the whole profile scales with it.7 · Spirals from Kepler
Johannes Kepler's third law (1619) says that the square of an orbit's period grows as the cube of its radius. Turned into an
angular speed: ω ∝ r−3/2. Gas near the inner edge laps the outer gas many times over; this
differential rotation shears any pattern on the disk into trailing spirals, like cream stirred into coffee.
The shader gets this for free by computing the disk's turbulence in a frame that rotates with the gas at each radius:
float omega = pow(r, -1.5) * uDiskSpeed; // Kepler: ω ∝ r^-1.5 (uDiskSpeed = 6) float phi = atan(p.z, p.x) - omega * uTime; // angle measured in the gas's own rotating frame float spiral = phi + log(r) * 2.5; // a logarithmic spiral vec3 q = vec3(cos(spiral) * 2.0, sin(spiral) * 2.0, r * 2.5); float density = smoothstep(0.25, 0.8, fbm(q) * 0.7 + fbm(q * 2.7 + 5.0) * 0.3);
Subtracting ω t glues the noise pattern to the gas: as time passes each radius carries its pattern around at its own
speed. Adding 2.5 · ln r to the angle bends lines of constant angle into logarithmic spirals (spirals that cross
every circle at the same angle). Using cos and sin of the angle, rather than the angle itself, avoids a seam
where the angle jumps from +180° to −180°. The noise is fbm (fractal Brownian motion): several layers of smooth value noise,
each twice as fine and half as strong as the last — the same 3D fbm as in the warp scene.
Hot spots. Three bright blobs, exp(−d² / 0.35), orbit at 1.2, 1.5 and 1.8 times the inner radius, each at its
own Keplerian speed. Their strength, 0.6 + 1.6 · pulse, jumps when the snare drum hits and decays as
exp(−5 · seconds since the hit).
diskSample() function on the
GPU. The faint ring in the middle is the horizon. Let time run and watch the inner disk overtake the outer disk. Set the disk speed
to 0 to freeze the pattern; push the snare pulse to see the hot spots flare. Seen from above, the gas moves sideways to the line of
sight, so the only color shifts left are the gravitational redshift and a small relativistic one (§8).8 · Doppler beaming and gravitational redshift
Near the inner edge the gas moves at a good fraction of the speed of light. The shader sets its speed (as a fraction
β of light speed) to
This is the speed of a circular orbit around a Schwarzschild hole as measured by an observer standing still at that radius:
exactly half the speed of light at the ISCO (r = 3), and the speed of light itself at the photon sphere — which is why
light can orbit there.
The Doppler effect (Christian Doppler, 1842) makes light from an approaching source bluer and from a receding one redder. At these speeds it needs the relativistic formula:
where β · n is the part of the velocity pointing toward the observer. The γ factor is time dilation —
moving clocks run slow — so even gas moving sideways is slightly reddened. Gravitational redshift adds another factor: light
climbing out of the gravity well loses energy, g = √(1 − 1/r) (0.82 at the ISCO).
The shader blends the physical shift with "no shift" using the doppler knob (default 0.6), then uses it twice: the
color temperature is multiplied by shift (bluer when approaching), and the brightness by shift³. The cube
is relativistic beaming: the quantity Iν/ν³ (intensity per frequency over frequency cubed) is
unchanged along a ray, so a source whose light arrives with every frequency multiplied by shift looks
shift³ times brighter. That is why one side of the disk blazes and the other fades. The film Interstellar
deliberately left this asymmetry out to keep the image readable; doppler = 0 reproduces that choice.
float beta = min(sqrt(0.5 / max(r - kHorizon, 0.05)), 0.95); vec3 velocity = normalize(vec3(-p.z, 0.0, p.x)) * beta; // along the orbit vec3 toObserver = -normalize(rayDir); // backwards along the traced ray (§1) float doppler = 1.0 / (gamma * (1.0 - dot(velocity, toObserver))); float gravity = sqrt(max(1.0 - kHorizon / r, 0.0)); float shift = mix(1.0, doppler * gravity, uDoppler);
δ, the redshift g, the blended
shift and the brightness factor shift³, on a logarithmic scale. Move the ring inward and set
doppler to 1: the brightness ratio between the two sides climbs into the tens. Raise the camera to 90° and the asymmetry
disappears — the gas no longer moves toward or away from you.9 · The lensed sky
A ray that escapes reads the background along its final direction: background(normalize(dir)). Since that
direction has been bent, the sky near the shadow is distorted like the view through a thick glass lens — gravitational
lensing. Stars slide along arcs around the shadow; a star exactly behind the hole would become a complete circle (an
Einstein ring, after Einstein's 1936 paper). Every point of the sky shows up at least twice — once on each side of the
hole — and then again, ever thinner, in the photon ring.
The sky itself is procedural:
- Stars (
starSky, shared with other scenes): space around the unit sphere is cut into 3D cells; a hash decides whether a cell holds a star (at most one in ten does) and where. The 2×2×2 nearest cells are searched so stars are never cut at cell borders. Two layers: sparse bright stars and a dense faint field. Each star is a soft dot sized from the angle covered by one pixel,1 / (resolution.y · focal), so stars stay about a pixel wide at any resolution. - A nebula band:
exp(−(3 · d·n)²)is brightest where the directiondis perpendicular ton= normalize(0.5, 0.8, 0.3) — a soft stripe around the whole sky, like the Milky Way — filled with fbm clouds tinted between violet and brown.
The algorithm, step by step
The complete scene, in order, with each step linked to the idea above that explains it. It is a single fullscreen pass: one fragment shader invocation per pixel.
// CPU, once per frame (BlackHoleScene::render)
incl = (9° + 2°·sin(0.2 t)) in radians
az = −0.4 + 0.035 t
dist = 26 · (1 − 0.3·smoothstep(0, 1, t/15.36))
pos = dist · (cos incl·sin az, sin incl,
cos incl·cos az)
camera = lookAt(pos, origin, focal 1.6)
maxSteps = 220 · quality; stepScale = 1 / quality
pulse = exp(−5 · seconds since snare) |
Everything that changes over time is reduced to a handful of uniforms, all computed directly from t. The camera
always looks at the origin, so the hole stays centered (§1). With scale < 1 the pass renders into a smaller target
and is stretched with a linear blit. |
// per pixel: set up the ray pos = camera position dir = normalize(fwd·1.6 + right·u + up·v) h2 = |pos × dir|² // = b², conserved escape = |camera| + 20 col = 0; α = 0; captured = false |
Ray tracing runs backwards from the eye (§1). The squared angular momentum is computed once: a pull toward the center can't change it (§2). |
repeat up to maxSteps: r = |pos| if r < 1: captured = true; break if r > escape and moving out: break dt = clamp(0.12·(r − 1), 0.012, 1.5) · stepScale prev = pos dir += −1.5 · h2 · pos / r⁵ · lensing · dt pos += dir · dt |
One semi-implicit Euler step of the photon orbit equation, rewritten as a central force (§2, §3). Steps shrink near the
horizon. Crossing r = 1 means the pixel belongs to the shadow (§4). |
if prev.y · pos.y < 0: // crossed y = 0 hit = lerp(prev, pos) to y = 0 if inner < |hit.xz| < outer: (c, a) = diskSample(hit, dir) col += (1 − α) · c α += (1 − α) · a if α > 0.99: break |
The disk is found by watching for sign changes of y, not by intersecting geometry; later crossings are the arc under
the shadow and the photon ring (§4, §5). Layers are composited front to back. |
// diskSample, part 1: what the gas is like
T = (r₀/r)^¾ · (1 − √(r₀/r))^¼ / 0.488
ω = r^−1.5 · diskSpeed
φ = atan(z, x) − ω·t
spiral = φ + 2.5·ln r
q = (2cos spiral, 2sin spiral, 2.5r)
density = smoothstep(0.25, 0.8,
0.7·fbm(q) + 0.3·fbm(2.7q + 5))
spots = Σ₃ exp(−|p − cᵢ(t)|² / 0.35)
· (0.6 + 1.6·pulse) |
Thin-disk temperature peaking at 1 (§6). Turbulence in the frame co-rotating at the Keplerian speed of each radius, wound into spirals (§7). Three hot spots orbit near the inner edge and flare with the snare. |
// diskSample, part 2: how it looks from here β = min(√(0.5 / (r − 1)), 0.95) δ = 1 / (γ · (1 − β·toObserver)) g = √(1 − 1/r) shift = mix(1, δ·g, doppler) edge = smoothstep(r₀, 1.12r₀, r) · smoothstep(outer, 0.65·outer, r) a = clamp((1.1·density + spots)·edge) · 0.9 I = T² · shift³ · (0.4 + 1.6·density + 3·spots) c = blackbody(T · shift · temp) · I · 5 return (c · a, a) |
Relativistic Doppler factor from the orbital speed, gravitational redshift from the depth in the well, blended by the
doppler knob. The shift colors (bluer when approaching) and beams (cube) the light (§8). The returned color is
premultiplied by its opacity. |
if jets > 0: // off in the demo col += (1 − α) · blue · jet(pos) · jets · dt · 0.8 |
Optional jets along the spin axis, accumulated as emission per unit length (§5 note). |
// after the loop if not captured and α < 0.99: col += (1 − α) · background(normalize(dir)) out = col // linear HDR |
The sky is looked up along the bent final direction: gravitational lensing (§9). The HDR result then goes through the demo's post chain: bloom, chromatic aberration, ACES tone mapping, vignette and grain (default settings, no overrides for this scene). |
The real source
These are the files as they are in the demo's repository. The browser version above uses the same shader code, with the
#includes pasted in.
shaders/scenes/blackhole/blackhole.frag fragment shader · the whole scene
src/scenes/blackhole/BlackHoleScene.cpp C++ · per frame
shaders/common/camera.glsl pinhole camera rays
src/scene/Camera.h C++ · lookAt
shaders/common/sky.glsl procedural stars
shaders/common/noise.glsl excerpt · hash, value noise, fbm
Glossary
- Fragment shader
- A program run once per covered pixel (fragment); it decides the pixel's color. This whole scene is one.
- Uniform
- A value set by the CPU before a draw and shared by every pixel of that draw (camera, time, disk radii…).
- Ray tracing
- Computing an image by following, for each pixel, the path light would take to reach the eye — traced backwards from the eye.
- Euler's method
- Following a differential equation in small steps: compute the rate of change here, take a small step with it, repeat. Semi-implicit Euler updates the velocity first and moves with the new velocity.
- Schwarzschild radius, event horizon
rs = 2GM/c²: the radius of no return around a non-spinning black hole. The unit of length on this page.- Impact parameter b
- The distance by which a ray would miss the hole if it went straight. For a unit-length direction it equals the angular momentum h.
- Angular momentum h
|position × direction|; constant along a path under a pull that always points at the center.- Photon orbit equation, Binet's equation
- The equation of a light path around a Schwarzschild hole,
u'' + u = 1.5 rs u²withu = 1/r; Binet's equation gives the path of a particle under a central force in the same form, which turns the light path into a force:−1.5 h² x / r⁵. - Photon sphere
- Radius 1.5 rs, where light can circle the hole on an unstable orbit.
- Shadow
- The dark region of the image made of all captured directions; radius
3√3/2 ≈ 2.6rs, larger than the horizon. - Photon ring
- The thin bright ring at the shadow's edge, made of rays that loop around the photon sphere before escaping.
- Accretion disk
- Gas orbiting a black hole and slowly spiraling in, heated by friction until it glows.
- ISCO
- Innermost stable circular orbit: 3 rs for a Schwarzschild hole. The disk's inner edge.
- Keplerian rotation
- Orbital angular speed falling as
r−3/2, so inner gas laps outer gas and patterns wind into spirals. - Blackbody
- An idealized hot object whose glow color depends only on its temperature: red, orange, white, blue-white.
- Doppler factor δ
- The ratio of received to emitted frequency for a moving source; above 1 when it approaches.
- Relativistic beaming
- The brightening of an approaching source (and dimming of a receding one), here by
shift³. - Gravitational redshift
- The loss of energy (reddening) of light climbing out of a gravity well: factor
√(1 − rs/r). - Gravitational lensing, Einstein ring
- The distortion of the background by bent light; a source exactly behind the lens appears as a ring.
- Front-to-back compositing
- Adding layers nearest first, each one weighted by how much of it is still visible through the layers in front:
col += (1 − α)·c. - Value noise / fbm
- Smoothly interpolated random values on a grid; fbm sums several octaves of it for natural detail.
- HDR
- High dynamic range: keeping brightness values above 1.0 until a final tone-mapping step.
- Tone mapping (ACES)
- A curve compressing HDR brightness into the screen's range while keeping highlights pleasant.
- Bloom
- Letting very bright areas glow into their neighborhood, like light scattering in a lens or eye.
Part of the Space Travel documentation. Direction & design: blacklizard · Code: Claude. The figures on this page are computed live in your browser; nothing is pre-rendered except the header image.
