Building a Custom Shader with GLSLify

WebGL can be a complicated and mysterious animal. While many people associate GL with 3D graphics, it is much more versatile than that. In this post we talk about getting set up to build your own shaders.

GL is general purpose graphics programming. It is one of the most powerful APIs on the web and is capable of pretty much any kind of drawing you’d want to task it with (including, obviously, 3D graphics). I’d like to see more people doing stuff with the mighty WebGL. One of the ways we can do that is by having a thriving community of programmers creating modules for WebGL that solve tricky problems. This is what excites me about initiatives like GLSLify and Stack.gl.

You’ll need to learn some WebGL…

Resources like The Book of Shaders and WebGL Fundamentals can get you up and running with WebGL. This is not a WebGL tutorial. Sorry! Check the links above. It’s been done and I doubt I could do it better than them.

It’s likely that most of the WebGL you have seen has been a total mess. A complete program stuck in a single file, like the good ol’ days of Javascript. It’s difficult to digest, understand, and maintain.

This lack of organization makes learning WebGL difficult, But once you can see a shader as a program, and not voodoo, you’ll start thinking you can wield your own magic.

A good structure to your WebGL code will definitely help. GLSLify helps structure your shader so that it’s organized, extensible, and reusable.

Good Patterns for Shader Development

ES6 and Node js have established good patterns for bundling together modules. They both support reusable modules and includes. WebGL shader code doesn’t have this. All your code needs to be in one big file.

This is what GLSLify takes care of. It allows you to assemble a WebGL shader from imported modules. This lets you break up your code into small modules that are easier to understand. It enables better collaboration, source control, maintenance, and debugging.

Getting GLSLify Setup

If you’re familiar with npm, browserify, and gulp, getting GLSLify setup is a piece of cake.

Depending on who you are, you can go about this two ways:

  1. If you just want the code, grab our GLSLify boilerplate from github.
  2. If you want a step by step walkthrough, Keep reading!

Create a new folder for your project and initialize a new npm package (if you don’t know how to do that, follow these docs).

Install the following packages as development dependencies

npm install gulp @babel/core @babel/preset-env babelify browserify glslify vinyl-source-stream --save-dev

Install these as dependencies
npm install twgl.js -save

To get started we’re going to need to get WebGL up and running. Doing this with the raw WebGL library is a ton of code. Luckily there is a small WebGL library called “TWGL” (twiggle) that reduces the amount of boilerplate significantly. We also need to setup GLSLify to compile our shader and send it to TWGL.

Create a file called gulpfile.js in the project root. This will house our build task. When we run this task, browserify will use glslify to transform our separate glsl packages into compiled text.

./gulpfile.js


const gulp = require("gulp");

const browserify = require("browserify");

const source = require("vinyl-source-stream");

gulp.task("build", function() {

  return browserify({

    entries: "./src/index.js",

    extensions: [".js"],

    debug: true

  })

  .transform("glslify") // the GLSLify transform
  .transform("babelify",{ presets: ["@babel/preset-env"] })
  .bundle()
  .pipe(source("app.js"))
  .pipe(gulp.dest("dist/js"));
}); 
  1. Create a folder called in the root called src
  2. Create a file in src called index.js
  3. Inside the src folder, create a folder called shader
  4. In the shader folder create two files. vertex.glsl and fragment.glsl

The two GLSL files will represent our main shader code. GLSLify will compile these into a string that will become our shader program.

Let’s set up the index.js file to load the shaders.

The glsl function glsl.file() can be used to load an external file. When this is compiled, it will be an inline string.

./src/index.js

 "use strict"  
const glsl = require("glslify"); 
const vertexShader = glsl.file("./shader/vertex.glsl"); 
const fragmentShader = glsl.file("./shader/fragment.glsl"); console.log(vertexShader, fragmentShader); 

Before we run the build, let’s put together a small shader so we can see the compiled version.

In vertex.glsl, add the following code. Let’s not worry about what it does.

./src/shader/vertex.glsl

attribute vec4 position;
void main() {
gl_Position = position;
}

Create a new glsl file called “tint.glsl” in the shader folder

./src/shader/tint.glsl

vec3 tint(vec3 thingToColor, vec3 tint) {
return thingToColor + tint;
}

#pragma glslify: export(tint);

This will take a vec3 as a thingToColor and a vec3 to tint it. Not very useful I know, but it’s for demonstration purposes.

notice this line…

#pragma glslify: export(tint);

That will export our tint function so we can import it into our fragment.glsl

./src/shader/fragment.glsl

precision mediump float;

uniform vec2 u_resolution;
uniform vec2 u_mouse;
uniform float u_time;

#pragma glslify: snoise3 = require(glsl-noise/simplex/3d)
#pragma glslify: tint = require("./tint.glsl");

