├── .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 |
--------------------------------------------------------------------------------