diff --git a/README.md b/README.md index a0c209e..bfeb2a1 100644 --- a/README.md +++ b/README.md @@ -34,9 +34,9 @@ using [Ash][2]. It runs on Window, Linux and MacOS. - [x] KHR_materials_pbrSpecularGlossiness - [x] KHR_materials_emissive_strength - [ ] KHR_draco_mesh_compression -- [ ] Camera controls +- [x] Camera controls - [x] Orbital - - [ ] First Person + - [x] First Person - [x] Drag and drop - [x] Background loading - [ ] Post processing @@ -63,10 +63,17 @@ using [Ash][2]. It runs on Window, Linux and MacOS. ## Controls +Orbital camera: - Left click and move to rotate camera around origin - Right click and move to move camera - Mouse wheel to un/zoom +FPS camera: +- WASD to move +- Left Ctrl to go down +- Space to go up +- Left click to rotate + ## Build it ```sh diff --git a/crates/viewer/src/camera.rs b/crates/viewer/src/camera.rs index 694f683..8ae4d3e 100644 --- a/crates/viewer/src/camera.rs +++ b/crates/viewer/src/camera.rs @@ -1,39 +1,113 @@ use crate::controls::*; -use math::cgmath::{InnerSpace, Matrix4, Point3, Vector3}; +use math::cgmath::{InnerSpace, Matrix3, Matrix4, Point3, Rad, Vector3, Zero}; use math::clamp; const MIN_ORBITAL_CAMERA_DISTANCE: f32 = 0.5; const TARGET_MOVEMENT_SPEED: f32 = 0.003; +const ROTATION_SPEED_DEG: f32 = 0.4; +pub const DEFAULT_FPS_MOVE_SPEED: f32 = 6.0; -#[derive(Clone, Copy)] -pub struct Camera { +#[derive(Debug, Clone, Copy)] +pub enum Camera { + Orbital(Orbital), + Fps(Fps), +} + +impl Default for Camera { + fn default() -> Self { + Self::Orbital(Default::default()) + } +} + +impl Camera { + pub fn update(&mut self, input: &InputState, delta_time_secs: f32) { + match self { + Self::Orbital(c) => c.update(input, delta_time_secs), + Self::Fps(c) => c.update(input, delta_time_secs), + } + } + + pub fn position(&self) -> Point3 { + match self { + Self::Orbital(c) => c.position(), + Self::Fps(c) => c.position(), + } + } + + pub fn target(&self) -> Point3 { + match self { + Self::Orbital(c) => c.target(), + Self::Fps(c) => c.target(), + } + } + + pub fn to_orbital(self) -> Self { + match self { + Self::Orbital(_) => self, + Self::Fps(c) => Self::Orbital(c.into()), + } + } + + pub fn to_fps(self) -> Self { + match self { + Self::Fps(_) => self, + Self::Orbital(c) => Self::Fps(c.into()), + } + } + + pub fn set_move_speed(&mut self, move_speed: f32) { + if let Self::Fps(c) = self { + c.move_speed = move_speed; + } + } +} + +#[derive(Debug, Clone, Copy)] +pub struct Orbital { theta: f32, phi: f32, r: f32, target: Point3, } -impl Camera { - pub fn position(&self) -> Point3 { - Point3::new( - self.target[0] + self.r * self.phi.sin() * self.theta.sin(), - self.target[1] + self.r * self.phi.cos(), - self.target[2] + self.r * self.phi.sin() * self.theta.cos(), - ) +impl Default for Orbital { + fn default() -> Self { + Self { + theta: 0.0_f32.to_radians(), + phi: 90.0_f32.to_radians(), + r: 10.0, + target: Point3::new(0.0, 0.0, 0.0), + } } +} - pub fn target(&self) -> Point3 { - self.target +impl From for Orbital { + fn from(fps: Fps) -> Self { + let Point3 { x, y, z } = fps.position; + let xx = x * x; + let yy = y * y; + let zz = z * z; + + let r = (xx + yy + zz).sqrt(); + let theta = x.signum() * (z / (f32::EPSILON + (zz + xx).sqrt())).acos(); + let phi = (y / (r + f32::EPSILON)).acos(); + + Self { + r, + theta, + phi, + target: Point3::new(0.0, 0.0, 0.0), + } } } -impl Camera { - pub fn update(&mut self, input: &InputState) { +impl Orbital { + fn update(&mut self, input: &InputState, _: f32) { // Rotation if input.is_left_clicked() { let delta = input.cursor_delta(); - let theta = delta[0] as f32 * (0.2_f32).to_radians(); - let phi = delta[1] as f32 * (0.2_f32).to_radians(); + let theta = delta[0] as f32 * ROTATION_SPEED_DEG.to_radians(); + let phi = delta[1] as f32 * ROTATION_SPEED_DEG.to_radians(); self.rotate(theta, phi); } @@ -41,7 +115,7 @@ impl Camera { if input.is_right_clicked() { let position = self.position(); let forward = (self.target - position).normalize(); - let up = Vector3::new(0.0, 1.0, 0.0); + let up = Vector3::unit_y(); let right = up.cross(forward).normalize(); let up = forward.cross(right.normalize()); @@ -69,19 +143,105 @@ impl Camera { self.r -= r; } } + + fn position(&self) -> Point3 { + Point3::new( + self.target[0] + self.r * self.phi.sin() * self.theta.sin(), + self.target[1] + self.r * self.phi.cos(), + self.target[2] + self.r * self.phi.sin() * self.theta.cos(), + ) + } + + fn target(&self) -> Point3 { + self.target + } } -impl Default for Camera { +#[derive(Debug, Clone, Copy)] +pub struct Fps { + position: Point3, + direction: Vector3, + move_speed: f32, +} + +impl Default for Fps { fn default() -> Self { - Camera { - theta: 0.0_f32.to_radians(), - phi: 90.0_f32.to_radians(), - r: 10.0, - target: Point3::new(0.0, 0.0, 0.0), + Self { + position: Point3::new(0.0, 0.0, 10.0), + direction: -Vector3::unit_z(), + move_speed: DEFAULT_FPS_MOVE_SPEED, } } } +impl From for Fps { + fn from(orbital: Orbital) -> Self { + let position = orbital.position(); + let target = orbital.target(); + let direction = (target - position).normalize(); + Self { + position, + direction, + move_speed: DEFAULT_FPS_MOVE_SPEED, + } + } +} + +impl Fps { + fn update(&mut self, input: &InputState, delta_time_secs: f32) { + let forward = self.direction.normalize(); + let up = Vector3::unit_y(); + let right = up.cross(forward).normalize(); + let up = forward.cross(right.normalize()); + + // compute movement + let mut move_dir = Vector3::zero(); + if input.is_forward_pressed() { + move_dir += forward; + } + if input.is_backward_pressed() { + move_dir -= forward; + } + if input.is_left_pressed() { + move_dir += right; + } + if input.is_right_pressed() { + move_dir -= right; + } + if input.is_up_pressed() { + move_dir += up; + } + if input.is_down_pressed() { + move_dir -= up; + } + + if !move_dir.is_zero() { + move_dir = move_dir.normalize() * delta_time_secs * self.move_speed; + } + + self.position += move_dir; + + // compute rotation + if input.is_left_clicked() { + let delta = input.cursor_delta(); + + let rot_speed = delta_time_secs * ROTATION_SPEED_DEG; + let rot_y = Matrix3::::from_angle_y(Rad(-delta[0] * rot_speed)); + let rot_x = Matrix3::::from_axis_angle(right, Rad(delta[1] * rot_speed)); + + self.direction = (rot_x * rot_y * forward).normalize(); + } + } + + fn position(&self) -> Point3 { + self.position + } + + fn target(&self) -> Point3 { + self.position + self.direction.normalize() + } +} + #[derive(Clone, Copy)] #[allow(dead_code)] pub struct CameraUBO { diff --git a/crates/viewer/src/controls.rs b/crates/viewer/src/controls.rs index 8c06153..15cb71b 100644 --- a/crates/viewer/src/controls.rs +++ b/crates/viewer/src/controls.rs @@ -1,9 +1,18 @@ -use vulkan::winit::event::{ - DeviceEvent, ElementState, Event, MouseButton, MouseScrollDelta, WindowEvent, +use vulkan::winit::{ + event::{ + DeviceEvent, ElementState, Event, KeyEvent, MouseButton, MouseScrollDelta, WindowEvent, + }, + keyboard::{KeyCode, PhysicalKey}, }; #[derive(Copy, Clone, Debug)] pub struct InputState { + is_forward_pressed: bool, + is_backward_pressed: bool, + is_left_pressed: bool, + is_right_pressed: bool, + is_up_pressed: bool, + is_down_pressed: bool, is_left_clicked: bool, is_right_clicked: bool, cursor_delta: [f32; 2], @@ -12,6 +21,12 @@ pub struct InputState { impl InputState { pub fn update(self, event: &Event<()>) -> Self { + let mut is_forward_pressed = None; + let mut is_backward_pressed = None; + let mut is_left_pressed = None; + let mut is_right_pressed = None; + let mut is_up_pressed = None; + let mut is_down_pressed = None; let mut is_left_clicked = None; let mut is_right_clicked = None; let mut wheel_delta = self.wheel_delta; @@ -28,21 +43,12 @@ impl InputState { if let Event::WindowEvent { event, .. } = event { match event { WindowEvent::MouseInput { button, state, .. } => { - if *state == ElementState::Pressed { - if *button == MouseButton::Left { - is_left_clicked = Some(true); - } - if *button == MouseButton::Right { - is_right_clicked = Some(true) - } - } else { - if *button == MouseButton::Left { - is_left_clicked = Some(false); - } - if *button == MouseButton::Right { - is_right_clicked = Some(false) - } - } + let clicked = matches!(state, ElementState::Pressed); + match button { + MouseButton::Left => is_left_clicked = Some(clicked), + MouseButton::Right => is_right_clicked = Some(clicked), + _ => {} + }; } WindowEvent::MouseWheel { delta: MouseScrollDelta::LineDelta(_, v_lines), @@ -50,6 +56,26 @@ impl InputState { } => { wheel_delta += v_lines; } + WindowEvent::KeyboardInput { + event: + KeyEvent { + physical_key: PhysicalKey::Code(scancode), + state, + .. + }, + .. + } => { + let pressed = matches!(state, ElementState::Pressed); + match scancode { + KeyCode::KeyW => is_forward_pressed = Some(pressed), + KeyCode::KeyS => is_backward_pressed = Some(pressed), + KeyCode::KeyA => is_left_pressed = Some(pressed), + KeyCode::KeyD => is_right_pressed = Some(pressed), + KeyCode::Space => is_up_pressed = Some(pressed), + KeyCode::ControlLeft => is_down_pressed = Some(pressed), + _ => {} + }; + } _ => {} } } @@ -64,6 +90,12 @@ impl InputState { } Self { + is_forward_pressed: is_forward_pressed.unwrap_or(self.is_forward_pressed), + is_backward_pressed: is_backward_pressed.unwrap_or(self.is_backward_pressed), + is_left_pressed: is_left_pressed.unwrap_or(self.is_left_pressed), + is_right_pressed: is_right_pressed.unwrap_or(self.is_right_pressed), + is_up_pressed: is_up_pressed.unwrap_or(self.is_up_pressed), + is_down_pressed: is_down_pressed.unwrap_or(self.is_down_pressed), is_left_clicked: is_left_clicked.unwrap_or(self.is_left_clicked), is_right_clicked: is_right_clicked.unwrap_or(self.is_right_clicked), cursor_delta, @@ -73,6 +105,30 @@ impl InputState { } impl InputState { + pub fn is_forward_pressed(&self) -> bool { + self.is_forward_pressed + } + + pub fn is_backward_pressed(&self) -> bool { + self.is_backward_pressed + } + + pub fn is_left_pressed(&self) -> bool { + self.is_left_pressed + } + + pub fn is_right_pressed(&self) -> bool { + self.is_right_pressed + } + + pub fn is_up_pressed(&self) -> bool { + self.is_up_pressed + } + + pub fn is_down_pressed(&self) -> bool { + self.is_down_pressed + } + pub fn is_left_clicked(&self) -> bool { self.is_left_clicked } @@ -93,6 +149,12 @@ impl InputState { impl Default for InputState { fn default() -> Self { Self { + is_forward_pressed: false, + is_backward_pressed: false, + is_left_pressed: false, + is_right_pressed: false, + is_up_pressed: false, + is_down_pressed: false, is_left_clicked: false, is_right_clicked: false, cursor_delta: [0.0, 0.0], diff --git a/crates/viewer/src/gui.rs b/crates/viewer/src/gui.rs index d57181e..72e4772 100644 --- a/crates/viewer/src/gui.rs +++ b/crates/viewer/src/gui.rs @@ -1,5 +1,6 @@ use crate::camera::Camera; use crate::renderer::{OutputMode, RendererSettings, ToneMapMode, DEFAULT_BLOOM_STRENGTH}; +use crate::DEFAULT_FPS_MOVE_SPEED; use egui::{ClippedPrimitive, Context, TexturesDelta, Ui, ViewportId, Widget}; use egui_winit::State as EguiWinit; use model::{metadata::*, PlaybackState}; @@ -137,6 +138,14 @@ impl Gui { self.state.animation_speed } + pub fn camera_mode(&self) -> CameraMode { + self.state.camera_mode + } + + pub fn camera_move_speed(&self) -> f32 { + self.state.camera_move_speed + } + pub fn should_reset_camera(&self) -> bool { self.state.reset_camera } @@ -232,6 +241,15 @@ fn build_camera_details_window(ui: &mut Ui, state: &mut State, camera: Option, @@ -362,6 +382,8 @@ impl State { ssao_strength: self.ssao_strength, ssao_kernel_size_index: self.ssao_kernel_size_index, ssao_enabled: self.ssao_enabled, + camera_mode: self.camera_mode, + camera_move_speed: self.camera_move_speed, ..Default::default() } } @@ -389,6 +411,8 @@ impl Default for State { stop_animation: false, animation_speed: 1.0, + camera_mode: CameraMode::Orbital, + camera_move_speed: DEFAULT_FPS_MOVE_SPEED, reset_camera: false, hdr_enabled: None, @@ -406,3 +430,9 @@ impl Default for State { } } } + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum CameraMode { + Orbital, + Fps, +} diff --git a/crates/viewer/src/main.rs b/crates/viewer/src/main.rs index 9b3855f..24d7e33 100644 --- a/crates/viewer/src/main.rs +++ b/crates/viewer/src/main.rs @@ -89,7 +89,7 @@ fn run(config: Config, enable_debug: bool, path: Option) { // End of event processing Event::AboutToWait => { let new_time = Instant::now(); - let delta_s = (new_time - time).as_secs_f64(); + let delta_s = (new_time - time).as_secs_f32(); time = new_time; // Load new model @@ -125,7 +125,7 @@ fn run(config: Config, enable_debug: bool, path: Option) { } gui.set_animation_playback_state(model.get_animation_playback_state()); - let delta_s = delta_s as f32 * gui.get_animation_speed(); + let delta_s = delta_s * gui.get_animation_speed(); model.update(delta_s); } @@ -135,8 +135,15 @@ fn run(config: Config, enable_debug: bool, path: Option) { camera = Default::default(); } + camera = match gui.camera_mode() { + gui::CameraMode::Orbital => camera.to_orbital(), + gui::CameraMode::Fps => camera.to_fps(), + }; + + camera.set_move_speed(gui.camera_move_speed()); + if !gui.is_hovered() { - camera.update(&input_state); + camera.update(&input_state, delta_s); gui.set_camera(Some(camera)); } } @@ -188,8 +195,7 @@ fn run(config: Config, enable_debug: bool, path: Option) { loader.load(path); } // Resizing - WindowEvent::Resized(new_size) => { - log::debug!("Window was resized. New size is {:?}", new_size); + WindowEvent::Resized(_) => { dirty_swapchain = true; } // Exit