Made by AI
The end credits: vector balls spelling SPACE beside the rising greetings
Space Travel · Interactive explainer · Scene 15

The End Credits

A hundred and twenty-eight shiny balls, a chunky 8×8 font and a scroll of greetings: the demoscene's oldest way to say goodbye, rebuilt with modern shaders — and running right here in your browser.

The scene

The last 64 beats of the demo — 30.72 seconds at 125 BPM — are its closing credits. It fades in from the black hole over three beats. On the left, a cluster of 128 glossy, rainbow-tinted balls changes shape every eight beats (3.84 s): a sphere, a cube lattice, a knot, a double helix, the word SPACE, a spiral — and then sphere and cube again, before the demo loops back to its title. The balls throb on every beat, and the cluster spins, except while it spells the word: then it turns to face you. On the right, the credits and greetings rise slowly in the demo's chunky 8×8 font. Behind everything, a dim starfield turns slowly over a deep violet gradient.

This scene is an homage. Vector balls, scrollers and bitmap fonts were the bread and butter of Amiga, Atari ST and PC demos in the late 1980s and early 1990s, when every pixel was pushed by hand. Here none of it is hand-pushed, and nothing is stored as an image or a model: the six shapes are short formulas evaluated once at start-up (one of them reads the pixels of the font), the morph is a per-ball blend driven by the beat counter, and the text is drawn by a fragment shader that, for every screen pixel, works out which letter — and which pixel of that letter — it is looking at.

You don't need graphics experience to follow along. Each section introduces one idea — and, where it applies, the classic effect it comes from — with a small interactive figure. Code appears as pseudo-code (plain logic) and as the real GLSL and C++ of the demo.

Live: the real shaders

This is the actual credits scene: the demo's three passes — backdrop, balls, scroller — with their real shaders ported to WebGL2, driven by a JavaScript copy of the scene's C++ render(). The ball sphere mesh, the six shapes, the font and the credits text are all built in this page from the same code and the same data (the font and the text are even read out of the C++ source listed at the bottom of the page). Drag the time slider to scrub: like every scene, each frame is a pure function of time.

Jump to:
Edit the credits text (one line per row; the shader takes up to 48 lines)
Try: jump to SPACE and watch the ripple run through the balls as they leave the helix. Set the ripple delay to 0 and they all move in lock-step. Turn off the beat pulse to see how much life the throb adds. Type your own greetings in the text box — lower case is shown as upper case, unknown characters as spaces, exactly like the demo's font.

Changes for WebGL2: mesh.vert reads each ball's matrix and color from instanced vertex attributes instead of a storage buffer (WebGL2 has neither storage buffers nor gl_BaseInstance); font.glsl's parameter length is renamed so it doesn't shadow the built-in length(); precision qualifiers added. Not reproduced: the demo's post chain (bloom, chromatic aberration, grain — replaced by ACES tone mapping and a glow with a hard threshold of 1.0 on blurred samples) and the crossfade from the black hole. The demo's bloom uses a soft knee, which starts admitting light above a brightness of 0.5 at that threshold. The beat pulse uses the 125 BPM grid. The text size follows the shader's rule max(1, floor(height / 220)), so in this smaller canvas a font pixel is 2–3 screen pixels instead of 4; the backdrop sizes its stars in screen pixels, so this port sizes them for the demo's 1080-line frame (1080 instead of uResolution.y in the star pixel angle) to keep them as fine as in the demo.

Background

Nine short chapters. The first five are about the balls, the next two about the text, and the last two about the two classics the credits greet by name — the sine scroller and the copper bar — and what the scene kept of them.

1 · Vector balls, then and now

The classic. In the late 1980s, Amiga and Atari ST demos were full of vector balls: dozens of shaded balls placed at the corners of a 3D shape, rotating, bouncing and morphing. Those machines could not light a sphere pixel by pixel in real time, so the ball was drawn once, by hand, as a small bitmap — a disc with a shiny highlight — and stamped onto the screen at each ball's projected position. On the Amiga such a moving stamp was called a bob (blitter object), because the blitter chip did the copying.

A stamp simply covers whatever is underneath. So the order matters: the balls must be drawn from the farthest to the nearest, the way a painter paints the sky before the trees — the painter's algorithm. Every frame, the coder rotated the points, sorted them by depth, and blitted them back to front.

