/* Logic for moving around with mouse and keyboard */ /* Helpers */ // Since with the mouse cursor we change the looking direction vertically or horizontally // it's significantly simpler to just use spherical coordinates and change the angles upon // mouse movement. // Since these are looking directions, the sphere radius is one (meaning the spherical coordinates // will always be converted into a cartesian unit vector). // The downside is that we have additional calculations to make, but it's worth it. SphericalCoord = function(theta, phi) { this.fulltheta = this.theta = theta; this.phi = phi; this.recalculateCartesian(); } SphericalCoord.prototype.recalculateCartesian = function() { return this.cartesian = [Math.sin(this.theta) * Math.sin(this.phi), Math.cos(this.theta), Math.sin(this.theta) * Math.cos(this.phi)]; } SphericalCoord.prototype.addAngles = function(theta, phi) { // I figured this out on my own :) // Theta is in the range [0, pi), however in a sphere we have 2pi degrees. // We keep a "full" theta, which is in the range [0, 2pi), and then adjust normal theta // accordingly. With just normal theta, after a positive value is passed (for example) // we wouldn't know in which direction to rotate (or more accurately, we would always // increase theta so it goes the the "south" pole, which is bad, since increasing the // value should just continue rotating the point, like with phi). // When you "overextend" past pi, so when you need to reach the other hemisphere, between // pi and 2pi, you need to rotate phi by 180 degrees (2pi), so theta is acting on the other // hemisphere, and then we need to remove from 2pi the "leftover" amount, since our theta // would signify the "reverse" angle than the one we want. var before = this.fulltheta <= Math.PI; this.fulltheta = (this.fulltheta + theta) % (2 * Math.PI); this.theta = (this.fulltheta <= Math.PI) ? this.fulltheta : -this.fulltheta; // We check if fulltheta passed through the 180 degree/pi border if (before && this.fulltheta > Math.PI) { this.phi = (this.phi + Math.PI) % (2 * Math.PI); } else if (!before && this.fulltheta <= Math.PI) { this.phi = (this.phi - Math.PI) % (2 * Math.PI); } this.phi = (this.phi + phi) % (2 * Math.PI); this.recalculateCartesian(); return this; } SphericalCoord.prototype.toCartesian = function() { return this.cartesian; } function vecSum(a, b) { return [a[0]+b[0], a[1]+b[1], a[2]+b[2]]; } function vecSub(a, b) { return [a[0]-b[0], a[1]-b[1], a[2]-b[2]]; } /* Variables */ const MOVE_SPEED = 1.5; const ROTATION_SPEED = 3; var pos = [1000, 0, 650]; function initCameraVectors() { forward = new SphericalCoord(Math.PI / 2, 0); up = new SphericalCoord(0, 0); // this vector can be calculated from forward and up, but this way we improve speed left = new SphericalCoord(Math.PI / 2, Math.PI / 2); } initCameraVectors(); var isPaused = true; /* Navigation keys */ // Since I'm lazy, there is a different function, changing pos, for every key // so when a key is pressed, we'll execute the action on the same index var pressedKeys = [ false, false, false, false, false, false ]; var keyAction = [ () => { pos = vecSum(pos, up.toCartesian()); }, // ' ' () => { pos = vecSub(pos, up.toCartesian()); }, // 'Shift' () => { pos = vecSum(pos, forward.toCartesian()); }, // 'w' () => { pos = vecSub(pos, forward.toCartesian()); }, // 's' () => { pos = vecSum(pos, left.toCartesian()); }, // 'a' () => { pos = vecSub(pos, left.toCartesian()); }, // 'd' ]; document.addEventListener('keydown', (event) => { if (isPaused) return; switch (event.key) { case ' ' : pressedKeys[0] = true; break; case 'Shift': pressedKeys[1] = true; break; case 'w' : pressedKeys[2] = true; break; case 's' : pressedKeys[3] = true; break; case 'a' : pressedKeys[4] = true; break; case 'd' : pressedKeys[5] = true; break; } }); document.addEventListener('keyup', (event) => { if (isPaused) return; switch (event.key) { case ' ' : pressedKeys[0] = false; break; case 'Shift': pressedKeys[1] = false; break; case 'w' : pressedKeys[2] = false; break; case 's' : pressedKeys[3] = false; break; case 'a' : pressedKeys[4] = false; break; case 'd' : pressedKeys[5] = false; break; } }); /* Mouse movement */ var onlyHorizontal = false; var onlyVertical = false; function updateLookingDirection(e) { if (isPaused) return; // Get mouse movement var vertical = (1-onlyHorizontal) * (e.movementY / canvas.offsetHeight) * ROTATION_SPEED; var horizontal = (1-onlyVertical) * (-e.movementX / canvas.offsetWidth) * ROTATION_SPEED; forward.addAngles(vertical, horizontal); up.addAngles(vertical, horizontal); left.addAngles(vertical, horizontal); } document.addEventListener('mousemove', updateLookingDirection); document.addEventListener('pointerlockchange', () => { isPaused = document.pointerLockElement !== canvas; menu.classList.toggle('hide'); }); function mouseInit() { // Capture mouse pointer when appropriate paused = document.getElementById('paused'); canvas.addEventListener('click', async () => { if (isPaused) await canvas.requestPointerLock(); }); paused.addEventListener('click', async () => { if (isPaused) await canvas.requestPointerLock(); }); document.addEventListener('keyup', (e) => { if (e.key === 'Escape') paused.click(); }); } /* On every drawFrame */ coordsElem = undefined; function updateCamera() { // Move camera position for (var i = 0; i < pressedKeys.length; i++) if (pressedKeys[i]) keyAction[i](); lookAt(pos, vecSum(pos, forward.toCartesian()), up.toCartesian()); // Update "external" variables if (coordsElem) coordsElem.innerText = 'X: '+pos[0].toFixed(2)+' Y: '+pos[1].toFixed(2)+' Z: '+pos[2].toFixed(2); gl.uniform3fv(uEye, pos); }