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