├── .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 | [](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 |