// SPDX-License-Identifier: AGPL-3.0-only //! Free-fly camera used to observe the world. //! //! The camera stores a world-space position and an orientation expressed as yaw and pitch //! angles. A view matrix is derived on demand from these values, and the orientation and //! position are advanced each frame from accumulated keyboard and mouse input. use glam::{Mat4, Vec3}; use crate::InputState; /// A free-flying camera driven by keyboard and mouse input. pub struct Camera { /// World-space position of the camera eye, measured in blocks. pub position: Vec3, /// Rotation about the world up axis (+Y), in radians. Controls left/right look. pub yaw: f32, /// Rotation above or below the horizon, in radians. Controls up/down look. pub pitch: f32, /// Translation speed applied to movement input, in blocks per second. pub speed: f32, /// Factor converting a unit of raw mouse motion into radians of rotation. pub sensitivity: f32, } impl Camera { /// Maximum absolute pitch, held just under vertical to avoid the view flipping over. const PITCH_LIMIT: f32 = 1.553; // ~89 degrees expressed in radians. /// Creates a camera at `position` facing the direction given by `yaw` and `pitch`. #[must_use] pub fn new(position: Vec3, yaw: f32, pitch: f32) -> Self { Self { position, yaw, pitch, speed: 20.0, sensitivity: 0.0025, } } /// Returns the normalised world-space direction the camera currently faces. #[must_use] pub fn forward(&self) -> Vec3 { // Spherical-to-Cartesian conversion: yaw sweeps around +Y, pitch tilts up and down. Vec3::new( self.yaw.cos() * self.pitch.cos(), self.pitch.sin(), self.yaw.sin() * self.pitch.cos(), ) .normalize() } /// Builds the right-handed view matrix for the current position and orientation. #[must_use] pub fn view_matrix(&self) -> Mat4 { Mat4::look_at_rh(self.position, self.position + self.forward(), Vec3::Y) } /// Advances the camera by a single frame, applying `input` accumulated over `dt` seconds. pub fn update(&mut self, input: &InputState, dt: f32) { // Apply accumulated mouse motion to the orientation. A downward mouse delta (positive y) lowers the pitch, so the vertical term is subtracted. #[expect(clippy::cast_possible_truncation)] { self.yaw += input.mouse_delta.0 as f32 * self.sensitivity; self.pitch -= input.mouse_delta.1 as f32 * self.sensitivity; } self.pitch = self.pitch.clamp(-Self::PITCH_LIMIT, Self::PITCH_LIMIT); // Derive the movement basis from the current facing. The right vector is horizontal because it is the cross product of the facing direction with the world up axis. let forward = self.forward(); let right = forward.cross(Vec3::Y).normalize(); // Accumulate a movement direction from the currently held keys. let mut direction = Vec3::ZERO; if input.forward { direction += forward; } if input.backward { direction -= forward; } if input.right { direction += right; } if input.left { direction -= right; } if input.up { direction += Vec3::Y; } if input.down { direction -= Vec3::Y; } // Normalising keeps diagonal movement the same speed as axis-aligned movement. The guard avoids normalising a zero vector, which would produce NaN when idle. if direction.length_squared() > 0.0 { self.position += direction.normalize() * self.speed * dt; } } }