Skip to main content

drm_gfx/
camera.rs

1use std::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>) -> &Self {
37        self.position = pos;
38
39        self.update_view();
40        self
41    }
42
43    pub fn set_near_far(&mut self, near: f32, far: f32) -> &Self {
44        self.near = near;
45        self.far = far;
46
47        self.update_projection()
48    }
49
50    pub fn set_fovy(&mut self, fovy: f32) -> &Self {
51        self.fov = fovy;
52
53        self.update_projection()
54    }
55
56    pub fn set_target(&mut self, target: Point3<f32>) -> &Self {
57        self.target = target;
58        self.update_view()
59    }
60
61    pub fn get_direction(&self) -> Vector3<f32> {
62        // Get direction from position to target and normalize it
63        let dir = self.target - self.position;
64        dir.normalize()
65    }
66
67    fn update_view(&mut self) -> &Self {
68        let view = Isometry3::look_at_rh(&self.position, &self.target, &Vector3::y());
69
70        self.view_matrix = view.to_homogeneous();
71        self.vp_matrix = self.projection_matrix * self.view_matrix;
72        self
73    }
74
75    fn update_projection(&mut self) -> &Self {
76        let projection = Perspective3::new(self.aspect_ratio, self.fov, self.near, self.far);
77        self.projection_matrix = projection.to_homogeneous();
78        self.vp_matrix = self.projection_matrix * self.view_matrix;
79        self
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86    use nalgebra::{Point3, Vector3};
87    use std::f32::consts::PI;
88
89    /// Helper function to compare floating point values with a tolerance
90    fn approx_eq(a: f32, b: f32, epsilon: f32) -> bool {
91        (a - b).abs() < epsilon
92    }
93
94    /// Helper function to compare points with a tolerance
95    fn points_approx_eq(a: Point3<f32>, b: Point3<f32>, epsilon: f32) -> bool {
96        approx_eq(a.x, b.x, epsilon) && approx_eq(a.y, b.y, epsilon) && approx_eq(a.z, b.z, epsilon)
97    }
98
99    /// Helper function to compare matrices with a tolerance
100    fn matrices_approx_eq(
101        a: &nalgebra::Matrix4<f32>,
102        b: &nalgebra::Matrix4<f32>,
103        epsilon: f32,
104    ) -> bool {
105        for i in 0..4 {
106            for j in 0..4 {
107                if !approx_eq(a[(i, j)], b[(i, j)], epsilon) {
108                    return false;
109                }
110            }
111        }
112        true
113    }
114
115    #[test]
116    fn test_camera_creation() {
117        let aspect_ratio = 16.0 / 9.0;
118        let camera = Camera::new(aspect_ratio);
119
120        // Check default values
121        assert!(points_approx_eq(
122            camera.position,
123            Point3::new(0.0, 0.0, 0.0),
124            0.001
125        ));
126        assert!(points_approx_eq(
127            camera.target,
128            Point3::new(0.0, 0.0, 0.0),
129            0.001
130        ));
131        assert!(approx_eq(camera.fov, PI / 2.0, 0.001));
132        assert!(approx_eq(camera.near, 0.4, 0.001));
133        assert!(approx_eq(camera.far, 20.0, 0.001));
134        assert!(approx_eq(camera.aspect_ratio, aspect_ratio, 0.001));
135
136        // Identity matrices as default
137        let identity = nalgebra::Matrix4::identity();
138        assert!(matrices_approx_eq(&camera.view_matrix, &identity, 0.001));
139    }
140
141    #[test]
142    fn test_set_position() {
143        let mut camera = Camera::new(1.0);
144        let new_position = Point3::new(1.0, 2.0, 3.0);
145
146        camera.set_position(new_position);
147
148        // Check that position was updated
149        assert!(points_approx_eq(camera.position, new_position, 0.001));
150
151        // View matrix should no longer be identity
152        let identity = nalgebra::Matrix4::identity();
153        assert!(!matrices_approx_eq(&camera.view_matrix, &identity, 0.001));
154
155        // VP matrix should also be updated
156        assert!(!matrices_approx_eq(&camera.vp_matrix, &identity, 0.001));
157    }
158
159    #[test]
160    fn test_set_target() {
161        let mut camera = Camera::new(1.0);
162        camera.set_position(Point3::new(0.0, 0.0, 10.0));
163
164        let new_target = Point3::new(0.0, 0.0, 0.0);
165        camera.set_target(new_target);
166
167        // Check that target was updated
168        assert!(points_approx_eq(camera.target, new_target, 0.001));
169
170        // Direction should be pointing toward -Z (from position at (0,0,10) to target at origin)
171        let direction = camera.get_direction();
172        let expected_direction = Vector3::new(0.0, 0.0, -1.0);
173
174        assert!(approx_eq(direction.x, expected_direction.x, 0.001));
175        assert!(approx_eq(direction.y, expected_direction.y, 0.001));
176        assert!(approx_eq(direction.z, expected_direction.z, 0.001));
177    }
178
179    #[test]
180    fn test_set_near_far() {
181        let mut camera = Camera::new(1.0);
182        let new_near = 1.0;
183        let new_far = 100.0;
184
185        camera.set_near_far(new_near, new_far);
186
187        // Check that near and far were updated
188        assert!(approx_eq(camera.near, new_near, 0.001));
189        assert!(approx_eq(camera.far, new_far, 0.001));
190
191        // Projection matrix should be updated
192        let before_projection = camera.projection_matrix.clone();
193        camera.set_near_far(2.0, 200.0);
194        assert!(!matrices_approx_eq(
195            &camera.projection_matrix,
196            &before_projection,
197            0.001
198        ));
199    }
200
201    #[test]
202    fn test_set_fovy() {
203        let mut camera = Camera::new(1.0);
204        let new_fov = PI / 4.0;
205
206        camera.set_fovy(new_fov);
207
208        // Check that FOV was updated
209        assert!(approx_eq(camera.fov, new_fov, 0.001));
210
211        // Projection matrix should be updated
212        let before_projection = camera.projection_matrix.clone();
213        camera.set_fovy(PI / 3.0);
214        assert!(!matrices_approx_eq(
215            &camera.projection_matrix,
216            &before_projection,
217            0.001
218        ));
219    }
220
221    #[test]
222    fn test_method_chaining() {
223        let mut camera = Camera::new(1.0);
224
225        // Since methods return &self, we need to call them separately
226        camera.set_position(Point3::new(1.0, 2.0, 3.0));
227        camera.set_target(Point3::new(0.0, 0.0, 0.0));
228        camera.set_near_far(0.1, 100.0);
229        camera.set_fovy(PI / 3.0);
230
231        // Verify all settings were applied
232        assert!(points_approx_eq(
233            camera.position,
234            Point3::new(1.0, 2.0, 3.0),
235            0.001
236        ));
237        assert!(points_approx_eq(
238            camera.target,
239            Point3::new(0.0, 0.0, 0.0),
240            0.001
241        ));
242        assert!(approx_eq(camera.near, 0.1, 0.001));
243        assert!(approx_eq(camera.far, 100.0, 0.001));
244        assert!(approx_eq(camera.fov, PI / 3.0, 0.001));
245    }
246
247    #[test]
248    fn test_get_direction() {
249        let mut camera = Camera::new(1.0);
250
251        // Test with camera at origin looking along -Z axis
252        camera.set_position(Point3::new(0.0, 0.0, 0.0));
253        camera.set_target(Point3::new(0.0, 0.0, -1.0));
254
255        let direction = camera.get_direction();
256        assert!(approx_eq(direction.x, 0.0, 0.001));
257        assert!(approx_eq(direction.y, 0.0, 0.001));
258        assert!(approx_eq(direction.z, -1.0, 0.001));
259
260        // Test with camera at (10,0,0) looking at origin
261        camera.set_position(Point3::new(10.0, 0.0, 0.0));
262        camera.set_target(Point3::new(0.0, 0.0, 0.0));
263
264        let direction = camera.get_direction();
265        // Direction should be normalized, pointing from (10,0,0) to (0,0,0)
266        // So it should be approximately (-1, 0, 0)
267        assert!(approx_eq(direction.x, -1.0, 0.001));
268        assert!(approx_eq(direction.y, 0.0, 0.001));
269        assert!(approx_eq(direction.z, 0.0, 0.001));
270    }
271
272    #[test]
273    fn test_look_at_matrix() {
274        let mut camera = Camera::new(1.0);
275
276        // Position camera at origin
277        camera.set_position(Point3::new(0.0, 0.0, 0.0));
278        camera.set_target(Point3::new(0.0, 0.0, -1.0));
279
280        // Create an object position
281        let object_position = Point3::new(0.0, 0.0, -5.0);
282
283        // Transform object position by view matrix
284        let transformed = camera.view_matrix.transform_point(&object_position);
285
286        // Object should be in front of camera on z-axis
287        assert!(approx_eq(transformed.x, 0.0, 0.001));
288        assert!(approx_eq(transformed.y, 0.0, 0.001));
289        assert!(transformed.z < 0.0);
290
291        // Now move camera and check if transformation still works
292        camera.set_position(Point3::new(0.0, 0.0, 5.0));
293        camera.set_target(Point3::new(0.0, 0.0, 0.0));
294
295        // Transform object position by new view matrix
296        let transformed = camera.view_matrix.transform_point(&object_position);
297
298        // Object should still be in front of camera
299        assert!(transformed.z < 0.0);
300    }
301}