From 4313ce8017f91d796931cfef5eabec781d1d1215 Mon Sep 17 00:00:00 2001 From: mars Date: Fri, 22 Apr 2022 21:42:33 -0600 Subject: [PATCH] Add flycam --- Cargo.toml | 1 + src/camera.rs | 231 ++++++++++++++++++++++++++++++++++++++ src/lib.rs | 24 +++- src/main.rs | 50 ++++++++- src/pass/mesh.rs | 17 ++- src/pass/mesh_shader.wgsl | 14 ++- 6 files changed, 329 insertions(+), 8 deletions(-) create mode 100644 src/camera.rs diff --git a/Cargo.toml b/Cargo.toml index 4dd158e..4ff6641 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" [dependencies] bytemuck = { version = "^1.0", features = ["derive"] } +glam = "0.20" multimap = "0.8" pollster = "0.2" rayon = "1" diff --git a/src/camera.rs b/src/camera.rs new file mode 100644 index 0000000..73bf9a7 --- /dev/null +++ b/src/camera.rs @@ -0,0 +1,231 @@ +use glam::{Mat4, Quat, Vec2, Vec3}; +use std::time::Instant; +use winit::event::{ElementState, VirtualKeyCode}; +use std::f32::consts::LN_2; + +pub trait Camera { + fn get_eye(&self) -> [f32; 4]; + fn get_vp(&self) -> [[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); + } + + /// 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) + } +} + +impl Camera for Flycam { + fn get_eye(&self) -> [f32; 4] { + self.position.extend(0.0).to_array() + } + + 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() + } +} diff --git a/src/lib.rs b/src/lib.rs index b4667bb..20f9f63 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,16 +5,30 @@ use rayon::prelude::*; use std::sync::Arc; use std::sync::Mutex; +pub mod camera; pub mod mesh; pub mod pass; pub mod phase; pub mod staging; +use camera::Camera; use pass::*; use phase::*; +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] pub struct ViewportUniform { - pub vp: [f32; 16], + pub eye: [f32; 4], + pub vp: [[f32; 4]; 4], +} + +impl ViewportUniform { + pub fn from_camera(camera: &impl Camera) -> Self { + Self { + eye: camera.get_eye(), + vp: camera.get_vp(), + } + } } impl ViewportUniform { @@ -159,6 +173,7 @@ impl Renderer { pub fn render( &mut self, + camera: &impl Camera, surface: &wgpu::Surface, format: wgpu::TextureFormat, ) -> Result<(), wgpu::SurfaceError> { @@ -170,6 +185,13 @@ impl Renderer { let frame_index = self.frame_index; let frame_data = &self.frame_datas[frame_index]; + let viewport_uniform = ViewportUniform::from_camera(camera); + self.queue.write_buffer( + &frame_data.viewport_uniform, + 0, + bytemuck::cast_slice(&[viewport_uniform]), + ); + let phase_passes = multimap::MultiMap::::new(); let phase_passes = std::sync::Mutex::new(phase_passes); diff --git a/src/main.rs b/src/main.rs index 2071069..1649c2d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -72,13 +72,20 @@ fn main() { let window = WindowBuilder::new().build(&event_loop).unwrap(); let (mut renderer, mut viewport) = pollster::block_on(make_window_renderer(&window)); + let mut camera = cyborg::camera::Flycam::new(0.002, 10.0, 0.25); + let mut is_grabbed = false; + let device = renderer.get_device(); - let mesh_pass = pass::mesh::MeshPass::new(device.to_owned(), viewport.config.format); + let layouts = renderer.get_layouts(); + + let mesh_pass = pass::mesh::MeshPass::new(device.to_owned(), layouts.to_owned(), viewport.config.format); + renderer.add_pass(mesh_pass); event_loop.run(move |event, _, control_flow| match event { Event::RedrawRequested(_) => { - match renderer.render(&viewport.surface, viewport.config.format) { + println!("camera: {:#?}", camera); + match renderer.render(&camera, &viewport.surface, viewport.config.format) { Ok(_) => {} Err(wgpu::SurfaceError::Lost) => viewport.resize(viewport.size), Err(wgpu::SurfaceError::OutOfMemory) => *control_flow = ControlFlow::Exit, @@ -86,12 +93,51 @@ fn main() { }; } Event::MainEventsCleared => { + camera.update(); window.request_redraw(); } + Event::DeviceEvent { ref event, .. } => match event { + DeviceEvent::MouseMotion { delta } => { + if is_grabbed { + camera.process_mouse(delta.0, delta.1); + } + } + _ => {} + }, Event::WindowEvent { ref event, window_id, } if window_id == window.id() => match event { + WindowEvent::KeyboardInput { + input: + KeyboardInput { + virtual_keycode: Some(key), + state, + .. + }, + .. + } => { + if *state == ElementState::Pressed && *key == VirtualKeyCode::Escape { + if is_grabbed { + window.set_cursor_grab(false).unwrap(); + window.set_cursor_visible(true); + is_grabbed = false; + } + } else { + camera.process_keyboard(*key, *state); + } + } + WindowEvent::MouseInput { + button: MouseButton::Left, + state: ElementState::Pressed, + .. + } => { + if !is_grabbed { + window.set_cursor_grab(true).unwrap(); + window.set_cursor_visible(false); + is_grabbed = true; + } + } WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, WindowEvent::Resized(physical_size) => { viewport.resize(*physical_size); diff --git a/src/pass/mesh.rs b/src/pass/mesh.rs index bd1b471..8700918 100644 --- a/src/pass/mesh.rs +++ b/src/pass/mesh.rs @@ -1,5 +1,6 @@ use super::*; use crate::mesh::*; +use crate::RenderLayouts; #[repr(C)] #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] @@ -33,6 +34,7 @@ pub struct FrameData {} pub struct MeshPass { device: Arc, + layouts: Arc, attr_store: Arc, mesh_pool: Arc, vertex_attr_id: AttrId, @@ -44,7 +46,11 @@ pub struct MeshPass { } impl MeshPass { - pub fn new(device: Arc, target_format: wgpu::TextureFormat) -> Self { + pub fn new( + device: Arc, + layouts: Arc, + target_format: wgpu::TextureFormat, + ) -> Self { let attr_store = AttrStore::new(); let mesh_pool = MeshPool::new(device.clone(), attr_store.to_owned()); @@ -98,7 +104,7 @@ impl MeshPass { let render_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("Render Pipeline Layout"), - bind_group_layouts: &[], + bind_group_layouts: &[&layouts.bind_viewport], push_constant_ranges: &[], }); @@ -141,6 +147,7 @@ impl MeshPass { Self { device, + layouts, attr_store, mesh_pool, index_attr_id, @@ -216,6 +223,7 @@ impl RenderPass for MeshPass { .collect(); cmds.set_pipeline(pipeline); + cmds.set_bind_group(0, data.bind_viewport, &[]); for (bindings, instances) in mesh_bindings.iter() { let vertices_pool = bindings.get(self.vertex_attr_id).unwrap(); @@ -235,7 +243,10 @@ impl RenderPass for MeshPass { let is_end = is_start + indices.count as u32; cmds.draw_indexed(is_start..is_end, vertices.offset as i32, 0..1); - println!("drew a mesh! {}..{} + {}", is_start, is_end, vertices.offset); + println!( + "drew a mesh! {}..{} + {}", + is_start, is_end, vertices.offset + ); } } diff --git a/src/pass/mesh_shader.wgsl b/src/pass/mesh_shader.wgsl index 93220be..ffba437 100644 --- a/src/pass/mesh_shader.wgsl +++ b/src/pass/mesh_shader.wgsl @@ -1,3 +1,8 @@ +struct CameraUniform { + eye: vec4; + vp: mat4x4; +}; + struct VertexInput { [[location(0)]] position: vec3; [[location(1)]] color: vec3; @@ -9,15 +14,20 @@ struct VertexOutput { [[location(1)]] color: vec3; }; +[[group(0), binding(0)]] +var camera: CameraUniform; + [[stage(vertex)]] fn vs_main( [[builtin(instance_index)]] mesh_idx: u32, [[builtin(vertex_index)]] vertex_idx: u32, vertex: VertexInput, ) -> VertexOutput { + let world_pos = vertex.position; + var out: VertexOutput; - out.clip_position = vec4(vertex.position, 1.0); - out.position = vertex.position; + out.clip_position = camera.vp * vec4(world_pos, 1.0); + out.position = world_pos; out.color = vertex.color; return out; }