Skip to main content

astrelis_render/
camera.rs

1//! Camera system for view-projection matrix management.
2//!
3//! Provides both orthographic and perspective cameras with coordinate conversion utilities.
4//!
5//! # Example
6//!
7//! ```ignore
8//! use astrelis_render::*;
9//! use glam::Vec3;
10//!
11//! // Create an orthographic camera for 2D
12//! let mut camera = Camera::orthographic(800.0, 600.0, 0.1, 100.0);
13//! camera.look_at(Vec3::ZERO, Vec3::new(0.0, 0.0, -1.0), Vec3::Y);
14//!
15//! // Get matrices
16//! let view_projection = camera.view_projection_matrix();
17//!
18//! // Create a perspective camera for 3D
19//! let mut camera = Camera::perspective(60.0, 16.0 / 9.0, 0.1, 100.0);
20//! camera.look_at(
21//!     Vec3::new(0.0, 5.0, 10.0),
22//!     Vec3::ZERO,
23//!     Vec3::Y
24//! );
25//!
26//! // Convert screen to world coordinates
27//! let world_pos = camera.screen_to_world(Vec2::new(400.0, 300.0), Vec2::new(800.0, 600.0));
28//! ```
29
30use glam::{Mat4, Vec2, Vec3, Vec4};
31
32/// Projection mode for a camera.
33#[derive(Debug, Clone, Copy, PartialEq)]
34pub enum ProjectionMode {
35    /// Orthographic projection (typically for 2D).
36    Orthographic {
37        left: f32,
38        right: f32,
39        bottom: f32,
40        top: f32,
41        near: f32,
42        far: f32,
43    },
44    /// Perspective projection (typically for 3D).
45    Perspective {
46        fov_y_radians: f32,
47        aspect_ratio: f32,
48        near: f32,
49        far: f32,
50    },
51}
52
53/// A camera with view and projection matrices.
54pub struct Camera {
55    /// Camera position in world space
56    position: Vec3,
57    /// Target position to look at
58    target: Vec3,
59    /// Up vector (typically Vec3::Y)
60    up: Vec3,
61    /// Projection mode
62    projection: ProjectionMode,
63    /// Cached view matrix
64    view_matrix: Mat4,
65    /// Cached projection matrix
66    projection_matrix: Mat4,
67    /// Cached view-projection matrix
68    view_projection_matrix: Mat4,
69    /// Dirty flag - set to true when position/target/up changes
70    dirty: bool,
71}
72
73impl Camera {
74    /// Create an orthographic camera.
75    ///
76    /// # Arguments
77    ///
78    /// * `width` - Viewport width
79    /// * `height` - Viewport height
80    /// * `near` - Near clip plane
81    /// * `far` - Far clip plane
82    pub fn orthographic(width: f32, height: f32, near: f32, far: f32) -> Self {
83        let half_width = width / 2.0;
84        let half_height = height / 2.0;
85
86        let projection = ProjectionMode::Orthographic {
87            left: -half_width,
88            right: half_width,
89            bottom: -half_height,
90            top: half_height,
91            near,
92            far,
93        };
94
95        let mut camera = Self {
96            position: Vec3::new(0.0, 0.0, 1.0),
97            target: Vec3::ZERO,
98            up: Vec3::Y,
99            projection,
100            view_matrix: Mat4::IDENTITY,
101            projection_matrix: Mat4::IDENTITY,
102            view_projection_matrix: Mat4::IDENTITY,
103            dirty: true,
104        };
105
106        camera.update_matrices();
107        camera
108    }
109
110    /// Create an orthographic camera with custom bounds.
111    pub fn orthographic_custom(
112        left: f32,
113        right: f32,
114        bottom: f32,
115        top: f32,
116        near: f32,
117        far: f32,
118    ) -> Self {
119        let projection = ProjectionMode::Orthographic {
120            left,
121            right,
122            bottom,
123            top,
124            near,
125            far,
126        };
127
128        let mut camera = Self {
129            position: Vec3::new(0.0, 0.0, 1.0),
130            target: Vec3::ZERO,
131            up: Vec3::Y,
132            projection,
133            view_matrix: Mat4::IDENTITY,
134            projection_matrix: Mat4::IDENTITY,
135            view_projection_matrix: Mat4::IDENTITY,
136            dirty: true,
137        };
138
139        camera.update_matrices();
140        camera
141    }
142
143    /// Create a perspective camera.
144    ///
145    /// # Arguments
146    ///
147    /// * `fov_y_degrees` - Vertical field of view in degrees
148    /// * `aspect_ratio` - Aspect ratio (width / height)
149    /// * `near` - Near clip plane
150    /// * `far` - Far clip plane
151    pub fn perspective(fov_y_degrees: f32, aspect_ratio: f32, near: f32, far: f32) -> Self {
152        let projection = ProjectionMode::Perspective {
153            fov_y_radians: fov_y_degrees.to_radians(),
154            aspect_ratio,
155            near,
156            far,
157        };
158
159        let mut camera = Self {
160            position: Vec3::new(0.0, 5.0, 10.0),
161            target: Vec3::ZERO,
162            up: Vec3::Y,
163            projection,
164            view_matrix: Mat4::IDENTITY,
165            projection_matrix: Mat4::IDENTITY,
166            view_projection_matrix: Mat4::IDENTITY,
167            dirty: true,
168        };
169
170        camera.update_matrices();
171        camera
172    }
173
174    /// Set the camera to look at a target from a position.
175    pub fn look_at(&mut self, eye: Vec3, target: Vec3, up: Vec3) {
176        self.position = eye;
177        self.target = target;
178        self.up = up;
179        self.dirty = true;
180    }
181
182    /// Set the camera position.
183    pub fn set_position(&mut self, position: Vec3) {
184        self.position = position;
185        self.dirty = true;
186    }
187
188    /// Get the camera position.
189    pub fn position(&self) -> Vec3 {
190        self.position
191    }
192
193    /// Set the camera target.
194    pub fn set_target(&mut self, target: Vec3) {
195        self.target = target;
196        self.dirty = true;
197    }
198
199    /// Get the camera target.
200    pub fn target(&self) -> Vec3 {
201        self.target
202    }
203
204    /// Set the up vector.
205    pub fn set_up(&mut self, up: Vec3) {
206        self.up = up;
207        self.dirty = true;
208    }
209
210    /// Get the up vector.
211    pub fn up(&self) -> Vec3 {
212        self.up
213    }
214
215    /// Get the forward direction (normalized vector from position to target).
216    pub fn forward(&self) -> Vec3 {
217        (self.target - self.position).normalize()
218    }
219
220    /// Get the right direction.
221    pub fn right(&self) -> Vec3 {
222        self.forward().cross(self.up).normalize()
223    }
224
225    /// Update the projection mode.
226    pub fn set_projection(&mut self, projection: ProjectionMode) {
227        self.projection = projection;
228        self.dirty = true;
229    }
230
231    /// Get the projection mode.
232    pub fn projection(&self) -> ProjectionMode {
233        self.projection
234    }
235
236    /// Update the aspect ratio (only affects perspective cameras).
237    pub fn set_aspect_ratio(&mut self, aspect_ratio: f32) {
238        if let ProjectionMode::Perspective {
239            fov_y_radians,
240            near,
241            far,
242            ..
243        } = self.projection
244        {
245            self.projection = ProjectionMode::Perspective {
246                fov_y_radians,
247                aspect_ratio,
248                near,
249                far,
250            };
251            self.dirty = true;
252        }
253    }
254
255    /// Get the view matrix.
256    pub fn view_matrix(&mut self) -> Mat4 {
257        if self.dirty {
258            self.update_matrices();
259        }
260        self.view_matrix
261    }
262
263    /// Get the projection matrix.
264    pub fn projection_matrix(&mut self) -> Mat4 {
265        if self.dirty {
266            self.update_matrices();
267        }
268        self.projection_matrix
269    }
270
271    /// Get the combined view-projection matrix.
272    pub fn view_projection_matrix(&mut self) -> Mat4 {
273        if self.dirty {
274            self.update_matrices();
275        }
276        self.view_projection_matrix
277    }
278
279    /// Convert screen coordinates to world coordinates.
280    ///
281    /// # Arguments
282    ///
283    /// * `screen_pos` - Screen position (pixels)
284    /// * `viewport_size` - Viewport size (pixels)
285    /// * `depth` - Depth in NDC space (-1.0 to 1.0, where -1.0 is near plane)
286    ///
287    /// # Returns
288    ///
289    /// World position as Vec3
290    pub fn screen_to_world(&mut self, screen_pos: Vec2, viewport_size: Vec2, depth: f32) -> Vec3 {
291        // Convert screen to NDC
292        let ndc_x = (screen_pos.x / viewport_size.x) * 2.0 - 1.0;
293        let ndc_y = 1.0 - (screen_pos.y / viewport_size.y) * 2.0; // Flip Y
294        let ndc = Vec4::new(ndc_x, ndc_y, depth, 1.0);
295
296        // Transform to world space
297        let view_proj = self.view_projection_matrix();
298        let inv_view_proj = view_proj.inverse();
299        let world = inv_view_proj * ndc;
300
301        // Perspective divide
302        Vec3::new(world.x / world.w, world.y / world.w, world.z / world.w)
303    }
304
305    /// Convert world coordinates to screen coordinates.
306    ///
307    /// # Arguments
308    ///
309    /// * `world_pos` - World position
310    /// * `viewport_size` - Viewport size (pixels)
311    ///
312    /// # Returns
313    ///
314    /// Screen position as Vec2 (pixels) and depth
315    pub fn world_to_screen(&mut self, world_pos: Vec3, viewport_size: Vec2) -> (Vec2, f32) {
316        let view_proj = self.view_projection_matrix();
317        let clip = view_proj * Vec4::new(world_pos.x, world_pos.y, world_pos.z, 1.0);
318
319        // Perspective divide
320        let ndc = Vec3::new(clip.x / clip.w, clip.y / clip.w, clip.z / clip.w);
321
322        // Convert NDC to screen
323        let screen_x = (ndc.x + 1.0) * 0.5 * viewport_size.x;
324        let screen_y = (1.0 - ndc.y) * 0.5 * viewport_size.y; // Flip Y
325
326        (Vec2::new(screen_x, screen_y), ndc.z)
327    }
328
329    /// Update all matrices.
330    fn update_matrices(&mut self) {
331        // Update view matrix
332        self.view_matrix = Mat4::look_at_rh(self.position, self.target, self.up);
333
334        // Update projection matrix
335        self.projection_matrix = match self.projection {
336            ProjectionMode::Orthographic {
337                left,
338                right,
339                bottom,
340                top,
341                near,
342                far,
343            } => Mat4::orthographic_rh(left, right, bottom, top, near, far),
344            ProjectionMode::Perspective {
345                fov_y_radians,
346                aspect_ratio,
347                near,
348                far,
349            } => Mat4::perspective_rh(fov_y_radians, aspect_ratio, near, far),
350        };
351
352        // Update combined matrix
353        self.view_projection_matrix = self.projection_matrix * self.view_matrix;
354
355        self.dirty = false;
356    }
357}
358
359/// Camera uniform buffer data for shaders.
360///
361/// This is a standard layout that can be used in shaders:
362///
363/// ```wgsl
364/// struct CameraUniform {
365///     view_proj: mat4x4<f32>,
366///     view: mat4x4<f32>,
367///     projection: mat4x4<f32>,
368///     position: vec3<f32>,
369/// }
370/// ```
371#[repr(C)]
372#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
373pub struct CameraUniform {
374    /// View-projection matrix
375    pub view_proj: [[f32; 4]; 4],
376    /// View matrix
377    pub view: [[f32; 4]; 4],
378    /// Projection matrix
379    pub projection: [[f32; 4]; 4],
380    /// Camera position in world space
381    pub position: [f32; 3],
382    /// Padding for alignment
383    pub _padding: f32,
384}
385
386impl CameraUniform {
387    /// Create camera uniform data from a camera.
388    pub fn from_camera(camera: &mut Camera) -> Self {
389        Self {
390            view_proj: camera.view_projection_matrix().to_cols_array_2d(),
391            view: camera.view_matrix().to_cols_array_2d(),
392            projection: camera.projection_matrix().to_cols_array_2d(),
393            position: camera.position().to_array(),
394            _padding: 0.0,
395        }
396    }
397}
398
399#[cfg(test)]
400mod tests {
401    use super::*;
402
403    #[test]
404    fn test_orthographic_camera() {
405        let mut camera = Camera::orthographic(800.0, 600.0, 0.1, 100.0);
406        camera.look_at(Vec3::ZERO, Vec3::new(0.0, 0.0, -1.0), Vec3::Y);
407
408        let view_proj = camera.view_projection_matrix();
409        assert!(!view_proj.is_nan());
410    }
411
412    #[test]
413    fn test_perspective_camera() {
414        let mut camera = Camera::perspective(60.0, 16.0 / 9.0, 0.1, 100.0);
415        camera.look_at(Vec3::new(0.0, 5.0, 10.0), Vec3::ZERO, Vec3::Y);
416
417        let view_proj = camera.view_projection_matrix();
418        assert!(!view_proj.is_nan());
419    }
420
421    #[test]
422    fn test_screen_to_world() {
423        let mut camera = Camera::orthographic(800.0, 600.0, 0.1, 100.0);
424        camera.look_at(Vec3::new(0.0, 0.0, 1.0), Vec3::ZERO, Vec3::Y);
425
426        let world_pos =
427            camera.screen_to_world(Vec2::new(400.0, 300.0), Vec2::new(800.0, 600.0), 0.0);
428
429        // Center of screen should map to roughly (0, 0) in world space
430        assert!((world_pos.x.abs()) < 0.1);
431        assert!((world_pos.y.abs()) < 0.1);
432    }
433
434    #[test]
435    fn test_world_to_screen() {
436        let mut camera = Camera::orthographic(800.0, 600.0, 0.1, 100.0);
437        camera.look_at(Vec3::new(0.0, 0.0, 1.0), Vec3::ZERO, Vec3::Y);
438
439        let (screen_pos, _depth) = camera.world_to_screen(Vec3::ZERO, Vec2::new(800.0, 600.0));
440
441        // World origin should map to center of screen
442        assert!((screen_pos.x - 400.0).abs() < 1.0);
443        assert!((screen_pos.y - 300.0).abs() < 1.0);
444    }
445}