Skip to main content

libdqg/
camera.rs

1#[repr(C)]
2#[derive(Debug, Clone, Copy)]
3pub(crate) struct CameraUniform {
4    view_proj: [[f32; 4]; 4],
5}
6
7impl CameraUniform {
8    pub(crate) fn new() -> Self {
9        Self {
10            view_proj: glam::Mat4::IDENTITY.to_cols_array_2d(),
11        }
12    }
13
14    pub(crate) fn update(&mut self, camera: &Camera) {
15        self.view_proj = camera.build_view_projection_matrix().to_cols_array_2d();
16    }
17}
18
19#[derive(Clone, Copy)]
20pub struct Camera {
21    pub position: glam::Vec3,
22    /// Radians, rotation around world +Y.
23    pub yaw: f32,
24    /// Radians. Callers should keep this in roughly (-PI/2, PI/2) to avoid the view flipping
25    /// through the poles — `up` is fixed to world +Y, so there's no roll to fall back on there.
26    pub pitch: f32,
27    pub aspect: f32,
28    /// Vertical field of view, in degrees.
29    pub fov: f32,
30    pub znear: f32,
31    pub zfar: f32,
32}
33
34impl Camera {
35    pub fn forward(&self) -> glam::Vec3 {
36        glam::Vec3::new(
37            self.yaw.cos() * self.pitch.cos(),
38            self.pitch.sin(),
39            self.yaw.sin() * self.pitch.cos(),
40        )
41    }
42
43    /// Extracts yaw/pitch (no roll) from a normalized direction vector — the inverse of
44    /// `forward()`. Shared by `look_at` and by `CameraComponent::derive_camera`, which both need
45    /// to turn a direction into this camera's angle representation.
46    pub fn yaw_pitch_from_forward(forward: glam::Vec3) -> (f32, f32) {
47        (forward.z.atan2(forward.x), forward.y.clamp(-1.0, 1.0).asin())
48    }
49
50    /// Points the camera at `target` from its current `position`, for call sites that think in
51    /// terms of "look at this point" rather than angles.
52    pub fn look_at(&mut self, target: glam::Vec3) {
53        let (yaw, pitch) = Self::yaw_pitch_from_forward((target - self.position).normalize());
54        self.yaw = yaw;
55        self.pitch = pitch;
56    }
57
58    pub fn build_view_projection_matrix(&self) -> glam::Mat4 {
59        let view = glam::camera::rh::view::look_to_mat4(self.position, self.forward(), glam::Vec3::Y);
60        let projection = glam::camera::rh::proj::directx::perspective(self.fov.to_radians(), self.aspect, self.znear, self.zfar);
61        projection * view
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    #[test]
70    fn forward_matches_identity_yaw_pitch() {
71        let camera = Camera { position: glam::Vec3::ZERO, yaw: 0.0, pitch: 0.0, aspect: 1.0, fov: 45.0, znear: 0.1, zfar: 100.0 };
72        assert!(camera.forward().abs_diff_eq(glam::Vec3::X, 1e-5));
73    }
74
75    #[test]
76    fn look_at_and_forward_round_trip() {
77        let mut camera = Camera { position: glam::Vec3::new(1.0, 2.0, 3.0), yaw: 0.0, pitch: 0.0, aspect: 1.0, fov: 45.0, znear: 0.1, zfar: 100.0 };
78        let target = glam::Vec3::new(-4.0, 5.0, 10.0);
79
80        camera.look_at(target);
81
82        let expected_dir = (target - camera.position).normalize();
83        assert!(camera.forward().abs_diff_eq(expected_dir, 1e-4));
84    }
85}