feat(renderer): add Camera and FpsController
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
143
crates/voltex_renderer/src/camera.rs
Normal file
143
crates/voltex_renderer/src/camera.rs
Normal file
@@ -0,0 +1,143 @@
|
||||
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()
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user