diff --git a/crates/viewer/src/camera.rs b/crates/viewer/src/camera.rs index bdaafc7d..b93989cd 100644 --- a/crates/viewer/src/camera.rs +++ b/crates/viewer/src/camera.rs @@ -1,4 +1,6 @@ -use glam::{vec3, Mat4}; +use glam::{Mat3, Mat4, Quat, Vec2, Vec3}; + +const MIN_ZOOM_FACTOR: f32 = 0.05; #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum Projection { @@ -8,14 +10,33 @@ enum Projection { Perspective, } -pub struct Camera { +pub struct OrbitCamera { projection: Projection, aspect_ratio: f32, + // Zoom factor used for orthographic projection. + zoom_factor: f32, + // The look-at target, in the center of the view. + target: Vec3, + // The radius of the orbit + radius: f32, + // The orientation of the camera around the target point + orientation: Quat, } -impl Camera { - pub fn new(width: u32, height: u32) -> Self { - Self { projection: Projection::Orthographic, aspect_ratio: width as f32 / height as f32 } +impl OrbitCamera { + pub fn new(width: u32, height: u32, init_pos: Vec3) -> Self { + let target = Vec3::ZERO; + let radius = init_pos.length(); + let look_at_matrix = Mat4::look_at_rh(init_pos, target, Vec3::Z); + let orientation = Quat::from_mat4(&look_at_matrix).inverse(); + Self { + projection: Projection::Orthographic, + aspect_ratio: width as f32 / height as f32, + zoom_factor: 1.0, + target, + radius, + orientation, + } } pub fn resize(&mut self, width: u32, height: u32) { @@ -30,29 +51,60 @@ impl Camera { self.projection = Projection::Orthographic; } + fn get_local_frame(&self) -> Mat3 { + Mat3::from_quat(self.orientation) + } + + /// Pan the camera view horizontally and vertically. Look-at target will move along with the + /// camera. + pub fn pan(&mut self, delta: Vec2) { + self.target -= self.get_local_frame() * delta.extend(0.0); + } + + /// Zoom in or out, while looking at the same target. + pub fn zoom(&mut self, zoom_delta: f32) { + self.zoom_factor = f32::max(self.zoom_factor * f32::exp(zoom_delta), MIN_ZOOM_FACTOR); + } + + /// Orbit around the target while keeping the distance. + pub fn rotate(&mut self, rotator: Quat) { + self.orientation = (self.orientation * rotator).normalize(); + } + pub fn matrix(&self) -> Mat4 { // These magic numbers are configured so that the particular model we are loading is // visible in its entirety. They will be dynamically computed eventually when we have "fit // to view" function or alike. - let proj = match self.projection { - Projection::Orthographic => Mat4::orthographic_rh( - -100.0 * self.aspect_ratio, - 100.0 * self.aspect_ratio, - -100.0, - 100.0, - -1000.0, - 1000.0, - ), + + let (proj, effective_radius) = match self.projection { + Projection::Orthographic => { + let proj = Mat4::orthographic_rh( + -50.0 * self.zoom_factor * self.aspect_ratio, + 50.0 * self.zoom_factor * self.aspect_ratio, + -50.0 * self.zoom_factor, + 50.0 * self.zoom_factor, + -1000.0, + 1000.0, + ); + (proj, self.radius) + }, Projection::Perspective => { - Mat4::perspective_rh(std::f32::consts::PI / 2.0, self.aspect_ratio, 0.01, 1000.0) + let proj = Mat4::perspective_rh( + std::f32::consts::PI / 2.0, + self.aspect_ratio, + 1.0, + 10_000.0, + ); + (proj, self.zoom_factor * self.radius) }, }; - let view = Mat4::look_at_rh( - vec3(20.0, -30.0, 20.0), // Eye position - vec3(0.0, 0.0, 0.0), // Look-at target - vec3(0.0, 0.0, 1.0), // Up vector of the camera - ); + let local_frame = self.get_local_frame(); + let position = self.target + effective_radius * local_frame.z_axis; + + // NOTE(mkovaxx): This is computing inverse(translation * orientation), but more efficiently + let view = + Mat4::from_quat(self.orientation.conjugate()) * Mat4::from_translation(-position); proj * view } diff --git a/crates/viewer/src/main.rs b/crates/viewer/src/main.rs index 7a0408c5..0ee7182d 100644 --- a/crates/viewer/src/main.rs +++ b/crates/viewer/src/main.rs @@ -3,8 +3,9 @@ use crate::{ surface_drawer::{CadMesh, SurfaceDrawer}, }; use anyhow::Error; +use camera::OrbitCamera; use clap::{Parser, ValueEnum}; -use glam::{vec3, DVec3, Mat4}; +use glam::{vec2, vec3, DVec3, Mat4, Quat, Vec2, Vec3}; use opencascade::primitives::Shape; use simple_game::{ graphics::{ @@ -17,7 +18,8 @@ use simple_game::{ use smaa::{SmaaMode, SmaaTarget}; use std::path::PathBuf; use winit::{ - event::{KeyEvent, WindowEvent}, + dpi::PhysicalPosition, + event::{ElementState, KeyEvent, MouseButton, MouseScrollDelta::PixelDelta, WindowEvent}, event_loop::EventLoopWindowTarget, keyboard::{KeyCode, PhysicalKey}, window::Window, @@ -27,10 +29,42 @@ mod camera; mod edge_drawer; mod surface_drawer; -const MIN_SCALE: f32 = 0.01; +// Multipliers to convert mouse position deltas to a more intuitve camera perspective change. +const ZOOM_MULTIPLIER: f32 = 5.0; +const TOUCHPAD_ZOOM_MULTIPLIER: f32 = 0.5; +const ROTATE_MULTIPLIER: f32 = 8.0; +const TOUCHPAD_ROTATE_MULTIPLIER: f32 = -0.05; +const PAN_MULTIPLIER: f32 = 150.0; +const TOUCHPAD_PAN_MULTIPLIER: f32 = 100.0; + +#[derive(Default)] +struct MouseState { + left_button_down: bool, + middle_button_down: bool, + right_button_down: bool, + last_position: PhysicalPosition, +} + +impl MouseState { + fn delta(&mut self, position: PhysicalPosition) -> (f64, f64) { + let delta = (position.x - self.last_position.x, position.y - self.last_position.y); + self.last_position = position; + delta + } + + fn input(&mut self, button: MouseButton, state: ElementState) { + match button { + MouseButton::Left => self.left_button_down = state == ElementState::Pressed, + MouseButton::Middle => self.middle_button_down = state == ElementState::Pressed, + MouseButton::Right => self.right_button_down = state == ElementState::Pressed, + _ => {}, + } + } +} struct ViewerApp { - camera: camera::Camera, + client_rect: Vec2, + camera: OrbitCamera, depth_texture: DepthTexture, text_system: TextSystem, fps_counter: FPSCounter, @@ -39,8 +73,7 @@ struct ViewerApp { smaa_target: SmaaTarget, rendered_edges: RenderedLine, cad_mesh: CadMesh, - angle: f32, - scale: f32, + mouse_state: MouseState, } #[derive(Parser, Debug, Clone)] @@ -149,7 +182,8 @@ impl GameApp for ViewerApp { let depth_texture_format = depth_texture.format(); Self { - camera: camera::Camera::new(width, height), + client_rect: vec2(width as f32, height as f32), + camera: OrbitCamera::new(width, height, Vec3::new(40.0, -40.0, 20.0)), depth_texture, text_system: TextSystem::new(device, surface_texture_format, width, height), fps_counter: FPSCounter::new(), @@ -168,12 +202,12 @@ impl GameApp for ViewerApp { smaa_target, cad_mesh, rendered_edges, - angle: 0.0, - scale: 1.0, + mouse_state: Default::default(), } } fn resize(&mut self, graphics_device: &mut GraphicsDevice, width: u32, height: u32) { + self.client_rect = vec2(width as f32, height as f32); self.camera.resize(width, height); self.depth_texture = DepthTexture::new(graphics_device.device(), width, height); self.text_system.resize(width, height); @@ -186,13 +220,53 @@ impl GameApp for ViewerApp { event: &WindowEvent, window_target: &EventLoopWindowTarget<()>, ) { + let screen_diagonal = self.client_rect.length(); + match event { WindowEvent::TouchpadRotate { delta, .. } => { - self.angle += 2.0 * delta * std::f32::consts::PI / 180.0; + let axis = Vec3::new(0.0, 0.0, 1.0); + let rotator = Quat::from_axis_angle(axis, TOUCHPAD_ROTATE_MULTIPLIER * delta); + self.camera.rotate(rotator); + }, + WindowEvent::CursorMoved { position, .. } => { + let delta = self.mouse_state.delta(*position); + // On the screen, Y is DOWN, but in camera space, it's UP + let camera_space_delta = + Vec2::new(delta.0 as f32, -delta.1 as f32) / screen_diagonal; + if self.mouse_state.left_button_down { + // Construct the camera space rotation axis perpendicular to delta + let axis = Vec3::new(camera_space_delta.y, -camera_space_delta.x, 0.0); + let magnitude = axis.length(); + if magnitude > 0.0 { + let rotator = + Quat::from_axis_angle(axis.normalize(), ROTATE_MULTIPLIER * magnitude); + self.camera.rotate(rotator); + } + } + if self.mouse_state.middle_button_down { + self.camera.pan(PAN_MULTIPLIER * camera_space_delta); + } + if self.mouse_state.right_button_down { + self.camera.zoom(camera_space_delta.y * ZOOM_MULTIPLIER); + } + }, + WindowEvent::MouseInput { state, button, .. } => { + self.mouse_state.input(*button, *state) + }, + WindowEvent::MouseWheel { delta: PixelDelta(delta), .. } => { + // winit can not distinguish mouse wheel and touchpad pan events unfortunately. + // Because of that, we assign pan operation to MouseWheel events. For mice, you + // need to instead use mouse move while holding down the right button. + + // On the screen, Y is DOWN, but in camera space, it's UP + let camera_space_delta = + Vec2::new(delta.x as f32, -delta.y as f32) / screen_diagonal; + + self.camera.pan(TOUCHPAD_PAN_MULTIPLIER * camera_space_delta); }, WindowEvent::TouchpadMagnify { delta, .. } => { - self.scale += *delta as f32; - self.scale = self.scale.max(MIN_SCALE); + let zoom_delta = *delta as f32 * TOUCHPAD_ZOOM_MULTIPLIER; + self.camera.zoom(-zoom_delta); }, WindowEvent::KeyboardInput { event: KeyEvent { physical_key: PhysicalKey::Code(key_code), .. }, @@ -219,8 +293,7 @@ impl GameApp for ViewerApp { ); let camera_matrix = self.camera.matrix(); - let transform = Mat4::from_rotation_z(self.angle) - * Mat4::from_scale(vec3(self.scale, self.scale, self.scale)); + let transform = Mat4::IDENTITY; self.surface_drawer.render( &mut frame_encoder.encoder,