Skip to main content

embedded_3dgfx/
camera.rs

1use core::f32::consts;
2
3#[cfg(feature = "render-layers")]
4use crate::render_layers::RenderLayers;
5use nalgebra::{Isometry3, Perspective3, Point3, Vector3};
6
7pub struct Camera {
8    pub position: Point3<f32>,
9    fov: f32,
10    pub near: f32,
11    pub far: f32,
12    pub view_matrix: nalgebra::Matrix4<f32>,
13    projection_matrix: nalgebra::Matrix4<f32>,
14    pub vp_matrix: nalgebra::Matrix4<f32>,
15    target: Point3<f32>,
16    aspect_ratio: f32,
17    /// Visibility layers this camera sees. Default: layer 0.
18    #[cfg(feature = "render-layers")]
19    pub layers: RenderLayers,
20}
21
22impl Camera {
23    pub fn new(aspect_ratio: f32) -> Camera {
24        let mut ret = Camera {
25            position: Point3::new(0.0, 0.0, 0.0),
26            fov: consts::PI / 2.0,
27            view_matrix: nalgebra::Matrix4::identity(),
28            projection_matrix: nalgebra::Matrix4::identity(),
29            vp_matrix: nalgebra::Matrix4::identity(),
30            target: Point3::new(0.0, 0.0, 0.0),
31            aspect_ratio,
32            near: 0.4,
33            far: 20.0,
34            #[cfg(feature = "render-layers")]
35            layers: RenderLayers::DEFAULT,
36        };
37
38        ret.update_projection();
39
40        ret
41    }
42
43    pub fn set_position(&mut self, pos: Point3<f32>) {
44        self.position = pos;
45
46        self.update_view();
47    }
48
49    pub fn set_fovy(&mut self, fovy: f32) {
50        self.fov = fovy;
51
52        self.update_projection();
53    }
54
55    /// Vertical field of view in radians.
56    pub fn fovy(&self) -> f32 {
57        self.fov
58    }
59
60    /// Restrict which mesh layers this camera sees.
61    #[cfg(feature = "render-layers")]
62    pub fn set_layers(&mut self, layers: RenderLayers) {
63        self.layers = layers;
64    }
65
66    pub fn set_near(&mut self, near: f32) {
67        self.near = near;
68
69        self.update_projection();
70    }
71
72    pub fn set_far(&mut self, far: f32) {
73        self.far = far;
74
75        self.update_projection();
76    }
77
78    /// Set both near and far planes (for better Z-buffer precision)
79    ///
80    /// **Important**: Keep the near/far ratio as small as possible to reduce Z-fighting.
81    /// A ratio of 20:1 or less is recommended. For example:
82    /// - Small scene (0.5-10 units): near=0.5, far=10.0 (20:1)
83    /// - Medium scene (1-15 units): near=1.0, far=15.0 (15:1)
84    /// - Large scene (2-20 units): near=2.0, far=20.0 (10:1)
85    ///
86    /// See `ZBUFFER_TUNING.md` for detailed guidance.
87    pub fn set_near_far(&mut self, near: f32, far: f32) {
88        self.near = near;
89        self.far = far;
90
91        self.update_projection();
92    }
93
94    /// Get the current near/far ratio (lower is better for Z-buffer precision)
95    pub fn get_near_far_ratio(&self) -> f32 {
96        self.far / self.near
97    }
98
99    pub fn set_target(&mut self, target: Point3<f32>) {
100        self.target = target;
101        self.update_view();
102    }
103
104    pub fn get_direction(&self) -> Vector3<f32> {
105        let transpose = self.view_matrix; //.transpose();
106
107        Vector3::new(transpose[(2, 0)], transpose[(2, 1)], transpose[(2, 2)])
108    }
109
110    pub fn get_aspect_ratio(&self) -> f32 {
111        self.aspect_ratio
112    }
113
114    fn update_view(&mut self) {
115        let view = Isometry3::look_at_rh(&self.position, &self.target, &Vector3::y());
116
117        self.view_matrix = view.to_homogeneous();
118        self.vp_matrix = self.projection_matrix * self.view_matrix;
119    }
120
121    fn update_projection(&mut self) {
122        let projection = Perspective3::new(self.aspect_ratio, self.fov, self.near, self.far);
123        self.projection_matrix = projection.to_homogeneous();
124        self.vp_matrix = self.projection_matrix * self.view_matrix;
125    }
126
127    #[cfg(feature = "dsp")]
128    /// Smoothly track target position using low-pass damping filter.
129    pub fn smooth_track_dsp(&mut self, target: Point3<f32>, alpha: f32) {
130        let alpha_clamped = alpha.clamp(0.01, 1.0);
131        let cur = self.target;
132        let smoothed_x = cur.x + (target.x - cur.x) * alpha_clamped;
133        let smoothed_y = cur.y + (target.y - cur.y) * alpha_clamped;
134        let smoothed_z = cur.z + (target.z - cur.z) * alpha_clamped;
135        self.set_target(Point3::new(smoothed_x, smoothed_y, smoothed_z));
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn test_camera_creation() {
145        let camera = Camera::new(16.0 / 9.0);
146        assert!((camera.get_aspect_ratio() - 16.0 / 9.0).abs() < 0.001);
147        assert_eq!(camera.near, 0.4);
148        assert_eq!(camera.far, 20.0);
149        assert_eq!(camera.position, Point3::new(0.0, 0.0, 0.0));
150    }
151
152    #[test]
153    fn test_camera_set_position() {
154        let mut camera = Camera::new(1.0);
155        let new_pos = Point3::new(5.0, 10.0, 15.0);
156        camera.set_position(new_pos);
157        assert_eq!(camera.position, new_pos);
158    }
159
160    #[test]
161    fn test_camera_set_target() {
162        let mut camera = Camera::new(1.0);
163        let target = Point3::new(1.0, 2.0, 3.0);
164        camera.set_target(target);
165        assert_eq!(camera.target, target);
166    }
167
168    #[test]
169    fn test_camera_set_fovy() {
170        let mut camera = Camera::new(1.0);
171        let new_fov = core::f32::consts::PI / 4.0; // 45 degrees
172        camera.set_fovy(new_fov);
173        assert!((camera.fov - new_fov).abs() < 0.001);
174    }
175
176    #[test]
177    fn test_camera_get_direction() {
178        let mut camera = Camera::new(1.0);
179        camera.set_position(Point3::new(0.0, 0.0, 5.0));
180        camera.set_target(Point3::new(0.0, 0.0, 0.0));
181
182        let direction = camera.get_direction();
183        // Direction should point roughly toward target
184        assert!(direction.magnitude() > 0.0);
185    }
186
187    #[test]
188    fn test_camera_vp_matrix_updates() {
189        let mut camera = Camera::new(1.0);
190        let initial_vp = camera.vp_matrix;
191
192        // Change position should update VP matrix
193        camera.set_position(Point3::new(5.0, 5.0, 5.0));
194        assert_ne!(camera.vp_matrix, initial_vp);
195
196        let after_pos = camera.vp_matrix;
197
198        // Change FOV should update VP matrix
199        camera.set_fovy(core::f32::consts::PI / 4.0);
200        assert_ne!(camera.vp_matrix, after_pos);
201    }
202
203    #[test]
204    fn test_camera_projection_target_center() {
205        let mut camera = Camera::new(1.0); // 1:1 aspect ratio
206        camera.set_position(Point3::new(0.0, 0.0, 5.0));
207        camera.set_target(Point3::new(0.0, 0.0, 0.0));
208
209        // Target point (0, 0, 0) transformed by VP matrix
210        let p_target = nalgebra::Vector4::new(0.0, 0.0, 0.0, 1.0);
211        let clip = camera.vp_matrix * p_target;
212
213        // In homogeneous clip space, x and y must be 0.0 (centered on screen)
214        assert!(clip.x.abs() < 1e-4);
215        assert!(clip.y.abs() < 1e-4);
216        assert!(clip.w > 0.0); // Point is in front of camera
217    }
218
219    #[test]
220    #[allow(non_snake_case)]
221    fn test_camera_view_matrix_orthogonality() {
222        let mut camera = Camera::new(16.0 / 9.0);
223        camera.set_position(Point3::new(3.0, 4.0, 5.0));
224        camera.set_target(Point3::new(0.0, 1.0, 0.0));
225
226        // Extract upper-left 3x3 rotation matrix R from view matrix
227        let R = camera.view_matrix.fixed_view::<3, 3>(0, 0);
228        let I = R * R.transpose();
229
230        // R * R^T must equal Identity for orthogonal rotation matrix
231        let identity = nalgebra::Matrix3::identity();
232        let diff = (I - identity).norm();
233        assert!(
234            diff < 1e-4,
235            "View matrix rotation is not orthogonal: diff = {}",
236            diff
237        );
238    }
239}