gltf_viewer_lib/
controls.rs

1use std::f32::consts::PI;
2
3use cgmath::{vec3, Deg};
4use cgmath::prelude::*;
5
6use log::{warn, trace};
7use num_traits::clamp;
8
9// type Point3 = cgmath::Point3<f32>;
10// type Vector3 = cgmath::Vector3<f32>;
11// type Matrix4 = cgmath::Matrix4<f32>;
12
13use crate::render::Camera;
14use crate::render::math::*;
15
16use glutin::dpi::PhysicalPosition;
17use glutin::dpi::PhysicalSize;
18
19// Defines several possible options for camera movement. Used as abstraction to stay away from window-system specific input methods
20#[derive(PartialEq, Clone, Copy)]
21pub enum CameraMovement {
22    FORWARD,
23    BACKWARD,
24    LEFT,
25    RIGHT,
26}
27use self::CameraMovement::*;
28
29#[derive(Debug)]
30pub struct CameraParams {
31    pub position: Vector3,
32    pub view_matrix: Matrix4,
33    pub projection_matrix: Matrix4,
34}
35
36// Default camera values
37const SPEED: f32 = 2.5;
38const ZOOM_SENSITIVITY: f32 = 0.1;
39pub const ZOOM: f32 = 45.0;
40const MIN_ZOOM: f32 = 1.0;
41const MAZ_ZOOM: f32 = 170.0;
42
43#[derive(Clone)]
44pub enum NavState {
45    None,
46    Rotating,
47    Panning,
48}
49
50/// Inspirted by `ThreeJS` `OrbitControls`
51pub struct OrbitControls {
52    pub camera: Camera,
53
54    pub position: Point3,
55
56    // "target" sets the location of focus, where the object orbits around
57    pub target: Point3,
58
59    pub state: NavState,
60
61    // current position in spherical coordinates
62    spherical: Spherical,
63    spherical_delta: Spherical,
64
65    scale: f32,
66    pan_offset: Vector3,
67
68    rotate_start: Option<Vector2>,
69    rotate_end: Vector2,
70
71    pan_start: Option<Vector2>,
72    pan_end: Vector2,
73
74    // for keyboard nav
75    // pub moving_up: bool,
76    pub moving_left: bool,
77    // pub moving_down: bool,
78    pub moving_right: bool,
79    pub moving_forward: bool,
80    pub moving_backward: bool,
81
82    pub screen_size: PhysicalSize,
83}
84
85impl OrbitControls {
86    pub fn new(position: Point3, screen_size: PhysicalSize) -> Self {
87        OrbitControls {
88            camera: Camera::default(),
89
90            position,
91            target: Point3::new(0.0, 0.0, 0.0),
92
93            state: NavState::None,
94
95            // current position in spherical coordinates
96            spherical: Spherical::default(),
97            spherical_delta: Spherical::default(),
98
99            scale: 1.0, // TODO!: not really used
100            pan_offset: Vector3::zero(),
101
102            rotate_start: None,
103            rotate_end: Vector2::zero(),
104
105            pan_start: None,
106            pan_end: Vector2::zero(),
107
108            // moving_up: false,
109            moving_left: false,
110            // moving_down: false,
111            moving_right: false,
112            moving_forward: false,
113            moving_backward: false,
114
115            screen_size,
116        }
117    }
118
119    // NOTE: could be cached
120    pub fn camera_params(&self) -> CameraParams {
121        CameraParams {
122            position: self.position.to_vec(),
123            view_matrix: self.view_matrix(),
124            projection_matrix: self.camera.projection_matrix,
125        }
126    }
127
128    fn view_matrix(&self) -> Matrix4 {
129        Matrix4::look_at(self.position, self.target, vec3(0.0, 1.0, 0.0))
130    }
131
132    pub fn handle_mouse_move(&mut self, pos: PhysicalPosition) {
133        match self.state {
134            NavState::Rotating => self.handle_mouse_move_rotate(pos),
135            NavState::Panning => self.handle_mouse_move_pan(pos),
136            NavState::None => ()
137        }
138    }
139
140    fn handle_mouse_move_rotate(&mut self, pos: PhysicalPosition) {
141        self.rotate_end.x = pos.x as f32;
142        self.rotate_end.y = pos.y as f32;
143        let rotate_delta = if let Some(rotate_start) = self.rotate_start {
144            self.rotate_end - rotate_start
145        } else {
146            Vector2::zero()
147        };
148
149        // rotating across whole screen goes 360 degrees around
150        let rotate_speed = 1.0; // TODO: const/param/remove?
151        let angle = 2.0 * PI * rotate_delta.x / self.screen_size.width as f32 * rotate_speed;
152        self.rotate_left(angle);
153
154        // rotating up and down along whole screen attempts to go 360, but limited to 180
155        let angle = 2.0 * PI * rotate_delta.y / self.screen_size.height as f32 * rotate_speed;
156        self.rotate_up(angle);
157
158        self.rotate_start = Some(self.rotate_end);
159
160        self.update();
161    }
162
163    pub fn handle_mouse_up(&mut self) {
164        self.rotate_start = None;
165        self.pan_start = None;
166    }
167
168    fn rotate_left(&mut self, angle: f32) {
169        self.spherical_delta.theta -= angle;
170    }
171
172    pub fn rotate_object(&mut self, angle: f32) {
173        self.rotate_left(angle);
174        self.update();
175    }
176    fn rotate_up(&mut self, angle: f32) {
177        self.spherical_delta.phi -= angle;
178    }
179
180    fn handle_mouse_move_pan(&mut self, pos: PhysicalPosition) {
181        self.pan_end.x = pos.x as f32;
182        self.pan_end.y = pos.y as f32;
183
184        let pan_delta = if let Some(pan_start) = self.pan_start {
185            self.pan_end - pan_start
186        } else {
187            Vector2::zero()
188        };
189
190        self.pan(pan_delta);
191
192        self.pan_start = Some(self.pan_end);
193
194        self.update();
195    }
196
197    fn pan(&mut self, delta: Vector2) {
198        if self.camera.is_perspective() {
199            let offset = self.position - self.target;
200            let mut target_distance = offset.magnitude();
201
202            // half of the fov is center to top of screen
203            target_distance *= (self.camera.fovy / 2.0).tan() * PI / 180.0;
204
205            // we actually don't use screen_width, since perspective camera is fixed to screen height
206            let distance = 50.0 * delta.x * target_distance / self.screen_size.height as f32;
207            self.pan_left(-distance);
208            let distance = 50.0 * delta.y * target_distance / self.screen_size.height as f32;
209            self.pan_up(-distance);
210        } else {
211            // TODO!: orthographic camera pan
212            warn!("unimplemented: orthographic camera pan")
213        }
214    }
215
216    pub fn pan_left(&mut self, distance: f32) {
217        self.pan_offset.x -= distance
218    }
219
220    pub fn pan_up(&mut self, distance: f32) {
221        self.pan_offset.y -= distance
222    }
223
224    // Processes input received from a mouse scroll-wheel event. Only requires input on the vertical wheel-axis
225    pub fn process_mouse_scroll(&mut self, mut yoffset: f32) {
226        yoffset *= ZOOM_SENSITIVITY;
227        if self.camera.fovy.0 >= MIN_ZOOM && self.camera.fovy.0 <= MAZ_ZOOM {
228            self.camera.fovy.0 -= yoffset;
229        }
230        if self.camera.fovy.0 <= MIN_ZOOM {
231            self.camera.fovy.0 = MIN_ZOOM;
232        }
233        if self.camera.fovy.0 >= MAZ_ZOOM {
234            self.camera.fovy.0 = MAZ_ZOOM;
235        }
236        self.camera.update_projection_matrix();
237    }
238
239    /// Update camera after processing mouse events
240    fn update(&mut self) {
241        let mut offset = self.position - self.target;
242
243        // NOTE: skipping rotate offset to "y-axis-is-up" space
244
245        // angle from z-axis around y-axis
246        self.spherical = Spherical::from_vec3(offset);
247
248        self.spherical.theta += self.spherical_delta.theta;
249        self.spherical.phi += self.spherical_delta.phi;
250
251        // NOTE!: left out theta restrictions / make_safe for now
252
253        // restrict phi to be between desired limits
254        let epsilon = 0.0001;
255        self.spherical.phi = clamp(self.spherical.phi, epsilon, PI - epsilon);
256
257        self.spherical.radius *= self.scale;
258
259        // TODO?: restrict radius to be between desired limits?
260
261        // move target to panned location
262        // NOTE: quite different from original
263        // NOTE: skipped from original: rotate offset back to "camera-up-vector-is-up" space
264        let pan_speed = 2.0; // TODO!!: test on non-retina display
265        self.pan_offset *= pan_speed;
266        let right = offset.cross(Vector3::unit_y()).normalize();
267        let up = right.cross(offset).normalize();
268        self.position += right * self.pan_offset.x;
269        self.position += up * self.pan_offset.y;
270        self.target += right * self.pan_offset.x;
271        self.target += up * self.pan_offset.y;
272
273        // apply rotation
274        offset = self.spherical.to_vec3();
275        self.position = self.target + offset;
276
277        // TODO?: if enable_damping...?
278        self.spherical_delta = Spherical::from_vec3(Vector3::zero());
279
280        self.scale = 1.0;
281        self.pan_offset = Vector3::zero();
282
283        // NOTE: skip zoomChanged stuff
284
285        trace!("Position: {:?}\tTarget: {:?}\tfovy: {:?}", self.position, self.target, Deg(self.camera.fovy));
286    }
287
288    pub fn process_keyboard(&mut self, direction: CameraMovement, pressed: bool) {
289        match direction {
290            FORWARD => self.moving_forward = pressed,
291            BACKWARD => self.moving_backward= pressed,
292            LEFT => self.moving_left = pressed,
293            RIGHT => self.moving_right = pressed,
294        }
295    }
296
297    /// Do frame-based updates that require delta_time
298    pub fn frame_update(&mut self, delta_time: f64) {
299        let velocity = SPEED * delta_time as f32;
300
301        let front = (self.target - self.position).normalize();
302        if self.moving_forward {
303            self.position += front * velocity;
304            self.target += front * velocity;
305        }
306        if self.moving_backward {
307            self.position += -(front * velocity);
308            self.target += -(front * velocity);
309        }
310
311        let right = front.cross(Vector3::unit_y()).normalize();
312        if self.moving_left {
313            self.position += -(right * velocity);
314            self.target += -(right * velocity);
315        }
316        if self.moving_right {
317            self.position += right * velocity;
318            self.target += right * velocity;
319        }
320    }
321
322    pub fn set_camera(&mut self, camera: &Camera, transform: &Matrix4) {
323        // spec: If no transformation is specified, the location of the camera is at the origin.
324        let pos = transform * vec4(0.0, 0.0, 0.0, 1.0);
325
326        // spec: ... the lens looks towards the local -Z axis ...
327        let look_at = transform * vec4(0.0, 0.0, -1.0, 0.0);
328
329        self.position = Point3::new(pos.x, pos.y, pos.z);
330        self.target = Point3::new(look_at.x, look_at.y, look_at.z);
331
332        // TODO!!: retaining current window aspect ratio for now... later maybe resize window accordingly?
333        let mut camera = camera.clone();
334        camera.update_aspect_ratio(self.camera.aspect_ratio());
335        self.camera = camera;
336
337        self.camera.update_projection_matrix();
338    }
339}