home/ modules/ building-glsl-color-and-coordinate-toolkit

GLSL fundamentals: coordinates, vectors, and color output

  • learner can use swizzling, component aliases, and correct float literals to manipulate GLSL vectors
  • learner can correct aspect ratio and normalize to clip space so shaders are resolution-independent
  • learner can pass CPU uniforms to the GPU and manage clamped color output channels
  • learner can use length() to build distance-from-origin radial gradients

Build a resolution-independent GLSL sketch that draws an aspect-corrected, clip-space-centered radial gradient wheel with length()-based falloff, using uniforms, swizzles, and clamped color output.

When you project shaders behind a live set — a Hydra patch, a Shadertoy port, a custom fragment stage in your VJ rig — the venue decides your resolution. A sketch that looks right on your 16:9 laptop will smear into ellipses on a square LED wall unless coordinates are handled correctly from line one. This module builds that reflex: a small toolkit of vector, coordinate, and color moves that every later shader module assumes.

The arc starts supported. First, manipulate vectors in a working scaffold: component aliases (.xyzw / .rgba are the same storage), swizzling to reorder channels in one expression, and the strict-typing gotcha that every float literal needs a decimal point — the single most common compile error for anyone arriving from JavaScript. Next, take over the opening lines of the shader yourself: normalize pixel coordinates to [0,1], recenter to clip space [-1,1], and apply the one-line aspect-ratio correction so circles stay circular. Then wire the CPU side: declare uniforms for resolution and time, and learn why colors saturate — every output channel clamps to [0,1], so anything unnormalized flattens to white. Finally, put the corrected coordinates to work: length(uv) gives each pixel its distance from center, the seed of every radial gradient and circular shape.

The capstone removes the scaffold: from a blank file, draw a radial gradient wheel that stays centered and round at any canvas size. Every required atom gates it — the wheel’s falloff is literally length()‘s distance-from-origin value, its roundness is the aspect correction, its portability is the uniform-fed normalization, and its smooth ramp depends on respecting channel clamping. Supporting atoms enrich rather than gate: the Shadertoy-flavored take on normalized camera coordinates (the same clip-space move seen from a second angle), the vertex/fragment pipeline picture, function argument qualifiers, the pow()-of-negative trap, and mix()-based color blending all deepen the toolkit but the wheel ships without them. Drill the normalization one-liner, float literals, and swizzles until they are automatic — they open virtually every shader you will ever write live.

Walkthrough

The venue picks your resolution, so coordinates must be handled right from line one. This lesson is the toolkit every later shader assumes: vectors, coordinate spaces, and length(). Paste each into The Book of Shaders editor. Each is a complete shader.

1 — vectors, aliases, and swizzles. A vec3 stores three floats; .xyz and .rgb are the same storage seen two ways. Swizzling reorders channels in one expression. And the JS-refugee gotcha: every float literal needs a decimal1.0, never 1 ([[glsl-vector-component-aliases]], [[glsl-swizzle]], [[glsl-float-decimal-requirement]]).

precision mediump float;
void main() {
  vec3 c = vec3(0.9, 0.3, 0.1);   // r, g, b
  gl_FragColor = vec4(c.bgr, 1.0); // swizzle: b→r, g→g, r→b (colour flips)
}

2 — normalize, then centre to clip space. UV [0,1] is fine for gradients, but shapes want the origin in the middle: uv * 2.0 - 1.0 remaps to [-1, 1] with (0,0) at centre ([[uv-normalization-clip-space]] if present).

precision mediump float;
uniform vec2 u_resolution;
void main() {
  vec2 uv = gl_FragCoord.xy / u_resolution;   // [0,1]
  uv = uv * 2.0 - 1.0;                         // [-1,1], centred
  gl_FragColor = vec4(uv * 0.5 + 0.5, 0.0, 1.0);
}

3 — distance from centre with length(). length(uv) is each pixel’s distance from the origin — 0 at centre, growing outward. That single value is the seed of every radial gradient and circle ([[length-distance-from-center]] if present).

precision mediump float;
uniform vec2 u_resolution;
void main() {
  vec2 uv = gl_FragCoord.xy / u_resolution * 2.0 - 1.0;
  float d = length(uv);            // distance from centre
  gl_FragColor = vec4(vec3(d), 1.0); // black centre → white edges
}

4 — fix the squash: aspect-ratio correction. On a non-square canvas that “circle” is an ellipse, because x and y span different pixel counts. Multiply x by the aspect ratio and roundness is restored — the reflex that saves you on a square LED wall ([[glsl-aspect-ratio-correction]]).

