Processing Intermediate Article 8

GLSL Shaders in Processing

Write and apply GLSL fragment and vertex shaders in Processing using PShader, pass uniforms, and create a UV-animated fullscreen pattern.

⏱ 20 min glsl shader pshader fragment vertex

Shaders are programs that run directly on the GPU, processing millions of pixels or vertices in parallel. Processing’s PShader class makes it straightforward to write GLSL (OpenGL Shading Language) and apply it to any geometry. This tutorial covers the full pipeline from your first fragment shader to a UV-animated fullscreen effect.

How Shaders Fit Into Processing

When you use the P2D or P3D renderer, Processing uses OpenGL under the hood. Every pixel you draw passes through a pair of programs:

  • Vertex shader — runs once per vertex, transforming 3D positions to screen space.
  • Fragment shader — runs once per pixel (fragment) between vertices, determining the final colour.

You can override these defaults with your own GLSL code. Processing handles the boilerplate of compiling and linking the shader program.

Your First Fragment Shader

Shaders live in separate files in the sketch’s data/ folder. Create data/color.glsl:

#ifdef GL_ES
precision mediump float;
#endif

uniform float time;
uniform vec2  resolution;

void main() {
  // Normalise pixel coordinates to 0.0–1.0
  vec2 uv = gl_FragCoord.xy / resolution;

  // Colour cycles over time
  float r = 0.5 + 0.5 * sin(time + uv.x * 6.28);
  float g = 0.5 + 0.5 * sin(time * 0.7 + uv.y * 6.28);
  float b = 0.5 + 0.5 * cos(time * 0.5);

  gl_FragColor = vec4(r, g, b, 1.0);
}

In Processing:

PShader myShader;

void setup() {
  size(800, 600, P2D);
  myShader = loadShader("color.glsl");
}

void draw() {
  myShader.set("time", millis() / 1000.0);
  myShader.set("resolution", float(width), float(height));

  shader(myShader);
  rect(0, 0, width, height);    // geometry the shader is applied to
  resetShader();                 // restore the default Processing shader
}

loadShader() compiles the GLSL. shader(myShader) activates it for subsequent geometry. resetShader() reverts to the default so any subsequent Processing drawing calls (text, UI) render correctly.

Passing Uniforms

Uniforms are read-only values that you send from Processing (CPU) to the shader (GPU) each frame. PShader.set() accepts most types:

myShader.set("time",       millis() / 1000.0);      // float
myShader.set("resolution", float(width), float(height)); // vec2
myShader.set("mousePos",   float(mouseX), float(mouseY)); // vec2
myShader.set("myColor",    1.0, 0.5, 0.2, 1.0);     // vec4

// Texture sampler
PImage img = loadImage("texture.jpg");
myShader.set("tex", img);                            // sampler2D

In GLSL, declare matching uniforms at the top of the shader:

uniform float time;
uniform vec2  resolution;
uniform vec2  mousePos;
uniform sampler2D tex;

Fragment Shader Basics

Key built-in variables in a GLSL fragment shader:

Variable Type Meaning
gl_FragCoord vec4 Pixel position in screen space (origin at bottom-left)
gl_FragColor vec4 Output: RGBA colour you write

GLSL has rich built-in math: sin, cos, pow, mod, fract, abs, clamp, mix, smoothstep, length, dot, cross, normalize. These execute on the GPU at full float precision.

Writing a Ripple Distortion Shader

Create data/ripple.glsl:

#ifdef GL_ES
precision mediump float;
#endif

uniform sampler2D tex;
uniform float     time;
uniform vec2      resolution;

void main() {
  vec2 uv = gl_FragCoord.xy / resolution;
  // Flip Y — OpenGL origin is bottom-left; Processing's is top-left
  uv.y = 1.0 - uv.y;

  // Ripple: offset UVs by a sine wave
  float freq = 20.0;
  float amp  = 0.012;
  uv.x += sin(uv.y * freq + time * 3.0) * amp;
  uv.y += cos(uv.x * freq + time * 3.0) * amp;

  gl_FragColor = texture2D(tex, uv);
}

Apply it to a quad:

PShader ripple;
PImage  img;

void setup() {
  size(800, 600, P2D);
  ripple = loadShader("ripple.glsl");
  img    = loadImage("photo.jpg");
}

