Made by AI
The supernova remnant: a glowing shell of pink and teal filaments around a pulsar
Space Travel · Interactive explainer · Scene 11

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.

You don't need graphics experience to follow along. Each section introduces one concept, with a small interactive figure. Code appears in two forms: pseudo-code (plain logic) and the real GLSL that runs in the demo.

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".

Try: pause around 12 s and lower steps to 16: the shell gets grainy but keeps its brightness (§7). Turn off jitter to see the banding the grain replaces. Drop the volume scale to 0.25 — the soft gas barely changes while the cost falls sixteenfold (§8). Turn off the volume pass to see what is left: the sky, the flash and the debris.
Changes for WebGL2: #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
The scene's clock: every curve is normalized to its own maximum so they fit one chart. Click or drag on the chart to move the live demo to that instant. Notice how much happens in the second after the flash — flash, shake, the steepest growth of the shell — and how slowly everything settles afterwards: exponential decays, each with its own rate.

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:

|ro + t·rd|² = r²  ⇒  t = −b ± √h,    b = ro·rd,    h = b² − (|ro|² − r²)

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:

Drag the camera (white) and the aim point (cyan) — a 2D slice through the scene. The gold segment is the part of the ray inside the sphere: the only part that gets marched, in steps equal slices (the dots). Move the camera inside the sphere to see 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.

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.

One slice of the real volume, built on your GPU by the ported generator and read back. Each panel shows the slice four times, 2 × 2: with the tile borders on, look for seams — there are none. Right: the Worley channel through 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.

Left: the star as 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 = (|p| − front) / (0.22·R + 1)

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:

density = profile · (0.06 + 1.6 · smoothstep(0.55, 0.85, G(q·1.6))) · (0.6 + 0.6 · R(q·4.5))

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.

Left: a slice through the center of the shell, computed with a JavaScript copy of 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.

The emission palette: 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:

Left: how a power turns a cosine into a narrow beam: brightness against the angle from the beam axis, for the demo's 600 (gold) and the exponent you choose (cyan). Right: the geometry, seen from outside — spin axis, the sweeping double beam and the cone it traces, and the equatorial ring. Drag to turn the view.

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:

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:

L = ∫ T(t) · E(t) dt,     T(t) = exp(−∫0t ρ(s) ds)

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:

One pixel's ray through the real shell density. Left: the slice of §5 with the ray; drag up or down to move it. Right: density along the ray (grey), the samples taken (ticks), the transmittance 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:

color = sky · (1 − volume.a) + volume.rgb + flash

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.

The same patch of shell marched at three resolutions and scaled up to the same size. The gas survives a sixteenfold reduction in rays remarkably well. Turn on stars at volume resolution to see why the sky is drawn separately, and turn off bilinear to see the raw low-resolution pixels.

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 seen by the scene's camera, computed with the same hashes and formulas as 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 origin ro, direction rd and distance t.
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 is e−τ.
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 / R so 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 →