Made by AI
Capital ships in wedge formation above the planet
Space Travel · Interactive explainer · Scene 09

Fleet Jump-in

Twelve capital ships of a hundred thousand triangles each, dropping out of hyperspace one every other beat. One mesh, one draw call, a shadow map, and a stopwatch tied to the music — explained from first principles, with the real shaders running here in your browser.

The scene

The fleet lasts thirty-two beats — 15.36 seconds at 125 beats per minute. It opens with a white flash that hides the cut from the canyon run: we are in low orbit, a huge blue planet below, stars above, the sun low behind the camera. The sky is empty. Then, on the fourth beat, the first capital ship arrives: it shoots in from far behind, smeared along its length, brakes in a fraction of a second and settles into place with a blue streak and a flash of light. Every two beats another one follows, until twelve ships hold a wedge formation and the camera, drifting alongside, hands over to the next scene — the same dreadnought, up close.

The scene is almost pure reuse: the ship's procedural mesh, its hull shader and the planet sky all come from the dreadnought scene that follows. What is new is small but carries the whole effect: instancing (drawing twelve copies of one mesh with a single command), a few exponential curves that turn a position into a hyperspace exit, a fullscreen pass that paints the streaks and flashes, and a schedule that puts every arrival exactly on a beat of the music.

You don't need graphics experience to follow along. Each section introduces one concept, usually with a small interactive figure. Code appears in two forms: pseudo-code (plain logic) and the real GLSL and C++ of the demo.
In the demo timeline this entry is fleet 32b flash 1b post.bloom=0.2 post.threshold=2.0: 32 beats long, blended in from the canyon with a one-beat flash transition (an HDR white flash peaking exactly at the cut, so bloom flares the whole screen), and with two post-processing overrides that turn the bloom down — see §6 for why. The dreadnought then follows with a two-beat crossfade.

Live: the real shaders

This is the fleet scene, rebuilt in WebGL2. The ship is generated in JavaScript by a line-for-line port of the demo's model code (same shapes, same random seed), and drawn with the demo's own vertex, shadow, hull, sky and flash shaders. The per-frame logic — arrival times, braking, stretch, camera path, shadow cascades, projected flash positions — is a JavaScript mirror of FleetScene::render(). Drag the time slider to scrub: like every scene in the demo, each frame is a pure function of time.

Try: scrub to just after 1.92 s and step forward slowly — the first ship is stretched to 26 times its length and covers most of 2,000 units in a fraction of a second. Turn off the hyperspace stretch to see how much of the effect it carries. Move the sun azimuth and watch the tower's shadow sweep over the decks; turn shadows off to see how flat the hull becomes. Set beats between arrivals to 0.5 for a jump-in barrage.
What differs from the demo. WebGL2 has no storage buffers and no gl_BaseInstance, so the per-instance data reaches the vertex shader through instanced vertex attributes (§3) instead of an SSBO; the shadow map wraps with CLAMP_TO_EDGE because WebGL2 has no border color; the Poisson table in lighting.glsl gets an explicit array size. Everything else in the shaders is the demo's code. The post chain (13-tap bloom with a soft knee, chromatic aberration, vignette, grain) is replaced by a light stand-in: ACES tone mapping plus a glow with a hard threshold of 2.0 on blurred mip samples. The demo's soft knee starts admitting light above a brightness of 1.0 at the fleet's threshold of 2.0. The glow slider defaults to the fleet's bloom intensity of 0.2. The one-beat flash transition from the canyon is not reproduced. The stars of sky.glsl are sized in pixels, so here they are sized for the demo's 1080-line frame instead of this smaller canvas. The ship's random detail uses a JavaScript Mersenne Twister with the demo's seed; tiny floating-point differences between C++ and JavaScript can move an occasional block.

Background

Nine ideas make up the scene, from how the ship is built to how its light reaches the screen.

1 · A ship built from boxes

GPUs draw triangles. A 3D model — a mesh — is a list of corner points (vertices) plus a list saying which three vertices form each triangle (indices). Each vertex carries a position and a normal, the direction the surface faces, which lighting needs. The demo's vertices also carry a material id (hull, engine, dark recess, dome, window) and a baked occlusion value that darkens crevices.

No modelling program was used: the dreadnought is built by C++ code when the demo starts. Almost everything is made with one primitive, the loft: take a regular polygon, stretch it to fill a square, and pull it through a series of rings along the ship's length, each ring with its own size and offset. Two rings and four sides make a box; six sides make the main hull, with a ridge on top and bottom and short vertical side walls; twenty sides make an engine nozzle.