void main() {
     vec2 st = gl_FragCoord.xy/u_resolution; // get the screen space
     
    vec3 pos = vec3(st.xy, u_time); // travel along the Z-dimension in time.
    vec3 rgb = vec3(0.1, cos(u_time), 0.5); // cycle the green color
    vec3 noise = vec3(snoise3(pos),snoise3(pos),snoise3(pos)); // generate the noise

    gl_FragColor = vec4(tint(noise, rgb), 1.0); // tint the noise with our function and draw the pixel
}

Ok, before we get to see these shaders, let’s get the build folder setup.

Create a folder called dist. Inside dist create an index.html file.

./dist/index.html

 
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>My First Shader by Me, Awesome Developer</title>
    <link rel="stylesheet" href="css/style.css">
</head>
<body>
    
</body>

<script src="js/app.js"></script>

</html> 

Build It!

Now that everything’s in place, we can use the gulp build task we created earlier to build our script.

Run this command:

gulp build

Gulp will run the index.js file through a bunch of transforms and produce an app.js file in the dist folder.

Run It!

In order to run this, you’ll need a web server. A really simple web server to use is http-server.

npm install -g http-server

Once that is ready, run it from the dist folder.

http-server -o

This should open a browser window with the compiled script running.

Check the console. You should see two strings with our shaders printed out! You should also be able to spot our tint() function. Also notice that all the #pragma entries are gone. Compilation Magic!

Let’s get the shaders actually working. To do this we will have to complete our setup of TWGL.

Open up index.js again. This code will create the canvas and do the basic setup so that the shaders can run. Our compiled “inline” shader code will get passed to the GL program to run.

./src/index.js

  
"use strict";

const glsl = require("glslify");
const twgl = require("twgl.js");

const vertexShader = glsl.file("./shader/vertex.glsl");
const fragmentShader = glsl.file("./shader/fragment.glsl");

const canvas = document.createElement("canvas");
document.body.appendChild(canvas);
const gl = canvas.getContext("webgl");

const programInfo = twgl.createProgramInfo(gl, [vertexShader, fragmentShader]);
let mouse = [0, 0];

const arrays = {
  position: [-1, -1, 0, 1, -1, 0, -1, 1, 0, -1, 1, 0, 1, -1, 0, 1, 1, 0]
};
const bufferInfo = twgl.createBufferInfoFromArrays(gl, arrays);

function render(time) {
  twgl.resizeCanvasToDisplaySize(gl.canvas);
  gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);

  const uniforms = {
    u_time: time * 0.001,
    u_resolution: [gl.canvas.width, gl.canvas.height],
    u_mouse: mouse
  };

  gl.useProgram(programInfo.program);
  twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
  twgl.setUniforms(programInfo, uniforms);
  twgl.drawBufferInfo(gl, bufferInfo);

  requestAnimationFrame(render);
}
requestAnimationFrame(render);

document.addEventListener("mousemove", e => {
  mouse[0] = e.pageX;
  mouse[1] = e.pageY;
});  

Run gulp build again

Once the build is complete, refresh the page and you should see a pastel blue screen. Yay, we did it!

Experiment with the tint values in the fragment.glsl file.

One last thing…

One of the great things about GLSLify is that it can make use of Stack.gl. Stack.gl packages are like node modules for GL. All we have to do is npm install them, and we can include them in our shader code. Check out all the stack.gl packages here http://stack.gl/packages/

Let’s have some fun with noise!

First, install glsl-noise

npm install glsl-noise --save

Update fragment.glsl to import the glsl-noise the same way you did the tint.glsl. You might need to check glsl-noise docs to see all it has to offer. Glsl has a feature called “function overloading” which allows functions with the same name to have different signatures. You might need to check the types of data necessary to pass into a function and what data is returned.

In this case the snoise3 (simplex noise 3D) function takes a vec3 and returns a float.

./src/shader/fragment.glsl


precision mediump float;

uniform vec2 u_resolution;
uniform float u_time;

#pragma glslify: snoise3 = require(glsl-noise/simplex/3d) // include some noise!!!

#pragma glslify: tint = require(&amp;amp;amp;amp;quot;./tint.glsl&amp;amp;amp;amp;quot;);

void main() {

// get the screen space
vec2 st = gl_FragCoord.xy/u_resolution;

// travel along the Z-dimension in time.
vec3 pos = vec3(st.xy, u_time);

// cycle the green color
vec3 rgb = vec3(0.1, cos(u_time), 0.5);

// generate the noise
vec3 noise = vec3(snoise3(pos),snoise3(pos),snoise3(pos));
// tint the noise with our function and draw the pixel
gl_FragColor = vec4(tint(noise, rgb), 1.0);
}

Run gulp build one last time. Refresh the browser and enjoy the show!

If you have any trouble getting this working, grab the git repository and check it against your own work.

I know it doesn’t look like much, but it’s got it where it counts. You should be able to start building the massive shader of your dreams knowing that it can remain clean, organized and digestible. If you have a great solution to a common WebGL problem, consider adding it as a Stack.gl package.

Avatar photo

Matthew Willox

Artist masquerading as a developer in a designers world.