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    #[cfg(feature = "dsp")]
110    /// Smoothly track target position using low-pass damping filter.
111    pub fn smooth_track_dsp(&mut self, target: Point3<f32>, alpha: f32) {
112        let alpha_clamped = alpha.clamp(0.01, 1.0);
113        let cur = self.target;
114        let smoothed_x = cur.x + (target.x - cur.x) * alpha_clamped;
115        let smoothed_y = cur.y + (target.y - cur.y) * alpha_clamped;
116        let smoothed_z = cur.z + (target.z - cur.z) * alpha_clamped;
117        self.set_target(Point3::new(smoothed_x, smoothed_y, smoothed_z));
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn test_camera_creation() {
127        let camera = Camera::new(16.0 / 9.0);
128        assert!((camera.get_aspect_ratio() - 16.0 / 9.0).abs() < 0.001);
129        assert_eq!(camera.near, 0.4);
130        assert_eq!(camera.far, 20.0);
131        assert_eq!(camera.position, Point3::new(0.0, 0.0, 0.0));
132    }
133
134    #[test]
135    fn test_camera_set_position() {
136        let mut camera = Camera::new(1.0);
137        let new_pos = Point3::new(5.0, 10.0, 15.0);
138        camera.set_position(new_pos);
139        assert_eq!(camera.position, new_pos);
140    }
141
142    #[test]
143    fn test_camera_set_target() {
144        let mut camera = Camera::new(1.0);
145        let target = Point3::new(1.0, 2.0, 3.0);
146        camera.set_target(target);
147        assert_eq!(camera.target, target);
148    }
149
150    #[test]
151    fn test_camera_set_fovy() {
152        let mut camera = Camera::new(1.0);
153        let new_fov = core::f32::consts::PI / 4.0; // 45 degrees
154        camera.set_fovy(new_fov);
155        assert!((camera.fov - new_fov).abs() < 0.001);
156    }
157
158    #[test]
159    fn test_camera_get_direction() {
160        let mut camera = Camera::new(1.0);
161        camera.set_position(Point3::new(0.0, 0.0, 5.0));
162        camera.set_target(Point3::new(0.0, 0.0, 0.0));
163
164        let direction = camera.get_direction();
165        // Direction should point roughly toward target
166        assert!(direction.magnitude() > 0.0);
167    }
168
169    #[test]
170    fn test_camera_vp_matrix_updates() {
171        let mut camera = Camera::new(1.0);
172        let initial_vp = camera.vp_matrix;
173
174        // Change position should update VP matrix
175        camera.set_position(Point3::new(5.0, 5.0, 5.0));
176        assert_ne!(camera.vp_matrix, initial_vp);
177
178        let after_pos = camera.vp_matrix;
179
180        // Change FOV should update VP matrix
181        camera.set_fovy(core::f32::consts::PI / 4.0);
182        assert_ne!(camera.vp_matrix, after_pos);
183    }
184
185    #[test]
186    fn test_camera_projection_target_center() {
187        let mut camera = Camera::new(1.0); // 1:1 aspect ratio
188        camera.set_position(Point3::new(0.0, 0.0, 5.0));
189        camera.set_target(Point3::new(0.0, 0.0, 0.0));
190
191        // Target point (0, 0, 0) transformed by VP matrix
192        let p_target = nalgebra::Vector4::new(0.0, 0.0, 0.0, 1.0);
193        let clip = camera.vp_matrix * p_target;
194
195        // In homogeneous clip space, x and y must be 0.0 (centered on screen)
196        assert!(clip.x.abs() < 1e-4);
197        assert!(clip.y.abs() < 1e-4);
198        assert!(clip.w > 0.0); // Point is in front of camera
199    }
200
201    #[test]
202    fn test_camera_view_matrix_orthogonality() {
203        let mut camera = Camera::new(16.0 / 9.0);
204        camera.set_position(Point3::new(3.0, 4.0, 5.0));
205        camera.set_target(Point3::new(0.0, 1.0, 0.0));
206
207        // Extract upper-left 3x3 rotation matrix R from view matrix
208        let R = camera.view_matrix.fixed_view::<3, 3>(0, 0);
209        let I = R * R.transpose();
210
211        // R * R^T must equal Identity for orthogonal rotation matrix
212        let identity = nalgebra::Matrix3::identity();
213        let diff = (I - identity).norm();
214        assert!(
215            diff < 1e-4,
216            "View matrix rotation is not orthogonal: diff = {}",
217            diff
218        );
219    }
220}