1use 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#[derive(Copy, Clone, PartialEq, Debug)]
45#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
46#[repr(C)]
47pub struct Projection {
48 pub cols: [Vector4; 4],
50}
51
52impl Projection {
53 pub const IDENTITY: Self = Self::from_diagonal(1.0, 1.0, 1.0, 1.0);
56
57 pub const ZERO: Self = Self::from_diagonal(0.0, 0.0, 0.0, 0.0);
60
61 pub const fn new(cols: [Vector4; 4]) -> Self {
63 Self { cols }
64 }
65
66 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 pub const fn from_cols(x: Vector4, y: Vector4, z: Vector4, w: Vector4) -> Self {
80 Self { cols: [x, y, z, w] }
81 }
82
83 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 pub fn create_fit_aabb(aabb: Aabb) -> Self {
102 let translate_unscaled = -2.0 * aabb.position - aabb.size; 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 #[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 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 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 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 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 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 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 #[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 #[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 pub fn determinant(&self) -> real {
352 self.glam(|mat| mat.determinant())
353 }
354
355 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 pub fn aspect(&self) -> real {
363 real::from_f64(self.as_inner().get_aspect())
364 }
365
366 pub fn far_plane_half_extents(&self) -> Vector2 {
369 self.as_inner().get_far_plane_half_extents()
370 }
371
372 pub fn fov(&self) -> real {
376 real::from_f64(self.as_inner().get_fov())
377 }
378
379 pub fn lod_multiplier(&self) -> real {
384 real::from_f64(self.as_inner().get_lod_multiplier())
385 }
386
387 pub fn get_pixels_per_meter(&self, pixel_width: i64) -> i64 {
392 self.as_inner().get_pixels_per_meter(pixel_width)
393 }
394
395 pub fn get_projection_plane(&self, plane: ProjectionPlane) -> Plane {
400 self.as_inner().get_projection_plane(plane as i64)
401 }
402
403 pub fn viewport_half_extents(&self) -> Vector2 {
408 self.as_inner().get_viewport_half_extents()
409 }
410
411 pub fn z_far(&self) -> real {
416 real::from_f64(self.as_inner().get_z_far())
417 }
418
419 pub fn z_near(&self) -> real {
424 real::from_f64(self.as_inner().get_z_near())
425 }
426
427 pub fn inverse(self) -> Self {
430 self.glam(|mat| mat.inverse())
431 }
432
433 pub fn is_orthogonal(&self) -> bool {
437 self.cols[3].w == 1.0
438
439 }
446
447 #[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 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
559unsafe 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#[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 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#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
599#[repr(C)]
600pub enum ProjectionEye {
601 LEFT = 1,
602 RIGHT = 2,
603}
604
605impl ProjectionEye {
606 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 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
639 write!(
640 f,
641 "\n{}, {}, {}, {}\n{}, {}, {}, {}\n{}, {}, {}, {}\n{}, {}, {}, {}\n",
642 self.cols[0][Vector4Axis::X],
644 self.cols[1][Vector4Axis::X],
645 self.cols[2][Vector4Axis::X],
646 self.cols[3][Vector4Axis::X],
647 self.cols[0][Vector4Axis::Y],
649 self.cols[1][Vector4Axis::Y],
650 self.cols[2][Vector4Axis::Y],
651 self.cols[3][Vector4Axis::Y],
652 self.cols[0][Vector4Axis::Z],
654 self.cols[1][Vector4Axis::Z],
655 self.cols[2][Vector4Axis::Z],
656 self.cols[3][Vector4Axis::Z],
657 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 #![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]
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]
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]
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 #[test]
1024 fn test_is_orthogonal() {
1025 fn f(v: isize) -> real {
1026 (v as real) * 0.5 - 0.5
1027 }
1028
1029 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 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 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 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}