Skip to main content

optic_render/camera/
camera.rs

1use optic_core::{CamProj, ClipDist, Size2D};
2use cgmath::*;
3
4use crate::util::transform::CamTransform;
5
6/// A 3D camera with perspective or orthographic projection.
7///
8/// Wraps a [`CamTransform`] that holds position, rotation, FOV, clip distances,
9/// and pre-computed view/projection matrices. After mutating any transform
10/// property, call [`pre_update`](Camera::pre_update) to recalculate.
11///
12/// # Conventions
13///
14/// - **Y-up** world coordinate system.
15/// - **-Z forward** — the camera looks down its local -Z axis by default.
16/// - **Euler angles** in degrees, applied in XYZ order.
17///
18/// # Movement methods
19///
20/// | Direction | Method | Axis |
21/// |---|---|---|
22/// | Forward (in look direction) | [`fly_forw`](Camera::fly_forw) | Camera-local -Z |
23/// | Backward | [`fly_back`](Camera::fly_back) | Camera-local +Z |
24/// | Left (strafe) | [`fly_left`](Camera::fly_left) | Camera-local -X |
25/// | Right (strafe) | [`fly_right`](Camera::fly_right) | Camera-local +X |
26/// | Up | [`fly_up`](Camera::fly_up) | World +Y |
27/// | Down | [`fly_down`](Camera::fly_down) | World -Y |
28///
29/// # Rotation methods
30///
31/// | Method | Effect | Common name |
32/// |---|---|---|
33/// | [`spin_x`](Camera::spin_x) | Pitch (look up/down) | Tilt forward/backward |
34/// | [`spin_y`](Camera::spin_y) | Yaw (look left/right) | Turn head side-to-side |
35/// | [`spin_z`](Camera::spin_z) | Roll (rotate view) | Tilt horizon |
36///
37/// # Getters and setters
38///
39/// | Property | Getter | Setter |
40/// |---|---|---|
41/// | Field of view (degrees) | [`fov`](Camera::fov) | [`set_fov`](Camera::set_fov), [`add_fov`](Camera::add_fov) |
42/// | Orthographic scale | [`ortho_scale`](Camera::ortho_scale) | [`set_ortho_scale`](Camera::set_ortho_scale), [`add_ortho_scale`](Camera::add_ortho_scale) |
43/// | Projection mode | [`proj`](Camera::proj) | [`set_proj`](Camera::set_proj) |
44/// | Clip distances | [`clip`](Camera::clip) | [`set_clip`](Camera::set_clip), [`set_clip_near`](Camera::set_clip_near), [`set_clip_far`](Camera::set_clip_far) |
45/// | Viewport size | — | [`set_size`](Camera::set_size) |
46/// | View/proj matrices | — | [`pre_update`](Camera::pre_update) |
47///
48/// # Example
49///
50/// ```ignore
51/// use optic_core::{CamProj, Size2D};
52/// use optic_render::Camera;
53///
54/// let mut cam = Camera::new(Size2D::from(1920, 1080), CamProj::Persp);
55/// cam.fly_forw(10.0);       // move in look direction
56/// cam.spin_y(-90.0);        // yaw left 90°
57/// cam.pre_update();          // recalculate matrices
58/// ```
59pub struct Camera {
60    pub transform: CamTransform,
61}
62
63impl Camera {
64    /// Creates a camera sized to match the given canvas dimensions.
65    pub fn match_canvas_size(canvas: &crate::handles::Canvas, proj: CamProj) -> Self {
66        Self::new(canvas.size(), proj)
67    }
68
69    /// Creates a camera at `(0, 0, 5)` with 75° FOV, default clip distances,
70    /// and the given projection type.
71    pub fn new(size: Size2D, proj: CamProj) -> Self {
72        let fov = 75.0;
73        let clip = ClipDist::default();
74        let pos = vec3(0.0, 0.0, 5.0);
75        let rot = vec3(0.0, -90.0, 0.0);
76
77        let pos_inverse = Matrix4::from_translation(vec3(-pos.x, -pos.y, -pos.z));
78        let rot_inverse = Matrix4::<f32>::from_angle_x(Rad::from(Deg(-rot.x)))
79            * Matrix4::<f32>::from_angle_y(Rad::from(Deg(-rot.y)))
80            * Matrix4::<f32>::from_angle_z(Rad::from(Deg(-rot.z)));
81
82        let view_matrix = pos_inverse * rot_inverse;
83
84        let mut transform = CamTransform {
85            pos,
86            rot,
87            fov,
88            clip,
89            size,
90            proj,
91            view_matrix,
92            ortho_scale: 2.0,
93            front: vec3(0.0, 0.0, -1.0),
94            persp_matrix: Matrix4::identity(),
95            ortho_matrix: Matrix4::identity(),
96        };
97        transform.calc_matrices();
98
99        Camera { transform }
100    }
101
102    /// Recalculates the view and projection matrices from the current transform state.
103    ///
104    /// Call this once per frame **after** all movement ([`fly_forw`](Camera::fly_forw), etc.)
105    /// and rotation ([`spin_y`](Camera::spin_y), etc.) have been applied.
106    /// The matrices are consumed by the rendering pipeline — without this call
107    /// the camera will continue using stale matrices from the previous frame.
108    pub fn pre_update(&mut self) {
109        self.transform.calc_matrices();
110    }
111
112    /// Returns the vertical field of view in degrees.
113    pub fn fov(&self) -> f32 { self.transform.fov }
114    /// Returns the orthographic scale factor.
115    pub fn ortho_scale(&self) -> f32 { self.transform.ortho_scale }
116    /// Returns the current projection type.
117    pub fn proj(&self) -> CamProj { self.transform.proj }
118    /// Returns the near/far clip distances.
119    pub fn clip(&self) -> ClipDist { self.transform.clip }
120
121    /// Sets both near and far clip distances at once.
122    pub fn set_clip(&mut self, clip: ClipDist) { self.transform.clip = clip; }
123    /// Sets the near clip plane distance.
124    pub fn set_clip_near(&mut self, near: f32) { self.transform.clip.near = near; }
125    /// Sets the far clip plane distance.
126    pub fn set_clip_far(&mut self, far: f32) { self.transform.clip.far = far; }
127    /// Sets the canvas/viewport size (used for aspect ratio calculation).
128    pub fn set_size(&mut self, size: Size2D) { self.transform.size = size; }
129    /// Switches between perspective and orthographic projection.
130    pub fn set_proj(&mut self, proj: CamProj) { self.transform.proj = proj; }
131
132    /// Sets the vertical field of view in degrees (clamped to ≥ 0.01).
133    pub fn set_fov(&mut self, fov: f32) {
134        self.transform.fov = fov.max(0.01);
135    }
136    /// Adds `value` to the FOV (clamped to ≥ 0.01).
137    pub fn add_fov(&mut self, value: f32) {
138        self.transform.fov = (self.transform.fov + value).max(0.01);
139    }
140
141    /// Sets the orthographic scale factor.
142    pub fn set_ortho_scale(&mut self, value: f32) { self.transform.ortho_scale = value; }
143    /// Adds `value` to the orthographic scale factor.
144    pub fn add_ortho_scale(&mut self, value: f32) { self.transform.ortho_scale += value; }
145
146    /// Moves the camera forward (in the direction it faces).
147    ///
148    /// The forward direction is derived from the camera's current rotation
149    /// (stored as `front` in [`CamTransform`]). For the default orientation this
150    /// moves along world -Z.
151    ///
152    /// # When to use
153    ///
154    /// Call this each frame with `speed * delta_time` for smooth first-person
155    /// movement. Call [`pre_update`](Camera::pre_update) after all movement and
156    /// rotation for the frame.
157    pub fn fly_forw(&mut self, speed: f32) {
158        self.transform.pos += speed * self.transform.front;
159    }
160    /// Moves the camera backward (opposite the direction it faces).
161    ///
162    /// The inverse of [`fly_forw`](Camera::fly_forw). Equivalent to
163    /// `fly_forw(-speed)`.
164    pub fn fly_back(&mut self, speed: f32) {
165        self.transform.pos -= speed * self.transform.front;
166    }
167    /// Moves the camera left (strafe), perpendicular to the forward direction.
168    ///
169    /// The strafe direction is computed as `front × world_up`, producing a
170    /// vector orthogonal to both the look direction and the world Y axis. This
171    /// keeps the horizon level even when pitching up or down.
172    pub fn fly_left(&mut self, speed: f32) {
173        self.transform.pos -= speed * self.transform.front.cross(Vector3::unit_y()).normalize();
174    }
175    /// Moves the camera right (strafe), perpendicular to the forward direction.
176    ///
177    /// The inverse of [`fly_left`](Camera::fly_left). Equivalent to
178    /// `fly_left(-speed)`.
179    pub fn fly_right(&mut self, speed: f32) {
180        self.transform.pos += speed * self.transform.front.cross(Vector3::unit_y()).normalize();
181    }
182    /// Moves the camera straight up (world Y axis).
183    ///
184    /// Unlike [`fly_forw`](Camera::fly_forw) / [`fly_left`](Camera::fly_left),
185    /// this always moves along the **world** Y axis regardless of the camera's
186    /// current pitch or roll.
187    pub fn fly_up(&mut self, speed: f32) { self.transform.pos.y += speed; }
188    /// Moves the camera straight down (world Y axis).
189    ///
190    /// The inverse of [`fly_up`](Camera::fly_up). Equivalent to
191    /// `fly_up(-speed)`.
192    pub fn fly_down(&mut self, speed: f32) { self.transform.pos.y -= speed; }
193
194    /// Pitches the camera up or down (rotation around the local X axis).
195    ///
196    /// Positive values tilt the view downward (looking toward the ground);
197    /// negative values tilt upward (looking toward the sky).
198    ///
199    /// # When to use
200    ///
201    /// Combine with [`spin_y`](Camera::spin_y) for full free-look control
202    /// (e.g. mouse-look in a first-person game).
203    pub fn spin_x(&mut self, speed: f32) { self.transform.rot.x += speed; }
204    /// Yaws the camera left or right (rotation around the local Y axis).
205    ///
206    /// Positive values turn right; negative values turn left. This is the
207    /// primary rotation for first-person horizontal look-around.
208    pub fn spin_y(&mut self, speed: f32) { self.transform.rot.y += speed; }
209    /// Rolls the camera (rotation around the local Z axis).
210    ///
211    /// Tilts the horizon. Rarely used in first-person games; useful for
212    /// cinematic cameras or flight simulators.
213    pub fn spin_z(&mut self, speed: f32) { self.transform.rot.z += speed; }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    fn approx_eq(a: f32, b: f32) -> bool {
221        (a - b).abs() < 1e-4
222    }
223
224    #[test]
225    fn camera_new_persp() {
226        let cam = Camera::new(Size2D::from(1920, 1080), CamProj::Persp);
227        assert!((cam.fov() - 75.0).abs() < f32::EPSILON);
228        assert!((cam.transform.pos.y - 0.0).abs() < f32::EPSILON);
229        assert!((cam.transform.pos.z - 5.0).abs() < f32::EPSILON);
230    }
231
232    #[test]
233    fn camera_new_ortho() {
234        let cam = Camera::new(Size2D::from(800, 600), CamProj::Ortho);
235        assert_eq!(cam.proj(), CamProj::Ortho);
236    }
237
238    #[test]
239    fn camera_set_clip() {
240        let mut cam = Camera::new(Size2D::from(1920, 1080), CamProj::Persp);
241        cam.set_clip(ClipDist::from(0.1, 500.0));
242        assert!(approx_eq(cam.clip().near, 0.1));
243        assert!(approx_eq(cam.clip().far, 500.0));
244    }
245
246    #[test]
247    fn camera_set_clip_near_far() {
248        let mut cam = Camera::new(Size2D::from(1920, 1080), CamProj::Persp);
249        cam.set_clip_near(0.5);
250        cam.set_clip_far(2000.0);
251        assert!(approx_eq(cam.clip().near, 0.5));
252        assert!(approx_eq(cam.clip().far, 2000.0));
253    }
254
255    #[test]
256    fn camera_set_fov() {
257        let mut cam = Camera::new(Size2D::from(1920, 1080), CamProj::Persp);
258        cam.set_fov(90.0);
259        assert!(approx_eq(cam.fov(), 90.0));
260    }
261
262    #[test]
263    fn camera_set_fov_clamped() {
264        let mut cam = Camera::new(Size2D::from(1920, 1080), CamProj::Persp);
265        cam.set_fov(-1.0); // clamped to 0.01
266        assert!(approx_eq(cam.fov(), 0.01));
267    }
268
269    #[test]
270    fn camera_add_fov() {
271        let mut cam = Camera::new(Size2D::from(1920, 1080), CamProj::Persp);
272        cam.add_fov(10.0);
273        assert!(approx_eq(cam.fov(), 85.0));
274    }
275
276    #[test]
277    fn camera_ortho_scale() {
278        let mut cam = Camera::new(Size2D::from(1920, 1080), CamProj::Ortho);
279        assert!((cam.ortho_scale() - 2.0).abs() < f32::EPSILON);
280        cam.set_ortho_scale(5.0);
281        assert!((cam.ortho_scale() - 5.0).abs() < f32::EPSILON);
282        cam.add_ortho_scale(3.0);
283        assert!((cam.ortho_scale() - 8.0).abs() < f32::EPSILON);
284    }
285
286    #[test]
287    fn camera_set_proj() {
288        let mut cam = Camera::new(Size2D::from(1920, 1080), CamProj::Persp);
289        assert_eq!(cam.proj(), CamProj::Persp);
290        cam.set_proj(CamProj::Ortho);
291        assert_eq!(cam.proj(), CamProj::Ortho);
292    }
293
294    #[test]
295    fn camera_set_size() {
296        let mut cam = Camera::new(Size2D::from(1920, 1080), CamProj::Persp);
297        cam.set_size(Size2D::from(800, 600));
298        assert_eq!(cam.transform.size.w, 800);
299        assert_eq!(cam.transform.size.h, 600);
300    }
301
302    #[test]
303    fn camera_fly_movements() {
304        let mut cam = Camera::new(Size2D::from(1920, 1080), CamProj::Persp);
305        let start = cam.transform.pos;
306
307        cam.fly_forw(1.0);
308        // front should be (0, 0, -1) more or less
309        let diff = cam.transform.pos - start;
310        assert!(approx_eq(diff.z, -1.0));
311        assert!(approx_eq(diff.x, 0.0));
312
313        cam.fly_up(1.0);
314        assert!(approx_eq(cam.transform.pos.y, 1.0));
315
316        cam.fly_down(0.5);
317        assert!(approx_eq(cam.transform.pos.y, 0.5));
318    }
319
320    #[test]
321    fn camera_spin() {
322        let mut cam = Camera::new(Size2D::from(1920, 1080), CamProj::Persp);
323        cam.spin_x(45.0);
324        cam.spin_y(90.0);
325        cam.spin_z(30.0);
326        assert!(approx_eq(cam.transform.rot.x, 45.0));
327        assert!(approx_eq(cam.transform.rot.y, 0.0));  // initial -90 + 90
328        assert!(approx_eq(cam.transform.rot.z, 30.0));
329    }
330
331    #[test]
332    fn camera_pre_update() {
333        let mut cam = Camera::new(Size2D::from(1920, 1080), CamProj::Persp);
334        let view_before = cam.transform.view_matrix;
335        cam.transform.rot.x = 30.0;
336        cam.pre_update();
337        assert!(view_before != cam.transform.view_matrix);
338    }
339}