shadertown

Parameters

The uniform ABI every shader in the catalogue shares.

The struct

struct Params {
  resolution : vec2f,   // physical pixels
  pointer    : vec2f,   // normalised, top-left origin
  time       : f32,     // seconds, per renderer
  speed      : f32,
  scale      : f32,
  intensity  : f32,
  vibrancy   : f32,
  grain      : f32,
  vignette   : f32,
  warp       : f32,
  a          : f32,     // shader-specific
  b          : f32,
  c          : f32,
  d          : f32,
  quality    : f32,     // 1 on the High tier, 0 on Low
  e          : f32,
  f          : f32,
  g          : f32,
  colorA     : vec4f,
  colorB     : vec4f,
  colorC     : vec4f,
  h          : f32,
  i          : f32,
  j          : f32,
  k          : f32,
  l          : f32,
  m          : f32,
  n          : f32,
}

160 bytes, one binding. Every shader uses this same layout, so the runtime can swap the shader behind a canvas without rebuilding a bind group.

Shared controls

ControlRangeWhat it does
speed0 – 4Time multiplier. 0 freezes the current frame
scale0.1 – 6Zoom of the underlying field
intensity0 – 2.5Brightness before the post chain
vibrancy0 – 2Saturation. 0 greyscale, 1 as authored
grain0 – 0.35Per-pixel noise; a little hides banding
vignette0 – 1Edge darkening
warp0 – 2.5Domain warp, or whatever the shader's strongest control is. Not every shader has one

Those are the widest bounds in the catalogue. Ranges and defaults are set per shader. Plasma's scale stops at 2 where Julia's goes to 6, because the two mean different things by "zoom". Read the real numbers off the registry rather than assuming the envelope applies to the shader in front of you.

Named controls

The fourteen generic slots are named by each shader. Aurora uses exposure, bloom and stars; Voronoi calls them jitter, edge, depth and drift. You always write the name, never the slot.

import { shaderRegistry } from "shadertown";

shaderRegistry.aurora.controls.exposure;
// { slot: "a", label: "Exposure", min: 0.2, max: 3, step: 0.01, default: 1.2, hint: … }

The registry is plain data, so building your own control panel is a map over Object.entries(definition.controls), which is exactly what the detail pages on this site do.

Colours

Three colours, given as hex strings and passed through to WGSL in sRGB with no linearisation. What you pick in the colour field is what the shader receives.

st_palette(t) interpolates colorA → colorB → colorC for t in [0, 1]; st_paletteCyclic(t) wraps, for shaders that cycle.

Quality

quality is the tier the runtime is drawing at: 1 on High, 0 on Low. A shader does not read it directly. The prelude turns it into an iteration count:

let steps = st_steps(96);   // 96 on High, 48 on Low, never under 3
for (var i = 0; i < steps; i = i + 1) { … }

Give st_steps the bound of every loop whose length is price rather than picture — march steps, noise octaves, blur taps, rays. st_fbm2, st_fbm3, st_ridge2 and st_srcBlur already do. A loop of three curtains or five layers is the picture and keeps its literal. Where a shorter march would stop short of what it has to reach, st_low() says which tier this is, so the step can grow to cover the ground: Tunnel relaxes its sphere trace and Hills doubles its stride on Low.

The post chain

Every shader returns through st_post, which applies vibrancy, grain and vignette in that order. That is why those three behave identically across all seventy-seven shaders, and why you can tune them once and reuse them.