gloss_renderer/
camera.rs

1extern crate nalgebra as na;
2extern crate nalgebra_glm as glm;
3
4use log::warn;
5use winit::{
6    dpi::PhysicalPosition,
7    event::{MouseButton, MouseScrollDelta, Touch},
8};
9
10use crate::{
11    components::{CamController, CamMode, PosLookat, Projection, ProjectionWithIntrinsics, TargetResolution, TargetResolutionUpdate},
12    scene::Scene,
13};
14use gloss_hecs::Entity;
15
16/// Camera implements most of the functionality related to cameras. It deals
17/// with processing of mouse events and with moving the camera in an orbit like
18/// manner. It contains a reference to the entity in the world so that changes
19/// done by the camera object will directly affect the entity.
20#[repr(C)]
21#[derive(Clone)]
22pub struct Camera {
23    pub entity: Entity,
24}
25
26impl Camera {
27    #[allow(clippy::missing_panics_doc)] //really will never panic because the entity definitelly already exists in the
28                                         // world
29    pub fn new(name: &str, scene: &mut Scene, initialize: bool) -> Self {
30        let entity = scene
31            .get_or_create_hidden_entity(name)
32            .insert(CamController::default())
33            .insert(TargetResolution::default())
34            .entity();
35        if initialize {
36            scene.world.insert_one(entity, PosLookat::default()).ok();
37            scene.world.insert_one(entity, Projection::default()).ok();
38        }
39        Self { entity }
40    }
41
42    pub fn from_entity(entity: Entity) -> Self {
43        Self { entity }
44    }
45
46    #[allow(clippy::missing_panics_doc)]
47    pub fn is_initialized(&self, scene: &Scene) -> bool {
48        scene.world.has::<PosLookat>(self.entity).unwrap()
49            && (scene.world.has::<Projection>(self.entity).unwrap() || scene.world.has::<ProjectionWithIntrinsics>(self.entity).unwrap())
50    }
51
52    /// # Panics
53    /// Will panic if the ``PosLookat`` component does not exist for this entity
54    pub fn view_matrix(&self, scene: &Scene) -> na::Matrix4<f32> {
55        let pos_lookat = scene.get_comp::<&PosLookat>(&self.entity).unwrap();
56        pos_lookat.view_matrix()
57    }
58
59    /// # Panics
60    /// Will panic if the ``Projection`` component does not exist for this
61    /// entity
62    pub fn proj_matrix(&self, scene: &Scene) -> na::Matrix4<f32> {
63        let proj = scene.get_comp::<&Projection>(&self.entity).unwrap();
64        let (width, height) = self.get_target_res(scene);
65        proj.proj_matrix(width, height)
66    }
67
68    /// # Panics
69    /// Will panic if the ``Projection`` component does not exist for this
70    /// entity
71    pub fn proj_matrix_reverse_z(&self, scene: &Scene) -> na::Matrix4<f32> {
72        let proj = scene.get_comp::<&Projection>(&self.entity).unwrap();
73        let (width, height) = self.get_target_res(scene);
74        proj.proj_matrix_reverse_z(width, height)
75    }
76
77    /// Performs a two-axis rotation of the camera by mapping the xy
78    /// coordianates of the mouse to rotation around the Y axis of the world and
79    /// the X axis of the camera.
80    pub fn two_axis_rotation(
81        cam_axis_x: na::Vector3<f32>,
82        viewport_size: na::Vector2<f32>,
83        speed: f32,
84        prev_mouse: na::Vector2<f32>,
85        current_mouse: na::Vector2<f32>,
86    ) -> (na::Rotation3<f32>, na::Rotation3<f32>) {
87        // rotate around Y axis of the world (the vector 0,1,0)
88        let angle_y = std::f32::consts::PI * (prev_mouse.x - current_mouse.x) / viewport_size.x * speed;
89        let rot_y = na::Rotation3::from_axis_angle(&na::Vector3::y_axis(), angle_y);
90
91        //rotate around x axis of the camera coordinate
92        let axis_x = cam_axis_x;
93        let axis_x = na::Unit::new_normalize(axis_x);
94        let angle_x = std::f32::consts::PI * (prev_mouse.y - current_mouse.y) / viewport_size.y * speed;
95        let rot_x = na::Rotation3::from_axis_angle(&axis_x, angle_x);
96
97        (rot_y, rot_x)
98    }
99
100    /// Projects from 3D world to 2D screen coordinates in the range [0,
101    /// `viewport_width`] and [0, `viewport_height`]
102    pub fn project(
103        &self,
104        point_world: na::Point3<f32>,
105        view: na::Matrix4<f32>,
106        proj: na::Matrix4<f32>,
107        viewport_size: na::Vector2<f32>,
108    ) -> na::Vector3<f32> {
109        //get the point from world to screen space
110        let p_view = view * point_world.to_homogeneous();
111        let mut p_proj = proj * p_view;
112        p_proj = p_proj / p_proj.w;
113        p_proj = p_proj * 0.5 + na::Vector4::<f32>::new(0.5, 0.5, 0.5, 0.5);
114        p_proj.x *= viewport_size.x;
115        p_proj.y *= viewport_size.y;
116
117        p_proj.fixed_rows::<3>(0).clone_owned()
118    }
119
120    /// Unprojects from 2D screen coordinates in range [0, `viewport_width`] and
121    /// [0, `viewport_height`] to 3D world # Panics
122    /// Will panic if the proj*view matrix is not invertable
123    pub fn unproject(
124        &self,
125        win: na::Point3<f32>,
126        view: na::Matrix4<f32>,
127        proj: na::Matrix4<f32>,
128        viewport_size: na::Vector2<f32>,
129    ) -> na::Vector3<f32> {
130        let inv = (proj * view).try_inverse().unwrap();
131
132        let mut tmp = win.to_homogeneous();
133        tmp.x /= viewport_size.x;
134        tmp.y /= viewport_size.y;
135        tmp = tmp * 2.0 - na::Vector4::<f32>::new(-1.0, -1.0, -1.0, -1.0);
136
137        let mut obj = inv * tmp;
138        obj = obj / obj.w;
139
140        let scene = obj.fixed_rows::<3>(0).clone_owned();
141
142        scene
143    }
144
145    /// Clear the click state once processed, so this doesnt keep running
146    pub fn clear_click(&self, scene: &mut Scene) {
147        let mut cam_control = scene.get_comp::<&mut CamController>(&self.entity).unwrap();
148        cam_control.is_last_press_click = false;
149    }
150
151    // Since we use LMB for both rotation and selection, rotations tamper with the selection
152    // which can be a little annoying. To avoid this we do -
153    // LMB click + release (within 0.25s) -> entity selection
154    // LMB click + hold + move -> camera rotation
155    pub fn is_click(&self, scene: &Scene) -> bool {
156        let cam_control = scene.get_comp::<&CamController>(&self.entity).unwrap();
157        cam_control.is_last_press_click
158    }
159
160    /// Handle the event of touching with a finger
161    /// # Panics
162    /// Will panic if the ``CamController`` component does not exist for this
163    /// entity
164    pub fn touch_pressed(&mut self, touch_event: &Touch, scene: &mut Scene) {
165        // println!("mouse pressed");
166        let mut cam_control = scene.get_comp::<&mut CamController>(&self.entity).unwrap();
167
168        cam_control.id2active_touches.insert(touch_event.id, *touch_event);
169
170        cam_control.mouse_pressed = true;
171
172        //adding a finger already invalidates the prev mouse point. If there was one
173        // finger before we added, then it means we switch from having the previous
174        // position be at the finger to it being in between the two fingers so we
175        // invalidate previous state
176        cam_control.prev_mouse_pos_valid = false;
177
178        cam_control.last_press = Some(wasm_timer::Instant::now());
179    }
180
181    /// Handle the event of pressing mouse
182    /// # Panics
183    /// Will panic if the ``CamController`` component does not exist for this
184    /// entity
185    pub fn touch_released(&mut self, touch_event: &Touch, scene: &mut Scene) {
186        let mut cam_control = scene.get_comp::<&mut CamController>(&self.entity).unwrap();
187
188        cam_control.id2active_touches.remove(&touch_event.id);
189
190        //no fingers are touching the screen
191        if cam_control.id2active_touches.is_empty() {
192            cam_control.mouse_pressed = false;
193            cam_control.prev_mouse_pos_valid = false;
194        }
195        //releasing a finger already invalidates the prev mouse point. If there was one
196        // finger before we removed, then we definitelly want to invalidate. If there
197        // were two then it means we switch from the center of the two fingers to just
198        // one finger so we also invalidate the previous pos
199        cam_control.prev_mouse_pos_valid = false;
200        cam_control.decide_if_click();
201        cam_control.mouse_moved_while_pressed = false; //do this AFTER decide_if_click because that function depends on if the mouse has moved while pressed
202    }
203
204    /// # Panics
205    /// Will panic if the ``CamController`` component does not exist for this
206    /// entity
207    pub fn reset_all_touch_presses(&mut self, scene: &mut Scene) {
208        let mut cam_control = scene.get_comp::<&mut CamController>(&self.entity).unwrap();
209
210        cam_control.id2active_touches.clear();
211
212        cam_control.mouse_pressed = false;
213        cam_control.prev_mouse_pos_valid = false;
214        cam_control.prev_mouse_pos_valid = false;
215        cam_control.mouse_moved_while_pressed = false;
216    }
217
218    /// # Panics
219    /// Will panic if the ``CamController`` component does not exist for this
220    /// entity
221    #[allow(clippy::cast_possible_truncation)]
222    #[allow(clippy::cast_lossless)]
223    pub fn process_touch_move(&mut self, touch_event: &Touch, viewport_width: u32, viewport_height: u32, scene: &mut Scene) {
224        let touches = {
225            let cam_control = scene.get_comp::<&CamController>(&self.entity).unwrap();
226            let all_touches: Vec<Touch> = cam_control.id2active_touches.values().copied().collect();
227            all_touches
228        };
229
230        //if we have only one finger, we rotate
231        if touches.len() == 1 {
232            {
233                let mut cam_control = scene.get_comp::<&mut CamController>(&self.entity).unwrap();
234                cam_control.mouse_mode = CamMode::Rotation;
235            }
236            self.process_mouse_move(touch_event.location.x, touch_event.location.y, viewport_width, viewport_height, scene);
237            //update position of this finger
238            let mut cam_control = scene.get_comp::<&mut CamController>(&self.entity).unwrap();
239            cam_control.id2active_touches.insert(touch_event.id, *touch_event);
240        } else if touches.len() == 2 {
241            //calculate difference between the two fingers, did it increase or decrease
242            // from the previous frame
243            let pos0 = touches[0].location;
244            let pos1 = touches[1].location;
245            let pos0_na = na::Vector2::<f64>::new(pos0.x / viewport_width as f64, pos0.y / viewport_height as f64);
246            let pos1_na = na::Vector2::<f64>::new(pos1.x / viewport_width as f64, pos1.y / viewport_height as f64);
247            let diff_prev = (pos0_na - pos1_na).norm();
248            // //update position of this finger
249            let current_touches = {
250                let mut cam_control = scene.get_comp::<&mut CamController>(&self.entity).unwrap();
251                cam_control.id2active_touches.insert(touch_event.id, *touch_event);
252                let current_touches: Vec<Touch> = cam_control.id2active_touches.values().copied().collect();
253                current_touches
254            };
255            //current difference between fingers
256            let pos0 = current_touches[0].location;
257            let pos1 = current_touches[1].location;
258            let pos0_na = na::Vector2::<f64>::new(pos0.x / viewport_width as f64, pos0.y / viewport_height as f64);
259            let pos1_na = na::Vector2::<f64>::new(pos1.x / viewport_width as f64, pos1.y / viewport_height as f64);
260            let diff_cur = (pos0_na - pos1_na).norm();
261
262            //check if the difference increased or not
263            {
264                let inc = diff_cur - diff_prev;
265                let speed = 1.0;
266                let mut pos_lookat = scene.get_comp::<&mut PosLookat>(&self.entity).unwrap();
267                pos_lookat.dolly(inc as f32 * speed);
268            }
269
270            //also move the camera depening on where the center of the two fingers moved
271            {
272                let mut cam_control = scene.get_comp::<&mut CamController>(&self.entity).unwrap();
273                cam_control.mouse_mode = CamMode::Translation;
274            }
275            let center = na::Vector2::<f64>::new((pos0.x + pos1.x) / 2.0, (pos0.y + pos1.y) / 2.0);
276            self.process_mouse_move(center.x, center.y, viewport_width, viewport_height, scene);
277        }
278    }
279
280    /// Handle the event of pressing mouse
281    /// # Panics
282    /// Will panic if the ``CamController`` component does not exist for this
283    /// entity
284    pub fn mouse_pressed(&mut self, mouse_button: &MouseButton, scene: &mut Scene) {
285        // println!("mouse pressed");
286        let mut cam_control = scene.get_comp::<&mut CamController>(&self.entity).unwrap();
287
288        match mouse_button {
289            MouseButton::Left => {
290                cam_control.last_press = Some(wasm_timer::Instant::now());
291                cam_control.mouse_mode = CamMode::Rotation;
292                cam_control.mouse_pressed = true;
293            }
294            MouseButton::Right => {
295                cam_control.mouse_mode = CamMode::Translation;
296                cam_control.mouse_pressed = true;
297            }
298            _ => {
299                cam_control.mouse_pressed = false;
300            }
301        }
302    }
303
304    /// Handle the event of releasing mouse
305    /// # Panics
306    /// Will panic if the ``CamController`` component does not exist for this
307    /// entity
308    pub fn mouse_released(&mut self, scene: &mut Scene) {
309        let mut cam_control = scene.get_comp::<&mut CamController>(&self.entity).unwrap();
310
311        cam_control.mouse_pressed = false;
312        cam_control.prev_mouse_pos_valid = false;
313        cam_control.decide_if_click();
314        cam_control.mouse_moved_while_pressed = false; //do this AFTER decide_if_click because that function depends on if the mouse has moved while pressed
315    }
316
317    /// Handle the event of dragging the mouse on the window
318    /// # Panics
319    /// Will panic if the ``PosLookat``, ``Projection``, ``CamController``
320    /// component does not exist for this entity
321    pub fn process_mouse_move(&mut self, position_x: f64, position_y: f64, viewport_width: u32, viewport_height: u32, scene: &mut Scene) {
322        let proj = self.proj_matrix(scene);
323        let mut pos_lookat = scene.get_comp::<&mut PosLookat>(&self.entity).unwrap();
324        let mut cam_control = scene.get_comp::<&mut CamController>(&self.entity).unwrap();
325
326        cam_control.cursor_position = Some(winit::dpi::PhysicalPosition::new(position_x, position_y));
327
328        #[allow(clippy::cast_possible_truncation)]
329        let (x, y) = (position_x as f32, position_y as f32);
330
331        let current_mouse = na::Vector2::<f32>::new(x, y);
332        #[allow(clippy::cast_precision_loss)] //it's ok, we don't have very big viewport sizes
333        let viewport_size = na::Vector2::<f32>::new(viewport_width as f32, viewport_height as f32);
334
335        if cam_control.mouse_pressed {
336            if cam_control.prev_mouse_pos_valid {
337                cam_control.mouse_moved_while_pressed = true;
338            }
339
340            if cam_control.mouse_mode == CamMode::Rotation && cam_control.prev_mouse_pos_valid {
341                let speed = 2.0;
342                let (rot_y, mut rot_x) = Self::two_axis_rotation(
343                    na::Vector3::from(pos_lookat.cam_axes().column(0)),
344                    viewport_size,
345                    speed,
346                    cam_control.prev_mouse,
347                    current_mouse,
348                );
349
350                //calculate the new position as if we apply this rotation
351                let mut new_pos_lookat = pos_lookat.clone();
352                new_pos_lookat.orbit(rot_x);
353                //calculate vertical angle
354                let dot_up = new_pos_lookat.direction().dot(&na::Vector3::<f32>::y_axis());
355                let angle_vertical = dot_up.acos();
356                // println!("angle vertical {}", angle_vertical);
357                if let Some(max_vertical_angle) = cam_control.limit_max_vertical_angle {
358                    if angle_vertical > max_vertical_angle || new_pos_lookat.up == na::Vector3::<f32>::new(0.0, -1.0, 0.0) {
359                        rot_x = na::Rotation3::<f32>::identity();
360                    }
361                }
362                if let Some(min_vertical_angle) = cam_control.limit_min_vertical_angle {
363                    if angle_vertical < min_vertical_angle {
364                        rot_x = na::Rotation3::<f32>::identity();
365                    }
366                }
367
368                let rot = rot_y * rot_x;
369                pos_lookat.orbit(rot);
370            } else if cam_control.mouse_mode == CamMode::Translation && cam_control.prev_mouse_pos_valid {
371                let view = pos_lookat.view_matrix();
372
373                let coord = self.project(pos_lookat.lookat, view, proj, viewport_size);
374                let down_mouse_z = coord.z;
375
376                let pos1 = self.unproject(na::Point3::<f32>::new(x, viewport_size.y - y, down_mouse_z), view, proj, viewport_size);
377                let pos0 = self.unproject(
378                    na::Point3::<f32>::new(cam_control.prev_mouse.x, viewport_size.y - cam_control.prev_mouse.y, down_mouse_z),
379                    view,
380                    proj,
381                    viewport_size,
382                );
383                let diff = pos1 - pos0;
384                // diff.array()*=speed_multiplier;
385                let new_pos = pos_lookat.position - diff;
386                pos_lookat.shift_cam(new_pos);
387            }
388            cam_control.prev_mouse = current_mouse;
389            cam_control.prev_mouse_pos_valid = true;
390        }
391    }
392
393    /// Handle event of scrolling the mouse wheel. It performs a zoom.
394    /// # Panics
395    /// Will panic if the ``PosLookat``, ``CamController`` component does not
396    /// exist for this entity
397    pub fn process_mouse_scroll(&mut self, delta: &MouseScrollDelta, scene: &mut Scene) {
398        let mut pos_lookat = scene.get_comp::<&mut PosLookat>(&self.entity).unwrap();
399        let cam_control = scene.get_comp::<&mut CamController>(&self.entity).unwrap();
400
401        let scroll = match delta {
402            // I'm assuming a line is about 100 pixels
403            MouseScrollDelta::LineDelta(_, scroll) => f64::from(scroll * 0.5),
404            #[allow(clippy::cast_precision_loss)] //it's ok, we don't have very big numbers
405            MouseScrollDelta::PixelDelta(PhysicalPosition { y: scroll, .. }) => *scroll,
406        };
407
408        let mut s = if scroll > 0.0 { 0.1 } else { -0.1 };
409
410        if let Some(max_dist) = cam_control.limit_max_dist {
411            let cur_dist = pos_lookat.dist_lookat();
412            if cur_dist > max_dist && s < 0.0 {
413                s = 0.0;
414            }
415        }
416
417        // self.push_away(val);
418        pos_lookat.dolly(s);
419    }
420
421    /// Resizing the window means that the projection matrix of the camera has
422    /// to change accordingly so as to not squish the scene # Panics
423    /// Will panic if the ``Projection`` component does not exist for this
424    /// entity
425    pub fn set_aspect_ratio(&mut self, val: f32, scene: &mut Scene) {
426        //extract data
427        let mut proj = scene.get_comp::<&mut Projection>(&self.entity).unwrap();
428        if let Projection::WithFov(ref mut proj) = *proj {
429            proj.aspect_ratio = val;
430        }
431    }
432
433    /// Resizing the window means that the projection matrix of the camera has
434    /// to change accordingly so as to not squish the scene
435    pub fn set_aspect_ratio_maybe(&mut self, val: f32, scene: &mut Scene) {
436        if let Ok(mut proj) = scene.get_comp::<&mut Projection>(&self.entity) {
437            if let Projection::WithFov(ref mut proj) = *proj {
438                proj.aspect_ratio = val;
439            }
440        } else if scene.nr_renderables() != 0 {
441            //if the nr of renderabled is 0 then might not have a projection matrix and
442            // that's fine because when we add renderables, the prepass will run and add a
443            // projection matrix
444            warn!("No Projection component yet so we couldn't set aspect ratio. This may not be an issue since the prepass might fix this. Ideally this warning should only appear at most once");
445        }
446    }
447
448    pub fn near_far(&self, scene: &mut Scene) -> (f32, f32) {
449        let proj = scene.get_comp::<&Projection>(&self.entity).unwrap();
450        proj.near_far()
451    }
452
453    /// Unconditionally sets the target res, regardless of the update mode
454    pub fn set_target_res(&mut self, width: u32, height: u32, scene: &mut Scene) {
455        {
456            let mut res = scene.get_comp::<&mut TargetResolution>(&self.entity).unwrap();
457            res.width = width;
458            res.height = height;
459        }
460
461        //sync also aspect ratio
462        #[allow(clippy::cast_precision_loss)]
463        self.set_aspect_ratio_maybe(width as f32 / height as f32, scene);
464    }
465
466    /// Sets the target res on window resizing, only updates if the updatemode
467    /// is `WindowSize`
468    pub fn on_window_resize(&mut self, width: u32, height: u32, scene: &mut Scene) {
469        {
470            let mut res = scene.get_comp::<&mut TargetResolution>(&self.entity).unwrap();
471            if res.update_mode == TargetResolutionUpdate::WindowSize {
472                res.width = width;
473                res.height = height;
474            }
475        }
476
477        //sync also aspect ratio
478        #[allow(clippy::cast_precision_loss)]
479        self.set_aspect_ratio_maybe(width as f32 / height as f32, scene);
480    }
481
482    pub fn get_target_res(&self, scene: &Scene) -> (u32, u32) {
483        let res = scene.get_comp::<&TargetResolution>(&self.entity).unwrap();
484        (res.width, res.height)
485    }
486}