The modern version. The demo draws every ball as a real 3D sphere: an icosphere mesh (a 20-sided solid whose triangles are split three times, giving 1,280 triangles) drawn 128 times in a single instanced draw call, each copy with its own position, size and color. No sorting: the GPU keeps a depth buffer (z-buffer, described by Edwin Catmull in 1974) that stores, for every pixel, the depth of the nearest surface drawn so far, and rejects anything behind it. Because the test is done per pixel rather than per ball, even two balls that cut into each other — as they do when the beat pulse swells them — are resolved exactly.

Left: the sphere shape drawn the 1988 way, as pre-shaded discs. Turn off sorting: balls are then drawn in index order (top to bottom of the sphere) and the back of the cluster pokes through the front. Bigger balls overlap more and make the errors obvious. Right: two balls cutting into each other. The painter's algorithm can only put one whole disc on top; the depth buffer, deciding per pixel, draws the curve where the two surfaces intersect.

2 · Six shapes from formulas

A shape is simply an array of 128 entries, one per ball: a 3D position and a scale, where scale 0 means "hidden". The demo builds the six shapes once, at start-up, from short formulas. Pick one below and read its recipe; then turn on show order to see how the formula numbers the balls — ball 0 to ball 127. That order matters more than it seems, as the next section shows.

Drag to turn the shape. Ball colors run through the rainbow with the index, as in the demo (hue = i / 128 + 0.05 t), so neighbours in the list are neighbours in color.
The sphere uses a well-known trick, the Fibonacci sphere: walk down from the north pole in 128 equal steps of height and turn by the golden angle π(3 − √5) ≈ 137.5° at each step. Because the golden angle is "the most irrational" turn, no two balls ever line up, and the points end up almost evenly spread. The word SPACE is not modelled at all: its ball positions are the lit pixels of the demo's own 8×8 font (§6), read on the CPU. Those glyphs have 137 lit pixels, more than 128 balls, so the code takes every second pixel: 69 balls spell the word and the other 59 shrink to nothing at the center.

3 · Morphing with a ripple

To morph from one shape to the next, ball i of the old shape simply flies to ball i of the new one. There is no matching of nearest points, no path planning: just a straight-line blend between two positions,

position = mix(a, b, m) = a + (b − a) · m

where m goes from 0 to 1. The scale is blended the same way, so balls that are hidden in one shape grow out of (or shrink into) the center. This works because the formulas all number their balls in orderly sweeps — top to bottom, around and around — so neighbours stay roughly neighbours and the cloud moves as a coherent swarm.

Two details make it feel alive. First, m is not a straight ramp but an ease in / ease out curve, smoothstep — slow start, fast middle, gentle landing — over two beats. Second, each ball starts a little after the previous one: ball i waits i × 0.006 beats. Ball 127 leaves 0.76 beats after ball 0, and because of the ordering that delay sweeps across the shape like a ripple.

mi = smoothstep(0.006 i, 2 + 0.006 i, beats into the shape)
Left: the morph, from the shape on the left list to the one on the right (default: helix to SPACE, as at beat 32 of the scene). Right: m over time for five balls; the dashed line is "now". Set the ripple to 0 and all curves coincide; raise it and watch the wave travel through the swarm. Turn off easing for a mechanical, constant-speed morph.

4 · Living on the beat

The tune runs at 125 beats per minute, so one beat lasts 0.48 s. The scene turns its local time t into a beat count and derives everything from it:

beat     = t · 125 / 60
current  = ⌊beat / 8⌋ mod 6            // which shape we are heading to
previous = (current + 5) mod 6         // the one we come from
into     = beat mod 8                  // beats since this shape began
first    = beat < 8                    // very first shape: no morph, just the sphere

The scene lasts 64 beats, eight slots of eight, so the six shapes play once and the first two play again. Because the credits start exactly on a musical section, the morphs land on the bars of the tune.

The throb. On each beat the balls swell by 30 % and relax: radius × (1 + 0.3 · pulse), with pulse = exp(−6 · secondsSinceBeat) — 1 on the beat, down to 1/e after a sixth of a second.

Facing the camera. The cluster normally spins around the vertical axis at 0.6 rad/s (yaw) and nods gently (pitch = 0.35 sin 0.4t). But a word is only readable from the front, so while it spells SPACE a facing factor blends both angles toward "front": yaw → 0.25 sin 0.5t (a slight sway), pitch → 0. Facing follows the same two-beat smoothstep as the morph, in on the way to SPACE and out on the way to the spiral.

