├── .babelrc ├── .gitignore ├── .npmignore ├── LICENSE.md ├── README.md ├── client.js ├── index.html ├── lib └── three-hmr.js ├── materials ├── inline.js ├── noise.js └── shaders │ ├── noise.frag │ └── noise.vert ├── package.json └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | presets: [ "es2015" ] 3 | } -------------------------------------------------------------------------------- /.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) 2016 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 | # webpack-three-hmr-test 2 | 3 | [![experimental](http://badges.github.io/stability-badges/dist/experimental.svg)](http://github.com/badges/stability-badges) 4 | 5 | Hot module replacement with ThreeJS + Webpack, reloading shaders without destroying application state. 6 | 7 | Click below to see a video demo. 8 | 9 | [](https://www.youtube.com/watch?v=XPYKYkD_5A0) 10 | 11 | [video demo](https://www.youtube.com/watch?v=XPYKYkD_5A0) 12 | 13 | This is **experimental** and a **proof of concept**. It only exists for reference and to hash out ideas for how it can all work in practice. If you want to help, feel free to discuss ideas/etc in the issue tracker. 14 | 15 | ## How it Works 16 | 17 | Shader materials intended to be hot-replaced must be defined in their own file with top-level `vertexShader` and `fragmentShader` strings. Typically this module will export a `RawShaderMaterial`, although `ShaderMaterial` should also work. 18 | 19 | Currently, the `module.hot` boilerplate is manually defined. See the following: 20 | 21 | - [materials/inline.js](./materials/inline.js) - plain GLSL with ES2015 template strings 22 | - [materials/noise.js](./materials/noise.js) - more advanced, using [glslify](https://github.com/stackgl/glslify) 23 | 24 | 25 | In a future version, this will be instrumented by a Babel transform, so that your "hot-shader" modules can just look like this: 26 | 27 | ```js 28 | // my-shader.js 29 | const vertexShader = '...' 30 | const fragmentShader = '...' 31 | 32 | export default function () { 33 | return new THREE.RawShaderMaterial({ 34 | vertexShader, fragmentShader 35 | }) 36 | } 37 | ``` 38 | 39 | ## glslify 40 | 41 | This approach works with regular [inline strings](./materials/inline.js) (like ES2015 template strings), but we can also take advantage of [glslify](https://github.com/stackgl/glslify) for a more advanced/powerful workflow. 42 | 43 | This allows our GLSL to be separated into files, so that it receives its own syntax highlighting and auto-completion. It also supports source transforms like hex colors and import statements to pull in GLSL components from npm. 44 | 45 | ```glsl 46 | precision mediump float; 47 | 48 | // shader components from npm 49 | import noise from 'glsl-noise/simplex/3d'; 50 | 51 | varying vec2 vUv; 52 | 53 | void main () { 54 | float n = noise(vec3(vUv.xy * 10.0, 1.0)); 55 | n = smoothstep(0.0, 0.1, n); 56 | 57 | // hex colors for convenience 58 | vec3 color = mix(vec3(#03A9F4), vec3(#3F51B5), n); 59 | gl_FragColor = vec4(color, 1.0); 60 | } 61 | ``` 62 | 63 | ## `ify-loader` 64 | 65 | The webpack config is using [ify-loader](https://github.com/hughsk/ify-loader). This is not strictly necessary, but solves some `glslify` issues for us: 66 | 67 | - First, it allows third-party modules with browserify transforms (like glslify) to be resolved and bundled automatically, e.g. [three-vignette-background](https://github.com/mattdesl/three-vignette-background) 68 | - Second, it allows us to specify `"browserify"` and `"glslify"` configuration in our local [package.json](./package.json) 69 | - Third, it allows us to use `glslify(file, { ... })` syntax without relying on Webpack `require` overloads 70 | 71 | ## Usage 72 | 73 | Clone, install and run: 74 | 75 | ```sh 76 | git clone https://github.com/mattdesl/webpack-three-hmr-test.git 77 | 78 | # setup 79 | cd webpack-three-hmr-test 80 | npm install 81 | 82 | # start dev server 83 | npm start 84 | ``` 85 | 86 | Now open `localhost:9966`. 87 | 88 | Make changes to [client.js](./client.js) and the browser will trigger a hard-refresh. Make changes to [materials/inline.js](./materials/inline.js) or the GLSL in [materials/shaders](./materials/shaders) and they will be updated with Hot Module Replacement, to avoid destroying application state. 89 | 90 | ## Roadmap 91 | 92 | Since I am new to webpack and authoring babel plugins, it will probably be a while before this is all realized properly. Some things to focus on, and areas I could use help with: 93 | 94 | - How do I write a babel plugin for instrumenting HMR? 95 | - How should the end-user "hot-shader" look? 96 | - How can we support both ES2015 and ES5 for the end-user's "hot-shaders"? 97 | - How can we improve error handling for `glslify`, e.g. a missing import? See react-transform errors for example. 98 | - How can we improve ThreeJS shader compile errors? This will require changes to ThreeJS core. 99 | - How can we support the same features in browserify workflows? `browserify-hmr` leaves a lot to be desired. 100 | - Does adding/removing uniforms and attributes lead to any problems? 101 | 102 | ## License 103 | 104 | MIT, see [LICENSE.md](http://github.com/mattdesl/webpack-three-hmr-test/blob/master/LICENSE.md) for details. 105 | -------------------------------------------------------------------------------- /client.js: -------------------------------------------------------------------------------- 1 | global.THREE = require('three') 2 | const createOrbitViewer = require('three-orbit-viewer')(THREE) 3 | const createBackground = require('three-vignette-background') 4 | const noiseMaterial = require('./materials/noise') 5 | const inlineMaterial = require('./materials/inline') 6 | 7 | const app = createOrbitViewer({ 8 | clearColor: 0x000000, 9 | clearAlpha: 1.0, 10 | fov: 45, 11 | position: new THREE.Vector3(3, 2, -3) 12 | }) 13 | 14 | const bg = createBackground() 15 | app.scene.add(bg) 16 | 17 | const boxGeo = new THREE.BoxGeometry(1, 1, 1) 18 | const mat1 = noiseMaterial() 19 | const box = new THREE.Mesh(boxGeo, mat1) 20 | app.scene.add(box) 21 | 22 | const sphereGeo = new THREE.SphereGeometry(1, 64, 64) 23 | const mat2 = inlineMaterial() 24 | const sphere = new THREE.Mesh(sphereGeo, mat2) 25 | sphere.scale.multiplyScalar(0.5) 26 | app.scene.add(sphere) 27 | 28 | let angle = 0 29 | app.on('tick', dt => { 30 | var width = window.innerWidth 31 | var height = window.innerHeight 32 | bg.style({ 33 | aspect: width / height, 34 | aspectCorrection: true, 35 | scale: 2.5, 36 | grainScale: 0 37 | }) 38 | 39 | box.rotation.y += dt * 0.0002 40 | const r = 2 41 | angle += dt * 0.0006 42 | sphere.position.x = Math.cos(angle) * r 43 | sphere.position.z = Math.sin(angle) * r 44 | }) -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | webpack-three-hmr-test 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /lib/three-hmr.js: -------------------------------------------------------------------------------- 1 | /* 2 | A babel plugin should help us inline 3 | this file into our bundle, so that users 4 | do not need to manually interact with it. 5 | */ 6 | 7 | var window = require('global/window') 8 | 9 | // cache 10 | // Stores all materials created by a hot module. 11 | module.exports.cache = function (filename) { 12 | var cache 13 | if (window.__hmrShaderCache) { 14 | cache = window.__hmrShaderCache 15 | } else { 16 | cache = {} 17 | Object.defineProperty(window, '__hmrShaderCache', { 18 | configurable: true, 19 | enumerable: false, 20 | writable: false, 21 | value: cache 22 | }) 23 | } 24 | if (!cache[filename]) { 25 | cache[filename] = {} 26 | } 27 | return cache[filename] 28 | } 29 | 30 | // Enables HMR on the given material 31 | module.exports.enable = enable 32 | function enable (cache, material) { 33 | var uuid = material.uuid 34 | if (cache[uuid]) { 35 | throw new Error('This material already has HMR set.') 36 | } 37 | 38 | cache[uuid] = material 39 | 40 | var oldDispose = material.dispose 41 | material.dispose = function () { 42 | if (cache[uuid]) delete cache[uuid] 43 | return oldDispose.call(material) 44 | } 45 | 46 | var oldClone = material.clone 47 | material.clone = function () { 48 | var newObj = oldClone.call(material) 49 | enable(cache, newObj) 50 | return newObj 51 | } 52 | } 53 | 54 | module.exports.update = function (cache, opt) { 55 | console.log('[ThreeJS]', 'Patching shaders') 56 | Object.keys(cache).forEach(uuid => { 57 | var material = cache[uuid] 58 | if (!material) return 59 | material.setValues(opt) 60 | material.needsUpdate = true 61 | }) 62 | } 63 | -------------------------------------------------------------------------------- /materials/inline.js: -------------------------------------------------------------------------------- 1 | /* 2 | An example of simple inline shaders, 3 | without using glslify. 4 | 5 | The "three-hmr" boilerplate will eventually 6 | be removed, and instrumented automatically by 7 | a babel transform. 8 | */ 9 | 10 | const hmr = require('../lib/three-hmr') 11 | const cache = hmr.cache(__filename) 12 | 13 | const vertexShader = ` 14 | attribute vec4 position; 15 | attribute vec2 uv; 16 | uniform mat4 projectionMatrix; 17 | uniform mat4 modelViewMatrix; 18 | varying vec2 vUv; 19 | void main () { 20 | vUv = uv; 21 | gl_Position = projectionMatrix * modelViewMatrix * position; 22 | } 23 | `.trim() 24 | 25 | const fragmentShader = ` 26 | precision mediump float; 27 | varying vec2 vUv; 28 | 29 | const vec3 colorA = vec3(1.0, 0.0, 0.0); 30 | const vec3 colorB = vec3(1.0, 1.0, 1.0); 31 | 32 | float checker(vec2 uv, float repeats) { 33 | float cx = floor(repeats * uv.x); 34 | float cy = floor(repeats * uv.y); 35 | float result = mod(cx + cy, 2.0); 36 | return sign(result); 37 | } 38 | 39 | void main () { 40 | float d = checker(vUv, 10.0); 41 | vec3 color = mix(colorA, colorB, d); 42 | gl_FragColor = vec4(color, 1.0); 43 | } 44 | `.trim() 45 | 46 | module.exports = function (opt) { 47 | const material = new THREE.RawShaderMaterial({ 48 | vertexShader, fragmentShader 49 | }) 50 | hmr.enable(cache, material) 51 | return material 52 | } 53 | 54 | if (module.hot) { 55 | module.hot.accept(err => { 56 | if (err) throw errr 57 | }) 58 | hmr.update(cache, { 59 | vertexShader, fragmentShader 60 | }) 61 | } 62 | -------------------------------------------------------------------------------- /materials/noise.js: -------------------------------------------------------------------------------- 1 | /* 2 | An example of statically inlining GLSL 3 | with glslify for source transforms, such as 4 | "import" statements and hex colors. 5 | 6 | The "three-hmr" boilerplate will eventually 7 | be removed, and instrumented automatically by 8 | a babel transform. 9 | */ 10 | 11 | const hmr = require('../lib/three-hmr') 12 | const cache = hmr.cache(__filename) 13 | const glslify = require('glslify') 14 | 15 | const vertexShader = glslify('./shaders/noise.vert') 16 | const fragmentShader = glslify('./shaders/noise.frag') 17 | 18 | module.exports = function (opt) { 19 | const material = new THREE.RawShaderMaterial({ 20 | vertexShader, fragmentShader 21 | }) 22 | hmr.enable(cache, material) 23 | return material 24 | } 25 | 26 | if (module.hot) { 27 | module.hot.accept(err => { 28 | if (err) throw errr 29 | }) 30 | hmr.update(cache, { 31 | vertexShader, fragmentShader 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /materials/shaders/noise.frag: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | 3 | // glslify fancy imports 4 | import noise from 'glsl-noise/simplex/3d'; 5 | 6 | varying vec2 vUv; 7 | 8 | void main () { 9 | float n = noise(vec3(vUv.xy * 10.0, 1.0)); 10 | n = smoothstep(0.0, 0.1, n); 11 | 12 | // glslify-hex allows for the color strings 13 | vec3 color = mix(vec3(#03A9F4), vec3(#3F51B5), n); 14 | gl_FragColor = vec4(color, 1.0); 15 | } 16 | -------------------------------------------------------------------------------- /materials/shaders/noise.vert: -------------------------------------------------------------------------------- 1 | attribute vec4 position; 2 | attribute vec2 uv; 3 | uniform mat4 projectionMatrix; 4 | uniform mat4 modelViewMatrix; 5 | 6 | varying vec2 vUv; 7 | 8 | void main () { 9 | vUv = uv; 10 | gl_Position = projectionMatrix * modelViewMatrix * position; 11 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-three-hmr-test", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "client.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 | "global": "^4.3.0", 14 | "glsl-checker": "^1.0.1", 15 | "glsl-noise": "0.0.0", 16 | "glslify": "^5.0.0", 17 | "glslify-hex": "^2.0.1", 18 | "glslify-fancy-imports": "^1.0.1", 19 | "three": "^0.73.0", 20 | "three-orbit-viewer": "^69.3.0", 21 | "three-vignette-background": "^1.0.2", 22 | "transform-loader": "^0.2.3" 23 | }, 24 | "devDependencies": { 25 | "babel-core": "^6.3.26", 26 | "babel-loader": "^6.2.0", 27 | "babel-preset-es2015": "^6.3.13", 28 | "ify-loader": "mattdesl/ify-loader#fix-errors", 29 | "webpack": "^1.12.9", 30 | "webpack-dev-server": "^1.14.0" 31 | }, 32 | "scripts": { 33 | "start": "webpack-dev-server client.js --inline --hot --colors --port=9966" 34 | }, 35 | "keywords": [], 36 | "repository": { 37 | "type": "git", 38 | "url": "git://github.com/mattdesl/webpack-three-hmr-test.git" 39 | }, 40 | "browserify": { 41 | "transform": [ 42 | "glslify" 43 | ] 44 | }, 45 | "glslify": { 46 | "transform": [ 47 | "glslify-fancy-imports", 48 | "glslify-hex" 49 | ] 50 | }, 51 | "homepage": "https://github.com/mattdesl/webpack-three-hmr-test", 52 | "bugs": { 53 | "url": "https://github.com/mattdesl/webpack-three-hmr-test/issues" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | module.exports = { 3 | entry: { 4 | app: [ './client.js' ] 5 | }, 6 | output: { 7 | filename: 'bundle.js' 8 | }, 9 | context: __dirname, 10 | node: { 11 | __filename: true 12 | }, 13 | module: { 14 | loaders: [ 15 | // use ES2015 on this app 16 | { 17 | test: /\.jsx?$/, 18 | exclude: /(node_modules|bower_components)/, 19 | loader: 'babel' 20 | }, 21 | // allow third-party glslify/browserify modules to work 22 | { 23 | test: /node_modules/, 24 | loader: 'ify' 25 | } 26 | ], 27 | // allow local glslify/browserify config to work 28 | postLoaders: [ 29 | { 30 | test: /\.js$/, 31 | loader: 'ify' 32 | } 33 | ] 34 | } 35 | } 36 | --------------------------------------------------------------------------------