Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support pan, rotate and zoom camera operations #108

Merged
merged 17 commits into from
Nov 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
mkovaxx marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -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(),
Expand All @@ -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);
Expand All @@ -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), .. },
Expand All @@ -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,
Expand Down