The polygon's corners start on a circle, rotated half a step so it has a flat bottom (dashed). Dividing by the largest x and y makes it touch all four sides of the square (solid), so a ring's scale is exactly its half width and half height. The hull rings are (z, half width, half height) = (−100, 55, 9), (−20, 33, 6.5), (60, 11, 2.8), (100, 0.6, 0.4): a dagger 200 units long, wide at the stern and nearly a point at the nose.

Try: 4 sides gives the box that most of the ship is made of; 6 gives the hull's cross-section.

Then come the detail layers, placed by rules and random numbers: a "city" of thousands of small blocks on the decks (denser in districts chosen by noise, with the occasional tower and long industrial rows, and only where all four corners of a block sit on the same level), armoured rims along every edge, turret batteries, rows of lit windows in the side walls, plates and fittings under the belly, the command tower with its bridge and twin domes, and radiator fins on the engine block. The random numbers come from a Mersenne Twister (Matsumoto and Nishimura, 1998) with a fixed seed, so every run builds exactly the same ship.

Finally each part is flattened: every triangle gets its own three vertices carrying the triangle's own normal. Shared vertices would average the normals and make the hull look soft and rounded; flat normals give the crisp facets of armour plating.

The real mesh, generated in your browser by the JavaScript port and drawn flat, triangle by triangle, colored by layer. Switch layers off to see what each one adds — and how many triangles it costs. The big forms are only a few thousand triangles; the detail is what makes it a city.

2 · Braking out of hyperspace

To put a mesh somewhere in the world, the GPU multiplies each vertex by a model matrix: a 4×4 table of numbers that can move (translate), turn (rotate) and resize (scale) the whole object in one operation. The ship is modelled around the origin with its nose along +z; the fleet only translates it to its position and scales it along z:

model = translate(position) · scale(1, 1, stretch)

The arrival is three exponential curves of s, the time since that ship's arrival moment:

approach = 2000 · e−7s     stretch = 1 + 25 · e−10s     position = slot − (0, 0, approach)

An exponential decay is the natural shape of braking: the speed is proportional to the remaining distance, so the ship is fastest at the start (14,000 units per second — the derivative of 2000·e−7s) and slows smoothly, never overshooting. After 0.3 s it is 245 units from its slot, after one second under 2 units. The stretch decays even faster: the ship starts 26 times longer than normal, a classic way to show the "snap" out of hyperspace, and is within 20 % of its true length after half a second. Because the scale is applied around the model's origin, the stretched ship reaches far ahead of its own position: at s = 0 its nose is already past the slot.

Top: one ship arriving at its slot, seen from the side, with the blue streak and the flash (§6). The pink dots are where it was at each 1/60 s frame: huge steps at first, then crowded together. Bottom: the four curves, each divided by its starting value so they share one axis.
The model matrix is the only thing that differs between the twelve ships — and the ship's shader reads it per instance, which is the subject of the next section. A small honesty note: mesh.vert transforms normals with mat3(model), which is exact only for rotations and uniform scales. During the stretch the normals tilt slightly, for a fraction of a second while the ship is a blur anyway.

3 · Twelve ships, one draw call

Every time the CPU asks the GPU to draw something it issues a draw call, and each call has a fixed cost: the driver validates state, the GPU switches context. Drawing twelve ships the obvious way means twelve calls per pass, each preceded by a uniform update with that ship's matrix. Instancing asks once: "draw this mesh n times". The GPU runs the vertex shader for every vertex of every copy, and tells it which copy it is working on through the built-in counter gl_InstanceID (0, 1, 2, …). Hardware instancing has been core OpenGL since version 3.1.

The per-copy data sits in a GPU buffer. The demo uses a shader storage buffer (SSBO) — a raw array of structs the shader can index freely — holding one Instance { mat4 model; vec4 color; } per ship: 16 + 4 floats, 80 bytes. Every frame the CPU writes the arrived ships into it and draws with glDrawElementsInstancedBaseInstance. The vertex shader picks its struct with instances[gl_BaseInstance + gl_InstanceID]; the base instance (core since OpenGL 4.2) lets other scenes keep several groups in one buffer and draw each slice separately. The fleet uses one group, so its base is 0.

