Supernova
A red supergiant shudders, collapses and explodes on the beat — and the shell of gas that races outward is not a mesh or a sprite but a glowing fog, computed ray by ray. How a shader marches through a 3D noise texture to build a supernova remnant, explained from first principles, with the real shaders running right here in your browser.
The scene
The supernova lasts 48 beats — 23 seconds. For the first sixteen beats we orbit a swollen red supergiant: its surface boils with giant convection cells, an orange haze of escaping gas surrounds it, and it breathes — slowly at first, then with growing violence. In the last 1.2 seconds it collapses to nothing. On beat 16, exactly one third into the scene, a white flash wipes out the screen and the camera shakes. When the light clears, a shell of glowing gas is racing outward: blue-white where the shock front is hottest, pink and teal in the cooling filaments behind it. A thin ring flares around the star's equator, thousands of debris streaks fly past, and a few seconds later a pulsar ignites at the center and sweeps two narrow beams through the gas while the camera pulls back to take in the whole remnant.
Almost none of this is geometry. The star, the haze, the shell, the ring and the beams are all computed per pixel: for each pixel a ray leaves the camera, and the shader walks along it in small steps asking "how much glowing gas is here, and what color is it?" — and adds up the answers. That is volume rendering by ray marching, and it is the heart of this page. The gas itself is just a formula: a density that depends on position, on the shell's radius and on time, made irregular by reading a precomputed 3D noise texture. Only the debris streaks are real triangles — 24,000 vertices with no vertex data at all.
Live: the real shaders
This is the actual supernova scene: the demo's volume shader, its full-resolution composite, the debris vertex shader and the
3D noise generator, ported to WebGL2. The uniforms come from a JavaScript copy of the scene's C++ render(), so the
timing, camera path and every curve are the demo's. Drag the time slider to scrub through the whole explosion: because every
frame is a pure function of time, scrubbing is just "render the frame for this time".
#version 300 es and precision lines, the #included helpers pasted in,
and the noise generator (a compute shader in the demo) run as a fragment shader, one draw per slice of the same 128³ RG8 3D
texture, with a floor-based modulo for its tile wrap. One explainer-only switch was added (uJitter), and the sky's stars are sized for the demo's 1080-line frame rather than this smaller canvas, so they keep the same size relative to the picture. The shell's small
pulse on every beat comes from the music player in the demo; here it is derived from the time. The demo's post-processing chain
(bloom, chromatic aberration, vignette, grain) is replaced by a simple stand-in: ACES tone mapping plus a glow with a hard threshold of 1.0 on blurred samples. The demo uses a soft knee, which starts admitting light above a brightness of 0.5 at that threshold.Background
Nine ideas make up the scene. The first four set the stage — the timeline, rays, the noise volume and the star — and sections 5 to 8 build the volumetric shell, which is the real subject of this page.
1 · A star's last 23 seconds
Stars shine by fusing light elements into heavier ones in their cores. A star much more massive than the Sun ends its life as a red supergiant — Betelgeuse in Orion is a famous example — hundreds of times wider than the Sun, with a cool, bloated, turbulent surface. Deep inside, fusion eventually produces iron, and fusing iron releases no energy. With nothing left to hold it up, the core collapses in a fraction of a second into a neutron star about 20 km across; the collapse rebounds as a shock wave that blows the rest of the star into space. That is a core-collapse supernova: for weeks it can outshine a whole galaxy. What remains is an expanding remnant — a shell of hot gas and filaments — and often a pulsar, the spinning neutron star sweeping beams of radiation around like a lighthouse. The Crab Nebula is the remnant of a supernova recorded in the year 1054, and it has a pulsar at its center.
The scene squeezes this into three acts. Like every scene in the demo it is a pure function of time: each frame, the C++
render() turns the scene time t into a handful of numbers and hands them to the shaders as
uniforms. Nothing is carried over from one frame to the next, so any instant can be rendered directly.
// C++, each frame (src/scenes/supernova/SupernovaScene.cpp) flashTime = duration / 3 // 7.68 s: beat 16 of 48 age = t − flashTime // seconds since the flash, negative before instability = clamp(t / flashTime, 0, 1)³ starRadius = 22 · (1 + 0.04 · sin(1.5t) · (1 + 2·instability)) // breathing, growing wilder · (1 − smoothstep(flashTime − 1.2, flashTime, t)) // collapse; 0 after the flash R(age) = 150 · (1 − e^(−0.25·age)) + 1.5·age // shell radius; 0 before the flash flash = 12 · e^(−2.5·age) ring = smoothstep(0.5, 1.5, age) · e^(−0.15·age) · 3 pulsar = smoothstep(3, 6, age) camDistance = 75 + 185 · smoothstep(0, 12, age) // pull back as the shell grows
Why one third? The scene is 48 beats long and the music changes pattern every 16 beats, so the flash lands on beat 16 — at
125 BPM, 7.68 s in. The shell radius R grows fast at first — 39 units per second at the instant of the flash — and then
slows down toward 150 plus a slow drift of 1.5 units per second. Real blast waves also decelerate as they sweep up the gas around
them, but this curve is an artistic stand-in with the same feel, not a physical model. All the constants here are tuning values
chosen by eye.
2 · Rays and spheres
The scene is drawn by a fragment shader: a small program the GPU runs once for every pixel. Its first job is to find the
ray through that pixel — the line of sight from the camera's position through the pixel, out into the world. The demo's
camera is a pinhole camera (the warp explainer derives it): a position ro (ray origin), three directions —
forward, right, up — and a focal length.
vec3 cameraRay(vec2 fragCoord, vec2 resolution) { // common/camera.glsl vec2 uv = (fragCoord - 0.5 * resolution) / resolution.y; // pixel → [-0.89, 0.89] × [-0.5, 0.5] return normalize(uCamForward * uFocal + uCamRight * uv.x + uCamUp * uv.y); }
The camera always looks at the explosion's center (the origin); it orbits slowly and, after the flash, pulls back from 75 to 260 units. With the focal length 1.1 the view is about 49° tall.
Many questions in this scene reduce to one: where does a ray meet a sphere? A point on the ray is ro + t·rd,
where t is the distance along it. The point is on a sphere of radius r around the origin when its length is
r. Squaring and expanding gives a quadratic in t, which has the familiar school solution:
If h < 0 the ray misses. Otherwise it enters at t₀ = −b − √h and leaves at t₁ = −b + √h.
The scene uses this three ways:
- The star's surface (before the flash): the entry point is where the ray hits the star; nothing behind it is visible.
- A bounding sphere (after the flash): the gas only exists within
1.35·R + 5of the center, so the ray is only marched betweent₀andt₁of that sphere — no steps are wasted on empty space. If the camera is inside it,t₀is negative and is clamped to 0. - Glows around a point:
tc = −bis where the ray passes closest to the center, and the distance there — the impact parameter — tells how near the line of sight comes. A brightness likeexp(−impact / size)paints a soft glow around the center with no marching at all: that is how the star's orange haze and the neutron star's glow are drawn.
t₀ clamp to 0; aim away to see the ray miss. The dashed line is the impact parameter.3 · A baked noise volume
Gas in space is irregular: clumps, holes, filaments. Shaders imitate this with noise: functions that look random but are smooth and repeatable. Two classic kinds are used here.
- Value noise gives every point of an integer grid a random value (a hash) and blends smoothly between them; summing octaves of it — each twice as fine and half as strong — is fractal Brownian motion (fbm), the soft, cloudy pattern of channel R. (It is a cousin of Ken Perlin's 1985 gradient noise, which interpolates random slopes instead of values.)
- Worley noise (Steven Worley, 1996) scatters one random feature point in every grid cell and returns the distance to
the nearest one. It looks like cells or soap foam. Inverted (
1 − distance) the cell centers become bright blobs with dark creases between them; summed over three octaves that is channel G, "billowy" and cellular.
Worley noise is expensive: every evaluation visits 27 neighboring cells, per octave. The shell needs four noise lookups per step, 80 steps per pixel, for hundreds of thousands of pixels, sixty times a second. So the demo computes both channels once at startup, into a 3D texture of 128 × 128 × 128 voxels with two bytes each (4 MB). After that a noise lookup is a single texture read, and the GPU's texture hardware blends the eight surrounding voxels for free (trilinear filtering).
One more trick makes the texture tileable: every octave uses a period (4, 8, 16, 32, 64 cells) that divides the volume, and
the cell coordinates wrap around at that period. The right face of the cube therefore matches the left face, top matches bottom,
front matches back. With the texture's wrap mode set to REPEAT, any coordinate — 3.7, −12.2 — reads a valid, seamless
value: an infinite noise field from 4 MB of data.
smoothstep(0.55, 0.85, G) — only the cell centers survive. That is the shape of the shell's filaments (§5).// common/noise3d.comp — one invocation per voxel (pseudo-code) p = (voxel + 0.5) / 128 // [0, 1)³ R = Σoctave 0..4 ½octave+1 · tiledValueNoise(p · period, period) // period = 4 << octave R = R / Σ ½octave+1 // normalize to [0, 1] G = 1 − (0.625·worley(p·4, 4) + 0.25·worley(p·8, 8) + 0.125·worley(p·16, 16)) store (R, G) as two 8-bit channels
4 · The red supergiant
Before the flash, the volume shader draws the star as a plain sphere (§2) and colors the point where the ray hits it. A real red
supergiant's surface is dominated by a few enormous convection cells — hot gas welling up, cooling and sinking — so the
shader reads the cellular G channel at a large scale (0.45: the whole star spans less than one tile, a handful of
cells) and adds a quarter of the finer R channel for detail. The sample point is the surface normal, slowly turned around the
vertical axis (0.03 radians per second) and drifting upward, so the cells creep.
heat = 0.75 · cells + 0.25 · detail color = dark red → orange as heat goes 0.3 → 0.75 → pale orange as heat goes 0.75 → 0.95 color · (0.25 + 0.75 · μ0.7) · 1.6 · flicker // μ = cos(angle between the normal and the view)
Limb darkening. Real stars look darker toward the edge of their disc: at the edge, your line of sight grazes the outer layers,
which are cooler, instead of looking deep into hot gas. The shader imitates it with μ (the cosine of the angle between the
surface normal and the direction to the camera): 1 at the center of the disc, 0 at the edge. The power 0.7 and the floor 0.25 are
artistic tuning.
Instability. instability = (t / flashTime)³ stays near zero for most of the act and shoots up at the end. It
widens the star's breathing (from ±4 % to ±12 % of its radius), brightens the haze, and adds a flicker,
1 + instability · (0.6·sin(9t + 20·detail) + 0.8): the 20·detail term gives every patch of the surface its
own phase, so the star shimmers instead of blinking as a whole.
The haze. Around the star, every ray adds an orange glow exp(−(impact − R) / (0.35·R)), using the impact
parameter of §2: full strength wherever the ray passes through the star, fading over about a third of a radius outside it.
starSurface() computes it, evaluated in JavaScript from the real noise
volume. Scrub time toward 7.68 s: the breathing widens, the flicker grows, and in the last 1.2 s the star collapses. Toggle the
ingredients to see what each contributes. Right: the heat-to-color ramp and the limb-darkening curve.5 · The shock shell: density from radius and time
After the flash, the shader no longer looks for a surface. It treats space as a participating medium — a fog — and needs one
function: the gas density at any point p, at the current shell radius R. It is built in layers.
A wrinkled front. A perfect sphere would look fake. When a heavy fluid is pushed by a lighter one — or decelerated by it, as
the ejected stellar layers are — the boundary grows fingers: the Rayleigh–Taylor instability, studied by Lord Rayleigh in the
1880s and G. I. Taylor in 1950, and clearly visible in real remnants. The shader imitates it by moving the front in and out by up to
±10 % depending on the direction: front = R · (1 + 0.2 · (noise(direction · 0.9) − 0.5)).
A signed coordinate. Next, the distance from the front is measured in units of the shell's thickness:
x = 0 is the front, negative values are inside, positive outside. Expressing everything in x means the
profile automatically scales with the shell. The profile is a soft-edged band:
smoothstep(0.15, 0, x) · smoothstep(−1.4, −0.2, x) — it ends sharply just outside the front (the shock) and fades out
gradually over 1.4 thicknesses inside (gas that has already passed through).
Filaments. Real remnants are not smooth shells but webs of knots and threads. The density is mostly made of the Worley
channel thresholded at smoothstep(0.55, 0.85, G) — the blobs of §3 — on top of a thin background
(0.06), and modulated by the fine R channel:
Approximately self-similar expansion. The noise is sampled at q = p / max(R, 1), not at p.
Once R ≥ 1, the noise pattern grows with the shell: a noise feature at 80 % of the radius stays at that fraction.
Sampling at p instead would make the shell slide through a fixed fog — filaments would appear to stream inward through
the front. The max(R, 1) stabilizes the early expansion, and the shell thickness is 0.22·R + 1, so its
relative thickness changes as it grows. The directional wrinkles and expanding noise give a largely consistent pattern, but the
complete density field is not an exactly frozen shape scaled up.
shellDensity() on the real noise volume (±240 units around the center; the emitted light is brightened for display, since one thin slice holds far less light than a whole ray). Hover to probe a point and choose the
direction plotted on the right: density along that radius, with the profile band and the front. Try: press Play, then
turn off self-similar and watch the filaments drift through the shell instead of growing with it.6 · Temperature, color and beams
Density says how much gas there is; the gas also needs a color. In this volume the color is emitted: the gas is the light source. Two ingredients decide it.
Heat. Gas just behind the shock is the hottest; it cools as the front moves on. So heat is a function of the same signed
coordinate: heat = smoothstep(−0.5, 0.1, x) — 1 at the front, 0 half a thickness inside. Hot glowing matter is
blue-white (the hotter a glowing body, the bluer its light), so heat blends toward vec3(0.7, 0.82, 1.0) × 3 — three times
brighter than the cool gas.
Composition. Cooled gas in real remnants glows in emission lines: each element emits at its own sharp wavelengths. In
photographs, hydrogen's Hα line (656 nm) gives the deep pink-red, and doubly ionized oxygen, [O III] (501 nm), the teal-green.
The shader picks between a pink and a teal by a third noise lookup, so the filaments come in patches of each:
cool = mix(pink, teal, smoothstep(0.45, 0.7, oxygen)). The noise channel is called "oxygen" in the code, but it is just a
mixing mask, not a chemical model.
mix(cool, blueWhite · 3, heat) for every heat (across) and oxygen value (up),
tone-mapped as the screen would show it. Hover to read a color. The dashed band marks where the pink-to-teal blend happens.Two extra lights. Inside the same loop, two analytic shapes add emission without any noise:
- The equatorial ring: a glowing tube around a circle of radius
40 + 14·agein a tilted plane. For each sample the shader measures the height above that plane and the radial distance from the circle, and addsexp(−(radial² + height²) / 6)— a Gaussian tube a couple of units thick. (Supernova 1987A, in the Large Magellanic Cloud, is famous for its bright ring.) - The pulsar beams: a beam axis turns around a tilted spin axis,
beamAxis = normalize(spin·0.4 + cross(spin, (cos a, 0, sin a)))witha = 7t, so it sweeps a cone about once every 0.9 s. For each sample,alignment = |dot(direction to p, beamAxis)|— the absolute value gives two opposite beams — raised to the power 600, which keeps only directions within a few degrees of the axis. The beams fade with distance asexp(−r / 50). Real pulsars spin from once every few seconds to hundreds of times a second.
7 · Glowing fog: the march
A surface either blocks a ray or doesn't. A fog does something in between, everywhere along the ray. Three things can happen to light in a medium:
- Emission — the gas glows and adds light toward the camera;
- Absorption — the gas swallows some of the light coming from behind it;
- Scattering — the gas bounces light from other directions (say, from a sun) into the ray. This needs lighting at every sample and a phase function — such as the Henyey–Greenstein function (1941) used by the Pillars scene — and is the costly part of cloud rendering.
The shock shell is self-luminous, so the demo uses only the first two: an emission–absorption model, the simplest of the optical
models Nelson Max described for volume rendering in 1995. Absorption follows the Beer–Lambert law (Bouguer, Lambert and Beer,
18th–19th century): passing through a thin layer of density ρ and thickness Δt keeps a fraction
e−ρΔt of the light. Layers multiply, so after many layers the light that survives — the
transmittance T — is the exponential of the summed density. Light emitted at some point reaches the camera
dimmed by the transmittance between that point and the camera. Adding up all the points gives the pixel:
The shader evaluates that integral by marching: it cuts the gold segment of §2 into uSteps = 80 equal steps and
walks it front to back, keeping two running values — the light gathered so far and the transmittance so far:
for (int i = 0; i < uSteps && transmittance > 0.02; ++i, t += dt) { ...density, emission at p = ro + rd·t... col += transmittance * emission * dt * 0.012; // light from this step, dimmed by everything in front transmittance *= exp(-density * dt * 0.012); // Beer–Lambert for this step }
Details that matter:
- The step adapts to the ray.
dt = (t₁ − t₀) / steps: every ray takes the same number of steps, and because each step's contribution is multiplied bydt, the result estimates the same integral whatever the step count. Fewer steps make it noisier, not darker. - The 0.012 is an artistic tuning value. The shell is hundreds of units across; this factor converts world distances into a thin, mostly transparent gas, so you see the far side of the shell through the near side.
- Jitter. If every pixel sampled at the same distances, the step pattern would show as rings (banding). Starting each pixel's
march at a random fraction of a step,
t₀ + dt · hash12(pixel), turns those rings into fine grain that the eye — and the bloom — average away. - Early exit. Once less than 2 % of the light gets through, nothing behind can matter, so the loop stops.
- Energy-conserving steps. The demo treats the emission of each step as constant and simply multiplies by
dt. A step that is itself partly opaque should dim its own glow; the exact result for a step with constant density isE · Δt · (1 − e−τ) / τ, with optical depthτ = ρ · Δt · k— the "energy-conserving" form popularized by Sébastien Hillaire's 2015 Frostbite course notes. For this thin gasτper step is small and the two agree within a few percent; the figure lets you compare them, and makes the gas thicker to show when it matters.
T (cyan) falling as the
ray passes through gas, and the light gathered so far (red, green, blue). The strip at the bottom is each step's contribution.
Try: raise the absorption to 20×: the far side of the shell disappears behind the near side and the two integration
steps start to disagree. (Ring and pulsar are left out here.)After the loop, the neutron star is added as a point glow, exp(−2·impact)·6 + exp(−0.4·impact)·0.3, multiplied by the
final transmittance from the entire marched interval. After collapse the march can include gas on the far side of the neutron star,
so this is an approximation: it can dim the glow with gas behind the star as well as gas in front. The shader writes the gathered light as the pixel's color and 1 − transmittance as its alpha: how
much of the background the gas hides.
8 · Half resolution, full sharpness
The march is the expensive part: at 1920 × 1080 with 80 steps it would be 166 million density evaluations per frame, each with four
3D texture reads. But gas is soft — it has no sharp edges for extra pixels to resolve. So the demo renders the volume into its own
render target at a fraction of the screen size (scale = 0.5 by default: a quarter of the pixels, a quarter of the
cost), in 16-bit floating point so bright values survive.
A second, full-resolution pass then builds the final image. It reads the small volume image with bilinear filtering — the texture unit blends the four nearest texels, so the upscaled gas stays smooth — and combines three things:
The sky — stars one or two pixels wide — is computed at full resolution in this pass; at half resolution it would turn into
blurry blobs or vanish between samples. It is dimmed by the volume's alpha, so the gas hides the stars behind it. The flash is
a uniform wash of white plus a sharp glare toward the explosion, uFlash · (0.6 + 2 · cos40(angle to the center)).
Note the cameraRay function normalizes by the image height, so the half- and full-resolution passes compute identical rays.
9 · Debris from a vertex index
The debris uses the warp scene's streak technique (see the warp explainer): the GPU is asked to draw 6 × 4000 vertices with
no vertex data, and each vertex works out what it is from its number, gl_VertexID: shard k = gl_VertexID / 6,
corner gl_VertexID % 6 of a two-triangle quad. Six random numbers per shard come from a hash of k.
Unlike the warp's float hash, this one is integer: hashU(k, channel) runs the PCG hash — a few multiplies,
shifts and XORs on 32-bit integers, derived from Melissa O'Neill's PCG random number generators and recommended for GPUs by Jarzynski
and Olano in 2020. Float hashes lose precision with large ids; integer ones don't.
dir = normalize(vec3(h1, h2, h3) · 2 − 1) // a random direction speed = mix(0.7, 1.2, h4) // some shards lead the shell, some lag head = dir · R(age) · speed // where it is now tail = dir · R(age − 0.12) · speed // where it was 0.12 s ago width = mix(0.8, 2.2, h5) pixels // half-width, in screen space color = mix(orange, pale yellow, h6) · 2.5·e^(−0.25·age) / (1 + 0.1·length)
Each shard has no stored state; its position is a pure function of the shell radius, so it rides the same deceleration as the gas.
The streak is a motion-blur trail: the line from where the shard was 0.12 s ago to where it is now, both projected to the screen with
the camera's view and projection matrices, then widened into a quad of a few pixels. The / (1 + 0.1·length) spreads a long
streak's light thinner, and the fragment shader (shared with the warp) shades it as a soft capsule. Streaks are drawn with additive
blending on top of everything, after the volume; vertices behind the camera are thrown off screen.
A subtle detail: normalizing a random point of a cube does not give evenly spread directions — the cube's corners are farther away, so directions toward the eight corners are up to about five times more likely than toward the faces. Among thousands of fast streaks it is invisible; the figure lets you compare with truly uniform directions.
debris.vert.
Each streak runs from its position 0.12 s ago to now: long and bright right after the flash, short and dim as the shell slows.
Use the slider to inspect one shard (circled) and its six hash values.The algorithm, step by step
Here is the complete scene, in order, with each step linked to the idea above that explains it.
// once, at startup for each voxel of a 128³ RG8 texture: R = tiled value-noise fbm (5 octaves) G = 1 − tiled Worley fbm (3 octaves) |
A compute shader bakes the shared noise volume; periods divide the volume, so it tiles seamlessly with REPEAT
wrapping (§3). |
// CPU, once per frame
age = t − duration / 3
instability = clamp(t / flashTime, 0, 1)³
starRadius = 22 · breathing · (1 − collapse)
R = 150·(1 − e^(−0.25·age)) + 1.5·age
flash, ring, pulsar, brightness = f(age)
camera = orbit(0.4 + 0.05t),
distance 75 → 260, shake 2.5·e^(−2·age) |
Everything that changes over time is computed directly from t and sent as uniforms (§1). The camera looks at the
origin from a slow orbit; the shake is three sines of unrelated frequencies (61, 47, 53) fading out after the flash. |
// pass 1: volume, at scale × screen size for each pixel: ro, rd = camera ray col = 0; T = 1 if starRadius > 0: col += orange · haze(impact) if ray hits the star: col += starSurface(normal, rd) T = 0; tStar = hit distance |
A ray per pixel (§2), at reduced resolution (§8). Before the flash: the orange haze from the impact parameter, then the sphere hit, colored by the cell noise, limb darkening and flicker (§4). The star is opaque: transmittance 0. |
if R > 0:
[t0, t1] = ray ∩ sphere(1.35R + 5)
dt = (t1 − t0) / steps
t = t0 + dt · hash12(pixel)
beamAxis = sweep(spinAxis, 7t) |
After the flash: march only inside the bounding sphere (§2), in equal steps with a per-pixel random start (§7). The pulsar's beam axis is computed once per pixel (§6). |
for i < steps while T > 0.02: p = ro + rd · t front = R · (1 + 0.2 · (noise(dir) − ½)) x = (|p| − front) / (0.22R + 1) profile = band(x) q = p / R density = profile · (0.06 + 1.6 · cells(q)) · (0.6 + 0.6 · fine(q)) heat = smoothstep(−0.5, 0.1, x) |
The density of the shell at p: a wrinkled front (§5), a signed coordinate in shell thicknesses, a soft band behind the
front, Worley filaments and fine noise, all sampled at p / R so the structure expands with the shell. |
cool = mix(pink, teal, oxygen(q))
E = mix(cool, blueWhite·3, heat)
· density · brightness
+ ring(p) + beams(p)
col += T · E · dt · 0.012
T *= exp(−density · dt · 0.012) |
Temperature and composition give the color (§6); the ring and pulsar beams add analytic emission. Then the emission–absorption step: light dimmed by the gas in front, and Beer–Lambert absorption (§7). |
col += T · neutronStarGlow(impact) · pulsar
out = (col, 1 − T) |
The neutron star is a point glow behind the gas. Alpha stores how much of the background is hidden (§7). |
// pass 2: full resolution for each pixel: v = bilinear(volume image) sky = star layers(rd) flash = uFlash · (0.6 + 2·cos⁴⁰(to center)) out = sky · (1 − v.a) + v.rgb + flash |
The cheap, sharp things are drawn at full resolution and the soft volume is upscaled underneath them (§8). |
// pass 3: debris, 6 × 4000 vertices, additive for each vertex v: k = v / 6; corner = quadCorner(v % 6) dir, speed, width, tint = hashU(k, 1..6) head = project(dir · R(age) · speed) tail = project(dir · R(age − 0.12) · speed) quad from tail to head, half-width px |
Debris shards built from the vertex index and an integer hash, stretched into motion-blur streaks (§9). Then the demo's post chain: bloom, tone mapping, grain. |
The real source
These are the files as they are in the demo's repository. The browser version above uses the same code, with the changes listed under the live demo.
shaders/scenes/supernova/volume.frag fullscreen · reduced resolution · ray march
shaders/scenes/supernova/composite.frag fullscreen · full resolution
shaders/scenes/supernova/debris.vert vertex shader · one shard per 6 vertices
shaders/scenes/warp/stars.frag fragment shader · capsule, shared with the warp
shaders/common/noise3d.comp compute shader · runs once
shaders/common/camera.glsl and sky.glsl included helpers
src/scenes/supernova/SupernovaScene.cpp C++ · per frame
src/core/Resources.cpp — noiseVolume() C++ · excerpt
Glossary
- Fragment shader
- A program the GPU runs once per covered pixel (fragment); it decides the pixel's color.
- Vertex shader
- A small program the GPU runs once per vertex; it decides where the vertex lands on screen.
- Uniform
- A value set by the CPU before a draw and shared by every vertex and pixel of that draw (time, shell radius, camera…).
- Ray
- A half-line from the camera through a pixel:
ro + t·rd, with originro, directionrdand distancet. - Impact parameter
- The closest distance between a ray and a point; used here to paint glows around the star and the neutron star without marching.
- Bounding sphere
- A sphere known to contain everything of interest; rays only do work inside it.
- Ray marching
- Walking along a ray in steps and evaluating a function at each step, instead of solving for an intersection.
- Participating medium
- A volume that interacts with light everywhere inside it — fog, smoke, gas — rather than only at a surface.
- Emission / absorption / scattering
- The three ways a medium changes light along a ray: adding its own glow, swallowing light, or redirecting light from elsewhere. The shell uses the first two.
- Transmittance
- The fraction of light that survives the medium between two points, from 1 (clear) to 0 (opaque).
- Optical depth
- The summed density along a path,
τ; transmittance ise−τ. - Beer–Lambert law
- Light passing through an absorbing medium decays exponentially with the amount of medium traversed.
- Phase function
- For scattering media, how much light is redirected into each angle (e.g. Henyey–Greenstein). Not needed for a purely emissive gas.
- Jitter (dithering)
- Offsetting each pixel's samples by a random fraction of a step, trading visible banding for fine, even noise.
- 3D texture / voxel
- A texture with depth as well as width and height; each of its cells is a voxel. Here 128³ voxels of two 8-bit channels.
- Trilinear / bilinear filtering
- Hardware blending of the 8 (3D) or 4 (2D) texels nearest to a lookup, giving smooth values between them.
- Tileable noise
- Noise whose opposite edges match, so copies placed side by side show no seams.
- Value noise / fbm
- Smoothly interpolated random values on a grid; fbm sums several octaves of it for natural detail.
- Worley (cellular) noise
- The distance to the nearest of a set of random feature points, one per grid cell; looks like cells or foam.
- Self-similar
- Keeping the same shape at every size; here, sampling the noise at
p / Rso the shell's pattern scales with it. - Limb darkening
- The darkening of a star's disc toward its edge, where the line of sight only reaches cooler outer layers.
- Rayleigh–Taylor instability
- Fingering of the boundary between two fluids when the lighter one pushes or decelerates the heavier one.
- Emission line
- Light emitted by an element at one sharp wavelength, such as hydrogen's red Hα or oxygen's teal [O III].
- Neutron star / pulsar
- The collapsed core of a massive star, about 20 km across; a pulsar is one whose beams sweep past us as it spins.
- Render target
- An off-screen image the GPU draws into, which can later be read as a texture.
- gl_VertexID
- The index of the vertex being processed. With no vertex data at all, it is enough to build geometry procedurally.
- Hash / PCG hash
- A deterministic scrambling function: same input, same random-looking output. PCG is an integer hash that stays exact for large ids.
- Additive blending
- Adding a new color to the existing pixel instead of replacing it — how light combines.
- 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.
← Previous: 10 · Dreadnought · All scenes · Next: 12 · Wormhole →