void draw() {
  ripple.set("tex",        img);
  ripple.set("time",       millis() / 1000.0);
  ripple.set("resolution", float(width), float(height));

  shader(ripple);
  image(img, 0, 0, width, height);   // image call triggers the shader
  resetShader();
}

Vertex Shaders

A vertex shader transforms each vertex before the rasteriser fills in the fragments. Processing passes several built-in attributes and uniforms:

// Vertex shader: data/wave.vert
uniform mat4 transform;   // Processing's combined MVP matrix
uniform float time;

attribute vec4 position;  // vertex position from Processing
attribute vec2 texCoord;  // UV coordinates
attribute vec4 color;     // per-vertex colour

varying vec2 vTexCoord;   // passed to fragment shader
varying vec4 vColor;

void main() {
  vec4 pos = position;
  // Wave deformation along Y
  pos.y += sin(pos.x * 0.05 + time * 2.0) * 20.0;

  gl_Position = transform * pos;
  vTexCoord   = texCoord;
  vColor      = color;
}

Pair it with a fragment shader in data/wave.frag that reads the varying values:

#ifdef GL_ES
precision mediump float;
#endif

uniform sampler2D texture;
varying vec2 vTexCoord;
varying vec4 vColor;

void main() {
  gl_FragColor = texture2D(texture, vTexCoord) * vColor;
}

Load both together:

PShader waveShader = loadShader("wave.frag", "wave.vert");

Full Example: UV-Animated Fullscreen Pattern

Create data/pattern.glsl:

#ifdef GL_ES
precision mediump float;
#endif

uniform vec2  resolution;
uniform float time;
uniform vec2  mouse;

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

void main() {
  vec2 uv = (gl_FragCoord.xy * 2.0 - resolution) / resolution.y;
  vec2 mu = (mouse * 2.0 - resolution) / resolution.y;

  float t = time * 0.8;

  // Layered rings centred on mouse
  float dist = length(uv - mu);
  float rings = sin(dist * 18.0 - t * 4.0) * 0.5 + 0.5;

  // Cross-hatching that animates
  float grid = sin(uv.x * 20.0 + t) * sin(uv.y * 20.0 - t);
  grid = smoothstep(0.0, 0.3, grid);

  // Combine
  float pattern = mix(rings, grid, 0.4 + 0.4 * sin(t * 0.3));

  // Palette: warm gold to deep blue
  vec3 colA = vec3(0.95, 0.75, 0.10);
  vec3 colB = vec3(0.05, 0.10, 0.40);
  vec3 col  = mix(colB, colA, pattern);

  // Vignette
  float vignette = 1.0 - smoothstep(0.6, 1.4, dist);
  col *= vignette;

  gl_FragColor = vec4(col, 1.0);
}

Processing sketch:

PShader pattern;

void setup() {
  size(900, 600, P2D);
  pattern = loadShader("pattern.glsl");
  // Hint Processing we are covering the full screen
  hint(DISABLE_DEPTH_MASK);
}

void draw() {
  pattern.set("resolution", float(width), float(height));
  pattern.set("time",       millis() / 1000.0);
  pattern.set("mouse",      float(mouseX), float(height - mouseY)); // flip Y

  shader(pattern);
  // A rectangle covering the full canvas triggers the fragment shader for every pixel
  rect(0, 0, width, height);
  resetShader();

  // HUD drawn after resetShader so it uses the default renderer
  fill(255, 200);
  noStroke();
  textAlign(RIGHT, BOTTOM);
  textSize(11);
  text("Move mouse to shift rings", width - 10, height - 10);
}

Move the mouse to reposition the ring pattern. The mix() blending between rings and grid oscillates slowly over time.

Key Takeaways

  • Place GLSL files in data/; load with loadShader("frag.glsl") or loadShader("frag.glsl", "vert.glsl").
  • Activate with shader(s) before drawing geometry; always call resetShader() afterwards.
  • Pass CPU values to the GPU each frame with PShader.set("name", value).
  • gl_FragCoord gives the current pixel position; divide by resolution to get normalised UVs.
  • Flip the Y axis (uv.y = 1.0 - uv.y) when sampling textures because OpenGL’s origin is bottom-left while Processing’s is top-left.