This is an experimental interface for live shader reloading in ThreeJS, regl, and other WebGL frameworks. This means you can edit your GLSL shader files without re-starting your entire application state. Works with regular strings, template strings, and/or transforms like brfs and glslify. Handles errors with a client-side popup that disappears on subsequent reloads.
See this tweet for a longer video.
You might also be interested in shader-reload-cli, a development server (drop-in replacement for budo) that supports live-reloading GLSL with glslify
built-in.
The code here could probably be adapted to work with other environments, e.g. Webpack/Express.
A quick way to test this is with the CLI version of this module, shader-reload-cli. This is a simple development server to get you up and running. For advanced projects, you may choose to use another development tool.
From your project folder using node@8.4.x
and npm@5.3.x
or higher:
npm install shader-reload-cli -g
Add a simple index.js
script like this:
index.js
const shader = require('./foo.shader');
// Initial source
console.log(shader.vertex, shader.fragment);
shader.on('change', () => {
// New source
console.log('Shader updated:', shader.vertex, shader.fragment);
});
It requires a shader module (which must have a .shader.js
extension) with the following syntax.
foo.shader.js
module.exports = require('shader-reload')({
vertex: '... shader source string ...',
fragment: '... shader source string ...'
});
Now you can start the development server and begin editing & developing your application. Saving the shader modules will trigger a 'change'
event without a hard page reload, but saving any other modules will reload the page as usual.
# opens the browser to localhost:9966/
shader-reload-cli src/index.js --open
💡 Under the hood, the
shader-reload-cli
script is running budo with glslify, so you can pass other options like--dir
and--port
. You can also add glslify transforms like glslify-hex to your package.json and they will get picked up byshader-reload-cli
.
You will need to separate your shader source into its own module, which must have the extension .shader.js
and require the shader-reload
function.
Pass statically analyzable GLSL source code to the function like this:
module.exports = require('shader-reload')({
vertex: '... shader source string ...',
fragment: '... shader source string ...'
});
The return value of the shader-reload
function is a Shader
object, which has the same vertex
and fragment
properties (which are mutated on file change). You can also attach a shader.on('change', fn)
event to react to changes.
Here is an example with inline shader source, using template strings.
blue.shader.js
module.exports = require('shader-reload')({
fragment: `
void main () {
gl_FragColor = vec4(0.0, 0.0, 1.0, 1.0);
}`,
vertex: `
void main () {
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos.xyz, 1.0);
}`
});
Then your ThreeJS source might look like this:
main.js
const shader = require('./blue.shader');
const material = new THREE.ShaderMaterial({
vertexShader: shader.vertex,
fragmentShader: shader.fragment
});
shader.on('change', () => {
// Mark shader for recompilation
material.vertexShader = shader.vertex;
material.fragmentShader = shader.fragment;
material.needsUpdate = true;
});
const mesh = new THREE.Mesh(geometry, material);
...
The examples include a LiveShaderMaterial which is a bit more robust for large applications.
Other than the .shader.js
modules, you also need to have this set up with your development tool. You have a few options:
- Use
shader-reload-cli
, it already includes glslify and shader reloading out of the box - Attach shader reloading to budo, see this gist for instructions
- Attach shader reloading to your existing development environment using WebSockets and broadcasting
'shader-reload'
events to clients
If you are using shader-reload-cli
, it already includes the transforms needed for shader reloading and glslify.
If you are using budo directly or your own browserify scripts, you will need to include a source transform, e.g. -t shader-reload/transform
, or in options:
...
browserify: {
transform: [ 'shader-reload/transform' ]
}
The shader-reload-cli
script already includes glslify support out of the box, so you can organize your shaders into their own files and require glsl modules from npm:
blue.shader.js
const glslify = require('glslify');
const path = require('path');
module.exports = require('shader-reload')({
vertex: glslify(path.resolve(__dirname, 'blue.vert')),
fragment: glslify(path.resolve(__dirname, 'blue.frag'))
});
If you are using budo directly or your own development server, make sure to include glslify
as a source transform before the shader-reload
transform.
Babel will replace import
statements with code that isn't easy to statically analyze, causing problems with this module. Instead of using import
for 'shader-reload'
, you should require()
it.
The same goes for requiring glslify
.
During production or when publishing the source to a non-development environment (i.e. without WebSockets), simply omit the shader-reload
transform. Shaders will not change after construction.
If you are using shader-reload-cli
and looking for a final JavaScript file for your static site, you can use browserify:
# install browserify
npm i browserify --save-dev
# bundle your index, with glslify if you need it
npx browserify index.js -t glslify > bundle.js
This module includes two Three.js utility classes for convenience in the three folder, LiveShaderMaterial
and LiveRawShaderMaterial
.
Read more about it here.
Pass in a shaderSource
with { vertex, fragment }
strings, and the Shader
emitter returned will contain the following:
shader.vertex // the latest vertex source
shader.fragment // the latest fragment source
shader.version // an integer, starts at 0, increased with each change
shader.on('touch', fn) // file was touched by fs file watcher
shader.on('change', fn) // vertex or fragment source was changed
This event is triggered after all shaders have been updated, allowing you to react to the event application-wide instead of on a per-shader basis.
Clone this repo and npm install
, then npm run example-three
(ThreeJS) or npm run example-regl
(regl). Edit the files inside the example/shaders/ folder and the shader will update without reloading the page. Saving other frontend files will reload the page as usual, restarting the application state.
In my experience, trying to apply Hot Module Replacement to an entire WebGL application leads to a lot of subtle issues because GL relies so heavily on state, GPU memory, performance, etc.
However, shaders are easy to "hot replace" since they are really just strings. I wanted a workflow that provides lightning fast GLSL reloads, works smoothly with glslify, and does not rely on a bundle-wide HMR solution (which would be overkill). This module also handles some special edge cases like handling shader errors with a client-side popup.
MIT, see LICENSE.md for details.