home/ modules/ shaping-procedural-color-and-look

Color grading and look development on the GPU

  • learner can build smooth procedural palettes with cosine-RGB and HSB polar mapping
  • learner can apply gamma correction from the start of a look-dev workflow
  • learner can grade output with pow contrast and S-curve for a filmic look
  • learner can drive a start-saturated look-dev workflow, pushing hues past comfortable and pulling back with fresh eyes

Develop the color look for a procedural shader scene: build a cosine-RGB palette, render an HSB color wheel, and grade the final image with gamma, pow-contrast, and an S-curve, documenting a start-saturated workflow.

In a live-coded visual set, color is the fastest lever you have: the same raymarched blob reads as toxic rave acid or warm ambient dusk depending purely on palette and grade. This module builds the whole task of look development — taking an already-animated procedural scene from your VJ rig and giving it an intentional, performance-ready color identity, the way a colorist grades a film after the edit is locked.

The arc starts supported. First, generate color from a single scalar using three phase-offset cosine waves — the atom on cosine-RGB palettes is your JIT pointer for what a, b, c, d each do, and interactive palette pickers keep this first exercise low-stakes. Next, prove you understand color spaces by rendering an HSB color wheel via atan and length, following “Mapping HSB to polar coordinates” step by step. Then the grading chain: turn on gamma correction immediately (per “Gamma correction must be applied from the start”), add a mild pow-contrast to separate dark detail, and finish with a smoothstep S-curve for the filmic snap. The capstone strips the supports: you develop a complete look on your own scene and write up a start-saturated workflow — pushing hues past comfortable on day one and pulling back with fresh eyes, per IQ’s principle.

Each required atom gates a capstone deliverable: no cosine formula, no palette; no polar mapping, no wheel; no gamma/pow/S-curve, no grade; no start-saturated principle, no documented workflow. The supporting atoms enrich rather than gate — mix() gives you an easier interpolation fallback, and the grouping-over-scatter principle previews how your graded palette will sit in a fuller composition.

Walkthrough

Colour is the fastest lever in a set — the same shape reads as rave-acid or ambient-dusk purely on palette and grade. Paste each into The Book of Shaders editor. Each is a complete shader.

1 — a cosine palette (Iñigo Quílez’s trick). Generate a whole colour ramp from one scalar with three phase-offset cosines: a + b*cos(2π(c*t + d)). a is the mid brightness, b the contrast, c the cycles per channel, d the per-channel phase — the most useful one-liner in shader colour ([[color-palette-cosine-rgb]]).

precision mediump float;
uniform vec2 u_resolution;
vec3 palette(float t) {
  return vec3(0.5) + vec3(0.5) * cos(6.28318 * (vec3(1.0, 1.0, 1.0) * t + vec3(0.0, 0.33, 0.67)));
}
void main() {
  float t = gl_FragCoord.x / u_resolution.x;   // sweep left→right
  gl_FragColor = vec4(palette(t), 1.0);
}

2 — an HSB colour wheel (polar mapping). Prove colour-space fluency: map screen position to hue via atan(y, x) (angle) and saturation via length (radius), then convert HSB→RGB. The wheel is the classic diagnostic ([[glsl-hsb-polar-coordinates]]).

precision mediump float;
uniform vec2 u_resolution;
vec3 hsb2rgb(vec3 c) {
  vec3 rgb = clamp(abs(mod(c.x * 6.0 + vec3(0.0, 4.0, 2.0), 6.0) - 3.0) - 1.0, 0.0, 1.0);
  return c.z * mix(vec3(1.0), rgb, c.y);
}
void main() {
  vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution) / u_resolution.y;
  float hue = atan(uv.y, uv.x) / 6.28318 + 0.5;   // angle → hue
  float sat = length(uv) * 2.0;                     // radius → saturation
  gl_FragColor = vec4(hsb2rgb(vec3(hue, sat, 1.0)), 1.0);
}

3 — gamma correction (turn it on from the start). Monitors are non-linear; do maths in linear light, then pow(col, 1.0/2.2) on the way out or your midtones read muddy. Compare the ramp with and without — gamma is not optional polish, it’s correctness ([[gamma-correction-workflow]]).

precision mediump float;
uniform vec2 u_resolution;
void main() {
  vec2 uv = gl_FragCoord.xy / u_resolution;
  vec3 col = vec3(uv.x);                        // linear ramp
  if (uv.y > 0.5) col = pow(col, vec3(1.0 / 2.2));  // top half: gamma-corrected
  gl_FragColor = vec4(col, 1.0);
}

4 — pow-contrast (separate the darks). Raising a 0..1 value to a power >1 pushes lows down while keeping highs — it opens up dark detail and adds punch without clipping ([[pow-contrast-enhancement]]).

precision mediump float;
uniform vec2 u_resolution;
void main() {
  vec2 uv = gl_FragCoord.xy / u_resolution;
  float v = uv.x;
  if (uv.y > 0.5) v = pow(v, 2.2);              // top half: contrast-boosted
  gl_FragColor = vec4(vec3(v), 1.0);
}

