godot_core/builtin/
projection.rs

1/*
2 * Copyright (c) godot-rust; Bromeon and contributors.
3 * This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
6 */
7
8use std::ops::Mul;
9
10use godot_ffi as sys;
11use sys::{ffi_methods, ExtVariantType, GodotFfi};
12
13use super::{Aabb, Rect2, Vector3};
14use crate::builtin::math::{ApproxEq, GlamConv, GlamType};
15use crate::builtin::{
16    inner, real, Plane, RMat4, RealConv, Transform3D, Vector2, Vector4, Vector4Axis,
17};
18
19/// A 4x4 matrix used for 3D projective transformations.
20///
21/// `Projection` can represent transformations such as translation, rotation, scaling, shearing, and perspective division.
22/// It consists of four [`Vector4`] columns. It is used internally as `Camera3D`'s projection matrix.
23///
24/// For purely linear transformations (translation, rotation, and scale), it is recommended to use [`Transform3D`], as that is
25/// more performant and has a lower memory footprint.
26///
27/// This builtin comes with two related types [`ProjectionEye`] and [`ProjectionPlane`], that are type-safe pendants to Godot's integers.
28///
29/// # All matrix types
30///
31/// | Dimension | Orthogonal basis | Affine transform      | Projective transform   |
32/// |-----------|------------------|-----------------------|------------------------|
33/// | 2D        |                  | [`Transform2D`] (2x3) |                        |
34/// | 3D        | [`Basis`] (3x3)  | [`Transform3D`] (3x4) | **`Projection`** (4x4) |
35///
36/// [`Basis`]: crate::builtin::Basis
37/// [`Transform2D`]: crate::builtin::Transform2D
38/// [`Transform3D`]: Transform3D
39///
40/// # Godot docs
41///
42/// [`Projection` (stable)](https://docs.godotengine.org/en/stable/classes/class_projection.html)
43#[derive(Copy, Clone, PartialEq, Debug)]
44#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
45#[repr(C)]
46pub struct Projection {
47    /// The columns of the projection matrix.
48    pub cols: [Vector4; 4],
49}
50
51impl Projection {
52    /// A Projection with no transformation defined. When applied to other data
53    /// structures, no transformation is performed.
54    pub const IDENTITY: Self = Self::from_diagonal(1.0, 1.0, 1.0, 1.0);
55
56    /// A Projection with all values initialized to 0. When applied to other
57    /// data structures, they will be zeroed.
58    pub const ZERO: Self = Self::from_diagonal(0.0, 0.0, 0.0, 0.0);
59
60    /// Create a new projection from a list of column vectors.
61    pub const fn new(cols: [Vector4; 4]) -> Self {
62        Self { cols }
63    }
64
65    /// Create a diagonal matrix from the given values.
66    pub const fn from_diagonal(x: real, y: real, z: real, w: real) -> Self {
67        Self::from_cols(
68            Vector4::new(x, 0.0, 0.0, 0.0),
69            Vector4::new(0.0, y, 0.0, 0.0),
70            Vector4::new(0.0, 0.0, z, 0.0),
71            Vector4::new(0.0, 0.0, 0.0, w),
72        )
73    }
74
75    /// Create a matrix from four column vectors.
76    ///
77    /// _Godot equivalent: `Projection(Vector4 x_axis, Vector4 y_axis, Vector4 z_axis, Vector4 w_axis)`_
78    pub const fn from_cols(x: Vector4, y: Vector4, z: Vector4, w: Vector4) -> Self {
79        Self { cols: [x, y, z, w] }
80    }
81
82    /// Creates a new Projection that projects positions from a depth range of
83    /// -1 to 1 to one that ranges from 0 to 1, and flips the projected
84    /// positions vertically, according to flip_y.
85    ///
86    /// _Godot equivalent: `Projection.create_depth_correction()`_
87    pub fn create_depth_correction(flip_y: bool) -> Self {
88        Self::from_cols(
89            Vector4::new(1.0, 0.0, 0.0, 0.0),
90            Vector4::new(0.0, if flip_y { -1.0 } else { 1.0 }, 0.0, 0.0),
91            Vector4::new(0.0, 0.0, 0.5, 0.0),
92            Vector4::new(0.0, 0.0, 0.5, 1.0),
93        )
94    }
95
96    /// Creates a new Projection that scales a given projection to fit around
97    /// a given AABB in projection space.
98    ///
99    /// _Godot equivalent: `Projection.create_fit_aabb()`_
100    pub fn create_fit_aabb(aabb: Aabb) -> Self {
101        let translate_unscaled = -2.0 * aabb.position - aabb.size; // -(start+end)
102
103        let scale = Vector3::splat(2.0) / aabb.size;
104        let translate = translate_unscaled / aabb.size;
105
106        Self::from_cols(
107            Vector4::new(scale.x, 0.0, 0.0, 0.0),
108            Vector4::new(0.0, scale.y, 0.0, 0.0),
109            Vector4::new(0.0, 0.0, scale.z, 0.0),
110            Vector4::new(translate.x, translate.y, translate.z, 1.0),
111        )
112    }
113
114    /// Creates a new Projection for projecting positions onto a head-mounted
115    /// display with the given X:Y aspect ratio, distance between eyes, display
116    /// width, distance to lens, oversampling factor, and depth clipping planes.
117    ///
118    /// _Godot equivalent: `Projection.create_for_hmd()`_
119    #[allow(clippy::too_many_arguments)]
120    pub fn create_for_hmd(
121        eye: ProjectionEye,
122        aspect: real,
123        intraocular_dist: real,
124        display_width: real,
125        display_to_lens: real,
126        oversample: real,
127        near: real,
128        far: real,
129    ) -> Self {
130        let mut f1 = (intraocular_dist * 0.5) / display_to_lens;
131        let mut f2 = ((display_width - intraocular_dist) * 0.5) / display_to_lens;
132        let f3 = ((display_width * 0.25 * oversample) / (display_to_lens * aspect)) * near;
133
134        let add = (f1 + f2) * (oversample - 1.0) * 0.5;
135        f1 = (f1 + add) * near;
136        f2 = (f2 + add) * near;
137
138        match eye {
139            ProjectionEye::LEFT => Self::create_frustum(-f2, f1, -f3, f3, near, far),
140            ProjectionEye::RIGHT => Self::create_frustum(-f1, f2, -f3, f3, near, far),
141        }
142    }
143
144    /// Creates a new Projection that projects positions in a frustum with the
145    /// given clipping planes.
146    ///
147    /// _Godot equivalent: `Projection.create_frustum()`_
148    pub fn create_frustum(
149        left: real,
150        right: real,
151        bottom: real,
152        top: real,
153        near: real,
154        far: real,
155    ) -> Self {
156        let dx = right - left;
157        let dy = top - bottom;
158        let dz = near - far;
159
160        let x = 2.0 * near / dx;
161        let y = 2.0 * near / dy;
162        let a = (right + left) / dx;
163        let b = (top + bottom) / dy;
164        let c = (far + near) / dz;
165        let d = 2.0 * near * far / dz;
166
167        Self::from_cols(
168            Vector4::new(x, 0.0, 0.0, 0.0),
169            Vector4::new(0.0, y, 0.0, 0.0),
170            Vector4::new(a, b, c, -1.0),
171            Vector4::new(0.0, 0.0, d, 0.0),
172        )
173    }
174
175    /// Creates a new Projection that projects positions in a frustum with the
176    /// given size, X:Y aspect ratio, offset, and clipping planes.
177    ///
178    /// `flip_fov` determines whether the projection's field of view is flipped
179    /// over its diagonal.
180    ///
181    /// _Godot equivalent: `Projection.create_frustum_aspect()`_
182    pub fn create_frustum_aspect(
183        size: real,
184        aspect: real,
185        offset: Vector2,
186        near: real,
187        far: real,
188        flip_fov: bool,
189    ) -> Self {
190        let (dx, dy) = if flip_fov {
191            (size, size / aspect)
192        } else {
193            (size * aspect, size)
194        };
195        let dz = near - far;
196
197        let x = 2.0 * near / dx;
198        let y = 2.0 * near / dy;
199        let a = 2.0 * offset.x / dx;
200        let b = 2.0 * offset.y / dy;
201        let c = (far + near) / dz;
202        let d = 2.0 * near * far / dz;
203
204        Self::from_cols(
205            Vector4::new(x, 0.0, 0.0, 0.0),
206            Vector4::new(0.0, y, 0.0, 0.0),
207            Vector4::new(a, b, c, -1.0),
208            Vector4::new(0.0, 0.0, d, 0.0),
209        )
210    }
211
212    /// Creates a new Projection that projects positions into the given Rect2.
213    ///
214    /// _Godot equivalent: `Projection.create_light_atlas_rect()`_
215    pub fn create_light_atlas_rect(rect: Rect2) -> Self {
216        Self::from_cols(
217            Vector4::new(rect.size.x, 0.0, 0.0, 0.0),
218            Vector4::new(0.0, rect.size.y, 0.0, 0.0),
219            Vector4::new(0.0, 0.0, 1.0, 0.0),
220            Vector4::new(rect.position.x, rect.position.y, 0.0, 1.0),
221        )
222    }
223
224    /// Creates a new Projection that projects positions using an orthogonal
225    /// projection with the given clipping planes.
226    ///
227    /// _Godot equivalent: `Projection.create_orthogonal()`_
228    pub fn create_orthogonal(
229        left: real,
230        right: real,
231        bottom: real,
232        top: real,
233        near: real,
234        far: real,
235    ) -> Self {
236        RMat4::orthographic_rh_gl(left, right, bottom, top, near, far).to_front()
237    }
238
239    /// Creates a new Projection that projects positions using an orthogonal
240    /// projection with the given size, X:Y aspect ratio, and clipping planes.
241    ///
242    /// `flip_fov` determines whether the projection's field of view is flipped
243    /// over its diagonal.
244    ///
245    /// _Godot equivalent: `Projection.create_orthogonal_aspect()`_
246    pub fn create_orthogonal_aspect(
247        size: real,
248        aspect: real,
249        near: real,
250        far: real,
251        flip_fov: bool,
252    ) -> Self {
253        let f = size / 2.0;
254
255        if flip_fov {
256            let fy = f / aspect;
257            Self::create_orthogonal(-f, f, -fy, fy, near, far)
258        } else {
259            let fx = f * aspect;
260            Self::create_orthogonal(-fx, fx, -f, f, near, far)
261        }
262    }
263
264    /// Creates a new Projection that projects positions using a perspective
265    /// projection with the given Y-axis field of view (in degrees), X:Y aspect
266    /// ratio, and clipping planes
267    ///
268    /// `flip_fov` determines whether the projection's field of view is flipped
269    /// over its diagonal.
270    ///
271    /// _Godot equivalent: `Projection.create_perspective()`_
272    pub fn create_perspective(
273        fov_y: real,
274        aspect: real,
275        near: real,
276        far: real,
277        flip_fov: bool,
278    ) -> Self {
279        let mut fov_y = fov_y.to_radians();
280        if flip_fov {
281            fov_y = ((fov_y * 0.5).tan() / aspect).atan() * 2.0;
282        }
283
284        RMat4::perspective_rh_gl(fov_y, aspect, near, far).to_front()
285    }
286
287    /// Creates a new Projection that projects positions using a perspective
288    /// projection with the given Y-axis field of view (in degrees), X:Y aspect
289    /// ratio, and clipping distances. The projection is adjusted for a
290    /// head-mounted display with the given distance between eyes and distance
291    /// to a point that can be focused on.
292    ///
293    /// `flip_fov` determines whether the projection's field of view is flipped
294    /// over its diagonal.
295    ///
296    /// _Godot equivalent: `Projection.create_perspective_hmd()`_
297    #[allow(clippy::too_many_arguments)]
298    pub fn create_perspective_hmd(
299        fov_y: real,
300        aspect: real,
301        near: real,
302        far: real,
303        flip_fov: bool,
304        eye: ProjectionEye,
305        intraocular_dist: real,
306        convergence_dist: real,
307    ) -> Self {
308        let fov_y = fov_y.to_radians();
309
310        let ymax = if flip_fov {
311            (fov_y * 0.5).tan() / aspect
312        } else {
313            fov_y.tan()
314        } * near;
315        let xmax = ymax * aspect;
316        let frustumshift = (intraocular_dist * near * 0.5) / convergence_dist;
317
318        let (left, right, model_translation) = match eye {
319            ProjectionEye::LEFT => (
320                frustumshift - xmax,
321                xmax + frustumshift,
322                intraocular_dist / 2.0,
323            ),
324            ProjectionEye::RIGHT => (
325                -frustumshift - xmax,
326                xmax - frustumshift,
327                intraocular_dist / -2.0,
328            ),
329        };
330
331        let mut ret = Self::create_frustum(left, right, -ymax, ymax, near, far);
332        ret.cols[0] += ret.cols[3] * model_translation;
333        ret
334    }
335
336    /// Returns the vertical field of view of a projection (in degrees) which
337    /// has the given horizontal field of view (in degrees) and aspect ratio.
338    ///
339    /// _Godot equivalent: `Projection.get_fovy()`_
340    #[doc(alias = "get_fovy")]
341    pub fn create_fovy(fov_x: real, aspect: real) -> real {
342        let half_angle_fov_x = f64::to_radians(fov_x.as_f64() * 0.5);
343        let vertical_transform = f64::atan(aspect.as_f64() * f64::tan(half_angle_fov_x));
344        let full_angle_fov_y = f64::to_degrees(vertical_transform * 2.0);
345
346        real::from_f64(full_angle_fov_y)
347    }
348
349    /// Return the determinant of the matrix.
350    pub fn determinant(&self) -> real {
351        self.glam(|mat| mat.determinant())
352    }
353
354    /// Returns a copy of this projection, with the signs of the values of the Y column flipped.
355    pub fn flipped_y(self) -> Self {
356        let [x, y, z, w] = self.cols;
357        Self::from_cols(x, -y, z, w)
358    }
359
360    /// Returns the X:Y aspect ratio of this Projection's viewport.
361    pub fn aspect(&self) -> real {
362        real::from_f64(self.as_inner().get_aspect())
363    }
364
365    /// Returns the dimensions of the far clipping plane of the projection,
366    /// divided by two.
367    pub fn far_plane_half_extents(&self) -> Vector2 {
368        self.as_inner().get_far_plane_half_extents()
369    }
370
371    /// Returns the horizontal field of view of the projection (in degrees).
372    ///
373    /// _Godot equivalent: `Projection.get_fov()`_
374    pub fn fov(&self) -> real {
375        real::from_f64(self.as_inner().get_fov())
376    }
377
378    /// Returns the factor by which the visible level of detail is scaled by
379    /// this Projection.
380    ///
381    /// _Godot equivalent: `Projection.get_lod_multiplier()`_
382    pub fn lod_multiplier(&self) -> real {
383        real::from_f64(self.as_inner().get_lod_multiplier())
384    }
385
386    /// Returns the number of pixels with the given pixel width displayed per
387    /// meter, after this Projection is applied.
388    ///
389    /// _Godot equivalent: `Projection.get_pixels_per_meter()`_
390    pub fn get_pixels_per_meter(&self, pixel_width: i64) -> i64 {
391        self.as_inner().get_pixels_per_meter(pixel_width)
392    }
393
394    /// Returns the clipping plane of this Projection whose index is given by
395    /// plane.
396    ///
397    /// _Godot equivalent: `Projection.get_projection_plane()`_
398    pub fn get_projection_plane(&self, plane: ProjectionPlane) -> Plane {
399        self.as_inner().get_projection_plane(plane as i64)
400    }
401
402    /// Returns the dimensions of the viewport plane that this Projection
403    /// projects positions onto, divided by two.
404    ///
405    /// _Godot equivalent: `Projection.get_viewport_half_extents()`_
406    pub fn viewport_half_extents(&self) -> Vector2 {
407        self.as_inner().get_viewport_half_extents()
408    }
409
410    /// Returns the distance for this Projection beyond which positions are
411    /// clipped.
412    ///
413    /// _Godot equivalent: `Projection.get_z_far()`_
414    pub fn z_far(&self) -> real {
415        real::from_f64(self.as_inner().get_z_far())
416    }
417
418    /// Returns the distance for this Projection before which positions are
419    /// clipped.
420    ///
421    /// _Godot equivalent: `Projection.get_z_near()`_
422    pub fn z_near(&self) -> real {
423        real::from_f64(self.as_inner().get_z_near())
424    }
425
426    /// Returns a Projection that performs the inverse of this Projection's
427    /// projective transformation.
428    pub fn inverse(self) -> Self {
429        self.glam(|mat| mat.inverse())
430    }
431
432    /// Returns `true` if this Projection performs an orthogonal projection.
433    ///
434    /// _Godot equivalent: `Projection.is_orthogonal()`_
435    pub fn is_orthogonal(&self) -> bool {
436        self.cols[3].w == 1.0
437
438        // TODO: Test the entire last row?
439        // The argument is that W should not mixed with any other dimensions.
440        // But if the only operation is projection and affine, it suffice
441        // to check if input W is nullified (v33 is zero).
442        // (Currently leave it as-is, matching Godot's implementation).
443        // (self.cols[0].w == 0.0) && (self.cols[1].w == 0.0) && (self.cols[2] == 0.0) && (self.cols[3].w == 1.0)
444    }
445
446    /// Returns a Projection with the X and Y values from the given [`Vector2`]
447    /// added to the first and second values of the final column respectively.
448    ///
449    /// _Godot equivalent: `Projection.jitter_offseted()`_
450    #[doc(alias = "jitter_offseted")]
451    #[must_use]
452    pub fn jitter_offset(&self, offset: Vector2) -> Self {
453        Self::from_cols(
454            self.cols[0],
455            self.cols[1],
456            self.cols[2],
457            self.cols[3] + Vector4::new(offset.x, offset.y, 0.0, 0.0),
458        )
459    }
460
461    /// Returns a Projection with the near clipping distance adjusted to be
462    /// `new_znear`.
463    ///
464    /// Note: The original Projection must be a perspective projection.
465    ///
466    /// _Godot equivalent: `Projection.perspective_znear_adjusted()`_
467    pub fn perspective_znear_adjusted(&self, new_znear: real) -> Self {
468        self.as_inner()
469            .perspective_znear_adjusted(new_znear.as_f64())
470    }
471
472    #[doc(hidden)]
473    pub(crate) fn as_inner(&self) -> inner::InnerProjection<'_> {
474        inner::InnerProjection::from_outer(self)
475    }
476}
477
478impl From<Transform3D> for Projection {
479    fn from(trans: Transform3D) -> Self {
480        trans.glam(RMat4::from)
481    }
482}
483
484impl Default for Projection {
485    fn default() -> Self {
486        Self::IDENTITY
487    }
488}
489
490impl Mul for Projection {
491    type Output = Self;
492
493    fn mul(self, rhs: Self) -> Self::Output {
494        self.glam2(&rhs, |a, b| a * b)
495    }
496}
497
498impl Mul<Vector4> for Projection {
499    type Output = Vector4;
500
501    fn mul(self, rhs: Vector4) -> Self::Output {
502        self.glam2(&rhs, |m, v| m * v)
503    }
504}
505
506impl std::ops::Index<Vector4Axis> for Projection {
507    type Output = Vector4;
508
509    fn index(&self, index: Vector4Axis) -> &Self::Output {
510        match index {
511            Vector4Axis::X => &self.cols[0],
512            Vector4Axis::Y => &self.cols[1],
513            Vector4Axis::Z => &self.cols[2],
514            Vector4Axis::W => &self.cols[3],
515        }
516    }
517}
518
519impl ApproxEq for Projection {
520    fn approx_eq(&self, other: &Self) -> bool {
521        for i in 0..4 {
522            let v = self.cols[i];
523            let w = other.cols[i];
524
525            if !v.x.approx_eq(&w.x)
526                || !v.y.approx_eq(&w.y)
527                || !v.z.approx_eq(&w.z)
528                || !v.w.approx_eq(&w.w)
529            {
530                return false;
531            }
532        }
533        true
534    }
535}
536
537impl GlamType for RMat4 {
538    type Mapped = Projection;
539
540    fn to_front(&self) -> Self::Mapped {
541        Projection::from_cols(
542            self.x_axis.to_front(),
543            self.y_axis.to_front(),
544            self.z_axis.to_front(),
545            self.w_axis.to_front(),
546        )
547    }
548
549    fn from_front(mapped: &Self::Mapped) -> Self {
550        Self::from_cols_array_2d(&mapped.cols.map(|v| v.to_glam().to_array()))
551    }
552}
553
554impl GlamConv for Projection {
555    type Glam = RMat4;
556}
557
558// SAFETY: This type is represented as `Self` in Godot, so `*mut Self` is sound.
559unsafe impl GodotFfi for Projection {
560    const VARIANT_TYPE: ExtVariantType = ExtVariantType::Concrete(sys::VariantType::PROJECTION);
561
562    ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. }
563}
564
565crate::meta::impl_godot_as_self!(Projection: ByValue);
566
567/// A projection's clipping plane.
568///
569/// See [Godot docs about `Projection` constants](https://docs.godotengine.org/en/stable/classes/class_projection.html#constants).
570#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
571#[repr(C)]
572pub enum ProjectionPlane {
573    NEAR = 0,
574    FAR = 1,
575    LEFT = 2,
576    TOP = 3,
577    RIGHT = 4,
578    BOTTOM = 5,
579}
580
581impl ProjectionPlane {
582    /// Convert from one of GDScript's `Projection.PLANE_*` integer constants.
583    pub fn try_from_ord(ord: i64) -> Option<Self> {
584        match ord {
585            0 => Some(Self::NEAR),
586            1 => Some(Self::FAR),
587            2 => Some(Self::LEFT),
588            3 => Some(Self::TOP),
589            4 => Some(Self::RIGHT),
590            5 => Some(Self::BOTTOM),
591            _ => None,
592        }
593    }
594}
595
596/// The eye to create a projection for, when creating a projection adjusted for head-mounted displays.
597#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
598#[repr(C)]
599pub enum ProjectionEye {
600    LEFT = 1,
601    RIGHT = 2,
602}
603
604impl ProjectionEye {
605    /// Convert from numbers `1` and `2`.
606    pub fn try_from_ord(ord: i64) -> Option<Self> {
607        match ord {
608            1 => Some(Self::LEFT),
609            2 => Some(Self::RIGHT),
610            _ => None,
611        }
612    }
613}
614
615impl std::fmt::Display for Projection {
616    /// Formats `Projection` to match Godot's string representation.
617    ///
618    /// # Example
619    /// ```
620    /// # use godot::prelude::*;
621    /// let proj = Projection::new([
622    ///     Vector4::new(1.0, 2.5, 1.0, 0.5),
623    ///     Vector4::new(0.0, 1.5, 2.0, 0.5),
624    ///     Vector4::new(0.0, 0.0, 3.0, 2.5),
625    ///     Vector4::new(3.0, 1.0, 4.0, 1.5),
626    /// ]);
627    ///
628    /// const FMT_RESULT: &str = r"
629    /// 1, 0, 0, 3
630    /// 2.5, 1.5, 0, 1
631    /// 1, 2, 3, 4
632    /// 0.5, 0.5, 2.5, 1.5
633    /// ";
634    ///
635    /// assert_eq!(format!("{}", proj), FMT_RESULT);
636    /// ```
637    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
638        write!(
639            f,
640            "\n{}, {}, {}, {}\n{}, {}, {}, {}\n{}, {}, {}, {}\n{}, {}, {}, {}\n",
641            // first row
642            self.cols[0][Vector4Axis::X],
643            self.cols[1][Vector4Axis::X],
644            self.cols[2][Vector4Axis::X],
645            self.cols[3][Vector4Axis::X],
646            // second row
647            self.cols[0][Vector4Axis::Y],
648            self.cols[1][Vector4Axis::Y],
649            self.cols[2][Vector4Axis::Y],
650            self.cols[3][Vector4Axis::Y],
651            // third row
652            self.cols[0][Vector4Axis::Z],
653            self.cols[1][Vector4Axis::Z],
654            self.cols[2][Vector4Axis::Z],
655            self.cols[3][Vector4Axis::Z],
656            // forth row
657            self.cols[0][Vector4Axis::W],
658            self.cols[1][Vector4Axis::W],
659            self.cols[2][Vector4Axis::W],
660            self.cols[3][Vector4Axis::W],
661        )
662    }
663}
664
665#[cfg(test)] #[cfg_attr(published_docs, doc(cfg(test)))]
666mod test {
667    // TODO(bromeon): reduce code duplication
668
669    #![allow(clippy::type_complexity, clippy::excessive_precision)]
670
671    use super::*;
672    use crate::assert_eq_approx;
673
674    const EPSILON: real = 1e-6;
675
676    fn is_matrix_equal_approx(a: &Projection, b: &RMat4) -> bool {
677        a.to_glam().abs_diff_eq(*b, EPSILON)
678    }
679
680    /// Test that diagonals matrices has certain property.
681    #[test]
682    fn test_diagonals() {
683        const DIAGONALS: [[real; 4]; 10] = [
684            [1.0, 1.0, 1.0, 1.0],
685            [2.0, 1.0, 2.0, 1.0],
686            [3.0, 2.0, 1.0, 1.0],
687            [-1.0, -1.0, 1.0, 1.0],
688            [0.0, 0.0, 0.0, 0.0],
689            [-2.0, -3.0, -4.0, -5.0],
690            [0.0, 5.0, -10.0, 50.0],
691            [-1.0, 0.0, 1.0, 100.0],
692            [-15.0, -22.0, 0.0, 11.0],
693            [-1.0, 3.0, 1.0, 0.0],
694        ];
695
696        for [x, y, z, w] in DIAGONALS {
697            let proj = Projection::from_diagonal(x, y, z, w);
698            assert_eq_approx!(
699                proj,
700                RMat4::from_cols_array(&[
701                    x, 0.0, 0.0, 0.0, 0.0, y, 0.0, 0.0, 0.0, 0.0, z, 0.0, 0.0, 0.0, 0.0, w,
702                ]),
703                fn = is_matrix_equal_approx,
704            );
705
706            let det = x * y * z * w;
707            assert_eq_approx!(proj.determinant(), det);
708            if det.abs() > 1e-6 {
709                assert_eq_approx!(
710                    proj.inverse(),
711                    RMat4::from_cols_array_2d(&[
712                        [1.0 / x, 0.0, 0.0, 0.0],
713                        [0.0, 1.0 / y, 0.0, 0.0],
714                        [0.0, 0.0, 1.0 / z, 0.0],
715                        [0.0, 0.0, 0.0, 1.0 / w],
716                    ]),
717                    fn = is_matrix_equal_approx,
718                );
719            }
720        }
721    }
722
723    /// Test `create_orthogonal` method.
724    /// All inputs and outputs are manually computed.
725    #[test]
726    fn test_orthogonal() {
727        const TEST_DATA: [([real; 6], [[real; 4]; 4]); 6] = [
728            (
729                [-1.0, 1.0, -1.0, 1.0, -1.0, 1.0],
730                [
731                    [1.0, 0.0, 0.0, 0.0],
732                    [0.0, 1.0, 0.0, 0.0],
733                    [0.0, 0.0, -1.0, 0.0],
734                    [0.0, 0.0, 0.0, 1.0],
735                ],
736            ),
737            (
738                [0.0, 1.0, 0.0, 1.0, 0.0, 1.0],
739                [
740                    [2.0, 0.0, 0.0, 0.0],
741                    [0.0, 2.0, 0.0, 0.0],
742                    [0.0, 0.0, -2.0, 0.0],
743                    [-1.0, -1.0, -1.0, 1.0],
744                ],
745            ),
746            (
747                [-1.0, 1.0, -1.0, 1.0, 0.0, 1.0],
748                [
749                    [1.0, 0.0, 0.0, 0.0],
750                    [0.0, 1.0, 0.0, 0.0],
751                    [0.0, 0.0, -2.0, 0.0],
752                    [0.0, 0.0, -1.0, 1.0],
753                ],
754            ),
755            (
756                [-10.0, 10.0, -10.0, 10.0, 0.0, 100.0],
757                [
758                    [0.1, 0.0, 0.0, 0.0],
759                    [0.0, 0.1, 0.0, 0.0],
760                    [0.0, 0.0, -0.02, 0.0],
761                    [0.0, 0.0, -1.0, 1.0],
762                ],
763            ),
764            (
765                [-1.0, 1.0, -1.0, 1.0, 1.0, -1.0],
766                [
767                    [1.0, 0.0, 0.0, 0.0],
768                    [0.0, 1.0, 0.0, 0.0],
769                    [0.0, 0.0, 1.0, 0.0],
770                    [0.0, 0.0, 0.0, 1.0],
771                ],
772            ),
773            (
774                [10.0, -10.0, 10.0, -10.0, -10.0, 10.0],
775                [
776                    [-0.1, 0.0, 0.0, 0.0],
777                    [0.0, -0.1, 0.0, 0.0],
778                    [0.0, 0.0, -0.1, 0.0],
779                    [0.0, 0.0, 0.0, 1.0],
780                ],
781            ),
782        ];
783
784        for ([left, right, bottom, top, near, far], mat) in TEST_DATA {
785            assert_eq_approx!(
786                Projection::create_orthogonal(left, right, bottom, top, near, far),
787                RMat4::from_cols_array_2d(&mat),
788                fn = is_matrix_equal_approx,
789                "orthogonal: left={left} right={right} bottom={bottom} top={top} near={near} far={far}",
790            );
791        }
792    }
793
794    /// Test `create_orthogonal_aspect` method.
795    #[test]
796    fn test_orthogonal_aspect() {
797        const TEST_DATA: [((real, real, real, real, bool), [[real; 4]; 4]); 6] = [
798            (
799                (2.0, 1.0, 0.0, 1.0, false),
800                [
801                    [1.0, 0.0, 0.0, 0.0],
802                    [0.0, 1.0, 0.0, 0.0],
803                    [0.0, 0.0, -2.0, 0.0],
804                    [0.0, 0.0, -1.0, 1.0],
805                ],
806            ),
807            (
808                (2.0, 1.0, 0.0, 1.0, true),
809                [
810                    [1.0, 0.0, 0.0, 0.0],
811                    [0.0, 1.0, 0.0, 0.0],
812                    [0.0, 0.0, -2.0, 0.0],
813                    [0.0, 0.0, -1.0, 1.0],
814                ],
815            ),
816            (
817                (1.0, 2.0, 0.0, 100.0, false),
818                [
819                    [1.0, 0.0, 0.0, 0.0],
820                    [0.0, 2.0, 0.0, 0.0],
821                    [0.0, 0.0, -0.02, 0.0],
822                    [0.0, 0.0, -1.0, 1.0],
823                ],
824            ),
825            (
826                (1.0, 2.0, 0.0, 100.0, true),
827                [
828                    [2.0, 0.0, 0.0, 0.0],
829                    [0.0, 4.0, 0.0, 0.0],
830                    [0.0, 0.0, -0.02, 0.0],
831                    [0.0, 0.0, -1.0, 1.0],
832                ],
833            ),
834            (
835                (64.0, 9.0 / 16.0, 0.0, 100.0, false),
836                [
837                    [(1.0 / 32.0) * (16.0 / 9.0), 0.0, 0.0, 0.0],
838                    [0.0, 1.0 / 32.0, 0.0, 0.0],
839                    [0.0, 0.0, -0.02, 0.0],
840                    [0.0, 0.0, -1.0, 1.0],
841                ],
842            ),
843            (
844                (64.0, 9.0 / 16.0, 0.0, 100.0, true),
845                [
846                    [1.0 / 32.0, 0.0, 0.0, 0.0],
847                    [0.0, (1.0 / 32.0) * (9.0 / 16.0), 0.0, 0.0],
848                    [0.0, 0.0, -0.02, 0.0],
849                    [0.0, 0.0, -1.0, 1.0],
850                ],
851            ),
852        ];
853
854        for ((size, aspect, near, far, flip_fov), mat) in TEST_DATA {
855            assert_eq_approx!(
856                Projection::create_orthogonal_aspect(size, aspect, near, far, flip_fov),
857                RMat4::from_cols_array_2d(&mat),
858                fn = is_matrix_equal_approx,
859                "orthogonal aspect: size={size} aspect={aspect} near={near} far={far} flip_fov={flip_fov}"
860            );
861        }
862    }
863
864    #[test]
865    fn test_perspective() {
866        const TEST_DATA: [((real, real, real, real, bool), [[real; 4]; 4]); 5] = [
867            (
868                (90.0, 1.0, 1.0, 2.0, false),
869                [
870                    [1.0, 0.0, 0.0, 0.0],
871                    [0.0, 1.0, 0.0, 0.0],
872                    [0.0, 0.0, -3.0, -1.0],
873                    [0.0, 0.0, -4.0, 0.0],
874                ],
875            ),
876            (
877                (90.0, 1.0, 1.0, 2.0, true),
878                [
879                    [1.0, 0.0, 0.0, 0.0],
880                    [0.0, 1.0, 0.0, 0.0],
881                    [0.0, 0.0, -3.0, -1.0],
882                    [0.0, 0.0, -4.0, 0.0],
883                ],
884            ),
885            (
886                (45.0, 1.0, 0.05, 100.0, false),
887                [
888                    [2.414213562373095, 0.0, 0.0, 0.0],
889                    [0.0, 2.414213562373095, 0.0, 0.0],
890                    [0.0, 0.0, -1.001000500250125, -1.0],
891                    [0.0, 0.0, -0.10005002501250625, 0.0],
892                ],
893            ),
894            (
895                (90.0, 9.0 / 16.0, 1.0, 2.0, false),
896                [
897                    [16.0 / 9.0, 0.0, 0.0, 0.0],
898                    [0.0, 1.0, 0.0, 0.0],
899                    [0.0, 0.0, -3.0, -1.0],
900                    [0.0, 0.0, -4.0, 0.0],
901                ],
902            ),
903            (
904                (90.0, 9.0 / 16.0, 1.0, 2.0, true),
905                [
906                    [1.0, 0.0, 0.0, 0.0],
907                    [0.0, 9.0 / 16.0, 0.0, 0.0],
908                    [0.0, 0.0, -3.0, -1.0],
909                    [0.0, 0.0, -4.0, 0.0],
910                ],
911            ),
912        ];
913
914        for ((fov_y, aspect, near, far, flip_fov), mat) in TEST_DATA {
915            assert_eq_approx!(
916                Projection::create_perspective(fov_y, aspect, near, far, flip_fov),
917                RMat4::from_cols_array_2d(&mat),
918                fn = is_matrix_equal_approx,
919                "perspective: fov_y={fov_y} aspect={aspect} near={near} far={far} flip_fov={flip_fov}"
920            );
921        }
922    }
923
924    #[test]
925    fn test_frustum() {
926        const TEST_DATA: [([real; 6], [[real; 4]; 4]); 3] = [
927            (
928                [-1.0, 1.0, -1.0, 1.0, 1.0, 2.0],
929                [
930                    [1.0, 0.0, 0.0, 0.0],
931                    [0.0, 1.0, 0.0, 0.0],
932                    [0.0, 0.0, -3.0, -1.0],
933                    [0.0, 0.0, -4.0, 0.0],
934                ],
935            ),
936            (
937                [0.0, 1.0, 0.0, 1.0, 1.0, 2.0],
938                [
939                    [2.0, 0.0, 0.0, 0.0],
940                    [0.0, 2.0, 0.0, 0.0],
941                    [1.0, 1.0, -3.0, -1.0],
942                    [0.0, 0.0, -4.0, 0.0],
943                ],
944            ),
945            (
946                [-0.1, 0.1, -0.025, 0.025, 0.05, 100.0],
947                [
948                    [0.5, 0.0, 0.0, 0.0],
949                    [0.0, 2.0, 0.0, 0.0],
950                    [0.0, 0.0, -1.001000500250125, -1.0],
951                    [0.0, 0.0, -0.10005002501250625, 0.0],
952                ],
953            ),
954        ];
955
956        for ([left, right, bottom, top, near, far], mat) in TEST_DATA {
957            assert_eq_approx!(
958                Projection::create_frustum(left, right, bottom, top, near, far),
959                RMat4::from_cols_array_2d(&mat),
960                fn = is_matrix_equal_approx,
961                "frustum: left={left} right={right} bottom={bottom} top={top} near={near} far={far}"
962            );
963        }
964    }
965
966    #[test]
967    fn test_frustum_aspect() {
968        const TEST_DATA: [((real, real, Vector2, real, real, bool), [[real; 4]; 4]); 4] = [
969            (
970                (2.0, 1.0, Vector2::ZERO, 1.0, 2.0, false),
971                [
972                    [1.0, 0.0, 0.0, 0.0],
973                    [0.0, 1.0, 0.0, 0.0],
974                    [0.0, 0.0, -3.0, -1.0],
975                    [0.0, 0.0, -4.0, 0.0],
976                ],
977            ),
978            (
979                (2.0, 1.0, Vector2::ZERO, 1.0, 2.0, true),
980                [
981                    [1.0, 0.0, 0.0, 0.0],
982                    [0.0, 1.0, 0.0, 0.0],
983                    [0.0, 0.0, -3.0, -1.0],
984                    [0.0, 0.0, -4.0, 0.0],
985                ],
986            ),
987            (
988                (1.0, 1.0, Vector2::new(0.5, 0.5), 1.0, 2.0, false),
989                [
990                    [2.0, 0.0, 0.0, 0.0],
991                    [0.0, 2.0, 0.0, 0.0],
992                    [1.0, 1.0, -3.0, -1.0],
993                    [0.0, 0.0, -4.0, 0.0],
994                ],
995            ),
996            (
997                (0.05, 4.0, Vector2::ZERO, 0.05, 100.0, false),
998                [
999                    [0.5, 0.0, 0.0, 0.0],
1000                    [0.0, 2.0, 0.0, 0.0],
1001                    [0.0, 0.0, -1.001000500250125, -1.0],
1002                    [0.0, 0.0, -0.10005002501250625, 0.0],
1003                ],
1004            ),
1005        ];
1006
1007        for ((size, aspect, offset, near, far, flip_fov), mat) in TEST_DATA {
1008            assert_eq_approx!(
1009                Projection::create_frustum_aspect(size, aspect, offset, near, far, flip_fov),
1010                RMat4::from_cols_array_2d(&mat),
1011                fn = is_matrix_equal_approx,
1012                "frustum aspect: size={size} aspect={aspect} offset=({0} {1}) near={near} far={far} flip_fov={flip_fov}",
1013                offset.x,
1014                offset.y,
1015            );
1016        }
1017    }
1018
1019    // TODO: Test create_for_hmd, create_perspective_hmd
1020
1021    #[test]
1022    fn test_is_orthogonal() {
1023        fn f(v: isize) -> real {
1024            (v as real) * 0.5 - 0.5
1025        }
1026
1027        // Orthogonal
1028        for left_i in 0..20 {
1029            let left = f(left_i);
1030            for right in (left_i + 1..=20).map(f) {
1031                for bottom_i in 0..20 {
1032                    let bottom = f(bottom_i);
1033                    for top in (bottom_i + 1..=20).map(f) {
1034                        for near_i in 0..20 {
1035                            let near = f(near_i);
1036                            for far in (near_i + 1..=20).map(f) {
1037                                assert!(
1038                                    Projection::create_orthogonal(left, right, bottom, top, near, far).is_orthogonal(),
1039                                    "projection should be orthogonal: left={left} right={right} bottom={bottom} top={top} near={near} far={far}",
1040                                );
1041                            }
1042                        }
1043                    }
1044                }
1045            }
1046        }
1047
1048        // Perspective
1049        for fov in (0..18).map(|v| (v as real) * 10.0) {
1050            for aspect_x in 1..=10 {
1051                for aspect_y in 1..=10 {
1052                    let aspect = (aspect_x as real) / (aspect_y as real);
1053                    for near_i in 1..10 {
1054                        let near = near_i as real;
1055                        for far in (near_i + 1..=20).map(|v| v as real) {
1056                            assert!(
1057                                !Projection::create_perspective(fov, aspect, near, far, false).is_orthogonal(),
1058                                "projection should be perspective: fov={fov} aspect={aspect} near={near} far={far}",
1059                            );
1060                        }
1061                    }
1062                }
1063            }
1064        }
1065
1066        // Frustum
1067        for left_i in 0..20 {
1068            let left = f(left_i);
1069            for right in (left_i + 1..=20).map(f) {
1070                for bottom_i in 0..20 {
1071                    let bottom = f(bottom_i);
1072                    for top in (bottom_i + 1..=20).map(f) {
1073                        for near_i in 0..20 {
1074                            let near = (near_i as real) * 0.5;
1075                            for far in (near_i + 1..=20).map(|v| (v as real) * 0.5) {
1076                                assert!(
1077                                    !Projection::create_frustum(left, right, bottom, top, near, far).is_orthogonal(),
1078                                    "projection should be perspective: left={left} right={right} bottom={bottom} top={top} near={near} far={far}",
1079                                );
1080                            }
1081                        }
1082                    }
1083                }
1084            }
1085        }
1086
1087        // Size, Aspect, Near, Far
1088        for size in (1..=10).map(|v| v as real) {
1089            for aspect_x in 1..=10 {
1090                for aspect_y in 1..=10 {
1091                    let aspect = (aspect_x as real) / (aspect_y as real);
1092                    for near_i in 1..10 {
1093                        let near = near_i as real;
1094                        for far in (near_i + 1..=20).map(|v| v as real) {
1095                            assert!(
1096                                Projection::create_orthogonal_aspect(size, aspect, near, far, false).is_orthogonal(),
1097                                "projection should be orthogonal: (size={size} aspect={aspect} near={near} far={far}",
1098                            );
1099                            assert!(
1100                                !Projection::create_frustum_aspect(size, aspect, Vector2::ZERO, near, far, false).is_orthogonal(),
1101                                "projection should be perspective: (size={size} aspect={aspect} near={near} far={far}",
1102                            );
1103                        }
1104                    }
1105                }
1106            }
1107        }
1108    }
1109
1110    #[cfg(feature = "serde")] #[cfg_attr(published_docs, doc(cfg(feature = "serde")))]
1111    #[test]
1112    fn serde_roundtrip() {
1113        let projection = Projection::IDENTITY;
1114        let expected_json = "{\"cols\":[{\"x\":1.0,\"y\":0.0,\"z\":0.0,\"w\":0.0},{\"x\":0.0,\"y\":1.0,\"z\":0.0,\"w\":0.0},{\"x\":0.0,\"y\":0.0,\"z\":1.0,\"w\":0.0},{\"x\":0.0,\"y\":0.0,\"z\":0.0,\"w\":1.0}]}";
1115
1116        crate::builtin::test_utils::roundtrip(&projection, expected_json);
1117    }
1118}