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