Skip to content

Commit

Permalink
Support pan, rotate and zoom camera operations (#108)
Browse files Browse the repository at this point in the history
* Support pan, rotate and zoom camera operations

* Add comments to Camera struct fields.

* Use quartenion rotation

* Adjust the conversion from mouse events to viewport change

* Fix CI

* Apply suggestions from code review

* get rotation working

* get pan working, fix making deltas relative to window size

* get zoom working

* make zooming work in ortho mode

* use MIN_ZOOM_FACTOR

* extend OrbitCamera::new with init_pos

* make clippy happy

* renormalize Quat after multiplication inside OrbitCamera

* revisit near and far clip values

* optimize a bit

* Update crates/viewer/src/camera.rs

---------

Co-authored-by: Máté Kovács <[email protected]>
Co-authored-by: Mate Kovacs <[email protected]>
Co-authored-by: Brian Schwind <[email protected]>
  • Loading branch information
4 people authored Nov 30, 2023
1 parent edf0924 commit ff63a99
Show file tree
Hide file tree
Showing 2 changed files with 159 additions and 34 deletions.
92 changes: 72 additions & 20 deletions crates/viewer/src/camera.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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) {
Expand All @@ -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
}
Expand Down
101 changes: 87 additions & 14 deletions crates/viewer/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand All @@ -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,
Expand All @@ -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<f64>,
}

impl MouseState {
fn delta(&mut self, position: PhysicalPosition<f64>) -> (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,
Expand All @@ -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)]
Expand Down Expand Up @@ -153,7 +186,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(),
Expand All @@ -172,12 +206,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);
Expand All @@ -190,13 +224,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), .. },
Expand All @@ -223,8 +297,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,
Expand Down

0 comments on commit ff63a99

Please sign in to comment.