103 lines
3.8 KiB
Rust
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;
|
|
}
|
|
}
|
|
}
|