Skip to main content

nightshade/ecs/camera/components/
perspective.rs

1use nalgebra_glm::Mat4;
2
3/// Perspective projection settings following glTF conventions.
4///
5/// Uses reverse-Z depth buffer for improved precision. Supports infinite
6/// far plane when `z_far` is `None`.
7#[derive(
8    Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize, enum2schema::Schema,
9)]
10pub struct PerspectiveCamera {
11    /// Aspect ratio (width/height). If `None`, uses viewport aspect ratio.
12    pub aspect_ratio: Option<f32>,
13    /// Vertical field of view in radians.
14    pub y_fov_rad: f32,
15    /// Far clipping plane distance. If `None`, uses infinite far plane.
16    pub z_far: Option<f32>,
17    /// Near clipping plane distance.
18    pub z_near: f32,
19}
20
21impl Default for PerspectiveCamera {
22    fn default() -> Self {
23        Self {
24            aspect_ratio: None,
25            y_fov_rad: 45.0_f32.to_radians(),
26            z_far: None,
27            z_near: 0.01,
28        }
29    }
30}
31
32impl PerspectiveCamera {
33    /// Returns the projection matrix using stored or default aspect ratio.
34    pub fn matrix(&self) -> Mat4 {
35        let aspect_ratio = self.aspect_ratio.unwrap_or(16.0 / 9.0);
36        self.matrix_with_aspect(aspect_ratio)
37    }
38
39    /// Returns the projection matrix with the specified aspect ratio.
40    pub fn matrix_with_aspect(&self, aspect_ratio: f32) -> Mat4 {
41        match self.z_far {
42            Some(z_far) => reverse_z_perspective(aspect_ratio, self.y_fov_rad, self.z_near, z_far),
43            None => infinite_reverse_z_perspective(aspect_ratio, self.y_fov_rad, self.z_near),
44        }
45    }
46}
47
48fn infinite_reverse_z_perspective(aspect_ratio: f32, y_fov_rad: f32, z_near: f32) -> Mat4 {
49    let f = 1.0 / (y_fov_rad / 2.0).tan();
50    Mat4::new(
51        f / aspect_ratio,
52        0.0,
53        0.0,
54        0.0,
55        0.0,
56        f,
57        0.0,
58        0.0,
59        0.0,
60        0.0,
61        0.0,
62        z_near,
63        0.0,
64        0.0,
65        -1.0,
66        0.0,
67    )
68}
69
70fn reverse_z_perspective(aspect_ratio: f32, y_fov_rad: f32, z_near: f32, z_far: f32) -> Mat4 {
71    let f = 1.0 / (y_fov_rad / 2.0).tan();
72    Mat4::new(
73        f / aspect_ratio,
74        0.0,
75        0.0,
76        0.0,
77        0.0,
78        f,
79        0.0,
80        0.0,
81        0.0,
82        0.0,
83        z_near / (z_far - z_near),
84        z_near * z_far / (z_far - z_near),
85        0.0,
86        0.0,
87        -1.0,
88        0.0,
89    )
90}