precision mediump float;
uniform vec2 u_resolution;
void main() {
  vec2 uv = gl_FragCoord.xy / u_resolution * 2.0 - 1.0;
  uv.x *= u_resolution.x / u_resolution.y;   // correct for aspect
  float d = length(uv);
  gl_FragColor = vec4(vec3(d), 1.0);
}

5 — colours clamp to [0,1]. Every output channel saturates at 1.0, so an unnormalized value flattens to white. Invert the distance for a bright centre, and clamp (or a bounded expression) keeps the ramp smooth instead of blowing out ([[color-channel-clamping]] if present).

precision mediump float;
uniform vec2 u_resolution;
void main() {
  vec2 uv = gl_FragCoord.xy / u_resolution * 2.0 - 1.0;
  uv.x *= u_resolution.x / u_resolution.y;
  float glow = clamp(1.0 - length(uv), 0.0, 1.0);  // bright centre, bounded
  gl_FragColor = vec4(vec3(glow), 1.0);
}

6 — the radial gradient wheel (the capstone). Everything at once: uniform-fed normalization (portable), clip-space centre, aspect correction (round), length() falloff, and a coloured, clamped ramp via swizzles. It stays centred and circular at any resolution:

precision mediump float;
uniform vec2 u_resolution;
void main() {
  vec2 uv = gl_FragCoord.xy / u_resolution * 2.0 - 1.0;
  uv.x *= u_resolution.x / u_resolution.y;      // aspect-correct → round
  float d = length(uv);                          // distance from centre
  float glow = clamp(1.0 - d, 0.0, 1.0);
  vec3 col = glow * vec3(0.3, 0.7, 1.0)          // tint the falloff
           + (1.0 - glow) * vec3(0.02, 0.0, 0.08);
  gl_FragColor = vec4(col, 1.0);
}

What good looks like. A gradient that is round and centred whatever the canvas shape — resize the editor and it must not smear into an ellipse (that’s the aspect correction) or drift off-centre (that’s the clip-space remap). The colour ramp should be smooth, not a hard white disc (that’s respecting the [0,1] clamp). If you get an all-white or all-black frame, you’ve either forgotten a decimal point (a compile error) or let a channel exceed 1.0. These five moves — normalize, centre, aspect-correct, length(), clamp — open almost every shader you’ll write live. (Skill map: live-visualist Domain A3 — the GLSL coordinate/colour toolkit.)

Now make it yours. Ring instead of disc: abs(d - 0.5). Add a second colour stop with mix(). Pulse the radius with u_time (1.0 - length(uv) + sin(u_time)*0.1). Tile the field before length() with fract(uv * 3.0) - 0.5 for a grid of wheels.

Runnable examples

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

gradient-ramp

gradient(0.3).out()

hydra-0170 · MIT

[fr, fr*0.5, 1-fr] >> rgb

punctual-0033 · CC0-1.0

Atoms in this module

Required — these gate the capstone

A GLSL vector's components have interchangeable names: .xyzw, .rgba, .stpq, and [i]
Concept L1 Foundations G
GLSL swizzling reads or writes a vector's components in any order in one expression
Concept L1 Foundations GH
GLSL requires a decimal point on all floating-point literals
Fact L2 First instrument G
Multiplying st.x by width/height corrects the UV space for non-square canvases
Procedure L1 Foundations G
Normalizing UV coordinates to clip space (−1 to 1, aspect-ratio-corrected) makes shaders independent of canvas resolution
Procedure L2 First instrument G
GLSL uniforms pass values from the CPU to the GPU each frame
Concept L2 First instrument GJ
GPU fragment shader color output channels are clamped to [0,1]; values exceeding 1 saturate and produce incorrect gradients
Concept L2 First instrument G
The GLSL length() function computes distance from the origin, enabling radial gradients and circular shapes
Concept L2 First instrument G

Supporting — enrichment, not gating

Normalizing pixel coordinates to [-1,1] makes shaders resolution-independent and centers the mathematical origin
Concept L1 Foundations G
A GLSL shader program splits into a vertex shader run per-vertex and a fragment shader run per-pixel
Concept L2 First instrument GH
GLSL's in/out/inout qualifiers set whether a function reads, writes, or modifies an argument
Concept L2 First instrument G
GLSL pow(x,y) returns undefined for negative x, causing silent visual bugs
Misconception L2 First instrument G
GLSL's mix() linearly interpolates between two colors by a 0.0–1.0 factor
Concept L1 Foundations GL