Left: the commands each approach costs per frame (the scene draws the ships three times: two shadow cascades and the color pass). Right: the instance buffer, one row per arrived ship, 20 floats each. Hover over a row to read its matrix: the translation column moves the ship, the z-scale entry is the stretch.
In WebGL2 there are no storage buffers and no base instance. The browser version uses the classic alternative: instanced vertex attributes. The same 80-byte structs go into an ordinary vertex buffer, and five attributes (the four columns of the matrix and the color) are marked with vertexAttribDivisor(location, 1) — "advance once per instance, not once per vertex". The shader receives them as in mat4 iModel; in vec4 iColor;. Another option is to store the data in a floating-point texture and fetch it with texelFetch by gl_InstanceID; with twelve instances the attributes are simpler.

4 · On the beat

The demo's music runs at 125 beats per minute, so one beat lasts 60 / 125 = 0.48 seconds. Scene entries in the timeline are measured in beats and the fleet begins on beat 272, so the scene's local time t is aligned with the beat grid: t / 0.48 is the beat number inside the scene. Two timeline parameters set the schedule — first = 4 beats and every = 2 beats:

si = t − (first + i · every) · 0.48     ship i is drawn when si ≥ 0

Ship 0 appears at 1.92 s, ship 11 at beat 26 (12.48 s), leaving the last six beats for the whole formation to cruise together. Nothing is triggered or remembered: at any time t the code simply computes, for every ship, how long ago it arrived — which is why scrubbing works and why the flashes land exactly on the beat whatever the frame rate.

A second, subtler sync: the engines. Their glow is 15 · (1 + 0.3 · beatPulse), where beatPulse = e−5 · sinceBeat jumps to 1 on every beat and fades over a fraction of a second. The engine discs are HDR emitters (brightness 15 to 19.5), so the pulse reads as a throb through the bloom.

The whole scene on one line, in beats. Gold markers: arrivals. White: the sum of all flash intensities; cyan: the streaks; bottom: the engine glow pulsing on every beat. These sliders drive the live demo too — try every = 3 and watch the last ships miss the end of the scene.

5 · Camera and projection

The whole formation cruises along +z at 8 units per second (drift = 8t), and so does everything else that matters: the slots, the camera, its target and the shadow cascades. The camera starts at (1100, 150, 600) — off the formation's right flank, a little above it so the planet shows below — and glides to (950, 120, 300) along a smoothstep curve over the 15.36 seconds, always looking at (0, −80, −350) + drift, the middle of the wedge.

Two matrices turn world positions into the screen. The view matrix (glm::lookAt) re-expresses everything relative to the camera. The projection matrix (glm::perspective) performs the pinhole camera's division by depth (see the warp explainer, §1) and squeezes the visible region into a cube from −1 to 1, clip space. The camera's focal = 1.4 is the distance to an image plane one screen-height tall, so the vertical field of view is 2 · atan(0.5 / 1.4) ≈ 39°. Near and far planes sit at 1 and 30,000 units.

Seen from above. The wedge's slots (numbered), the arrival tracks of ships still to come (dotted, 2,000 units behind each slot), the camera's path (gold dashes) and its field of view at 16:9. The dashed circles are the two shadow cascades of §7.

6 · Streaks, flashes and bloom

The streak and the flash are not 3D objects. After the ships are drawn, a single fullscreen pass adds them on top with additive blending. For that, the CPU first projects two points of every arriving ship to pixel coordinates — the same math the GPU applies to vertices: multiply by the view-projection matrix, divide by w (the depth term), map −1…1 to 0…width and 0…height:

tail = project(position − (0, 0, 120 · stretch))     head = project(position + (0, 0, 100))

The pixel positions and two intensities per ship are passed to the shader as small uniform arrays. For every pixel and every ship, the shader measures the distance to the segment tail→head and adds a sharp core plus a wide, faint halo (2·e−0.8d + 0.15·e−0.08d); around the head it adds a round flash and a thin horizontal flare (e−0.4|y| · e−0.004|x|: fast falloff vertically, very slow horizontally), the classic anamorphic-lens streak. Intensities decay as 3·e−6s and 4·e−4s, so the streak is gone first and the flash lingers. Distances are measured in pixels of a 1080-line frame (scale = height / 1080), so the look does not depend on the resolution. If either end of the streak is behind the camera (w ≤ 0) the projection is meaningless, so that streak is skipped.

flashes.frag, evaluated in JavaScript for one ship. Drag the tail and the head; scrub the time since arrival. Right: the two falloff profiles — note the long, faint tails that make the glow feel large without a hard edge.

