shader-reload
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.
Quick Start
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 [email protected]
and [email protected]
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
.
Details
.shader.js
)
Shader Files (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.
Development Tool
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
Browserify Transform
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' ]
}
Use with glslify
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 and ES6 import
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
.
Production Bundling
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
Use with ThreeJS
This module includes two Three.js utility classes for convenience in the three folder, LiveShaderMaterial
and LiveRawShaderMaterial
.
Read more about it here.
API Doc
shader = require('reload-shader')(shaderSource)
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
require('reload-shader/receiver').on('touch', fn)
require('reload-shader/receiver').on('change', fn)
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.
Running from Source
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.
Why not Webpack/Parcel HMR?
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.
License
MIT, see LICENSE.md for details.