Working (better) with GLSL source files

This post is a follow-on from a previous post, where I detailed the workflow I had developed for working with GLSL files, as part of developing 3D content for the web. Since then, I have refined my approach so I’m posting an update.

At a high level, my approach is as follows:

  • Store shaders as separate files, with common code imported using #include statements.
  • Use a custom Require.js plugin to inject these shaders into my JavaScript when I need them.
  • Allow overriding of #define statements to allow for customization of shaders at runtime.

Terrain

The rest of this post will go into details about how each piece works, and the underlying motivation.

An example application that uses this structure can be found here: https://github.com/felixpalmer/amd-three.js.

Storing shaders in their own files

Being able to edit GLSL code in individual files is a big deal for me, as it keeps the shader code separate from the JavaScript codebase and allows me to run a validator over the code to check there are no obvious bugs.

To actually perform the code validation, I’ve created a command line tool which compiles the GLSL code and reports any errors. Using this I can easily integrate with the editor I’m using to check for bugs every time I save. For more details, see this post.

Injecting shaders with Require.js

I use Require.js to organize my code, so I needed to find a way to pull my shader code into modules where I’d need them. Require.js has a text plugin, which does exactly that, you pass it the path to a file and it will load in the raw content of that file, like so:

// myText.txt
Hello world!

// main.js
require( ['text!myText.txt'], function ( myText ) {
  // myText now contains "Hello world!"
} );

This is great, except I wanted to do more, so I made my own Require.js plugin which added some functionality to the above, namely #include statement support and the ability to redefine #define statements from within JavaScript.

#include statments

Once my shaders started to grow, it became difficult to work with them a large monolithic files, especially when different shaders would share common code. To remedy this, I implemented support for the #include statement in both the Require.js shader plugin, and the command line validator. Usage is as you’d expect:

// shift.glsl
vec3 shift(vec3 p) {
    return p + vec3(500.0, 0, 0);
}

// main.vert
#include shift.glsl
void main() {
  // Example usage of included file, see shift.glsl for function definition
  vec3 shiftedPosition = shift(position);
  gl_Position = projectionMatrix * modelViewMatrix * vec4(shiftedPosition, 1.0);
}

Redefining #defines

As mentioned above, I’ve rolled my own plugin for injecting shaders, which works similarly to the text plugin. As well as supporting the #include function, it allows you to modify #define statements. I’ve found this useful for when I want to use a shader in different contexts, but with slightly different parameters, without having to pass it these values as uniform values. It can also be used to conditionally compile portions of the GLSL.

Here’s how it’s used:

define( ["shader!simple.frag", "shader!simple.vert"], function ( simpleFrag, simpleVert ) {
  simpleFrag.define( "faceColor", "vec3(1.0, 0, 0)" );

  // To actually get the text content of the shader, use myShader.value
  var material = new THREE.ShaderMaterial( {
    vertexShader: simpleVert.value,
    fragmentShader: simpleFrag.value
  });
} );

Example

To see how all this fits together, check out the example project.