Why the bloom is turned down. The demo's post chain extracts bright light from the HDR frame using a threshold with a soft "knee", blurs it over six mip levels, and adds it back scaled by an intensity (the 13-tap downsample from Jimenez's 2014 Call of Duty talk). Brightness is the maximum RGB channel of each filtered sample. With the default knee width of half the threshold, contribution starts above half the threshold: 0.5 by default, 1.0 for the fleet, and 1.25 for the dreadnought. The default — threshold 1, intensity 0.5 — suits glowing nebulae, but a sunlit grey hull stays below about 0.9 and its specular highlights and windows go well above 1: the fine plating would dissolve into a haze. The fleet raises the threshold to 2 and lowers the intensity to 0.2, so bloom mainly emphasizes the engines, flashes and brightest windows, and the hull detail stays crisp. The dreadnought scene goes further still (2.5 and 0.15).

The soft threshold of bloom_down.frag: how much of a pixel's brightness goes into the bloom, times the bloom intensity. Gold bands mark typical values in this scene.

7 · Shadow maps

A point is in shadow if something lies between it and the sun. Testing that directly for every pixel against a hundred thousand triangles per ship is out of the question. Shadow mapping (Lance Williams, 1978) turns it into a lookup. First render the scene from the light's point of view, keeping only depth: each texel of this shadow map stores how far the light travels before it hits the first surface. Then, while shading a pixel from the camera, compute where that point falls in the shadow map and how far it is from the light. If it is farther than the stored depth, something closer to the light covers it: shadow.

The sun is a directional light (its rays are parallel), so the light's "camera" uses an orthographic projection, a box instead of a pyramid. fitDirectionalShadow fits the box around a sphere of the scene, extends it 400 units toward the sun so ships outside the sphere still cast shadows into it, and snaps it to whole texels so the shadows do not shimmer as the formation drifts. Hardware does the comparison: the map is a depth texture with compare mode on, read as sampler2DArrayShadow, and each lookup returns the lit fraction, already averaged over 2×2 texels.

A shadow map in 2D: a side cut through decks, tiers and the tower. The strip at the top left is the map: each texel shoots one ray along the light and stores the depth of the first hit (brighter = nearer). Gold surfaces pass the test, violet ones fail it (shadow), grey ones face away from the light. Try: turn the normal offset off and the bias to 0 — the sunlit decks break into stripes, "shadow acne", because a texel's single depth cannot describe a surface that slopes away from the light inside it. Then lower the texel count to see blocky shadows, and widen the filter to soften them.

The details the demo uses, each fixing one artefact:

8 · Painting metal

The hull's fragment shader decides the color of every pixel of every ship. It first looks at the vertex's material: engine discs (material 1) simply glow blue, with a tiny flicker; windows (4) glow warm or, for 15 % of them, cold, with about one in eight left dark. Everything else is metal, shaded by a short sum of light contributions:

The variation that makes it read as armour plating is procedural too. The shader picks the two coordinates of the plane the surface faces most (planar mapping) and cuts them into rectangular plates: regions of 28 units each pick their own plate size and orientation from a hash, some plates are split in two, and each plate gets its own hash that sets its tone and roughness. The distance to the nearest plate edge gives dark seams; a sparse grid of raised blocks adds small bumps; low-frequency noise adds weathering and grime streaks along the hull.

Seams and blocks are not geometry: they are a height that tilts the normal, bump mapping (Blinn, 1978). Because the mesh has no texture coordinates, the demo uses Morten Mikkelsen's 2010 method, which needs only the screen-space derivatives dFdx/dFdy — the difference of a value between a pixel and its neighbour — of the position and the height.

A patch of top deck, seen from straight above, computed with JavaScript copies of panel() and blocks(). The bump normal here is also a finite difference between neighbouring samples, just as dFdx is. Try: a grazing sun (elevation around 5–10°) makes the seams and blocks pop; hover to read a plate's hash and roughness.
The plating is computed from the object-space position vObjectPos — before the model matrix — so it sticks to the ship. During the arrival stretch it is the plating that gets stretched, not a fresh pattern on a longer hull.

9 · The planet below

Behind the ships, a fullscreen pass paints the sky: for each pixel, a ray from the camera through that pixel. Stars come from hashed 3D cells around the view direction (two layers, sparse bright and dense faint), plus the sun's disc and halo. The planet is a sphere of radius 20,000 whose surface lies 450 units below the formation's origin, and the question "does this ray hit it?" has an exact answer. A ray ro + t·rd meets the sphere where its distance to the centre equals the radius; writing that out gives a quadratic equation in t:

t² + 2b·t + (|oc|² − R²) = 0    with   oc = ro − centre,   b = oc · rd

