Color grading and look development on the GPU
Learning objectives
- 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
Capstone — one whole task that evidences the objectives
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.
Prerequisite modules
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
Supporting — enrichment, not gating
Part of curricula
- Live Visualist — zero to performing live-coded & generative visuals — Reactive & procedural — make it listen, and go to the GPU recommended
- Shader Artist — real-time GPU craft to a demoscene-grade visual — Procedural fields and the color look required
Unlocks — modules that require this one