Skip to content

mattdesl/shader-reload

Repository files navigation

shader-reload

experimental

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.

screenshot

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 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 by shader-reload-cli.

Details

Shader Files (.shader.js)

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.

About

An interface for reloading GLSL shaders on the fly.

Resources

License

Stars

Watchers

Forks

Packages

No packages published