144 lines
3.9 KiB
Rust
144 lines
3.9 KiB
Rust
use voltex_math::{Vec3, Mat4};
|
|
|
|
pub struct Camera {
|
|
pub position: Vec3,
|
|
pub yaw: f32, // radians, Y-axis rotation
|
|
pub pitch: f32, // radians, X-axis rotation
|
|
pub fov_y: f32, // radians
|
|
pub aspect: f32,
|
|
pub near: f32,
|
|
pub far: f32,
|
|
}
|
|
|
|
impl Camera {
|
|
pub fn new(position: Vec3, aspect: f32) -> Self {
|
|
Self {
|
|
position,
|
|
yaw: 0.0,
|
|
pitch: 0.0,
|
|
fov_y: std::f32::consts::FRAC_PI_4, // 45 degrees
|
|
aspect,
|
|
near: 0.1,
|
|
far: 100.0,
|
|
}
|
|
}
|
|
|
|
pub fn forward(&self) -> Vec3 {
|
|
Vec3::new(
|
|
self.yaw.sin() * self.pitch.cos(),
|
|
self.pitch.sin(),
|
|
-self.yaw.cos() * self.pitch.cos(),
|
|
)
|
|
}
|
|
|
|
pub fn right(&self) -> Vec3 {
|
|
self.forward().cross(Vec3::Y).normalize()
|
|
}
|
|
|
|
pub fn view_matrix(&self) -> Mat4 {
|
|
let target = self.position + self.forward();
|
|
Mat4::look_at(self.position, target, Vec3::Y)
|
|
}
|
|
|
|
pub fn projection_matrix(&self) -> Mat4 {
|
|
Mat4::perspective(self.fov_y, self.aspect, self.near, self.far)
|
|
}
|
|
|
|
pub fn view_projection(&self) -> Mat4 {
|
|
self.projection_matrix() * self.view_matrix()
|
|
}
|
|
}
|
|
|
|
pub struct FpsController {
|
|
pub speed: f32,
|
|
pub mouse_sensitivity: f32,
|
|
}
|
|
|
|
impl FpsController {
|
|
pub fn new() -> Self {
|
|
Self { speed: 5.0, mouse_sensitivity: 0.003 }
|
|
}
|
|
|
|
pub fn process_movement(
|
|
&self, camera: &mut Camera,
|
|
forward: f32, right: f32, up: f32, dt: f32,
|
|
) {
|
|
let cam_forward = camera.forward();
|
|
let cam_right = camera.right();
|
|
let velocity = self.speed * dt;
|
|
camera.position = camera.position
|
|
+ cam_forward * (forward * velocity)
|
|
+ cam_right * (right * velocity)
|
|
+ Vec3::Y * (up * velocity);
|
|
}
|
|
|
|
pub fn process_mouse(&self, camera: &mut Camera, dx: f64, dy: f64) {
|
|
camera.yaw += dx as f32 * self.mouse_sensitivity;
|
|
camera.pitch -= dy as f32 * self.mouse_sensitivity;
|
|
let limit = 89.0_f32.to_radians();
|
|
camera.pitch = camera.pitch.clamp(-limit, limit);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::f32::consts::PI;
|
|
|
|
fn approx_eq(a: f32, b: f32) -> bool {
|
|
(a - b).abs() < 1e-5
|
|
}
|
|
|
|
fn vec3_approx_eq(a: Vec3, b: Vec3) -> bool {
|
|
approx_eq(a.x, b.x) && approx_eq(a.y, b.y) && approx_eq(a.z, b.z)
|
|
}
|
|
|
|
#[test]
|
|
fn test_camera_default_forward() {
|
|
let cam = Camera::new(Vec3::new(0.0, 0.0, 0.0), 1.0);
|
|
// yaw=0, pitch=0 → forward ≈ (0, 0, -1)
|
|
let fwd = cam.forward();
|
|
assert!(
|
|
vec3_approx_eq(fwd, Vec3::new(0.0, 0.0, -1.0)),
|
|
"Expected (0, 0, -1), got ({}, {}, {})",
|
|
fwd.x, fwd.y, fwd.z
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_camera_yaw_90() {
|
|
let mut cam = Camera::new(Vec3::new(0.0, 0.0, 0.0), 1.0);
|
|
cam.yaw = PI / 2.0;
|
|
// yaw=PI/2 → forward ≈ (1, 0, 0)
|
|
let fwd = cam.forward();
|
|
assert!(
|
|
vec3_approx_eq(fwd, Vec3::new(1.0, 0.0, 0.0)),
|
|
"Expected (1, 0, 0), got ({}, {}, {})",
|
|
fwd.x, fwd.y, fwd.z
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_fps_pitch_clamp() {
|
|
let controller = FpsController::new();
|
|
let mut cam = Camera::new(Vec3::new(0.0, 0.0, 0.0), 1.0);
|
|
let limit = 89.0_f32.to_radians();
|
|
|
|
// Extreme upward mouse movement
|
|
controller.process_mouse(&mut cam, 0.0, -1_000_000.0);
|
|
assert!(
|
|
cam.pitch <= limit + 1e-5,
|
|
"Pitch should be clamped to +89°, got {}",
|
|
cam.pitch.to_degrees()
|
|
);
|
|
|
|
// Extreme downward mouse movement
|
|
controller.process_mouse(&mut cam, 0.0, 1_000_000.0);
|
|
assert!(
|
|
cam.pitch >= -limit - 1e-5,
|
|
"Pitch should be clamped to -89°, got {}",
|
|
cam.pitch.to_degrees()
|
|
);
|
|
}
|
|
}
|