use glam::{Mat4, Vec3};
use winit::event::*;
pub struct Camera {
position: Vec3,
target: Vec3,
up: Vec3,
aspect: f32,
fovy: f32,
near: f32,
far: f32,
rotation: Vec3, distance: f32,
mouse_pressed: bool,
last_mouse_pos: (f32, f32),
}
impl Camera {
pub fn new(position: Vec3, target: Vec3, aspect: f32) -> Self {
let distance = (position - target).length();
let offset = position - target;
let yaw = offset.z.atan2(offset.x);
let pitch = (offset.y / distance).asin();
Self {
position,
target,
up: Vec3::Y,
aspect,
fovy: 45.0_f32.to_radians(),
near: 0.1,
far: 100.0,
rotation: Vec3::new(yaw, pitch, 0.0),
distance,
mouse_pressed: false,
last_mouse_pos: (0.0, 0.0),
}
}
pub fn view_matrix(&self) -> Mat4 {
Mat4::look_at_rh(self.position, self.target, self.up)
}
pub fn projection_matrix(&self) -> Mat4 {
Mat4::perspective_rh(self.fovy, self.aspect, self.near, self.far)
}
pub fn view_proj_matrix(&self) -> Mat4 {
self.projection_matrix() * self.view_matrix()
}
pub fn set_aspect(&mut self, aspect: f32) {
self.aspect = aspect;
}
pub fn handle_input(&mut self, event: &WindowEvent) -> bool {
match event {
WindowEvent::MouseInput {
state,
button: MouseButton::Left,
..
} => {
self.mouse_pressed = *state == ElementState::Pressed;
true
}
WindowEvent::CursorMoved { position, .. } => {
let pos = (position.x as f32, position.y as f32);
if self.mouse_pressed {
let delta_x = pos.0 - self.last_mouse_pos.0;
let delta_y = pos.1 - self.last_mouse_pos.1;
self.rotation.x -= delta_x * 0.005; self.rotation.y -= delta_y * 0.005;
let max_pitch = std::f32::consts::FRAC_PI_2 - 0.1; self.rotation.y = self.rotation.y.clamp(-max_pitch, max_pitch);
}
self.last_mouse_pos = pos;
self.mouse_pressed
}
WindowEvent::MouseWheel { delta, .. } => {
let scroll = match delta {
MouseScrollDelta::LineDelta(_, y) => *y,
MouseScrollDelta::PixelDelta(pos) => pos.y as f32 * 0.01,
};
self.distance -= scroll * 0.1 * self.distance;
self.distance = self.distance.clamp(0.5, 10.0);
true
}
_ => false,
}
}
pub fn update(&mut self) {
let yaw = self.rotation.x;
let pitch = self.rotation.y.clamp(
-std::f32::consts::FRAC_PI_2 + 0.1,
std::f32::consts::FRAC_PI_2 - 0.1,
);
let x = self.distance * yaw.cos() * pitch.cos();
let y = self.distance * pitch.sin();
let z = self.distance * yaw.sin() * pitch.cos();
self.position = self.target + Vec3::new(x, y, z);
}
pub fn uniform_data(&self) -> CameraUniform {
CameraUniform {
view_proj: self.view_proj_matrix().to_cols_array(),
view_pos: [self.position.x, self.position.y, self.position.z, 1.0],
}
}
}
#[repr(C)]
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct CameraUniform {
pub view_proj: [f32; 16],
pub view_pos: [f32; 4],
}
#[cfg(test)]
mod tests {
use super::*;
use glam::Vec3;
#[test]
fn test_camera_creation() {
let camera = Camera::new(Vec3::new(0.0, 0.0, 3.0), Vec3::ZERO, 16.0 / 9.0);
assert_eq!(camera.position, Vec3::new(0.0, 0.0, 3.0));
assert_eq!(camera.target, Vec3::ZERO);
assert_eq!(camera.aspect, 16.0 / 9.0);
}
#[test]
fn test_camera_view_matrix() {
let camera = Camera::new(Vec3::new(0.0, 0.0, 3.0), Vec3::ZERO, 1.0);
let view = camera.view_matrix();
assert!(!view.to_cols_array().iter().any(|&x| x.is_nan()));
}
#[test]
fn test_camera_projection_matrix() {
let camera = Camera::new(Vec3::new(0.0, 0.0, 3.0), Vec3::ZERO, 1.0);
let proj = camera.projection_matrix();
assert!(!proj.to_cols_array().iter().any(|&x| x.is_nan()));
}
#[test]
fn test_camera_uniform_data() {
let camera = Camera::new(Vec3::new(0.0, 0.0, 3.0), Vec3::ZERO, 1.0);
let uniform = camera.uniform_data();
assert!(!uniform.view_proj.iter().any(|&x| x.is_nan()));
assert!(!uniform.view_pos.iter().any(|&x| x.is_nan()));
}
#[test]
fn test_camera_set_aspect() {
let mut camera = Camera::new(Vec3::new(0.0, 0.0, 3.0), Vec3::ZERO, 1.0);
camera.set_aspect(16.0 / 9.0);
assert_eq!(camera.aspect, 16.0 / 9.0);
}
}