Made by AI
The giant star with its corona, captured from the demo
Space Travel · Interactive explainer · Scene 05

The Giant Star

A white flash, and we drop out of hyperspace beside a giant star. Its boiling surface, the flames streaming off its edge and the loops of plasma arching above it are not textures, meshes or particles: every pixel is computed from one ray, one sphere and a few noise functions. Explained from first principles, with the real shaders running right here in your browser.

The scene

The giant star is the fifth scene of the demo. It starts at beat 112 (53.76 s into the demo) and lasts thirty-two beats — 15.36 seconds at 125 BPM. It enters with the timeline's flash transition: over one beat (0.48 s) a white HDR flash swells, peaks exactly at the cut from the warp's tunnel to the star, and fades — the bloom floods the whole screen, so the switch is invisible. When the glare clears, the camera is patrolling: it orbits about a quarter of the way around the star while slowly closing in from 3.6 to 2.35 star radii (closest at about 9.5 s, then drifting back out), and it aims a little past the star so the disc drifts across the frame like a fly-by.

The star is a perfect sphere — a mathematical one, not a mesh of triangles. For every pixel, the shader sends a ray from the camera through that pixel and asks three questions: does the ray hit the sphere? (then paint the boiling surface); how close does it pass? (then paint the corona's flames); does it cross one of five plasma loops? (then add their glow). Noise functions supply all the detail, and the time variable makes it move. This page builds those pieces up one at a time.

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. If you haven't yet, the warp page introduces the pinhole camera, hashes and fractal noise in more detail.

Live: the real shaders

This is the actual scene: the demo's two fragment shaders — star.frag, which draws the star into a reduced-resolution layer, and composite.frag, which paints the star sky at full resolution and lays the star layer on top — ported to WebGL2. The camera path, the star's rotation and the music pulses come from a JavaScript copy of the scene's C++ render(). Drag the time slider to scrub: every frame is a pure function of time.

Try: pause near 9.5 s (the closest approach) and lower the exposure to 0.2 — the granulation and sunspots that the glare hides appear. Set render scale to 0.2: the star turns soft and blocky while the sky's stars stay sharp (§9). Raise corona reach for long flames; set surface to 0 and the black disc shows how it hides the corona behind it. Restart to see the flash.
Port notes: the shader code is the demo's, unchanged apart from the #version line, precision qualifiers and inlining the common/ helpers it includes — plus one line: the sky's stars are sized for the demo's 1080-line frame rather than this smaller canvas, so they keep the demo's apparent size. On some systems the star shader takes a few seconds to compile; the readout shows it. The demo's post chain (bloom, chromatic aberration, vignette, grain) is replaced by ACES tone mapping and a glow with a hard threshold of 1.0 on blurred mip samples. The demo's soft knee starts admitting light above a brightness of 0.5 at that threshold. During the first half of the flash the demo still shows the warp's last frame; here it shows the star. The music pulses are computed from the placeholder song's pattern data (first pass of the demo, audio latency ignored).

Background

Nine ideas make up the scene. The first two are geometry, the next three paint the surface, then the corona, the prominences, and finally how the layers, the music and the transition come together.

1 · Rays from an orbiting camera

The warp scene projected points onto the screen: given a star, find its pixel. This scene works the other way round — given a pixel, find what it sees. That is ray casting (Arthur Appel described it in 1968): for every pixel, build the straight line that leaves the camera through that pixel, and follow it into the scene.

The camera is described by a position and three perpendicular unit vectors: forward (where it looks), right and up. Picture the screen as a small window placed at distance focal in front of the camera. A pixel's position on that window, measured in screen heights from the center, is uv; the ray direction is just "go forward by focal, then sideways by uv":

uv = (pixel − ½ · resolution) / resolution.y     rd = normalize(forward · focal + right · uv.x + up · uv.y)

Dividing by the height (not by the width) keeps pixels square, and makes uv independent of the resolution: a half-resolution image and a full-resolution image compute the same ray for the same point of the screen. That detail matters in §9. The scene's focal length is 1.1, a vertical field of view of 2 · atan(0.5 / 1.1) ≈ 49°.

The camera itself is placed by a function of time, cameraAt(t), in C++. It orbits in a circle around the star (angle grows by 0.11 rad/s), bobs up and down (elevation ±0.28 rad), and its distance swings between 3.6 and 2.35 star radii with a slow cosine. It does not aim at the star's center: the target point slides sideways and up and down, so the star drifts across the frame. A slight roll (±0.08 rad) completes the hand-held feel. From position and target, lookAt builds the three vectors: forward = normalize(target − position), right = normalize(forward × worldUp), up = right × forward.

Left: the scene seen from above. The violet line is the camera's whole path during the scene; the gold dot is the camera now, with its view cone (the horizontal field of view of a 16:9 frame). The cross is the point it aims at — never the star's center. Right: distance to the star's center and the camera's elevation and roll over the 15.36 s. The orbit speed slider is shared with the live demo.
// C++ (src/scenes/corona/CoronaScene.cpp), t = scene time × orbit speed
angle     = 0.4 + 0.11 · t
elevation = 0.28 · sin(0.17 · t + 0.8)
approach  = 0.5 − 0.5 · cos(0.33 · t)              // 0 = far, 1 = closest (at t = π / 0.33 ≈ 9.5 s)
distance  = mix(3.6, 2.35, approach)
position  = distance · (cos(elev)·cos(angle), sin(elev), cos(elev)·sin(angle))
side      = normalize(cross(−position, worldUp))
aim       = side · 0.55·sin(0.21·t + 0.3) + (0, 0.15·sin(0.13·t), 0)
camera    = lookAt(position, aim, focal = 1.1, roll = 0.08 · sin(0.19 · t))
At the closest approach the star's apparent radius is asin(1 / 2.35) ≈ 25°, more than half the 49° vertical field of view: the disc overflows the frame. At the start, from 3.6 radii, it is asin(1 / 3.6) ≈ 16°.

2 · Hitting a sphere

The star is a sphere of radius 1 at the origin. A ray is every point p(t) = ro + t · rd: start at the camera ro, walk a distance t along the unit direction rd. The ray touches the sphere where |p(t)|² = 1. Expanding gives an ordinary quadratic equation in t:

t² + 2b·t + (|ro|² − 1) = 0    with b = ro · rd    ⇒    t = −b ± √h,   h = b² − (|ro|² − 1)

If h is negative there is no real solution: the ray misses. Otherwise the smaller root −b − √h is where the ray enters the star — the visible point — and, because the radius is 1, the surface normal there (the direction straight out of the surface) is simply that point: n = ro + rd · tHit.

The same numbers give something the corona needs. Halfway between the two roots, at t = −b, the ray passes closest to the star's center. That closest distance is called the impact parameter — a term from scattering physics — and a little algebra shows h = 1 − impact²: the ray hits exactly when it passes closer than one radius.

Drag the camera (white) and the direction handle (cyan). When the ray hits, the entry point and its normal are shown; the pink point is the closest approach and the dashed line its distance from the center — the impact parameter. The numbers on the right are exactly what the shader computes. Bottom right: the antialiased edge. Move the ray past the rim and watch the coverage alpha fall from 1 to 0 over a pixel or two.

A soft edge. If each pixel simply asked "hit or miss?", the disc's outline would be a staircase of pixels. The shader instead measures how fast the impact parameter changes from one pixel to the next — fwidth(impact), which the GPU computes by comparing neighboring pixels — and fades the disc's coverage over 1.5 of those steps around radius 1:

alpha = 1 − smoothstep(1 − e, 1 + e, impact),    e = 1.5 · fwidth(impact)

Pixels in that thin fade zone that just miss the sphere still need a surface color, so they use the limb point under the closest approach, normalize(ro − rd · b). The coverage is kept as the layer's alpha, which §9 uses to put the star over the sky.

3 · A boiling surface: domain warping

The real Sun's visible surface is covered with granules: the tops of convection cells, where hot plasma wells up in bright centers and sinks back, cooler, along darker edges. They constantly appear, drift and dissolve. The shader imitates the look with noise.

Noise on a sphere. The demo's fbm is fractal value noise: random values on a 3D grid, smoothly blended, summed over five octaves of doubling frequency and halving amplitude (see the warp page, §9). Because it is defined everywhere in 3D, the shader can sample it directly at the surface point n — no 2D texture wrapped around a ball, so no seam and no pinching at the poles. Before sampling, n is turned by −uRotation into the star's own frame, so the pattern spins with the star (0.03 rad per second).

Domain warping — a trick popularized in the shader community by Inigo Quilez — means evaluating a noise not at p but at p pushed around by another noise: fbm(p + k · warp(p)). Straight blobs get bent into swirls, filaments and folds — very much like turbulent fluid. Here, three fbm fields at frequency 3 (different offsets, so independent) form a 3D displacement vector, each drifting slowly with time; the granulation fbm, three times finer, is looked up at the displaced point. As the displacement drifts, the granules churn.

// surface(n): p = n turned into the star's frame, t = time
warp  = ( fbm(p·3 + (0, 0, 0.05t)),  fbm(p·3 + (7.1, 0, −0.04t)),  fbm(p·3 + (0, 13.7, 0.03t)) )
gran  = fbm(p·9 + warp·2.2 + 0.04t)                  // warped granulation
cells = valueNoise(p·30 + warp·4 + 0.1t)             // one octave of fine grain
spots = smoothstep(0.66, 0.74, fbm(p·1.7 + 4))       // large, slow sunspots (no time term)
heat  = (0.75·gran + 0.25·cells) · (1 − 0.85·spots)
Try: set the warp amount to 0 — plain fbm, round and lifeless — then raise it past 1 (the demo's value: offsets of 2.2 and 4). The warped lattice view draws the grid lines of the granulation's first octave after warping: straight lines would mean no warp. Speed up time to see the boiling; at ×1 it is the demo's pace. This figure is a small separate WebGL shader using the same functions, seen from straight ahead, without limb darkening or HDR intensity.

4 · From heat to color

A hot object glows, and its color tells its temperature: a blackbody (Max Planck derived its spectrum in 1900) goes from dull red through orange and yellow to white as it heats up, and gets much brighter on the way. The shader borrows that idea, without the physics: the single number heat is turned into a color by two blends — deep red to orange, then orange to a pale yellow-white — each switched on by a smoothstep over a range of heat. The colors and ranges are artistic tuning values, not temperatures.

col = mix((0.8, 0.18, 0.02), (1.0, 0.62, 0.2), smoothstep(0.25, 0.6, heat))   // red → orange
col = mix(col, (1.0, 0.93, 0.75), smoothstep(0.55, 0.85, heat))             // → white-hot

Sunspots multiply the heat by 0.15, which drops them to the deep red end — on the real Sun, spots are cooler regions where strong magnetic fields hold back the convection, so they look dark next to the surface around them. Finally the color is multiplied by uSurfaceIntensity = 5 (and by up to 25 % more on each beat, §9). That pushes most of the disc far above 1.0, "whiter than white" in the HDR image; tone mapping squeezes it back to near white and bloom spreads the excess as glare. That is why, in the capture, the disc looks white-hot with only faint structure.

Top strip: the color ramp, heat 0 → 1. Second strip: the same after ×intensity and ACES tone mapping — what reaches the screen. Plot: the two smoothstep weights, over a histogram of the heat values actually found on the star's surface (3,000 points). Try: raise the sunspot mask and see the pink marker fall to the red end; lower the intensity to 1 and the ramp's colors survive tone mapping.

5 · Limb darkening

Photographs of the Sun show a disc that is brightest in the middle and noticeably darker and redder near its edge, the limb. Looking at the center, our line of sight goes straight down into the hot, deeper layers; near the edge it enters the gas at a grazing angle and only reaches higher, cooler layers before the gas becomes opaque. Astronomers describe it with simple laws in μ, the cosine of the angle between the line of sight and the surface normal (1 at the center, 0 at the limb). The common linear law is I(μ) = 1 − u · (1 − μ), with u about 0.6 for the Sun in visible light.

The shader uses its own artistic curve:

μ = max(n · (−rd), 0)     col ×= 0.3 + 0.7 · μ0.55

The exponent below 1 keeps most of the disc bright and concentrates the darkening near the edge; the 0.3 floor keeps the limb from going black. Besides realism, limb darkening does an important job: without it a sphere lit from within looks like a flat coin. The fall-off is what tells the eye "this is round".

Left: the star's surface (its real heat field, at time 0, seen face-on). Right: the brightness factor from the center (r = 0) to the limb (r = 1). Gold: the demo's curve with your floor and exponent; cyan dashes: the linear law with u = 0.6; gray: no darkening. Try: toggle limb darkening off — the ball becomes a disc. Turn on tone mapping: even when ×5 and ACES wash out the center, the edge still darkens.

6 · The corona: one sample per ray

The Sun's corona is its outer atmosphere: an extremely hot, very thin gas that reaches far into space and is seen with the naked eye only during a total eclipse, as a pearly glow with long streamers. Thin glowing gas is a volume: every point along a ray that crosses it adds a little light, and a faithful renderer would add up many samples along the ray.

The shader takes a shortcut. The corona's brightness depends mostly on height above the surface, and falls off quickly. A ray that passes the star is at its lowest height — where the gas is brightest — at its point of closest approach (§2). So the shader evaluates the corona once per ray, at that point, with height = impact − 1. One sample instead of dozens.

glow = exp(−3 · height / reach),  reach = uCoronaReach · (0.35 + 1.3 · flames)   // flame tongues
rim  = exp(−30 · height)                                                       // thin bright ring at the limb
halo = 0.2 · exp(−1.2 · height)                                                // faint wide halo
col  = mix(gold, red, smoothstep(0, 1.2, height))
corona = col · ((glow · (0.6 + 1.4·flames) + 1.5·rim) · (1 + uFlare) + halo) · uCoronaIntensity

flames (0 to 1, from noise — next section) does double duty: it stretches the glow's reach and boosts its brightness, so where the noise is high a long bright tongue licks outward and where it is low the corona hugs the star. The music's snare (uFlare, §9) brightens glow and rim but not the halo. If the closest approach lies behind the camera (tc < 0), the corona is zero. For rays that hit the disc the corona is still computed, then replaced by the surface according to the coverage alpha — that is why it never shows in front of the star.

Left: a fan of camera rays seen from the side. Rays that miss the star are marked at their closest approach, colored by the corona brightness there; the gold ray is the one you select. Right: the three terms against height (in star radii), for one value of flames, and below them the resulting color after tone mapping. Try: sweep flames from 0 to 1 to see how a noise value becomes a short or a long flame.

7 · Flames that flow outward

Where does flames come from? From two fbm lookups, on the direction of the closest point from the star's center (a point on the unit sphere) — and on a single extra number that mixes height and time:

flow = height − 0.12 · t     flames = 0.7 · fbm(dir · 5 + flow · 0.9) + 0.3 · fbm(dir · 11 + flow · 1.7 + 3.1)

Two things happen. Stretching: going around the star, the noise changes quickly (frequency 5 on the direction); going outward, slowly (0.9 per star radius). Features are therefore several times longer than wide and all point away from the star: streamers. It is the same trick as the warp's tunnel (warp page, §8).

Flowing: a given feature of the noise sits at one fixed value of flow. If height − 0.12 · t must stay constant while t grows, the feature's height grows at 0.12 star radii per second. Every flame streams outward, forever, yet nothing is moving and nothing is stored: the shader just slides the noise's input. Both layers — the coarse one and the fine one — move at that same speed. (The direction is also turned by half the star's rotation angle; note that it turns in the opposite sense to the surface, a difference that is hard to notice with only 0.46 rad of total spin over the scene.)

Left: the corona seen face-on (the disc is drawn flat). Right: the same ring of corona "unrolled": angle around the star across, height up, the star's surface along the bottom edge. The dashed cyan lines rise at 0.12 radii per second — exactly with the flames. Try: without the height term every flame is a perfectly straight spoke that only flickers in place; with height but no time the streamers are frozen. Only together do they flow. This figure is a small WebGL copy of corona() with a switch for flow.

8 · Prominences: glowing tubes

Prominences are huge loops of plasma that rise above the Sun's surface, following magnetic field lines whose two ends are anchored in the surface. The shader draws five of them, each one a circle standing on the star: the circle's center is a point on the surface, so half of it is buried inside the star and the other half arches above it. Each arc has its own frame — a surface point center and two directions u, w — built from hashes of its index, so the arcs are scattered "randomly" but identically every frame.

Distance to a tube. For a point p, write it in the arc's frame: local = (p·u, p·center, p·w) relative to the arc's foot point. In the plane of the circle, the distance to the circle line is |local.xy| − height; adding the out-of-plane part gives the distance to the circle's center line, and subtracting the tube's thickness gives the distance to the tube's skin:

d = | ( |local.xy| − height, local.z ) | − thickness

That is the classic signed distance function of a torus (a doughnut): negative inside, positive outside. The radius height (0.18 to 0.38 star radii, from a hash) breathes slowly between 70 % and 100 % of that value, and the thickness (0.022) is modulated by sin(9 · angle + 2t), so knots of brightness travel along each arc.

Ray marching. There is no neat formula for "how much glow does this ray collect from five thin tubes", so the shader marches: it clips the ray to a bounding sphere of radius 1.5 (another quadratic, as in §2), stops it at the star's surface tHit (which hides buried halves and arcs behind the star), and takes 48 equal steps. At each step it finds the distance d to the nearest tube and adds a glow:

density += ( e−70·d + 0.08 · e−12·d ) · Δt     color = (1, 0.32, 0.12) · density · 18 · uProminences

This is emission only: the glowing gas adds light but absorbs nothing (unlike a smoke or dust volume, where the Beer–Lambert law would dim what lies behind). The sharp term gives a bright core, the wide term a soft aura. The soft fall-off also matters for sampling: a step through the bounding sphere can be up to 3 / 48 ≈ 0.06 long, wider than the 0.044-wide tube, so a sample can land beside a tube rather than inside it. Because the glow decays smoothly with distance, neighboring samples still pick it up. Compare with sphere tracing (John C. Hart, 1996), which steps by the distance itself to find a surface: here the goal is to collect glow, so fixed steps are used.

Left: a cross-section through one arc's plane: the star's surface, the bounding sphere (dashed) and the tube's glow. Drag the camera (white) and the direction (cyan); the dots are the march samples, lit by what each one collects. Right: each sample's glow and the running total, and the final color. Try: lower the steps to 8 and slide the ray: the arc flickers in and out as samples miss it. Aim at the arc's foot: the march stops at the surface.

9 · Two layers, the music and the flash

Two resolutions. Everything above runs for each pixel of the star layer, and the surface alone calls fbm five times (25 octaves of noise) and the prominence march computes 48 × 5 tube distances. To stay fast, the star is rendered at a render scale of 0.5 — half the width and half the height, a quarter of the pixels — into a 16-bit floating-point layer. A second pass runs at full resolution: it paints the sky — two faint nebula tints from fbm and a field of stars — and puts the star layer, smoothly upscaled, on top. Glowing gas and a bright disc lose nothing at half resolution; the sky's one-pixel stars would. That is why both passes compute rays the same resolution-independent way (§1): the two images line up exactly.

Premultiplied alpha. The star layer stores rgb = the light it emits and a = how much of what is behind it the disc hides. Compositing is then a single line, the "over" operator of Thomas Porter and Tom Duff (1984):

final = sky · (1 − a) + star.rgb

The disc (a = 1) hides the sky; corona and prominences have a = 0 and simply add their light; the antialiased rim of §2 blends between the two.

The sky's stars come from the shared starSky helper: directions are scaled into a grid of 3D cells, a hash picks about one cell in ten to hold a star at a random spot, and each pixel checks the 2 × 2 × 2 nearest cells, so a star is never cut off at a cell border. Each star is a tiny Gaussian dot sized in pixels, so it stays sharp at any resolution.

The music. Two values follow the tune. uFlare jumps to 0.8 on every snare hit (instrument 2) and decays as e−4s; it multiplies the corona's glow and rim by (1 + flare). uPulse jumps to 1 on every beat and decays as e−6s; it brightens the surface by up to 25 %. Both are computed from the song's timing alone, so they are as scrubbable as everything else.

The flash. During the first beat the timeline's transition adds (1, 0.95, 0.9) · 6 · e−((progress − 0.5) · 5)² to the image — a bell-shaped HDR flash peaking at the cut. Before the peak the screen shows the warp, after it the star; at the peak no one can tell.

The scene's 32 beats: pink, the snare flare (ticks: snare hits — two per bar, plus a drum fill at the end of the first 16 beats); violet, the beat pulse; white, the flash at the start; cyan, the camera's distance to the star. The gold line is the current time.

The algorithm, step by step

Here is the complete scene, in order, with each step linked to the idea above that explains it.

// CPU, once per frame
camera   = cameraAt(t · orbit)
rotation = 0.03 · t
flare    = snarePulse(decay 4) · 0.8
pulse    = beatPulse(decay 6)
layer    = screenSize · renderScale (0.5)
The camera path is a pure function of time (§1). The star's spin and the two music pulses (§9) are the other values that change; everything is sent to the GPU as uniforms.
// pass 1: star layer, reduced resolution
for each pixel:
    ro = camPos;  rd = cameraRay(pixel)
    col = corona(ro, rd, out impact)
    b = ro·rd;  h = b² − (|ro|² − 1)
    e = 1.5 · fwidth(impact)
    alpha = 1 − smoothstep(1 − e, 1 + e, impact)
One ray per pixel (§1). The corona is evaluated first, at the ray's closest approach, which also yields the impact parameter (§6). The quadratic's h tells hit or miss, and the impact parameter gives an antialiased coverage (§2).
    tHit = ∞
    if alpha > 0:
        if h > 0: tHit = −b − √h;  n = ro + rd·tHit
        else:     n = normalize(ro − rd·b)
        col = mix(col, surface(n, rd), alpha)
    col += prominences(ro, rd, tHit)
    out = (col, alpha)                // premultiplied
On the disc the surface replaces the corona; at the very rim, pixels that just miss use the limb point. Prominences are added on top, marched only up to the surface (§8). The layer keeps light in rgb and coverage in a (§9).
// surface(n, rd)
p     = rotateY(n, −rotation)
warp  = 3 × fbm(p·3 + offsets ± time drifts)
gran  = fbm(p·9 + warp·2.2 + 0.04t)
cells = valueNoise(p·30 + warp·4 + 0.1t)
spots = smoothstep(0.66, 0.74, fbm(p·1.7 + 4))
heat  = (0.75·gran + 0.25·cells)·(1 − 0.85·spots)
col   = ramp(heat)  // red → orange → white
μ     = max(n·(−rd), 0)
col  ×= 0.3 + 0.7·μ^0.55
return col · 5 · (1 + 0.25·pulse)
Noise sampled in 3D on the star's own frame; domain warping makes it churn (§3). Heat becomes a blackbody-like color, darkened in sunspots (§4); limb darkening rounds the disc (§5). The intensity of 5 makes it HDR-bright.
// corona(ro, rd)
tc = −ro·rd;  c = ro + rd·tc;  impact = |c|
if tc < 0: return 0
dir    = rotateY(c / impact, 0.5·rotation)
height = max(impact − 1, 0)
flow   = height − 0.12t
flames = 0.7·fbm(dir·5 + flow·0.9)
       + 0.3·fbm(dir·11 + flow·1.7 + 3.1)
reach  = 0.6 · (0.35 + 1.3·flames)
glow = e^(−3·height/reach);  rim = e^(−30·height)
halo = 0.2·e^(−1.2·height)
return mix(gold, red, height) ·
       ((glow·(0.6 + 1.4·flames) + 1.5·rim)
        · (1 + flare) + halo) · 1.5
One sample per ray, at the closest approach (§6). Sliding the noise's input by height − time turns it into streamers that flow outward at 0.12 radii per second (§7). The flame value sets both the reach and the brightness; the snare flares glow and rim.
// prominences(ro, rd, tEnd)
[t0, t1] = ray ∩ sphere(1.5), t1 = min(t1, tEnd)
for i in 0..4:                     // arc frames
    center = normalize(hash3(i) − 0.5)
    u, w   = perpendiculars, spun by hash(i)·2π
    height = mix(0.18, 0.38, hash(i))
             · (0.85 + 0.15·sin(0.6t + 1.7i))
for 48 steps p (into the star frame):
    d = min over arcs of
        |(|local.xy| − height, local.z)| − thick
    density += (e^(−70d) + 0.08·e^(−12d))·Δt
return (1, 0.32, 0.12) · density · 18
Five rings standing on the surface, each in its own hashed frame (§8). The march accumulates emission from the torus distance field; the beads come from the thickness 0.022·(0.75 + 0.25·sin(9·angle + 2t + i)).
// pass 2: full resolution
for each pixel:
    rd   = cameraRay(pixel)
    sky  = nebulaTints(fbm(rd)) + starSky(rd)
    star = texture(starLayer, pixel / size)
    out  = sky · (1 − star.a) + star.rgb
Same rays at full resolution, so the layers line up. The sky stays pixel-sharp; the star layer is upscaled with bilinear filtering and composited with the "over" operator (§9).
// post (whole demo) + first beat of this scene
hdr += (1, 0.95, 0.9) · 6 · e^(−((p − 0.5)·5)²)
bloom → chromatic aberration → ACES → vignette, grain
The flash transition hides the cut from the warp (§9); the demo's post chain turns the HDR image into what you see.

The real source

These are the files as they are in the demo's repository. The browser version above runs the same two shaders, with the helpers from shaders/common/ pasted in place of their #include lines.

shaders/scenes/corona/star.frag fullscreen · reduced resolution · the star
shaders/scenes/corona/composite.frag fullscreen · full resolution · sky + layer
src/scenes/corona/CoronaScene.cpp C++ · the whole scene
shaders/common/camera.glsl included · camera rays
shaders/common/sky.glsl included · star sky
shaders/transitions/flash.frag timeline transition · first beat
src/audio/SyncState.h — pulses (excerpt) C++ · music sync

Glossary

Fragment shader
A program the GPU runs once per pixel (fragment); it decides the pixel's color. Both of this scene's passes are fragment shaders drawn over the whole screen.
Uniform
A value set by the CPU before a draw and shared by every pixel of that draw (time, camera, rotation, pulses…).
Ray casting
Computing each pixel by following the straight line from the camera through that pixel and finding what it meets.
Ray–sphere intersection
Solving the quadratic equation |ro + t·rd|² = r² for t; no real solution means a miss.
Impact parameter
The smallest distance between a ray and the sphere's center, reached at t = −ro·rd. Here it decides hit/miss, the corona's height and the antialiasing.
Normal
The unit direction pointing straight out of a surface. On a unit sphere it is the point itself.
fwidth
How much a value changes between neighboring pixels, computed by the GPU from 2 × 2 pixel blocks. Used to make edges exactly about a pixel soft.
Antialiasing
Avoiding staircase edges by giving edge pixels partial coverage instead of all-or-nothing.
Value noise / fbm
Smoothly interpolated random values on a grid; fbm sums several octaves of it for natural detail.
Domain warping
Evaluating a noise at a position displaced by another noise, f(p + k·g(p)), to get swirling, fluid-looking patterns.
Granulation
The cellular pattern of the Sun's surface: tops of convection cells, bright where hot gas rises, dark where it sinks.
Sunspot
A cooler, darker region of a star's surface, caused by strong magnetic fields.
Blackbody
An idealized glowing object whose color depends only on its temperature: red, then orange, yellow and white as it heats.
Limb darkening
A star's disc looking darker toward its edge (limb), because grazing lines of sight only reach cooler, higher layers.
Corona
A star's hot, thin outer atmosphere, extending far into space in streamers.
Prominence
A loop of plasma rising from a star's surface along magnetic field lines.
Signed distance function (SDF)
A function giving the distance from any point to a shape's surface, negative inside. Here: a torus.
Ray marching
Following a ray in steps and evaluating something at each one; here, adding up the glow of the prominences.
Emission
Light given off by glowing gas. An emission-only volume adds light along a ray and absorbs none.
Render scale
The fraction of the screen resolution at which an expensive pass is rendered before being upscaled.
Premultiplied alpha
Storing color already multiplied by coverage, so a layer is composited with below · (1 − a) + rgb and can also hold pure added light (a = 0).
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.
Pulse
A music-sync value that jumps to 1 on a beat or a drum hit and decays exponentially, e−decay · seconds.
Transition
A short shader that blends the previous scene into the next one on the timeline; here a white HDR flash.

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.
← 04 · Inside the Warp  ·  All scenes  ·  06 · Asteroid chase →