use glam::{Mat4, Quat, Vec3}; use std::f32::consts::LN_2; use std::time::Instant; use winit::event::{ElementState, VirtualKeyCode}; #[derive(Clone, Debug)] pub struct Camera { pub eye: [f32; 4], pub vp: [[f32; 4]; 4], } #[derive(Debug)] pub struct Flycam { // input // currently held keys is_world_up_pressed: bool, is_world_down_pressed: bool, is_cam_up_pressed: bool, is_cam_down_pressed: bool, is_forward_pressed: bool, is_backward_pressed: bool, is_left_pressed: bool, is_right_pressed: bool, // accumulated mouse movement yet to be processed mouse_dx: f32, mouse_dy: f32, // state // timestamp last_update: Instant, // camera orientation euler_x: f32, euler_y: f32, // camera movement state velocity: Vec3, position: Vec3, // constants // camera movement turn_sensitivity: f32, // coefficient for mouse_dx/dy -> euler_x/y thrust_mag: f32, // coefficient for thrust acceleration vector damping_coeff: f32, // coefficient for damping acceleration vector // camera frustum aspect: f32, fovy: f32, znear: f32, zfar: f32, } impl Flycam { /// thrust_speed: top speed when using a single thruster, in units/second /// duration to halve difference between current and target velocity, in seconds pub fn new(turn_sensitivity: f32, thrust_speed: f32, damper_half_life: f32) -> Self { Self { is_world_up_pressed: false, is_world_down_pressed: false, is_cam_up_pressed: false, is_cam_down_pressed: false, is_forward_pressed: false, is_backward_pressed: false, is_left_pressed: false, is_right_pressed: false, mouse_dx: 0.0, mouse_dy: 0.0, last_update: Instant::now(), euler_x: 0.0, euler_y: 0.0, velocity: Vec3::new(0.0, 0.0, 0.0), position: Vec3::new(0.0, 0.5, 1.0), turn_sensitivity, thrust_mag: thrust_speed / damper_half_life * LN_2, damping_coeff: LN_2 / damper_half_life, aspect: 1.0, // TODO compute from size fovy: std::f32::consts::FRAC_PI_2, znear: 0.01, zfar: 100.0, } } } impl Flycam { /// update stored keyboard state for use in update() pub fn process_keyboard(&mut self, key: VirtualKeyCode, state: ElementState) { let is_pressed = state == ElementState::Pressed; match key { VirtualKeyCode::Space => { self.is_world_up_pressed = is_pressed; } VirtualKeyCode::LShift => { self.is_world_down_pressed = is_pressed; } VirtualKeyCode::Q => { self.is_cam_down_pressed = is_pressed; } VirtualKeyCode::E => { self.is_cam_up_pressed = is_pressed; } VirtualKeyCode::W | VirtualKeyCode::Up => { self.is_forward_pressed = is_pressed; } VirtualKeyCode::A | VirtualKeyCode::Left => { self.is_left_pressed = is_pressed; } VirtualKeyCode::S | VirtualKeyCode::Down => { self.is_backward_pressed = is_pressed; } VirtualKeyCode::D | VirtualKeyCode::Right => { self.is_right_pressed = is_pressed; } _ => {} } } /// update accumulated mouse movement for use in update() pub fn process_mouse(&mut self, mouse_dx: f64, mouse_dy: f64) { self.mouse_dx += mouse_dx as f32; self.mouse_dy += mouse_dy as f32; } pub fn resize(&mut self, width: u32, height: u32) { self.aspect = (width as f32) / (height as f32); } /// disable all key presses pub fn defocus(&mut self) { self.is_world_down_pressed = false; self.is_world_up_pressed = false; self.is_cam_down_pressed = false; self.is_cam_up_pressed = false; self.is_forward_pressed = false; self.is_left_pressed = false; self.is_backward_pressed = false; self.is_right_pressed = false; } /// apply input and update camera movement pub fn update(&mut self) { let dt = self.last_update.elapsed(); self.last_update = Instant::now(); let dt = dt.as_micros() as f32 / 1_000_000.0; self.update_orientation(dt); self.update_kinematic(dt); } fn update_orientation(&mut self, _dt: f32) { let t = self.turn_sensitivity; self.euler_x -= t * self.mouse_dy; // mouse +y = 2D plane down = look down = 3d space -x self.euler_y -= t * self.mouse_dx; // mouse +x = 2D plane right = look to the right = 3d space -y self.mouse_dx = 0.0; self.mouse_dy = 0.0; // Clamp euler_x to [-pi/2, pi/2] let euler_x_limit = std::f32::consts::FRAC_PI_2; if self.euler_x < -euler_x_limit { self.euler_x = -euler_x_limit; } else if self.euler_x > euler_x_limit { self.euler_x = euler_x_limit; } } /// update velocity and position from acceleration using forward differences fn update_kinematic(&mut self, dt: f32) { let net_acc = self.get_thrust_acc() + self.get_damping_acc(); let delta_vel = net_acc * dt; self.velocity += delta_vel; let delta_pos = self.velocity * dt; self.position += delta_pos; } /// use keyboard key pairs to trigger directional thrusters in camera and world coordinates /// thrust_speed is the max speed (under drag) with a single thruster, but combinations can /// produce higher speeds (e.g. forward and right, camera down and world down) fn get_thrust_acc(&self) -> glam::Vec3 { let axis = Self::key_axis; let thruster_cam_x = axis(self.is_left_pressed, self.is_right_pressed); let thruster_cam_y = axis(self.is_cam_down_pressed, self.is_cam_up_pressed); let thruster_cam_z = -axis(self.is_backward_pressed, self.is_forward_pressed); // forward is -z let thruster_world_y = axis(self.is_world_down_pressed, self.is_world_up_pressed); let thrusters_cam = Vec3::new(thruster_cam_x, thruster_cam_y, thruster_cam_z); let thrusters_world = Vec3::new(0.0, thruster_world_y, 0.0); let cam_to_world = self.get_orientation(); let thrusters_total = thrusters_world + cam_to_world * thrusters_cam; self.thrust_mag * thrusters_total } /// calculate a damping force (proportional to velocity) /// the damping coefficient is calculated in the constructor, which is parameterized in terms /// of more physically meaningful values fn get_damping_acc(&self) -> glam::Vec3 { self.damping_coeff * -self.velocity } /// a helper function to turn a pair of key states into a sign for thruster direction fn key_axis(negative: bool, positive: bool) -> f32 { if negative { if positive { 0.0 // positive + negative cancel out } else { -1.0 // negative only } } else { if positive { 1.0 // positive only } else { 0.0 // neutral } } } /// the current camera orientation, which can be seen as a rotation (in quaternion form) from /// camera axes to world axes /// glam's YXZ ordering matches the standard roll-pitch-yaw Euler angles fn get_orientation(&self) -> glam::Quat { Quat::from_euler(glam::EulerRot::YXZ, self.euler_y, self.euler_x, 0.0) } pub fn get_camera(&self) -> Camera { Camera { eye: self.get_eye(), vp: self.get_vp(), } } pub fn get_eye(&self) -> [f32; 4] { self.position.extend(0.0).to_array() } pub fn get_vp(&self) -> [[f32; 4]; 4] { // view matrix is inverted camera pose (world space to camera space) let rotation = Mat4::from_quat(self.get_orientation().inverse()); let translation = Mat4::from_translation(-self.position); let view = rotation * translation; // perspective projection let proj = Mat4::perspective_rh_gl(self.fovy, self.aspect, self.znear, self.zfar); let vp = proj * view; vp.to_cols_array_2d() } }