synvael/crates/client/src/camera.rs

103 lines
3.8 KiB
Rust

// 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 {
glam::camera::rh::view::look_at_mat4(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,
reason = "mouse deltas are small; f32 precision is sufficient for camera input"
)]
{
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;
}
}
}