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:
#include
statements.#define
statements to allow for customization of shaders at runtime.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.
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.
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.
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);
}
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
});
} );
To see how all this fits together, check out the example project.