Introduction to Shaders
Shaders are special programs that run on the graphics processing unit, or GPU, that can do some incredible things. They take advantage of the GPU to process many pixels at once, making them fast and particularly well suited for certain tasks, like generating noise, applying filters like blur, or shading polygons. Shader programming can feel daunting at first, requiring a different approach than the 2D drawing of p5.js. This document will outline the basics of shader programming and point you towards other resources.
Table of Contents
Setup
p5.js is a great tool for working with shaders because it handles a lot of the WebGL setup so you can focus on the shader code itself. Before we can get started with shaders we have to set up our canvas so that it uses p5.js WebGL model.
...
function setup() {
createCanvas(windowWidth, windowHeight, WEBGL);
}
..
A shader program consists of two parts, a vertex shader and a fragment shader. The vertex shader affects where the 3D geometry is drawn on the screen and the fragment shader is responsible for affecting the color output. Each of these live in separate files and are loaded into p5.js using loadShader(). Once a shader is loaded it can be used within draw(). The following example will show how to set up a basic shader within p5.js:
let myShader;
function preload() {
// load each shader file (don't worry, we will come back to these!)
myShader = loadShader('shader.vert', 'shader.frag');
}
function setup() {
// the canvas has to be created with WEBGL mode
createCanvas(windowWidth, windowHeight, WEBGL);
describe('a simple shader example that outputs the color red')
}
function draw() {
// shader() sets the active shader, which will be applied to what is drawn next
shader(myShader);
// apply the shader to a rectangle taking up the full canvas
rect(0,0,width,height);
}
Shading Language (GLSL)
So now you might be wondering what we actually write in these shader files! Shader files are written in Graphics Library Shading Language, or GLSL, and have a very different syntax and structure than we are familiar with. GLSL has a syntax that resembles C, which means it comes with a handful of concepts that aren't present in JavaScript.
For one, the shading language is much more strict about types. Each variable you create has to be labeled with the kind of data it is storing. Here is a list of some of the common types:
vec2(x,y) // a vector of two floats
vec3(r,g,b) // a vector of three floats
vec4(r,g,b,a) // a vector of four floats
float // a number with decimal points
int // a whole number without decimal points
sampler2D // a reference to a texture
In general the shading language is much more strict than JavaScript. A missing semicolon for example is not allowed and will result in an error message. You can't use different types of numbers, like floats or integers interchangeably.
First let's look at a basic vertex shader:
attribute vec3 aPosition;
void main() {
vec4 positionVec4 = vec4(aPosition, 1.0);
positionVec4.xy = positionVec4.xy * 2.0 - 1.0;
gl_Position = positionVec4;
}
This vertex shader begins with an attribute, which p5.js uses to share vertex position information with the shader. This attribute is a vec3, meaning it contains a value for x, y, and z. Attributes are special variable types that are only used in the vertex shader and are typically provided by p5.js. When you use p5.js methods like rect() or vertex(), p5.js passes the vertex information to the shader automatically.
All vertex shaders require a function, main(), within which we position our vertices. In this example, the vertex shader repositions our vertices so that the shader output takes up the full sketch. At the end of main(), we have to assign a value to gl_Position.
Don't worry if this doesn't make a ton of sense yet. The vertex shader plays an important role but it is often just responsible for making sure what we create in our fragment shader displays properly on the geometry. You'll probably find yourself reusing the same vertex shaders in many of your projects. The fragment shader on the other hand is responsible for the color output of our shader and is where we will do a lot of our shader programming. Here is a very simple fragment shader that will just display the color red:
precision mediump float;
void main() {
vec4 myColor = vec4(1.0, 0.0, 0.0, 1.0);
gl_FragColor = myColor;
}
The fragment shader begins with a line specifying the float 'precision'. This value can either be lowp, mediump, or highp, although you will likely use mediump, or highp in certain situations.
precision mediump float;
And similar to the vertex shader, our fragment shader also requires a main() function, but instead of setting gl_Position, we will assign a color to gl_FragColor.
...
vec4 myColor = vec4(1.0, 0.0, 0.0, 1.0);
gl_FragColor = myColor;
...
The variable, myColor, is defined as a vec4, meaning it stores 4 values. Since we are dealing with color, those four values are red, green, blue, and alpha. Shaders don't use 0 - 255 for colors like our sketches do, instead they use values between 0.0 and 1.0.
Now that we have a vertex shader and a fragment shader, these can be saved to separate files (shader.vert and shader.frag respectively), and loaded into our sketch using loadShader().
Uniforms: Passing data from sketch to shader
A simple shader like this can be useful by itself, but there are times when it's necessary to communicate variables from the p5.js sketch to a shader. This is when uniforms come in. Uniforms are special variables that can be sent from a sketch to a shader. These make it possible to have much more control over a shader. For example, you could use the p5.js method millis() to pass a 'time' uniform to our sketch to introduce motion. In the shader, uniforms are defined at the top of the file, outside of main(). In this following fragment shader we are creating a color uniform, myColor, that will allow us to change the color from our sketch.
precision mediump float;
uniform vec3 myColor;
void main() {
// the color we have passed in as a uniform is assigned to the pixel
gl_FragColor = vec4(myColor, 1.0);
}
Back in our p5.js sketch, this color can now be sent using setUniform():
...
function draw() {
shader(myShader);
// setUniform can then be used to pass data to our shader variable, myColor
myShader.setUniform('myColor', [1.0,0.0,0.0]); // send red as a uniform
// apply the shader to a rectangle taking up the full canvas
rect(0,0,width,height);
}
...
There are also attributes, which are usually used to share certain data about the geometry between the sketch and the vertex shader, and varying variables, which share data between the vertex shader and the fragment shader. This makes it possible to use position or other geometry data within our fragment shaders.
// (thank you to Adam Ferriss for the foundation of these example shaders)
// position information that is used with gl_Position
attribute vec3 aPosition;
// texture coordinates
attribute vec2 aTexCoord;
// the varying variable will pass the texture coordinate to our fragment shader
varying vec2 vTexCoord;
void main() {
// assign attribute to varying, so it can be used in the fragment
vTexCoord = aTexCoord;
vec4 positionVec4 = vec4(aPosition, 1.0);
positionVec4.xy = positionVec4.xy * 2.0 - 1.0;
gl_Position = positionVec4;
}
Now with the texture coordinate attribute assigned to the varying variable, we can use the texture coordinate in our fragment shader. The result in the example below is a blue and magenta visualization of our texture coordinates.
precision mediump float;
varying vec2 vTexCoord;
void main() {
// now because of the varying vTexCoord, we can access the current texture coordinate
vec2 uv = vTexCoord;
// and now these coordinates are assigned to the color output of the shader
gl_FragColor = vec4(uv,1.0,1.0);
}
Conclusion
With these skills you will be able to create some basic shaders, but shader programming can go incredibly deep, and there are many shader topics that go beyond this tutorial. Shaders in p5.js can be a powerful tool for creating visuals, effects, and even textures that can be mapped to your 3D geometry.
Want to keep learning more about shaders? Check out some of these websites!
- The Book of Shaders, a shader guide by Patricio Gonzalez Vivo and Jen Lowe.
- P5.js shaders, a shader guide by Casey Conchinha and Louise Lessél.
- Shadertoy, a massive online collection of shaders that are written in a browser editor.
- p5jsShaderExamples, a collection of resources by Adam Ferriss.
Other Tutorials
This tutorial is part of a series about the basics of using WebGL in p5.js. Check out each of these other tutorials below.
- Coordinates and Transformations
- Creating Custom Geometry in WebGL
- Styling and Appearance
- Introduction to Shaders (you are here)
Glossary
Shader
A special graphics card program that can efficiently produce many visual effects and filters.
GLSL
Graphics Library Shader Language (GLSL) is a programming language that is used to write shaders.
Uniform
A variable that is passed from your sketch to a shader.
Vector
A data type that stores a group of numbers, most commonly two, three, or four, to represent colors, positions, and more.
Float
A data type that stores floating point numbers, which can have a decimal point.
Int
A data type that stores integers, which are whole numbers without a decimal.
Sampler
A data type that represents a texture being passed into the shader.
Attribute
A GLSL variable that is generated in the p5.js sketch and made available in the vertex shader. For most situations these are provided by p5.js.
Texture
An image that is passed into a shader program.
Type
A label describing the characterics of a piece of data, such as an int, a float, a vector, etc.
Vertex Shader
The part of a shader program that is responsible for positioning geometry in 3D space.
Fragment Shader
The part of a shader program that is responsible for the color and appearance of each pixel output by the shader.