There is one trap. By beat 32 the cluster has spun about 1.5 turns (0.6 t ≈ 9.2 rad), and blending straight to a front pose near 0 would whirl it back through all of them in under a second. Any whole number of turns looks the same, so the code moves the front pose instead: it adds turns · 2π, with turns = the whole number of turns nearest to the spin at the start of the ramp (the first beat of the shape). The cluster then only has to turn by the leftover, less than half a turn, and because turns is fixed for the whole ramp, the blend stays smooth.

spin      = 0.6 t                                   // left unwrapped
rampStart = time of this shape's first beat
turns     = round((0.6 · rampStart − 0.25 sin 0.5·rampStart) / 2π)
faceYaw   = 0.25 sin 0.5t + turns · 2π
yaw       = mix(spin, faceYaw, facing)
Top: the eight shape slots; the lighter part of each is the morph window (2 beats, plus 0.76 for the ripple). Pink: the yaw angle; cyan: facing (0 to 1); gold, bottom: the ball radius multiplier. Compare the three ways of handling the spin: the demo's (no visible jump), an earlier version (red marker), and no wrapping at all (a long whirl back to the front, and another one on the way out).
The earlier version wrapped the spin into [−π, π] with std::remainder. That also keeps the unwinding short, but the wrapped spin jumps from +π to −π every 10.5 s. While facing is 0 the jump is a whole turn and invisible; one of them, though, fell at t ≈ 15.71 s, inside the ramp toward SPACE where yaw is a blend, and there the jump is scaled by 1 − facing: the cluster snapped by about 110° in one frame. Choosing the turn count once, at the start of the ramp, removes it. (A whole-turn jump does still happen at beat 40, when the next shape's ramp picks its own count, but there facing is exactly 1, so the pose jumps by exactly 2π and nothing moves.)

5 · Glossy balls

The old bobs were painted shiny. The demo's balls are lit shiny: the fragment shader adds up four kinds of light for every pixel of every ball, using the surface normal N (the direction the surface faces) and the view direction V (toward the camera).

Left: one ball, computed per pixel with a JavaScript copy of ball.frag. Right: the whole studio environment, unrolled (left–right: all the way around; top: straight up). Hover over the ball to see where its reflection ray lands in the studio. Toggle the terms one at a time to see what each contributes.

6 · An 8×8 bitmap font

Home computers of the 1980s drew text from bitmap fonts: each character is a tiny grid of on/off pixels. The Commodore 64 and the IBM PC's CGA text mode both used 8×8 character cells, and demos drew their scrollers with fonts of the same kind — often painted with gradients and drop shadows, but still a fixed grid per letter. An 8×8 glyph is just 64 bits: eight bytes.

The demo carries its own font in src/core/BitmapFont.cpp: 51 glyphs (space, punctuation, digits and capital letters), each written in the C++ source as eight strings of eight characters, X for a lit pixel and . for an empty one. You can read the letters right in the code. The design uses two-pixel-wide strokes in a 7×7 area, leaving the bottom row and the right column empty so neighbouring letters never touch. A static_assert checks at compile time that every glyph is exactly 64 characters of X and ..

At start-up the glyphs are packed side by side into one small texture, an atlas 408 × 8 pixels: glyph g sits in columns 8g … 8g+7. Letters are looked up by a table that maps lower case to upper case and anything unknown to glyph 0, the space.


      
Click pixels to edit the selected glyph. The C++ literal updates as you go; the atlas strip below is the texture the shader reads (click it to pick a glyph). Your edits go straight into the live demo above — both the scroller and the SPACE shape of the balls, which is built from the same pixels. The font on this page was parsed from the C++ source listed at the bottom, so it is the demo's font, string for string.

7 · Text in a fragment shader

Classic scrollers copied letters into screen memory. A fragment shader works the other way around: it runs once for every screen pixel and has to answer one question — "is this pixel part of a letter, and if so which color?" The scroller draws one fullscreen triangle and answers it with a chain of simple divisions:

  1. Size. pix = max(1, floor(height / 220)) screen pixels per font pixel: 4 at 1080p. A text line is 12 font pixels tall (8 for the glyph, 4 of gap): 48 screen pixels.
  2. Which line? Measure the pixel's distance from the top, add the scroll offset (100 px/s at 1080p, scaled with the height), subtract the screen height so the text starts below the bottom edge, and divide by the line height. Outside the text: discard.
  3. Which font pixel? The remainder gives the row (0–7 inside the glyph). Each line is centered on a column at 72 % of the screen width, so its left edge is 0.72 · width − length · 8 · pix / 2, and the horizontal font pixel is (x − left) / pix.
  4. Which letter, and is it lit? Two texture reads (font.glsl): the text texture — one row per line, one texel per character, holding glyph numbers — gives the glyph at column p.x / 8; the atlas at (glyph · 8 + p.x mod 8, row) says on or off. The text texture is an integer texture (R8UI, read with usampler2D and texelFetch) so glyph numbers come back exact, never blended.
  5. Style. The fractional position inside the font pixel gives a thin darker bevel on its top and left (the 12 % nearest the edge: step(0.12, f)) — a chunky, LED-like look. Each line gets its own color from a cosine palette that drifts with time (§9), and the text fades in over the bottom 12 % of the screen and out over the top 15 %. The result is alpha-blended over the balls.
Hover over the image.
Hover to follow one pixel through the steps above; the boxes show its font pixel and its glyph cell. This figure is a JavaScript copy of scroller.frag run for each of the 294,000 pixels, with the column at 50 % so the text is centered. At the demo's 4 screen pixels per font pixel the bevel is at most one screen pixel wide and appears only where a pixel center falls inside it; raise pixel size to see it clearly.
Why not draw quads per letter, as most text renderers do? Because here the per-pixel approach is the whole effect in a dozen lines: no vertex buffers, no layout code, scrolling is one uniform, and a line's color, fade and bevel are just a few more lines in the same shader. Up to 48 lines fit (the length of each is passed in a uniform array); the credits use 34.

8 · The sine scroller

The credits greet "whoever wrote a sine scroller", and that is an honest tribute: the scene's own scroller is a straight, dignified credits roll. The sine scroller is its rowdy ancestor, a fixture of 1980s cracktros and demos on the C64 and the Amiga: a single line of text slides across the screen from right to left while every column of it is lifted by a sine wave, so the letters ride over hills that roll along on their own. Coders kept a precomputed sine table in memory, because computing a sine on a 6502 or a 68000 was far too slow; each column read its height from the table at an index advanced by its position and the frame counter.

In a fragment shader the same effect is one extra line: before looking up the font, offset the pixel's vertical position by a sine of its horizontal position. With the scroller's textPixel() it could look like this:


Not in the demo: a sine scroller drawn with the demo's font, scrolling the greetings. Amplitude and frequency shape the wave, wave speed makes it roll independently of the text. Switch to per character for the blockier look of scrollers that moved whole letters (cheaper on old hardware: one blit per letter instead of one per column).

9 · Copper bars and raster colors

The Amiga's display chips contained the Copper (coprocessor), part of the Agnus chip: a tiny processor running in step with the video beam. Its program — the copper list — has essentially two instructions: WAIT for the beam to reach a given position, and MOVE a value into a hardware register. Point the MOVE at a color register and the background color can change on every scanline, for free, while the CPU does something else. Stacking shaded horizontal stripes and moving them on sine curves gave the famous copper bars (or raster bars, as they were called on machines like the C64, where the same trick was done with interrupts timed to the raster beam). The original Amiga chipset could show 4,096 colors — 4 bits per channel — so every MOVE wrote a 12-bit color.

Hover over the image to read the copper list at that line.
Copper bars: one color per scanline, bars overwritten in order, colors rounded to 12 bits. Hover to see the WAIT/MOVE pair that would set that line. Switch to this scene's heirs to see what the credits kept of the idea.

What the scene kept. There are no copper bars in the credits, but two of their descendants. The backdrop's base color is a vertical gradient — mix(violet, deep blue, y / height) — the per-pixel version of the copper's per-line sky gradients. And the scroller gives each text line its own color from Inigo Quilez's cosine palette, 0.7 + 0.3 · cos(2π (0.07 · line + 0.05 t + (0, 0.33, 0.67))): the three channels are the same wave shifted by a third of a turn, so the colors walk around the rainbow from line to line and drift slowly over time — color per line, changing every frame, just as a copper list would.

The rest of the backdrop. Behind the gradient the shader adds a faint violet nebula (3D fractal noise, kept only where it is dense: smoothstep(0.5, 0.9, fbm)) and the demo's shared star sky — two layers of stars hashed into 3D cells on the celestial sphere, as explained on the other scene pages. The view direction is turned around the vertical axis at 0.03 rad/s, so the stars drift slowly behind the balls.

The algorithm, step by step

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

// CPU, once at start-up
ball  = icosphere(3)            // 642 vertices, 1280 triangles
shapes = [ fibonacciSphere, lattice 5³,
           torusKnot(2,3), doubleHelix,
           fontPixels("SPACE"), spiral ]
// each: 128 × (x, y, z, scale)
text  = R8UI texture of glyph numbers,
        one row per credits line
atlas = R8 texture 408 × 8, 51 glyphs
The sphere mesh, the six shapes (§2) and the two text textures (§6, §7) are built once. Every shape is a list of 128 slots; slots a shape doesn't need get scale 0 and sit at the center. The word is made of the font's own pixels, every second one.
// CPU, every frame
beat     = t · 125 / 60
current  = ⌊beat / 8⌋ mod 6
previous = (current + 5) mod 6
into     = beat mod 8;  first = beat < 8
morphAll = smoothstep(0, 2, into)
facing   = current == SPACE  ? morphAll
         : previous == SPACE ? 1 − morphAll : 0
turns = round((0.6·t₀ − 0.25 sin 0.5t₀) / 2π)
        // t₀ = this shape's first beat
yaw   = mix(0.6 t, 0.25 sin 0.5t + 2π·turns,
            facing)
pitch = mix(0.35 sin 0.4t, 0, facing)
pulse = 1 + 0.3 · exp(−6 · secondsSinceBeat)
Everything that changes over time comes from the beat counter (§4). facing turns the cluster to the camera while it spells the word; the front pose is shifted by whole turns, chosen once per ramp, so the turn to the front is short and smooth. pulse swells the balls on each beat.
// CPU, for each ball i = 0 … 127
m     = first ? 1 : smoothstep(0.006 i,
                         2 + 0.006 i, into)
s     = mix(shapes[previous][i],
            shapes[current][i], m)
pos   = (−1.6, 0.15, 0)
        + Rx(pitch) · Ry(yaw) · s.xyz · 1.3
r     = 0.09 · s.scale · pulse
hue   = i / 128 + 0.05 t
tint  = 0.55 + 0.45 cos 2π(hue + (0, .33, .67))
instance[i] = (translate(pos) · scale(r), tint)
The ripple morph (§3): each ball blends between its slot in the old and the new shape, starting 0.006 beats after the previous ball. The cluster sits left of the credits column. The 128 matrices and colors go to the GPU in one buffer.
// draw 1: backdrop, fullscreen
for each pixel:
    d   = Ry(0.03 t) · cameraRay(pixel)
    col = mix(violet, deepBlue, y / height)
        + nebula · smoothstep(0.5, 0.9, fbm(2d))
        + starSky(d)
A per-pixel vertical gradient — the copper sky's heir (§9) — with a faint nebula and the shared hashed star sky, slowly turning. No depth is written.
// draw 2: 128 instanced spheres, depth test on
vertex:   world = model[i] · vertex
          N = normalize(model[i] · normal)
fragment: col  = tint · (0.08 + 0.9 max(N·key, 0))
          col += specular(N, V, key, 0.15) · 0.9
          col += magenta · max(N·rim, 0)³ · 0.6
          col += studio(reflect(−V, N))
                 · mix(0.25, 1, fresnel)
One draw call for all balls (§1); the depth buffer replaces the sorting of the bob era. Each pixel adds key light, a Blinn–Phong highlight, a magenta rim and a Fresnel-weighted reflection of a made-up studio (§5).
// draw 3: scroller, fullscreen, alpha blend
for each pixel:
    pix      = max(1, ⌊height / 220⌋)
    scrolled = fromTop − height + scroll
    line     = ⌊scrolled / (12 · pix)⌋
    if outside the text: discard
    row  = (scrolled − line · 12 pix) / pix
    left = 0.72 · width − len[line] · 4 · pix
    p    = (⌊(x − left) / pix⌋, ⌊row⌋)
    g    = text[line][p.x / 8]
    if !atlas[g · 8 + p.x mod 8][p.y]: discard
    color = palette(0.07 line + 0.05 t) · 1.6
            · (bevel ? 0.7 : 1)
    alpha = fade(y) · (bevel ? 0.6 : 1)
Per-pixel text (§7): a few divisions find the line, the font pixel and the glyph; two texelFetch reads decide on or off (§6). Each line has its own drifting palette color (§9), and the text fades at the top and bottom edges.
// post (shared by all scenes)
bloom → chromatic aberration → ACES
→ vignette → grain → sRGB
The frame is rendered in 16-bit floating point; the demo's post chain then makes the highlights and the bright text glow. The live demo above uses a lighter stand-in: ACES tone mapping and a mip-map glow.

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 WebGL2 changes listed under the live demo); the font and the credits text in this page are parsed from the two C++ listings below.

