76

GitHub - mattdesl/shader-reload: An interface for reloading GLSL shaders on the...

 6 years ago
source link: https://github.com/mattdesl/shader-reload
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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.

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 [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

bulb 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.

warning 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 Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK