p5.js Intermediate Article 9

Shaders in p5.js

Write GLSL vertex and fragment shaders, pass uniforms, and apply them to 2D and 3D geometry.

⏱ 25 min read shaders GLSL WebGL vertex shader fragment shader uniforms

What is a shader?

A shader is a program that runs on the GPU rather than the CPU. Two types matter here:

  • Vertex shader — runs once per vertex; positions geometry in clip space
  • Fragment shader — runs once per pixel (fragment); determines the final colour

Shaders are written in GLSL (OpenGL Shading Language). They’re extremely fast because the GPU runs thousands in parallel.

Setting up

Shaders require WEBGL mode. Create separate files (or strings) for vertex and fragment shaders:

let shd;

function preload() {
  shd = loadShader('shader.vert', 'shader.frag');
}

function setup() {
  createCanvas(600, 400, WEBGL);
}

function draw() {
  shader(shd);
  // pass uniforms
  shd.setUniform('u_time',       frameCount * 0.01);
  shd.setUniform('u_resolution', [width, height]);
  // draw a full-screen rect to trigger the fragment shader
  rect(-width / 2, -height / 2, width, height);
}

The vertex shader

The minimal vertex shader passes position and texture coordinates:

// shader.vert
attribute vec3 aPosition;
attribute vec2 aTexCoord;

varying vec2 vUv;

void main() {
  vUv = aTexCoord;
  vec4 positionVec4 = vec4(aPosition, 1.0);
  positionVec4.xy = positionVec4.xy * 2.0 - 1.0;
  gl_Position = positionVec4;
}

This is the standard boilerplate for 2D full-screen effects — copy it verbatim.

Your first fragment shader

A simple gradient:

// shader.frag
precision mediump float;

varying vec2 vUv;
uniform float u_time;
uniform vec2  u_resolution;

void main() {
  vec2 uv = vUv;
  vec3 col = vec3(uv.x, uv.y, abs(sin(u_time)));
  gl_FragColor = vec4(col, 1.0);
}

Passing uniforms from p5.js

shd.setUniform('u_time',       millis() / 1000.0);
shd.setUniform('u_resolution', [width, height]);
shd.setUniform('u_mouse',      [mouseX / width, 1.0 - mouseY / height]);
shd.setUniform('u_texture',    myImage);    // p5.Image or p5.Graphics
shd.setUniform('u_float',      3.14);
shd.setUniform('u_bool',       true);
shd.setUniform('u_vec3',       [1.0, 0.5, 0.2]);

Signed Distance Functions

The most expressive pattern in GLSL: define a scene as a mathematical function and render it entirely in the fragment shader:

precision mediump float;
varying vec2 vUv;
uniform float u_time;
uniform vec2  u_resolution;

// Smooth minimum — blends two shapes
float smin(float a, float b, float k) {
  float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
  return mix(b, a, h) - k * h * (1.0 - h);
}

float sdCircle(vec2 p, float r) {
  return length(p) - r;
}

float sdBox(vec2 p, vec2 b) {
  vec2 d = abs(p) - b;
  return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
}

void main() {
  vec2 uv = (vUv - 0.5) * vec2(u_resolution.x / u_resolution.y, 1.0) * 2.0;

  float t = u_time;
  vec2 c1 = vec2(cos(t) * 0.4, sin(t * 0.7) * 0.3);
  vec2 c2 = vec2(cos(t * 1.3 + 1.0) * 0.3, sin(t * 0.9 + 2.0) * 0.4);

  float d1 = sdCircle(uv - c1, 0.3);
  float d2 = sdCircle(uv - c2, 0.25);
  float d  = smin(d1, d2, 0.15);

  // Colouring
  vec3 col = vec3(0.08, 0.05, 0.12);
  col = mix(col, vec3(0.5, 0.3, 1.0), 1.0 - smoothstep(0.0, 0.01, d));
  col += vec3(0.1, 0.4, 0.8) * (1.0 - smoothstep(0.0, 0.1, abs(d)));

  gl_FragColor = vec4(col, 1.0);
}

Applying a shader to a texture

Post-processing effects — blur, colour grade, distortion:

// glitch.frag
precision mediump float;
varying vec2 vUv;
uniform sampler2D u_texture;
uniform float     u_time;

void main() {
  vec2 uv = vUv;

  // Chromatic aberration
  float offset = sin(u_time * 3.0 + uv.y * 20.0) * 0.005;
  float r = texture2D(u_texture, uv + vec2(offset, 0.0)).r;
  float g = texture2D(u_texture, uv).g;
  float b = texture2D(u_texture, uv - vec2(offset, 0.0)).b;

  gl_FragColor = vec4(r, g, b, 1.0);
}
// In p5.js:
let pg = createGraphics(width, height);  // draw your scene here
shd.setUniform('u_texture', pg);

Inline shaders (no separate files)

For sharing a single file, embed the GLSL as template literals:

const vertSrc = `
  attribute vec3 aPosition;
  attribute vec2 aTexCoord;
  varying vec2 vUv;
  void main() {
    vUv = aTexCoord;
    vec4 p = vec4(aPosition, 1.0);
    p.xy = p.xy * 2.0 - 1.0;
    gl_Position = p;
  }
`;

const fragSrc = `
  precision mediump float;
  varying vec2 vUv;
  uniform float u_time;
  void main() {
    float v = sin(vUv.x * 20.0 + u_time) * 0.5 + 0.5;
    gl_FragColor = vec4(v, vUv.y, 1.0 - v, 1.0);
  }
`;

let shd;
function setup() {
  createCanvas(600, 400, WEBGL);
  shd = createShader(vertSrc, fragSrc);
}

Key takeaways

  • Shaders run on the GPU; vertex shaders position geometry, fragment shaders colour pixels
  • The minimal vertex boilerplate: scale position to clip space and pass UV coordinates
  • shd.setUniform() passes values from p5 to the shader each frame
  • Signed Distance Functions define shapes mathematically in the fragment shader
  • The standard 2D full-screen effect setup: rect(-w/2, -h/2, w, h) covers the canvas
  • Embed GLSL as template literals for single-file sketches, or load from separate .vert/.frag files