├── .eslintrc.js
├── .gitignore
├── .nojekyll
├── LICENSE
├── README.md
├── gulpfile.js
├── index.html
├── package.json
└── src
├── audio.js
├── box3.js
├── boxColors.js
├── boxGeom.js
├── boxIndices.js
├── boxTransforms.js
├── bufferAttr.js
├── bufferGeom.js
├── camera.js
├── controls.js
├── directGeom.js
├── directionalLight.js
├── entity.js
├── face3.js
├── fbm.js
├── geom.js
├── index.js
├── keys.js
├── maps.js
├── mat4.js
├── material.js
├── math.js
├── mesh.js
├── noise.js
├── object3d.js
├── physics.js
├── player.js
├── pointerLock.js
├── quat.js
├── ray.js
├── shader.js
├── shaders
├── phong_frag.glsl.js
└── phong_vert.glsl.js
├── utils.js
└── vec3.js
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | extends: ['eslint:recommended', 'plugin:prettier/recommended'],
5 | env: {
6 | browser: true,
7 | es6: true,
8 | },
9 | plugins: ['prettier'],
10 | parserOptions: {
11 | ecmaVersion: 2018,
12 | sourceType: 'module',
13 | },
14 | rules: {
15 | 'func-style': ['error', 'expression'],
16 | 'object-shorthand': ['error', 'always'],
17 | },
18 | };
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (https://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # TypeScript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | # next.js build output
61 | .next
62 |
63 | build
64 | dist
65 |
--------------------------------------------------------------------------------
/.nojekyll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/razh/js13k-2018/97cd0f273e6280f918582a172c6ee2e8ad9cf360/.nojekyll
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 razh
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # js13k-2018
2 | 🎛
3 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | /* eslint-disable func-style */
3 |
4 | 'use strict';
5 |
6 | const gulp = require('gulp');
7 | const $ = require('gulp-load-plugins')();
8 |
9 | const del = require('del');
10 | const rollup = require('rollup').rollup;
11 |
12 | const composer = require('gulp-uglify/composer');
13 | const uglify = composer(require('terser'), console);
14 |
15 | const escapeStringRegexp = require('escape-string-regexp');
16 | const operators = require('glsl-tokenizer/lib/operators');
17 |
18 | const SPACES_AROUND_OPERATORS_REGEX = new RegExp(
19 | `\\s*(${operators.map(escapeStringRegexp).join('|')})\\s*`,
20 | 'g',
21 | );
22 |
23 | gulp.task('clean', () => del(['build', 'dist']));
24 |
25 | // https://github.com/mrdoob/three.js/blob/dev/rollup.config.js
26 | function glsl() {
27 | function minify(code) {
28 | return (
29 | code
30 | // Remove //
31 | .replace(/\s*\/\/.*\n/g, '')
32 | // Remove /* */
33 | .replace(/\s*\/\*[\s\S]*?\*\//g, '')
34 | // # \n+ to \n
35 | .replace(/\n{2,}/g, '\n')
36 | // Remove tabs and consecutive spaces with a single space
37 | .replace(/\s{2,}|\t/g, ' ')
38 | .split('\n')
39 | .map((line, index, array) => {
40 | line = line.trim();
41 |
42 | // Remove spaces around operators if not an #extension directive.
43 | // For example, #extension GL_OES_standard_derivatives : enable.
44 | if (!line.startsWith('#extension')) {
45 | line = line.replace(SPACES_AROUND_OPERATORS_REGEX, '$1');
46 | }
47 |
48 | // Append newlines after preprocessor directives.
49 | if (line[0] === '#') {
50 | line += '\n';
51 |
52 | // Append newlines before the start of preprocessor directive blocks.
53 | if (index > 0) {
54 | if (array[index - 1][0] !== '#') {
55 | line = '\n' + line;
56 | }
57 | }
58 | }
59 |
60 | return line;
61 | })
62 | .join('')
63 | );
64 | }
65 |
66 | return {
67 | transform(code, id) {
68 | if (!id.endsWith('.glsl.js')) {
69 | return;
70 | }
71 |
72 | const startIndex = code.indexOf('`');
73 | const prefix = code.slice(0, startIndex);
74 | const endIndex = code.lastIndexOf('`');
75 | const glslString = code.slice(startIndex + 1, endIndex - 1).trim();
76 |
77 | return {
78 | code: `${prefix}\`${minify(glslString)}\``,
79 | map: { mappings: '' },
80 | };
81 | },
82 | };
83 | }
84 |
85 | gulp.task('rollup', () => {
86 | return (
87 | rollup({
88 | input: 'src/index.js',
89 | plugins: [glsl()],
90 | })
91 | .then(bundle =>
92 | bundle.write({
93 | file: 'build/bundle.js',
94 | format: 'iife',
95 | }),
96 | )
97 | // eslint-disable-next-line no-console
98 | .catch(error => console.error(error))
99 | );
100 | });
101 |
102 | gulp.task('uglify', () => {
103 | return gulp
104 | .src('build/bundle.js')
105 | .pipe(uglify())
106 | .pipe(gulp.dest('dist'));
107 | });
108 |
109 | gulp.task('js', gulp.series('rollup', 'uglify'));
110 |
111 | gulp.task('html', () => {
112 | return gulp
113 | .src('./index.html')
114 | .pipe(
115 | $.htmlmin({
116 | collapseWhitespace: true,
117 | removeAttributeQuotes: true,
118 | removeComments: true,
119 | minifyCSS: true,
120 | }),
121 | )
122 | .pipe($.replace('./src/index.js', './bundle.js'))
123 | .pipe(gulp.dest('dist'));
124 | });
125 |
126 | gulp.task('build', gulp.series('clean', gulp.parallel('html', 'js')));
127 |
128 | gulp.task('compress', () => {
129 | return gulp
130 | .src('dist/**/*')
131 | .pipe($.zip('build.zip'))
132 | .pipe($.size())
133 | .pipe($.size({ pretty: false }))
134 | .pipe(gulp.dest('build'));
135 | });
136 |
137 | gulp.task('dist', gulp.series('build', 'compress'));
138 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | js13k-2018
6 |
81 |
82 |
83 |
84 |
85 |
WANDER
86 |
87 |
88 |
89 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "js13k-2018",
3 | "version": "1.0.0",
4 | "description": "js13k-2018",
5 | "main": "src/index.js",
6 | "scripts": {
7 | "gulp": "gulp",
8 | "watch": "browser-sync start --server --watch --no-open --no-ghost-mode",
9 | "lint": "eslint ./src",
10 | "test": "test"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git+https://github.com/razh/js13k-2018.git"
15 | },
16 | "author": "razh",
17 | "license": "MIT",
18 | "bugs": {
19 | "url": "https://github.com/razh/js13k-2018/issues"
20 | },
21 | "homepage": "https://github.com/razh/js13k-2018#readme",
22 | "devDependencies": {
23 | "browser-sync": "2.26.0",
24 | "del": "3.0.0",
25 | "escape-string-regexp": "1.0.5",
26 | "eslint": "5.6.1",
27 | "eslint-config-prettier": "3.1.0",
28 | "eslint-plugin-prettier": "3.0.0",
29 | "glsl-tokenizer": "2.1.5",
30 | "gulp": "4.0.0",
31 | "gulp-htmlmin": "5.0.1",
32 | "gulp-load-plugins": "1.5.0",
33 | "gulp-replace": "1.0.0",
34 | "gulp-size": "3.0.0",
35 | "gulp-uglify": "3.0.1",
36 | "gulp-zip": "4.2.0",
37 | "prettier": "1.14.3",
38 | "rollup": "0.66.4",
39 | "terser": "3.9.3"
40 | },
41 | "prettier": {
42 | "singleQuote": true,
43 | "trailingComma": "all"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/audio.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 | import { randFloatSpread } from './math.js';
3 |
4 | var AudioContext = window.AudioContext || window.webkitAudioContext;
5 | var OfflineAudioContext =
6 | window.OfflineAudioContext || window.webkitOfflineAudioContext;
7 |
8 | var audioContext = new AudioContext();
9 | var { sampleRate } = audioContext;
10 |
11 | // A4 is 69.
12 | var toFreq = note => 2 ** ((note - 69) / 12) * 440;
13 |
14 | var playSound = (sound, destination = audioContext.destination) => {
15 | var source = audioContext.createBufferSource();
16 | source.buffer = sound;
17 | source.connect(destination);
18 | source.start();
19 | };
20 |
21 | var generateAudioBuffer = (fn, duration, volume) => {
22 | var length = duration * sampleRate;
23 |
24 | var buffer = audioContext.createBuffer(1, length, sampleRate);
25 | var channel = buffer.getChannelData(0);
26 | for (var i = 0; i < length; i++) {
27 | channel[i] = fn(i / sampleRate, i, channel) * volume;
28 | }
29 |
30 | return buffer;
31 | };
32 |
33 | var noteNames = [
34 | 'c',
35 | 'cs',
36 | 'd',
37 | 'ds',
38 | 'e',
39 | 'f',
40 | 'fs',
41 | 'g',
42 | 'gs',
43 | 'a',
44 | 'as',
45 | 'b',
46 | ];
47 |
48 | var toNoteString = note => {
49 | var name = noteNames[note % 12];
50 | var octave = Math.floor(note / 12) - 1;
51 | return name + octave;
52 | };
53 |
54 | var generateNotes = (fn, duration, volume) => {
55 | var notes = {};
56 |
57 | var createNoteProperty = note => {
58 | var sound;
59 |
60 | var descriptor = {
61 | get() {
62 | if (!sound) {
63 | sound = generateAudioBuffer(fn(toFreq(note)), duration, volume);
64 | }
65 |
66 | return sound;
67 | },
68 | };
69 |
70 | Object.defineProperty(notes, note, descriptor);
71 | Object.defineProperty(notes, toNoteString(note), descriptor);
72 | };
73 |
74 | // From A1 (21) to A7 (105).
75 | for (var i = 21; i <= 105; i++) {
76 | createNoteProperty(i);
77 | }
78 |
79 | return notes;
80 | };
81 |
82 | var wet = audioContext.createGain();
83 | wet.gain.value = 0.3;
84 | wet.connect(audioContext.destination);
85 |
86 | var dry = audioContext.createGain();
87 | dry.gain.value = 1 - wet.gain.value;
88 | dry.connect(audioContext.destination);
89 |
90 | var convolver = audioContext.createConvolver();
91 | convolver.connect(wet);
92 |
93 | var master = audioContext.createGain();
94 | master.gain.value = 0.8;
95 | master.connect(dry);
96 | master.connect(convolver);
97 |
98 | var impulseResponse = (t, i, a) => {
99 | return (2 * Math.random() - 1) * Math.pow(a.length, -i / a.length);
100 | };
101 |
102 | var impulseResponseBuffer = generateAudioBuffer(impulseResponse, 2, 1);
103 |
104 | // Cheap hack for reverb.
105 | var renderLowPassOffline = (
106 | convolver,
107 | startFrequency,
108 | endFrequency,
109 | duration,
110 | ) => {
111 | var offlineCtx = new OfflineAudioContext(
112 | 1,
113 | impulseResponseBuffer.length,
114 | sampleRate,
115 | );
116 |
117 | var offlineFilter = offlineCtx.createBiquadFilter();
118 | offlineFilter.type = 'lowpass';
119 | offlineFilter.Q.value = 0.0001;
120 | offlineFilter.frequency.value = startFrequency;
121 | offlineFilter.frequency.linearRampToValueAtTime(endFrequency, duration);
122 | offlineFilter.connect(offlineCtx.destination);
123 |
124 | var offlineBufferSource = offlineCtx.createBufferSource();
125 | offlineBufferSource.buffer = impulseResponseBuffer;
126 | offlineBufferSource.connect(offlineFilter);
127 | offlineBufferSource.start();
128 |
129 | var render = offlineCtx.startRendering();
130 |
131 | // https://developer.mozilla.org/en-US/docs/Web/API/OfflineAudioContext/startRendering
132 | if (render !== undefined) {
133 | // Promises.
134 | render.then(buffer => (convolver.buffer = buffer));
135 | } else {
136 | // Callbacks.
137 | offlineCtx.oncomplete = event => (convolver.buffer = event.renderedBuffer);
138 | }
139 | };
140 |
141 | // A4 to A3.
142 | renderLowPassOffline(convolver, 1760, 220, 1);
143 |
144 | // Oscillators
145 | // f: frequency, t: parameter.
146 | var sin = f => t => Math.sin(t * 2 * Math.PI * f);
147 |
148 | var saw = f => t => {
149 | var n = ((t % (1 / f)) * f) % 1;
150 | return -1 + 2 * n;
151 | };
152 |
153 | var tri = f => t => {
154 | var n = ((t % (1 / f)) * f) % 1;
155 | return n < 0.5 ? -1 + 2 * (2 * n) : 1 - 2 * (2 * n);
156 | };
157 |
158 | var square = f => t => {
159 | var n = ((t % (1 / f)) * f) % 1;
160 | return n > 0.5 ? 1 : -1;
161 | };
162 |
163 | var decay = d => () => t => Math.exp(-t * d);
164 |
165 | // Brown noise.
166 | // https://github.com/Tonejs/Tone.js/blob/master/Tone/source/Noise.js
167 | var noise = () => {
168 | var value = 0;
169 |
170 | return () => {
171 | var step = (value + 0.02 * randFloatSpread(1)) / 1.02;
172 | value += step;
173 |
174 | // Limit to [-1, 1].
175 | if (-1 > value || value > 1) {
176 | value -= step;
177 | }
178 |
179 | return value * 3.5;
180 | };
181 | };
182 |
183 | // Operators
184 | var add = (a, b) => f => {
185 | var af = a(f);
186 | var bf = b(f);
187 |
188 | return t => af(t) + bf(t);
189 | };
190 |
191 | var mul = (a, b) => f => {
192 | var af = a(f);
193 | var bf = b(f);
194 |
195 | return t => af(t) * bf(t);
196 | };
197 |
198 | var zero = () => () => 0;
199 | var one = () => () => 1;
200 |
201 | var scale = (fn, n) => f => {
202 | var fnf = fn(f);
203 | return t => n * fnf(t);
204 | };
205 |
206 | var steps = (f, d) => f * 2 ** (d / 12);
207 |
208 | var detune = (fn, d) => f => fn(steps(f, d));
209 |
210 | // Sequencer
211 | var d = ms => new Promise(resolve => setTimeout(resolve, ms));
212 |
213 | var synthFn = mul(
214 | add(add(sin, detune(sin, 0.1)), detune(sin, -0.1)),
215 | decay(16),
216 | );
217 | var drumFn = mul(mul(sin, noise), decay(32));
218 | var snareFn = mul(mul(sin, () => () => randFloatSpread(0.8)), decay(24));
219 | var spaceFn = mul(add(sin, detune(sin, 7)), decay(1));
220 |
221 | var synth0 = generateNotes(synthFn, 2, 0.2);
222 | var synth1 = generateNotes(synthFn, 2, 0.2);
223 | var drum0 = generateNotes(drumFn, 2, 0.5);
224 | var snare0 = generateNotes(snareFn, 2, 1);
225 | var space0 = generateNotes(spaceFn, 6, 0.3);
226 |
227 | var W = 1000;
228 | var H = W / 2;
229 | var Q = H / 2;
230 | var E = Q / 2;
231 | var S = E / 2;
232 | var T = S / 2;
233 |
234 | var play = sound => playSound(sound, master);
235 |
236 | export var playSuccess = () => play(space0.e3);
237 | export var playFire = () => play(synth1.a3);
238 | export var playHit = () => play(snare0.a3);
239 | export var playBells = () => play(space0.a2);
240 |
241 | var startPlaying = async () => {
242 | audioContext.resume();
243 | };
244 |
245 | var onClick = () => {
246 | audioContext.resume();
247 | startPlaying();
248 | document.removeEventListener('click', onClick);
249 | };
250 |
251 | document.addEventListener('click', onClick);
252 |
--------------------------------------------------------------------------------
/src/box3.js:
--------------------------------------------------------------------------------
1 | import { object3d_traverse, object3d_updateMatrixWorld } from './object3d.js';
2 | import {
3 | vec3_create,
4 | vec3_add,
5 | vec3_applyMatrix4,
6 | vec3_min,
7 | vec3_max,
8 | } from './vec3.js';
9 |
10 | export var box3_create = (
11 | min = vec3_create(Infinity, Infinity, Infinity),
12 | max = vec3_create(-Infinity, -Infinity, -Infinity),
13 | ) => {
14 | return {
15 | min,
16 | max,
17 | };
18 | };
19 |
20 | export var box3_copy = (a, b) => {
21 | Object.assign(a.min, b.min);
22 | Object.assign(a.max, b.max);
23 | return a;
24 | };
25 |
26 | export var box3_makeEmpty = box => {
27 | box.min.x = box.min.y = box.min.z = Infinity;
28 | box.max.x = box.max.y = box.max.z -= Infinity;
29 | return box;
30 | };
31 |
32 | export var box3_expandByPoint = (box, point) => {
33 | vec3_min(box.min, point);
34 | vec3_max(box.max, point);
35 | return box;
36 | };
37 |
38 | export var box3_expandByObject = (() => {
39 | var scope;
40 | var v1 = vec3_create();
41 |
42 | var traverse = node => {
43 | var { geometry } = node;
44 | if (geometry) {
45 | geometry.vertices.map(vertex => {
46 | Object.assign(v1, vertex);
47 | vec3_applyMatrix4(v1, node.matrixWorld);
48 | box3_expandByPoint(scope, v1);
49 | });
50 | }
51 | };
52 |
53 | return (box, object) => {
54 | scope = box;
55 | object3d_updateMatrixWorld(object);
56 | object3d_traverse(object, traverse);
57 | return box;
58 | };
59 | })();
60 |
61 | export var box3_setFromPoints = (box, points) => {
62 | box3_makeEmpty(box);
63 | points.map(point => box3_expandByPoint(box, point));
64 | return box;
65 | };
66 |
67 | export var box3_setFromObject = (box, object) => {
68 | box3_makeEmpty(box);
69 | box3_expandByObject(box, object);
70 | return box;
71 | };
72 |
73 | export var box3_containsPoint = (box, point) => {
74 | // prettier-ignore
75 | return (
76 | box.min.x <= point.x && point.x <= box.max.x &&
77 | box.min.y <= point.y && point.y <= box.max.y &&
78 | box.min.z <= point.z && point.z <= box.max.z
79 | );
80 | };
81 |
82 | export var box3_intersectsBox = (a, b) => {
83 | // prettier-ignore
84 | return !(
85 | a.max.x < b.min.x || a.min.x > b.max.x ||
86 | a.max.y < b.min.y || a.min.y > b.max.y ||
87 | a.max.z < b.min.z || a.min.z > b.max.z
88 | );
89 | };
90 |
91 | export var box3_overlapsBox = (a, b) => {
92 | // prettier-ignore
93 | return !(
94 | a.max.x <= b.min.x || a.min.x >= b.max.x ||
95 | a.max.y <= b.min.y || a.min.y >= b.max.y ||
96 | a.max.z <= b.min.z || a.min.z >= b.max.z
97 | );
98 | };
99 |
100 | export var box3_translate = (box, offset) => {
101 | vec3_add(box.min, offset);
102 | vec3_add(box.max, offset);
103 | return box;
104 | };
105 |
--------------------------------------------------------------------------------
/src/boxColors.js:
--------------------------------------------------------------------------------
1 | import boxIndices from './boxIndices.js';
2 | import { vec3_create, vec3_fromArray } from './vec3.js';
3 | import { rearg } from './utils.js';
4 |
5 | export var setFaceVertexColor = (face, index, color) => {
6 | if (face.a === index) {
7 | face.vertexColors[0] = color;
8 | }
9 |
10 | if (face.b === index) {
11 | face.vertexColors[1] = color;
12 | }
13 |
14 | if (face.c === index) {
15 | face.vertexColors[2] = color;
16 | }
17 | };
18 |
19 | export var applyBoxVertexColors = (() => {
20 | return (geom, colors) => {
21 | Object.keys(colors).map(key => {
22 | var color = vec3_fromArray(vec3_create(), colors[key]);
23 | var indices = boxIndices[key];
24 |
25 | geom.faces.map(face =>
26 | indices.map(index => setFaceVertexColor(face, index, color)),
27 | );
28 | });
29 |
30 | return geom;
31 | };
32 | })();
33 |
34 | export var applyDefaultVertexColors = (geom, defaultColor) => {
35 | var color = vec3_fromArray(vec3_create(), defaultColor);
36 |
37 | geom.faces.map(face => {
38 | for (var i = 0; i < 3; i++) {
39 | if (face.vertexColors[i] === undefined) {
40 | face.vertexColors[i] = color;
41 | }
42 | }
43 | });
44 |
45 | return geom;
46 | };
47 |
48 | export var colors = rearg(applyBoxVertexColors);
49 | export var defaultColors = rearg(applyDefaultVertexColors);
50 |
--------------------------------------------------------------------------------
/src/boxGeom.js:
--------------------------------------------------------------------------------
1 | import { geom_create, geom_push } from './geom.js';
2 |
3 | export var boxGeom_create = (width, height, depth) => {
4 | var halfWidth = width / 2;
5 | var halfHeight = height / 2;
6 | var halfDepth = depth / 2;
7 |
8 | // Generated from new THREE.BoxGeometry(1, 1, 1).
9 | // prettier-ignore
10 | var vertices = [
11 | // px.
12 | halfWidth, halfHeight, halfDepth,
13 | halfWidth, halfHeight, -halfDepth,
14 | halfWidth, -halfHeight, halfDepth,
15 | halfWidth, -halfHeight, -halfDepth,
16 |
17 | // nx.
18 | -halfWidth, halfHeight, -halfDepth,
19 | -halfWidth, halfHeight, halfDepth,
20 | -halfWidth, -halfHeight, -halfDepth,
21 | -halfWidth, -halfHeight, halfDepth,
22 | ];
23 |
24 | // prettier-ignore
25 | var faces = [
26 | 0, 2, 1,
27 | 2, 3, 1,
28 | 4, 6, 5,
29 | 6, 7, 5,
30 | 4, 5, 1,
31 | 5, 0, 1,
32 | 7, 6, 2,
33 | 6, 3, 2,
34 | 5, 7, 0,
35 | 7, 2, 0,
36 | 1, 3, 4,
37 | 3, 6, 4,
38 | ];
39 |
40 | return geom_push(geom_create(), vertices, faces);
41 | };
42 |
--------------------------------------------------------------------------------
/src/boxIndices.js:
--------------------------------------------------------------------------------
1 | // Vertices.
2 | // pz-nz order is reversed for the nx side.
3 | var px_py_pz = [0];
4 | var px_py_nz = [1];
5 | var px_ny_pz = [2];
6 | var px_ny_nz = [3];
7 | var nx_py_nz = [4];
8 | var nx_py_pz = [5];
9 | var nx_ny_nz = [6];
10 | var nx_ny_pz = [7];
11 |
12 | // Edges.
13 | var px_py = [].concat(px_py_pz, px_py_nz);
14 | var px_ny = [].concat(px_ny_pz, px_ny_nz);
15 | var nx_py = [].concat(nx_py_nz, nx_py_pz);
16 | var nx_ny = [].concat(nx_ny_nz, nx_ny_pz);
17 |
18 | var px_pz = [].concat(px_py_pz, px_ny_pz);
19 | var px_nz = [].concat(px_py_nz, px_ny_nz);
20 | var nx_nz = [].concat(nx_py_nz, nx_ny_nz);
21 | var nx_pz = [].concat(nx_py_pz, nx_ny_pz);
22 |
23 | var py_pz = [].concat(px_py_pz, nx_py_pz);
24 | var py_nz = [].concat(px_py_nz, nx_py_nz);
25 | var ny_pz = [].concat(px_ny_pz, nx_ny_pz);
26 | var ny_nz = [].concat(px_ny_nz, nx_ny_nz);
27 |
28 | // Faces.
29 | var px = [].concat(px_py, px_ny);
30 | var nx = [].concat(nx_py, nx_ny);
31 | var py = [].concat(px_py, nx_py);
32 | var ny = [].concat(px_ny, nx_ny);
33 | var pz = [].concat(px_pz, nx_pz);
34 | var nz = [].concat(px_nz, nx_nz);
35 |
36 | // All vertices.
37 | var all = [].concat(px, nx);
38 |
39 | export default {
40 | px_py_pz,
41 | px_py_nz,
42 | px_ny_pz,
43 | px_ny_nz,
44 | nx_py_nz,
45 | nx_py_pz,
46 | nx_ny_nz,
47 | nx_ny_pz,
48 |
49 | px_py,
50 | px_ny,
51 | nx_py,
52 | nx_ny,
53 |
54 | px_pz,
55 | px_nz,
56 | nx_nz,
57 | nx_pz,
58 |
59 | py_pz,
60 | py_nz,
61 | ny_pz,
62 | ny_nz,
63 |
64 | px,
65 | nx,
66 | py,
67 | ny,
68 | pz,
69 | nz,
70 |
71 | all,
72 | };
73 |
--------------------------------------------------------------------------------
/src/boxTransforms.js:
--------------------------------------------------------------------------------
1 | import { geom_translate } from './geom.js';
2 | import boxIndices from './boxIndices.js';
3 | import {
4 | vec3_create,
5 | vec3_add,
6 | vec3_divideScalar,
7 | vec3_fromArray,
8 | vec3_multiply,
9 | vec3_set,
10 | vec3_setScalar,
11 | vec3_setX,
12 | vec3_setY,
13 | vec3_setZ,
14 | vec3_subVectors,
15 | } from './vec3.js';
16 | import { rearg } from './utils.js';
17 |
18 | var computeCentroid = (geom, indices, vector = vec3_create()) => {
19 | vec3_set(vector, 0, 0, 0);
20 |
21 | indices.map(index => vec3_add(vector, geom.vertices[index]));
22 | vec3_divideScalar(vector, indices.length);
23 |
24 | return vector;
25 | };
26 |
27 | var alignBoxVertices = (() => {
28 | var centroid = vec3_create();
29 |
30 | return (geom, key) => {
31 | var indices = boxIndices[key];
32 | computeCentroid(geom, indices, centroid);
33 | return geom_translate(geom, -centroid.x, -centroid.y, -centroid.z);
34 | };
35 | })();
36 |
37 | var relativeAlignBoxVertices = (() => {
38 | var centroidA = vec3_create();
39 | var centroidB = vec3_create();
40 | var delta = vec3_create();
41 |
42 | return (geomA, keyA, geomB, keyB) => {
43 | var indicesA = boxIndices[keyA];
44 | var indicesB = boxIndices[keyB];
45 |
46 | computeCentroid(geomA, indicesA, centroidA);
47 | computeCentroid(geomB, indicesB, centroidB);
48 |
49 | vec3_subVectors(delta, centroidB, centroidA);
50 | return geom_translate(geomA, delta.x, delta.y, delta.z);
51 | };
52 | })();
53 |
54 | export var align = rearg(alignBoxVertices);
55 | export var relativeAlign = rearg(relativeAlignBoxVertices);
56 |
57 | var transformBoxVertices = (() => {
58 | var vector = vec3_create();
59 |
60 | return (method, identity = vec3_create()) => {
61 | return (geom, vectors) => {
62 | Object.keys(vectors).map(key => {
63 | var delta = vectors[key];
64 | var indices = boxIndices[key];
65 |
66 | if (Array.isArray(delta)) {
67 | vec3_fromArray(vector, delta);
68 | } else if (typeof delta === 'object') {
69 | Object.assign(vector, identity, delta);
70 | } else if (typeof delta === 'number') {
71 | vec3_setScalar(vector, delta);
72 | } else {
73 | return;
74 | }
75 |
76 | indices.map(index => method(geom.vertices[index], vector));
77 | });
78 |
79 | return geom;
80 | };
81 | };
82 | })();
83 |
84 | export var $translate = rearg(transformBoxVertices(vec3_add));
85 | export var $scale = rearg(
86 | transformBoxVertices(vec3_multiply, vec3_create(1, 1, 1)),
87 | );
88 |
89 | var transformAxisBoxVertices = (() => {
90 | var vector = vec3_create();
91 |
92 | return (method, identity = vec3_create()) => {
93 | return axis => {
94 | return (geom, vectors) => {
95 | Object.keys(vectors).map(key => {
96 | var { [key]: delta = identity[axis] } = vectors;
97 | var indices = boxIndices[key];
98 |
99 | Object.assign(vector, identity);
100 | vector[axis] = delta;
101 |
102 | indices.map(index => method(geom.vertices[index], vector));
103 | });
104 |
105 | return geom;
106 | };
107 | };
108 | };
109 | })();
110 |
111 | var translateAxisBoxVertices = transformAxisBoxVertices(vec3_add);
112 |
113 | export var $translateX = rearg(translateAxisBoxVertices('x'));
114 | export var $translateY = rearg(translateAxisBoxVertices('y'));
115 | export var $translateZ = rearg(translateAxisBoxVertices('z'));
116 |
117 | var callBoxVertices = method => {
118 | return (geom, vectors) => {
119 | Object.keys(vectors).map(key => {
120 | var value = vectors[key];
121 | var indices = boxIndices[key];
122 | indices.map(index => method(geom.vertices[index], value));
123 | });
124 |
125 | return geom;
126 | };
127 | };
128 |
129 | export var $set = rearg(callBoxVertices(vec3_fromArray));
130 | export var $setX = rearg(callBoxVertices(vec3_setX));
131 | export var $setY = rearg(callBoxVertices(vec3_setY));
132 | export var $setZ = rearg(callBoxVertices(vec3_setZ));
133 |
--------------------------------------------------------------------------------
/src/bufferAttr.js:
--------------------------------------------------------------------------------
1 | export var bufferAttr_copyVector3sArray = (array, vectors) => {
2 | var offset = 0;
3 |
4 | vectors.map(vector => {
5 | array[offset++] = vector.x;
6 | array[offset++] = vector.y;
7 | array[offset++] = vector.z;
8 | });
9 |
10 | return array;
11 | };
12 |
--------------------------------------------------------------------------------
/src/bufferGeom.js:
--------------------------------------------------------------------------------
1 | import { bufferAttr_copyVector3sArray } from './bufferAttr.js';
2 | import { directGeom_fromGeom } from './directGeom.js';
3 |
4 | export var bufferGeom_create = () => {
5 | return {
6 | attrs: {},
7 | };
8 | };
9 |
10 | export var bufferGeom_fromGeom = (bufferGeom, geom) =>
11 | bufferGeom_fromDirectGeom(bufferGeom, directGeom_fromGeom(geom));
12 |
13 | export var bufferGeom_fromDirectGeom = (bufferGeom, geom) => {
14 | var positions = new Float32Array(geom.vertices.length * 3);
15 | bufferGeom.attrs.position = bufferAttr_copyVector3sArray(
16 | positions,
17 | geom.vertices,
18 | );
19 |
20 | var colors = new Float32Array(geom.colors.length * 3);
21 | bufferGeom.attrs.color = bufferAttr_copyVector3sArray(colors, geom.colors);
22 |
23 | return bufferGeom;
24 | };
25 |
--------------------------------------------------------------------------------
/src/camera.js:
--------------------------------------------------------------------------------
1 | import { mat4_create, mat4_lookAt } from './mat4.js';
2 | import { object3d_create } from './object3d.js';
3 | import { quat_setFromRotationMatrix } from './quat.js';
4 | import { vec3_clone, vec3_Y } from './vec3.js';
5 |
6 | var DEG_TO_RAD = Math.PI / 180;
7 |
8 | export var camera_create = (fov = 60, aspect = 1, near = 0.1, far = 2000) => {
9 | var camera = {
10 | ...object3d_create(),
11 | fov,
12 | near,
13 | far,
14 | aspect,
15 | up: vec3_clone(vec3_Y),
16 | matrixWorldInverse: mat4_create(),
17 | projectionMatrix: mat4_create(),
18 | };
19 |
20 | camera_updateProjectionMatrix(camera);
21 |
22 | return camera;
23 | };
24 |
25 | export var camera_lookAt = (() => {
26 | var m1 = mat4_create();
27 |
28 | return (camera, vector) => {
29 | mat4_lookAt(m1, camera.position, vector, camera.up);
30 | quat_setFromRotationMatrix(camera.quaternion, m1);
31 | };
32 | })();
33 |
34 | export var camera_updateProjectionMatrix = camera => {
35 | var { near, far } = camera;
36 |
37 | var top = near * Math.tan(camera.fov * 0.5 * DEG_TO_RAD);
38 | var bottom = -top;
39 | var left = bottom * camera.aspect;
40 | var right = top * camera.aspect;
41 |
42 | var x = (2 * near) / (right - left);
43 | var y = (2 * near) / (top - bottom);
44 |
45 | var a = (right + left) / (right - left);
46 | var b = (top + bottom) / (top - bottom);
47 | var c = -(far + near) / (far - near);
48 | var d = (-2 * far * near) / (far - near);
49 |
50 | // prettier-ignore
51 | camera.projectionMatrix.set([
52 | x, 0, 0, 0,
53 | 0, y, 0, 0,
54 | a, b, c, -1,
55 | 0, 0, d, 0,
56 | ]);
57 | };
58 |
--------------------------------------------------------------------------------
/src/controls.js:
--------------------------------------------------------------------------------
1 | import { clamp } from './math.js';
2 | import { quat_create, quat_multiply, quat_setFromEuler } from './quat.js';
3 | import { vec3_create } from './vec3.js';
4 |
5 | var pitchQuat = quat_create();
6 | var yawQuat = quat_create();
7 |
8 | export var controls_create = object => {
9 | var pitchEuler = vec3_create();
10 | var yawEuler = vec3_create();
11 |
12 | var controls = {
13 | object,
14 | sensitivity: 0.002,
15 | enabled: false,
16 | onMouseMove(event) {
17 | if (!controls.enabled) {
18 | return;
19 | }
20 |
21 | var { movementX, movementY } = event;
22 |
23 | var pitch = -movementY * controls.sensitivity;
24 | var yaw = -movementX * controls.sensitivity;
25 |
26 | pitchEuler.x += pitch;
27 | yawEuler.y += yaw;
28 |
29 | pitchEuler.x = clamp(pitchEuler.x, -Math.PI / 2, Math.PI / 2);
30 |
31 | quat_setFromEuler(pitchQuat, pitchEuler);
32 | quat_setFromEuler(yawQuat, yawEuler);
33 |
34 | quat_multiply(yawQuat, pitchQuat);
35 | Object.assign(object.quaternion, yawQuat);
36 | },
37 | };
38 |
39 | document.addEventListener('mousemove', controls.onMouseMove);
40 |
41 | return controls;
42 | };
43 |
44 | export var controls_dispose = controls => {
45 | document.removeEventListener('mousemove', controls.onMouseMove);
46 | };
47 |
--------------------------------------------------------------------------------
/src/directGeom.js:
--------------------------------------------------------------------------------
1 | export var directGeom_fromGeom = geom => {
2 | var vertices = [];
3 | var colors = [];
4 |
5 | geom.faces.map(face => {
6 | vertices.push(
7 | geom.vertices[face.a],
8 | geom.vertices[face.b],
9 | geom.vertices[face.c],
10 | );
11 |
12 | var { vertexColors } = face;
13 | if (vertexColors.length === 3) {
14 | colors.push(...vertexColors);
15 | } else {
16 | var { color } = face;
17 | colors.push(color, color, color);
18 | }
19 | });
20 |
21 | return {
22 | vertices,
23 | colors,
24 | };
25 | };
26 |
--------------------------------------------------------------------------------
/src/directionalLight.js:
--------------------------------------------------------------------------------
1 | import { object3d_create } from './object3d.js';
2 | import { vec3_create } from './vec3.js';
3 |
4 | export var light_create = (color = vec3_create(), intensity = 1) => {
5 | return {
6 | ...object3d_create(),
7 | color,
8 | intensity,
9 | target: object3d_create(),
10 | };
11 | };
12 |
--------------------------------------------------------------------------------
/src/entity.js:
--------------------------------------------------------------------------------
1 | export var component_create = options => {
2 | return {
3 | parent: undefined,
4 | update() {},
5 | ...options,
6 | };
7 | };
8 |
9 | export var entity_add = (entity, ...components) => {
10 | components.map(component => {
11 | if (entity_has(entity, component)) {
12 | return;
13 | }
14 |
15 | component.parent = entity;
16 | entity.components.push(component);
17 | });
18 |
19 | return entity;
20 | };
21 |
22 | export var entity_has = (entity, component) => {
23 | return entity.components.includes(component);
24 | };
25 |
26 | export var entity_find = (entity, predicate) => {
27 | return entity.components.find(predicate);
28 | };
29 |
30 | export var entity_filter = (entity, predicate) => {
31 | return entity.components.filter(predicate);
32 | };
33 |
34 | export var entity_remove = (entity, ...components) => {
35 | components.map(component => {
36 | var index = entity.components.indexOf(component);
37 |
38 | if (index >= 0) {
39 | entity.components
40 | .splice(index, 1)
41 | .map(component => (component.parent = undefined));
42 | }
43 | });
44 | };
45 |
46 | export var entity_update = (entity, ...args) => {
47 | entity.components.map(component => component.update(component, ...args));
48 | };
49 |
--------------------------------------------------------------------------------
/src/face3.js:
--------------------------------------------------------------------------------
1 | import { vec3_create, vec3_clone } from './vec3.js';
2 |
3 | export var face3_create = (a, b, c) => {
4 | return {
5 | a,
6 | b,
7 | c,
8 | color: vec3_create(1, 1, 1),
9 | vertexColors: [],
10 | };
11 | };
12 |
13 | export var face3_clone = face => {
14 | return {
15 | a: face.a,
16 | b: face.b,
17 | c: face.c,
18 | color: vec3_clone(face.color),
19 | vertexColors: face.vertexColors.map(vec3_clone),
20 | };
21 | };
22 |
--------------------------------------------------------------------------------
/src/fbm.js:
--------------------------------------------------------------------------------
1 | import { noise3d } from './noise.js';
2 |
3 | export var fbm3d = ({
4 | octaves = 8,
5 | period = 16,
6 | lacunarity = 2,
7 | gain = 0.5,
8 | } = {}) => {
9 | return (x, y, z) => {
10 | var frequency = 1 / period;
11 | var amplitude = gain;
12 |
13 | var sum = 0;
14 | for (var i = 0; i < octaves; i++) {
15 | sum += amplitude * noise3d(x * frequency, y * frequency, z * frequency);
16 |
17 | frequency *= lacunarity;
18 | amplitude *= gain;
19 | }
20 |
21 | return sum;
22 | };
23 | };
24 |
--------------------------------------------------------------------------------
/src/geom.js:
--------------------------------------------------------------------------------
1 | import {
2 | vec3_create,
3 | vec3_set,
4 | vec3_clone,
5 | vec3_add,
6 | vec3_multiply,
7 | } from './vec3.js';
8 | import { face3_create, face3_clone } from './face3.js';
9 |
10 | export var geom_create = () => {
11 | return {
12 | vertices: [],
13 | faces: [],
14 | };
15 | };
16 |
17 | export var geom_push = (geom, vertices, faces) => {
18 | var offset = geom.vertices.length;
19 |
20 | var i;
21 | for (i = 0; i < vertices.length; i += 3) {
22 | geom.vertices.push(
23 | vec3_create(vertices[i], vertices[i + 1], vertices[i + 2]),
24 | );
25 | }
26 |
27 | for (i = 0; i < faces.length; i += 3) {
28 | geom.faces.push(
29 | face3_create(
30 | offset + faces[i],
31 | offset + faces[i + 1],
32 | offset + faces[i + 2],
33 | ),
34 | );
35 | }
36 |
37 | return geom;
38 | };
39 |
40 | export var geom_translate = (() => {
41 | var vector = vec3_create();
42 |
43 | return (geom, x, y, z) => {
44 | vec3_set(vector, x, y, z);
45 | geom.vertices.map(vertex => vec3_add(vertex, vector));
46 | return geom;
47 | };
48 | })();
49 |
50 | export var geom_scale = (() => {
51 | var vector = vec3_create();
52 |
53 | return (geom, x, y, z) => {
54 | vec3_set(vector, x, y, z);
55 | geom.vertices.map(vertex => vec3_multiply(vertex, vector));
56 | return geom;
57 | };
58 | })();
59 |
60 | export var geom_merge = (a, b) => {
61 | var vertexOffset = a.vertices.length;
62 |
63 | a.vertices.push(...b.vertices.map(vec3_clone));
64 |
65 | a.faces.push(
66 | ...b.faces.map(face => {
67 | var faceCopy = face3_clone(face);
68 | faceCopy.a += vertexOffset;
69 | faceCopy.b += vertexOffset;
70 | faceCopy.c += vertexOffset;
71 | return faceCopy;
72 | }),
73 | );
74 |
75 | return a;
76 | };
77 |
78 | export var geom_clone = geom => {
79 | var clone = geom_create();
80 | clone.vertices = geom.vertices.map(vec3_clone);
81 | clone.faces = geom.faces.map(face3_clone);
82 | return clone;
83 | };
84 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | /* global c */
2 |
3 | import {} from './audio.js';
4 | import { bufferGeom_fromGeom, bufferGeom_create } from './bufferGeom.js';
5 | import { camera_create, camera_updateProjectionMatrix } from './camera.js';
6 | import { controls_create } from './controls.js';
7 | import { entity_update } from './entity.js';
8 | import { createMap } from './maps.js';
9 | import { mat4_getInverse, mat4_multiplyMatrices } from './mat4.js';
10 | import {
11 | object3d_create,
12 | object3d_traverse,
13 | object3d_updateMatrixWorld,
14 | } from './object3d.js';
15 | import { pointerLock_create } from './pointerLock.js';
16 | import {
17 | createShaderProgram,
18 | createFloat32Buffer,
19 | setFloat32Attribute,
20 | setFloatUniform,
21 | setMat4Uniform,
22 | setVec3Uniform,
23 | getAttributeLocations,
24 | getUniformLocations,
25 | } from './shader.js';
26 | import {
27 | vec3_create,
28 | vec3_multiplyScalar,
29 | vec3_setFromMatrixPosition,
30 | vec3_sub,
31 | vec3_transformDirection,
32 | } from './vec3.js';
33 |
34 | import { vert } from './shaders/phong_vert.glsl.js';
35 | import { frag } from './shaders/phong_frag.glsl.js';
36 |
37 | var gl = c.getContext('webgl');
38 |
39 | gl.clearColor(0, 0, 0, 0);
40 | gl.enable(gl.DEPTH_TEST);
41 | gl.enable(gl.CULL_FACE);
42 | gl.getExtension('OES_standard_derivatives');
43 |
44 | var running = true;
45 |
46 | // Scene
47 | var scene = object3d_create();
48 | scene.fogColor = vec3_create(1, 1, 1);
49 | scene.fogNear = 1;
50 | scene.fogFar = 1000;
51 |
52 | // Camera
53 | var camera = camera_create(90);
54 | pointerLock_create(controls_create(camera), c);
55 |
56 | var lights = createMap(gl, scene, camera);
57 |
58 | // Shader
59 | var program = createShaderProgram(
60 | gl,
61 | vert,
62 | frag.replace(/NUM_DIR_LIGHTS/g, lights.directional.length),
63 | );
64 |
65 | gl.useProgram(program);
66 |
67 | var attributes = getAttributeLocations(gl, program);
68 | var uniforms = getUniformLocations(gl, program);
69 |
70 | var dt = 1 / 60;
71 | var accumulatedTime = 0;
72 | var previousTime;
73 |
74 | var update = () => {
75 | var time = (performance.now() || 0) * 1e-3;
76 | if (!previousTime) {
77 | previousTime = time;
78 | }
79 |
80 | var frameTime = Math.min(time - previousTime, 0.1);
81 | accumulatedTime += frameTime;
82 | previousTime = time;
83 |
84 | while (accumulatedTime >= dt) {
85 | object3d_traverse(scene, object => {
86 | entity_update(object, dt, scene);
87 | });
88 |
89 | accumulatedTime -= dt;
90 | }
91 | };
92 |
93 | var bufferGeomBuffers = new WeakMap();
94 |
95 | var setFloat32AttributeBuffer = (name, location, bufferGeom, size) => {
96 | var buffers = bufferGeomBuffers.get(bufferGeom);
97 |
98 | if (!buffers) {
99 | buffers = {};
100 | bufferGeomBuffers.set(bufferGeom, buffers);
101 | }
102 |
103 | var buffer = buffers[name];
104 | if (!buffer) {
105 | buffer = createFloat32Buffer(gl, bufferGeom.attrs[name]);
106 | buffers[name] = buffer;
107 | }
108 |
109 | setFloat32Attribute(gl, location, buffer, size);
110 | };
111 |
112 | var bufferGeoms = new WeakMap();
113 |
114 | var renderMesh = mesh => {
115 | var { geometry, material } = mesh;
116 |
117 | setVec3Uniform(gl, uniforms.fogColor, scene.fogColor);
118 | setFloatUniform(gl, uniforms.fogNear, scene.fogNear);
119 | setFloatUniform(gl, uniforms.fogFar, scene.fogFar);
120 |
121 | setVec3Uniform(gl, uniforms.diffuse, material.color);
122 | setVec3Uniform(gl, uniforms.specular, material.specular);
123 | setFloatUniform(gl, uniforms.shininess, material.shininess);
124 | setVec3Uniform(gl, uniforms.emissive, material.emissive);
125 |
126 | mat4_multiplyMatrices(
127 | mesh.modelViewMatrix,
128 | camera.matrixWorldInverse,
129 | mesh.matrixWorld,
130 | );
131 |
132 | setMat4Uniform(gl, uniforms.modelViewMatrix, mesh.modelViewMatrix);
133 | setMat4Uniform(gl, uniforms.projectionMatrix, camera.projectionMatrix);
134 |
135 | var bufferGeom = bufferGeoms.get(geometry);
136 | if (!bufferGeom) {
137 | bufferGeom = bufferGeom_fromGeom(bufferGeom_create(), geometry);
138 | bufferGeoms.set(geometry, bufferGeom);
139 | }
140 |
141 | setFloat32AttributeBuffer('position', attributes.position, bufferGeom, 3);
142 | setFloat32AttributeBuffer('color', attributes.color, bufferGeom, 3);
143 |
144 | gl.drawArrays(gl.TRIANGLES, 0, bufferGeom.attrs.position.length / 3);
145 | };
146 |
147 | var lightDirection = vec3_create();
148 |
149 | var render = () => {
150 | object3d_updateMatrixWorld(scene);
151 | mat4_getInverse(camera.matrixWorldInverse, camera.matrixWorld);
152 |
153 | gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
154 |
155 | setVec3Uniform(gl, uniforms.ambientLightColor, lights.ambient);
156 |
157 | lights.directional.map((light, index) => {
158 | var temp = vec3_create();
159 |
160 | var direction = vec3_setFromMatrixPosition(
161 | lightDirection,
162 | light.matrixWorld,
163 | );
164 | vec3_setFromMatrixPosition(temp, light.target.matrixWorld);
165 | vec3_transformDirection(
166 | vec3_sub(direction, temp),
167 | camera.matrixWorldInverse,
168 | );
169 |
170 | var color = vec3_multiplyScalar(
171 | Object.assign(temp, light.color),
172 | light.intensity,
173 | );
174 |
175 | setVec3Uniform(
176 | gl,
177 | uniforms[`directionalLights[${index}].direction`],
178 | direction,
179 | );
180 | setVec3Uniform(gl, uniforms[`directionalLights[${index}].color`], color);
181 | });
182 |
183 | object3d_traverse(scene, object => {
184 | if (object.visible && object.geometry && object.material) {
185 | renderMesh(object);
186 | }
187 | });
188 | };
189 |
190 | var animate = () => {
191 | update();
192 | render();
193 |
194 | if (running) {
195 | requestAnimationFrame(animate);
196 | }
197 | };
198 |
199 | var setSize = (width, height) => {
200 | var { devicePixelRatio = 1 } = window;
201 |
202 | c.width = width * devicePixelRatio;
203 | c.height = height * devicePixelRatio;
204 | c.style.width = `${width}px`;
205 | c.style.height = `${height}px`;
206 | gl.viewport(0, 0, c.width, c.height);
207 |
208 | camera.aspect = width / height;
209 | camera_updateProjectionMatrix(camera);
210 | };
211 |
212 | setSize(window.innerWidth, window.innerHeight);
213 | animate();
214 |
215 | window.addEventListener('resize', () => {
216 | setSize(window.innerWidth, window.innerHeight);
217 | render();
218 | });
219 |
220 | document.addEventListener('keypress', event => {
221 | // Pause/play.
222 | if (event.code === 'KeyP') {
223 | running = !running;
224 | if (running) {
225 | animate();
226 | } else {
227 | document.exitPointerLock();
228 | }
229 | }
230 | });
231 |
--------------------------------------------------------------------------------
/src/keys.js:
--------------------------------------------------------------------------------
1 | export var keys_create = () => {
2 | var keys = {};
3 |
4 | document.addEventListener('keydown', event => (keys[event.code] = true));
5 | document.addEventListener('keyup', event => (keys[event.code] = false));
6 |
7 | return keys;
8 | };
9 |
--------------------------------------------------------------------------------
/src/maps.js:
--------------------------------------------------------------------------------
1 | import { playSuccess, playFire, playHit, playBells } from './audio.js';
2 | import { box3_create, box3_containsPoint } from './box3.js';
3 | import { boxGeom_create } from './boxGeom.js';
4 | import { colors, defaultColors } from './boxColors.js';
5 | import { align, $scale } from './boxTransforms.js';
6 | import { component_create, entity_add } from './entity.js';
7 | import { geom_merge } from './geom.js';
8 | import { keys_create } from './keys.js';
9 | import { light_create } from './directionalLight.js';
10 | import { fbm3d } from './fbm.js';
11 | import { material_create } from './material.js';
12 | import { clamp, mapLinear, randFloat } from './math.js';
13 | import { mesh_create } from './mesh.js';
14 | import { object3d_create, object3d_add, object3d_lookAt } from './object3d.js';
15 | import {
16 | player_create,
17 | player_update,
18 | PMF_GRAPPLE_FLY,
19 | PMF_GRAPPLE_PULL,
20 | PMF_GRAPPLE,
21 | GRAPPLE_SPEED,
22 | } from './player.js';
23 | import {
24 | physics_add,
25 | physics_bodies,
26 | physics_update,
27 | get_physics_component,
28 | BODY_STATIC,
29 | BODY_DYNAMIC,
30 | } from './physics.js';
31 | import { ray_create, ray_intersectObjects } from './ray.js';
32 | import {
33 | vec3_create,
34 | vec3_add,
35 | vec3_addScaledVector,
36 | vec3_applyQuaternion,
37 | vec3_cross,
38 | vec3_distanceTo,
39 | vec3_fromArray,
40 | vec3_length,
41 | vec3_multiplyScalar,
42 | vec3_normalize,
43 | vec3_set,
44 | vec3_subVectors,
45 | } from './vec3.js';
46 | import { compose } from './utils.js';
47 |
48 | var DEBUG = false;
49 |
50 | export var createMap = (gl, scene, camera) => {
51 | var fogColor = [0.8, 0.9, 1];
52 | gl.clearColor(...fogColor, 1);
53 | vec3_set(scene.fogColor, ...fogColor);
54 | scene.fogFar = 2048;
55 |
56 | var map = object3d_create();
57 | object3d_add(scene, map);
58 |
59 | var keys = keys_create();
60 |
61 | // Lights
62 | var ambient = vec3_create(0.3, 0.3, 0.3);
63 |
64 | var light0 = light_create(vec3_create(0.8, 0.8, 1));
65 | vec3_set(light0.position, 0, 64, 256);
66 |
67 | var light1 = light_create(vec3_create(0.5, 0.5, 0.6), 4);
68 | vec3_set(light1.position, 128, 512, -128);
69 |
70 | var directionalLights = [light0, light1];
71 |
72 | directionalLights.map(light => object3d_add(map, light));
73 |
74 | // Camera
75 | var cameraObject = object3d_create();
76 | object3d_add(cameraObject, camera);
77 | object3d_add(map, cameraObject);
78 |
79 | // Action
80 | var playerMesh = physics_add(
81 | mesh_create(boxGeom_create(30, 56, 30), material_create()),
82 | BODY_DYNAMIC,
83 | );
84 | playerMesh.position.y += 28;
85 | playerMesh.visible = false;
86 | object3d_add(map, playerMesh);
87 |
88 | var player = player_create(playerMesh, get_physics_component(playerMesh));
89 | player.scene = map;
90 |
91 | var GRAPPLE_OFFSET = vec3_create(8, -8, 0);
92 | var grappleStartDelta = vec3_create();
93 | var grapplePositionDelta = vec3_create();
94 |
95 | var rayGeometry = align('nz')(boxGeom_create(2, 1, 1));
96 | var rayMaterial = material_create();
97 |
98 | vec3_set(rayMaterial.color, 0.1, 0.1, 0.1);
99 | vec3_set(rayMaterial.emissive, 0.9, 0.9, 1);
100 | var rayMesh = mesh_create(rayGeometry, rayMaterial);
101 | rayMesh.visible = false;
102 | object3d_add(map, rayMesh);
103 |
104 | var pointGeometry = boxGeom_create(3, 3, 3);
105 | var pointMaterial = material_create();
106 | vec3_set(pointMaterial.specular, 0.3, 0.3, 0.3);
107 | var pointMesh = mesh_create(pointGeometry, pointMaterial);
108 | pointMesh.visible = false;
109 | object3d_add(map, pointMesh);
110 |
111 | var grappleAmmo = 100;
112 | var grappleFireRate = grappleAmmo / 6;
113 | var grappleRegenRate = grappleAmmo / 8;
114 |
115 | var gameEl = document.querySelector('.g');
116 | var progressEl = document.querySelector('.p');
117 | progressEl.hidden = false;
118 |
119 | var safePositions = [
120 | // Beginning
121 | [0, 60, 0],
122 | // First leap
123 | [0, 180, -900],
124 | // Island
125 | [372, 80, -2700],
126 | // Tiny outcrop
127 | [1560, 24, -2960],
128 | // Play area
129 | [2480, 128, -1600],
130 | // Drop
131 | [5448, 80, -2112],
132 | // Run up end
133 | [5876, 200, -7200],
134 | // Atrium
135 | [8192, -144, -11800],
136 | ].map(([x, y, z]) => vec3_create(x, y, z));
137 | var lastSafeIndex = 0;
138 | var safePositionThreshold = 64;
139 |
140 | if (DEBUG) {
141 | Object.assign(playerMesh.position, safePositions[safePositions.length - 1]);
142 | }
143 |
144 | var checkpointGeom = geom_merge(
145 | compose(
146 | align('ny'),
147 | $scale({ py: [0, 1, 0] }),
148 | )(boxGeom_create(24, 24, 24)),
149 | compose(
150 | align('py'),
151 | $scale({ ny: [0, 1, 0] }),
152 | )(boxGeom_create(24, 24, 24)),
153 | );
154 | var checkpointMeshes = safePositions.map((position, index) => {
155 | var material = material_create();
156 | vec3_set(material.color, 0.5, 0.5, 0.5);
157 | vec3_set(material.emissive, 0.5, 0.5, 0.5);
158 | var mesh = mesh_create(checkpointGeom, material);
159 | Object.assign(mesh.position, position);
160 | mesh.position.y += 28;
161 | mesh.visible = index > 0;
162 | object3d_add(map, mesh);
163 | return mesh;
164 | });
165 |
166 | if (DEBUG) {
167 | document.addEventListener('keydown', event => {
168 | if (event.code === 'ShiftRight') {
169 | // eslint-disable-next-line no-console
170 | console.log(playerMesh.position);
171 | }
172 | });
173 | }
174 |
175 | var atriumBox = box3_create(
176 | vec3_create(7680, -260, -12312),
177 | vec3_create(8704, 0, -11288),
178 | );
179 |
180 | var ceilingMaterial = material_create();
181 | var ceilingMesh = physics_add(
182 | mesh_create(boxGeom_create(1024, 24, 1024), ceilingMaterial),
183 | BODY_STATIC,
184 | );
185 | object3d_add(map, ceilingMesh);
186 | vec3_set(ceilingMaterial.color, 0.5, 0.5, 0.5);
187 | vec3_set(ceilingMesh.position, 8192, 5120, -11800);
188 |
189 | entity_add(
190 | map,
191 | component_create({
192 | update(component, dt) {
193 | var bodies = physics_bodies(map);
194 | physics_update(bodies);
195 |
196 | player.dt = dt;
197 |
198 | player.command.forward = 0;
199 | player.command.right = 0;
200 | player.command.up = 0;
201 | player.command.hook = 0;
202 |
203 | if (keys.KeyW || keys.ArrowUp) player.command.forward++;
204 | if (keys.KeyS || keys.ArrowDown) player.command.forward--;
205 | if (keys.KeyA || keys.ArrowLeft) player.command.right--;
206 | if (keys.KeyD || keys.ArrowRight) player.command.right++;
207 | if (keys.Space) player.command.up++;
208 | if (keys.ShiftLeft || keys.ShiftRight) player.command.hook++;
209 |
210 | var movespeed = 127;
211 | player.command.forward *= movespeed;
212 | player.command.right *= movespeed;
213 | player.command.up *= movespeed;
214 |
215 | vec3_applyQuaternion(
216 | vec3_set(player.viewForward, 0, 0, -1),
217 | camera.quaternion,
218 | );
219 | vec3_normalize(
220 | vec3_cross(vec3_set(player.viewRight, 0, -1, 0), player.viewForward),
221 | );
222 |
223 | player_update(player);
224 | Object.assign(cameraObject.position, playerMesh.position);
225 |
226 | // Grappling hook
227 | var ray = ray_create();
228 | Object.assign(ray.origin, playerMesh.position);
229 | vec3_set(ray.direction, 0, 0, -1);
230 | vec3_applyQuaternion(ray.direction, camera.quaternion);
231 |
232 | var isGrappling = player.movementFlags & PMF_GRAPPLE;
233 |
234 | if (player.command.hook) {
235 | // Hook fire.
236 | if (!isGrappling) {
237 | var intersections = ray_intersectObjects(
238 | ray,
239 | bodies
240 | .map(body => body.parent)
241 | .filter(object => object !== playerMesh),
242 | );
243 | if (intersections.length) {
244 | Object.assign(player.grapplePoint, intersections[0].point);
245 | Object.assign(pointMesh.position, playerMesh.position);
246 | vec3_add(
247 | pointMesh.position,
248 | vec3_applyQuaternion(
249 | Object.assign(grappleStartDelta, GRAPPLE_OFFSET),
250 | camera.quaternion,
251 | ),
252 | );
253 | player.movementFlags |= PMF_GRAPPLE_FLY;
254 | playFire();
255 | }
256 | }
257 | } else {
258 | // Hook free.
259 | player.movementFlags &= ~PMF_GRAPPLE;
260 | }
261 |
262 | // Hook think.
263 | isGrappling = player.movementFlags & PMF_GRAPPLE;
264 | rayMesh.visible = isGrappling;
265 | pointMesh.visible = isGrappling;
266 | if (isGrappling) {
267 | if (player.movementFlags & PMF_GRAPPLE_FLY) {
268 | // Hook move.
269 | vec3_subVectors(
270 | grapplePositionDelta,
271 | player.grapplePoint,
272 | pointMesh.position,
273 | );
274 | var grappleDeltaLength = Math.min(
275 | vec3_length(grapplePositionDelta),
276 | GRAPPLE_SPEED * dt,
277 | );
278 | vec3_normalize(grapplePositionDelta);
279 | vec3_addScaledVector(
280 | pointMesh.position,
281 | grapplePositionDelta,
282 | grappleDeltaLength,
283 | );
284 |
285 | // Hook connected.
286 | if (!grappleDeltaLength) {
287 | player.movementFlags &= ~PMF_GRAPPLE_FLY;
288 | player.movementFlags |= PMF_GRAPPLE_PULL;
289 | playHit();
290 | }
291 | }
292 |
293 | Object.assign(rayMesh.position, playerMesh.position);
294 | vec3_add(
295 | rayMesh.position,
296 | vec3_applyQuaternion(
297 | Object.assign(grappleStartDelta, GRAPPLE_OFFSET),
298 | camera.quaternion,
299 | ),
300 | );
301 | rayMesh.scale.z = vec3_distanceTo(
302 | rayMesh.position,
303 | pointMesh.position,
304 | );
305 | object3d_lookAt(rayMesh, pointMesh.position);
306 | }
307 |
308 | grappleAmmo += isGrappling
309 | ? -grappleFireRate * dt
310 | : grappleRegenRate * dt;
311 | if (grappleAmmo <= 0) {
312 | player.movementFlags &= ~PMF_GRAPPLE;
313 | keys.ShiftLeft = false;
314 | keys.ShiftRight = false;
315 | }
316 | grappleAmmo = clamp(grappleAmmo, 0, 100);
317 |
318 | progressEl.style.setProperty('--p-w', `${grappleAmmo}%`);
319 | var opacity = mapLinear(playerMesh.position.y, -512, -1536, 1, 0);
320 | gameEl.style.opacity = clamp(opacity, 0, 1);
321 |
322 | // Safe positions
323 | safePositions.map((position, index) => {
324 | var mesh = checkpointMeshes[index];
325 | var hasVisited = mesh.material.emissive.y === 1;
326 | if (
327 | position &&
328 | vec3_distanceTo(playerMesh.position, position) <=
329 | safePositionThreshold
330 | ) {
331 | lastSafeIndex = index;
332 | mesh.material.emissive.y = 1;
333 | if (!hasVisited && index > 0) {
334 | playSuccess();
335 | }
336 | }
337 | });
338 |
339 | var isDead = playerMesh.position.y <= -2048;
340 | var safePosition = safePositions[lastSafeIndex];
341 | if (isDead && safePosition) {
342 | playBells();
343 | Object.assign(playerMesh.position, safePosition);
344 | grappleAmmo = 100;
345 | }
346 |
347 | // Close the box.
348 | if (box3_containsPoint(atriumBox, playerMesh.position)) {
349 | ceilingMesh.position.y -= 2048 * dt;
350 | ceilingMesh.position.y = Math.max(-132, ceilingMesh.position.y);
351 | }
352 | },
353 | }),
354 | );
355 |
356 | var fbm = fbm3d();
357 |
358 | var perturbVertex = (offset, scale = 1) => {
359 | return vertex =>
360 | vec3_multiplyScalar(
361 | vertex,
362 | 1 +
363 | scale * fbm(vertex.x + offset, vertex.y + offset, vertex.z + offset),
364 | );
365 | };
366 |
367 | var createRockGeometry = (
368 | width,
369 | depth,
370 | topHeight,
371 | bottomHeight,
372 | topScale = 1,
373 | bottomScale = 1,
374 | topTransforms = [],
375 | bottomTransforms = [],
376 | fbmScale = 1,
377 | ) => {
378 | var geom = geom_merge(
379 | compose(
380 | align('ny'),
381 | $scale({ py: [topScale, 1, topScale] }),
382 | ...topTransforms,
383 | )(boxGeom_create(width, topHeight, depth)),
384 | compose(
385 | align('py'),
386 | $scale({ ny: [bottomScale, 1, bottomScale] }),
387 | ...bottomTransforms,
388 | )(boxGeom_create(width, bottomHeight, depth)),
389 | );
390 |
391 | geom.vertices.map(perturbVertex(width * Math.random(), fbmScale));
392 | return geom;
393 | };
394 |
395 | var createPlatformGeometry = (...args) => {
396 | return createRockGeometry(
397 | ...args,
398 | 0.6,
399 | 0.2,
400 | [],
401 | [defaultColors([0.8, 0.8, 0.8]), colors({ ny: [0.3, 0.2, 0.2] })],
402 | );
403 | };
404 |
405 | var towerColor = [0.6, 0.6, 0.6];
406 |
407 | var topTowerTransforms = [defaultColors(towerColor)];
408 | var bottomTowerTransforms = [
409 | defaultColors(towerColor),
410 | colors({ ny: [0.3, 0.2, 0.2] }),
411 | ];
412 |
413 | var createRockMesh = (geom, material, position) => {
414 | var mesh = physics_add(mesh_create(geom, material), BODY_STATIC);
415 | vec3_fromArray(mesh.position, position);
416 | object3d_add(map, mesh);
417 | };
418 |
419 | // Platforms
420 | [
421 | [createPlatformGeometry(128, 128, 16, 32), [-64, 16, -320]],
422 | [createPlatformGeometry(128, 128, 16, 32), [-32, 48, -512]],
423 | [createPlatformGeometry(128, 128, 16, 32), [0, 80, -704]],
424 | [createPlatformGeometry(128, 128, 16, 32), [0, 120, -920]],
425 | [createPlatformGeometry(192, 400, 16, 32), [-256, -72, -1536]],
426 | [createPlatformGeometry(512, 192, 24, 32), [360, 16, -2700]],
427 | [createPlatformGeometry(128, 128, 12, 32), [960, -64, -2800]],
428 | [createPlatformGeometry(128, 128, 12, 24), [1560, -24, -2950]],
429 | // Long rest path
430 | [createPlatformGeometry(192, 1024, 40, 128), [1920, 116, -2160]],
431 | // Play area
432 | [createPlatformGeometry(768, 512, 32, 128), [2496, 64, -1600]],
433 | // Tower climb resting
434 | [createPlatformGeometry(256, 256, 16, 32), [4672, 160, -1984]],
435 | // Drop
436 | [createPlatformGeometry(128, 128, 16, 32), [4864, 128, -2016]],
437 | [createPlatformGeometry(128, 128, 16, 32), [5056, 96, -2048]],
438 | [createPlatformGeometry(128, 128, 16, 32), [5248, 64, -2080]],
439 | [createPlatformGeometry(128, 128, 16, 32), [5448, 24, -2112]],
440 | // Run up
441 | [createPlatformGeometry(256, 1024, 24, 32), [5704, -128, -2784]],
442 | // Run up land
443 | [createPlatformGeometry(512, 512, 24, 32), [5876, 128, -7200]],
444 | // // Ramp rest
445 | [createPlatformGeometry(360, 360, 16, 32), [6912, 128, -11456]],
446 | ].map(([geom, position]) => {
447 | var material = material_create();
448 | vec3_set(material.color, 0.5, randFloat(0.7, 0.8), 0.5);
449 | createRockMesh(geom, material, position);
450 | });
451 |
452 | var createTowerGeometry = (...args) =>
453 | createRockGeometry(
454 | ...args,
455 | randFloat(0.8, 0.9),
456 | randFloat(0.4, 0.6),
457 | topTowerTransforms,
458 | bottomTowerTransforms,
459 | 0.2,
460 | );
461 |
462 | // Towers and rocks
463 | [
464 | [createTowerGeometry(240, 480, 40, 480), [0, -24, 0]],
465 | // Towers
466 | [createTowerGeometry(384, 384, 384, 640), [-480, 170, -1536]],
467 | [createTowerGeometry(384, 384, 384, 680), [480, 130, -2048]],
468 | [createTowerGeometry(384, 384, 384, 480), [-480, 130, -2560]],
469 | [createTowerGeometry(768, 320, 1536, 512), [384, 256, -3072]],
470 | [createTowerGeometry(256, 300, 240, 128), [1440, 320, -2900]],
471 | [createTowerGeometry(256, 300, 240, 128), [1440, 320, -2960]],
472 | [createTowerGeometry(256, 300, 240, 320), [1920, 320, -3200]],
473 | [createTowerGeometry(240, 270, 240, 360), [2240, 272, -2800]],
474 | [createTowerGeometry(256, 192, 240, 360), [2240, 128, -2240]],
475 | // After play area tower climb
476 | [createTowerGeometry(256, 192, 240, 360), [3072, 0, -1440]],
477 | [createTowerGeometry(256, 256, 256, 360), [3584, 128, -1952]],
478 | [createTowerGeometry(256, 256, 320, 240), [4096, 256, -1664]],
479 | [createTowerGeometry(160, 160, 208, 400), [4672, 448, -1984]],
480 | // Run up boost
481 | [createTowerGeometry(480, 480, 64, 128), [5768, 1600, -4032]],
482 | // Run up sides
483 | [createTowerGeometry(128, 256, 320, 480), [5448, 1472, -4928]],
484 | [createTowerGeometry(128, 384, 320, 240), [6088, 1536, -5888]],
485 | // Run up block
486 | [createTowerGeometry(640, 128, 800, 512), [5768, 768, -6592]],
487 | // Ramp
488 | [createTowerGeometry(256, 256, 640, 512), [6400, 0, -10496]],
489 | [createTowerGeometry(224, 224, 768, 576), [6912, 512, -11456]],
490 | ].map(([geom, position]) => {
491 | var material = material_create();
492 | vec3_set(material.color, 0.5, 0.5, 0.5);
493 | createRockMesh(geom, material, position);
494 | });
495 |
496 | // Floors and walls
497 | [
498 | // Play area boxes
499 | [[256, 64, 64], [2508, 96, -1500]],
500 | [[64, 56, 320], [2240, 96, -1600]],
501 | // Walkway
502 | [[128, 128, 2048], [5920, -320, -8736]],
503 | // Atrium
504 | [[1024, 8, 1024], [8192, -260, -11800]],
505 | // Walls
506 | [[1024, 128, 32], [8192, -192, -12312]],
507 | [[1024, 128, 32], [8192, -192, -11288]],
508 | [[32, 128, 1024], [7680, -192, -11800]],
509 | [[32, 128, 1024], [8704, -192, -11800]],
510 | // Block thing
511 | [[256, 64, 256], [7936, -256, -12088]],
512 | ].map(([dimensions, position]) => {
513 | var material = material_create();
514 | var mesh = physics_add(
515 | mesh_create(boxGeom_create(...dimensions), material),
516 | BODY_STATIC,
517 | );
518 | vec3_set(material.color, 0.5, 0.5, 0.5);
519 | vec3_fromArray(mesh.position, position);
520 | object3d_add(map, mesh);
521 | });
522 |
523 | return {
524 | ambient,
525 | directional: directionalLights,
526 | };
527 | };
528 |
--------------------------------------------------------------------------------
/src/mat4.js:
--------------------------------------------------------------------------------
1 | import {
2 | vec3_create,
3 | vec3_crossVectors,
4 | vec3_length,
5 | vec3_normalize,
6 | vec3_subVectors,
7 | } from './vec3.js';
8 |
9 | export var mat4_create = () => {
10 | // prettier-ignore
11 | return new Float32Array([
12 | 1, 0, 0, 0,
13 | 0, 1, 0, 0,
14 | 0, 0, 1, 0,
15 | 0, 0, 0, 1,
16 | ]);
17 | };
18 |
19 | export var mat4_identity = m => {
20 | // prettier-ignore
21 | m.set([
22 | 1, 0, 0, 0,
23 | 0, 1, 0, 0,
24 | 0, 0, 1, 0,
25 | 0, 0, 0, 1,
26 | ]);
27 |
28 | return m;
29 | };
30 |
31 | export var mat4_copy = (a, b) => {
32 | a.set(b);
33 | return a;
34 | };
35 |
36 | export var mat4_makeRotationFromQuaternion = (() => {
37 | var zero = vec3_create();
38 | var one = vec3_create(1, 1, 1);
39 |
40 | return (m, q) => {
41 | return mat4_compose(m, zero, q, one);
42 | };
43 | })();
44 |
45 | export var mat4_lookAt = (() => {
46 | var x = vec3_create();
47 | var y = vec3_create();
48 | var z = vec3_create();
49 |
50 | return (m, eye, target, up) => {
51 | vec3_normalize(vec3_subVectors(z, eye, target));
52 |
53 | if (!vec3_length(z)) {
54 | z.z = 1;
55 | }
56 |
57 | vec3_normalize(vec3_crossVectors(x, up, z));
58 |
59 | if (!vec3_length(x)) {
60 | // up and z are parallel
61 | if (Math.abs(up.z) === 1) {
62 | z.x += 0.0001;
63 | } else {
64 | z.z += 0.0001;
65 | }
66 |
67 | vec3_normalize(vec3_crossVectors(x, up, z));
68 | }
69 |
70 | vec3_crossVectors(y, z, x);
71 |
72 | m[0] = x.x;
73 | m[4] = y.x;
74 | m[8] = z.x;
75 |
76 | m[1] = x.y;
77 | m[5] = y.y;
78 | m[9] = z.y;
79 |
80 | m[2] = x.z;
81 | m[6] = y.z;
82 | m[10] = z.z;
83 |
84 | return m;
85 | };
86 | })();
87 |
88 | export var mat4_multiplyMatrices = (m, a, b) => {
89 | var a11 = a[0],
90 | a12 = a[4],
91 | a13 = a[8],
92 | a14 = a[12];
93 | var a21 = a[1],
94 | a22 = a[5],
95 | a23 = a[9],
96 | a24 = a[13];
97 | var a31 = a[2],
98 | a32 = a[6],
99 | a33 = a[10],
100 | a34 = a[14];
101 | var a41 = a[3],
102 | a42 = a[7],
103 | a43 = a[11],
104 | a44 = a[15];
105 |
106 | var b11 = b[0],
107 | b12 = b[4],
108 | b13 = b[8],
109 | b14 = b[12];
110 | var b21 = b[1],
111 | b22 = b[5],
112 | b23 = b[9],
113 | b24 = b[13];
114 | var b31 = b[2],
115 | b32 = b[6],
116 | b33 = b[10],
117 | b34 = b[14];
118 | var b41 = b[3],
119 | b42 = b[7],
120 | b43 = b[11],
121 | b44 = b[15];
122 |
123 | m[0] = a11 * b11 + a12 * b21 + a13 * b31 + a14 * b41;
124 | m[4] = a11 * b12 + a12 * b22 + a13 * b32 + a14 * b42;
125 | m[8] = a11 * b13 + a12 * b23 + a13 * b33 + a14 * b43;
126 | m[12] = a11 * b14 + a12 * b24 + a13 * b34 + a14 * b44;
127 |
128 | m[1] = a21 * b11 + a22 * b21 + a23 * b31 + a24 * b41;
129 | m[5] = a21 * b12 + a22 * b22 + a23 * b32 + a24 * b42;
130 | m[9] = a21 * b13 + a22 * b23 + a23 * b33 + a24 * b43;
131 | m[13] = a21 * b14 + a22 * b24 + a23 * b34 + a24 * b44;
132 |
133 | m[2] = a31 * b11 + a32 * b21 + a33 * b31 + a34 * b41;
134 | m[6] = a31 * b12 + a32 * b22 + a33 * b32 + a34 * b42;
135 | m[10] = a31 * b13 + a32 * b23 + a33 * b33 + a34 * b43;
136 | m[14] = a31 * b14 + a32 * b24 + a33 * b34 + a34 * b44;
137 |
138 | m[3] = a41 * b11 + a42 * b21 + a43 * b31 + a44 * b41;
139 | m[7] = a41 * b12 + a42 * b22 + a43 * b32 + a44 * b42;
140 | m[11] = a41 * b13 + a42 * b23 + a43 * b33 + a44 * b43;
141 | m[15] = a41 * b14 + a42 * b24 + a43 * b34 + a44 * b44;
142 |
143 | return m;
144 | };
145 |
146 | export var mat4_setPosition = (m, v) => {
147 | m[12] = v.x;
148 | m[13] = v.y;
149 | m[14] = v.z;
150 |
151 | return m;
152 | };
153 |
154 | export var mat4_getInverse = (a, b) => {
155 | // based on http://www.euclideanspace.com/maths/algebra/matrix/functions/inverse/fourD/index.htm
156 | var n11 = b[0],
157 | n21 = b[1],
158 | n31 = b[2],
159 | n41 = b[3];
160 | var n12 = b[4],
161 | n22 = b[5],
162 | n32 = b[6],
163 | n42 = b[7];
164 | var n13 = b[8],
165 | n23 = b[9],
166 | n33 = b[10],
167 | n43 = b[11];
168 | var n14 = b[12],
169 | n24 = b[13],
170 | n34 = b[14],
171 | n44 = b[15];
172 |
173 | var t11 =
174 | n23 * n34 * n42 -
175 | n24 * n33 * n42 +
176 | n24 * n32 * n43 -
177 | n22 * n34 * n43 -
178 | n23 * n32 * n44 +
179 | n22 * n33 * n44;
180 | var t12 =
181 | n14 * n33 * n42 -
182 | n13 * n34 * n42 -
183 | n14 * n32 * n43 +
184 | n12 * n34 * n43 +
185 | n13 * n32 * n44 -
186 | n12 * n33 * n44;
187 | var t13 =
188 | n13 * n24 * n42 -
189 | n14 * n23 * n42 +
190 | n14 * n22 * n43 -
191 | n12 * n24 * n43 -
192 | n13 * n22 * n44 +
193 | n12 * n23 * n44;
194 | var t14 =
195 | n14 * n23 * n32 -
196 | n13 * n24 * n32 -
197 | n14 * n22 * n33 +
198 | n12 * n24 * n33 +
199 | n13 * n22 * n34 -
200 | n12 * n23 * n34;
201 |
202 | var det = n11 * t11 + n21 * t12 + n31 * t13 + n41 * t14;
203 |
204 | if (det === 0) {
205 | return mat4_identity(a);
206 | }
207 |
208 | var detInv = 1 / det;
209 |
210 | a[0] = t11 * detInv;
211 | a[1] =
212 | (n24 * n33 * n41 -
213 | n23 * n34 * n41 -
214 | n24 * n31 * n43 +
215 | n21 * n34 * n43 +
216 | n23 * n31 * n44 -
217 | n21 * n33 * n44) *
218 | detInv;
219 | a[2] =
220 | (n22 * n34 * n41 -
221 | n24 * n32 * n41 +
222 | n24 * n31 * n42 -
223 | n21 * n34 * n42 -
224 | n22 * n31 * n44 +
225 | n21 * n32 * n44) *
226 | detInv;
227 | a[3] =
228 | (n23 * n32 * n41 -
229 | n22 * n33 * n41 -
230 | n23 * n31 * n42 +
231 | n21 * n33 * n42 +
232 | n22 * n31 * n43 -
233 | n21 * n32 * n43) *
234 | detInv;
235 |
236 | a[4] = t12 * detInv;
237 | a[5] =
238 | (n13 * n34 * n41 -
239 | n14 * n33 * n41 +
240 | n14 * n31 * n43 -
241 | n11 * n34 * n43 -
242 | n13 * n31 * n44 +
243 | n11 * n33 * n44) *
244 | detInv;
245 | a[6] =
246 | (n14 * n32 * n41 -
247 | n12 * n34 * n41 -
248 | n14 * n31 * n42 +
249 | n11 * n34 * n42 +
250 | n12 * n31 * n44 -
251 | n11 * n32 * n44) *
252 | detInv;
253 | a[7] =
254 | (n12 * n33 * n41 -
255 | n13 * n32 * n41 +
256 | n13 * n31 * n42 -
257 | n11 * n33 * n42 -
258 | n12 * n31 * n43 +
259 | n11 * n32 * n43) *
260 | detInv;
261 |
262 | a[8] = t13 * detInv;
263 | a[9] =
264 | (n14 * n23 * n41 -
265 | n13 * n24 * n41 -
266 | n14 * n21 * n43 +
267 | n11 * n24 * n43 +
268 | n13 * n21 * n44 -
269 | n11 * n23 * n44) *
270 | detInv;
271 | a[10] =
272 | (n12 * n24 * n41 -
273 | n14 * n22 * n41 +
274 | n14 * n21 * n42 -
275 | n11 * n24 * n42 -
276 | n12 * n21 * n44 +
277 | n11 * n22 * n44) *
278 | detInv;
279 | a[11] =
280 | (n13 * n22 * n41 -
281 | n12 * n23 * n41 -
282 | n13 * n21 * n42 +
283 | n11 * n23 * n42 +
284 | n12 * n21 * n43 -
285 | n11 * n22 * n43) *
286 | detInv;
287 |
288 | a[12] = t14 * detInv;
289 | a[13] =
290 | (n13 * n24 * n31 -
291 | n14 * n23 * n31 +
292 | n14 * n21 * n33 -
293 | n11 * n24 * n33 -
294 | n13 * n21 * n34 +
295 | n11 * n23 * n34) *
296 | detInv;
297 | a[14] =
298 | (n14 * n22 * n31 -
299 | n12 * n24 * n31 -
300 | n14 * n21 * n32 +
301 | n11 * n24 * n32 +
302 | n12 * n21 * n34 -
303 | n11 * n22 * n34) *
304 | detInv;
305 | a[15] =
306 | (n12 * n23 * n31 -
307 | n13 * n22 * n31 +
308 | n13 * n21 * n32 -
309 | n11 * n23 * n32 -
310 | n12 * n21 * n33 +
311 | n11 * n22 * n33) *
312 | detInv;
313 |
314 | return a;
315 | };
316 |
317 | export var mat4_scale = (m, v) => {
318 | var { x, y, z } = v;
319 |
320 | m[0] *= x;
321 | m[4] *= y;
322 | m[8] *= z;
323 |
324 | m[1] *= x;
325 | m[5] *= y;
326 | m[9] *= z;
327 |
328 | m[2] *= x;
329 | m[6] *= y;
330 | m[10] *= z;
331 |
332 | m[3] *= x;
333 | m[7] *= y;
334 | m[11] *= z;
335 |
336 | return m;
337 | };
338 |
339 | export var mat4_compose = (m, position, quaternion, scale) => {
340 | var { x, y, z, w } = quaternion;
341 | var x2 = x + x,
342 | y2 = y + y,
343 | z2 = z + z;
344 | var xx = x * x2,
345 | xy = x * y2,
346 | xz = x * z2;
347 | var yy = y * y2,
348 | yz = y * z2,
349 | zz = z * z2;
350 | var wx = w * x2,
351 | wy = w * y2,
352 | wz = w * z2;
353 |
354 | var sx = scale.x,
355 | sy = scale.y,
356 | sz = scale.z;
357 |
358 | m[0] = (1 - (yy + zz)) * sx;
359 | m[1] = (xy + wz) * sx;
360 | m[2] = (xz - wy) * sx;
361 | m[3] = 0;
362 |
363 | m[4] = (xy - wz) * sy;
364 | m[5] = (1 - (xx + zz)) * sy;
365 | m[6] = (yz + wx) * sy;
366 | m[7] = 0;
367 |
368 | m[8] = (xz + wy) * sz;
369 | m[9] = (yz - wx) * sz;
370 | m[10] = (1 - (xx + yy)) * sz;
371 | m[11] = 0;
372 |
373 | m[12] = position.x;
374 | m[13] = position.y;
375 | m[14] = position.z;
376 | m[15] = 1;
377 |
378 | return m;
379 | };
380 |
--------------------------------------------------------------------------------
/src/material.js:
--------------------------------------------------------------------------------
1 | import { vec3_create } from './vec3.js';
2 |
3 | // MeshPhongMaterial.
4 | export var material_create = () => {
5 | return {
6 | color: vec3_create(1, 1, 1),
7 | // 0x111111
8 | specular: vec3_create(1 / 15, 1 / 15, 1 / 15),
9 | shininess: 30,
10 | emissive: vec3_create(),
11 | };
12 | };
13 |
--------------------------------------------------------------------------------
/src/math.js:
--------------------------------------------------------------------------------
1 | export var clamp = (value, min, max) => {
2 | return Math.min(Math.max(value, min), max);
3 | };
4 |
5 | export var lerp = (x, y, t) => {
6 | return (1 - t) * x + t * y;
7 | };
8 |
9 | export var mapLinear = (x, a1, a2, b1, b2) => {
10 | return b1 + ((x - a1) * (b2 - b1)) / (a2 - a1);
11 | };
12 |
13 | export var randFloat = (low, high) => {
14 | return low + Math.random() * (high - low);
15 | };
16 |
17 | export var randFloatSpread = range => {
18 | return range * (0.5 - Math.random());
19 | };
20 |
--------------------------------------------------------------------------------
/src/mesh.js:
--------------------------------------------------------------------------------
1 | import { object3d_create } from './object3d.js';
2 |
3 | export var mesh_create = (geometry, material) => {
4 | return {
5 | ...object3d_create(),
6 | geometry,
7 | material,
8 | };
9 | };
10 |
--------------------------------------------------------------------------------
/src/noise.js:
--------------------------------------------------------------------------------
1 | // From https://github.com/jwagner/simplex-noise.js
2 | var F3 = 1.0 / 3.0;
3 | var G3 = 1.0 / 6.0;
4 |
5 | var buildPermutationTable = random => {
6 | var i;
7 | var p = new Uint8Array(256);
8 | for (i = 0; i < 256; i++) {
9 | p[i] = i;
10 | }
11 | for (i = 0; i < 255; i++) {
12 | var r = i + ~~(random() * (256 - i));
13 | var aux = p[i];
14 | p[i] = p[r];
15 | p[r] = aux;
16 | }
17 | return p;
18 | };
19 |
20 | var p = buildPermutationTable(Math.random);
21 | var perm = new Uint8Array(512);
22 | var permMod12 = new Uint8Array(512);
23 | for (var i = 0; i < 512; i++) {
24 | perm[i] = p[i & 255];
25 | permMod12[i] = perm[i] % 12;
26 | }
27 |
28 | // prettier-ignore
29 | var grad3 = new Float32Array([
30 | 1, 1, 0,
31 | -1, 1, 0,
32 | 1, -1, 0,
33 |
34 | -1, -1, 0,
35 | 1, 0, 1,
36 | -1, 0, 1,
37 |
38 | 1, 0, -1,
39 | -1, 0, -1,
40 | 0, 1, 1,
41 |
42 | 0, -1, 1,
43 | 0, 1, -1,
44 | 0, -1, -1
45 | ]);
46 |
47 | export var noise3d = (xin, yin, zin) => {
48 | var n0, n1, n2, n3; // Noise contributions from the four corners
49 | // Skew the input space to determine which simplex cell we're in
50 | var s = (xin + yin + zin) * F3; // Very nice and simple skew factor for 3D
51 | var i = Math.floor(xin + s);
52 | var j = Math.floor(yin + s);
53 | var k = Math.floor(zin + s);
54 | var t = (i + j + k) * G3;
55 | var X0 = i - t; // Unskew the cell origin back to (x,y,z) space
56 | var Y0 = j - t;
57 | var Z0 = k - t;
58 | var x0 = xin - X0; // The x,y,z distances from the cell origin
59 | var y0 = yin - Y0;
60 | var z0 = zin - Z0;
61 | // For the 3D case, the simplex shape is a slightly irregular tetrahedron.
62 | // Determine which simplex we are in.
63 | var i1, j1, k1; // Offsets for second corner of simplex in (i,j,k) coords
64 | var i2, j2, k2; // Offsets for third corner of simplex in (i,j,k) coords
65 | if (x0 >= y0) {
66 | if (y0 >= z0) {
67 | i1 = 1;
68 | j1 = 0;
69 | k1 = 0;
70 | i2 = 1;
71 | j2 = 1;
72 | k2 = 0;
73 | } // X Y Z order
74 | else if (x0 >= z0) {
75 | i1 = 1;
76 | j1 = 0;
77 | k1 = 0;
78 | i2 = 1;
79 | j2 = 0;
80 | k2 = 1;
81 | } // X Z Y order
82 | else {
83 | i1 = 0;
84 | j1 = 0;
85 | k1 = 1;
86 | i2 = 1;
87 | j2 = 0;
88 | k2 = 1;
89 | } // Z X Y order
90 | } else {
91 | // x0 {
27 | return {
28 | parent: undefined,
29 | children: [],
30 | components: [],
31 | position: vec3_create(),
32 | quaternion: quat_create(),
33 | scale: vec3_create(1, 1, 1),
34 | matrix: mat4_create(),
35 | matrixWorld: mat4_create(),
36 | modelViewMatrix: mat4_create(),
37 | visible: true,
38 | };
39 | };
40 |
41 | export var object3d_lookAt = (() => {
42 | var m1 = mat4_create();
43 |
44 | return (object, vector) => {
45 | mat4_lookAt(m1, vector, object.position, vec3_Y);
46 | quat_setFromRotationMatrix(object.quaternion, m1);
47 | };
48 | })();
49 |
50 | export var object3d_add = (parent, child) => {
51 | child.parent = parent;
52 | parent.children.push(child);
53 | return parent;
54 | };
55 |
56 | export var object3d_remove = (parent, child) => {
57 | var index = parent.children.indexOf(child);
58 | if (index >= 0) {
59 | parent.children.splice(index, 1);
60 | }
61 | };
62 |
63 | export var object3d_rotateOnAxis = (() => {
64 | var q1 = quat_create();
65 |
66 | return (obj, axis, angle) => {
67 | quat_setFromAxisAngle(q1, axis, angle);
68 | quat_multiply(obj.quaternion, q1);
69 | return obj;
70 | };
71 | })();
72 |
73 | export var object3d_rotateX = (obj, angle) => {
74 | return object3d_rotateOnAxis(obj, vec3_X, angle);
75 | };
76 |
77 | export var object3d_rotateY = (obj, angle) => {
78 | return object3d_rotateOnAxis(obj, vec3_Y, angle);
79 | };
80 |
81 | export var object3d_rotateZ = (obj, angle) => {
82 | return object3d_rotateOnAxis(obj, vec3_Z, angle);
83 | };
84 |
85 | export var object3d_translateOnAxis = (() => {
86 | var v1 = vec3_create();
87 |
88 | return (obj, axis, distance) => {
89 | vec3_applyQuaternion(Object.assign(v1, axis), obj.quaternion);
90 | vec3_add(obj.position, vec3_multiplyScalar(v1, distance));
91 | return obj;
92 | };
93 | })();
94 |
95 | export var object3d_translateX = (obj, distance) => {
96 | return object3d_translateOnAxis(obj, vec3_X, distance);
97 | };
98 |
99 | export var object3d_translateY = (obj, distance) => {
100 | return object3d_translateOnAxis(obj, vec3_Y, distance);
101 | };
102 |
103 | export var object3d_translateZ = (obj, distance) => {
104 | return object3d_translateOnAxis(obj, vec3_Z, distance);
105 | };
106 |
107 | export var object3d_traverse = (obj, callback) => {
108 | callback(obj);
109 | obj.children.map(child => object3d_traverse(child, callback));
110 | };
111 |
112 | export var object3d_updateMatrix = obj => {
113 | mat4_compose(obj.matrix, obj.position, obj.quaternion, obj.scale);
114 | };
115 |
116 | export var object3d_updateMatrixWorld = obj => {
117 | object3d_updateMatrix(obj);
118 |
119 | if (!obj.parent) {
120 | mat4_copy(obj.matrixWorld, obj.matrix);
121 | } else {
122 | mat4_multiplyMatrices(obj.matrixWorld, obj.parent.matrixWorld, obj.matrix);
123 | }
124 |
125 | obj.children.map(object3d_updateMatrixWorld);
126 | };
127 |
--------------------------------------------------------------------------------
/src/physics.js:
--------------------------------------------------------------------------------
1 | import {
2 | box3_create,
3 | box3_copy,
4 | box3_overlapsBox,
5 | box3_setFromObject,
6 | box3_translate,
7 | } from './box3.js';
8 | import {
9 | component_create,
10 | entity_add,
11 | entity_filter,
12 | entity_find,
13 | } from './entity.js';
14 | import { object3d_traverse } from './object3d.js';
15 | import {
16 | vec3_create,
17 | vec3_add,
18 | vec3_addScaledVector,
19 | vec3_multiplyScalar,
20 | vec3_normalize,
21 | vec3_set,
22 | vec3_sub,
23 | pm_clipVelocity,
24 | OVERCLIP,
25 | } from './vec3.js';
26 |
27 | export var BODY_STATIC = 1;
28 | export var BODY_DYNAMIC = 2;
29 |
30 | export var SHAPE_BOX = 1;
31 |
32 | export var physics_create = (entity, physics) => {
33 | return component_create({
34 | physics,
35 | shape: SHAPE_BOX,
36 | boundingBox: box3_setFromObject(box3_create(), entity),
37 | velocity: vec3_create(),
38 | update(component, dt) {
39 | vec3_addScaledVector(component.parent.position, component.velocity, dt);
40 | },
41 | });
42 | };
43 |
44 | export var physics_add = (entity, physics) => {
45 | return entity_add(entity, physics_create(entity, physics));
46 | };
47 |
48 | export var get_physics_component = entity => {
49 | return entity_find(entity, is_physics_component);
50 | };
51 |
52 | export var is_physics_component = object => object.physics;
53 |
54 | export var physics_bodies = object => {
55 | var bodies = [];
56 |
57 | object3d_traverse(object, node => {
58 | bodies.push(...entity_filter(node, is_physics_component));
59 | });
60 |
61 | return bodies;
62 | };
63 |
64 | var narrowPhase = (() => {
65 | var penetration = vec3_create();
66 |
67 | return {
68 | [SHAPE_BOX | SHAPE_BOX](bodyA, bodyB, boxA, boxB) {
69 | // Determine overlap.
70 | // d0 is negative side or 'left' side.
71 | // d1 is positive or 'right' side.
72 | var d0x = boxB.max.x - boxA.min.x;
73 | var d1x = boxA.max.x - boxB.min.x;
74 |
75 | var d0y = boxB.max.y - boxA.min.y;
76 | var d1y = boxA.max.y - boxB.min.y;
77 |
78 | var d0z = boxB.max.z - boxA.min.z;
79 | var d1z = boxA.max.z - boxB.min.z;
80 |
81 | // Only overlapping on an axis if both ranges intersect.
82 | var dx = 0;
83 | if (d0x > 0 && d1x > 0) {
84 | dx = d0x < d1x ? d0x : -d1x;
85 | }
86 |
87 | var dy = 0;
88 | if (d0y > 0 && d1y > 0) {
89 | dy = d0y < d1y ? d0y : -d1y;
90 | }
91 |
92 | var dz = 0;
93 | if (d0z > 0 && d1z > 0) {
94 | dz = d0z < d1z ? d0z : -d1z;
95 | }
96 |
97 | // Determine minimum axis of separation.
98 | var adx = Math.abs(dx);
99 | var ady = Math.abs(dy);
100 | var adz = Math.abs(dz);
101 |
102 | if (adx < ady && adx < adz) {
103 | vec3_set(penetration, dx, 0, 0);
104 | } else if (ady < adz) {
105 | vec3_set(penetration, 0, dy, 0);
106 | } else {
107 | vec3_set(penetration, 0, 0, dz);
108 | }
109 |
110 | var objectA = bodyA.parent;
111 | var objectB = bodyB.parent;
112 |
113 | if (bodyA.physics === BODY_STATIC) {
114 | vec3_addScaledVector(objectB.position, penetration, -OVERCLIP);
115 | pm_clipVelocity(bodyB.velocity, vec3_normalize(penetration), OVERCLIP);
116 | } else if (bodyB.physics === BODY_STATIC) {
117 | vec3_addScaledVector(objectA.position, penetration, OVERCLIP);
118 | pm_clipVelocity(bodyA.velocity, vec3_normalize(penetration), OVERCLIP);
119 | } else {
120 | vec3_multiplyScalar(penetration, 0.5);
121 | vec3_add(objectA.position, penetration);
122 | vec3_sub(objectB.position, penetration);
123 | }
124 | },
125 | };
126 | })();
127 |
128 | export var physics_update = (() => {
129 | var boxA = box3_create();
130 | var boxB = box3_create();
131 |
132 | return bodies => {
133 | var contacts = [];
134 |
135 | for (var i = 0; i < bodies.length; i++) {
136 | var bodyA = bodies[i];
137 |
138 | for (var j = i + 1; j < bodies.length; j++) {
139 | var bodyB = bodies[j];
140 |
141 | // Immovable objects.
142 | if (bodyA.physics === BODY_STATIC && bodyB.physics === BODY_STATIC) {
143 | return;
144 | }
145 |
146 | // Two dynamic bodies, or one static and one dynamic body.
147 | var objectA = bodyA.parent;
148 | var objectB = bodyB.parent;
149 |
150 | box3_translate(box3_copy(boxA, bodyA.boundingBox), objectA.position);
151 | box3_translate(box3_copy(boxB, bodyB.boundingBox), objectB.position);
152 |
153 | if (box3_overlapsBox(boxA, boxB)) {
154 | var contact = narrowPhase[bodyA.shape | bodyB.shape](
155 | bodyA,
156 | bodyB,
157 | boxA,
158 | boxB,
159 | );
160 | if (contact) {
161 | contacts.push(contact);
162 | }
163 | }
164 | }
165 | }
166 |
167 | return contacts;
168 | };
169 | })();
170 |
--------------------------------------------------------------------------------
/src/player.js:
--------------------------------------------------------------------------------
1 | import {
2 | box3_create,
3 | box3_copy,
4 | box3_overlapsBox,
5 | box3_translate,
6 | } from './box3.js';
7 | import { physics_bodies } from './physics.js';
8 | import {
9 | vec3_create,
10 | vec3_add,
11 | vec3_addScaledVector,
12 | vec3_dot,
13 | vec3_length,
14 | vec3_multiplyScalar,
15 | vec3_normalize,
16 | vec3_setScalar,
17 | vec3_subVectors,
18 | pm_clipVelocity,
19 | OVERCLIP,
20 | } from './vec3.js';
21 |
22 | // movement flags
23 | var PMF_JUMP_HELD = 2;
24 | export var PMF_GRAPPLE_FLY = 1024;
25 | export var PMF_GRAPPLE_PULL = 2048; // pull towards grapple location
26 | export var PMF_GRAPPLE = PMF_GRAPPLE_FLY | PMF_GRAPPLE_PULL;
27 | export var GRAPPLE_SPEED = 1024;
28 |
29 | var JUMP_VELOCITY = 270;
30 |
31 | // movement parameters
32 | var PM_STOPSPEED = 100;
33 |
34 | var PM_ACCELERATE = 10;
35 | var PM_AIRACCELERATE = 1;
36 |
37 | var PM_FRICTION = 6;
38 |
39 | var g_speed = 320;
40 | var g_gravity = 800;
41 |
42 | export var player_create = (object, body) => {
43 | return {
44 | object,
45 | body,
46 |
47 | scene: undefined,
48 |
49 | // player input
50 | command: {
51 | forward: 0,
52 | right: 0,
53 | up: 0,
54 | hook: 0,
55 | },
56 |
57 | // run-time variables
58 | dt: 0,
59 | gravity: g_gravity,
60 | speed: g_speed,
61 | viewForward: vec3_create(),
62 | viewRight: vec3_create(),
63 | grapplePoint: vec3_create(), // location of grapple to pull towards if PMF_GRAPPLE_PULL
64 |
65 | // walk movement
66 | movementFlags: 0,
67 | walking: false,
68 | groundPlane: false,
69 | groundTrace: {
70 | normal: vec3_create(0, 1, 0),
71 | },
72 | };
73 | };
74 |
75 | export var player_update = player => {
76 | if (player.command.up < 10) {
77 | // not holding jump
78 | player.movementFlags &= ~PMF_JUMP_HELD;
79 | }
80 |
81 | player_checkGround(player);
82 |
83 | if (player.movementFlags & PMF_GRAPPLE_PULL) {
84 | player_grappleMove(player);
85 | // We can wiggle a bit
86 | player_airMove(player);
87 | } else if (player.walking) {
88 | // walking on ground
89 | player_walkMove(player);
90 | } else {
91 | // airborne
92 | player_airMove(player);
93 | }
94 |
95 | player_checkGround(player);
96 | };
97 |
98 | var player_checkJump = player => {
99 | if (player.command.up < 10) {
100 | // not holding jump
101 | return false;
102 | }
103 |
104 | if (player.movementFlags & PMF_JUMP_HELD) {
105 | player.command.up = 0;
106 | return false;
107 | }
108 |
109 | player.groundPlane = false;
110 | player.walking = false;
111 | player.movementFlags |= PMF_JUMP_HELD;
112 |
113 | player.body.velocity.y = JUMP_VELOCITY;
114 |
115 | return true;
116 | };
117 |
118 | var player_walkMove = (() => {
119 | var wishvel = vec3_create();
120 | var wishdir = vec3_create();
121 |
122 | return player => {
123 | if (player_checkJump(player)) {
124 | player_airMove(player);
125 | return;
126 | }
127 |
128 | player_friction(player);
129 |
130 | var fmove = player.command.forward;
131 | var smove = player.command.right;
132 |
133 | var scale = player_cmdScale(player);
134 |
135 | // project moves down to flat plane
136 | player.viewForward.y = 0;
137 | player.viewRight.y = 0;
138 |
139 | // project the forward and right directions onto the ground plane
140 | pm_clipVelocity(player.viewForward, player.groundTrace.normal, OVERCLIP);
141 | pm_clipVelocity(player.viewRight, player.groundTrace.normal, OVERCLIP);
142 | //
143 | vec3_normalize(player.viewForward);
144 | vec3_normalize(player.viewRight);
145 |
146 | vec3_setScalar(wishvel, 0);
147 | vec3_addScaledVector(wishvel, player.viewForward, fmove);
148 | vec3_addScaledVector(wishvel, player.viewRight, smove);
149 |
150 | Object.assign(wishdir, wishvel);
151 | var wishspeed = vec3_length(wishdir);
152 | vec3_normalize(wishdir);
153 | wishspeed *= scale;
154 |
155 | player_accelerate(player, wishdir, wishspeed, PM_ACCELERATE);
156 |
157 | pm_clipVelocity(player.body.velocity, player.groundTrace.normal, OVERCLIP);
158 |
159 | // don't do anything if standing still
160 | if (!player.body.velocity.x && !player.body.velocity.z) {
161 | return;
162 | }
163 | };
164 | })();
165 |
166 | var player_airMove = (() => {
167 | var wishvel = vec3_create();
168 | var wishdir = vec3_create();
169 |
170 | return player => {
171 | player_friction(player);
172 |
173 | var fmove = player.command.forward;
174 | var smove = player.command.right;
175 |
176 | var scale = player_cmdScale(player);
177 |
178 | // project moves down to flat plane
179 | player.viewForward.y = 0;
180 | player.viewRight.y = 0;
181 | vec3_normalize(player.viewForward);
182 | vec3_normalize(player.viewRight);
183 |
184 | vec3_setScalar(wishvel, 0);
185 | vec3_addScaledVector(wishvel, player.viewForward, fmove);
186 | vec3_addScaledVector(wishvel, player.viewRight, smove);
187 | wishvel.y = 0;
188 |
189 | Object.assign(wishdir, wishvel);
190 | var wishspeed = vec3_length(wishdir);
191 | vec3_normalize(wishdir);
192 | wishspeed *= scale;
193 |
194 | // not on ground, so little effect on velocity
195 | player_accelerate(player, wishdir, wishspeed, PM_AIRACCELERATE);
196 |
197 | // we may have a ground plane that is very steep, even
198 | // though we don't have a groundentity
199 | // slide along the steep plane
200 | if (player.groundPlane) {
201 | pm_clipVelocity(
202 | player.body.velocity,
203 | player.groundTrace.normal,
204 | OVERCLIP,
205 | );
206 | }
207 |
208 | player.body.velocity.y -= player.gravity * player.dt;
209 | };
210 | })();
211 |
212 | var player_grappleMove = (() => {
213 | var vel = vec3_create();
214 | var v = vec3_create();
215 |
216 | return player => {
217 | vec3_multiplyScalar(Object.assign(v, player.viewForward), -16);
218 | vec3_add(v, player.grapplePoint);
219 | vec3_subVectors(vel, v, player.object.position);
220 | var vlen = vec3_length(vel);
221 | vec3_normalize(vel);
222 |
223 | if (vlen <= 100) {
224 | vec3_multiplyScalar(vel, 10 * vlen);
225 | } else {
226 | vec3_multiplyScalar(vel, 800);
227 | }
228 |
229 | Object.assign(player.body.velocity, vel);
230 |
231 | player.groundPlane = false;
232 | };
233 | })();
234 |
235 | var player_friction = (() => {
236 | var vec = vec3_create();
237 |
238 | return player => {
239 | var vel = player.body.velocity;
240 |
241 | Object.assign(vec, vel);
242 | if (player.walking) {
243 | vec.y = 0; // ignore slope movement
244 | }
245 |
246 | var speed = vec3_length(vec);
247 | if (speed < 1) {
248 | vel.x = 0;
249 | vel.z = 0;
250 | return;
251 | }
252 |
253 | var drop = 0;
254 |
255 | // apply ground friction
256 | if (player.walking) {
257 | var control = speed < PM_STOPSPEED ? PM_STOPSPEED : speed;
258 | drop += control * PM_FRICTION * player.dt;
259 | }
260 |
261 | // scale the velocity
262 | var newspeed = speed - drop;
263 | if (newspeed < 0) {
264 | newspeed = 0;
265 | }
266 | newspeed /= speed;
267 |
268 | vec3_multiplyScalar(vel, newspeed);
269 | };
270 | })();
271 |
272 | var player_cmdScale = player => {
273 | var max = Math.abs(player.command.forward);
274 | if (Math.abs(player.command.right) > max) {
275 | max = Math.abs(player.command.right);
276 | }
277 |
278 | if (Math.abs(player.command.up) > max) {
279 | max = Math.abs(player.command.up);
280 | }
281 |
282 | if (!max) {
283 | return 0;
284 | }
285 |
286 | var total = Math.sqrt(
287 | player.command.forward ** 2 +
288 | player.command.right ** 2 +
289 | player.command.up ** 2,
290 | );
291 | var scale = (player.speed * max) / (127 * total);
292 |
293 | return scale;
294 | };
295 |
296 | var player_accelerate = (player, wishdir, wishspeed, accel) => {
297 | var currentspeed = vec3_dot(player.body.velocity, wishdir);
298 | var addspeed = wishspeed - currentspeed;
299 | if (addspeed <= 0) {
300 | return;
301 | }
302 | var accelspeed = accel * player.dt * wishspeed;
303 | if (accelspeed > addspeed) {
304 | accelspeed = addspeed;
305 | }
306 |
307 | vec3_addScaledVector(player.body.velocity, wishdir, accelspeed);
308 | };
309 |
310 | var player_checkGround = (() => {
311 | var boxA = box3_create();
312 | var boxB = box3_create();
313 |
314 | var delta = vec3_create(0, -0.25, 0);
315 |
316 | return player => {
317 | var bodies = physics_bodies(player.scene).filter(
318 | body => body !== player.body,
319 | );
320 |
321 | box3_translate(
322 | box3_copy(boxA, player.body.boundingBox),
323 | player.object.position,
324 | );
325 | box3_translate(boxA, delta);
326 |
327 | for (var i = 0; i < bodies.length; i++) {
328 | var body = bodies[i];
329 | box3_translate(box3_copy(boxB, body.boundingBox), body.parent.position);
330 |
331 | if (box3_overlapsBox(boxA, boxB)) {
332 | player.groundPlane = true;
333 | player.walking = true;
334 | return;
335 | }
336 | }
337 |
338 | // If we do not overlap anything, we are in free fall.
339 | player.groundPlane = false;
340 | player.walking = false;
341 | };
342 | })();
343 |
--------------------------------------------------------------------------------
/src/pointerLock.js:
--------------------------------------------------------------------------------
1 | export var pointerLock_create = (controls, element) => {
2 | var hasPointerLock = 'pointerLockElement' in document;
3 |
4 | if (!hasPointerLock) {
5 | controls.enabled = true;
6 | return;
7 | }
8 |
9 | var onPointerLockChange = () => {
10 | controls.enabled = element === document.pointerLockElement;
11 | };
12 |
13 | document.addEventListener('pointerlockchange', onPointerLockChange);
14 | document.addEventListener('click', () => element.requestPointerLock());
15 | };
16 |
--------------------------------------------------------------------------------
/src/quat.js:
--------------------------------------------------------------------------------
1 | import { clamp } from './math.js';
2 |
3 | export var quat_create = (x = 0, y = 0, z = 0, w = 1) => {
4 | return {
5 | x,
6 | y,
7 | z,
8 | w,
9 | };
10 | };
11 |
12 | export var quat_set = (q, x, y, z, w) => {
13 | q.x = x;
14 | q.y = y;
15 | q.z = z;
16 | q.w = w;
17 | return q;
18 | };
19 |
20 | export var quat_copy = (a, b) => {
21 | a.x = b.x;
22 | a.y = b.y;
23 | a.z = b.z;
24 | a.w = b.w;
25 | return a;
26 | };
27 |
28 | export var quat_setFromEuler = (q, euler) => {
29 | var { x, y, z } = euler;
30 |
31 | // http://www.mathworks.com/matlabcentral/fileexchange/
32 | // 20696-function-to-convert-between-dcm-euler-angles-quaternions-and-euler-vectors/
33 | // content/SpinCalc.m
34 |
35 | var c1 = Math.cos(x / 2);
36 | var c2 = Math.cos(y / 2);
37 | var c3 = Math.cos(z / 2);
38 |
39 | var s1 = Math.sin(x / 2);
40 | var s2 = Math.sin(y / 2);
41 | var s3 = Math.sin(z / 2);
42 |
43 | q.x = s1 * c2 * c3 + c1 * s2 * s3;
44 | q.y = c1 * s2 * c3 - s1 * c2 * s3;
45 | q.z = c1 * c2 * s3 + s1 * s2 * c3;
46 | q.w = c1 * c2 * c3 - s1 * s2 * s3;
47 |
48 | return q;
49 | };
50 |
51 | export var quat_setFromAxisAngle = (q, axis, angle) => {
52 | // http://www.euclideanspace.com/maths/geometry/rotations/conversions/angleToQuaternion/index.htm
53 | // assumes axis is normalized
54 |
55 | var halfAngle = angle / 2;
56 | var s = Math.sin(halfAngle);
57 |
58 | q.x = axis.x * s;
59 | q.y = axis.y * s;
60 | q.z = axis.z * s;
61 | q.w = Math.cos(halfAngle);
62 |
63 | return q;
64 | };
65 |
66 | export var quat_setFromRotationMatrix = (q, m) => {
67 | // http://www.euclideanspace.com/maths/geometry/rotations/conversions/matrixToQuaternion/index.htm
68 | // assumes the upper 3x3 of m is a pure rotation matrix (i.e, unscaled)
69 |
70 | var m11 = m[0],
71 | m12 = m[4],
72 | m13 = m[8];
73 | var m21 = m[1],
74 | m22 = m[5],
75 | m23 = m[9];
76 | var m31 = m[2],
77 | m32 = m[6],
78 | m33 = m[10];
79 |
80 | var trace = m11 + m22 + m33;
81 | var s;
82 |
83 | if (trace > 0) {
84 | s = 0.5 / Math.sqrt(trace + 1);
85 |
86 | q.w = 0.25 / s;
87 | q.x = (m32 - m23) * s;
88 | q.y = (m13 - m31) * s;
89 | q.z = (m21 - m12) * s;
90 | } else if (m11 > m22 && m11 > m33) {
91 | s = 2 * Math.sqrt(1 + m11 - m22 - m33);
92 |
93 | q.w = (m32 - m23) / s;
94 | q.x = 0.25 * s;
95 | q.y = (m12 + m21) / s;
96 | q.z = (m13 + m31) / s;
97 | } else if (m22 > m33) {
98 | s = 2 * Math.sqrt(1 + m22 - m11 - m33);
99 |
100 | q.w = (m13 - m31) / s;
101 | q.x = (m12 + m21) / s;
102 | q.y = 0.25 * s;
103 | q.z = (m23 + m32) / s;
104 | } else {
105 | s = 2 * Math.sqrt(1 + m33 - m11 - m22);
106 |
107 | q.w = (m21 - m12) / s;
108 | q.x = (m13 + m31) / s;
109 | q.y = (m23 + m32) / s;
110 | q.z = 0.25 * s;
111 | }
112 |
113 | return q;
114 | };
115 |
116 | export var quat_angleTo = (a, b) => {
117 | return 2 * Math.acos(Math.abs(clamp(quat_dot(a, b), -1, 1)));
118 | };
119 |
120 | export var quat_rotateTowards = (a, b, step) => {
121 | var angle = quat_angleTo(a, b);
122 |
123 | if (!angle) return a;
124 |
125 | var t = Math.min(1, step / angle);
126 |
127 | quat_slerp(a, b, t);
128 |
129 | return a;
130 | };
131 |
132 | export var quat_dot = (a, b) => {
133 | return a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w;
134 | };
135 |
136 | export var quat_length = q => {
137 | return Math.sqrt(q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w);
138 | };
139 |
140 | export var quat_normalize = q => {
141 | var l = quat_length(q);
142 |
143 | if (!l) {
144 | q.x = 0;
145 | q.y = 0;
146 | q.z = 0;
147 | q.w = 1;
148 | } else {
149 | l = 1 / l;
150 |
151 | q.x = q.x * l;
152 | q.y = q.y * l;
153 | q.z = q.z * l;
154 | q.w = q.w * l;
155 | }
156 |
157 | return q;
158 | };
159 |
160 | export var quat_multiply = (a, b) => {
161 | // from http://www.euclideanspace.com/maths/algebra/realNormedAlgebra/quaternions/code/index.htm
162 | var qax = a.x,
163 | qay = a.y,
164 | qaz = a.z,
165 | qaw = a.w;
166 | var qbx = b.x,
167 | qby = b.y,
168 | qbz = b.z,
169 | qbw = b.w;
170 |
171 | a.x = qax * qbw + qaw * qbx + qay * qbz - qaz * qby;
172 | a.y = qay * qbw + qaw * qby + qaz * qbx - qax * qbz;
173 | a.z = qaz * qbw + qaw * qbz + qax * qby - qay * qbx;
174 | a.w = qaw * qbw - qax * qbx - qay * qby - qaz * qbz;
175 |
176 | return a;
177 | };
178 |
179 | export var quat_slerp = (a, b, t) => {
180 | if (t === 0) return a;
181 | if (t === 1) return quat_copy(a, b);
182 |
183 | var { x, y, z, w } = a;
184 |
185 | // http://www.euclideanspace.com/maths/algebra/realNormedAlgebra/quaternions/slerp/
186 |
187 | var cosHalfTheta = w * b.w + x * b.x + y * b.y + z * b.z;
188 |
189 | if (cosHalfTheta < 0) {
190 | a.w = -b.w;
191 | a.x = -b.x;
192 | a.y = -b.y;
193 | a.z = -b.z;
194 |
195 | cosHalfTheta = -cosHalfTheta;
196 | } else {
197 | quat_copy(a, b);
198 | }
199 |
200 | if (cosHalfTheta >= 1.0) {
201 | a.w = w;
202 | a.x = x;
203 | a.y = y;
204 | a.z = z;
205 |
206 | return a;
207 | }
208 |
209 | var sqrSinHalfTheta = 1.0 - cosHalfTheta * cosHalfTheta;
210 |
211 | if (sqrSinHalfTheta <= Number.EPSILON) {
212 | var s = 1 - t;
213 | a.w = s * w + t * a.w;
214 | a.x = s * x + t * a.x;
215 | a.y = s * y + t * a.y;
216 | a.z = s * z + t * a.z;
217 |
218 | return quat_normalize(a);
219 | }
220 |
221 | var sinHalfTheta = Math.sqrt(sqrSinHalfTheta);
222 | var halfTheta = Math.atan2(sinHalfTheta, cosHalfTheta);
223 | var ratioA = Math.sin((1 - t) * halfTheta) / sinHalfTheta,
224 | ratioB = Math.sin(t * halfTheta) / sinHalfTheta;
225 |
226 | a.w = w * ratioA + a.w * ratioB;
227 | a.x = x * ratioA + a.x * ratioB;
228 | a.y = y * ratioA + a.y * ratioB;
229 | a.z = z * ratioA + a.z * ratioB;
230 |
231 | return a;
232 | };
233 |
--------------------------------------------------------------------------------
/src/ray.js:
--------------------------------------------------------------------------------
1 | import { mat4_create, mat4_getInverse } from './mat4.js';
2 | import {
3 | vec3_create,
4 | vec3_add,
5 | vec3_applyMatrix4,
6 | vec3_clone,
7 | vec3_cross,
8 | vec3_crossVectors,
9 | vec3_distanceTo,
10 | vec3_dot,
11 | vec3_multiplyScalar,
12 | vec3_subVectors,
13 | vec3_transformDirection,
14 | } from './vec3.js';
15 |
16 | export var ray_create = (origin = vec3_create(), direction = vec3_create()) => {
17 | return {
18 | origin,
19 | direction,
20 | };
21 | };
22 |
23 | export var ray_copy = (a, b) => {
24 | Object.assign(a.origin, b.origin);
25 | Object.assign(a.direction, b.direction);
26 | return a;
27 | };
28 |
29 | export var ray_at = (ray, t, result = vec3_create()) => {
30 | return vec3_add(
31 | vec3_multiplyScalar(Object.assign(result, ray.direction), t),
32 | ray.origin,
33 | );
34 | };
35 |
36 | export var ray_intersectBox = (ray, box, target) => {
37 | var { origin, direction } = ray;
38 |
39 | var txmin = (box.min.x - origin.x) / direction.x;
40 | var txmax = (box.max.x - origin.x) / direction.x;
41 | if (txmin > txmax) {
42 | [txmin, txmax] = [txmax, txmin];
43 | }
44 |
45 | var tymin = (box.min.y - origin.y) / direction.y;
46 | var tymax = (box.max.y - origin.y) / direction.y;
47 | if (tymin > tymax) {
48 | [tymin, tymax] = [tymax, tymin];
49 | }
50 |
51 | if (txmin > tymax || tymin > txmax) {
52 | return;
53 | }
54 |
55 | // Math.min/max with NaN support (0 / 0).
56 | var tmin = tymin > txmin || txmin !== txmin ? tymin : txmin;
57 | var tmax = tymax < txmax || txmax !== txmax ? tymax : txmax;
58 |
59 | var tzmin = (box.min.z - origin.z) / direction.z;
60 | var tzmax = (box.max.z - origin.z) / direction.z;
61 | if (tzmin > tzmax) {
62 | [tzmin, tzmax] = [tzmax, tzmin];
63 | }
64 |
65 | if (tmin > tzmax || tzmin > tmax) {
66 | return;
67 | }
68 |
69 | tmin = tzmin > tmin || tmin !== tmin ? tzmin : tmin;
70 | tmax = tzmax < tmax || tmax !== tmax ? tzmax : tmax;
71 |
72 | if (tmax < 0) {
73 | return;
74 | }
75 |
76 | return ray_at(ray, tmin >= 0 ? tmin : tmax, target);
77 | };
78 |
79 | export var ray_intersectTriangle = (() => {
80 | var diff = vec3_create();
81 | var edge1 = vec3_create();
82 | var edge2 = vec3_create();
83 | var normal = vec3_create();
84 |
85 | return (ray, a, b, c, target) => {
86 | vec3_subVectors(edge1, b, a);
87 | vec3_subVectors(edge2, c, a);
88 |
89 | vec3_crossVectors(normal, edge1, edge2);
90 |
91 | // Determinant.
92 | var DdN = vec3_dot(ray.direction, normal);
93 | var sign = 1;
94 |
95 | if (DdN > 0) {
96 | return;
97 | } else if (DdN < 0) {
98 | sign = -1;
99 | DdN *= -1;
100 | } else {
101 | return;
102 | }
103 |
104 | vec3_subVectors(diff, ray.origin, a);
105 | var DdQxE2 =
106 | sign * vec3_dot(ray.direction, vec3_crossVectors(edge2, diff, edge2));
107 |
108 | // b1 < 0, no intersection
109 | if (DdQxE2 < 0) {
110 | return;
111 | }
112 |
113 | var DdE1xQ = sign * vec3_dot(ray.direction, vec3_cross(edge1, diff));
114 |
115 | // b2 < 0, no intersection
116 | if (DdE1xQ < 0) {
117 | return;
118 | }
119 |
120 | // b1+b2 > 1, no intersection
121 | if (DdQxE2 + DdE1xQ > DdN) {
122 | return;
123 | }
124 |
125 | // Line intersects triangle, check if ray does.
126 | var QdN = -sign * vec3_dot(diff, normal);
127 |
128 | // t < 0, no intersection
129 | if (QdN < 0) {
130 | return;
131 | }
132 |
133 | // Ray intersects triangle.
134 | return ray_at(ray, QdN / DdN, target);
135 | };
136 | })();
137 |
138 | export var ray_intersectsMesh = (() => {
139 | var inverseMatrix = mat4_create();
140 | var rayCopy = ray_create();
141 |
142 | var intersectionPoint = vec3_create();
143 | var intersectionPointWorld = vec3_create();
144 |
145 | var checkIntersection = (object, ray, a, b, c, point) => {
146 | var intersect = ray_intersectTriangle(ray, a, b, c, point);
147 | if (!intersect) {
148 | return;
149 | }
150 |
151 | Object.assign(intersectionPointWorld, point);
152 | vec3_applyMatrix4(intersectionPointWorld, object.matrixWorld);
153 |
154 | return vec3_clone(intersectionPointWorld);
155 | };
156 |
157 | return (ray, object) => {
158 | var intersections = [];
159 |
160 | mat4_getInverse(inverseMatrix, object.matrixWorld);
161 | ray_applyMatrix4(ray_copy(rayCopy, ray), inverseMatrix);
162 |
163 | var { vertices, faces } = object.geometry;
164 |
165 | faces.map((face, faceIndex) => {
166 | var a = vertices[face.a];
167 | var b = vertices[face.b];
168 | var c = vertices[face.c];
169 |
170 | var point = checkIntersection(
171 | object,
172 | rayCopy,
173 | a,
174 | b,
175 | c,
176 | intersectionPoint,
177 | );
178 | if (point) {
179 | intersections.push({
180 | point,
181 | object,
182 | face,
183 | faceIndex,
184 | distance: vec3_distanceTo(ray.origin, point),
185 | });
186 | }
187 | });
188 |
189 | return intersections;
190 | };
191 | })();
192 |
193 | export var ray_applyMatrix4 = (r, m) => {
194 | vec3_applyMatrix4(r.origin, m);
195 | vec3_transformDirection(r.direction, m);
196 | return r;
197 | };
198 |
199 | export var ray_intersectObjects = (ray, objects) => {
200 | return []
201 | .concat(...objects.map(object => ray_intersectsMesh(ray, object)))
202 | .sort((a, b) => a.distance - b.distance);
203 | };
204 |
--------------------------------------------------------------------------------
/src/shader.js:
--------------------------------------------------------------------------------
1 | export var createShaderProgram = (gl, vs, fs) => {
2 | var program = gl.createProgram();
3 |
4 | var createShader = (type, source) => {
5 | var shader = gl.createShader(type);
6 | gl.shaderSource(shader, source);
7 | gl.compileShader(shader);
8 | gl.attachShader(program, shader);
9 | };
10 |
11 | createShader(gl.VERTEX_SHADER, vs);
12 | createShader(gl.FRAGMENT_SHADER, fs);
13 |
14 | gl.linkProgram(program);
15 |
16 | return program;
17 | };
18 |
19 | export var createFloat32Buffer = (gl, array) => {
20 | var buffer = gl.createBuffer();
21 | gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
22 | gl.bufferData(gl.ARRAY_BUFFER, array, gl.STATIC_DRAW);
23 | return buffer;
24 | };
25 |
26 | export var setFloat32Attribute = (gl, location, buffer, size) => {
27 | gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
28 | gl.enableVertexAttribArray(location);
29 | gl.vertexAttribPointer(location, size, gl.FLOAT, false, 0, 0);
30 | };
31 |
32 | export var setFloatUniform = (gl, location, value) => {
33 | gl.uniform1f(location, value);
34 | };
35 |
36 | export var setMat4Uniform = (gl, location, array) => {
37 | gl.uniformMatrix4fv(location, false, array);
38 | };
39 |
40 | export var setVec3Uniform = (gl, location, vector) => {
41 | gl.uniform3f(location, vector.x, vector.y, vector.z);
42 | };
43 |
44 | export var getAttributeLocations = (gl, program) => {
45 | var locations = {};
46 |
47 | var count = gl.getProgramParameter(program, gl.ACTIVE_ATTRIBUTES);
48 |
49 | for (var i = 0; i < count; i++) {
50 | var attribute = gl.getActiveAttrib(program, i);
51 | var { name } = attribute;
52 | locations[name] = gl.getAttribLocation(program, name);
53 | }
54 |
55 | return locations;
56 | };
57 |
58 | export var getUniformLocations = (gl, program) => {
59 | var locations = {};
60 |
61 | var count = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS);
62 |
63 | for (var i = 0; i < count; i++) {
64 | var uniform = gl.getActiveUniform(program, i);
65 | var { name } = uniform;
66 | locations[name] = gl.getUniformLocation(program, name);
67 | }
68 |
69 | return locations;
70 | };
71 |
--------------------------------------------------------------------------------
/src/shaders/phong_frag.glsl.js:
--------------------------------------------------------------------------------
1 | export var frag = `
2 | #extension GL_OES_standard_derivatives : enable
3 |
4 | precision highp float;
5 | precision highp int;
6 |
7 | #define RECIPROCAL_PI 0.31830988618
8 | #define saturate(a) clamp(a, 0.0, 1.0)
9 |
10 | uniform vec3 diffuse;
11 | uniform vec3 emissive;
12 | uniform vec3 specular;
13 | uniform float shininess;
14 |
15 | struct IncidentLight {
16 | vec3 color;
17 | vec3 direction;
18 | };
19 |
20 | struct ReflectedLight {
21 | vec3 directDiffuse;
22 | vec3 directSpecular;
23 | vec3 indirectDiffuse;
24 | vec3 indirectSpecular;
25 | };
26 |
27 | struct GeometricContext {
28 | vec3 position;
29 | vec3 normal;
30 | vec3 viewDir;
31 | };
32 |
33 | varying vec3 vColor;
34 |
35 | uniform vec3 fogColor;
36 | varying float fogDepth;
37 | uniform float fogNear;
38 | uniform float fogFar;
39 |
40 | vec3 BRDF_Diffuse_Lambert(const in vec3 diffuseColor) {
41 | return RECIPROCAL_PI * diffuseColor;
42 | }
43 |
44 | vec3 F_Schlick(const in vec3 specularColor, const in float dotLH) {
45 | float fresnel = exp2((-5.55473 * dotLH - 6.98316) * dotLH);
46 | return (1.0 - specularColor) * fresnel + specularColor;
47 | }
48 |
49 | float G_BlinnPhong_Implicit() {
50 | return 0.25;
51 | }
52 |
53 | float D_BlinnPhong(const in float shininess, const in float dotNH) {
54 | return RECIPROCAL_PI * (shininess * 0.5 + 1.0) * pow(dotNH, shininess);
55 | }
56 |
57 | vec3 BRDF_Specular_BlinnPhong(const in IncidentLight incidentLight, const in GeometricContext geometry, const in vec3 specularColor, const in float shininess) {
58 | vec3 halfDir = normalize(incidentLight.direction + geometry.viewDir);
59 | float dotNH = saturate(dot(geometry.normal, halfDir));
60 | float dotLH = saturate(dot(incidentLight.direction, halfDir));
61 | vec3 F = F_Schlick(specularColor, dotLH);
62 | float G = G_BlinnPhong_Implicit();
63 | float D = D_BlinnPhong(shininess, dotNH);
64 | return F * (G * D);
65 | }
66 |
67 | uniform vec3 ambientLightColor;
68 | vec3 getAmbientLightIrradiance(const in vec3 ambientLightColor) {
69 | vec3 irradiance = ambientLightColor;
70 | return irradiance;
71 | }
72 |
73 | struct DirectionalLight {
74 | vec3 direction;
75 | vec3 color;
76 | };
77 |
78 | uniform DirectionalLight directionalLights[NUM_DIR_LIGHTS];
79 |
80 | void getDirectionalDirectLightIrradiance(const in DirectionalLight directionalLight, const in GeometricContext geometry, out IncidentLight directLight) {
81 | directLight.color = directionalLight.color;
82 | directLight.direction = directionalLight.direction;
83 | }
84 |
85 | varying vec3 vViewPosition;
86 |
87 | struct BlinnPhongMaterial {
88 | vec3 diffuseColor;
89 | vec3 specularColor;
90 | float specularShininess;
91 | };
92 |
93 | void RE_Direct_BlinnPhong(const in IncidentLight directLight, const in GeometricContext geometry, const in BlinnPhongMaterial material, inout ReflectedLight reflectedLight) {
94 | float dotNL = saturate(dot(geometry.normal, directLight.direction));
95 | vec3 irradiance = dotNL * directLight.color;
96 | reflectedLight.directDiffuse += irradiance * BRDF_Diffuse_Lambert(material.diffuseColor);
97 | reflectedLight.directSpecular += irradiance * BRDF_Specular_BlinnPhong(directLight, geometry, material.specularColor, material.specularShininess);
98 | }
99 |
100 | void RE_IndirectDiffuse_BlinnPhong(const in vec3 irradiance, const in GeometricContext geometry, const in BlinnPhongMaterial material, inout ReflectedLight reflectedLight) {
101 | reflectedLight.indirectDiffuse += irradiance * BRDF_Diffuse_Lambert(material.diffuseColor);
102 | }
103 |
104 | void main() {
105 | vec3 diffuseColor = diffuse;
106 | ReflectedLight reflectedLight = ReflectedLight(vec3(0.0), vec3(0.0), vec3(0.0), vec3(0.0));
107 |
108 | diffuseColor *= vColor;
109 |
110 | vec3 fdx = dFdx(vViewPosition);
111 | vec3 fdy = dFdy(vViewPosition);
112 | vec3 normal = normalize(cross(fdx, fdy));
113 |
114 | BlinnPhongMaterial material;
115 | material.diffuseColor = diffuseColor;
116 | material.specularColor = specular;
117 | material.specularShininess = shininess;
118 |
119 | GeometricContext geometry;
120 | geometry.position = -vViewPosition;
121 | geometry.normal = normal;
122 | geometry.viewDir = normalize(vViewPosition);
123 | IncidentLight directLight;
124 |
125 | DirectionalLight directionalLight;
126 | for (int i = 0; i < NUM_DIR_LIGHTS; i++) {
127 | directionalLight = directionalLights[i];
128 | getDirectionalDirectLightIrradiance(directionalLight, geometry, directLight);
129 | RE_Direct_BlinnPhong(directLight, geometry, material, reflectedLight);
130 | }
131 |
132 | vec3 irradiance = getAmbientLightIrradiance(ambientLightColor);
133 | RE_IndirectDiffuse_BlinnPhong(irradiance, geometry, material, reflectedLight);
134 |
135 | vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular + emissive;
136 |
137 | float fogFactor = smoothstep(fogNear, fogFar, fogDepth);
138 | gl_FragColor = vec4(mix(outgoingLight, fogColor, fogFactor), 1.0);
139 | }
140 | `.trim();
141 |
--------------------------------------------------------------------------------
/src/shaders/phong_vert.glsl.js:
--------------------------------------------------------------------------------
1 | export var vert = `
2 | precision highp float;
3 | precision highp int;
4 |
5 | uniform mat4 modelViewMatrix;
6 | uniform mat4 projectionMatrix;
7 | attribute vec3 position;
8 | varying vec3 vViewPosition;
9 |
10 | attribute vec3 color;
11 | varying vec3 vColor;
12 |
13 | varying float fogDepth;
14 |
15 | void main() {
16 | vColor.xyz = color.xyz;
17 |
18 | vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
19 |
20 | gl_Position = projectionMatrix * mvPosition;
21 | vViewPosition = -mvPosition.xyz;
22 |
23 | fogDepth = -mvPosition.z;
24 | }
25 | `.trim();
26 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | export var compose = (...fns) =>
2 | fns.reduceRight((f, g) => (...args) => f(g(...args)));
3 |
4 | export var rearg = fn => (...args) => value => fn(value, ...args);
5 |
6 | export var remove = (array, element) => {
7 | var index = array.indexOf(element);
8 | if (index >= 0) {
9 | array.splice(index, 1);
10 | }
11 | };
12 |
--------------------------------------------------------------------------------
/src/vec3.js:
--------------------------------------------------------------------------------
1 | export var vec3_create = (x = 0, y = 0, z = 0) => {
2 | return {
3 | x,
4 | y,
5 | z,
6 | };
7 | };
8 |
9 | export var vec3_set = (v, x, y, z) => {
10 | v.x = x;
11 | v.y = y;
12 | v.z = z;
13 | return v;
14 | };
15 |
16 | export var vec3_setScalar = (v, scalar) => {
17 | v.x = scalar;
18 | v.y = scalar;
19 | v.z = scalar;
20 | return v;
21 | };
22 |
23 | export var vec3_setX = (v, x) => {
24 | v.x = x;
25 | return v;
26 | };
27 |
28 | export var vec3_setY = (v, y) => {
29 | v.y = y;
30 | return v;
31 | };
32 |
33 | export var vec3_setZ = (v, z) => {
34 | v.z = z;
35 | return v;
36 | };
37 |
38 | export var vec3_clone = v => {
39 | return vec3_create(v.x, v.y, v.z);
40 | };
41 |
42 | export var vec3_add = (a, b) => {
43 | a.x += b.x;
44 | a.y += b.y;
45 | a.z += b.z;
46 | return a;
47 | };
48 |
49 | export var vec3_addVectors = (v, a, b) => {
50 | v.x = a.x + b.x;
51 | v.y = a.y + b.y;
52 | v.z = a.z + b.z;
53 | return v;
54 | };
55 |
56 | export var vec3_addScaledVector = (a, b, s) => {
57 | a.x += b.x * s;
58 | a.y += b.y * s;
59 | a.z += b.z * s;
60 | return a;
61 | };
62 |
63 | export var vec3_sub = (a, b) => {
64 | a.x -= b.x;
65 | a.y -= b.y;
66 | a.z -= b.z;
67 | return a;
68 | };
69 |
70 | export var vec3_subVectors = (v, a, b) => {
71 | v.x = a.x - b.x;
72 | v.y = a.y - b.y;
73 | v.z = a.z - b.z;
74 | return v;
75 | };
76 |
77 | export var vec3_multiply = (a, b) => {
78 | a.x *= b.x;
79 | a.y *= b.y;
80 | a.z *= b.z;
81 | return a;
82 | };
83 |
84 | export var vec3_multiplyScalar = (v, scalar) => {
85 | v.x *= scalar;
86 | v.y *= scalar;
87 | v.z *= scalar;
88 | return v;
89 | };
90 |
91 | export var vec3_applyMatrix4 = (v, m) => {
92 | var { x, y, z } = v;
93 |
94 | var w = 1 / (m[3] * x + m[7] * y + m[11] * z + m[15]);
95 |
96 | v.x = (m[0] * x + m[4] * y + m[8] * z + m[12]) * w;
97 | v.y = (m[1] * x + m[5] * y + m[9] * z + m[13]) * w;
98 | v.z = (m[2] * x + m[6] * y + m[10] * z + m[14]) * w;
99 |
100 | return v;
101 | };
102 |
103 | export var vec3_applyQuaternion = (v, q) => {
104 | var { x, y, z } = v;
105 | var qx = q.x,
106 | qy = q.y,
107 | qz = q.z,
108 | qw = q.w;
109 |
110 | // calculate quat * vector
111 |
112 | var ix = qw * x + qy * z - qz * y;
113 | var iy = qw * y + qz * x - qx * z;
114 | var iz = qw * z + qx * y - qy * x;
115 | var iw = -qx * x - qy * y - qz * z;
116 |
117 | // calculate result * inverse quat
118 |
119 | v.x = ix * qw + iw * -qx + iy * -qz - iz * -qy;
120 | v.y = iy * qw + iw * -qy + iz * -qx - ix * -qz;
121 | v.z = iz * qw + iw * -qz + ix * -qy - iy * -qx;
122 |
123 | return v;
124 | };
125 |
126 | export var vec3_transformDirection = (v, m) => {
127 | // input: THREE.Matrix4 affine matrix
128 | // vector interpreted as a direction
129 |
130 | var { x, y, z } = v;
131 |
132 | v.x = m[0] * x + m[4] * y + m[8] * z;
133 | v.y = m[1] * x + m[5] * y + m[9] * z;
134 | v.z = m[2] * x + m[6] * y + m[10] * z;
135 |
136 | return vec3_normalize(v);
137 | };
138 |
139 | export var vec3_divideScalar = (v, scalar) => {
140 | return vec3_multiplyScalar(v, 1 / scalar);
141 | };
142 |
143 | export var vec3_min = (a, b) => {
144 | a.x = Math.min(a.x, b.x);
145 | a.y = Math.min(a.y, b.y);
146 | a.z = Math.min(a.z, b.z);
147 | return a;
148 | };
149 |
150 | export var vec3_max = (a, b) => {
151 | a.x = Math.max(a.x, b.x);
152 | a.y = Math.max(a.y, b.y);
153 | a.z = Math.max(a.z, b.z);
154 | return a;
155 | };
156 |
157 | export var vec3_dot = (a, b) => {
158 | return a.x * b.x + a.y * b.y + a.z * b.z;
159 | };
160 |
161 | export var vec3_length = v => {
162 | return Math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z);
163 | };
164 |
165 | export var vec3_normalize = v => {
166 | return vec3_divideScalar(v, vec3_length(v) || 1);
167 | };
168 |
169 | export var vec3_cross = (a, b) => {
170 | var { x, y, z } = a;
171 |
172 | a.x = y * b.z - z * b.y;
173 | a.y = z * b.x - x * b.z;
174 | a.z = x * b.y - y * b.x;
175 |
176 | return a;
177 | };
178 |
179 | export var vec3_crossVectors = (v, a, b) => {
180 | var ax = a.x;
181 | var ay = a.y;
182 | var az = a.z;
183 |
184 | var bx = b.x;
185 | var by = b.y;
186 | var bz = b.z;
187 |
188 | v.x = ay * bz - az * by;
189 | v.y = az * bx - ax * bz;
190 | v.z = ax * by - ay * bx;
191 |
192 | return v;
193 | };
194 |
195 | export var vec3_distanceTo = (a, b) => {
196 | return Math.sqrt(vec3_distanceToSquared(a, b));
197 | };
198 |
199 | export var vec3_distanceToSquared = (a, b) => {
200 | var dx = a.x - b.x,
201 | dy = a.y - b.y,
202 | dz = a.z - b.z;
203 | return dx * dx + dy * dy + dz * dz;
204 | };
205 |
206 | export var vec3_setFromMatrixPosition = (v, m) => {
207 | v.x = m[12];
208 | v.y = m[13];
209 | v.z = m[14];
210 | return v;
211 | };
212 |
213 | export var vec3_fromArray = (v, array) => {
214 | v.x = array[0];
215 | v.y = array[1];
216 | v.z = array[2];
217 | return v;
218 | };
219 |
220 | export var vec3_X = vec3_create(1, 0, 0);
221 | export var vec3_Y = vec3_create(0, 1, 0);
222 | export var vec3_Z = vec3_create(0, 0, 1);
223 |
224 | // https://github.com/id-Software/Quake-III-Arena/blob/master/code/game/bg_pmove.c
225 | export var OVERCLIP = 1.001;
226 |
227 | export var pm_clipVelocity = (vector, normal, overbounce) => {
228 | var backoff = vec3_dot(vector, normal);
229 |
230 | if (backoff < 0) {
231 | backoff *= overbounce;
232 | } else {
233 | backoff /= overbounce;
234 | }
235 |
236 | vec3_addScaledVector(vector, normal, -backoff);
237 | };
238 |
--------------------------------------------------------------------------------