1use 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#[derive(Copy, Clone, PartialEq, Debug)]
44#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
45#[repr(C)]
46pub struct Projection {
47 pub cols: [Vector4; 4],
49}
50
51impl Projection {
52 pub const IDENTITY: Self = Self::from_diagonal(1.0, 1.0, 1.0, 1.0);
55
56 pub const ZERO: Self = Self::from_diagonal(0.0, 0.0, 0.0, 0.0);
59
60 pub const fn new(cols: [Vector4; 4]) -> Self {
62 Self { cols }
63 }
64
65 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 pub const fn from_cols(x: Vector4, y: Vector4, z: Vector4, w: Vector4) -> Self {
79 Self { cols: [x, y, z, w] }
80 }
81
82 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 pub fn create_fit_aabb(aabb: Aabb) -> Self {
101 let translate_unscaled = -2.0 * aabb.position - aabb.size; 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 #[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 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 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 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 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 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 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 #[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 #[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 pub fn determinant(&self) -> real {
351 self.glam(|mat| mat.determinant())
352 }
353
354 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 pub fn aspect(&self) -> real {
362 real::from_f64(self.as_inner().get_aspect())
363 }
364
365 pub fn far_plane_half_extents(&self) -> Vector2 {
368 self.as_inner().get_far_plane_half_extents()
369 }
370
371 pub fn fov(&self) -> real {
375 real::from_f64(self.as_inner().get_fov())
376 }
377
378 pub fn lod_multiplier(&self) -> real {
383 real::from_f64(self.as_inner().get_lod_multiplier())
384 }
385
386 pub fn get_pixels_per_meter(&self, pixel_width: i64) -> i64 {
391 self.as_inner().get_pixels_per_meter(pixel_width)
392 }
393
394 pub fn get_projection_plane(&self, plane: ProjectionPlane) -> Plane {
399 self.as_inner().get_projection_plane(plane as i64)
400 }
401
402 pub fn viewport_half_extents(&self) -> Vector2 {
407 self.as_inner().get_viewport_half_extents()
408 }
409
410 pub fn z_far(&self) -> real {
415 real::from_f64(self.as_inner().get_z_far())
416 }
417
418 pub fn z_near(&self) -> real {
423 real::from_f64(self.as_inner().get_z_near())
424 }
425
426 pub fn inverse(self) -> Self {
429 self.glam(|mat| mat.inverse())
430 }
431
432 pub fn is_orthogonal(&self) -> bool {
436 self.cols[3].w == 1.0
437
438 }
445
446 #[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 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
558unsafe 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#[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 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#[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 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 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
638 write!(
639 f,
640 "\n{}, {}, {}, {}\n{}, {}, {}, {}\n{}, {}, {}, {}\n{}, {}, {}, {}\n",
641 self.cols[0][Vector4Axis::X],
643 self.cols[1][Vector4Axis::X],
644 self.cols[2][Vector4Axis::X],
645 self.cols[3][Vector4Axis::X],
646 self.cols[0][Vector4Axis::Y],
648 self.cols[1][Vector4Axis::Y],
649 self.cols[2][Vector4Axis::Y],
650 self.cols[3][Vector4Axis::Y],
651 self.cols[0][Vector4Axis::Z],
653 self.cols[1][Vector4Axis::Z],
654 self.cols[2][Vector4Axis::Z],
655 self.cols[3][Vector4Axis::Z],
656 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 #![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]
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]
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]
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 #[test]
1022 fn test_is_orthogonal() {
1023 fn f(v: isize) -> real {
1024 (v as real) * 0.5 - 0.5
1025 }
1026
1027 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 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 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 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}