Skip to main content

flow_ngin/
camera.rs

1use cgmath::num_traits::ToPrimitive;
2use cgmath::*;
3use core::f32;
4use std::f32::consts::FRAC_PI_2;
5use std::time::Duration;
6use winit::event::*;
7use winit::keyboard::KeyCode;
8use winit::{dpi::PhysicalPosition, keyboard::PhysicalKey};
9
10#[rustfmt::skip]
11pub const OPENGL_TO_WGPU_MATRIX: cgmath::Matrix4<f32> = cgmath::Matrix4::from_cols(
12    cgmath::Vector4::new(1.0, 0.0, 0.0, 0.0),
13    cgmath::Vector4::new(0.0, 1.0, 0.0, 0.0),
14    cgmath::Vector4::new(0.0, 0.0, 0.5, 0.0),
15    cgmath::Vector4::new(0.0, 0.0, 0.5, 1.0),
16);
17
18const SAFE_FRAC_PI_2: f32 = FRAC_PI_2 - 0.0001;
19
20fn screen_to_ndc(mouse_x: f32, mouse_y: f32, width: f32, height: f32) -> cgmath::Vector3<f32> {
21    let x = (2.0 * mouse_x / width) - 1.0;
22    let y = 1.0 - (2.0 * mouse_y) / height; // y is flipped
23    let z = 1.0;
24    Vector3::new(x, y, z)
25}
26
27#[derive(Debug)]
28pub struct Ray {
29    pub origin: Point3<f32>,
30    pub direction: Vector3<f32>,
31}
32
33// TODO: calculate intersection with depth buffer elem aswell for a picking alternative
34impl Ray {
35    /**
36     * Calculates the intersection of the ray `self` with the floor (y = 0.0).
37     *
38     * Returns None if the ray is not pointed towards the floor.
39     */
40    pub fn intersect_with_floor(&self) -> Option<Point2<f32>> {
41        if self.direction.y.abs() < f32::EPSILON {
42            return None;
43        }
44        let t = -self.origin.y / self.direction.y;
45        if t < 0.0 {
46            return None;
47        }
48        let intersection_point = self.origin + self.direction * t;
49        Some(Point2::new(intersection_point.x, intersection_point.z))
50    }
51}
52
53#[repr(C)]
54#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
55pub struct CameraUniform {
56    view_position: [f32; 4],
57    view_proj: [[f32; 4]; 4],
58}
59
60impl CameraUniform {
61    pub fn new() -> Self {
62        Self {
63            view_position: [0.0; 4],
64            view_proj: cgmath::Matrix4::identity().into(),
65        }
66    }
67
68    pub fn update_view_proj(&mut self, camera: &Camera, projection: &Projection) {
69        self.view_position = camera.position.to_homogeneous().into();
70        self.view_proj = (projection.calc_matrix() * camera.calc_matrix()).into();
71    }
72}
73
74#[derive(Debug, Clone)]
75pub struct Camera {
76    pub position: Point3<f32>,
77    yaw: Rad<f32>,
78    pitch: Rad<f32>,
79}
80
81impl Camera {
82    pub fn new<V: Into<Point3<f32>>, Y: Into<Rad<f32>>, P: Into<Rad<f32>>>(
83        position: V,
84        yaw: Y,
85        pitch: P,
86    ) -> Self {
87        Self {
88            position: position.into(),
89            yaw: yaw.into(),
90            pitch: pitch.into(),
91        }
92    }
93
94    pub fn calc_matrix(&self) -> Matrix4<f32> {
95        let (sin_pitch, cos_pitch) = self.pitch.0.sin_cos();
96        let (sin_yaw, cos_yaw) = self.yaw.0.sin_cos();
97
98        Matrix4::look_to_rh(
99            self.position,
100            Vector3::new(cos_pitch * cos_yaw, sin_pitch, cos_pitch * sin_yaw).normalize(),
101            Vector3::unit_y(),
102        )
103    }
104
105    /**
106     * This method casts a ray from the location of the mouse pointer using the camera's FOV and view.
107     */
108    pub fn cast_ray_from_mouse(
109        &self,
110        position: PhysicalPosition<f64>,
111        width: f32,
112        height: f32,
113        projection: &Projection,
114    ) -> Ray {
115        let (mouse_x, mouse_y) = position.into();
116        let ndc = screen_to_ndc(mouse_x, mouse_y, width, height);
117
118        let clip = cgmath::Vector4::new(ndc.x, ndc.y, 1.0, 1.0);
119
120        let inv_proj_view = (projection.calc_matrix() * self.calc_matrix())
121            .invert()
122            .unwrap();
123
124        let mut world_coords = inv_proj_view * clip;
125
126        world_coords /= world_coords.w;
127        // TODO: does it make sense to use Point3 or should I stay with Vector3?
128        let world_point = Point3::new(world_coords.x, world_coords.y, world_coords.z);
129
130        let ray_origin = self.position;
131        let ray_direction = (world_point - ray_origin).normalize();
132
133        Ray {
134            origin: ray_origin,
135            direction: ray_direction,
136        }
137    }
138}
139
140#[derive(Debug)]
141pub struct Projection {
142    aspect: f32,
143    fovy: Rad<f32>,
144    pub znear: f32,
145    pub zfar: f32,
146}
147
148impl Projection {
149    pub fn new<F: Into<Rad<f32>>>(
150        width: u32,
151        height: u32,
152        fovy: F,
153        znear: f32,
154        zfar: f32,
155    ) -> Result<Self, anyhow::Error> {
156        let width = width.to_f32().ok_or(anyhow::anyhow!(
157            "Width value {} is too large to be represented as f32.",
158            width
159        ))?;
160        let height = height.to_f32().ok_or(
161            anyhow::anyhow!(
162                "Height value {} is too large to be represented as f32.",
163                height
164            )
165        )?;
166        let aspect = width / height;
167        Ok(Self {
168            aspect,
169            fovy: fovy.into(),
170            znear,
171            zfar,
172        })
173    }
174
175    pub fn resize(&mut self, width: u32, height: u32) {
176        let width = width.to_f32().unwrap_or(f32::MAX);
177        let height = height.to_f32().unwrap_or(f32::MAX);
178        self.aspect = width as f32 / height as f32;
179    }
180
181    pub fn calc_matrix(&self) -> Matrix4<f32> {
182        OPENGL_TO_WGPU_MATRIX * perspective(self.fovy, self.aspect, self.znear, self.zfar)
183    }
184}
185
186#[derive(Debug, Clone)]
187pub struct CameraController {
188    amount_left: f32,
189    amount_right: f32,
190    amount_forward: f32,
191    amount_backward: f32,
192    amount_up: f32,
193    amount_down: f32,
194    rotate_horizontal: f32,
195    rotate_vertical: f32,
196    scroll: f32,
197    speed: f32,
198    sensitivity: f32,
199}
200
201impl CameraController {
202    pub fn new(speed: f32, sensitivity: f32) -> Self {
203        Self {
204            amount_left: 0.0,
205            amount_right: 0.0,
206            amount_forward: 0.0,
207            amount_backward: 0.0,
208            amount_up: 0.0,
209            amount_down: 0.0,
210            rotate_horizontal: 0.0,
211            rotate_vertical: 0.0,
212            scroll: 0.0,
213            speed,
214            sensitivity,
215        }
216    }
217
218    pub fn handle_window_events(&mut self, event: &WindowEvent) -> bool {
219        if let WindowEvent::KeyboardInput {
220            event:
221                KeyEvent {
222                    physical_key: PhysicalKey::Code(key),
223                    state: key_state,
224                    ..
225                },
226            ..
227        } = event
228        {
229            let amount = if key_state.is_pressed() { 1.0 } else { 0.0 };
230            match key {
231                KeyCode::KeyW | KeyCode::ArrowUp => {
232                    self.amount_forward = amount;
233                    true
234                }
235                KeyCode::KeyS | KeyCode::ArrowDown => {
236                    self.amount_backward = amount;
237                    true
238                }
239                KeyCode::KeyA | KeyCode::ArrowLeft => {
240                    self.amount_left = amount;
241                    true
242                }
243                KeyCode::KeyD | KeyCode::ArrowRight => {
244                    self.amount_right = amount;
245                    true
246                }
247                KeyCode::Space => {
248                    self.amount_up = amount;
249                    true
250                }
251                KeyCode::ShiftLeft => {
252                    self.amount_down = amount;
253                    true
254                }
255                _ => false,
256            }
257        } else {
258            false
259        }
260    }
261
262    pub fn handle_mouse(&mut self, mouse_dx: f64, mouse_dy: f64) {
263        let dx = mouse_dx as f32;
264        let dy = mouse_dy as f32;
265        // handle f32 to f64 conversion without panicing:
266        if dx.is_finite() && dy.is_finite() {
267            self.rotate_horizontal = dx;
268            self.rotate_vertical = dy;
269        } else {
270            log::warn!(
271                "Mouse coordinates of ({}, {}) are out of bounds and are not updated. The maximum supported coordinate value is {}.",
272                mouse_dx,
273                mouse_dy,
274                f32::MAX
275            );
276        }
277    }
278
279    pub fn handle_scroll(&mut self, delta: &MouseScrollDelta) {
280        self.scroll = match delta {
281            MouseScrollDelta::LineDelta(_, scroll) => -scroll * 0.5,
282            MouseScrollDelta::PixelDelta(PhysicalPosition { y: scroll, .. }) => -*scroll as f32,
283        };
284    }
285
286    pub fn update(&mut self, camera: &mut Camera, dt: Duration) {
287        let dt = dt.as_secs_f32();
288
289        let (yaw_sin, yaw_cos) = camera.yaw.0.sin_cos();
290        let forward = Vector3::new(yaw_cos, 0.0, yaw_sin).normalize();
291        let right = Vector3::new(-yaw_sin, 0.0, yaw_cos).normalize();
292        camera.position += forward * (self.amount_forward - self.amount_backward) * self.speed * dt;
293        camera.position += right * (self.amount_right - self.amount_left) * self.speed * dt;
294
295        // Move in/out (aka. "zoom")
296        // Note: this isn't an actual zoom. The camera's position
297        // changes when zooming. I've added this to make it easier
298        // to get closer to an object you want to focus on.
299        let (pitch_sin, pitch_cos) = camera.pitch.0.sin_cos();
300        let scrollward =
301            Vector3::new(pitch_cos * yaw_cos, pitch_sin, pitch_cos * yaw_sin).normalize();
302        camera.position += scrollward * self.scroll * self.speed * self.sensitivity * dt;
303        self.scroll = 0.0;
304
305        // Move up/down. Since we don't use roll, we can just
306        // modify the y coordinate directly.
307        camera.position.y += (self.amount_up - self.amount_down) * self.speed * dt;
308
309        // Rotate
310        camera.yaw += (Rad(self.rotate_horizontal) * self.speed * self.sensitivity * dt) / 10.0;
311        camera.pitch += (Rad(-self.rotate_vertical) * self.speed * self.sensitivity * dt) / 10.0;
312
313        // If process_mouse isn't called every frame, these values
314        // will not get set to zero, and the camera will rotate
315        // when moving in a non cardinal direction.
316        self.rotate_horizontal = 0.0;
317        self.rotate_vertical = 0.0;
318
319        // Keep the camera's angle from going too high/low.
320        if camera.pitch < -Rad(SAFE_FRAC_PI_2) {
321            camera.pitch = -Rad(SAFE_FRAC_PI_2);
322        } else if camera.pitch > Rad(SAFE_FRAC_PI_2) {
323            camera.pitch = Rad(SAFE_FRAC_PI_2);
324        }
325    }
326}
327
328#[derive(Debug)]
329pub struct CameraResources {
330    pub camera: Camera,
331    pub controller: CameraController,
332    pub uniform: CameraUniform,
333    pub buffer: wgpu::Buffer,
334    pub bind_group: wgpu::BindGroup,
335    pub bind_group_layout: wgpu::BindGroupLayout,
336}