Its discriminant h = b² − (|oc|² − R²) decides everything: if h > 0 and the nearer root −b − √h is in front of the camera, the ray hits, and the surface point and its normal follow. Then the shader paints oceans and continents from fractal noise, sun glint on the water, drifting clouds, city lights on the night side and blue haze toward the edge. If the ray misses but passes over the planet (−b > 0), its closest approach to the centre gives the altitude at which it skims the atmosphere, and a glow of e−altitude/60 draws the thin bright line above the horizon.

At true scale, the camera's surroundings seen from the side. Blue rays hit the planet, grey ones miss. The selected ray shows the quadratic's numbers. Even 640 units up, the horizon is about 14° below level — that is why the planet's limb curves so visibly in the captures.

The algorithm, step by step

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

// once, at start-up
ship = makeDreadnought()   // lofts, boxes, seed 20261017
upload ship as one vertex + index buffer
shadowMap = depth array 2048², 2 layers
The mesh is built procedurally and flattened for crisp facets (§1): about 100,000 triangles. The shadow map is a depth texture array with hardware comparison (§7).
// CPU, every frame
beat   = 60 / 125                 // 0.48 s
s      = smoothstep(0, 1, t / duration)
drift  = (0, 0, 8 · t)
camera = lookAt(mix(A, B, s) + drift,
                (0, −80, −350) + drift, focal 1.4)
viewProj = perspective(39.3°, aspect, 1, 30000)
           · view(camera)
L = sunDirection(azimuth 70°, elevation 18°)
The camera glides between two points along a smoothstep while the whole formation cruises along z (§5).
for i in 0..11:
    sᵢ = t − (first + i · every) · beat
    if sᵢ < 0: continue          // not yet
    pos     = slot[i] + drift − (0, 0, 2000·e^(−7sᵢ))
    stretch = 1 + 25 · e^(−10sᵢ)
    instances += { translate(pos) · scale(1, 1, stretch),
                   color (1, 1, 1, 1) }
    tail = project(pos − (0, 0, 120 · stretch))
    head = project(pos + (0, 0, 100))
    streak[i] = both in front ? 3·e^(−6sᵢ) : 0
    flash[i]  = head in front ? 4·e^(−4sᵢ) : 0
Arrivals are scheduled on the beat (§4). Exponential braking and stretch (§2) become one model matrix per ship. The streak's ends and the flash are projected to pixels on the CPU (§6).
cascade c = sphere((0, −40, −420) + drift,
                   c == 0 ? 700 : 1400)
lightMatrix[c] = ortho box around the sphere,
                 +400 toward the sun, texel-snapped
upload instances  // visible × 80 bytes
Two nested cascades around the formation (§7). The instance buffer gets only the ships that have arrived (§3).
// pass 1: shadow maps
polygonOffset(2, 4)
for c in 0..1:
    clear depth layer c
    drawInstanced(ship, visible)
        gl_Position = lightMatrix[c]
                      · instance.model · vertex
Depth only, from the sun's point of view, all ships in one call per cascade (§3, §7).
// pass 2: sky, per pixel (no depth test)
rd  = cameraRay(pixel)
col = stars(rd) + sun disc and halo
if ray hits planet:
    col = oceans, land, clouds, glint,
          night lights, limb haze
else if ray passes over it:
    col += blue · e^(−altitude / 60) · daySide
A fullscreen ray per pixel: hashed star cells, then a ray-sphere test against the planet (§9).
// pass 3: hulls, drawInstanced(ship, visible)
// vertex shader
inst      = instances[gl_BaseInstance + gl_InstanceID]
world     = inst.model · position
normal    = mat3(inst.model) · normal
objectPos = position              // for the plating
Each vertex reads its ship's matrix by instance index (§3). The WebGL port reads it from instanced attributes instead.
// fragment shader
if engine: return blue · 15·(1 + 0.3·beatPulse)
if window: return warm/cold · 2.5 (some dark)
plate, seam = panel(planarUV(objectPos))
N     = bump(normal, 0.04·seam + blocks)
albedo = grey · plate tone · seam · weathering
shadow = 12-tap PCF in cascade(world)
col  = albedo · sun · N·L · shadow
     + sun · specular · fresnel · shadow
     + albedo · (planet bounce + ambient)
     + environment reflection
