Merge pull request 'Flycam rewrite' (#7) from turtle1331/cyborg:flycam-rewrite into main

Reviewed-on: #7
This commit is contained in:
mars 2022-03-18 05:26:23 +00:00
commit 607730fb83
2 changed files with 116 additions and 50 deletions

View File

@ -1,6 +1,7 @@
use glam::{Mat4, Quat, Vec2, Vec3}; use glam::{Mat4, Quat, Vec2, Vec3};
use std::time::Instant; use std::time::Instant;
use winit::event::{ElementState, VirtualKeyCode}; use winit::event::{ElementState, VirtualKeyCode};
use std::f32::consts::LN_2;
pub trait Camera { pub trait Camera {
fn get_eye(&self) -> [f32; 4]; fn get_eye(&self) -> [f32; 4];
@ -9,22 +10,35 @@ pub trait Camera {
pub struct Flycam { pub struct Flycam {
// input // input
is_up_pressed: bool, // currently held keys
is_down_pressed: bool, 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_forward_pressed: bool,
is_backward_pressed: bool, is_backward_pressed: bool,
is_left_pressed: bool, is_left_pressed: bool,
is_right_pressed: bool, is_right_pressed: bool,
// accumulated mouse movement yet to be processed
mouse_dx: f32, mouse_dx: f32,
mouse_dy: f32, mouse_dy: f32,
// state // state
// timestamp
last_update: Instant, last_update: Instant,
pan: f32, // camera orientation
tilt: f32, euler_x: f32,
euler_y: f32,
// camera movement state
velocity: Vec3,
position: Vec3, position: Vec3,
// constants // constants
speed: f32, // camera movement
turn_speed: f32, 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, aspect: f32,
fovy: f32, fovy: f32,
znear: f32, znear: f32,
@ -32,10 +46,14 @@ pub struct Flycam {
} }
impl Flycam { impl Flycam {
pub fn new(speed: f32, turn_speed: f32) -> Self { /// 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 { Self {
is_up_pressed: false, is_world_up_pressed: false,
is_down_pressed: false, is_world_down_pressed: false,
is_cam_up_pressed: false,
is_cam_down_pressed: false,
is_forward_pressed: false, is_forward_pressed: false,
is_backward_pressed: false, is_backward_pressed: false,
is_left_pressed: false, is_left_pressed: false,
@ -43,11 +61,13 @@ impl Flycam {
mouse_dx: 0.0, mouse_dx: 0.0,
mouse_dy: 0.0, mouse_dy: 0.0,
last_update: Instant::now(), last_update: Instant::now(),
pan: 0.0, euler_x: 0.0,
tilt: 0.0, euler_y: 0.0,
velocity: Vec3::new(0.0, 0.0, 0.0),
position: Vec3::new(0.0, 0.5, 1.0), position: Vec3::new(0.0, 0.5, 1.0),
speed, turn_sensitivity,
turn_speed, thrust_mag: thrust_speed / damper_half_life * LN_2,
damping_coeff: LN_2 / damper_half_life,
aspect: 1.0, // TODO compute from size aspect: 1.0, // TODO compute from size
fovy: std::f32::consts::FRAC_PI_2, fovy: std::f32::consts::FRAC_PI_2,
znear: 0.01, znear: 0.01,
@ -57,14 +77,21 @@ impl Flycam {
} }
impl Flycam { impl Flycam {
/// update stored keyboard state for use in update()
pub fn process_keyboard(&mut self, key: VirtualKeyCode, state: ElementState) { pub fn process_keyboard(&mut self, key: VirtualKeyCode, state: ElementState) {
let is_pressed = state == ElementState::Pressed; let is_pressed = state == ElementState::Pressed;
match key { match key {
VirtualKeyCode::Space => { VirtualKeyCode::Space => {
self.is_up_pressed = is_pressed; self.is_world_up_pressed = is_pressed;
} }
VirtualKeyCode::LShift => { VirtualKeyCode::LShift => {
self.is_down_pressed = is_pressed; 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 => { VirtualKeyCode::W | VirtualKeyCode::Up => {
self.is_forward_pressed = is_pressed; self.is_forward_pressed = is_pressed;
@ -82,6 +109,7 @@ impl Flycam {
} }
} }
/// update accumulated mouse movement for use in update()
pub fn process_mouse(&mut self, mouse_dx: f64, mouse_dy: f64) { pub fn process_mouse(&mut self, mouse_dx: f64, mouse_dy: f64) {
self.mouse_dx += mouse_dx as f32; self.mouse_dx += mouse_dx as f32;
self.mouse_dy += mouse_dy as f32; self.mouse_dy += mouse_dy as f32;
@ -91,60 +119,94 @@ impl Flycam {
self.aspect = (width as f32) / (height as f32); self.aspect = (width as f32) / (height as f32);
} }
/// apply input and update camera movement
pub fn update(&mut self) { pub fn update(&mut self) {
let dt = self.last_update.elapsed(); let dt = self.last_update.elapsed();
self.last_update = Instant::now(); self.last_update = Instant::now();
let dt = dt.as_micros() as f32 / 1_000_000.0; let dt = dt.as_micros() as f32 / 1_000_000.0;
let t = self.turn_speed; self.update_orientation(dt);
self.pan += t * self.mouse_dx; self.update_kinematic(dt);
self.tilt += t * self.mouse_dy; }
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_dx = 0.0;
self.mouse_dy = 0.0; self.mouse_dy = 0.0;
let tilt_limit = std::f32::consts::FRAC_PI_2; // Clamp euler_x to [-pi/2, pi/2]
if self.tilt < -tilt_limit { let euler_x_limit = std::f32::consts::FRAC_PI_2;
self.tilt = -tilt_limit; if self.euler_x < -euler_x_limit {
} else if self.tilt > tilt_limit { self.euler_x = -euler_x_limit;
self.tilt = tilt_limit; } else if self.euler_x > euler_x_limit {
self.euler_x = euler_x_limit;
} }
let s = dt * self.speed;
let axis = Self::key_axis;
let truck = s * axis(self.is_backward_pressed, self.is_forward_pressed);
let dolly = s * axis(self.is_right_pressed, self.is_left_pressed);
let boom = s * axis(self.is_down_pressed, self.is_up_pressed);
self.move_position(truck, dolly, boom);
} }
/// 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 { fn key_axis(negative: bool, positive: bool) -> f32 {
if negative { if negative {
if positive { if positive {
0.0 0.0 // positive + negative cancel out
} else { } else {
-1.0 -1.0 // negative only
} }
} else { } else {
if positive { if positive {
1.0 1.0 // positive only
} else { } else {
0.0 0.0 // neutral
} }
} }
} }
fn move_position(&mut self, truck: f32, dolly: f32, boom: f32) { /// the current camera orientation, which can be seen as a rotation (in quaternion form) from
// truck direction from straight down /// camera axes to world axes
let h = Vec2::new(self.pan.sin(), -self.pan.cos()); /// glam's YXZ ordering matches the standard roll-pitch-yaw Euler angles
// truck direction from the side fn get_orientation(&self) -> glam::Quat {
let v = Vec2::new(self.tilt.cos(), -self.tilt.sin()); Quat::from_euler(glam::EulerRot::YXZ, self.euler_y, self.euler_x, 0.0)
// composite to get forward direction
let truck_to = Vec3::new(h.x * v.x, v.y, h.y * v.x);
let dolly_to = Vec3::new(-self.pan.cos(), 0.0, -self.pan.sin());
self.position += (truck_to * truck) + (dolly_to * dolly);
self.position.y += boom;
} }
} }
@ -154,10 +216,14 @@ impl Camera for Flycam {
} }
fn get_vp(&self) -> [[f32; 4]; 4] { fn get_vp(&self) -> [[f32; 4]; 4] {
let orientation = Quat::from_euler(glam::EulerRot::XYZ, self.tilt, self.pan, 0.0); // view matrix is inverted camera pose (world space to camera space)
let rotation = Mat4::from_quat(orientation); let rotation = Mat4::from_quat(self.get_orientation().inverse());
let view = rotation * Mat4::from_translation(-self.position); 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 proj = Mat4::perspective_rh_gl(self.fovy, self.aspect, self.znear, self.zfar);
let vp = proj * view; let vp = proj * view;
vp.to_cols_array_2d() vp.to_cols_array_2d()
} }

View File

@ -149,7 +149,7 @@ async fn make_window_renderer(window: &winit::window::Window) -> Renderer {
fn main() { fn main() {
let event_loop = EventLoop::new(); let event_loop = EventLoop::new();
let window = WindowBuilder::new().build(&event_loop).unwrap(); let window = WindowBuilder::new().build(&event_loop).unwrap();
let mut camera = Flycam::new(10.0, 0.002); let mut camera = Flycam::new(0.002, 10.0, 0.25);
let mut is_grabbed = false; let mut is_grabbed = false;
let mut ren = pollster::block_on(make_window_renderer(&window)); let mut ren = pollster::block_on(make_window_renderer(&window));
// let mut state: Box<dyn WorldState> = Box::new(Planets::new(&mut ren)); // let mut state: Box<dyn WorldState> = Box::new(Planets::new(&mut ren));