src/scenes/credits/CreditsScene.cpp C++ · shapes, ripple morph, three passes
shaders/scenes/credits/background.frag fullscreen · gradient, nebula, stars
shaders/common/mesh.vert vertex shader · instanced meshes
shaders/scenes/credits/ball.frag fragment shader · glossy balls
shaders/scenes/credits/scroller.frag fullscreen · per-pixel text
shaders/common/font.glsl GLSL · two texel fetches
src/core/BitmapFont.cpp C++ · the 8×8 font as X/. strings
Helpers (excerpts) Motion.h · SyncState.h · Camera.h · color.glsl · lighting.glsl

Glossary

Demoscene
The computer art subculture, born in the 1980s on home computers like the C64, Amiga, Atari ST and PC, that makes real-time audiovisual "demos" to show off code, graphics and music.
Vector balls
A classic demo effect: shaded balls at the points of a rotating, morphing 3D shape.
Bob / blitter
On the Amiga, a bob (blitter object) is a bitmap copied onto the screen by the blitter, a chip that moves rectangular blocks of memory.
Painter's algorithm
Drawing objects from the farthest to the nearest so that nearer ones cover farther ones. Needs sorting, and fails for objects that intersect.
Depth buffer (z-buffer)
A per-pixel memory of the nearest depth drawn so far; a new fragment is kept only if it is nearer. Makes the draw order irrelevant for opaque objects.
Instancing
Drawing the same mesh many times in one call, each copy with its own per-instance data (here a matrix and a color).
Icosphere
A sphere mesh made by repeatedly splitting the 20 triangles of an icosahedron and pushing the new points onto the sphere; its triangles are nearly all the same size.
Golden angle / Fibonacci sphere
π(3 − √5) ≈ 137.5°. Turning by it at each step of a spiral spreads points almost evenly, on a sunflower head or on a sphere.
Torus knot
A closed curve wound around a doughnut; the (2, 3) torus knot is the trefoil knot.
mix / linear interpolation
mix(a, b, m) = a + (b − a) m: the point a fraction m of the way from a to b.
smoothstep / easing
An S-shaped 0-to-1 curve (3x² − 2x³) that starts and ends with zero speed, used to make motion ease in and out.
Beat / BPM
The pulse of the music; 125 beats per minute = 0.48 s per beat. The demo's timeline and morphs are measured in beats.
Bitmap font
A font stored as a grid of on/off pixels per character, here 8×8.
Texture atlas
Many small images (here 51 glyphs) packed into one texture, addressed by offset.
texelFetch / integer texture
Reading one exact texel by integer coordinates, with no filtering; integer textures (R8UI, usampler2D) return whole numbers such as glyph indices.
discard
A fragment shader statement that throws the current pixel away: nothing is written.
Sine scroller
A horizontally scrolling line of text whose columns or letters are displaced by a sine wave.
Copper / copper list
The Amiga's beam-synchronized coprocessor and its program of WAIT and MOVE instructions, used to change colors and other registers mid-screen.
Raster bars (copper bars)
Horizontal color bands made by changing the background color on every scanline.
Cosine palette
Inigo Quilez's a + b · cos(2π (c t + d)): a smooth color ramp from four vectors.
Normal
The unit vector perpendicular to a surface; it decides how light reflects there.
Lambert / diffuse
Matte lighting: brightness proportional to the cosine of the angle between the normal and the light.
Blinn–Phong specular
A highlight model using the half-vector between light and view; a higher exponent gives a smaller, sharper spot.
Rim light
A light from behind the subject that outlines its edge.
Fresnel / Schlick
Surfaces reflect more at grazing angles; Schlick's formula f0 + (1 − f0)(1 − cos θ)⁵ approximates it cheaply.
Environment reflection
Coloring a shiny surface with what its reflected view ray would see, here an analytic "studio".
Alpha blending
new · α + old · (1 − α): drawing a partly transparent layer over the image.
Uniform
A value set by the CPU before a draw and shared by every vertex and pixel of that draw (time, scroll, resolution…).
Vertex / fragment shader
GPU programs run once per vertex (placing it on screen) and once per covered pixel (coloring it).
HDR / tone mapping (ACES)
Keeping brightness above 1.0 while rendering, then compressing it into the screen's range with a filmic curve.
Bloom
Letting very bright areas glow into their neighbourhood.

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.