Shaders &
GLSL.
Shaders are small programs that run directly on the GPU — massively parallel, blindingly fast, and deeply visual. GLSL (OpenGL Shading Language) is their language: C-like syntax, built-in vector types, and a single job: decide the colour of every pixel on screen.
The key mental shift: a CPU program runs once, top to bottom. A shader runs simultaneously for every pixel — potentially millions of instances at once, each knowing only its own coordinates. There are no loops over pixels; the GPU is the loop.
The GPU rendering pipeline
Your JavaScript hands data to the GPU; two shader stages do the visual work.
Runs once per vertex. Transforms 3D positions into 2D screen coordinates using matrix math. Outputs gl_Position.
Runs once per pixel. Receives interpolated data, computes and outputs a colour as gl_FragColor — a vec4 (r,g,b,a).
Uniforms are constants sent from JS (time, resolution, mouse). Varyings are values interpolated between vertex and fragment stages.
GLSL built-in types
GLSL speaks vectors natively — no library needed.
| Type | Size | Used for |
|---|---|---|
| float | 1 | Single decimal number. Everything in GLSL is floating point. |
| vec2 | 2 | 2D coordinates, UV texture coords, screen position (x, y) |
| vec3 | 3 | 3D position, RGB colour, surface normals, light direction |
| vec4 | 4 | RGBA colour, homogeneous 3D coords (x, y, z, w) |
| mat2/3/4 | 4/9/16 | Transformation matrices for rotation, scale, projection |
| sampler2D | — | A texture unit — the shader can sample any pixel of an image |
| bool / int | 1 | Conditionals and loop counters (loops must have constant bounds) |
Your first shader — raw WebGL
A minimal WebGL setup in JavaScript that draws a colour gradient across a full-screen quad.
// attribute: per-vertex data sent from JS
attribute vec2 a_position;
varying vec2 v_uv; // passes UV to fragment shader
void main() {
v_uv = a_position * 0.5 + 0.5; // remap [-1,1] → [0,1]
gl_Position = vec4(a_position, 0.0, 1.0);
}
precision mediump float;
varying vec2 v_uv; // (0,0) bottom-left → (1,1) top-right
uniform float u_time; // seconds since start, sent from JS
void main() {
// Animate hue using time
vec3 colA = vec3(0.78, 0.32, 0.16); // terracotta
vec3 colB = vec3(0.08, 0.08, 0.06); // near-black
float t = sin(u_time + v_uv.x * 3.14) * 0.5 + 0.5;
vec3 col = mix(colB, colA, t * v_uv.y);
gl_FragColor = vec4(col, 1.0);
}
const canvas = document.querySelector('canvas');
const gl = canvas.getContext('webgl');
// helper: compile one shader stage
function compile(type, src) {
const s = gl.createShader(type);
gl.shaderSource(s, src);
gl.compileShader(s);
return s;
}
const prog = gl.createProgram();
gl.attachShader(prog, compile(gl.VERTEX_SHADER, vertSrc));
gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, fragSrc));
gl.linkProgram(prog);
gl.useProgram(prog);
// full-screen quad: two triangles
const verts = new Float32Array([-1,-1, 1,-1, -1,1, 1,1]);
const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, verts, gl.STATIC_DRAW);
const loc = gl.getAttribLocation(prog, 'a_position');
const uTime = gl.getUniformLocation(prog, 'u_time');
gl.enableVertexAttribArray(loc);
gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);
// animation loop
function loop(t) {
gl.uniform1f(uTime, t / 1000);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
Activity — Live WebGL gradient
The fragment shader below runs in real time in your browser. Adjust the sliders to change the colours.
Shaders in p5.js
p5.js wraps WebGL with a friendlier API. Use createShader() and shader() — the GLSL is identical.
// ── fragment shader (string passed to createShader) ──
const fragSrc = `
precision mediump float;
uniform vec2 u_resolution;
uniform float u_time;
void main() {
vec2 uv = gl_FragCoord.xy / u_resolution;
uv = uv * 2.0 - 1.0; // centre origin
uv.x *= u_resolution.x / u_resolution.y; // fix aspect
float dist = length(uv); // distance from centre
float rings = sin(dist * 12.0 - u_time * 2.5);
float col = smoothstep(0.0, 0.05, rings); // sharp edges
gl_FragColor = vec4(col * 0.78, col * 0.32, col * 0.16, 1.0);
}
`;
// ── p5.js sketch ──
let sh;
function setup() {
createCanvas(400, 400, WEBGL);
sh = createShader(defaultVertSrc, fragSrc);
}
function draw() {
shader(sh);
sh.setUniform('u_resolution', [width, height]);
sh.setUniform('u_time', millis() / 1000.0);
rect(-width/2, -height/2, width, height); // full canvas quad
}
Activity — Distance field rings
A classic shader pattern: compute the distance from each pixel to the centre, then use sin() to draw concentric rings.
JavaScript vs GLSL — key differences
Same mathematical ideas, very different execution model.
| Concept | JavaScript (CPU) | GLSL (GPU) |
|---|---|---|
| Execution | Sequential, one thread | Parallel, millions at once |
| Loop over pixels | for (let y…) for (let x…) | No loop — you ARE one pixel |
| Vector math | Manual: x*cos-y*sin … | Built-in: mat2 * vec2 |
| Textures | ImageData, getImageData() | texture2D(sampler, uv) |
| Time | Date.now(), performance.now() | uniform float u_time (sent from JS) |
| Conditionals | if/else freely | Prefer step(), mix(), smoothstep() — branches are costly |
| Output | ctx.fillStyle = … | gl_FragColor = vec4(r,g,b,a) |