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}