3 Ways to Work with Color in Shaders
Let’s discover a few techniques for manipulating color using p5.strands, and learn a bit of fragment shader fundamentals in the process.
We’ll explore color in shaders to enhance our p5.js sketches with more sophisticated effects that JavaScript alone wouldn’t let us do.
What is p5.strands?
A new way of writing shaders using JavaScript in p5.js version 2.
- p5.strands: Writing Shaders in JavaScript, blog post by Dave Pagurek
- p5.strands: Introduction to Shaders, tutorial by Luke Plowden
To understand what advantages p5.strands brings to us when dealing with shaders, we’ll look at each technique through p5.js examples. One sketch will show an approach in p5.strands, while another will compare it with GLSL, a popular language for writing shaders in the browser. Using p5.strands, instead of GLSL, can be practical in your p5.js sketches if you prefer to use familiar p5.js syntax and type less code.
Each technique will present a few ideas for working with p5.js syntax that you can try in the p5.strands sketches. This can help you learn how to combine them.
Here’s an overview with a skip link to every technique we’ll cover:
You can skip to the conclusion as well, if you prefer to read from end to beginning.
What is a shader?
A set of instructions implementing algorithms that control how a mesh is rendered into pixels on the screen. It runs on the GPU. In the browser when working with WebGL, it’s written in GLSL and there are two types: vertex and fragment.
- Introduction to GLSL, tutorial by Dave Pagurek, Austin Lee Slominski, Adam Ferriss
Although GLSL can be used alongside JavaScript in a p5.js sketch, it is a separate shader language that shaders require. With p5.strands you can focus only on JavaScript, given that the GLSL is handled by p5.js.
Let’s start from simple to complex, like having a three-course meal, by first dipping our toes into an appetizer.
1. Solid Colors
We’ll begin by simply painting the canvas of our sketch and one 3D primitive.
What is pixel density?
The number of pixels packed into a given area of a computer display. Screens with smaller pixels (e.g. Retina displays) have a higher pixel density and create sharper images.
- pixelDensity(), p5.js reference
Because all of our sketches will be running shaders, the p5.js built-in function pixelDensity(1) is always implemented inside the setup(). We set the pixel density to 1, making the shaders run faster by reducing the number of pixels they need to process, since the default is often 2 on high-density displays.
What is a fragment shader?
A shader that sets the color of each pixel on the screen by outputting RGBA values, each ranging from 0.0 to 1.0.
- RGBA Color, self-guided workshop on fragment shaders by Hugh Kennedy
- RGB Color Model, fragment shader course by ilithya
With p5.strands we can modify the color of a shader while benefiting from several color notations and models that we already know from p5.js, since it translates them for us into the RGB representation used by GLSL with hooks (e.g. finalColor).
These hooks allow us to speak GLSL when we use them with familiar p5.js syntax. The following table shows a p5.strands equivalent:
| GLSL (fragment) | p5.strands |
|---|---|
gl_FragColor = val | hook.set(val) |
Try this!
We can use p5.js built-in functions like color() to paint with the shader:
- named color,
color('blue') - hex color value,
color('#0000ff') - hsl values,
color('hsl(40, 100%, 50%)') - RGB 8-bit values,
color(0, 255, 0)
sketch with GLSL
sketch with p5.strands
2. Textured Colors
We’ll continue evolving our canvas and 3D primitive sketch as we learn more techniques to color with our fragment shaders. Let’s add a colored texture to them.
What is a p5.strands hook?
An access point into a shader that connects p5.js with GLSL to modify its behavior.
- p5.strands Reference Sheet, a quick interactive guide by Kit Kuksenok
With p5.strands we can apply randomness to a color equation, creating visually smooth organic textures like clouds, as well as rougher textures found in crystals, by using the noise() p5.js built-in function with a hook (e.g. pixelInputs) instead of implementing GLSL algorithms from scratch.
We can also add the time elapsed with millis() to our textures to simulate natural-looking movements. This value becomes a GLSL uniform, useful to adjust the shader.
What is a GLSL uniform?
A global variable that allows the CPU (JavaScript) and GPU (GLSL) to communicate. The shader cannot change it, only read its value to control a specific feature, such as size or motion. It’s set and updated in JavaScript.
- Uniforms Chapter, The Book of Shaders by Patricio Gonzalez Vivo, Jen Lowe
- setUniform(), p5.js reference
p5.strands lets us set shader uniforms with p5.js elements we know, providing a bridge between JavaScript and GLSL.
Here are other p5.strands syntax examples we’ll use, and their GLSL translations:
| GLSL (fragment) | p5.strands |
|---|---|
varying vec2 uv | hook.texCoord |
gl_FragColor | hook.color |
Try this!
Experiment with different inputs for the cloud texture:
- static,
noise(uv); - animated,
noise(uv + millis() * 0.00015); - horizontal mouse movement,
noise(uv + mouseX / width); - vertical mouse movement,
noise(uv + mouseY / height);
sketch with GLSL
sketch with p5.strands
3. Image Color Filters
Let’s add an image to our canvas and 3D primitive and learn how we can create a color filter with adjustments in the fragment shader. To simplify things, we’ll use the same image for both.
What is chromatic aberration?
It’s a color distortion that occurs when the colors of light don’t focus on the same point. The RGB channels appear to be separated, making the colors of an image appear shifted.
- Chromatic aberration, wikipedia
- Filter Shader (p5.strands), example by Dave Pagurek
- Filter Shader (GLSL), example by Dave Pagurek
With p5.strands we can customize an image filter to our liking and create a chromatic aberration effect by using p5.js elements with a hook (e.g. filterColor). We can displace the image colors vertically and horizontally with the p5.js built-in variables mouseX and mouseY, which are translated into GLSL uniforms to control the shader.
We’ll add these p5.strands vs. GLSL translations to our knowledge base:
| GLSL (fragment) | p5.strands |
|---|---|
uniform sampler2D u_texture | hook.canvasContent |
uniform sampler2D u_texture | uniformTexture(texture) |
texture2D(u_texture, uv) | getTexture(texture, uv) |
Try this!
Explore different offsets in both axes (x, y) for the image displacement:
- animated,
noise(uv + millis() * 0.00025) - linear animation,
sin(millis() * 0.001) * 0.5 - centered horizontal mouse movement,
(0.5 - mouseX / width) * 0.5 - centered vertical mouse movement,
(0.5 - mouseY / height) * 0.5
sketch with GLSL
sketch with p5.strands
Wrapping Up
We learned three ways to work with color in fragment shaders with p5.strands, which can be practical in your p5.js sketches, as you can keep on writing JavaScript using familiar p5.js syntax and type less code.
Instead of this:
// p5.js with GLSL
let vertex = `
precision highp float;
attribute vec3 aPosition;
uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;
void main() {
gl_Position = uProjectionMatrix * uModelViewMatrix * vec4(aPosition, 1.0);
}
`;
let fragment = `
precision highp float;
void main() {
vec3 color = vec3(0.0, 0.0, 1.0);
gl_FragColor = vec4(color, 1.0);
}
`;
We can do this:
// p5.js with p5.strands
function fragmentColor() {
finalColor.begin();
finalColor.set(color('blue'));
finalColor.end();
}
When should I use p5.strands?
It might be ideal for certain shader techniques or features where you want to avoid writing the GLSL needed from scratch, and instead use p5.strands hooks alongside p5.js built-in functions and variables.
- p5.strands Overview, technical contributor documentation
- p5.js Reference, easy explanations for every piece of p5.js code
If instead of being comfortable with p5.js, you come from GLSL, p5.strands also enables you to use GLSL built-in functions, as for example step() and mix(). Check out our list of supported GLSL built-ins. This list is also part of the interactive p5.strands reference.
Even when we’re in the p5.js ecosystem, learning some GLSL fundamentals might unlock future possibilities to additionally work with other web-based creative coding tools that offer more advanced shader capabilities with equally friendly communities, including Three.js and shadertoy.
You can continue exploring color in shaders with p5.strands using more p5.js elements we didn’t cover in our examples, such as random().
Try this!
Feel free to submit your p5.strands explorations here:
- p5.strands Community Workshop, OpenProcessing Curation
p5.strands is in active development by the community. If you want to get involve, join our discord server, we’ll be happy to meet you!