Skip to main content

mirage_engine/
camera.rs

1use core::ops::Range;
2
3use crate::Ray;
4use crate::math::camera::rh::proj::directx::{orthographic, perspective};
5use crate::math::camera::rh::view::look_at_mat4;
6use crate::math::{Mat3, Mat4, Quat, UVec2, Vec2, Vec3, Vec4};
7use crate::mesh::BoundingSphere;
8
9/// A [`View`] and a [`Projection`]: where a frame is viewed from, and how
10/// that view is projected.
11///
12/// A frame is drawn from the last call to
13/// [`FrameContext::set_camera`](crate::FrameContext::set_camera); each
14/// frame needs its own.
15#[derive(Clone, Copy, Debug, PartialEq)]
16pub struct Camera {
17    view: View,
18    projection: Projection,
19}
20
21impl Camera {
22    /// A camera from `view` and `projection`.
23    pub const fn new(view: View, projection: Projection) -> Self {
24        Self { view, projection }
25    }
26
27    /// The world's viewpoint.
28    pub const fn view(&self) -> View {
29        self.view
30    }
31
32    /// The view's projection onto the screen.
33    pub const fn projection(&self) -> Projection {
34        self.projection
35    }
36
37    /// The ray through `pixel` on a surface `size` across, in world space.
38    ///
39    /// Both are physical pixels, as
40    /// [`FrameContext::window_size`](crate::FrameContext::window_size)
41    /// reports them. The ray starts at the [`View::eye`] where the lens
42    /// foreshortens, and in the view plane where it does not.
43    pub fn ray_through(&self, pixel: Vec2, size: UVec2) -> Ray {
44        let size = size.as_vec2();
45        let clip = clip_space(pixel, size);
46        let world = self.view_projection(size.x / size.y).inverse();
47        let near = world.project_point3(clip.extend(0.0));
48        let far = world.project_point3(clip.extend(1.0));
49
50        match self.projection.lens {
51            Lens::Perspective { .. } => Ray::new(self.view.eye, far - near),
52            Lens::Orthographic { .. } => Ray::new(near, far - near),
53        }
54    }
55
56    /// The pixel `point` draws at, on a surface `size` across, in physical
57    /// pixels.
58    ///
59    /// The inverse of [`Camera::ray_through`] at the same `size`, counted
60    /// from the drawing area's top left. A point off screen returns a pixel
61    /// outside the surface, for the caller to clamp. `None` where `point`
62    /// lies at or behind the [`View::eye`], where either side of `size` is
63    /// zero, or where the view or the lens has no shape to draw through.
64    pub fn pixel_of(&self, point: Vec3, size: UVec2) -> Option<Vec2> {
65        self.depth_in_front(point)?;
66        let size = surface(size)?;
67        let clip = self.view_projection(size.x / size.y).project_point3(point);
68        let pixel = Vec2::new(clip.x + 1.0, 1.0 - clip.y) / 2.0 * size;
69
70        pixel.is_finite().then_some(pixel)
71    }
72
73    /// The pixels a meter across the view covers at the depth of `point` in
74    /// front of the [`View::eye`], on a surface `size` across: the scale.
75    ///
76    /// The scale falls with depth under [`Lens::Perspective`] and stays the
77    /// same at every depth under [`Lens::Orthographic`]. `None` where
78    /// `point` lies at or behind the [`View::eye`], where either side of
79    /// `size` is zero, or where the view or the lens has no shape to draw
80    /// through.
81    pub fn pixels_per_meter(&self, point: Vec3, size: UVec2) -> Option<f32> {
82        let depth = self.depth_in_front(point)?;
83        let size = surface(size)?;
84        let visible_height = match self.projection.lens {
85            Lens::Perspective { fov_degrees } => {
86                2.0 * depth * (fov_degrees.to_radians() / 2.0).tan()
87            }
88            Lens::Orthographic { world_height } => world_height,
89        };
90        let scale = size.y / visible_height;
91
92        scale.is_normal().then_some(scale)
93    }
94
95    /// The same camera moved so that `point` draws at `lands_at`, on a
96    /// surface `size` across.
97    ///
98    /// Required if you want a drag to pan the view. `lands_at` is a pixel
99    /// as [`pixel_of`](Camera::pixel_of) returns one: physical pixels from
100    /// the drawing area's top left. The camera is moved and never turned,
101    /// and it moves in the view plane, so `point` keeps its depth and the
102    /// [`View::eye`] and [`View::target`] move by the same vector. `None`
103    /// where `point` lies at or behind the [`View::eye`], where either side
104    /// of `size` is zero, where the view or the lens has no shape to draw
105    /// through, where `lands_at` is not finite, or where the camera it
106    /// would return does not draw `point`: the [`View::eye`] and
107    /// [`View::target`] landed together.
108    pub fn shifted_so(&self, point: Vec3, lands_at: Vec2, size: UVec2) -> Option<Camera> {
109        let drawn = self.pixel_of(point, size)?;
110        let scale = self.pixels_per_meter(point, size)?;
111        let axes = self.view.basis()?;
112        if !lands_at.is_finite() {
113            return None;
114        }
115        let moved = lands_at - drawn;
116        let shift = (axes.upward * moved.y - axes.across * moved.x) / scale;
117        let shifted = Self::new(self.view.moved(shift), self.projection);
118
119        shifted.pixel_of(point, size).is_some().then_some(shifted)
120    }
121
122    /// The same camera zoomed about `point`, on a surface `size` across, so
123    /// that `point` keeps the pixel it draws at.
124    ///
125    /// Required if you want a wheel to zoom towards what the pointer is
126    /// over. The distance from the [`View::eye`] to `point` becomes the
127    /// fraction `1.0 / factor` of what it was, so `2.0` halves it, and the
128    /// direction the view looks and its field of view are left alone. Under
129    /// [`Lens::Orthographic`] the [`View::eye`] keeps its own depth: the
130    /// lens takes that same fraction of its world height and the
131    /// [`View::eye`] moves in the view plane instead. `None` where `point`
132    /// lies at or behind the [`View::eye`], where either side of `size` is
133    /// zero, where the view or the lens has no shape to draw through, where
134    /// `factor` is not finite or not above zero, or where the camera it
135    /// would return does not draw `point`: the [`View::eye`] and
136    /// [`View::target`] landed together, or the lens was left with no
137    /// finite height.
138    pub fn zoomed_about(&self, point: Vec3, factor: f32, size: UVec2) -> Option<Camera> {
139        self.pixel_of(point, size)?;
140        let closer = (factor.is_finite() && factor > 0.0).then(|| 1.0 - 1.0 / factor)?;
141        let to_point = point - self.view.eye;
142
143        let line = match self.projection.lens {
144            Lens::Orthographic { .. } => {
145                let axes = self.view.basis()?;
146                to_point - axes.forward * to_point.dot(axes.forward)
147            }
148            Lens::Perspective { .. } => to_point,
149        };
150        let zoomed = Self::new(
151            self.view.moved(line * closer),
152            self.projection.zoomed(factor)?,
153        );
154
155        zoomed.pixel_of(point, size).is_some().then_some(zoomed)
156    }
157
158    /// The same camera turned about `point`, so that `point` keeps the pixel
159    /// it draws at.
160    ///
161    /// Required if you want a drag to orbit the view about what the pointer
162    /// is over. The [`View::eye`] turns about `point` by `pitch` radians
163    /// about the direction across the view to the right, then by `yaw`
164    /// radians about the [`View::up`] direction, and the direction the view
165    /// looks turns with it, so the distance from the [`View::eye`] to
166    /// `point` and the [`Projection`] are left alone. `None` where `point`
167    /// lies at or behind the [`View::eye`], where the view has no shape to
168    /// draw through, where `yaw` or `pitch` is not finite, or where the turn
169    /// takes the direction the view looks past the [`View::up`] direction —
170    /// the pole.
171    pub fn turned_about(&self, point: Vec3, yaw: f32, pitch: f32) -> Option<Camera> {
172        self.depth_in_front(point)?;
173        let axes = self.view.basis()?;
174        let turn = Quat::from_axis_angle(axes.up, yaw) * Quat::from_axis_angle(axes.across, pitch);
175        let turned = Self::new(self.view.turned(point, turn), self.projection);
176
177        // `point` keeps its pixel where the view's own axes turned with the
178        // viewpoint; past the pole the right axis points the other way. No
179        // type states which side of the pole a turn left the view on.
180        (turned.view.basis()?.across.dot(turn * axes.across) > 0.0).then_some(turned)
181    }
182
183    /// The matrix that takes world space to clip space, with `aspect`.
184    pub(crate) fn view_projection(&self, aspect: f32) -> Mat4 {
185        self.projection.matrix(aspect) * self.view.matrix()
186    }
187
188    /// The inverse of this camera with its [`View::eye`] left at the
189    /// origin, with `aspect`: it takes a clip position to a point on the ray
190    /// the camera reads that position along.
191    ///
192    /// Under [`Lens::Orthographic`] every such ray is the direction the view
193    /// looks, which this matrix does not hold; see
194    /// [`Camera::foreshortened`].
195    pub(crate) fn rays_from_clip(&self, aspect: f32) -> Mat4 {
196        let turned = Mat4::from_mat3(Mat3::from_mat4(self.view.matrix()));
197
198        (self.projection.matrix(aspect) * turned).inverse()
199    }
200
201    /// Whether rays leave the [`View::eye`], rather than running level
202    /// through it.
203    pub(crate) fn foreshortened(&self) -> bool {
204        matches!(self.projection.lens(), Lens::Perspective { .. })
205    }
206
207    /// How far `point` lies in front of the [`View::eye`], in meters along
208    /// the view direction; `None` where it lies at or behind the
209    /// [`View::eye`], or the view has no direction.
210    fn depth_in_front(&self, point: Vec3) -> Option<f32> {
211        let depth = -self.view.matrix().transform_point3(point).z;
212        (depth > 0.0 && depth.is_finite()).then_some(depth)
213    }
214}
215
216impl Default for Camera {
217    /// Looks at the origin from 5 meters back and 2 meters up, at 60°.
218    fn default() -> Self {
219        Self::new(
220            View::look_at(Vec3::new(0.0, 2.0, 5.0), Vec3::ZERO),
221            Projection::perspective(60.0),
222        )
223    }
224}
225
226/// The world's viewpoint.
227#[derive(Clone, Copy, Debug, PartialEq)]
228pub struct View {
229    eye: Vec3,
230    target: Vec3,
231    up: Vec3,
232}
233
234impl View {
235    /// Looks from `eye` at `target`, `+Y` up.
236    pub const fn look_at(eye: Vec3, target: Vec3) -> Self {
237        Self {
238            eye,
239            target,
240            up: Vec3::Y,
241        }
242    }
243
244    /// Sets the view's up direction; `+Y` by default.
245    #[must_use]
246    pub const fn with_up(mut self, up: Vec3) -> Self {
247        self.up = up;
248        self
249    }
250
251    /// Camera position.
252    pub const fn eye(&self) -> Vec3 {
253        self.eye
254    }
255
256    /// The camera's target.
257    pub const fn target(&self) -> Vec3 {
258        self.target
259    }
260
261    /// Up direction on the screen.
262    pub const fn up(&self) -> Vec3 {
263        self.up
264    }
265
266    /// The direction from the [`View::eye`] to the [`View::target`], one
267    /// meter long; zero where the two are one point.
268    pub fn direction(&self) -> Vec3 {
269        (self.target - self.eye).normalize_or_zero()
270    }
271
272    fn matrix(&self) -> Mat4 {
273        look_at_mat4(self.eye, self.target, self.up)
274    }
275
276    /// Where this view looks, the directions across and up the screen, and
277    /// the up direction it holds, each one meter long; `None` where the view
278    /// has no shape.
279    fn basis(&self) -> Option<Basis> {
280        let forward = self.direction();
281        if forward == Vec3::ZERO {
282            return None;
283        }
284        let across = forward.cross(self.up).try_normalize()?;
285        let up = self.up.try_normalize()?;
286
287        Some(Basis {
288            forward,
289            across,
290            upward: across.cross(forward),
291            up,
292        })
293    }
294
295    /// The same view moved by `offset`, looking the same way.
296    fn moved(self, offset: Vec3) -> Self {
297        Self {
298            eye: self.eye + offset,
299            target: self.target + offset,
300            up: self.up,
301        }
302    }
303
304    /// The same view with its [`View::eye`] and its [`View::target`] turned
305    /// by `turn` about `about`, under the up direction it had.
306    fn turned(self, about: Vec3, turn: Quat) -> Self {
307        Self {
308            eye: about + turn * (self.eye - about),
309            target: about + turn * (self.target - about),
310            up: self.up,
311        }
312    }
313}
314
315/// A view's own directions, each one meter long.
316struct Basis {
317    /// Where the view looks.
318    forward: Vec3,
319    /// Across the screen, to the right.
320    across: Vec3,
321    /// Up the screen, at a right angle to [`Basis::forward`] and
322    /// [`Basis::across`].
323    upward: Vec3,
324    /// The up direction the view holds, which it is drawn the right way up
325    /// by and turned about.
326    up: Vec3,
327}
328
329/// Screen extent of the world, and its foreshortening by depth.
330///
331/// The view is always the drawing area's shape; games never need an aspect
332/// ratio.
333#[derive(Clone, Copy, Debug, PartialEq)]
334pub struct Projection {
335    lens: Lens,
336    near: f32,
337    far: f32,
338}
339
340impl Projection {
341    const DEFAULT_NEAR: f32 = 0.1;
342    const DEFAULT_FAR: f32 = 1000.0;
343
344    /// Foreshortened by depth, over a vertical field of view, in degrees.
345    pub const fn perspective(fov_degrees: f32) -> Self {
346        Self {
347            lens: Lens::Perspective { fov_degrees },
348            near: Self::DEFAULT_NEAR,
349            far: Self::DEFAULT_FAR,
350        }
351    }
352
353    /// No foreshortening; `world_height` meters of world are visible over the
354    /// drawing area's height.
355    pub const fn orthographic(world_height: f32) -> Self {
356        Self {
357            lens: Lens::Orthographic { world_height },
358            near: Self::DEFAULT_NEAR,
359            far: Self::DEFAULT_FAR,
360        }
361    }
362
363    /// Sets the distances, in meters, between which the world is visible;
364    /// `0.1..1000.0` by default.
365    #[must_use]
366    pub const fn clip(mut self, clip: Range<f32>) -> Self {
367        self.near = clip.start;
368        self.far = clip.end;
369        self
370    }
371
372    /// This projection's lens, and the number shaping it.
373    pub const fn lens(&self) -> Lens {
374        self.lens
375    }
376
377    /// This projection's `near` clip distance, in meters.
378    pub const fn near(&self) -> f32 {
379        self.near
380    }
381
382    /// This projection's `far` clip distance, in meters.
383    pub const fn far(&self) -> f32 {
384        self.far
385    }
386
387    /// The same projection over the fraction `1.0 / factor` of the world
388    /// this one covers. Under [`Lens::Perspective`] that is this projection
389    /// unchanged: there the distance from the [`View::eye`] is what changes
390    /// how much world the view covers, not the lens.
391    ///
392    /// Takes a `factor` the caller has already read as finite and above
393    /// zero; [`Lens::Perspective`] returns itself whatever it is given.
394    /// `None` where the world height it leaves is not finite or not above
395    /// zero. A lens whose height was already zero or under it leaves such
396    /// a height whatever `factor` is.
397    pub(crate) fn zoomed(&self, factor: f32) -> Option<Self> {
398        let Lens::Orthographic { world_height } = self.lens else {
399            return Some(*self);
400        };
401        let world_height = world_height / factor;
402
403        (world_height.is_finite() && world_height > 0.0).then_some(Self {
404            lens: Lens::Orthographic { world_height },
405            ..*self
406        })
407    }
408
409    fn matrix(&self, aspect: f32) -> Mat4 {
410        match self.lens {
411            Lens::Perspective { fov_degrees } => {
412                perspective(fov_degrees.to_radians(), aspect, self.near, self.far)
413            }
414            Lens::Orthographic { world_height } => {
415                let half_height = world_height / 2.0;
416                let half_width = half_height * aspect;
417                orthographic(
418                    -half_width,
419                    half_width,
420                    -half_height,
421                    half_height,
422                    self.near,
423                    self.far,
424                )
425            }
426        }
427    }
428}
429
430/// The shape a projection applies to depth, and the number shaping it.
431#[derive(Clone, Copy, Debug, PartialEq)]
432pub enum Lens {
433    /// Foreshortens with distance.
434    Perspective {
435        /// The vertical field of view, in degrees.
436        fov_degrees: f32,
437    },
438    /// Keeps size the same at any distance.
439    Orthographic {
440        /// The meters of world visible over the drawing area's height.
441        world_height: f32,
442    },
443}
444
445/// The six planes a camera draws within, in world space, each facing what it
446/// keeps.
447pub(crate) struct Frustum([Vec4; 6]);
448
449impl Frustum {
450    /// The planes `view_projection` clips against, read off its rows: the
451    /// sides from the last row plus and minus the first two, the nearest
452    /// from the depth row alone (depth runs from zero), the furthest from
453    /// the last row minus it.
454    pub(crate) fn new(view_projection: Mat4) -> Self {
455        let [across, up, depth, clip] = [0, 1, 2, 3].map(|row| view_projection.row(row));
456
457        Self(
458            [
459                clip + across,
460                clip - across,
461                clip + up,
462                clip - up,
463                depth,
464                clip - depth,
465            ]
466            .map(facing_inward),
467        )
468    }
469
470    /// True where the camera covers any of `sphere`: one that crosses a
471    /// plane is kept, one left past a plane is not.
472    pub(crate) fn holds(&self, sphere: BoundingSphere) -> bool {
473        let center = sphere.center().extend(1.0);
474
475        !self
476            .0
477            .iter()
478            .any(|plane| plane.dot(center) < -sphere.radius())
479    }
480}
481
482/// `plane` scaled to a unit normal, so what it measures is in meters. A
483/// plane that a squashed matrix left with no direction is returned as
484/// `Vec4::ZERO`, which keeps nothing out.
485fn facing_inward(plane: Vec4) -> Vec4 {
486    let reach = plane.truncate().length();
487    if reach.is_normal() {
488        plane / reach
489    } else {
490        Vec4::ZERO
491    }
492}
493
494/// `size` as the surface a pixel is measured over; `None` where either
495/// side is zero.
496fn surface(size: UVec2) -> Option<Vec2> {
497    (size.x > 0 && size.y > 0).then(|| size.as_vec2())
498}
499
500/// Position of `pixel` in clip space: `-1..1` across the surface and up
501/// it, which pixels measure down.
502fn clip_space(pixel: Vec2, size: Vec2) -> Vec2 {
503    let across = pixel / size * 2.0 - Vec2::ONE;
504    Vec2::new(across.x, -across.y)
505}
506
507#[cfg(test)]
508mod tests {
509    use super::*;
510    use crate::ray;
511
512    /// The surface every ray is taken across, in physical pixels.
513    const SIZE: UVec2 = UVec2::new(1280, 720);
514
515    /// Every lens a ray is taken through.
516    const LENSES: [Projection; 2] = [
517        Projection::perspective(60.0),
518        Projection::orthographic(20.0),
519    ];
520
521    /// The corners of that surface, and its middle.
522    const PIXELS: [Vec2; 5] = [
523        Vec2::ZERO,
524        Vec2::new(1280.0, 0.0),
525        Vec2::new(0.0, 720.0),
526        Vec2::new(1280.0, 720.0),
527        Vec2::new(640.0, 360.0),
528    ];
529
530    /// Looking down at the origin from up and back, through `projection`.
531    fn overhead(projection: Projection) -> Camera {
532        Camera::new(
533            View::look_at(Vec3::new(0.0, 10.0, 10.0), Vec3::ZERO),
534            projection,
535        )
536    }
537
538    fn looking(camera: &Camera) -> Vec3 {
539        (camera.view().target() - camera.view().eye()).normalize()
540    }
541
542    #[test]
543    fn the_middle_pixel_looks_where_the_camera_does() {
544        for projection in LENSES {
545            let camera = overhead(projection);
546            let ray = camera.ray_through(SIZE.as_vec2() / 2.0, SIZE);
547
548            assert!(
549                ray.direction().abs_diff_eq(looking(&camera), 1e-5),
550                "{:?} against {:?}",
551                ray.direction(),
552                looking(&camera)
553            );
554        }
555    }
556
557    #[test]
558    fn a_foreshortened_view_takes_every_ray_from_the_eye() {
559        let camera = overhead(Projection::perspective(60.0));
560
561        for pixel in PIXELS {
562            let ray = camera.ray_through(pixel, SIZE);
563            assert!(ray.origin().abs_diff_eq(camera.view().eye(), 1e-4));
564        }
565    }
566
567    #[test]
568    fn a_flat_view_takes_every_ray_from_the_near_plane_it_lies_in() {
569        let camera = overhead(Projection::orthographic(20.0));
570        let middle = camera.ray_through(SIZE.as_vec2() / 2.0, SIZE);
571        let near = camera.view().eye() + looking(&camera) * camera.projection().near();
572
573        assert!(middle.origin().abs_diff_eq(near, 1e-4));
574        for pixel in PIXELS {
575            let ray = camera.ray_through(pixel, SIZE);
576            assert!(
577                ray.direction().abs_diff_eq(middle.direction(), 1e-5),
578                "every ray is parallel, {pixel} was not"
579            );
580            assert!(
581                (ray.origin() - near).dot(middle.direction()).abs() < 1e-3,
582                "and starts in the same plane, {pixel} did not"
583            );
584        }
585    }
586
587    #[test]
588    fn a_surface_with_no_area_names_a_ray_that_reaches_nothing() {
589        for projection in LENSES {
590            let ray = overhead(projection).ray_through(Vec2::ZERO, UVec2::ZERO);
591
592            assert_eq!(ray.direction(), Vec3::ZERO);
593            assert_eq!(
594                ray.hit_plane(ray::Plane {
595                    point: Vec3::ZERO,
596                    normal: Vec3::Y
597                }),
598                None
599            );
600        }
601    }
602
603    /// The reach of a camera at the origin looking down `-Z`, over a square
604    /// target.
605    fn ahead() -> Frustum {
606        Frustum::new(
607            Camera::new(
608                View::look_at(Vec3::ZERO, Vec3::NEG_Z),
609                Projection::perspective(60.0),
610            )
611            .view_projection(1.0),
612        )
613    }
614
615    /// A one-meter sphere `at`.
616    fn ball(at: Vec3) -> BoundingSphere {
617        BoundingSphere::new(at, 1.0)
618    }
619
620    #[test]
621    fn a_camera_reaches_what_is_ahead_of_it_and_nothing_past_its_own_planes() {
622        let seen = ahead();
623
624        assert!(seen.holds(ball(Vec3::new(0.0, 0.0, -5.0))));
625        assert!(!seen.holds(ball(Vec3::new(0.0, 0.0, 5.0))), "behind it");
626        assert!(!seen.holds(ball(Vec3::new(0.0, 0.0, -2000.0))), "past far");
627        assert!(!seen.holds(ball(Vec3::new(20.0, 0.0, -5.0))), "beside it");
628        assert!(!seen.holds(ball(Vec3::new(0.0, 20.0, -5.0))), "above it");
629    }
630
631    #[test]
632    fn a_sphere_lying_across_a_plane_is_reached_by_the_camera_it_crosses() {
633        let seen = ahead();
634
635        assert!(
636            seen.holds(ball(Vec3::new(0.0, 0.0, 0.5))),
637            "one behind the eye still reaching in front of it is kept"
638        );
639        assert!(
640            seen.holds(ball(Vec3::new(3.0, 0.0, -5.0))),
641            "and so is one reaching in over the side"
642        );
643        assert!(
644            seen.holds(BoundingSphere::new(Vec3::new(0.0, 0.0, 400.0), 1e4)),
645            "as is one the whole view sits inside"
646        );
647    }
648
649    #[test]
650    fn a_camera_with_no_shape_to_it_keeps_every_sphere() {
651        let squashed = Frustum::new(Mat4::ZERO);
652        let nowhere = Frustum::new(
653            Camera::new(
654                View::look_at(Vec3::ZERO, Vec3::ZERO),
655                Projection::perspective(60.0),
656            )
657            .view_projection(0.0),
658        );
659
660        assert!(squashed.holds(ball(Vec3::new(0.0, 0.0, 5.0))));
661        assert!(nowhere.holds(ball(Vec3::new(0.0, 0.0, 5.0))));
662        assert!(
663            ahead().holds(ball(Vec3::NAN)),
664            "and a sphere placed nowhere is kept by any camera"
665        );
666    }
667
668    #[test]
669    fn a_ray_lands_where_the_pixel_it_came_from_draws() {
670        for projection in LENSES {
671            let camera = overhead(projection);
672
673            for pixel in PIXELS {
674                let ray = camera.ray_through(pixel, SIZE);
675                let Some(distance) = ray.hit_plane(ray::Plane {
676                    point: Vec3::ZERO,
677                    normal: Vec3::Y,
678                }) else {
679                    panic!("{pixel} of an overhead view reaches the ground");
680                };
681                let ground = ray.at(distance);
682
683                assert!(ground.y.abs() < 1e-3, "{ground} left the ground");
684                let drawn = camera.pixel_of(ground, SIZE);
685                assert!(
686                    drawn.is_some_and(|drawn| drawn.abs_diff_eq(pixel, 0.05)),
687                    "{pixel} landed at {ground}, which draws at {drawn:?}"
688                );
689            }
690        }
691    }
692
693    #[test]
694    fn every_point_a_ray_reaches_draws_back_at_the_pixel_it_came_from() {
695        for projection in LENSES {
696            let camera = overhead(projection);
697
698            for pixel in PIXELS {
699                let ray = camera.ray_through(pixel, SIZE);
700
701                for distance in [0.5, 3.0, 40.0] {
702                    let point = ray.at(distance);
703                    let drawn = camera.pixel_of(point, SIZE);
704
705                    assert!(
706                        drawn.is_some_and(|drawn| drawn.abs_diff_eq(pixel, 0.05)),
707                        "{pixel} reaches {point} at {distance} meters, which draws at {drawn:?}"
708                    );
709                }
710            }
711        }
712    }
713
714    #[test]
715    fn a_point_behind_the_camera_draws_nowhere_and_has_no_scale() {
716        for projection in LENSES {
717            let camera = overhead(projection);
718            let behind = camera.view().eye() - looking(&camera) * 5.0;
719
720            assert_eq!(camera.pixel_of(behind, SIZE), None);
721            assert_eq!(camera.pixels_per_meter(behind, SIZE), None);
722        }
723    }
724
725    #[test]
726    fn two_points_a_meter_apart_across_the_view_draw_the_scale_apart() {
727        for projection in LENSES {
728            let camera = overhead(projection);
729            let across = looking(&camera).cross(camera.view().up()).normalize();
730            let point = camera.view().eye() + looking(&camera) * 8.0;
731
732            let (Some(scale), Some(here), Some(there)) = (
733                camera.pixels_per_meter(point, SIZE),
734                camera.pixel_of(point, SIZE),
735                camera.pixel_of(point + across, SIZE),
736            ) else {
737                panic!("{point} is in front of a {projection:?} camera");
738            };
739
740            assert!(
741                ((there - here).length() - scale).abs() < 0.05,
742                "a meter draws {} pixels across, against a scale of {scale}",
743                (there - here).length()
744            );
745        }
746    }
747
748    #[test]
749    fn depth_shrinks_the_scale_of_a_foreshortened_view_and_leaves_a_flat_one_alone() {
750        for projection in LENSES {
751            let camera = overhead(projection);
752            let [near, far] = [5.0, 20.0].map(|depth| {
753                camera.pixels_per_meter(camera.view().eye() + looking(&camera) * depth, SIZE)
754            });
755
756            let (Some(near), Some(far)) = (near, far) else {
757                panic!("both depths lie in front of a {projection:?} camera");
758            };
759
760            match projection.lens() {
761                Lens::Perspective { .. } => {
762                    assert!(far < near, "{far} at 20 meters is not under {near} at 5")
763                }
764                Lens::Orthographic { .. } => assert_eq!(near, far),
765            }
766        }
767    }
768
769    #[test]
770    fn a_shifted_camera_draws_the_point_at_the_pixel_it_was_given_and_looks_the_same_way() {
771        for projection in LENSES {
772            let camera = overhead(projection);
773            let point = Vec3::new(2.0, 0.0, -1.0);
774
775            for lands_at in PIXELS {
776                let Some(shifted) = camera.shifted_so(point, lands_at, SIZE) else {
777                    panic!("{point} draws under a {projection:?} camera");
778                };
779
780                let drawn = shifted.pixel_of(point, SIZE);
781                assert!(
782                    drawn.is_some_and(|drawn| drawn.abs_diff_eq(lands_at, 0.01)),
783                    "given {lands_at}, drew at {drawn:?}"
784                );
785                assert!(
786                    looking(&shifted).abs_diff_eq(looking(&camera), 1e-6),
787                    "and the camera turned to {:?}",
788                    looking(&shifted)
789                );
790                assert_eq!(shifted.projection(), camera.projection());
791            }
792        }
793    }
794
795    #[test]
796    fn a_camera_zoomed_about_a_point_keeps_its_pixel_and_halves_what_the_view_covers() {
797        for projection in LENSES {
798            let camera = overhead(projection);
799            let point = Vec3::new(2.0, 0.0, -1.0);
800
801            let (Some(before), Some(zoomed)) = (
802                camera.pixel_of(point, SIZE),
803                camera.zoomed_about(point, 2.0, SIZE),
804            ) else {
805                panic!("{point} draws under a {projection:?} camera");
806            };
807
808            let drawn = zoomed.pixel_of(point, SIZE);
809            assert!(
810                drawn.is_some_and(|drawn| drawn.abs_diff_eq(before, 0.01)),
811                "{point} drew at {before} and now draws at {drawn:?}"
812            );
813            assert!(
814                looking(&zoomed).abs_diff_eq(looking(&camera), 1e-6),
815                "and the camera turned to {:?}",
816                looking(&zoomed)
817            );
818
819            match (camera.projection().lens(), zoomed.projection().lens()) {
820                (Lens::Perspective { fov_degrees }, Lens::Perspective { fov_degrees: same }) => {
821                    let (was, now) = (
822                        camera.view().eye().distance(point),
823                        zoomed.view().eye().distance(point),
824                    );
825                    assert!(
826                        (now - was / 2.0).abs() < 1e-4,
827                        "{was} meters away became {now}"
828                    );
829                    assert_eq!(fov_degrees, same, "over the same field of view");
830                }
831                (
832                    Lens::Orthographic { world_height },
833                    Lens::Orthographic {
834                        world_height: halved,
835                    },
836                ) => {
837                    assert!(
838                        (halved - world_height / 2.0).abs() < 1e-4,
839                        "{world_height} meters of world became {halved}"
840                    );
841                    let along = looking(&camera);
842                    assert!(
843                        (zoomed.view().eye() - camera.view().eye()).dot(along).abs() < 1e-4,
844                        "and the eye kept its depth"
845                    );
846                }
847                (lens, zoomed) => panic!("{lens:?} zoomed to a {zoomed:?}"),
848            }
849        }
850    }
851
852    /// The number the lens takes its shape from, which every camera a zoom
853    /// returns must still hold.
854    fn covered(camera: &Camera) -> f32 {
855        match camera.projection().lens() {
856            Lens::Perspective { fov_degrees } => fov_degrees,
857            Lens::Orthographic { world_height } => world_height,
858        }
859    }
860
861    #[test]
862    fn every_camera_a_zoom_returns_still_draws_the_point_it_was_zoomed_about() {
863        let point = Vec3::new(2.0, 0.0, -1.0);
864        let factors = [
865            f32::MIN_POSITIVE,
866            1e-38,
867            1e-30,
868            1e-20,
869            1e-12,
870            1e-8,
871            1e-6,
872            0.5,
873            1.0,
874            2.0,
875            1e6,
876        ];
877
878        for projection in LENSES {
879            let camera = overhead(projection);
880
881            for factor in factors {
882                let Some(zoomed) = camera.zoomed_about(point, factor, SIZE) else {
883                    continue;
884                };
885
886                assert!(
887                    zoomed.pixel_of(point, SIZE).is_some(),
888                    "a {projection:?} camera zoomed by {factor} draws nothing"
889                );
890                assert!(
891                    zoomed.view().eye().is_finite() && zoomed.view().target().is_finite(),
892                    "and sits at {:?} looking at {:?}",
893                    zoomed.view().eye(),
894                    zoomed.view().target()
895                );
896                let shape = covered(&zoomed);
897                assert!(
898                    shape.is_finite() && shape > 0.0,
899                    "and its lens is shaped by {shape}"
900                );
901            }
902            assert!(camera.zoomed_about(point, 2.0, SIZE).is_some());
903        }
904    }
905
906    #[test]
907    fn a_zoom_out_doubles_what_the_view_covers_and_a_zoom_of_one_changes_nothing() {
908        for projection in LENSES {
909            let camera = overhead(projection);
910            let point = Vec3::new(2.0, 0.0, -1.0);
911
912            let (Some(before), Some(out)) = (
913                camera.pixel_of(point, SIZE),
914                camera.zoomed_about(point, 0.5, SIZE),
915            ) else {
916                panic!("{point} draws under a {projection:?} camera");
917            };
918
919            let drawn = out.pixel_of(point, SIZE);
920            assert!(
921                drawn.is_some_and(|drawn| drawn.abs_diff_eq(before, 0.01)),
922                "{point} drew at {before} and now draws at {drawn:?}"
923            );
924            match projection.lens() {
925                Lens::Perspective { .. } => {
926                    let (was, now) = (
927                        camera.view().eye().distance(point),
928                        out.view().eye().distance(point),
929                    );
930                    assert!((now - was * 2.0).abs() < 1e-3, "{was} meters became {now}");
931                }
932                Lens::Orthographic { world_height } => assert!(
933                    (covered(&out) - world_height * 2.0).abs() < 1e-3,
934                    "{world_height} meters of world became {}",
935                    covered(&out)
936                ),
937            }
938
939            assert_eq!(
940                camera.zoomed_about(point, 1.0, SIZE),
941                Some(camera),
942                "and a zoom of one leaves the camera where it was"
943            );
944        }
945    }
946
947    #[test]
948    fn a_camera_turned_about_a_point_draws_it_at_the_pixel_it_drew_at() {
949        for projection in LENSES {
950            let camera = overhead(projection);
951            let point = Vec3::new(2.0, 0.0, -1.0);
952            let Some(before) = camera.pixel_of(point, SIZE) else {
953                panic!("{point} draws under a {projection:?} camera");
954            };
955
956            for (yaw, pitch) in [(0.5, 0.0), (0.0, 0.3), (-1.2, 0.4), (3.0, -0.6)] {
957                let Some(turned) = camera.turned_about(point, yaw, pitch) else {
958                    panic!("a turn of {yaw} and {pitch} keeps {point} in front of the camera");
959                };
960
961                let drawn = turned.pixel_of(point, SIZE);
962                assert!(
963                    drawn.is_some_and(|drawn| drawn.abs_diff_eq(before, 0.05)),
964                    "{point} drew at {before} and, turned by {yaw} and {pitch}, draws at {drawn:?}"
965                );
966                assert!(
967                    (turned.view().eye().distance(point) - camera.view().eye().distance(point))
968                        .abs()
969                        < 1e-3,
970                    "and it turned to {} meters from {} away",
971                    turned.view().eye().distance(point),
972                    camera.view().eye().distance(point)
973                );
974                assert_eq!(turned.projection(), camera.projection());
975            }
976        }
977    }
978
979    #[test]
980    fn a_turn_keeps_the_point_at_its_pixel_under_a_view_with_an_up_of_its_own() {
981        let tilted = Camera::new(
982            View::look_at(Vec3::new(0.0, 10.0, 10.0), Vec3::ZERO)
983                .with_up(Vec3::new(0.3, 1.0, 0.0).normalize()),
984            Projection::perspective(60.0),
985        );
986        let point = Vec3::new(2.0, 0.0, -1.0);
987        let Some(before) = tilted.pixel_of(point, SIZE) else {
988            panic!("{point} draws under a camera holding its own up");
989        };
990
991        for (yaw, pitch) in [(0.7, 0.0), (0.0, 0.4), (-1.1, 0.25)] {
992            let Some(turned) = tilted.turned_about(point, yaw, pitch) else {
993                panic!("a turn of {yaw} and {pitch} keeps {point} in front of the camera");
994            };
995
996            let drawn = turned.pixel_of(point, SIZE);
997            assert!(
998                drawn.is_some_and(|drawn| drawn.abs_diff_eq(before, 0.05)),
999                "{point} drew at {before} and, turned by {yaw} and {pitch}, draws at {drawn:?}"
1000            );
1001        }
1002    }
1003
1004    #[test]
1005    fn a_turn_of_a_whole_circle_returns_the_camera_it_started_from() {
1006        for projection in LENSES {
1007            let camera = overhead(projection);
1008            let point = Vec3::new(2.0, 0.0, -1.0);
1009
1010            let Some(turned) = camera.turned_about(point, core::f32::consts::TAU, 0.0) else {
1011                panic!("a whole circle about {point} is a turn a {projection:?} camera takes");
1012            };
1013
1014            assert!(
1015                turned.view().eye().abs_diff_eq(camera.view().eye(), 1e-4),
1016                "the eye came back to {:?} from {:?}",
1017                turned.view().eye(),
1018                camera.view().eye()
1019            );
1020            assert!(
1021                turned
1022                    .view()
1023                    .target()
1024                    .abs_diff_eq(camera.view().target(), 1e-4),
1025                "and looks at {:?}",
1026                turned.view().target()
1027            );
1028            assert_eq!(turned.projection(), camera.projection());
1029        }
1030    }
1031
1032    #[test]
1033    fn a_quarter_turn_moves_the_eye_a_quarter_of_the_way_about_the_point() {
1034        let camera = overhead(Projection::perspective(60.0));
1035        let point = Vec3::new(2.0, 0.0, -1.0);
1036
1037        let Some(turned) = camera.turned_about(point, core::f32::consts::FRAC_PI_2, 0.0) else {
1038            panic!("a quarter turn about {point} is a turn this camera takes");
1039        };
1040
1041        let (was, now) = (camera.view().eye() - point, turned.view().eye() - point);
1042        let flat = |offset: Vec3| Vec2::new(offset.x, offset.z);
1043
1044        assert!(
1045            (now.length() - was.length()).abs() < 1e-4,
1046            "{} meters out became {}",
1047            was.length(),
1048            now.length()
1049        );
1050        assert!((now.y - was.y).abs() < 1e-4, "and left the height alone");
1051        assert!(
1052            (flat(now).angle_to(flat(was)).abs() - core::f32::consts::FRAC_PI_2).abs() < 1e-4,
1053            "a quarter of the way about {point}, not {} radians",
1054            flat(now).angle_to(flat(was))
1055        );
1056    }
1057
1058    #[test]
1059    fn a_turn_that_takes_the_view_past_the_up_direction_returns_no_camera() {
1060        let level = Camera::new(
1061            View::look_at(Vec3::new(0.0, 0.0, 10.0), Vec3::ZERO),
1062            Projection::perspective(60.0),
1063        );
1064        let short_of = core::f32::consts::FRAC_PI_2 - 0.1;
1065
1066        for pitch in [short_of, -short_of] {
1067            assert!(
1068                level.turned_about(Vec3::ZERO, 0.0, pitch).is_some(),
1069                "{pitch} radians leaves the view under the pole"
1070            );
1071        }
1072        for pitch in [
1073            short_of + 0.2,
1074            -short_of - 0.2,
1075            core::f32::consts::PI,
1076            -core::f32::consts::PI,
1077        ] {
1078            assert_eq!(
1079                level.turned_about(Vec3::ZERO, 0.0, pitch),
1080                None,
1081                "{pitch} radians takes it past the pole"
1082            );
1083        }
1084    }
1085
1086    #[test]
1087    fn a_turn_about_a_point_the_camera_does_not_draw_returns_no_camera() {
1088        for projection in LENSES {
1089            let camera = overhead(projection);
1090            let behind = camera.view().eye() - looking(&camera) * 5.0;
1091            let nowhere = Camera::new(View::look_at(Vec3::ZERO, Vec3::ZERO), projection);
1092
1093            assert_eq!(camera.turned_about(behind, 0.5, 0.0), None);
1094            assert_eq!(camera.turned_about(camera.view().eye(), 0.5, 0.0), None);
1095            assert_eq!(nowhere.turned_about(Vec3::NEG_Z, 0.5, 0.0), None);
1096
1097            for angle in [f32::NAN, f32::INFINITY] {
1098                assert_eq!(camera.turned_about(Vec3::ZERO, angle, 0.0), None, "{angle}");
1099                assert_eq!(camera.turned_about(Vec3::ZERO, 0.0, angle), None, "{angle}");
1100            }
1101        }
1102    }
1103
1104    #[test]
1105    fn a_shift_that_would_leave_the_point_undrawn_returns_no_camera() {
1106        for projection in LENSES {
1107            let camera = overhead(projection);
1108
1109            for far in [1e20, 1e25, 1e30, 1e38] {
1110                assert_eq!(
1111                    camera.shifted_so(Vec3::ZERO, Vec2::splat(far), SIZE),
1112                    None,
1113                    "{far} pixels out of a {projection:?} camera"
1114                );
1115            }
1116        }
1117
1118        let wide = Camera::new(
1119            View::look_at(Vec3::new(0.0, 10.0, 10.0), Vec3::ZERO),
1120            Projection::orthographic(f32::MAX),
1121        );
1122
1123        assert!(
1124            wide.pixel_of(Vec3::ZERO, SIZE).is_some()
1125                && wide.pixels_per_meter(Vec3::ZERO, SIZE).is_some(),
1126            "a camera that draws on its own"
1127        );
1128        assert_eq!(wide.shifted_so(Vec3::ZERO, Vec2::splat(1e30), SIZE), None);
1129    }
1130
1131    #[test]
1132    fn neither_move_returns_a_camera_where_no_pixel_is_drawn_or_a_number_is_not_finite() {
1133        for projection in LENSES {
1134            let camera = overhead(projection);
1135            let behind = camera.view().eye() - looking(&camera) * 5.0;
1136            let nowhere = Camera::new(View::look_at(Vec3::ZERO, Vec3::ZERO), projection);
1137            let middle = SIZE.as_vec2() / 2.0;
1138
1139            assert_eq!(camera.shifted_so(behind, middle, SIZE), None);
1140            assert_eq!(camera.zoomed_about(behind, 2.0, SIZE), None);
1141            assert_eq!(nowhere.shifted_so(Vec3::NEG_Z, middle, SIZE), None);
1142            assert_eq!(nowhere.zoomed_about(Vec3::NEG_Z, 2.0, SIZE), None);
1143
1144            for size in [UVec2::ZERO, UVec2::new(1280, 0), UVec2::new(0, 720)] {
1145                assert_eq!(camera.shifted_so(Vec3::ZERO, middle, size), None, "{size}");
1146                assert_eq!(camera.zoomed_about(Vec3::ZERO, 2.0, size), None, "{size}");
1147            }
1148
1149            for lands_at in [Vec2::NAN, Vec2::INFINITY, Vec2::new(0.0, f32::NAN)] {
1150                assert_eq!(camera.shifted_so(Vec3::ZERO, lands_at, SIZE), None);
1151            }
1152            for factor in [0.0, -1.0, f32::NAN, f32::INFINITY] {
1153                assert_eq!(camera.zoomed_about(Vec3::ZERO, factor, SIZE), None);
1154            }
1155        }
1156    }
1157
1158    #[test]
1159    fn a_surface_with_no_area_and_a_view_with_no_direction_draw_no_pixel() {
1160        for projection in LENSES {
1161            let camera = overhead(projection);
1162            let nowhere = Camera::new(View::look_at(Vec3::ZERO, Vec3::ZERO), projection);
1163
1164            for size in [UVec2::ZERO, UVec2::new(1280, 0), UVec2::new(0, 720)] {
1165                assert_eq!(camera.pixel_of(Vec3::ZERO, size), None, "{size}");
1166                assert_eq!(camera.pixels_per_meter(Vec3::ZERO, size), None, "{size}");
1167            }
1168            assert_eq!(nowhere.pixel_of(Vec3::NEG_Z, SIZE), None);
1169            assert_eq!(nowhere.pixels_per_meter(Vec3::NEG_Z, SIZE), None);
1170        }
1171    }
1172}