Creating GLSL shaders

Shaders
12 Jan 2022

Shaders are a powerful tool for creating beautiful, fluid, interactive animations usable on websites, but due to the way they work they can also be intimidating to learn.

Not only is there GLSL to learn (a new language and tools) but it requires thinking about creating visuals in a fundamentally different way from standard design tools.

I wrote this article to share my experiences and some advice I picked up along the way trying to learn for myself.

Background

I first really became aware of shaders in 2018 at a creative coding conference in Paris called Grow. One of the speakers, Patricio Gonzalez Vivo, gave a presentation called Shaders, a computational language of light.

At the time I didn’t realise his role in the shader community but his site, The book of shaders, is undoubtedly the first resource I’d recommend to anyone who wants to learn.

During that conference I attended a fantastic two day workshop run by Matt DesLauriers we briefly experimented creating a simple shader. But we covered a lot of ground over those two days and shaders were just one of many topics.

An example shader from Matt DesLaurier’s generative art workshop

Here is the GLSL code which generates this animation.

precision highp float;
uniform float time;
uniform float aspect;
varying vec2 vUv;
void main () {
  // Get a vector from current UV to (0.5, 0.5)
  vec2 center = vUv - 0.5;
  // Fix it for current aspect ratio
  center.x *= aspect;
  // Get length of the vector (i.e. radius of polar coordinate)
  float dist = length(center);
  // Create a "mask" circle
  float mask = smoothstep(0.2025, 0.2, dist);
  vec3 color = 0.5 + 0.5 * cos(time + vUv.xyx + vec3(0.0, 2.0, 4.0));
  gl_FragColor = vec4(color, mask);
}
precision highp float;
uniform float time;
uniform float aspect;
varying vec2 vUv;
void main () {
  // Get a vector from current UV to (0.5, 0.5)
  vec2 center = vUv - 0.5;
  // Fix it for current aspect ratio
  center.x *= aspect;
  // Get length of the vector (i.e. radius of polar coordinate)
  float dist = length(center);
  // Create a "mask" circle
  float mask = smoothstep(0.2025, 0.2, dist);
  vec3 color = 0.5 + 0.5 * cos(time + vUv.xyx + vec3(0.0, 2.0, 4.0));
  gl_FragColor = vec4(color, mask);
}

While experimenting (along with being amazed by the visual effects achievable) I was struck by just how trivial it was to seemingly completely break the code.

Suddenly nothing would display in the browser and there wasn’t really a simple way of outputting variables to a console–or really any of the other debugging techniques I was used to from other languages.

Understanding why is a core concept and what also underpins their power.

Shaders work in the same way the GPU does, by calculating each pixel independently. So the program runs for each pixel, without knowing anything about what the other pixels or doing, or storing any state. If you have a canvas 1000px by 1000px it will run 1,000,000 times (per frame). But it will run very fast.

Writing code which works like this means you may have to alter how you think about programming.

If this may seem mind-bending enough already but it’s not the only learning curve. Concepts like vectors and floats (and the strongly typed nature when working with them) often led to my confusion over why something wasn’t working as expected. For example, setting a value as 0 instead of 0.0 could often be the source of a frustrating bug.

Then there is the way the self-contained GLSL program, how it is included in an HTML page with javascript, and data passed into it. It felt like lots of moving parts and ways to go wrong. So as interesting as they were from these difficulties, and not having a specific project in mind, I set shaders aside as something to return to at a later date.

Another approach

From time to time I’d see examples of a beautiful shaders in the wild that would re-ignite my interest in them but it wasn’t really until I had the need to use one in a project I am working on that I was motivated enough to have another go.

I’d found a few tutorials online with real world examples of using shaders on websites, but they’d all had a bit of a ‘How to Draw an Owl’ energy to them.

How to draw an owl
How to draw an owl

What changed things for me was finding SuperHi, an online learning site, and their course called Shaders for the Web.

Each chapter (there are ten in total) walks through every step of building a functioning webpage covering topics like animating color spectrums, kaleidoscopes, generating image effects on scroll, transitions, and more.

The first thing I noticed was that instead of starting in a browser all the chapters began with first writing the GLSL using an editor called KodeLife. The benefits of this approach were obvious immediately–clearer error messages, changes are instantaneous but most of all I found was it just makes everything more intuitive. The software if free to use, too.

Screenshot of KodeLife

KodeLife – a real-time GPU shader editor

Instead of a text editor and browser the process becomes

  1. Use KodeLife to code the shader to achieve the desired visual effect
  2. Transfer this code into the right format for the web, either using GLSLCanvas or Three.js
  3. Make final adjustments from data passed into the shader from javascript

It isn't a free course, but in my opinion well worth the subscription cost.

Resources