//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'; 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, }; } 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; return false; case 'Space': if (!context.flying) { if (context.jumpAmount > 0) { const amount = context.jumpForce; context.camera.velocity[1] = amount; context.jumpAmount -= 1; } } return false; } } else { context.keys.delete(e.code); } }; const moveListener = e => { context.camera.orientation[1] -= e.movementX / 500; context.camera.orientation[0] -= e.movementY / 500; context.camera.orientation[0] = Math.min(Math.max( context.camera.orientation[0], -Math.PI / 2 ), Math.PI / 2); }; 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) => { const dir = [right, 0, -forward, 1.0]; const ori = se3.roty(context.camera.orientation[1]); const tf = se3.apply(ori, dir); const maxSpeed = 8; const airMovement = 0.08; if (context.flying) { context.camera.position[0] += tf[0] / 60; context.camera.position[2] += tf[2] / 60; } if (context.isOnGround) { context.camera.velocity[0] = tf[0]; context.camera.velocity[2] = tf[2]; } else { const vel = context.camera.velocity; vel[0] += tf[0] * airMovement; vel[2] += tf[2] * airMovement; const curVel = Math.sqrt(vel[0] * vel[0] + vel[2] * vel[2]); if (curVel > maxSpeed) { vel[0] *= maxSpeed / curVel; vel[2] *= maxSpeed / curVel; } } }; context.keys.forEach(key => { switch (key) { case 'KeyW': move(8, 0.0); return; case 'KeyA': move(0.0, -8); return; case 'KeyS': move(-8, 0.0); return; case 'KeyD': move(0.0, 8); return; case 'Space': if (context.flying) { context.camera.position[1] += 8 / 60; } return; case 'ShiftLeft': context.camera.position[1] -= 8 / 60; return; } }); } function updatePhysics(time, context) { } function updateGeometry(context, timeout_ms = 10) { } function normalizeAngle(theta) { const twopi = 2 * Math.PI; return theta - twopi * Math.floor((theta + twopi) / twopi); } /** Let's be honest I should clean this up. * Right now it mostly only works with elliptical orbits (e < 1), * which is fine for planets & stuff, but not for my spacecraft (or comets) * * 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?). */ function getCartesianPosition(orbit, mu, time) { const { excentricity: e, semimajorAxis: a, inclination: i, ascendingNodeLongitude: Om, periapsisArgument: w, } = orbit; const n = Math.sqrt(mu/(a**3)); // mean motion const M = n * time; // mean anomaly // const nu = ( // M // + (2 * e - e**3 / 4) * Math.sin(M) // + 5/4 * e**2 * Math.sin(2*M) // + 13/12 * e**3 * Math.sin(3*M) // ); // true anomaly, doesn't work :( // Newton's method var E2; var E = M; for (var j = 1; j < 20; ++j) { if (e < 0.001) { break; } if (e < 1) { E2 = E - (E - e * Math.sin(E) - M) / (1 - e * Math.cos(E)); } else if (e > 1) { E2 = E - (-E + e * sinh(E) - M) / (e * cosh(E) - 1); } else { E2 = E - (E + E*E*E/3 - M) / (1 + E*E); } if (Math.abs(E - E2) < 1e-10) { break; } E = E2; } const nu = 2 * Math.atan(Math.sqrt((1+e) / (1-e)) * Math.tan(E/2)); const r = a * (1 - e**2) / (1 + e * Math.cos(nu)); const cOm = Math.cos(Om); const sOm = Math.sin(Om); const cwnu = Math.cos(w + nu); const swnu = Math.sin(w + nu); const x = r * Math.cos(i) * (cOm * cwnu - sOm * swnu); const y = r * (sOm * cwnu + cOm * swnu); const z = r * Math.sin(i) * cwnu; return [x, y, z]; } 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 = [ se3.rotz(orbit.ascendingNodeLongitude), se3.roty(-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, }; } function getObjects(context, body, time, parentBody, parentPosition) { const kGravitationalConstant = 6.674e-11; const objects = []; const {gl, glContext} = context; let position = [0, 0, 0]; if (parentBody !== undefined) { // const mu = kGravitationalConstant * parentBody.mass; const mu = 10; const coord = getCartesianPosition(body.orbit, mu, time); position = [ parentPosition[0] + coord[0], parentPosition[1] + coord[1], parentPosition[2] + coord[2], ]; objects.push(makeOrbitObject(context, body.orbit, parentPosition)); } objects.push({ geometry: makeBufferFromFaces(gl, body.geometry), orientation: getOrientation(body, time), position, glContext, }); if (body.children !== undefined) { for (const child of body.children) { objects.push(...getObjects(context, child, time, body, position)); } } return objects; } function draw(context, time) { const {gl, camera, universe} = context; const objects = getObjects(context, universe, time * 0.001); 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 camrot = camera.orientation; const campos = camera.position; const viewMatrix = se3.product( se3.rotxyz(-camrot[0], -camrot[1], -camrot[2]), se3.translation(-campos[0], -campos[1], -campos[2]) ); 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); updatePhysics(time, context); const campos = context.camera.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, time); 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], }, ]; } function makeSolarSystem(gl) { return { mass: 1.0, spin: [0, 1.0, 0], geometry: makeCube([0, 4]), children: [ { mass: 0.1, spin: [0.2, 0.0, 0.0], geometry: makeCube([0, 8]), orbit: { excentricity: 0, semimajorAxis: 3, inclination: 0, ascendingNodeLongitude: 0, periapsisArgument: 0, trueAnomaly: 0, }, }, { mass: 0.1, spin: [0.2, 0.0, 0.0], geometry: makeCube([0, 1]), orbit: { excentricity: 0.8, semimajorAxis: 5, inclination: 0, ascendingNodeLongitude: 0, periapsisArgument: 0, trueAnomaly: 0, }, }, { mass: 0.1, spin: [0.0, 0.0, 1.0], geometry: makeCube([9, 9]), orbit: { excentricity: 0.3, semimajorAxis: 5, inclination: 1.0, ascendingNodeLongitude: 0, periapsisArgument: 0, trueAnomaly: 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; } const context = { gl, projMatrix: se3.perspective(Math.PI / 3, canvas.clientWidth / canvas.clientHeight, 0.1, 100.0), camera: { position: [0.0, 0.0, 2.0], orientation: [0.0, 0.0, 0.0], velocity: [0, 0, 0], }, keys: new Set(), lightDirection: [-0.2, -0.5, 0.4], skyColor: [0.10, 0.15, 0.2], ambiantLight: 0.7, blockSelectDistance: 8, flying: true, isOnGround: false, gravity: -17, jumpForce: 6.5, // objects: makeObjects(gl), universe: makeSolarSystem(gl), }; context.glContext = await initWorldGl(gl); context.orbitGlContext = getOrbitDrawContext(gl); initUiListeners(canvas, context); // setupParamPanel(context); requestAnimationFrame(time => tick(time, context)); } window.onload = main;