5 — an S-curve for filmic snap. smoothstep(0.0, 1.0, v) is a gentle S — it darkens shadows and brightens highlights around the midpoint, the “film” contrast a colourist reaches for ([[s-curve-contrast]]).

precision mediump float;
uniform vec2 u_resolution;
void main() {
  vec2 uv = gl_FragCoord.xy / u_resolution;
  float v = uv.x;
  if (uv.y > 0.5) v = smoothstep(0.1, 0.9, v);  // top half: S-curve graded
  gl_FragColor = vec4(vec3(v), 1.0);
}

6 — a full look: palette + graded scene (the capstone). Grade a procedural field end to end: drive a cosine palette with an animated pattern value, then run the whole grading chain — gamma, pow-contrast, S-curve — for a performance-ready look. Start over-saturated and pull back with fresh eyes (IQ’s principle):

precision mediump float;
uniform vec2 u_resolution;
uniform float u_time;
vec3 palette(float t) {
  return vec3(0.5) + vec3(0.5) * cos(6.28318 * (vec3(1.0) * t + vec3(0.0, 0.15, 0.5)));
}
void main() {
  vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution) / u_resolution.y;
  float pattern = sin(length(uv) * 8.0 - u_time) * 0.5 + 0.5;   // animated field
  vec3 col = palette(pattern + 0.2);
  col = pow(col, vec3(1.3));                    // pow-contrast (separate darks)
  col = smoothstep(vec3(0.0), vec3(1.0), col);  // S-curve (filmic snap)
  col = pow(col, vec3(1.0 / 2.2));              // gamma (last, on the way out)
  gl_FragColor = vec4(col, 1.0);
}

What good looks like. A frame with an identity — a coherent palette (the cosine ramp, not random hues) and a graded contrast that reads as intentional, not flat or crushed. The grade order matters: work in linear, grade, and apply gamma last. If it looks muddy, you skipped gamma; if it looks harsh, your pow/S-curve is too strong; if the colours clash, your cosine d phases are fighting — nudge them. Start saturated and pull back tomorrow. (Skill map: live-visualist Domain B2 — procedural colour and grading.)

Now make it yours. Change the cosine d phases for a totally new palette (try vec3(0.0, 0.1, 0.2) for warm). Tint shadows and highlights different hues (split-toning). Drive the palette input with a raymarched scene’s depth. Ease the S-curve harder for high-contrast rave; soften it for ambient.

Runnable examples

Generated from the context/ instrument corpus by concept (redistributable idioms only). Do not edit — regenerate with gen-module-examples.mjs.

noise-field

noise(4, 0.1).out()

hydra-0002 · CC0-1.0

float h21(vec2 p){return fract(sin(dot(p,vec2(12.9898,78.233)))*43758.5453);}

glsl-0013 · public-domain

palette-cycle

osc(30, 0.1, 1).colorama(0.1).out()

hydra-0015 · CC0-1.0

hsvrgb [fract (ft/6.28 + 0.1*time), 1, 1] >> rgb

punctual-0024 · CC0-1.0

polar-warp

float r = length(uv); float a = atan(uv.y, uv.x);

glsl-0008 · public-domain

[rtx [fr, ft + 0.2*time], rty [fr, ft], 0.5] >> rgb

punctual-0032 · CC0-1.0

gamma-correction

col = pow(col, vec3(1.0/2.2));

glsl-0021 · public-domain

pow ([lo,mid,hi]) 0.4545 >> rgb

punctual-0034 · CC0-1.0

outline-stroke

(circle 0 0.42 - circle 0 0.38) >> add

punctual-0019 · CC0-1.0

value-contrast

stroke(255); fill(0); rect(0, 0, w, h)

p5live-0027 · CC0-1.0

Atoms in this module

Required — these gate the capstone

Three phase-offset cosine waves generate smooth procedural color palettes
Concept L2 First instrument G
Mapping HSB to polar coordinates with atan and length renders a color wheel
Procedure L2 First instrument GL
Gamma correction must be applied from the start of shader development, not added at the end
Principle L2 First instrument G
Applying pow() with a value slightly above 1.0 to the final color enhances contrast by darkening shadows without affecting highlights
Concept L3 Craft G
Applying a smooth-step S-curve to output color increases contrast and creates a filmic look
Concept L3 Craft G
Starting with over-saturated colors and pulling back is more reliable than building up from grey
Principle L3 Craft G

Supporting — enrichment, not gating

GLSL's mix() linearly interpolates between two colors by a 0.0–1.0 factor
Concept L1 Foundations GL
Grouping scene elements rather than distributing them uniformly prevents visual noise and creates organic set design
Principle L3 Craft GL
Value contrast (light vs dark) is the strongest visual cue, outranking saturation and hue
Principle L2 First instrument LHG
Geometric visuals are built on precision — outline-stroke shapes, high value-contrast, no noise, on a flat ground
Concept L2 First instrument HG
A near-monochrome ground with one saturated accent — letting contrast do the work, not hue variety — is the geometric palette recipe
Principle L2 First instrument HGL
Classic fractal coloring maps iteration count or distance to a cycled multi-stop ramp with a dark base so bright filaments glow
Concept L2 First instrument HG