Emissive materials first, then metal: procedural plates and seams (§8), shadow lookup with normal offset and a rotated Poisson disc (§7), and a sum of diffuse, specular, fill and reflection.
// pass 4: flashes, fullscreen, additive
for i in 0..11:
    d   = distance(pixel, segment(tail, head))
    col += blue · streak[i]
           · (2·e^(−0.8d) + 0.15·e^(−0.08d))
    r   = distance(pixel, head)
    col += white · flash[i]
           · (1.5·e^(−0.05r) + 0.2·e^(−0.01r))
    col += blue · flash[i] · horizontalFlare
Screen-space glows summed per pixel, in 1080p pixel units (§6).
// post chain, with this entry's overrides
bloom  threshold 2.0, intensity 0.2
→ chromatic aberration → ACES → vignette
→ grain → sRGB
Only true light sources bloom, so the hull detail stays readable (§6).

The real source

These are the files as they are in the demo's repository. The browser version above uses the same shader code, with the changes listed under the live demo.

src/scenes/fleet/FleetScene.cpp C++ · the whole scene
shaders/scenes/fleet/flashes.frag fullscreen · additive
shaders/common/mesh.vert vertex shader · instanced
shaders/common/shadow.vert + shadow.frag depth only
shaders/scenes/dreadnought/hull.frag fragment shader · metal
shaders/common/lighting.glsl shadows · bump · specular
shaders/scenes/dreadnought/sky.frag fullscreen · planet and stars
shaders/common/sky.glsl star layers
src/scenes/dreadnought/DreadnoughtModel.cpp C++ · the procedural ship
src/gfx/MeshData.cpp — makeLoft(), flattened() C++ · excerpt
src/gfx/ShadowMap.cpp — fitDirectionalShadow() C++ · excerpt

Glossary

Mesh, vertex, index
A 3D model as a list of corner points (vertices) and a list of triples of their numbers (indices), one triple per triangle.
Normal
The direction a surface faces, stored per vertex; lighting compares it with the light and view directions.
Loft
A shape made by pulling a cross-section through rings along an axis; with two rings and four sides it is a box.
Flat shading
Giving each triangle its own normal (no shared vertices), for crisp faceted surfaces.
Model / view / projection matrix
4×4 matrices that place an object in the world, express the world relative to the camera, and apply perspective.
Clip space
The coordinates after projection; dividing by w gives positions from −1 to 1 across the screen.
Draw call
One request from the CPU to the GPU to draw something. Each has a fixed overhead.
Instancing
Drawing many copies of one mesh with a single draw call; the shader tells them apart by gl_InstanceID.
SSBO (shader storage buffer)
A GPU buffer the shader can read as an array of structs. The demo keeps its per-instance data in one; WebGL2 has none.
gl_BaseInstance
The first instance of a draw within the buffer, so several draws can share one buffer. Not available in WebGL2.
Vertex attribute divisor
Makes a vertex attribute advance once per instance instead of once per vertex — WebGL's way of feeding instance data.
Uniform
A value set by the CPU before a draw and shared by every vertex and pixel of that draw.
Beat, BPM
The music's pulse; at 125 beats per minute one beat lasts 0.48 s. The demo measures scenes and events in beats.
Exponential decay
e−kt: drops fast, then ever more slowly; the shape of braking, fading flashes and beat pulses.
Additive blending
Adding a new color to the existing pixel instead of replacing it — how light combines.
Shadow map
A depth image rendered from the light; a point is in shadow if it is farther from the light than the depth stored there.
Shadow acne, bias, normal offset
Stripes of false self-shadowing on lit surfaces, and the small depth and position offsets that cure them.
PCF
Percentage-closer filtering: averaging several shadow comparisons for soft shadow edges.
Cascades
Several shadow maps of increasing size, so nearby shadows get fine texels and distant ones are still covered.
Lambert diffuse
Matte reflection proportional to N·L, the cosine of the angle to the light.
Blinn-Phong specular
A highlight computed from the half-way vector between light and view directions; rougher surfaces give wider, dimmer highlights.
Fresnel
Surfaces reflect more at grazing angles; Schlick's formula approximates it cheaply.
Bump mapping
Tilting the normal according to a height pattern so a flat surface shades as if it had relief.
Planar mapping
Using two of the three position coordinates as texture coordinates, chosen by the direction the surface faces.
Hash
A deterministic scrambling function: same input, same "random-looking" output. Used instead of stored random numbers.
Ray-sphere intersection
Solving a quadratic to find where a ray enters a sphere, or how close it passes.
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, threshold, knee
The glow around bright areas; the soft knee admits some light below the nominal threshold, beginning above threshold minus knee width.

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: 08 · Canyon run · All scenes · Next: 10 · Dreadnought →