From 52c2e1dacdf915a02ca1f73fd58ef229a8cb2b65 Mon Sep 17 00:00:00 2001 From: Gulum Date: Wed, 4 Feb 2026 22:41:53 +0100 Subject: [PATCH] Add diffrent camera modes --- index.html | 40 ++++++++++--- src/Camera.ts | 13 ++++- src/FPSCamera.ts | 98 +++++++++++++++++++++++++++++++ src/ICamera.ts | 11 ++++ src/main.ts | 148 +++++++++++++++++++++++++++++++++++++++++++---- 5 files changed, 290 insertions(+), 20 deletions(-) create mode 100644 src/FPSCamera.ts create mode 100644 src/ICamera.ts diff --git a/index.html b/index.html index 63b4095..56a253b 100644 --- a/index.html +++ b/index.html @@ -358,17 +358,21 @@

🌊 Ocean Controls

- Camera Rotation:
- WASD or - Arrow Keys + Camera Mode: C (FPS/Orbital)
+ Current: FPS
- Zoom:
- Q / E or + / - + FPS Camera:
+ WASD Move
+ QE or SpaceCtrl Up/Down
+ Shift Sprint
+ Mouse: Look around
- Mouse: Click and drag to rotate + Orbital Camera:
+ WASD or Arrows Rotate
+ QE or +- Zoom
+ Mouse: Click and drag to rotate
Reset: R @@ -387,6 +391,7 @@ // Toggle controls visibility const controls = document.getElementById('controls'); const toggleBtn = document.getElementById('toggle-controls'); + const cameraModeDisplay = document.getElementById('current-camera-mode'); toggleBtn.addEventListener('click', () => { controls.classList.toggle('hidden'); @@ -396,6 +401,27 @@ if (evt.key === 'h' || evt.key === 'H') { controls.classList.toggle('hidden'); } + + // Update camera mode display when C is pressed + if (evt.key === 'c' || evt.key === 'C') { + setTimeout(() => { + // Get camera mode from any displayed element + const cameraMode = document.getElementById('camera-mode'); + if (cameraMode && cameraModeDisplay) { + const mode = cameraMode.textContent.replace('Camera: ', ''); + cameraModeDisplay.textContent = `Current: ${mode}`; + } + }, 100); + } + }); + + // Listen for custom camera mode toggle events from UI + window.addEventListener('toggleCameraMode', () => { + const cameraMode = document.getElementById('camera-mode'); + if (cameraMode && cameraModeDisplay) { + const mode = cameraMode.textContent.replace('Camera: ', ''); + cameraModeDisplay.textContent = `Current: ${mode}`; + } }); diff --git a/src/Camera.ts b/src/Camera.ts index 742d15e..722a56c 100644 --- a/src/Camera.ts +++ b/src/Camera.ts @@ -1,7 +1,8 @@ import { vec3, mat4, vec4 } from 'gl-matrix'; +import { ICamera } from './ICamera'; -/** A camera that always looks at the world origin. Can have an offset and be rotated. */ -export class Camera { +/** Orbital camera that rotates around the world origin. */ +export class OrbitalCamera implements ICamera { pos: vec3; target: vec3; up: vec3; @@ -64,4 +65,12 @@ export class Camera { mat4.lookAt(ret, this.pos, this.target, this.up); return ret; } + + /** Get view direction for LOD calculations */ + getViewDirection(): vec3 { + const dir = vec3.create(); + vec3.subtract(dir, this.target, this.pos); + vec3.normalize(dir, dir); + return dir; + } } diff --git a/src/FPSCamera.ts b/src/FPSCamera.ts new file mode 100644 index 0000000..ef62d81 --- /dev/null +++ b/src/FPSCamera.ts @@ -0,0 +1,98 @@ +import { vec3, mat4 } from 'gl-matrix'; +import { ICamera } from './ICamera'; + +/** FPS-style flight camera with free movement */ +export class FPSCamera implements ICamera { + pos: vec3; + target: vec3; + up: vec3; + + // FPS camera angles (in radians) + pitch: number; // Up/down rotation + yaw: number; // Left/right rotation + + // Direction vectors + forward: vec3; + right: vec3; + + constructor() { + this.pos = vec3.create(); + vec3.set(this.pos, 0.0, -3.0, 2.0); // Start above and behind origin + this.target = vec3.create(); + this.up = vec3.create(); + vec3.set(this.up, 0.0, 0.0, 1.0); // Z is up + this.forward = vec3.create(); + this.right = vec3.create(); + this.pitch = -0.3; // Looking slightly down + this.yaw = Math.PI / 2; // Looking toward +Y + this.updateVectors(); + } + + /** Rotate camera by mouse delta */ + rotate(deltaX: number, deltaY: number, sensitivity: number = 0.003): void { + this.yaw -= deltaX * sensitivity; + this.pitch -= deltaY * sensitivity; + + // Clamp pitch to avoid flipping + const maxPitch = Math.PI / 2 - 0.01; + this.pitch = Math.max(-maxPitch, Math.min(maxPitch, this.pitch)); + + this.updateVectors(); + } + + /** Move camera in the direction it's looking */ + moveForward(amount: number): void { + vec3.scaleAndAdd(this.pos, this.pos, this.forward, amount); + this.updateVectors(); + } + + moveRight(amount: number): void { + vec3.scaleAndAdd(this.pos, this.pos, this.right, amount); + this.updateVectors(); + } + + moveUp(amount: number): void { + // Move along world Z axis + this.pos[2] += amount; + this.updateVectors(); + } + + /** Move in the actual look direction (including vertical) */ + moveInLookDirection(amount: number): void { + vec3.scaleAndAdd(this.pos, this.pos, this.forward, amount); + this.updateVectors(); + } + + /** Update direction vectors from pitch/yaw */ + private updateVectors(): void { + // Calculate forward vector from pitch and yaw + // Z is up, so we use different axis mapping + this.forward[0] = Math.cos(this.pitch) * Math.cos(this.yaw); + this.forward[1] = Math.cos(this.pitch) * Math.sin(this.yaw); + this.forward[2] = Math.sin(this.pitch); + vec3.normalize(this.forward, this.forward); + + // Right vector is perpendicular to forward and world up + const worldUp = vec3.fromValues(0, 0, 1); + vec3.cross(this.right, this.forward, worldUp); + vec3.normalize(this.right, this.right); + + // Camera up is perpendicular to forward and right + vec3.cross(this.up, this.right, this.forward); + vec3.normalize(this.up, this.up); + + // Update target + vec3.add(this.target, this.pos, this.forward); + } + + getViewMatrix(): mat4 { + const ret: mat4 = mat4.create(); + mat4.lookAt(ret, this.pos, this.target, this.up); + return ret; + } + + /** Get view direction for LOD calculations */ + getViewDirection(): vec3 { + return vec3.clone(this.forward); + } +} diff --git a/src/ICamera.ts b/src/ICamera.ts new file mode 100644 index 0000000..7df9bfc --- /dev/null +++ b/src/ICamera.ts @@ -0,0 +1,11 @@ +import { vec3, mat4 } from 'gl-matrix'; + +/** Camera interface that both camera types implement */ +export interface ICamera { + pos: vec3; + target: vec3; + up: vec3; + + getViewMatrix(): mat4; + getViewDirection(): vec3; +} diff --git a/src/main.ts b/src/main.ts index da61d66..2de6b75 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,5 +1,7 @@ import { vec3, mat4 } from 'gl-matrix'; -import { Camera } from './Camera'; +import { ICamera } from './ICamera'; +import { OrbitalCamera } from './Camera'; +import { FPSCamera } from './FPSCamera'; import { Grid } from './Grid'; import { Skybox } from './Skybox'; import { createProgram } from './Shader'; @@ -142,11 +144,17 @@ var keyboardRotationY = 0; var keyboardZoom = 0; var keysPressed: Set = new Set(); /** Objects and states*/ -var camera: Camera; +var camera: ICamera; +var orbitalCamera: OrbitalCamera; +var fpsCamera: FPSCamera; var oceanGrid: Grid; var skybox: Skybox; var curRotX = Config.CAMERA_DEFAULT_ROT_X; var curRotY = Config.CAMERA_DEFAULT_ROT_Y; +/** Camera modes */ +var cameraMode: 'orbital' | 'fps' = 'fps'; +var moveSpeed = 0.15; +var fastMoveSpeed = 0.4; function drawScene() { fps++; let now = new Date(); @@ -208,9 +216,26 @@ function drawScene() { mat4.identity(projection); mat4.perspective(projection, Config.FOV, viewportWidth / viewportHeight, Config.NEAR_PLANE, Config.FAR_PLANE); //projection mode should actually be camera specific - camera.setOffset(Config.CAMERA_DEFAULT_OFFSET + keyboardZoom); - camera.setRotationX((curRotX += mouseYVel * Config.MOUSE_SENSITIVITY + keyboardRotationX)); - camera.setRotationY((curRotY += mouseXVel * Config.MOUSE_SENSITIVITY + keyboardRotationY)); + // Handle camera movement and rotation based on mode + if (cameraMode === 'fps') { + // FPS camera - direct movement + camera = fpsCamera; + handleFPSCameraMovement(); + + // Apply mouse rotation for FPS mode + if (mouseXVel !== 0 || mouseYVel !== 0) { + fpsCamera.rotate(mouseXVel, mouseYVel); + mouseXVel = 0; + mouseYVel = 0; + } + } else { + // Orbital camera - original behavior + camera = orbitalCamera; + orbitalCamera.setOffset(Config.CAMERA_DEFAULT_OFFSET + keyboardZoom); + orbitalCamera.setRotationX((curRotX += mouseYVel * Config.MOUSE_SENSITIVITY + keyboardRotationX)); + orbitalCamera.setRotationY((curRotY += mouseXVel * Config.MOUSE_SENSITIVITY + keyboardRotationY)); + } + var view = camera.getViewMatrix(); // Sun direction (matches the one in ocean shader) @@ -260,6 +285,41 @@ function drawScene() { requestAnimationFrame(drawScene); } +/** Handle FPS camera movement */ +function handleFPSCameraMovement() { + const speed = keysPressed.has('Shift') ? fastMoveSpeed : moveSpeed; + + // WASD for horizontal movement + if (keysPressed.has('w') || keysPressed.has('W')) { + fpsCamera.moveForward(speed); + } + if (keysPressed.has('s') || keysPressed.has('S')) { + fpsCamera.moveForward(-speed); + } + if (keysPressed.has('a') || keysPressed.has('A')) { + fpsCamera.moveRight(-speed); + } + if (keysPressed.has('d') || keysPressed.has('D')) { + fpsCamera.moveRight(speed); + } + + // Q/E for vertical movement + if (keysPressed.has('q') || keysPressed.has('Q')) { + fpsCamera.moveUp(-speed); + } + if (keysPressed.has('e') || keysPressed.has('E')) { + fpsCamera.moveUp(speed); + } + + // Space to go up, Ctrl to go down + if (keysPressed.has(' ')) { + fpsCamera.moveUp(speed); + } + if (keysPressed.has('Control')) { + fpsCamera.moveUp(-speed); + } +} + /** Handle keyboard input for camera controls */ function handleKeyboardInput() { keyboardRotationX = 0; @@ -328,25 +388,85 @@ function main() { // Keyboard controls window.addEventListener('keydown', (evt) => { keysPressed.add(evt.key); - handleKeyboardInput(); + + // Toggle camera mode with 'C' key + if (evt.key === 'c' || evt.key === 'C') { + cameraMode = cameraMode === 'fps' ? 'orbital' : 'fps'; + console.log(`Camera mode: ${cameraMode.toUpperCase()}`); + + // Update FPS display to show camera mode + if (fpsDisplay) { + const modeText = document.createElement('div'); + modeText.id = 'camera-mode'; + modeText.style.cssText = 'position: absolute; top: 40px; left: 10px; color: white; font-family: monospace;'; + modeText.textContent = `Camera: ${cameraMode.toUpperCase()}`; + + const existingMode = document.getElementById('camera-mode'); + if (existingMode) { + existingMode.textContent = `Camera: ${cameraMode.toUpperCase()}`; + } else { + document.body.appendChild(modeText); + } + } + } // Reset camera on 'R' key if (evt.key === 'r' || evt.key === 'R') { - curRotX = Config.CAMERA_DEFAULT_ROT_X; - curRotY = Config.CAMERA_DEFAULT_ROT_Y; - keyboardZoom = 0; + if (cameraMode === 'fps') { + fpsCamera = new FPSCamera(); // Reset to initial FPS position + camera = fpsCamera; + console.log('Camera reset to FPS default position'); + } else { + curRotX = Config.CAMERA_DEFAULT_ROT_X; + curRotY = Config.CAMERA_DEFAULT_ROT_Y; + keyboardZoom = 0; + console.log('Camera reset to orbital default position'); + } + } + + // Prevent default for space to avoid page scroll + if (evt.key === ' ' && cameraMode === 'fps') { + evt.preventDefault(); + } + + // Handle orbital camera keyboard input + if (cameraMode === 'orbital') { + handleKeyboardInput(); } }); window.addEventListener('keyup', (evt) => { keysPressed.delete(evt.key); - handleKeyboardInput(); + + if (cameraMode === 'orbital') { + handleKeyboardInput(); + } }); // Window resize handler window.addEventListener('resize', () => { updateCanvasSize(canvas); }); + + // Camera mode toggle from UI controls + window.addEventListener('toggleCameraMode', () => { + cameraMode = cameraMode === 'fps' ? 'orbital' : 'fps'; + camera = cameraMode === 'fps' ? fpsCamera : orbitalCamera; + console.log(`Camera mode switched to: ${cameraMode.toUpperCase()}`); + + // Update display + const modeText = document.createElement('div'); + modeText.id = 'camera-mode'; + modeText.style.cssText = 'position: absolute; top: 40px; left: 10px; color: white; font-family: monospace;'; + modeText.textContent = `Camera: ${cameraMode.toUpperCase()}`; + + const existingMode = document.getElementById('camera-mode'); + if (existingMode) { + existingMode.textContent = `Camera: ${cameraMode.toUpperCase()}`; + } else { + document.body.appendChild(modeText); + } + }); initShaders(); initGeometry(); @@ -358,7 +478,13 @@ function main() { skybox = new Skybox(); skybox.initVAO(gl); - camera = new Camera(); + // Initialize both cameras + orbitalCamera = new OrbitalCamera(); + fpsCamera = new FPSCamera(); + camera = fpsCamera; // Start with FPS camera + + console.log('Cameras initialized - Press C to toggle between FPS and Orbital modes'); + //Check if any errors apeared during init. if (gl.getError() != gl.NO_ERROR) { console.log("OpenGL Error!: ");