Skip to main content

embedded_3dgfx/
camera.rs

1use core::f32::consts;
2
3use nalgebra::{Isometry3, Perspective3, Point3, Vector3};
4
5pub struct Camera {
6    pub position: Point3<f32>,
7    fov: f32,
8    pub near: f32,
9    pub far: f32,
10    view_matrix: nalgebra::Matrix4<f32>,
11    projection_matrix: nalgebra::Matrix4<f32>,
12    pub vp_matrix: nalgebra::Matrix4<f32>,
13    target: Point3<f32>,
14    aspect_ratio: f32,
15}
16
17impl Camera {
18    pub fn new(aspect_ratio: f32) -> Camera {
19        let mut ret = Camera {
20            position: Point3::new(0.0, 0.0, 0.0),
21            fov: consts::PI / 2.0,
22            view_matrix: nalgebra::Matrix4::identity(),
23            projection_matrix: nalgebra::Matrix4::identity(),
24            vp_matrix: nalgebra::Matrix4::identity(),
25            target: Point3::new(0.0, 0.0, 0.0),
26            aspect_ratio,
27            near: 0.4,
28            far: 20.0,
29        };
30
31        ret.update_projection();
32
33        ret
34    }
35
36    pub fn set_position(&mut self, pos: Point3<f32>) {
37        self.position = pos;
38
39        self.update_view();
40    }
41
42    pub fn set_fovy(&mut self, fovy: f32) {
43        self.fov = fovy;
44
45        self.update_projection();
46    }
47
48    pub fn set_near(&mut self, near: f32) {
49        self.near = near;
50
51        self.update_projection();
52    }
53
54    pub fn set_far(&mut self, far: f32) {
55        self.far = far;
56
57        self.update_projection();
58    }
59
60    /// Set both near and far planes (for better Z-buffer precision)
61    ///
62    /// **Important**: Keep the near/far ratio as small as possible to reduce Z-fighting.
63    /// A ratio of 20:1 or less is recommended. For example:
64    /// - Small scene (0.5-10 units): near=0.5, far=10.0 (20:1)
65    /// - Medium scene (1-15 units): near=1.0, far=15.0 (15:1)
66    /// - Large scene (2-20 units): near=2.0, far=20.0 (10:1)
67    ///
68    /// See `ZBUFFER_TUNING.md` for detailed guidance.
69    pub fn set_near_far(&mut self, near: f32, far: f32) {
70        self.near = near;
71        self.far = far;
72
73        self.update_projection();
74    }
75
76    /// Get the current near/far ratio (lower is better for Z-buffer precision)
77    pub fn get_near_far_ratio(&self) -> f32 {
78        self.far / self.near
79    }
80
81    pub fn set_target(&mut self, target: Point3<f32>) {
82        self.target = target;
83        self.update_view();
84    }
85
86    pub fn get_direction(&self) -> Vector3<f32> {
87        let transpose = self.view_matrix; //.transpose();
88
89        Vector3::new(transpose[(2, 0)], transpose[(2, 1)], transpose[(2, 2)])
90    }
91
92    pub fn get_aspect_ratio(&self) -> f32 {
93        self.aspect_ratio
94    }
95
96    fn update_view(&mut self) {
97        let view = Isometry3::look_at_rh(&self.position, &self.target, &Vector3::y());
98
99        self.view_matrix = view.to_homogeneous();
100        self.vp_matrix = self.projection_matrix * self.view_matrix;
101    }
102
103    fn update_projection(&mut self) {
104        let projection = Perspective3::new(self.aspect_ratio, self.fov, self.near, self.far);
105        self.projection_matrix = projection.to_homogeneous();
106        self.vp_matrix = self.projection_matrix * self.view_matrix;
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn test_camera_creation() {
116        let camera = Camera::new(16.0 / 9.0);
117        assert!((camera.get_aspect_ratio() - 16.0 / 9.0).abs() < 0.001);
118        assert_eq!(camera.near, 0.4);
119        assert_eq!(camera.far, 20.0);
120        assert_eq!(camera.position, Point3::new(0.0, 0.0, 0.0));
121    }
122
123    #[test]
124    fn test_camera_set_position() {
125        let mut camera = Camera::new(1.0);
126        let new_pos = Point3::new(5.0, 10.0, 15.0);
127        camera.set_position(new_pos);
128        assert_eq!(camera.position, new_pos);
129    }
130
131    #[test]
132    fn test_camera_set_target() {
133        let mut camera = Camera::new(1.0);
134        let target = Point3::new(1.0, 2.0, 3.0);
135        camera.set_target(target);
136        assert_eq!(camera.target, target);
137    }
138
139    #[test]
140    fn test_camera_set_fovy() {
141        let mut camera = Camera::new(1.0);
142        let new_fov = core::f32::consts::PI / 4.0; // 45 degrees
143        camera.set_fovy(new_fov);
144        assert!((camera.fov - new_fov).abs() < 0.001);
145    }
146
147    #[test]
148    fn test_camera_get_direction() {
149        let mut camera = Camera::new(1.0);
150        camera.set_position(Point3::new(0.0, 0.0, 5.0));
151        camera.set_target(Point3::new(0.0, 0.0, 0.0));
152
153        let direction = camera.get_direction();
154        // Direction should point roughly toward target
155        assert!(direction.magnitude() > 0.0);
156    }
157
158    #[test]
159    fn test_camera_vp_matrix_updates() {
160        let mut camera = Camera::new(1.0);
161        let initial_vp = camera.vp_matrix;
162
163        // Change position should update VP matrix
164        camera.set_position(Point3::new(5.0, 5.0, 5.0));
165        assert_ne!(camera.vp_matrix, initial_vp);
166
167        let after_pos = camera.vp_matrix;
168
169        // Change FOV should update VP matrix
170        camera.set_fovy(core::f32::consts::PI / 4.0);
171        assert_ne!(camera.vp_matrix, after_pos);
172    }
173}