Introduction

Shaders are a powerful way to apply powerful logic to the individual pixels in a p5 sketch. We write them in the GLSL language (a form of C) then they are compiled and (generally) run on your graphics card. They are much faster than the same operation written in pure Javascript.

My laptop has 16 CPU cores (and a javascript process typically only uses 1); however, it has 6,144 GPU shader cores, and a WebGL shader can use all of them. It looks something like this video.

Here’s a diagram from the Graphics Pipeline article on Wikipedia:

Graphics Pipeline

This summary will focus on only the “Pixel shading” portion of the pipeline as used in WebGL shaders; it is possible to use “Vertex Shaders” too, but I’ve found those to be of relatively limited use when post-processing a p5 sketch.

Pixel Shaders for 2D

We’ll use the WEBGL canvas in P5 to render the viewport. We typically use WEBGL for 3D - to make it work for 2D graphics, we’ll make a straightforward 3D scene.

camera looking at a plane

  createCanvas(400,400, WEBGL);
  noStroke();
  rect(0, 0, width, height);

Our shader consists of two separate files, a .vert file and a .frag file.

Here’s a boilerplate vertex shader. You can pretty much use this version of shader.vert for everything else we’re about to do.

// our vertex data (set by p5)
// this will be in the World Coordinate space
attribute vec3 aPosition;

// our texture coorinates (set by p5)
// These will go from 0 to 1
attribute vec2 aTexCoord;

// The varying qualifier identifies this as something that will
// be passed down the pipeline to the fragment shader
//
// It's useful to have the texture coordinates from 0-1 so we
// copy them into this varying field
varying vec2 vTexCoord;

void main() {

  // copy the texture coordinates
  vTexCoord = aTexCoord;

  // copy the position data into a vec4, using 1.0 as the w component
  vec4 positionVec4 = vec4(aPosition, 1.0);
  
  // Our scale is from 0-1, but we want to make it look like it's
  // from -1 to 1 since that's going to look the same as the p5
  // layout
  positionVec4.xy = positionVec4.xy * 2.0 - 1.0;

  // send the vertex information on to the fragment shader
  gl_Position = positionVec4;
}

Now let’s create a fragment or pixel shader in a file called shader.frag.

// this is required
#ifdef GL_ES
precision mediump float;
#endif

// varying to receive the UV coordinates from the vertex shader
varying vec2 vTexCoord;

// function to calculate the color of the pixel
void main() {
  gl_FragColor = vec4(vTexCoord.x,0.0,0.0,1.0); // R,G,B,A
}

Finally we can set this up in our sketch.js file

let theShader;

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

function draw() {
  // shader() sets the active shader with our shader
  shader(theShader);

  // we draw our 2d drawing onto the plane
  rect(0, 0, width, height);
}

Here’s what we get:

Limitations on shaders

  • p5 only supports GLSL ES 1.0 (some code online uses new versions which won’t compile here)
  • You can use 1-dimensional arrays, but not 2d (or more)
  • No indeterminate looping (compiler unrolls all loops)
  • All floating point numbers must have a decimal - so you can use 0.0 but not just 0
  • Vector operations are the only thing that’s easier than javascript!

Passing values into the shader from p5

We can pass values into the shader as a “uniform”. This is one value that gets shared between all the shaders cores processing each frame of the drawing. Useful things to pass in might be:

  theShader.setUniform("uResolution", [width, height]);
  theShader.setUniform("uMouse", [mouseX,mouseY]);
  theShader.setUniform("uMillis", millis());

Now we can change our shader to vary the color based on the mouse position:

void main() {
  
  // convert the mouse position into the same coordinate space
  // by dividing by resolution - then flipping the y coordinate
  vec2 mouseTransformed = uMouse / uResolution;
  mouseTransformed.y = 1.0-mouseTransformed.y;
  
  
  // calculate the distance from the mouse to the pixel
  float distToMouse = length(vTexCoord - mouseTransformed);
  
  // render a color based on this distance
  gl_FragColor = vec4(1.0-distToMouse,0.0,0.0,1.0); // R,G,B,A
}

What can you do with this

Everything on shadertoy.com uses this approach; there are a lot of really good samples here which create all kinds of shapes and forms by varying the color of the pixels without having any true 3d objects behind them. (Shadertoy uses GLSL ES 3.0, so not everything there can easily be ported to p5)

I built my project for the Clock Assignment by marching a ray from the Camera position through the pixel we’re interested in on the screen and then used a sliding noise texture to decide if I’d hit a snowflake.

<high school geometry>You can use the vector-dot-product to test which side of a plane a particular point is on. I define the light shaft as four planes and consider the flake highlighted if the dot-product of the snowflake position and each of the four planes is negative</high school geometry>

Resources