├── .babelrc ├── .gitignore ├── LICENSE.md ├── README.md ├── app ├── assets │ └── models │ │ ├── honeycomb.bin │ │ └── honeycomb.gltf ├── index.html └── style.css ├── package-lock.json ├── package.json ├── src ├── context.js ├── index.js ├── startup.js ├── util │ ├── AssetManager.js │ ├── EquiToCube.js │ ├── Random.js │ ├── isMobile.js │ ├── loadEnvMap.js │ ├── loadTexture.js │ └── query.js ├── vendor │ └── .gitkeep └── webgl │ ├── WebGLApp.js │ ├── materials │ └── LiveShaderMaterial.js │ ├── scene │ ├── Honeycomb.js │ └── SpinningBox.js │ └── shaders │ ├── honey.frag │ ├── honey.shader.js │ └── honey.vert └── tools └── bundler.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "babel-preset-env", { 5 | "useBuiltIns": "usage" 6 | } 7 | ] 8 | ], 9 | "ignore": "src/vendor/*.js", 10 | "plugins": [ 11 | "babel-plugin-transform-class-properties" 12 | ] 13 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | node_modules 3 | *.log 4 | .DS_Store 5 | bundle.js 6 | -------------------------------------------------------------------------------- /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 | # threejs-app 2 | 3 | [demo](http://test-webgl.surge.sh/?gui) 4 | 5 | My current organization for medium & large WebGL apps (i.e. must scale to a large team and run the course of a few months). 6 | 7 | > ⚛ This branch only includes bare WebGL. If you want UI (with [Preact](https://github.com/developit/preact)), see the [preact](https://github.com/mattdesl/threejs-app/tree/preact) branch. 8 | 9 | This is by no means stable; you probably shouldn't just go cloning it and trying to build your own apps. It is really opinionated and has a lot of things that might seem odd or overkill (though I have found them necessary on most big projects). Instead, you may just want to study it to see if you can find anything of interest. 10 | 11 | Some things it tries to do: 12 | 13 | - Basic ThreeJS setup with render loop, camera, resize events, controls, tap events, GLTF loader, etc. 14 | - Budo for quick dev cycle, source maps, etc 15 | - Babel + ES2015 + bound class functions 16 | - A few optimizations thrown in for smaller output bundle size 17 | - glslify + glslify-hex transform 18 | - shader-reload for live shader reloading during dev 19 | - global access to canvas, dat.gui, camera, app width & height, controls, etc 20 | - an AssetManager & preloader to keep texture/GLTF/etc code clean and avoid promise/async hell 21 | - include `NODE_ENV=production` or development 22 | - a simple way to organize complex ThreeJS scenes: 23 | - build them out of smaller "components", where each component extends `THREE.Object3D`, `THREE.Group` or `THREE.Mesh` 24 | - functions like `update(dt, time)`, `onTouchStart(ev, pos)`, etc propagate through entire scene graph 25 | 26 | At some point many of these tools will be published on npm or as self-contained scripts, making this whole thing a bit more convenient. Until then... enjoy the mess! :) 27 | 28 | ## Usage 29 | 30 | Clone, `npm install`, then: 31 | 32 | ```sh 33 | # start development server 34 | npm run start 35 | ``` 36 | 37 | Now open [localhost:9966](http://localhost:9966/) and start editing your source code. Edit the `honey.frag` or `honey.vert` to see it reloaded without losing application state. 38 | 39 | You can launch [localhost:9966/?gui](http://localhost:9966/?gui) to open dat.gui. 40 | 41 | For production: 42 | 43 | ```sh 44 | # create a production bundle.js 45 | npm run bundle 46 | 47 | # deploy to a surge link for demoing 48 | npm run deploy 49 | ``` 50 | 51 | For deploy to work, you will need to change the surge URL in `package.json` `"scripts" > "deploy"` field to something else. 52 | 53 | ## License 54 | 55 | MIT, see [LICENSE.md](http://github.com/mattdesl/threejs-app/blob/master/LICENSE.md) for details. 56 | -------------------------------------------------------------------------------- /app/assets/models/honeycomb.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/threejs-app/7d89217aa764bb8732a7451bfa5ef625cb78bc2a/app/assets/models/honeycomb.bin -------------------------------------------------------------------------------- /app/assets/models/honeycomb.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "accessors" : [ 3 | { 4 | "bufferView" : 0, 5 | "componentType" : 5123, 6 | "count" : 612, 7 | "max" : [ 8 | 335 9 | ], 10 | "min" : [ 11 | 0 12 | ], 13 | "type" : "SCALAR" 14 | }, 15 | { 16 | "bufferView" : 1, 17 | "componentType" : 5126, 18 | "count" : 336, 19 | "max" : [ 20 | 1.132531762123108, 21 | 0.9043031930923462, 22 | 0.2850000262260437 23 | ], 24 | "min" : [ 25 | -1.132531762123108, 26 | -0.9043031930923462, 27 | -0.2850000262260437 28 | ], 29 | "type" : "VEC3" 30 | }, 31 | { 32 | "bufferView" : 2, 33 | "componentType" : 5126, 34 | "count" : 336, 35 | "max" : [ 36 | 1.0, 37 | 1.0, 38 | 1.0 39 | ], 40 | "min" : [ 41 | -1.0, 42 | -1.0, 43 | -1.0 44 | ], 45 | "type" : "VEC3" 46 | } 47 | ], 48 | "asset" : { 49 | "generator" : "Khronos Blender glTF 2.0 exporter", 50 | "version" : "2.0" 51 | }, 52 | "bufferViews" : [ 53 | { 54 | "buffer" : 0, 55 | "byteLength" : 1224, 56 | "byteOffset" : 0, 57 | "target" : 34963 58 | }, 59 | { 60 | "buffer" : 0, 61 | "byteLength" : 4032, 62 | "byteOffset" : 1224, 63 | "target" : 34962 64 | }, 65 | { 66 | "buffer" : 0, 67 | "byteLength" : 4032, 68 | "byteOffset" : 5256, 69 | "target" : 34962 70 | } 71 | ], 72 | "buffers" : [ 73 | { 74 | "byteLength" : 9288, 75 | "uri" : "honeycomb.bin" 76 | } 77 | ], 78 | "meshes" : [ 79 | { 80 | "name" : "honeycomb", 81 | "primitives" : [ 82 | { 83 | "attributes" : { 84 | "NORMAL" : 2, 85 | "POSITION" : 1 86 | }, 87 | "indices" : 0 88 | } 89 | ] 90 | } 91 | ], 92 | "nodes" : [ 93 | { 94 | "name" : "camera", 95 | "rotation" : [ 96 | 0.466261088848114, 97 | 0.3226812779903412, 98 | -0.1875857561826706, 99 | 0.8020530343055725 100 | ], 101 | "translation" : [ 102 | 7.333371162414551, 103 | 6.029646873474121, 104 | 7.63869571685791 105 | ] 106 | }, 107 | { 108 | "name" : "camera_target" 109 | }, 110 | { 111 | "mesh" : 0, 112 | "name" : "honeycomb", 113 | "scale" : [ 114 | 1.0, 115 | 0.9995182752609253, 116 | 1.0 117 | ] 118 | }, 119 | { 120 | "name" : "light_target" 121 | }, 122 | { 123 | "name" : "sun", 124 | "rotation" : [ 125 | 0.18757343292236328, 126 | 0.749933123588562, 127 | -0.23946259915828705, 128 | 0.5874302387237549 129 | ], 130 | "scale" : [ 131 | 1.0, 132 | 1.0, 133 | 0.9999998211860657 134 | ], 135 | "translation" : [ 136 | 4.076245307922363, 137 | 5.903861999511719, 138 | -1.0054539442062378 139 | ] 140 | } 141 | ], 142 | "scene" : 0, 143 | "scenes" : [ 144 | { 145 | "name" : "Scene", 146 | "nodes" : [ 147 | 2, 148 | 3, 149 | 4, 150 | 1, 151 | 0 152 | ] 153 | } 154 | ] 155 | } 156 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | test 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | overflow: hidden; 4 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "threejs-app", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "./src/index.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 | "clamp": "^1.0.1", 14 | "dat.gui": "^0.7.0", 15 | "defined": "^1.0.0", 16 | "glslify-hex": "^2.1.1", 17 | "load-img": "^1.0.0", 18 | "load-json-xhr": "^3.0.3", 19 | "map-limit": "0.0.1", 20 | "object-assign": "^4.1.1", 21 | "orbit-controls": "^1.2.0", 22 | "query-string": "^5.0.1", 23 | "right-now": "^1.0.0", 24 | "seed-random": "^2.2.0", 25 | "simplex-noise": "^2.3.0", 26 | "three": "^0.89.0", 27 | "touches": "^1.2.2", 28 | "xhr": "^2.4.1" 29 | }, 30 | "semistandard": { 31 | "globals": [ 32 | "THREE" 33 | ] 34 | }, 35 | "devDependencies": { 36 | "babel-core": "^6.26.0", 37 | "babel-plugin-transform-class-properties": "^6.24.1", 38 | "babel-preset-env": "^1.6.1", 39 | "babel-preset-es2015": "^6.24.1", 40 | "babelify": "^8.0.0", 41 | "browserify": "^14.5.0", 42 | "budo": "^10.0.4", 43 | "glslify": "^6.1.0", 44 | "loose-envify": "^1.3.1", 45 | "semistandard": "^12.0.0", 46 | "shader-reload": "^1.2.2", 47 | "surge": "^0.19.0", 48 | "uglify-js": "^3.2.1", 49 | "unreachable-branch-transform": "^0.5.1" 50 | }, 51 | "scripts": { 52 | "start": "NODE_ENV=development node tools/bundler.js", 53 | "bundle": "NODE_ENV=production node tools/bundler.js | uglifyjs -m -c warnings=false > app/bundle.js", 54 | "deploy:upload": "surge -p app/ -d test-webgl.surge.sh", 55 | "deploy": "npm run bundle && npm run deploy:upload" 56 | }, 57 | "glslify": { 58 | "transform": [ 59 | "glslify-hex" 60 | ] 61 | }, 62 | "keywords": [], 63 | "repository": { 64 | "type": "git", 65 | "url": "git://github.com/mattdesl/threejs-app.git" 66 | }, 67 | "homepage": "https://github.com/mattdesl/threejs-app", 68 | "bugs": { 69 | "url": "https://github.com/mattdesl/threejs-app/issues" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/context.js: -------------------------------------------------------------------------------- 1 | const WebGLApp = require('./webgl/WebGLApp'); 2 | const AssetManager = require('./util/AssetManager'); 3 | const query = require('./util/query'); 4 | const dat = require('dat.gui'); 5 | 6 | // Setup dat.gui 7 | const gui = new dat.GUI(); 8 | 9 | if (!query.gui) { 10 | document.querySelector('.dg.ac').style.display = 'none'; 11 | } 12 | 13 | // Grab our canvas 14 | const canvas = document.querySelector('.main-canvas'); 15 | 16 | // Setup the WebGLRenderer 17 | const webgl = new WebGLApp({ 18 | canvas 19 | }); 20 | 21 | // Setup an asset manager 22 | const assets = new AssetManager({ 23 | renderer: webgl.renderer 24 | }); 25 | 26 | module.exports = { 27 | assets, 28 | canvas, 29 | webgl, 30 | gui 31 | }; 32 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | global.THREE = require('three'); 2 | 3 | // include any additional ThreeJS vendor libraries here 4 | require('three/examples/js/loaders/GLTFLoader.js'); 5 | 6 | // ensure context is loaded before entry 7 | require('./context'); 8 | 9 | // now start up WebGL app 10 | require('./startup')(); 11 | -------------------------------------------------------------------------------- /src/startup.js: -------------------------------------------------------------------------------- 1 | const Honeycomb = require('./webgl/scene/Honeycomb'); 2 | // const SpinningBox = require('./webgl/scene/SpinningBox'); 3 | 4 | const { assets, webgl, gui } = require('./context'); 5 | 6 | module.exports = function () { 7 | // Set background color 8 | const background = 'white'; 9 | document.body.style.background = background; 10 | webgl.renderer.setClearColor(background); 11 | 12 | // Hide canvas 13 | webgl.canvas.style.visibility = 'hidden'; 14 | 15 | // Preload any queued assets 16 | assets.loadQueued(() => { 17 | console.log('Done loading'); 18 | 19 | // Show canvas 20 | webgl.canvas.style.visibility = ''; 21 | 22 | // To avoid page pulling and such 23 | webgl.canvas.addEventListener('touchstart', ev => ev.preventDefault()); 24 | 25 | // Add any "WebGL components" here... 26 | // webgl.scene.add(new SpinningBox()); 27 | webgl.scene.add(new Honeycomb()); 28 | 29 | // start animation loop 30 | webgl.start(); 31 | webgl.draw(); 32 | }); 33 | }; 34 | -------------------------------------------------------------------------------- /src/util/AssetManager.js: -------------------------------------------------------------------------------- 1 | const noop = () => { 2 | }; 3 | 4 | const isImage = (ext) => /\.(jpe?g|png|gif|bmp|tga|tif)$/i.test(ext); 5 | const isSVG = (ext) => /\.svg$/i.test(ext); 6 | const isAudio = (ext) => /\.(wav|mp3|ogg|mp4)$/i.test(ext); 7 | const isJSON = (ext) => /\.json$/i.test(ext); 8 | const isGLTF = (ext) => /\.(gltf|glb)$/i.test(ext); 9 | 10 | const xhr = require('xhr'); 11 | const path = require('path'); 12 | const mapLimit = require('map-limit'); 13 | 14 | const loadTexture = require('./loadTexture'); 15 | const loadEnvMap = require('./loadEnvMap'); 16 | const loadImage = require('load-img'); 17 | const loadJSON = require('load-json-xhr'); 18 | 19 | class AssetManager { 20 | constructor (opt = {}) { 21 | this._cache = {}; 22 | this._queue = []; 23 | this._renderer = opt.renderer; 24 | this._asyncLimit = 10; 25 | this._onProgressListeners = []; 26 | this._finishDelay = 0; 27 | } 28 | 29 | addProgressListener (fn) { 30 | if (typeof fn !== 'function') { 31 | throw new TypeError('onProgress must be a function'); 32 | } 33 | this._onProgressListeners.push(fn); 34 | } 35 | 36 | // Add an asset to be queued, format: { url, ...options } 37 | queue (opt = {}) { 38 | if (!opt || typeof opt !== 'object') { 39 | throw new Error('First parameter must be an object!'); 40 | } 41 | if (!opt.url) throw new TypeError('Must specify a URL or opt.url for AssetManager#queue()'); 42 | opt = Object.assign({}, opt); 43 | opt.key = opt.key || opt.url; 44 | const queued = this._getQueued(opt.key); 45 | if (!queued) this._queue.push(opt); 46 | return opt.key; 47 | } 48 | 49 | // Fetch a loaded asset by key or URL 50 | get (key = '') { 51 | if (!key) throw new TypeError('Must specify a key or URL for AssetManager#get()'); 52 | if (!(key in this._cache)) { 53 | throw new Error(`Could not find an asset by the key or URL ${key}`); 54 | } 55 | return this._cache[key]; 56 | } 57 | 58 | isQueueEmpty () { 59 | return this._queue.length === 0; 60 | } 61 | 62 | // Loads all queued assets 63 | loadQueued (cb = noop) { 64 | const queue = this._queue.slice(); 65 | this._queue.length = 0; // clear queue 66 | let count = 0; 67 | let total = queue.length; 68 | if (total === 0) { 69 | process.nextTick(() => { 70 | this._onProgressListeners.forEach(fn => fn(1)); 71 | cb(null); 72 | }); 73 | return; 74 | } 75 | if (process.env.NODE_ENV === 'development') { 76 | console.log(`[assets] Loading ${total} queued items`); 77 | } 78 | mapLimit(queue, this._asyncLimit, (item, next) => { 79 | this.load(item, (err, result) => { 80 | const percent = total <= 1 ? 1 : (count / (total - 1)); 81 | this._onProgressListeners.forEach(fn => fn(percent)); 82 | if (err) { 83 | console.error(`[assets] Skipping ${item.key} from asset loading:`); 84 | console.error(err); 85 | } 86 | count++; 87 | next(null, result); 88 | }); 89 | }, cb); 90 | } 91 | 92 | // Loads a single asset on demand, returning from 93 | // cache if it exists otherwise adding it to the cache 94 | // after loading. 95 | load (item, cb = noop) { 96 | const url = item.url; 97 | const ext = path.extname(url); 98 | const key = item.key || url; 99 | const cache = this._cache; 100 | const renderer = this._renderer; 101 | 102 | if (key in cache) { 103 | const ret = cache[key]; 104 | process.nextTick(() => cb(null, ret)); 105 | return ret; 106 | } else { 107 | if (process.env.NODE_ENV === 'development') { 108 | console.log(`[assets] Loading ${url}`); 109 | } 110 | const done = (err, data) => { 111 | if (err) { 112 | delete cache[key]; 113 | } else { 114 | cache[key] = data; 115 | } 116 | if (this._finishDelay) { 117 | setTimeout(() => { 118 | cb(err, data); 119 | }, this._finishDelay); 120 | } else { 121 | cb(err, data); 122 | } 123 | }; 124 | if (isGLTF(ext)) { 125 | const loader = new THREE.GLTFLoader(); 126 | return loader.load(url, (data) => { 127 | // get out of Promise land from GLTFLoader 128 | process.nextTick(() => done(null, data)); 129 | }, noop, (err) => { 130 | process.nextTick(() => { 131 | console.error(err); 132 | done(new Error(`Could not load GLTF asset ${url}`)); 133 | }); 134 | }); 135 | } else if (isJSON(ext)) { 136 | loadJSON(url, done); 137 | return; 138 | } else if (item.envMap) { 139 | const opts = Object.assign({renderer}, item); 140 | return loadEnvMap(opts, done); 141 | } else if (isSVG(ext) || isImage(ext)) { 142 | let ret; 143 | if (item.texture) { 144 | const opts = Object.assign({renderer}, item); 145 | ret = loadTexture(url, opts, done); 146 | } else { 147 | ret = loadImage(url, item, done); 148 | } 149 | cache[key] = ret; 150 | return ret; 151 | } else if (isAudio(ext)) { 152 | // instead of retaining audio objects in memory 153 | // (which isn't super helpful) and waiting for 154 | // them to finish loading (which can be a while 155 | // with long tracks) we will only XHR the resource 156 | // and mark the preload as immediately complete 157 | // so it warms up the cache. 158 | xhr({ 159 | uri: url, 160 | responseType: 'arraybuffer' 161 | }, (err) => { 162 | if (err) { 163 | console.warn(`Audio file at ${url} could not load:`); 164 | console.warn(err); 165 | } 166 | }); 167 | // Unlike other load events, we do not retain anything 168 | // in the asset cache... 169 | process.nextTick(() => { 170 | if (cb) cb(null); 171 | }); 172 | return; 173 | } else { 174 | throw new Error(`Could not load ${url}, unknown file extension!`); 175 | } 176 | } 177 | } 178 | 179 | _getQueued (key) { 180 | for (let i = 0; i < this._queue.length; i++) { 181 | const item = this._queue[i]; 182 | if (item.key === key) return item; 183 | } 184 | return null; 185 | } 186 | } 187 | 188 | module.exports = AssetManager; 189 | -------------------------------------------------------------------------------- /src/util/EquiToCube.js: -------------------------------------------------------------------------------- 1 | const CUBE_FACE_SIZE = 1024; 2 | 3 | let _maxSize = null; 4 | let _sphere; 5 | let _timer; 6 | 7 | function EquiToCube (renderer) { 8 | this.renderer = renderer; 9 | 10 | if (_maxSize === null) { 11 | const gl = renderer.getContext(); 12 | _maxSize = gl.getParameter(gl.MAX_CUBE_MAP_TEXTURE_SIZE); 13 | } 14 | 15 | this.material = new THREE.MeshBasicMaterial({ 16 | map: null, 17 | side: THREE.BackSide 18 | }); 19 | if (!_sphere) { 20 | _sphere = new THREE.SphereBufferGeometry(100, 256, 64); 21 | } 22 | 23 | this.mesh = new THREE.Mesh(_sphere, this.material); 24 | this.scene = new THREE.Scene(); 25 | this.scene.add(this.mesh); 26 | 27 | const mapSize = Math.min(CUBE_FACE_SIZE, _maxSize); 28 | this.camera = new THREE.CubeCamera(1, 1000, mapSize); 29 | this.cubeTexture = this.camera.renderTarget.texture; 30 | 31 | // After N seconds, dispose the sphere geometry 32 | // and let it be re-created if necessary 33 | clearTimeout(_timer); 34 | _timer = setTimeout(() => { 35 | _sphere.dispose(); 36 | }, 3000); 37 | } 38 | 39 | EquiToCube.prototype.convert = function (map) { 40 | this.material.map = map; 41 | this.material.needsUpdate = true; 42 | this.camera.update(this.renderer, this.scene); 43 | }; 44 | 45 | module.exports = EquiToCube; 46 | -------------------------------------------------------------------------------- /src/util/Random.js: -------------------------------------------------------------------------------- 1 | // a utility for random number generation 2 | const seedRandom = require('seed-random'); 3 | const SimplexNoise = require('simplex-noise'); 4 | 5 | class Rand { 6 | constructor (defaultSeed = null, opt = {}) { 7 | this.defaultRandom = Math.random; 8 | this.quiet = opt.quiet; 9 | this.setSeed(defaultSeed); 10 | } 11 | 12 | setSeed (seed) { 13 | if (typeof seed === 'number' || typeof seed === 'string') { 14 | if (!this.quiet) console.log('Random Seed:', seed); 15 | this.seed = String(seed); 16 | this.random = seedRandom(this.seed); 17 | } else { 18 | this.seed = null; 19 | this.random = this.defaultRandom; 20 | } 21 | this.simplex = new SimplexNoise(this.random); 22 | } 23 | 24 | noise2D (x, y) { 25 | return this.simplex.noise2D(x, y); 26 | } 27 | 28 | noise3D (x, y, z) { 29 | return this.simplex.noise3D(x, y, z); 30 | } 31 | 32 | noise4D (x, y, z, w) { 33 | return this.simplex.noise4D(x, y, z, w); 34 | } 35 | 36 | randomSign () { 37 | return this.random() > 0.5 ? 1 : -1; 38 | } 39 | 40 | randomFloat (min, max) { 41 | if (max === undefined) { 42 | max = min; 43 | min = 0; 44 | } 45 | 46 | if (typeof min !== 'number' || typeof max !== 'number') { 47 | throw new TypeError('Expected all arguments to be numbers'); 48 | } 49 | 50 | return this.random() * (max - min) + min; 51 | } 52 | 53 | randomInt (min, max) { 54 | if (max === undefined) { 55 | max = min; 56 | min = 0; 57 | } 58 | 59 | if (typeof min !== 'number' || typeof max !== 'number') { 60 | throw new TypeError('Expected all arguments to be numbers'); 61 | } 62 | 63 | return Math.floor(this.randomFloat(min, max)); 64 | } 65 | 66 | shuffle (arr) { 67 | if (!Array.isArray(arr)) { 68 | throw new TypeError('Expected Array, got ' + typeof arr); 69 | } 70 | 71 | var rand; 72 | var tmp; 73 | var len = arr.length; 74 | var ret = arr.slice(); 75 | while (len) { 76 | rand = Math.floor(this.random() * len--); 77 | tmp = ret[len]; 78 | ret[len] = ret[rand]; 79 | ret[rand] = tmp; 80 | } 81 | return ret; 82 | } 83 | 84 | randomCircle (out, scale = 1) { 85 | var r = this.random() * 2.0 * Math.PI; 86 | out[0] = Math.cos(r) * scale; 87 | out[1] = Math.sin(r) * scale; 88 | return out; 89 | } 90 | 91 | randomSphere (out, scale = 1) { 92 | var r = this.random() * 2.0 * Math.PI; 93 | var z = (this.random() * 2.0) - 1.0; 94 | var zScale = Math.sqrt(1.0 - z * z) * scale; 95 | out[0] = Math.cos(r) * zScale; 96 | out[1] = Math.sin(r) * zScale; 97 | out[2] = z * scale; 98 | return out; 99 | } 100 | 101 | randomHemisphere (out, scale = 1) { 102 | var r = this.random() * 1.0 * Math.PI; 103 | var z = (this.random() * 2.0) - 1.0; 104 | var zScale = Math.sqrt(1.0 - z * z) * scale; 105 | out[0] = Math.cos(r) * zScale; 106 | out[1] = Math.sin(r) * zScale; 107 | out[2] = z * scale; 108 | return out; 109 | } 110 | 111 | randomQuaternion (out) { 112 | const u1 = this.random(); 113 | const u2 = this.random(); 114 | const u3 = this.random(); 115 | 116 | const sq1 = Math.sqrt(1 - u1); 117 | const sq2 = Math.sqrt(u1); 118 | 119 | const theta1 = Math.PI * 2 * u2; 120 | const theta2 = Math.PI * 2 * u3; 121 | 122 | const x = Math.sin(theta1) * sq1; 123 | const y = Math.cos(theta1) * sq1; 124 | const z = Math.sin(theta2) * sq2; 125 | const w = Math.cos(theta2) * sq2; 126 | out[0] = x; 127 | out[1] = y; 128 | out[2] = z; 129 | out[3] = w; 130 | return out; 131 | } 132 | } 133 | 134 | module.exports = Rand; 135 | 136 | Rand.getRandomSeed = function () { 137 | const seed = String(Math.floor(Math.random() * 100000)); 138 | return seed; 139 | }; 140 | -------------------------------------------------------------------------------- /src/util/isMobile.js: -------------------------------------------------------------------------------- 1 | // Very dumb mobile check... 2 | module.exports = /(Android|iOS|iPhone|iPod|iPad)/i.test(navigator.userAgent); 3 | -------------------------------------------------------------------------------- /src/util/loadEnvMap.js: -------------------------------------------------------------------------------- 1 | const noop = () => {}; 2 | const EquiToCube = require('./EquiToCube'); 3 | const loadTexture = require('./loadTexture'); 4 | const clamp = require('clamp'); 5 | 6 | module.exports = function loadEnvMap (opt = {}, cb = noop) { 7 | const renderer = opt.renderer; 8 | const basePath = opt.url; 9 | if (!renderer) throw new Error('PBR Map requires renderer to be set on AssetManager!'); 10 | 11 | if (opt.equirectangular) { 12 | const equiToCube = new EquiToCube(renderer); 13 | loadTexture(basePath, { renderer }, (err, tex) => { 14 | if (err) return cb(err); 15 | equiToCube.convert(tex); 16 | tex.dispose(); // dispose original texture 17 | tex.image.data = null; // remove Image reference 18 | onCubeMapLoaded(equiToCube.cubeTexture); 19 | }); 20 | return equiToCube.cubeTexture; 21 | } else { 22 | const isHDR = opt.hdr; 23 | const extension = isHDR ? '.hdr' : '.png'; 24 | const urls = genCubeUrls(basePath.replace(/\/$/, '') + '/', extension); 25 | 26 | if (isHDR) { 27 | // load a float HDR texture 28 | return new THREE.HDRCubeTextureLoader() 29 | .load(THREE.UnsignedByteType, urls, onCubeMapLoaded, noop, onError); 30 | } else { 31 | // load a RGBM encoded texture 32 | return new THREE.CubeTextureLoader() 33 | .load(urls, cubeMap => { 34 | cubeMap.encoding = THREE.RGBM16Encoding; 35 | onCubeMapLoaded(cubeMap); 36 | }, noop, onError); 37 | } 38 | } 39 | 40 | function onError () { 41 | const err = new Error(`Could not load PBR map: ${basePath}`); 42 | console.error(err); 43 | cb(err); 44 | cb = noop; 45 | } 46 | 47 | function onCubeMapLoaded (cubeMap) { 48 | if (opt.pbr || typeof opt.level === 'number') { 49 | // prefilter the environment map for irradiance 50 | const pmremGenerator = new THREE.PMREMGenerator(cubeMap); 51 | pmremGenerator.update(renderer); 52 | if (opt.pbr) { 53 | const pmremCubeUVPacker = new THREE.PMREMCubeUVPacker(pmremGenerator.cubeLods); 54 | pmremCubeUVPacker.update(renderer); 55 | const target = pmremCubeUVPacker.CubeUVRenderTarget; 56 | cubeMap = target.texture; 57 | } else { 58 | const idx = clamp(Math.floor(opt.level), 0, pmremGenerator.cubeLods.length); 59 | cubeMap = pmremGenerator.cubeLods[idx].texture; 60 | } 61 | } 62 | if (opt.mapping) cubeMap.mapping = opt.mapping; 63 | cb(null, cubeMap); 64 | cb = noop; 65 | } 66 | } 67 | 68 | function genCubeUrls (prefix, postfix) { 69 | return [ 70 | prefix + 'px' + postfix, prefix + 'nx' + postfix, 71 | prefix + 'py' + postfix, prefix + 'ny' + postfix, 72 | prefix + 'pz' + postfix, prefix + 'nz' + postfix 73 | ]; 74 | } 75 | -------------------------------------------------------------------------------- /src/util/loadTexture.js: -------------------------------------------------------------------------------- 1 | const loadImg = require('load-img'); 2 | const noop = () => {}; 3 | 4 | module.exports = function loadTexture (src, opt, cb) { 5 | if (typeof opt === 'function') { 6 | cb = opt; 7 | opt = {}; 8 | } 9 | opt = Object.assign({}, opt); 10 | cb = cb || noop; 11 | 12 | const texture = new THREE.Texture(); 13 | texture.name = src; 14 | texture.encoding = opt.encoding || THREE.LinearEncoding; 15 | setTextureParams(src, texture, opt); 16 | loadImg(src, { 17 | crossOrigin: 'Anonymous' 18 | }, (err, image) => { 19 | if (err) { 20 | const msg = `Could not load texture ${src}`; 21 | console.error(msg); 22 | return cb(new Error(msg)); 23 | } 24 | texture.image = image; 25 | texture.needsUpdate = true; 26 | if (opt.renderer) { 27 | // Force texture to be uploaded to GPU immediately, 28 | // this will avoid "jank" on first rendered frame 29 | opt.renderer.setTexture2D(texture, 0); 30 | } 31 | cb(null, texture); 32 | }); 33 | return texture; 34 | } 35 | 36 | function setTextureParams (url, texture, opt) { 37 | if (typeof opt.flipY === 'boolean') texture.flipY = opt.flipY; 38 | if (typeof opt.mapping !== 'undefined') { 39 | texture.mapping = opt.mapping; 40 | } 41 | if (typeof opt.format !== 'undefined') { 42 | texture.format = opt.format; 43 | } else { 44 | // choose a nice default format 45 | const isJPEG = url.search(/\.(jpg|jpeg)$/) > 0 || url.search(/^data\:image\/jpeg/) === 0; 46 | texture.format = isJPEG ? THREE.RGBFormat : THREE.RGBAFormat; 47 | } 48 | if (opt.repeat) texture.repeat.copy(opt.repeat); 49 | texture.wrapS = opt.wrapS || THREE.ClampToEdgeWrapping; 50 | texture.wrapT = opt.wrapT || THREE.ClampToEdgeWrapping; 51 | texture.minFilter = opt.minFilter || THREE.LinearMipMapLinearFilter; 52 | texture.magFilter = opt.magFilter || THREE.LinearFilter; 53 | texture.generateMipmaps = opt.generateMipmaps !== false; 54 | } 55 | -------------------------------------------------------------------------------- /src/util/query.js: -------------------------------------------------------------------------------- 1 | // an object holding all the parsed query parameters 2 | // (tries to parse them as numbers/boolean) 3 | const qs = require('query-string'); 4 | 5 | function parseOptions () { 6 | if (typeof window === 'undefined') return {}; 7 | const parsed = qs.parse(window.location.search); 8 | Object.keys(parsed).forEach(key => { 9 | if (parsed[key] === null) parsed[key] = true; 10 | if (parsed[key] === 'false') parsed[key] = false; 11 | if (parsed[key] === 'true') parsed[key] = true; 12 | if (isNumber(parsed[key])) { 13 | parsed[key] = Number(parsed[key]); 14 | } 15 | }); 16 | return parsed; 17 | } 18 | 19 | function isNumber (x) { 20 | if (typeof x === 'number') return true; 21 | if (/^0x[0-9a-f]+$/i.test(x)) return true; 22 | return /^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(e[-+]?\d+)?$/.test(x); 23 | } 24 | 25 | module.exports = parseOptions(); 26 | -------------------------------------------------------------------------------- /src/vendor/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/threejs-app/7d89217aa764bb8732a7451bfa5ef625cb78bc2a/src/vendor/.gitkeep -------------------------------------------------------------------------------- /src/webgl/WebGLApp.js: -------------------------------------------------------------------------------- 1 | const { EventEmitter } = require('events'); 2 | const assign = require('object-assign'); 3 | const defined = require('defined'); 4 | const rightNow = require('right-now'); 5 | const createOrbitControls = require('orbit-controls'); 6 | const createTouches = require('touches'); 7 | 8 | const tmpTarget = new THREE.Vector3(); 9 | 10 | module.exports = class WebGLApp extends EventEmitter { 11 | 12 | constructor (opt = {}) { 13 | super(); 14 | 15 | this.renderer = new THREE.WebGLRenderer(assign({ 16 | antialias: true, 17 | alpha: false, 18 | // enabled for saving screen shots of the canvas, 19 | // may wish to disable this for perf reasons 20 | preserveDrawingBuffer: true, 21 | failIfMajorPerformanceCaveat: true 22 | }, opt)); 23 | 24 | this.renderer.sortObjects = false; 25 | this.canvas = this.renderer.domElement; 26 | 27 | // really basic touch handler that propagates through the scene 28 | this.touchHandler = createTouches(this.canvas, { 29 | target: this.canvas, 30 | filtered: true 31 | }); 32 | this.touchHandler.on('start', (ev, pos) => this._traverse('onTouchStart', ev, pos)); 33 | this.touchHandler.on('end', (ev, pos) => this._traverse('onTouchEnd', ev, pos)); 34 | this.touchHandler.on('move', (ev, pos) => this._traverse('onTouchMove', ev, pos)); 35 | 36 | // default background color 37 | const background = defined(opt.background, '#000'); 38 | const backgroundAlpha = defined(opt.backgroundAlpha, 1); 39 | this.renderer.setClearColor(background, backgroundAlpha); 40 | 41 | // clamp pixel ratio for performance 42 | this.maxPixelRatio = defined(opt.maxPixelRatio, 2); 43 | 44 | // clamp delta to stepping anything too far forward 45 | this.maxDeltaTime = defined(opt.maxDeltaTime, 1 / 30); 46 | 47 | // setup a basic camera 48 | const fov = defined(opt.fov, 45); 49 | const near = defined(opt.near, 0.01); 50 | const far = defined(opt.far, 100); 51 | this.camera = new THREE.PerspectiveCamera(fov, 1, near, far); 52 | 53 | // set up a simple orbit controller 54 | this.controls = createOrbitControls(assign({ 55 | element: this.canvas, 56 | parent: window, 57 | distance: 4 58 | }, opt)); 59 | 60 | this.time = 0; 61 | this._running = false; 62 | this._lastTime = rightNow(); 63 | this._rafID = null; 64 | 65 | this.scene = new THREE.Scene(); 66 | 67 | // handle resize events 68 | window.addEventListener('resize', () => this.resize()); 69 | window.addEventListener('orientationchange', () => this.resize()); 70 | 71 | // force an initial resize event 72 | this.resize(); 73 | } 74 | 75 | get running () { 76 | return this._running; 77 | } 78 | 79 | resize (width, height, pixelRatio) { 80 | // get default values 81 | width = defined(width, window.innerWidth); 82 | height = defined(height, window.innerHeight); 83 | pixelRatio = defined(pixelRatio, Math.min(this.maxPixelRatio, window.devicePixelRatio)); 84 | 85 | this.width = width; 86 | this.height = height; 87 | this.pixelRatio = pixelRatio; 88 | 89 | // update pixel ratio if necessary 90 | if (this.renderer.getPixelRatio() !== pixelRatio) { 91 | this.renderer.setPixelRatio(pixelRatio); 92 | } 93 | 94 | // setup new size & update camera aspect if necessary 95 | this.renderer.setSize(width, height); 96 | if (this.camera.isPerspectiveCamera) { 97 | this.camera.aspect = width / height; 98 | } 99 | this.camera.updateProjectionMatrix(); 100 | 101 | // draw a frame to ensure the new size has been registered visually 102 | this.draw(); 103 | return this; 104 | } 105 | 106 | // convenience function to trigger a PNG download of the canvas 107 | saveScreenshot (opt = {}) { 108 | // force a specific output size 109 | this.resize(defined(opt.width, 2560), defined(opt.height, 1440), 1, true); 110 | this.draw(); 111 | 112 | const dataURI = this.canvas.toDataURL('image/png'); 113 | 114 | // reset to default size 115 | this.resize(); 116 | this.draw(); 117 | 118 | // save 119 | const file = defined(opt.fileName, defaultFile('.png')); 120 | saveDataURI(file, dataURI); 121 | } 122 | 123 | update (dt = 0, time = 0) { 124 | this.controls.update(); 125 | 126 | // reposition to orbit controls 127 | this.camera.up.fromArray(this.controls.up); 128 | this.camera.position.fromArray(this.controls.position); 129 | tmpTarget.fromArray(this.controls.target); 130 | this.camera.lookAt(tmpTarget); 131 | 132 | // recursively tell all child objects to update 133 | this.scene.traverse(obj => { 134 | if (typeof obj.update === 'function') { 135 | obj.update(dt, time); 136 | } 137 | }); 138 | 139 | return this; 140 | } 141 | 142 | draw () { 143 | this.renderer.render(this.scene, this.camera); 144 | return this; 145 | } 146 | 147 | start () { 148 | if (this._rafID !== null) return; 149 | this._rafID = window.requestAnimationFrame(this.animate); 150 | this._running = true; 151 | return this; 152 | } 153 | 154 | stop () { 155 | if (this._rafID === null) return; 156 | window.cancelAnimationFrame(this._rafID); 157 | this._rafID = null; 158 | this._running = false; 159 | return this; 160 | } 161 | 162 | animate = () => { // <-- Note: using class functions thanks to a Babel plugin 163 | if (!this.running) return; 164 | window.requestAnimationFrame(this.animate); 165 | 166 | const now = rightNow(); 167 | const dt = Math.min(this.maxDeltaTime, (now - this._lastTime) / 1000); 168 | this.time += dt; 169 | this._lastTime = now; 170 | this.update(dt, this.time); 171 | this.draw(); 172 | } 173 | 174 | _traverse = (fn, ...args) => { 175 | this.scene.traverse(child => { 176 | if (typeof child[fn] === 'function') { 177 | child[fn].apply(child, args); 178 | } 179 | }); 180 | } 181 | } 182 | 183 | function dataURIToBlob (dataURI) { 184 | const binStr = window.atob(dataURI.split(',')[1]); 185 | const len = binStr.length; 186 | const arr = new Uint8Array(len); 187 | for (var i = 0; i < len; i++) { 188 | arr[i] = binStr.charCodeAt(i); 189 | } 190 | return new window.Blob([arr]); 191 | } 192 | 193 | function saveDataURI (name, dataURI) { 194 | const blob = dataURIToBlob(dataURI); 195 | 196 | // force download 197 | const link = document.createElement('a'); 198 | link.download = name; 199 | link.href = window.URL.createObjectURL(blob); 200 | link.onclick = () => { 201 | process.nextTick(() => { 202 | window.URL.revokeObjectURL(blob); 203 | link.removeAttribute('href'); 204 | }); 205 | }; 206 | link.click(); 207 | } 208 | 209 | function defaultFile (ext) { 210 | const str = `${new Date().toLocaleDateString()} at ${new Date().toLocaleTimeString()}${ext}`; 211 | return str.replace(/\//g, '-').replace(/:/g, '.'); 212 | } -------------------------------------------------------------------------------- /src/webgl/materials/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 | const 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 | -------------------------------------------------------------------------------- /src/webgl/scene/Honeycomb.js: -------------------------------------------------------------------------------- 1 | const { gui, webgl, assets } = require('../../context'); 2 | 3 | const LiveShaderMaterial = require('../materials/LiveShaderMaterial'); 4 | const honeyShader = require('../shaders/honey.shader'); 5 | 6 | // tell the preloader to include this asset 7 | // we need to define this outside of our class, otherwise 8 | // it won't get included in the preloader until *after* its done loading 9 | const gltfKey = assets.queue({ 10 | url: 'assets/models/honeycomb.gltf' 11 | }); 12 | 13 | module.exports = class Honeycomb extends THREE.Object3D { 14 | constructor () { 15 | super(); 16 | 17 | // now fetch the loaded resource 18 | const gltf = assets.get(gltfKey); 19 | 20 | this.material = new LiveShaderMaterial(honeyShader, { 21 | uniforms: { 22 | time: { value: 0 }, 23 | colorA: { value: new THREE.Color('rgb(213,70,70)') }, 24 | colorB: { value: new THREE.Color('rgb(223,191,86)') } 25 | } 26 | }); 27 | 28 | // Replaces all meshes material with something basic 29 | gltf.scene.traverse(child => { 30 | if (child.isMesh) { 31 | child.material = this.material; 32 | 33 | // ThreeJS attaches something odd here on GLTF ipmport 34 | child.onBeforeRender = () => {}; 35 | } 36 | }); 37 | 38 | this.add(gltf.scene); 39 | 40 | if (gui) { // assume it can be falsey, e.g. if we strip dat-gui out of bundle 41 | // attach dat.gui stuff here as usual 42 | const folder = gui.addFolder('honeycomb'); 43 | const settings = { 44 | colorA: this.material.uniforms.colorA.value.getStyle(), 45 | colorB: this.material.uniforms.colorB.value.getStyle() 46 | }; 47 | const update = () => { 48 | this.material.uniforms.colorA.value.setStyle(settings.colorA); 49 | this.material.uniforms.colorB.value.setStyle(settings.colorB); 50 | }; 51 | folder.addColor(settings, 'colorA').onChange(update); 52 | folder.addColor(settings, 'colorB').onChange(update); 53 | folder.open(); 54 | } 55 | } 56 | 57 | update (dt = 0, time = 0) { 58 | // This function gets propagated down from the WebGL app to all children 59 | this.rotation.y += dt * 0.1; 60 | this.material.uniforms.time.value = time; 61 | } 62 | 63 | onTouchStart (ev, pos) { 64 | const [ x, y ] = pos; 65 | console.log('Touchstart / mousedown: (%d, %d)', x, y); 66 | 67 | // For example, raycasting is easy: 68 | const coords = new THREE.Vector2().set( 69 | pos[0] / webgl.width * 2 - 1, 70 | -pos[1] / webgl.height * 2 + 1 71 | ); 72 | const raycaster = new THREE.Raycaster(); 73 | raycaster.setFromCamera(coords, webgl.camera); 74 | const hits = raycaster.intersectObject(this, true); 75 | console.log(hits.length > 0 ? `Hit ${hits[0].object.name}!` : 'No hit'); 76 | } 77 | 78 | onTouchMove (ev, pos) { 79 | } 80 | 81 | onTouchEnd (ev, pos) { 82 | } 83 | }; 84 | -------------------------------------------------------------------------------- /src/webgl/scene/SpinningBox.js: -------------------------------------------------------------------------------- 1 | module.exports = class SpinningBox extends THREE.Object3D { 2 | constructor () { 3 | super(); 4 | this.add(new THREE.Mesh( 5 | new THREE.BoxGeometry(1, 1, 1), 6 | new THREE.MeshBasicMaterial({ 7 | wireframe: true, color: 'black' 8 | }) 9 | )); 10 | } 11 | 12 | update (dt = 0) { 13 | this.rotation.x += dt * 0.1; 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /src/webgl/shaders/honey.frag: -------------------------------------------------------------------------------- 1 | uniform vec3 colorA; 2 | uniform vec3 colorB; 3 | uniform float time; 4 | 5 | varying vec3 vNormal; 6 | 7 | // Because this is glslify, you can import 8 | // GLSL modules from npm like 'glsl-noise' 9 | 10 | // Also, using glslify-hex, you can use #ff00ff to create vec3 colors 11 | 12 | void main () { 13 | vec3 norm = vNormal * 0.5 + 0.5; 14 | float t = norm.x; 15 | t *= sin(time * 5.0 + norm.x * 10.0) * 0.5 + 0.5; 16 | vec3 color = mix(colorA, colorB, t); 17 | gl_FragColor = vec4(color, 1.0); 18 | } -------------------------------------------------------------------------------- /src/webgl/shaders/honey.shader.js: -------------------------------------------------------------------------------- 1 | const glslify = require('glslify'); 2 | const path = require('path'); 3 | 4 | module.exports = require('shader-reload')({ 5 | vertex: glslify(path.resolve(__dirname, './honey.vert')), 6 | fragment: glslify(path.resolve(__dirname, './honey.frag')) 7 | }); 8 | -------------------------------------------------------------------------------- /src/webgl/shaders/honey.vert: -------------------------------------------------------------------------------- 1 | #if defined( FLAT_SHADED ) || defined( USE_BUMPMAP ) || defined( USE_NORMALMAP ) 2 | 3 | varying vec3 vViewPosition; 4 | 5 | #endif 6 | 7 | #ifndef FLAT_SHADED 8 | 9 | varying vec3 vNormal; 10 | 11 | #endif 12 | 13 | void main () { 14 | 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | #ifndef FLAT_SHADED // Normal computed with derivatives when FLAT_SHADED 22 | 23 | vNormal = normalize( transformedNormal ); 24 | 25 | #endif 26 | 27 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position.xyz, 1.0); 28 | } -------------------------------------------------------------------------------- /tools/bundler.js: -------------------------------------------------------------------------------- 1 | const budo = require('budo'); 2 | const browserify = require('browserify'); 3 | const path = require('path'); 4 | 5 | // a utility that attaches shader reloading capabilities to budo 6 | const attachShaderReload = require('shader-reload/bin/budo-attach'); 7 | 8 | // root source 9 | const entry = require.resolve('../'); 10 | 11 | // You could add more transforms here if you like 12 | const transforms = [ 13 | 'babelify', 14 | 'glslify' 15 | ]; 16 | 17 | // during development 18 | module.exports.dev = function () { 19 | const args = [ entry ].concat(process.argv.slice(2)); 20 | const app = budo.cli(args, { 21 | dir: path.resolve(__dirname, '../app'), 22 | serve: 'bundle.js', 23 | live: false, 24 | browserify: { 25 | transform: transforms.concat([ 'shader-reload/transform' ]) 26 | } 27 | }); 28 | if (app) attachShaderReload(app); 29 | return app; 30 | }; 31 | 32 | // create a file for production 33 | module.exports.bundle = function () { 34 | const bundler = browserify(entry, { 35 | fullPaths: process.env.DISC === '1' 36 | }); 37 | 38 | // add common transforms 39 | transforms.forEach(t => bundler.transform(t)); 40 | 41 | // add production transforms 42 | return bundler 43 | .transform('loose-envify', { global: true }) 44 | .transform('unreachable-branch-transform', { global: true }) 45 | .bundle(); 46 | }; 47 | 48 | if (!module.parent) { 49 | if (process.env.NODE_ENV === 'production') { 50 | module.exports.bundle().pipe(process.stdout); 51 | } else { 52 | module.exports.dev(); 53 | } 54 | } 55 | --------------------------------------------------------------------------------