//import { initUiListeners, setupParamPanel, tick } from './game'; //import { initWorldGl, makeWorld } from './world'; import * as se3 from '../se3'; import {loadTexture, makeProgram} from '../gl'; import {makeFace, makeBufferFromFaces} from '../geometry'; import { loadStlModel } from './stl'; import * as linalg from './linalg'; const VSHADER = ` attribute vec3 aPosition; attribute vec3 aNormal; attribute vec2 aTextureCoord; uniform mat4 uProjection; uniform mat4 uModel; uniform mat4 uView; uniform vec3 uLightDirection; uniform float uAmbiantLight; varying highp vec2 vTextureCoord; varying lowp vec3 vLighting; varying lowp float vDistance; void main() { highp mat4 modelview = uView * uModel; gl_Position = uProjection * modelview * vec4(aPosition, 1.0); lowp vec3 normal = mat3(uModel) * aNormal; lowp float diffuseAmount = max(dot(-uLightDirection, normal), 0.0); lowp vec3 ambiant = uAmbiantLight * vec3(1.0, 1.0, 0.9); vLighting = ambiant + vec3(1.0, 1.0, 1.0) * diffuseAmount; vTextureCoord = aTextureCoord; vDistance = length(modelview * vec4(aPosition, 1.0)); } `; const FSHADER = ` uniform sampler2D uSampler; uniform lowp vec3 uFogColor; varying highp vec2 vTextureCoord; varying lowp vec3 vLighting; varying lowp float vDistance; void main() { highp vec4 color = texture2D(uSampler, vTextureCoord); if (color.a < 0.1) { discard; } lowp float fogamount = 0.0; //smoothstep(80.0, 100.0, vDistance); gl_FragColor = mix(vec4(vLighting * color.rgb, color.a), vec4(uFogColor, 1.0), fogamount); } `; const kEpoch = 0; async function initWorldGl(gl) { const program = makeProgram(gl, VSHADER, FSHADER); const texture = await loadTexture(gl, 'texture.png'); // load those ahead of time const viewLoc = gl.getUniformLocation(program, 'uView'); const modelLoc = gl.getUniformLocation(program, 'uModel'); const projLoc = gl.getUniformLocation(program, 'uProjection'); const samplerLoc = gl.getUniformLocation(program, 'uSampler'); const fogColorLoc = gl.getUniformLocation(program, 'uFogColor'); const lightDirectionLoc = gl.getUniformLocation(program, 'uLightDirection'); const ambiantLoc = gl.getUniformLocation(program, 'uAmbiantLight'); const positionLoc = gl.getAttribLocation(program, 'aPosition'); const normalLoc = gl.getAttribLocation(program, 'aNormal'); const textureLoc = gl.getAttribLocation(program, 'aTextureCoord'); const setupScene = (sceneParams) => { const { projectionMatrix, viewMatrix, fogColor, lightDirection, ambiantLightAmount, } = sceneParams; gl.useProgram(program); gl.uniformMatrix4fv(projLoc, false, new Float32Array(projectionMatrix)); gl.uniformMatrix4fv(viewLoc, false, new Float32Array(viewMatrix)); gl.uniform3fv(fogColorLoc, fogColor); gl.uniform3fv(lightDirectionLoc, lightDirection); gl.uniform1f(ambiantLoc, ambiantLightAmount); // doing this here because it's the same for all world stuff gl.uniformMatrix4fv(modelLoc, false, new Float32Array(se3.identity())); gl.uniform1i(samplerLoc, 0); gl.enableVertexAttribArray(positionLoc); gl.enableVertexAttribArray(normalLoc); gl.enableVertexAttribArray(textureLoc); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, texture); }; const drawObject = (objectParams) => { const { position, orientation, glBuffer, numVertices, } = objectParams; gl.uniformMatrix4fv(modelLoc, false, new Float32Array(se3.product( se3.translation(...position), orientation))); gl.bindBuffer(gl.ARRAY_BUFFER, glBuffer); gl.vertexAttribPointer(positionLoc, 3, gl.FLOAT, false, 20, 0); gl.vertexAttribPointer(normalLoc, 3, gl.BYTE, true, 20, 12); gl.vertexAttribPointer(textureLoc, 2, gl.UNSIGNED_SHORT, true, 20, 16); gl.drawArrays(gl.TRIANGLES, 0, numVertices); }; return { setupScene, drawObject, }; } const ORBIT_VSHADER = ` attribute vec3 aPosition; attribute vec2 aValue; uniform mat4 uProjection; uniform mat4 uModel; uniform mat4 uView; varying lowp vec2 vCoords; void main() { highp mat4 modelview = uView * uModel; gl_Position = uProjection * modelview * vec4(aPosition, 1.0); vCoords = aValue; } `; const ORBIT_FSHADER = ` varying lowp vec2 vCoords; void main() { lowp float x = vCoords.x; lowp float y = vCoords.y; lowp float f = sqrt(x * x + y * y); if (f > 1.00) { discard; } else if (f < 0.98) { discard; } gl_FragColor = vec4(1, .5, 0, 0.5); } `; function getOrbitDrawContext(gl) { const program = makeProgram(gl, ORBIT_VSHADER, ORBIT_FSHADER); // load those ahead of time const viewLoc = gl.getUniformLocation(program, 'uView'); const modelLoc = gl.getUniformLocation(program, 'uModel'); const projLoc = gl.getUniformLocation(program, 'uProjection'); const positionLoc = gl.getAttribLocation(program, 'aPosition'); const valueLoc = gl.getAttribLocation(program, 'aValue'); const setupScene = (sceneParams) => { const { projectionMatrix, viewMatrix, } = sceneParams; gl.useProgram(program); gl.uniformMatrix4fv(projLoc, false, new Float32Array(projectionMatrix)); gl.uniformMatrix4fv(viewLoc, false, new Float32Array(viewMatrix)); // doing this here because it's the same for all world stuff gl.uniformMatrix4fv(modelLoc, false, new Float32Array(se3.identity())); gl.enableVertexAttribArray(positionLoc); gl.enableVertexAttribArray(valueLoc); }; const drawObject = (objectParams) => { const { position, orientation, value, glBuffer, } = objectParams; gl.uniformMatrix4fv(modelLoc, false, new Float32Array(se3.product( se3.translation(...position), orientation))); gl.bindBuffer(gl.ARRAY_BUFFER, glBuffer); gl.vertexAttribPointer(positionLoc, 3, gl.FLOAT, false, 20, 0); gl.vertexAttribPointer(valueLoc, 2, gl.FLOAT, false, 20, 12); gl.disable(gl.CULL_FACE); gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); gl.enable(gl.CULL_FACE); }; return { setupScene, drawObject, }; } const BlockType = { UNDEFINED: 0, AIR: 1, DIRT: 2, GRASS: 3, STONE: 4, WATER: 5, TREE: 6, LEAVES: 7, SUN: 8, }; const CHUNKSIZE = 32; /** seed: some kind of number uniquely defining the body * x, y, z: space coordinates in the body's frame * * returns: a chunk */ function getChunk(seed, x, y, z) { // XXX: for now, return a premade chunk: a 24x24x24 cube of dirt // surrounded by 4 blocks of air all around const cs = CHUNKSIZE; if (seed !== 1337) { return {}; } // if (Math.abs const blocks = new Array(cs * cs * cs); blocks.fill(BlockType.AIR); const dirt = new Array(24).fill(BlockType.DIRT); for (let i = 0; i < 24; i++) { for (let j = 4; j < 28; j++) { const offset = cs * cs * (i + 4) + cs * j; blocks.splice(offset + 4, 24, ...dirt); } } const half = cs / 2; return { position: [-half, -half, -half], blocks, underground: false, }; } function getBodyChunks(seed) { if (seed === 0) { return makeSun(); } return [getChunk(seed, 0, 0, 0)]; } function faceTexture(type, dir) { switch (type) { case BlockType.GRASS: switch (dir) { case '+y': return [0, 15]; case '-y': return [2, 15]; default: return [1, 15]; } case BlockType.DIRT: return [2, 15]; case BlockType.STONE: return [3, 15]; case BlockType.WATER: return [4, 15]; case BlockType.TREE: switch (dir) { case '+y': case '-y': return [5, 15]; default: return [6, 15]; } case BlockType.LEAVES: return [7, 15]; case BlockType.SUN: return [0, 4]; default: return [0, 0]; } } function* makeChunkFaces(chunk) { const cs = CHUNKSIZE; function faceCenter(pos, dir) { switch (dir) { case '-x': return [pos[0] - 0.5, pos[1], pos[2]]; case '+x': return [pos[0] + 0.5, pos[1], pos[2]]; case '-y': return [pos[0], pos[1] - 0.5, pos[2]]; case '+y': return [pos[0], pos[1] + 0.5, pos[2]]; case '-z': return [pos[0], pos[1], pos[2] - 0.5]; case '+z': return [pos[0], pos[1], pos[2] + 0.5]; } } function* neighbors(x, y, z) { const idx = ( z * cs * cs + y * cs + x ); if (x > 0) { yield { block: chunk.blocks[idx - 1], dir: '-x', }; } else { yield { block: BlockType.AIR, dir: '-x', }; } if (x < 31) { yield { block: chunk.blocks[idx + 1], dir: '+x', }; } else { yield { block: BlockType.AIR, dir: '+x', }; } if (y > 0) { yield { block: chunk.blocks[idx - cs], dir: '-y', }; } else { yield { block: BlockType.AIR, dir: '-y', }; } if (y < 31) { yield { block: chunk.blocks[idx + cs], dir: '+y', }; } else { yield { block: BlockType.AIR, dir: '+y', }; } if (z > 0) { yield { block: chunk.blocks[idx - cs * cs], dir: '-z', }; } else { yield { block: BlockType.AIR, dir: '-z', }; } if (z < 31) { yield { block: chunk.blocks[idx + cs * cs], dir: '+z', }; } else { yield { block: BlockType.AIR, dir: '+z', }; } } for (let x = 0; x < cs; x++) { for (let y = 0; y < cs; y++) { for (let z = 0; z < cs; z++) { const idx = ( z * cs * cs + y * cs + x ); const chpos = chunk.position; const bkpos = [ chpos[0] + x, chpos[1] + y, chpos[2] + z, ]; const bt = chunk.blocks[idx]; if (bt === BlockType.AIR) { continue; } for (const {block, dir} of neighbors(x, y, z)) { if (block !== BlockType.AIR) { continue; } yield makeFace(dir, faceTexture(bt), faceCenter(bkpos, dir)); } } } } } function closeToPlanet(context) { const body = findSoi(context); const relativePos = linalg.diff(context.player.position, body.position); return linalg.norm(relativePos) < 20; } function makeSun(seed) { const radius = 7; const radiusChunks = Math.floor(radius / CHUNKSIZE); const chunks = []; function makeSunChunk(i, j, k) { const cs = CHUNKSIZE; const half = cs / 2; const blocks = new Array(cs**3); blocks.fill(BlockType.SUN); let underground = true; for (let x = 0; x < cs; x++) { for (let y = 0; y < cs; y++) { for (let z = 0; z < cs; z++) { const pos = [ x + i * cs - half, y + j * cs - half, z + k * cs - half, ]; const idx = ( z * cs * cs + y * cs + x ); if (pos[0]**2 + pos[1]**2 + pos[2]**2 > radius**2) { blocks[idx] = BlockType.AIR; underground = false; } } } } return { position: [i * cs - half, j * cs - half, k * cs - half], blocks, underground, }; } for (let i = -radiusChunks; i <= radiusChunks; i++) { for (let j = -radiusChunks; j <= radiusChunks; j++) { for (let k = -radiusChunks; k <= radiusChunks; k++) { chunks.push(makeSunChunk(i, j, k)); } } } return chunks; } function getBodyGeometry(seed) { const faces = getBodyChunks(seed) .filter(chunk => !chunk.underground) .map(chunk => [...makeChunkFaces(chunk)]); return faces.reduce((a, b) => a.concat(b)); } function getSolarSystem(seed) { /// XXX: only returns 1 body for now return { name: 'Tat', mass: 1000.0, spin: [0, 0, 0.2], geometry: getBodyGeometry(0), children: [ { name: 'Quicksilver', seed: 1336, mass: 0.1, spin: [0.0, 0.0, 0.05], geometry: makeCube([0, 4]), orbit: { excentricity: 0.0, semimajorAxis: 200, inclination: 0.8, ascendingNodeLongitude: 0, periapsisArgument: 0, t0: 0, }, }, { name: 'Satourne', seed: 1338, mass: 0.1, spin: [0.0, 0.5, 0.0], geometry: makeCube([0, 5]), orbit: { excentricity: 0.0, semimajorAxis: 900, inclination: 0.0, ascendingNodeLongitude: 0, periapsisArgument: 0, t0: 0, }, children: [ { name: 'Kyoujin', seed: 13381, mass: 0.01, spin: [0.0, 0.0, 0.05], geometry: makeCube([0, 6]), orbit: { excentricity: 0.0, semimajorAxis: 20, inclination: Math.PI / 2, ascendingNodeLongitude: 0, periapsisArgument: 0, t0: 0, }, }, ], }, { name: 'Tataooine', seed: 1337, mass: 50, spin: [0.0, 0.0, 0.05], geometry: getBodyGeometry(1337), orbit: { excentricity: 0.3, semimajorAxis: 500, inclination: 0.0, ascendingNodeLongitude: 0, periapsisArgument: 0, t0: 0, }, children: [ { name: 'Mun', seed: 13371, mass: 0.01, spin: [0.0, 0.0, 0.05], geometry: makeCube([0, 7]), orbit: { excentricity: 0.0, semimajorAxis: 50, inclination: Math.PI / 2, ascendingNodeLongitude: 0, periapsisArgument: 0, t0: 0, }, }, ], }, ], }; } function initUiListeners(canvas, context) { const canvasClickHandler = () => { canvas.requestPointerLock(); canvas.onclick = null; // const clickListener = e => { // switch(e.button) { // case 0: // left click // destroySelectedBlock(context); // break; // case 2: // right click // makeDirBlock(context); // break; // } // }; const clickListener = e => {}; const keyListener = e => { if (e.type === 'keydown') { if (e.repeat) return; context.keys.add(e.code); switch (e.code) { case 'KeyF': context.flying = !context.flying; context.player.velocity = [0, 0, 0]; delete context.orbit; return false; case 'KeyL': if (closeToPlanet(context)) { context.landing = True; } return false; case 'Space': if (!context.flying) { if (context.jumpAmount > 0) { const amount = context.jumpForce; context.player.velocity[1] = amount; context.jumpAmount -= 1; } } return false; } } else { context.keys.delete(e.code); } }; const moveListener = e => { context.camera.orientation[0] -= e.movementY / 500; context.camera.orientation[1] -= e.movementX / 500; context.camera.tf = se3.product( se3.roty(context.camera.orientation[1]), se3.rotx(context.camera.orientation[0]), ); }; const changeListener = () => { if (document.pointerLockElement === canvas) { return; } document.removeEventListener('pointerdown', clickListener); document.removeEventListener('pointerlockchange', changeListener); document.removeEventListener('pointermove', moveListener); document.removeEventListener('keydown', keyListener); document.removeEventListener('keyup', keyListener); canvas.onclick = canvasClickHandler; }; document.addEventListener('pointerdown', clickListener); document.addEventListener('pointerlockchange', changeListener); document.addEventListener('pointermove', moveListener); document.addEventListener('keydown', keyListener); document.addEventListener('keyup', keyListener); }; canvas.onclick = canvasClickHandler; document.addEventListener('keydown', e => { if (e.repeat) return; switch (e.code) { case 'F11': canvas.requestFullscreen(); break; } }); } function handleInput(context) { const move = (forward, right) => { if (context.keys.has('ShiftLeft')) { forward *= 10; right *= 10; } const tf = se3.product( se3.orientationOnly(context.player.tf), context.camera.tf, ); const dir = [right, 0, -forward, 10]; if (context.flying) { context.player.tf = [ context.player.tf, context.camera.tf, se3.translation(...dir), ].reduce(se3.product); context.camera.tf = se3.identity(); context.camera.orientation = [0, 0, 0]; } else { const vel = context.player.velocity; const dv = linalg.scale(se3.apply(tf, dir), 1/dir[3]); context.player.velocity = linalg.add(vel, dv); delete context.orbit; } }; context.keys.forEach(key => { switch (key) { case 'KeyW': move(0.5, 0.0); return; case 'KeyA': move(0.0, -0.5); return; case 'KeyS': move(-0.5, 0.0); return; case 'KeyD': move(0.0, 0.5); return; case 'KeyR': context.timeOffset += 1; return; } }); } function updateBodyPhysics(time, body, parentBody) { if (parentBody !== undefined) { const mu = parentBody.mass; const {position, velocity} = getCartesianState(body.orbit, mu, time); body.position = [ parentBody.position[0] + position[0], parentBody.position[1] + position[1], parentBody.position[2] + position[2], ]; body.velocity = [ parentBody.velocity[0] + velocity[0], parentBody.velocity[1] + velocity[1], parentBody.velocity[2] + velocity[2], ]; } else { body.position = [0, 0, 0]; body.velocity = [0, 0, 0]; } body.orientation = getOrientation(body, time); if (body.children !== undefined) { for (const child of body.children) { updateBodyPhysics(time, child, body); } } } function findSoi(context) { const bodies = [context.universe]; let body; while (bodies.length > 0) { body = bodies.shift(); if (body.children === undefined) { return body; } for (const child of body.children) { const soi = child.orbit.semimajorAxis * Math.pow(child.mass / body.mass, 2/5); const pos = context.player.position; const bod = child.position; const dr = [pos[0] - bod[0], pos[1] - bod[1], pos[2] - bod[2]]; if (dr[0]**2 + dr[1]**2 + dr[2]**2 < soi**2) { bodies.push(child); } } } return body; } function computeOrbit(player, body, time) { const {cross, diff, norm, dot, scale} = linalg; const rvec = diff(player.position, body.position); const r = norm(rvec); if (norm(player.velocity) < 1e-6) { // cheating console.log('cheating'); player.velocity = scale(cross([1, 1, 1], rvec), 0.01/(r**2)); } const vvec = diff(player.velocity, body.velocity); const v = norm(vvec); const Hvec = cross(rvec, vvec); const H = norm(Hvec); const mu = body.mass; const p = H**2 / mu; const resinnu = Math.sqrt(p/mu) * dot(vvec, rvec) const recosnu = p - r; const e = Math.sqrt(resinnu**2 + recosnu**2) / r; // should also work for hyperbolic orbits const a = p/(1-e**2); const x = scale(rvec, 1/r); const yy = cross(Hvec, rvec); const y = scale(yy, 1/norm(yy)); const z = scale(Hvec, 1/H); // Om i and w can be skipped when we just give tf... let Om = Math.atan2(Hvec[0], -Hvec[1]); if (Hvec[0] === 0 && Hvec[1] === 0) { Om = 0; } let i = Math.atan2(Math.sqrt(Hvec[0]**2 + Hvec[1]**2), Hvec[2]); if (i * Math.sign((Hvec[0] || 1) * Math.sin(Om)) < 0) { i *= -1; } const nu = Math.atan2(resinnu, recosnu); let w = Math.atan2(rvec[2] / r / Math.sin(i), y[2] / Math.sin(i)) || 0 - nu; let t0; let E; if (a < 0) { E = 2 * Math.atanh(Math.sqrt((e-1)/(e+1)) * Math.tan(nu/2)); const n = Math.sqrt(-mu / (a**3)); t0 = time - (e*Math.sinh(E) - E) / n; } else { E = 2 * Math.atan(Math.sqrt((1-e)/(1+e)) * Math.tan(nu/2)); const n = Math.sqrt(mu / (a**3)); t0 = time - (1/n)*(E - e*Math.sin(E)); } // column-major... see se3.js const tf = se3.product([ x[0], x[1], x[2], 0, y[0], y[1], y[2], 0, z[0], z[1], z[2], 0, 0, 0, 0, 1, ], se3.rotz(-nu)); const orbit = { excentricity: e, semimajorAxis: a, inclination: i, ascendingNodeLongitude: Om, periapsisArgument: w, t0, tf, lastE: E, }; return orbit; } function updatePhysics(time, context) { const {player} = context; const dt = time - (context.lastTime || 0); context.lastTime = time; player.position = se3.apply(player.tf, [0, 0, 0, 1]); updateBodyPhysics(time, context.universe); if (!context.flying) { if (context.orbit === undefined) { const newPos = linalg.add(player.position, linalg.scale(player.velocity, dt)); const dx = linalg.diff(newPos, player.position); player.tf = se3.product(se3.translation(...dx), player.tf); const body = findSoi(context); context.orbit = computeOrbit(player, body, time); console.log(`orbiting ${body.name}, excentricity: ${context.orbit.excentricity}`); context.orbitBody = body; } else { const {position: orbitPos, velocity: orbitVel} = getCartesianState( context.orbit, context.orbitBody.mass, time); if (orbitPos === undefined) { const newPos = linalg.add(player.position, linalg.scale(player.velocity, dt)); const dx = linalg.diff(newPos, player.position); player.tf = se3.product(se3.translation(...dx), player.tf); } else { const position = linalg.add(orbitPos, context.orbitBody.position); const velocity = linalg.add(orbitVel, context.orbitBody.velocity); player.velocity = velocity; const dx = linalg.diff(position, player.position); player.tf = se3.product(se3.translation(...dx), player.tf); } } } } function updateGeometry(context, timeout_ms = 10) { } function normalizeAngle(theta) { const twopi = 2 * Math.PI; return theta - twopi * Math.floor((theta + Math.PI) / twopi); } /** Let's be honest I should clean this up. * * This is the part that solves Kepler's equation using Newton's method. * For circular-ish orbits, one or two iterations are usually enough. * More excentric orbits can take more (6 or 7?). * * For near-parabolic orbits (and some others?) it often fails to converge... */ function getCartesianState(orbit, mu, time) { const { excentricity: e, semimajorAxis: a, inclination: i, ascendingNodeLongitude: Om, periapsisArgument: w, t0, } = orbit; let n = Math.sqrt(mu/(a**3)); if (a < 0) { n = Math.sqrt(mu/-(a**3)); // mean motion } const M = n * (time - t0); // mean anomaly // Newton's method var E2 = 0; var E = orbit.lastE || M; let iterations = 0; // a clever guess? https://link.springer.com/article/10.1023/A:1008200607490 // doesn't work at all. while (Math.abs(E - E2) > 1e-10) { if (e < 0.001) { break; } E = E2; if (e < 1) { E2 = E - (E - e * Math.sin(E) - M) / (1 - e * Math.cos(E)); } else if (e > 1) { E2 = E - (-E + e * Math.sinh(E) - M) / (e * Math.cosh(E) - 1); } else { E2 = E - (E + E*E*E/3 - M) / (1 + E*E); } iterations++; if (iterations > 100) { console.log('numerical instability'); return {}; } } orbit.lastE = E; let nu; if (e > 1) { nu = 2 * Math.atan(Math.sqrt((e+1) / (e-1)) * Math.tanh(E/2)); } else { nu = 2 * Math.atan(Math.sqrt((1+e) / (1-e)) * Math.tan(E/2)); } const p = a * (1 - e**2); const r = p / (1 + e * Math.cos(nu));// * ((a < 0) ? -1 : 1); const rd = e * Math.sqrt(mu / p) * Math.sin(nu); if (orbit.tf === undefined) { // FIXME: this is actually borken. :/ orbit.tf = [se3.rotz(Om), se3.rotx(i), se3.rotz(w)].reduce(se3.product); } const tf = se3.product(orbit.tf, se3.rotz(nu)); const pos = se3.apply(tf, [r, 0, 0, 1]); const vel = se3.apply(tf, [rd, Math.sqrt(p * mu) / r, 0, 1]); return { position: pos.slice(0, 3), velocity: vel.slice(0, 3), }; } function getOrientation(body, time) { return se3.rotxyz( body.spin[0] * time, body.spin[1] * time, body.spin[2] * time, ); } function makeOrbitObject(context, orbit, parentPosition) { const {gl} = context; const position = parentPosition; const glContext = context.orbitGlContext; const orientation = orbit.tf; // FIXME: currently borken. // const orientation = [ // se3.rotz(orbit.ascendingNodeLongitude), // se3.rotx(orbit.inclination), // se3.rotz(orbit.periapsisArgument), // ].reduce(se3.product); const buffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, buffer); const a = orbit.semimajorAxis; const b = a * Math.sqrt(1 - orbit.excentricity**2); const x = - orbit.semimajorAxis * orbit.excentricity; gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ x-a, -b, 0, -1, -1, x-a, +b, 0, -1, +1, x+a, -b, 0, +1, -1, x+a, +b, 0, +1, +1, ]), gl.STATIC_DRAW); const geometry = { glBuffer: buffer, numVertices: 4, delete: () => gl.deleteBuffer(buffer), }; return { geometry, orientation, position, glContext, }; } const kGravitationalConstant = 6.674e-11; function getObjects(context, body, parentPosition) { const objects = []; const {gl, glContext, player} = context; const {position, orientation} = body; if (body.glBuffer === undefined) { body.glBuffer = makeBufferFromFaces(gl, body.geometry); } objects.push({ geometry: body.glBuffer, orientation, position, glContext, }); if (parentPosition !== undefined) { const orbitObject = makeOrbitObject(context, body.orbit, parentPosition); objects.push(orbitObject); } else { const shipOrientation = [ se3.rotationOnly(player.tf), se3.rotationOnly(context.camera.tf), se3.rotxyz(-Math.PI / 2, 0, Math.PI / 2), ].reduce(se3.product); const shipPos = player.position; objects.push({ geometry: makeBufferFromFaces(gl, context.spaceship), orientation: shipOrientation, position: shipPos, glContext, }); } if (body.children !== undefined) { for (const child of body.children) { objects.push(...getObjects(context, child, position)); } } return objects; } function draw(context) { const {gl, camera, player, universe} = context; const objects = getObjects(context, universe); if (context.orbit !== undefined && context.orbit.excentricity < 1) { objects.push(makeOrbitObject(context, context.orbit, context.orbitBody.position)); } gl.clearColor(...context.skyColor, 1.0); gl.clearDepth(1.0); gl.enable(gl.DEPTH_TEST); gl.depthFunc(gl.LEQUAL); gl.enable(gl.CULL_FACE); gl.cullFace(gl.BACK); gl.enable(gl.BLEND); gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); const viewMatrix = se3.inverse([ context.player.tf, // player position & orientation context.camera.tf, // camera orientation relative to player se3.translation(0, 1, 4), // step back from the player ].reduce(se3.product)); let lastGlContext; for (const {position, orientation, geometry, glContext} of objects) { if (glContext !== lastGlContext) { glContext.setupScene({ projectionMatrix: context.projMatrix, viewMatrix, fogColor: context.skyColor, lightDirection: context.lightDirection, ambiantLightAmount: context.ambiantLight, }); } lastGlContext = glContext; glContext.drawObject({ position, orientation, glBuffer: geometry.glBuffer, numVertices: geometry.numVertices, }); } } function tick(time, context) { handleInput(context); const simTime = time * 0.001 + context.timeOffset; updatePhysics(simTime, context); const campos = context.player.position; // world generation / geometry update { // frame time is typically 16.7ms, so this may lag a bit let timeLeft = 10; const start = performance.now(); updateGeometry(context, timeLeft); } draw(context); const dt = (time - context.lastFrameTime) * 0.001; context.lastFrameTime = time; document.querySelector('#fps').textContent = `${1.0 / dt} fps`; requestAnimationFrame(time => tick(time, context)); } function makeCube(texture) { return [ makeFace('-x', texture, [-0.5, 0, 0]), makeFace('+x', texture, [+0.5, 0, 0]), makeFace('-y', texture, [0, -0.5, 0]), makeFace('+y', texture, [0, +0.5, 0]), makeFace('-z', texture, [0, 0, -0.5]), makeFace('+z', texture, [0, 0, +0.5]), ]; } function makeObjects(gl) { const texture = [0, 4]; const faces = [ makeFace('-x', texture, [-0.5, 0, 0]), makeFace('+x', texture, [+0.5, 0, 0]), makeFace('-y', texture, [0, -0.5, 0]), makeFace('+y', texture, [0, +0.5, 0]), makeFace('-z', texture, [0, 0, -0.5]), makeFace('+z', texture, [0, 0, +0.5]), ]; return [ { geometry: makeBufferFromFaces(gl, faces), orientation: [0, 0, 0], position: [0, 0, 0], }, ]; } async function main() { const canvas = document.querySelector('#game'); // adjust canvas aspect ratio to that of the screen canvas.height = screen.height / screen.width * canvas.width; const gl = canvas.getContext('webgl'); if (gl === null) { console.error('webgl not available') return; } // TODO // [ ] loading bar // [ ] spaceship // [ ] landing // [ ] huge planets // [ ] lighting const modelPromise = loadStlModel('spaceship.stl'); const context = { gl, projMatrix: se3.perspective(Math.PI / 3, canvas.clientWidth / canvas.clientHeight, 0.1, 10000.0), player: { tf: se3.translation(0.0, 0.0, 2.0), position: [0.0, 0.0, 2.0], velocity: [0, 0, 0], }, camera: { orientation: [0, 0, 0], tf: se3.identity(), }, keys: new Set(), lightDirection: [-0.2, -0.5, 0.4], skyColor: [0.10, 0.15, 0.2], ambiantLight: 0.4, blockSelectDistance: 8, flying: true, isOnGround: false, gravity: -17, jumpForce: 6.5, // objects: makeObjects(gl), universe: getSolarSystem(0), timeOffset: 0, }; context.glContext = await initWorldGl(gl); context.orbitGlContext = getOrbitDrawContext(gl); initUiListeners(canvas, context); // setupParamPanel(context); const starshipGeom = await modelPromise; console.log(`loaded ${starshipGeom.length} triangles`); context.spaceship = starshipGeom; requestAnimationFrame(time => tick(time, context)); } window.onload = main;