├── .gitignore ├── .npmignore ├── LICENSE.md ├── README.md ├── browser.js ├── lib ├── Shader.js ├── error-reporter.js ├── glslify-browser.js └── websocket-listener.js ├── package-lock.json ├── package.json ├── receiver.js ├── screenshots └── shader.gif ├── server.js ├── three ├── LiveRawShaderMaterial.js ├── LiveShaderMaterial.js └── README.md ├── transform.js └── wrapper.js /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | node_modules 3 | *.log 4 | .DS_Store 5 | bundle.js 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | node_modules 3 | *.log 4 | .DS_Store 5 | bundle.js 6 | test 7 | test.js 8 | demo/ 9 | .npmignore 10 | LICENSE.md -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2017 Matt DesLauriers 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 19 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 20 | OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # shader-reload 2 | 3 | [![experimental](http://badges.github.io/stability-badges/dist/experimental.svg)](http://github.com/badges/stability-badges) 4 | 5 | This is an experimental interface for live shader reloading in ThreeJS, [regl](https://github.com/regl-project/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](https://www.npmjs.com/package/glslify). Handles errors with a client-side popup that disappears on subsequent reloads. 6 | 7 | [![screenshot](./screenshots/shader.gif)](https://twitter.com/mattdesl/status/944246533016424453) 8 | 9 | > See [this tweet](https://twitter.com/mattdesl/status/944246533016424453) for a longer video. 10 | 11 | You might also be interested in [shader-reload-cli](https://github.com/mattdesl/shader-reload-cli), a development server (drop-in replacement for [budo](https://www.npmjs.com/package/budo)) that supports live-reloading GLSL with `glslify` built-in. 12 | 13 | The code here could _probably_ be adapted to work with other environments, e.g. Webpack/Express. 14 | 15 | ## Quick Start 16 | 17 | A quick way to test this is with the CLI version of this module, [shader-reload-cli](https://github.com/mattdesl/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](#development-tool). 18 | 19 | From your project folder using `node@8.4.x` and `npm@5.3.x` or higher: 20 | 21 | ```sh 22 | npm install shader-reload-cli -g 23 | ``` 24 | 25 | Add a simple `index.js` script like this: 26 | 27 | `index.js` 28 | 29 | ```js 30 | const shader = require('./foo.shader'); 31 | 32 | // Initial source 33 | console.log(shader.vertex, shader.fragment); 34 | 35 | shader.on('change', () => { 36 | // New source 37 | console.log('Shader updated:', shader.vertex, shader.fragment); 38 | }); 39 | ``` 40 | 41 | It requires a shader module (which must have a `.shader.js` extension) with the following syntax. 42 | 43 | `foo.shader.js` 44 | 45 | ```js 46 | module.exports = require('shader-reload')({ 47 | vertex: '... shader source string ...', 48 | fragment: '... shader source string ...' 49 | }); 50 | ``` 51 | 52 | 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. 53 | 54 | ```sh 55 | # opens the browser to localhost:9966/ 56 | shader-reload-cli src/index.js --open 57 | ``` 58 | 59 | > :bulb: Under the hood, the `shader-reload-cli` script is running [budo](https://www.npmjs.com/package/budo) with [glslify](https://www.npmjs.com/package/glslify), so you can pass other options like `--dir` and `--port`. You can also add glslify transforms like [glslify-hex](https://www.npmjs.com/package/glslify-hex) to your package.json and they will get picked up by `shader-reload-cli`. 60 | 61 | ## Details 62 | 63 | ### Shader Files (`.shader.js`) 64 | 65 | 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. 66 | 67 | Pass statically analyzable GLSL source code to the function like this: 68 | 69 | ```js 70 | module.exports = require('shader-reload')({ 71 | vertex: '... shader source string ...', 72 | fragment: '... shader source string ...' 73 | }); 74 | ``` 75 | 76 | 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. 77 | 78 | Here is an example with inline shader source, using template strings. 79 | 80 | `blue.shader.js` 81 | 82 | ```js 83 | module.exports = require('shader-reload')({ 84 | fragment: ` 85 | void main () { 86 | gl_FragColor = vec4(0.0, 0.0, 1.0, 1.0); 87 | }`, 88 | vertex: ` 89 | void main () { 90 | gl_Position = projectionMatrix * modelViewMatrix * vec4(pos.xyz, 1.0); 91 | }` 92 | }); 93 | ``` 94 | 95 | Then your ThreeJS source might look like this: 96 | 97 | `main.js` 98 | 99 | ```js 100 | const shader = require('./blue.shader'); 101 | 102 | const material = new THREE.ShaderMaterial({ 103 | vertexShader: shader.vertex, 104 | fragmentShader: shader.fragment 105 | }); 106 | 107 | shader.on('change', () => { 108 | // Mark shader for recompilation 109 | material.vertexShader = shader.vertex; 110 | material.fragmentShader = shader.fragment; 111 | material.needsUpdate = true; 112 | }); 113 | 114 | const mesh = new THREE.Mesh(geometry, material); 115 | ... 116 | ``` 117 | 118 | The examples include a [LiveShaderMaterial](./example/materials/LiveShaderMaterial.js) which is a bit more robust for large applications. 119 | 120 | ### Development Tool 121 | 122 | Other than the `.shader.js` modules, you also need to have this set up with your development tool. You have a few options: 123 | 124 | - Use `shader-reload-cli`, it already includes glslify and shader reloading out of the box 125 | - Attach shader reloading to [budo](https://www.npmjs.com/package/budo), see [this gist](https://gist.github.com/mattdesl/ad4542d7a21e920b8ad0fba0c8e8e947) for instructions 126 | - Attach shader reloading to your existing development environment using WebSockets and broadcasting `'shader-reload'` events to clients 127 | 128 | ### Browserify Transform 129 | 130 | If you are using `shader-reload-cli`, it already includes the transforms needed for shader reloading and glslify. 131 | 132 | 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: 133 | 134 | ```js 135 | ... 136 | browserify: { 137 | transform: [ 'shader-reload/transform' ] 138 | } 139 | ``` 140 | 141 | ## Use with glslify 142 | 143 | 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: 144 | 145 | `blue.shader.js` 146 | 147 | ```js 148 | const glslify = require('glslify'); 149 | const path = require('path'); 150 | 151 | module.exports = require('shader-reload')({ 152 | vertex: glslify(path.resolve(__dirname, 'blue.vert')), 153 | fragment: glslify(path.resolve(__dirname, 'blue.frag')) 154 | }); 155 | ``` 156 | 157 | 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. 158 | 159 | ## :warning: Babel and ES6 `import` 160 | 161 | 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. 162 | 163 | The same goes for requiring `glslify`. 164 | 165 | ## Production Bundling 166 | 167 | 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. 168 | 169 | If you are using `shader-reload-cli` and looking for a final JavaScript file for your static site, you can use browserify: 170 | 171 | ```sh 172 | # install browserify 173 | npm i browserify --save-dev 174 | 175 | # bundle your index, with glslify if you need it 176 | npx browserify index.js -t glslify > bundle.js 177 | ``` 178 | 179 | ## Use with ThreeJS 180 | 181 | This module includes two Three.js utility classes for convenience in the [three](./three) folder, `LiveShaderMaterial` and `LiveRawShaderMaterial`. 182 | 183 | Read more about it [here](./three/README.md). 184 | 185 | ## API Doc 186 | 187 | #### `shader = require('reload-shader')(shaderSource)` 188 | 189 | Pass in a `shaderSource` with `{ vertex, fragment }` strings, and the `Shader` emitter returned will contain the following: 190 | 191 | ```js 192 | shader.vertex // the latest vertex source 193 | shader.fragment // the latest fragment source 194 | shader.version // an integer, starts at 0, increased with each change 195 | shader.on('touch', fn) // file was touched by fs file watcher 196 | shader.on('change', fn) // vertex or fragment source was changed 197 | ``` 198 | 199 | #### `require('reload-shader/receiver').on('touch', fn)` 200 | #### `require('reload-shader/receiver').on('change', fn)` 201 | 202 | 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. 203 | 204 | ## Running from Source 205 | 206 | 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/](./examples/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. 207 | 208 | ## Why not Webpack/Parcel HMR? 209 | 210 | 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. 211 | 212 | 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. 213 | 214 | ## License 215 | 216 | MIT, see [LICENSE.md](http://github.com/mattdesl/shader-reload/blob/master/LICENSE.md) for details. 217 | -------------------------------------------------------------------------------- /browser.js: -------------------------------------------------------------------------------- 1 | var listener = require('./lib/websocket-listener'); 2 | var Shader = require('./lib/Shader'); 3 | var errors = require('./lib/error-reporter'); 4 | var receiver = require('./receiver'); 5 | 6 | var shaderMap = {}; 7 | 8 | module.exports = createShader; 9 | function createShader (opt, filename) { 10 | opt = opt || {}; 11 | var shader = new Shader(opt); 12 | if (shaderMap && typeof filename === 'string') { 13 | if (filename in shaderMap) { 14 | // File already exists in cache, we could warn the user...? 15 | } 16 | shaderMap[filename] = shader; 17 | } 18 | return shader; 19 | } 20 | 21 | function reloadShaders (updates) { 22 | if (!shaderMap) return; 23 | updates = (Array.isArray(updates) ? updates : [ updates ]).filter(Boolean); 24 | if (updates.length === 0) return; 25 | 26 | var hasTouched = false; 27 | var hasChanged = false; 28 | updates.forEach(function (update) { 29 | var file = update.file; 30 | if (!file) { 31 | // No file field, just skip this... 32 | return; 33 | } 34 | if (file in shaderMap) { 35 | var shader = shaderMap[file]; 36 | var oldVertex = shader.vertex; 37 | var oldFragment = shader.fragment; 38 | shader.vertex = update.vertex || ''; 39 | shader.fragment = update.fragment || ''; 40 | shader.emit('touch'); 41 | hasTouched = true; 42 | if (oldVertex !== shader.vertex || oldFragment !== shader.fragment) { 43 | shader.emit('change'); 44 | shader.version++; 45 | hasChanged = true; 46 | } 47 | } else { 48 | // We have a file field but somehow it didn't end up in our shader map... 49 | // Maybe user isn't using the reload-shader function? 50 | } 51 | }); 52 | 53 | // broadcast change events 54 | if (hasTouched) receiver.emit('touch'); 55 | if (hasChanged) receiver.emit('change'); 56 | } 57 | 58 | // Listen for LiveReload connections during development 59 | listener({ 60 | route: '/shader-reload' 61 | }, function (data) { 62 | if (data.event === 'shader-reload' || data.event === 'reload') { 63 | errors.hide(); 64 | } 65 | if (data.event === 'shader-reload' && data.updates && data.updates.length > 0) { 66 | reloadShaders(data.updates); 67 | } else if (data.event === 'shader-error' && data.error) { 68 | errors.show(data.error); 69 | } 70 | }); 71 | -------------------------------------------------------------------------------- /lib/Shader.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require('events').EventEmitter; 2 | var inherits = require('util').inherits; 3 | 4 | function Shader (opt) { 5 | EventEmitter.call(this); 6 | opt = opt || {}; 7 | this.vertex = opt.vertex || ''; 8 | this.fragment = opt.fragment || ''; 9 | this.version = 0; 10 | this.setMaxListeners(10000); 11 | } 12 | 13 | inherits(Shader, EventEmitter); 14 | 15 | module.exports = Shader; 16 | -------------------------------------------------------------------------------- /lib/error-reporter.js: -------------------------------------------------------------------------------- 1 | var popupContainer, popupText; 2 | 3 | module.exports.hide = clearPopup; 4 | function clearPopup () { 5 | if (popupContainer && popupContainer.parentNode) { 6 | popupContainer.parentNode.removeChild(popupContainer); 7 | } 8 | if (popupText && popupText.parentNode) { 9 | popupText.parentNode.removeChild(popupText); 10 | } 11 | popupContainer = null; 12 | popupText = null; 13 | } 14 | 15 | module.exports.show = show; 16 | function show (message) { 17 | if (popupText) { 18 | popupText.textContent = message; 19 | return; 20 | } 21 | 22 | var element = document.createElement('div'); 23 | var child = document.createElement('pre'); 24 | child.textContent = message; 25 | 26 | css(element, { 27 | position: 'fixed', 28 | top: '0', 29 | left: '0', 30 | width: '100%', 31 | zIndex: '100000000', 32 | padding: '0', 33 | margin: '0', 34 | 'box-sizing': 'border-box', 35 | background: 'transparent', 36 | display: 'block', 37 | overflow: 'initial' 38 | }); 39 | css(child, { 40 | padding: '20px', 41 | overflow: 'initial', 42 | zIndex: '100000000', 43 | 'box-sizing': 'border-box', 44 | background: '#fff', 45 | display: 'block', 46 | 'font-size': '12px', 47 | 'font-weight': 'normal', 48 | 'font-family': 'monospace', 49 | 'word-wrap': 'break-word', 50 | 'white-space': 'pre-wrap', 51 | color: '#ff0000', 52 | margin: '10px', 53 | border: '1px dashed hsla(0, 0%, 50%, 0.25)', 54 | borderRadius: '5px', 55 | boxShadow: '0px 10px 20px rgba(0, 0, 0, 0.2)' 56 | }); 57 | element.appendChild(child); 58 | document.body.appendChild(element); 59 | popupText = child; 60 | popupContainer = element; 61 | } 62 | 63 | function css (element, obj) { 64 | for (var k in obj) { 65 | if (obj.hasOwnProperty(k)) element.style[k] = obj[k]; 66 | } 67 | return obj; 68 | } 69 | -------------------------------------------------------------------------------- /lib/glslify-browser.js: -------------------------------------------------------------------------------- 1 | // From: 2 | // https://github.com/glslify/glslify/blob/master/browser.js 3 | 4 | // Included manually in this module to speed up install time. 5 | 6 | module.exports = function (strings) { 7 | if (typeof strings === 'string') strings = [strings]; 8 | var exprs = [].slice.call(arguments, 1); 9 | var parts = []; 10 | for (var i = 0; i < strings.length - 1; i++) { 11 | parts.push(strings[i], exprs[i] || ''); 12 | } 13 | parts.push(strings[i]); 14 | return parts.join(''); 15 | }; 16 | -------------------------------------------------------------------------------- /lib/websocket-listener.js: -------------------------------------------------------------------------------- 1 | module.exports = connect; 2 | 3 | function connect (opt, cb) { 4 | opt = opt || {}; 5 | 6 | // If budo is running, we will try to hook into that since it 7 | // will produce less spam in console on reconnect errors. 8 | const devClient = window['budo-livereload']; 9 | if (devClient && typeof devClient.listen === 'function') { 10 | return devClient.listen(cb); 11 | } 12 | 13 | // Otherwise we will just create our own socket interface 14 | var route = typeof opt.route === 'undefined' ? '/' : opt.route; 15 | 16 | var reconnectPoll = 1000; 17 | var maxRetries = 50; 18 | var retries = 0; 19 | var reconnectInterval; 20 | var isReconnecting = false; 21 | var protocol = document.location.protocol; 22 | var hostname = document.location.hostname; 23 | var port = document.location.port; 24 | var host = hostname + ':' + port; 25 | 26 | var isIOS = /(iOS|iPhone|iPad|iPod)/i.test(navigator.userAgent); 27 | var isSSL = /^https:/i.test(protocol); 28 | var socket = createWebSocket(); 29 | 30 | function scheduleReconnect () { 31 | if (isIOS && isSSL) { 32 | // Special case for iOS with a self-signed certificate. 33 | return; 34 | } 35 | if (isSSL) { 36 | // Don't attempt to re-connect in SSL since it will likely be insecure 37 | return; 38 | } 39 | if (retries >= maxRetries) { 40 | return; 41 | } 42 | if (!isReconnecting) { 43 | isReconnecting = true; 44 | } 45 | retries++; 46 | clearTimeout(reconnectInterval); 47 | reconnectInterval = setTimeout(reconnect, reconnectPoll); 48 | } 49 | 50 | function reconnect () { 51 | if (socket) { 52 | // force close the existing socket 53 | socket.onclose = function () {}; 54 | socket.close(); 55 | } 56 | socket = createWebSocket(); 57 | } 58 | 59 | function createWebSocket () { 60 | var wsProtocol = isSSL ? 'wss://' : 'ws://'; 61 | var wsUrl = wsProtocol + host + route; 62 | var ws = new window.WebSocket(wsUrl); 63 | ws.onmessage = function (event) { 64 | var data; 65 | try { 66 | data = JSON.parse(event.data); 67 | } catch (err) { 68 | console.warn('Error parsing WebSocket Server data: ' + event.data); 69 | return; 70 | } 71 | 72 | cb(data); 73 | }; 74 | ws.onclose = function (ev) { 75 | if (ev.code === 1000 || ev.code === 1001) { 76 | // Browser is navigating away. 77 | return; 78 | } 79 | scheduleReconnect(); 80 | }; 81 | ws.onopen = function () { 82 | if (isReconnecting) { 83 | isReconnecting = false; 84 | retries = 0; 85 | } 86 | }; 87 | ws.onerror = function () { 88 | return false; 89 | }; 90 | return ws; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shader-reload", 3 | "version": "2.0.1", 4 | "description": "A few tools for GLSL shader reloading at runtime.", 5 | "main": "./wrapper.js", 6 | "license": "MIT", 7 | "author": { 8 | "name": "Matt DesLauriers", 9 | "email": "dave.des@gmail.com", 10 | "url": "https://github.com/mattdesl" 11 | }, 12 | "dependencies": { 13 | "jsesc": "^2.5.1", 14 | "pumpify": "^1.3.5", 15 | "static-module": "^2.0.0", 16 | "through": "^2.3.8" 17 | }, 18 | "semistandard": { 19 | "globals": [ 20 | "THREE" 21 | ] 22 | }, 23 | "devDependencies": { 24 | "budo": "^11.1.3", 25 | "glsl-noise": "0.0.0", 26 | "glslify": "^6.1.0", 27 | "perspective-camera": "^2.0.1", 28 | "primitive-icosphere": "^1.0.2", 29 | "primitive-plane": "^2.0.0", 30 | "regl": "^1.3.0", 31 | "semistandard": "^11.0.0", 32 | "three": "^0.89.0" 33 | }, 34 | "scripts": { 35 | "example-three": "glsl-server example/three.js", 36 | "example-regl": "glsl-server example/regl.js" 37 | }, 38 | "keywords": [ 39 | "shader", 40 | "glsl", 41 | "glslify", 42 | "editor", 43 | "dev", 44 | "ux", 45 | "dx", 46 | "reload", 47 | "live", 48 | "edit", 49 | "update", 50 | "hot", 51 | "module", 52 | "replacement", 53 | "shader", 54 | "webgl", 55 | "threejs", 56 | "three" 57 | ], 58 | "repository": { 59 | "type": "git", 60 | "url": "git://github.com/mattdesl/shader-reload.git" 61 | }, 62 | "homepage": "https://github.com/mattdesl/shader-reload", 63 | "bugs": { 64 | "url": "https://github.com/mattdesl/shader-reload/issues" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /receiver.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require('events').EventEmitter; 2 | var emitter = new EventEmitter(); 3 | emitter.setMaxListeners(10000); 4 | module.exports = emitter; 5 | -------------------------------------------------------------------------------- /screenshots/shader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/shader-reload/e23312ce571d58f48b408149f7b64cd8bd64a149/screenshots/shader.gif -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const sources = {}; 4 | const shaderFileRegex = /\.shader\.js$/i; 5 | 6 | module.exports.updateShaderSource = function (file, opt) { 7 | sources[file] = opt; 8 | }; 9 | 10 | module.exports.isShaderReload = function (deps) { 11 | return deps.length > 0 && deps.every(dep => shaderFileRegex.test(dep)); 12 | }; 13 | 14 | module.exports.isShaderError = function (err) { 15 | return err.filename && shaderFileRegex.test(err.filename); 16 | }; 17 | 18 | module.exports.getErrorEvent = function (error) { 19 | return { 20 | event: 'shader-error', 21 | error: error.message 22 | }; 23 | }; 24 | 25 | module.exports.getEvent = function (deps, cwd) { 26 | cwd = cwd || process.cwd(); 27 | 28 | // Use the same format as "__filename" in the browser 29 | const files = deps.map(dep => { 30 | return path.join(path.sep, path.relative(process.cwd(), dep)); 31 | }); 32 | 33 | const updates = getUpdates(files); 34 | return { 35 | event: 'shader-reload', 36 | updates: updates 37 | }; 38 | }; 39 | 40 | function getUpdates (files) { 41 | return files.map(file => { 42 | if (file in sources) { 43 | return Object.assign({}, sources[file], { 44 | file 45 | }); 46 | } else { 47 | return null; 48 | } 49 | }).filter(Boolean); 50 | } 51 | -------------------------------------------------------------------------------- /three/LiveRawShaderMaterial.js: -------------------------------------------------------------------------------- 1 | // We hook into needsUpdate so it will lazily check 2 | // shader updates every frame of rendering. 3 | 4 | var inherits = require('util').inherits; 5 | var isDevelopment = process.env.NODE_ENV !== 'production'; 6 | 7 | function LiveShaderMaterial (shader, parameters) { 8 | parameters = parameters || {}; 9 | THREE.RawShaderMaterial.call(this, parameters); 10 | this.shader = shader; 11 | if (this.shader) { 12 | this.vertexShader = this.shader.vertex; 13 | this.fragmentShader = this.shader.fragment; 14 | } 15 | this.shaderVersion = this.shader ? this.shader.version : undefined; 16 | this._needsUpdate = true; 17 | } 18 | 19 | inherits(LiveShaderMaterial, THREE.RawShaderMaterial); 20 | 21 | // Handle material.clone() and material.copy() functions properly 22 | LiveShaderMaterial.prototype.copy = function (source) { 23 | THREE.RawShaderMaterial.prototype.copy.call(this, source); 24 | this.shader = source.shader; 25 | this.shaderVersion = this.shader.version; 26 | this.vertexShader = this.shader.vertex; 27 | this.fragmentShader = this.shader.fragment; 28 | return this; 29 | }; 30 | 31 | // Check if shader is out of date, if so we should mark this as dirty 32 | LiveShaderMaterial.prototype.isShaderUpdate = function () { 33 | var shader = this.shader; 34 | 35 | var dirty = false; 36 | if (isDevelopment) { 37 | // If source has changed, recompile. 38 | // We could also do a string equals check, but since this is 39 | // done per frame across potentially thousands of objects, 40 | // it's probably better to just use the integer version check. 41 | dirty = this.shaderVersion !== shader.version; 42 | if (dirty) { 43 | this.shaderVersion = shader.version; 44 | this.vertexShader = shader.vertex; 45 | this.fragmentShader = shader.fragment; 46 | this.needsUpdate = true; 47 | } 48 | } 49 | 50 | return dirty; 51 | }; 52 | 53 | // Hook into needsUpdate so we can check shader version per frame 54 | Object.defineProperty(LiveShaderMaterial.prototype, 'needsUpdate', { 55 | get: function () { 56 | return this.isShaderUpdate() || this._needsUpdate; 57 | }, 58 | set: function (v) { 59 | this._needsUpdate = v; 60 | } 61 | }); 62 | 63 | module.exports = LiveShaderMaterial; 64 | -------------------------------------------------------------------------------- /three/LiveShaderMaterial.js: -------------------------------------------------------------------------------- 1 | // We hook into needsUpdate so it will lazily check 2 | // shader updates every frame of rendering. 3 | 4 | var inherits = require('util').inherits; 5 | var isDevelopment = process.env.NODE_ENV !== 'production'; 6 | 7 | function LiveShaderMaterial (shader, parameters) { 8 | parameters = parameters || {}; 9 | THREE.ShaderMaterial.call(this, parameters); 10 | this.shader = shader; 11 | if (this.shader) { 12 | this.vertexShader = this.shader.vertex; 13 | this.fragmentShader = this.shader.fragment; 14 | } 15 | this.shaderVersion = this.shader ? this.shader.version : undefined; 16 | this._needsUpdate = true; 17 | } 18 | 19 | inherits(LiveShaderMaterial, THREE.ShaderMaterial); 20 | 21 | // Handle material.clone() and material.copy() functions properly 22 | LiveShaderMaterial.prototype.copy = function (source) { 23 | THREE.ShaderMaterial.prototype.copy.call(this, source); 24 | this.shader = source.shader; 25 | this.shaderVersion = this.shader.version; 26 | this.vertexShader = this.shader.vertex; 27 | this.fragmentShader = this.shader.fragment; 28 | return this; 29 | }; 30 | 31 | // Check if shader is out of date, if so we should mark this as dirty 32 | LiveShaderMaterial.prototype.isShaderUpdate = function () { 33 | var shader = this.shader; 34 | 35 | var dirty = false; 36 | if (isDevelopment) { 37 | // If source has changed, recompile. 38 | // We could also do a string equals check, but since this is 39 | // done per frame across potentially thousands of objects, 40 | // it's probably better to just use the integer version check. 41 | dirty = this.shaderVersion !== shader.version; 42 | if (dirty) { 43 | this.shaderVersion = shader.version; 44 | this.vertexShader = shader.vertex; 45 | this.fragmentShader = shader.fragment; 46 | this.needsUpdate = true; 47 | } 48 | } 49 | 50 | return dirty; 51 | }; 52 | 53 | // Hook into needsUpdate so we can check shader version per frame 54 | Object.defineProperty(LiveShaderMaterial.prototype, 'needsUpdate', { 55 | get: function () { 56 | return this.isShaderUpdate() || this._needsUpdate; 57 | }, 58 | set: function (v) { 59 | this._needsUpdate = v; 60 | } 61 | }); 62 | 63 | module.exports = LiveShaderMaterial; 64 | -------------------------------------------------------------------------------- /three/README.md: -------------------------------------------------------------------------------- 1 | # ThreeJS Materials 2 | 3 | The `shader-reload` module maintains two ThreeJS utility materials: LiveShaderMaterial and LiveRawShaderMaterial. This makes it easier to work with Shader Reloading in ThreeJS, and they will auto-update when the passed shader is changed. 4 | 5 | ```js 6 | const LiveShaderMaterial = require('shader-reload/three/LiveShaderMaterial'); 7 | const shader = require('./fancy-effect.shader'); 8 | 9 | // Pass the shader to set up vertexShader / fragmentShader 10 | const material = new LiveShaderMaterial(shader, { 11 | // Pass shader material parameters as usual 12 | side: THREE.DoubleSide, 13 | uniforms: { 14 | time: { value: 0 } 15 | } 16 | }); 17 | ``` 18 | 19 | When NODE_ENV is `'production'`, the shader reload check will be ignored to avoid any performance impacts. 20 | 21 | _Note:_ These modules already assume that `THREE` exists in global (window) scope. -------------------------------------------------------------------------------- /transform.js: -------------------------------------------------------------------------------- 1 | const staticModule = require('static-module'); 2 | const through = require('through'); 3 | const path = require('path'); 4 | const escape = require('jsesc'); 5 | const glslifyConcat = require('./lib/glslify-browser'); 6 | const pumpify = require('pumpify'); 7 | const reloader = require('./server'); 8 | const entryFilePath = require.resolve('./browser.js'); 9 | 10 | module.exports = function shaderReloadTransform (file, opts) { 11 | if (!/\.shader\.js$/i.test(file)) return through(); 12 | 13 | if (!opts) opts = {}; 14 | const vars = opts.vars || { 15 | __filename: file, 16 | __dirname: path.dirname(file) 17 | }; 18 | 19 | const glslify = staticModule({ 'glslify': glslifyHandler }, { vars: vars }); 20 | const reload = staticModule({ 'shader-reload': reloadHandler }, { vars: vars }); 21 | 22 | return pumpify(glslify, reload); 23 | 24 | function glslifyHandler (parts) { 25 | return `'${escape(glslifyConcat(parts))}'`; 26 | } 27 | 28 | function reloadHandler (opt) { 29 | const fileRelative = path.join(path.sep, path.relative(process.cwd(), file)); 30 | const vertex = opt.vertex || ''; 31 | const fragment = opt.fragment || ''; 32 | reloader.updateShaderSource(fileRelative, { vertex, fragment }); 33 | 34 | return [ 35 | `require('${escape(entryFilePath)}')({\n`, 36 | ` vertex: '${escape(vertex)}',\n`, 37 | ` fragment: '${escape(fragment)}'\n`, 38 | `}, '${escape(fileRelative)}')` 39 | ].join(''); 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /wrapper.js: -------------------------------------------------------------------------------- 1 | var Shader = require('./lib/Shader'); 2 | 3 | module.exports = createShader; 4 | function createShader (opt) { 5 | opt = opt || {}; 6 | return new Shader(opt); 7 | } 8 | --------------------------------------------------------------------------------