use std::ops::Mul;
use godot_ffi as sys;
use sys::{ExtVariantType, GodotFfi, ffi_methods};
use crate::builtin::math::{ApproxEq, GlamConv, GlamType};
use crate::builtin::{
Aabb, Plane, RMat4, RealConv, Rect2, Transform3D, Vector2, Vector3, Vector4, Vector4Axis,
inner, real,
};
#[derive(Copy, Clone, PartialEq, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[repr(C)]
pub struct Projection {
pub cols: [Vector4; 4],
}
impl Projection {
pub const IDENTITY: Self = Self::from_diagonal(1.0, 1.0, 1.0, 1.0);
pub const ZERO: Self = Self::from_diagonal(0.0, 0.0, 0.0, 0.0);
pub const fn new(cols: [Vector4; 4]) -> Self {
Self { cols }
}
pub const fn from_diagonal(x: real, y: real, z: real, w: real) -> Self {
Self::from_cols(
Vector4::new(x, 0.0, 0.0, 0.0),
Vector4::new(0.0, y, 0.0, 0.0),
Vector4::new(0.0, 0.0, z, 0.0),
Vector4::new(0.0, 0.0, 0.0, w),
)
}
pub const fn from_cols(x: Vector4, y: Vector4, z: Vector4, w: Vector4) -> Self {
Self { cols: [x, y, z, w] }
}
pub fn create_depth_correction(flip_y: bool) -> Self {
Self::from_cols(
Vector4::new(1.0, 0.0, 0.0, 0.0),
Vector4::new(0.0, if flip_y { -1.0 } else { 1.0 }, 0.0, 0.0),
Vector4::new(0.0, 0.0, 0.5, 0.0),
Vector4::new(0.0, 0.0, 0.5, 1.0),
)
}
pub fn create_fit_aabb(aabb: Aabb) -> Self {
let translate_unscaled = -2.0 * aabb.position - aabb.size;
let scale = Vector3::splat(2.0) / aabb.size;
let translate = translate_unscaled / aabb.size;
Self::from_cols(
Vector4::new(scale.x, 0.0, 0.0, 0.0),
Vector4::new(0.0, scale.y, 0.0, 0.0),
Vector4::new(0.0, 0.0, scale.z, 0.0),
Vector4::new(translate.x, translate.y, translate.z, 1.0),
)
}
#[allow(clippy::too_many_arguments)]
pub fn create_for_hmd(
eye: ProjectionEye,
aspect: real,
intraocular_dist: real,
display_width: real,
display_to_lens: real,
oversample: real,
near: real,
far: real,
) -> Self {
let mut f1 = (intraocular_dist * 0.5) / display_to_lens;
let mut f2 = ((display_width - intraocular_dist) * 0.5) / display_to_lens;
let f3 = ((display_width * 0.25 * oversample) / (display_to_lens * aspect)) * near;
let add = (f1 + f2) * (oversample - 1.0) * 0.5;
f1 = (f1 + add) * near;
f2 = (f2 + add) * near;
match eye {
ProjectionEye::LEFT => Self::create_frustum(-f2, f1, -f3, f3, near, far),
ProjectionEye::RIGHT => Self::create_frustum(-f1, f2, -f3, f3, near, far),
}
}
pub fn create_frustum(
left: real,
right: real,
bottom: real,
top: real,
near: real,
far: real,
) -> Self {
let dx = right - left;
let dy = top - bottom;
let dz = near - far;
let x = 2.0 * near / dx;
let y = 2.0 * near / dy;
let a = (right + left) / dx;
let b = (top + bottom) / dy;
let c = (far + near) / dz;
let d = 2.0 * near * far / dz;
Self::from_cols(
Vector4::new(x, 0.0, 0.0, 0.0),
Vector4::new(0.0, y, 0.0, 0.0),
Vector4::new(a, b, c, -1.0),
Vector4::new(0.0, 0.0, d, 0.0),
)
}
pub fn create_frustum_aspect(
size: real,
aspect: real,
offset: Vector2,
near: real,
far: real,
flip_fov: bool,
) -> Self {
let (dx, dy) = if flip_fov {
(size, size / aspect)
} else {
(size * aspect, size)
};
let dz = near - far;
let x = 2.0 * near / dx;
let y = 2.0 * near / dy;
let a = 2.0 * offset.x / dx;
let b = 2.0 * offset.y / dy;
let c = (far + near) / dz;
let d = 2.0 * near * far / dz;
Self::from_cols(
Vector4::new(x, 0.0, 0.0, 0.0),
Vector4::new(0.0, y, 0.0, 0.0),
Vector4::new(a, b, c, -1.0),
Vector4::new(0.0, 0.0, d, 0.0),
)
}
pub fn create_light_atlas_rect(rect: Rect2) -> Self {
Self::from_cols(
Vector4::new(rect.size.x, 0.0, 0.0, 0.0),
Vector4::new(0.0, rect.size.y, 0.0, 0.0),
Vector4::new(0.0, 0.0, 1.0, 0.0),
Vector4::new(rect.position.x, rect.position.y, 0.0, 1.0),
)
}
pub fn create_orthogonal(
left: real,
right: real,
bottom: real,
top: real,
near: real,
far: real,
) -> Self {
RMat4::orthographic_rh_gl(left, right, bottom, top, near, far).to_front()
}
pub fn create_orthogonal_aspect(
size: real,
aspect: real,
near: real,
far: real,
flip_fov: bool,
) -> Self {
let f = size / 2.0;
if flip_fov {
let fy = f / aspect;
Self::create_orthogonal(-f, f, -fy, fy, near, far)
} else {
let fx = f * aspect;
Self::create_orthogonal(-fx, fx, -f, f, near, far)
}
}
pub fn create_perspective(
fov_y: real,
aspect: real,
near: real,
far: real,
flip_fov: bool,
) -> Self {
let mut fov_y = fov_y.to_radians();
if flip_fov {
fov_y = ((fov_y * 0.5).tan() / aspect).atan() * 2.0;
}
RMat4::perspective_rh_gl(fov_y, aspect, near, far).to_front()
}
#[allow(clippy::too_many_arguments)]
pub fn create_perspective_hmd(
fov_y: real,
aspect: real,
near: real,
far: real,
flip_fov: bool,
eye: ProjectionEye,
intraocular_dist: real,
convergence_dist: real,
) -> Self {
let fov_y = fov_y.to_radians();
let ymax = if flip_fov {
(fov_y * 0.5).tan() / aspect
} else {
fov_y.tan()
} * near;
let xmax = ymax * aspect;
let frustumshift = (intraocular_dist * near * 0.5) / convergence_dist;
let (left, right, model_translation) = match eye {
ProjectionEye::LEFT => (
frustumshift - xmax,
xmax + frustumshift,
intraocular_dist / 2.0,
),
ProjectionEye::RIGHT => (
-frustumshift - xmax,
xmax - frustumshift,
intraocular_dist / -2.0,
),
};
let mut ret = Self::create_frustum(left, right, -ymax, ymax, near, far);
ret.cols[0] += ret.cols[3] * model_translation;
ret
}
#[doc(alias = "get_fovy")]
pub fn create_fovy(fov_x: real, aspect: real) -> real {
let half_angle_fov_x = f64::to_radians(fov_x.as_f64() * 0.5);
let vertical_transform = f64::atan(aspect.as_f64() * f64::tan(half_angle_fov_x));
let full_angle_fov_y = f64::to_degrees(vertical_transform * 2.0);
real::from_f64(full_angle_fov_y)
}
pub fn determinant(&self) -> real {
self.glam(|mat| mat.determinant())
}
pub fn flipped_y(self) -> Self {
let [x, y, z, w] = self.cols;
Self::from_cols(x, -y, z, w)
}
pub fn aspect(&self) -> real {
real::from_f64(self.as_inner().get_aspect())
}
pub fn far_plane_half_extents(&self) -> Vector2 {
self.as_inner().get_far_plane_half_extents()
}
pub fn fov(&self) -> real {
real::from_f64(self.as_inner().get_fov())
}
pub fn lod_multiplier(&self) -> real {
real::from_f64(self.as_inner().get_lod_multiplier())
}
pub fn get_pixels_per_meter(&self, pixel_width: i64) -> i64 {
self.as_inner().get_pixels_per_meter(pixel_width)
}
pub fn get_projection_plane(&self, plane: ProjectionPlane) -> Plane {
self.as_inner().get_projection_plane(plane as i64)
}
pub fn viewport_half_extents(&self) -> Vector2 {
self.as_inner().get_viewport_half_extents()
}
pub fn z_far(&self) -> real {
real::from_f64(self.as_inner().get_z_far())
}
pub fn z_near(&self) -> real {
real::from_f64(self.as_inner().get_z_near())
}
pub fn inverse(self) -> Self {
self.glam(|mat| mat.inverse())
}
pub fn is_orthogonal(&self) -> bool {
self.cols[3].w == 1.0
}
#[doc(alias = "jitter_offseted")]
#[must_use]
pub fn jitter_offset(&self, offset: Vector2) -> Self {
Self::from_cols(
self.cols[0],
self.cols[1],
self.cols[2],
self.cols[3] + Vector4::new(offset.x, offset.y, 0.0, 0.0),
)
}
pub fn perspective_znear_adjusted(&self, new_znear: real) -> Self {
self.as_inner()
.perspective_znear_adjusted(new_znear.as_f64())
}
#[doc(hidden)]
pub(crate) fn as_inner(&self) -> inner::InnerProjection<'_> {
inner::InnerProjection::from_outer(self)
}
}
impl From<Transform3D> for Projection {
fn from(trans: Transform3D) -> Self {
trans.glam(RMat4::from)
}
}
impl Default for Projection {
fn default() -> Self {
Self::IDENTITY
}
}
impl Mul for Projection {
type Output = Self;
fn mul(self, rhs: Self) -> Self::Output {
self.glam2(&rhs, |a, b| a * b)
}
}
impl Mul<Vector4> for Projection {
type Output = Vector4;
fn mul(self, rhs: Vector4) -> Self::Output {
self.glam2(&rhs, |m, v| m * v)
}
}
impl std::ops::Index<Vector4Axis> for Projection {
type Output = Vector4;
fn index(&self, index: Vector4Axis) -> &Self::Output {
match index {
Vector4Axis::X => &self.cols[0],
Vector4Axis::Y => &self.cols[1],
Vector4Axis::Z => &self.cols[2],
Vector4Axis::W => &self.cols[3],
}
}
}
impl ApproxEq for Projection {
fn approx_eq(&self, other: &Self) -> bool {
for i in 0..4 {
let v = self.cols[i];
let w = other.cols[i];
if !v.x.approx_eq(&w.x)
|| !v.y.approx_eq(&w.y)
|| !v.z.approx_eq(&w.z)
|| !v.w.approx_eq(&w.w)
{
return false;
}
}
true
}
}
impl GlamType for RMat4 {
type Mapped = Projection;
fn to_front(&self) -> Self::Mapped {
Projection::from_cols(
self.x_axis.to_front(),
self.y_axis.to_front(),
self.z_axis.to_front(),
self.w_axis.to_front(),
)
}
fn from_front(mapped: &Self::Mapped) -> Self {
Self::from_cols_array_2d(&mapped.cols.map(|v| v.to_glam().to_array()))
}
}
impl GlamConv for Projection {
type Glam = RMat4;
}
unsafe impl GodotFfi for Projection {
const VARIANT_TYPE: ExtVariantType = ExtVariantType::Concrete(sys::VariantType::PROJECTION);
ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. }
}
crate::meta::impl_godot_as_self!(Projection: ByValue);
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
#[repr(C)]
pub enum ProjectionPlane {
NEAR = 0,
FAR = 1,
LEFT = 2,
TOP = 3,
RIGHT = 4,
BOTTOM = 5,
}
impl ProjectionPlane {
pub fn try_from_ord(ord: i64) -> Option<Self> {
match ord {
0 => Some(Self::NEAR),
1 => Some(Self::FAR),
2 => Some(Self::LEFT),
3 => Some(Self::TOP),
4 => Some(Self::RIGHT),
5 => Some(Self::BOTTOM),
_ => None,
}
}
}
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
#[repr(C)]
pub enum ProjectionEye {
LEFT = 1,
RIGHT = 2,
}
impl ProjectionEye {
pub fn try_from_ord(ord: i64) -> Option<Self> {
match ord {
1 => Some(Self::LEFT),
2 => Some(Self::RIGHT),
_ => None,
}
}
}
impl std::fmt::Display for Projection {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"\n{}, {}, {}, {}\n{}, {}, {}, {}\n{}, {}, {}, {}\n{}, {}, {}, {}\n",
self.cols[0][Vector4Axis::X],
self.cols[1][Vector4Axis::X],
self.cols[2][Vector4Axis::X],
self.cols[3][Vector4Axis::X],
self.cols[0][Vector4Axis::Y],
self.cols[1][Vector4Axis::Y],
self.cols[2][Vector4Axis::Y],
self.cols[3][Vector4Axis::Y],
self.cols[0][Vector4Axis::Z],
self.cols[1][Vector4Axis::Z],
self.cols[2][Vector4Axis::Z],
self.cols[3][Vector4Axis::Z],
self.cols[0][Vector4Axis::W],
self.cols[1][Vector4Axis::W],
self.cols[2][Vector4Axis::W],
self.cols[3][Vector4Axis::W],
)
}
}
#[cfg(test)] #[cfg_attr(published_docs, doc(cfg(test)))]
mod test {
#![allow(clippy::type_complexity, clippy::excessive_precision)]
use super::*;
use crate::assert_eq_approx;
const EPSILON: real = 1e-6;
fn is_matrix_equal_approx(a: &Projection, b: &RMat4) -> bool {
a.to_glam().abs_diff_eq(*b, EPSILON)
}
#[test]
fn test_diagonals() {
const DIAGONALS: [[real; 4]; 10] = [
[1.0, 1.0, 1.0, 1.0],
[2.0, 1.0, 2.0, 1.0],
[3.0, 2.0, 1.0, 1.0],
[-1.0, -1.0, 1.0, 1.0],
[0.0, 0.0, 0.0, 0.0],
[-2.0, -3.0, -4.0, -5.0],
[0.0, 5.0, -10.0, 50.0],
[-1.0, 0.0, 1.0, 100.0],
[-15.0, -22.0, 0.0, 11.0],
[-1.0, 3.0, 1.0, 0.0],
];
for [x, y, z, w] in DIAGONALS {
let proj = Projection::from_diagonal(x, y, z, w);
assert_eq_approx!(
proj,
RMat4::from_cols_array(&[
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,
]),
fn = is_matrix_equal_approx,
);
let det = x * y * z * w;
assert_eq_approx!(proj.determinant(), det);
if det.abs() > 1e-6 {
assert_eq_approx!(
proj.inverse(),
RMat4::from_cols_array_2d(&[
[1.0 / x, 0.0, 0.0, 0.0],
[0.0, 1.0 / y, 0.0, 0.0],
[0.0, 0.0, 1.0 / z, 0.0],
[0.0, 0.0, 0.0, 1.0 / w],
]),
fn = is_matrix_equal_approx,
);
}
}
}
#[test]
fn test_orthogonal() {
const TEST_DATA: [([real; 6], [[real; 4]; 4]); 6] = [
(
[-1.0, 1.0, -1.0, 1.0, -1.0, 1.0],
[
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, -1.0, 0.0],
[0.0, 0.0, 0.0, 1.0],
],
),
(
[0.0, 1.0, 0.0, 1.0, 0.0, 1.0],
[
[2.0, 0.0, 0.0, 0.0],
[0.0, 2.0, 0.0, 0.0],
[0.0, 0.0, -2.0, 0.0],
[-1.0, -1.0, -1.0, 1.0],
],
),
(
[-1.0, 1.0, -1.0, 1.0, 0.0, 1.0],
[
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, -2.0, 0.0],
[0.0, 0.0, -1.0, 1.0],
],
),
(
[-10.0, 10.0, -10.0, 10.0, 0.0, 100.0],
[
[0.1, 0.0, 0.0, 0.0],
[0.0, 0.1, 0.0, 0.0],
[0.0, 0.0, -0.02, 0.0],
[0.0, 0.0, -1.0, 1.0],
],
),
(
[-1.0, 1.0, -1.0, 1.0, 1.0, -1.0],
[
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 0.0, 1.0],
],
),
(
[10.0, -10.0, 10.0, -10.0, -10.0, 10.0],
[
[-0.1, 0.0, 0.0, 0.0],
[0.0, -0.1, 0.0, 0.0],
[0.0, 0.0, -0.1, 0.0],
[0.0, 0.0, 0.0, 1.0],
],
),
];
for ([left, right, bottom, top, near, far], mat) in TEST_DATA {
assert_eq_approx!(
Projection::create_orthogonal(left, right, bottom, top, near, far),
RMat4::from_cols_array_2d(&mat),
fn = is_matrix_equal_approx,
"orthogonal: left={left} right={right} bottom={bottom} top={top} near={near} far={far}",
);
}
}
#[test]
fn test_orthogonal_aspect() {
const TEST_DATA: [((real, real, real, real, bool), [[real; 4]; 4]); 6] = [
(
(2.0, 1.0, 0.0, 1.0, false),
[
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, -2.0, 0.0],
[0.0, 0.0, -1.0, 1.0],
],
),
(
(2.0, 1.0, 0.0, 1.0, true),
[
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, -2.0, 0.0],
[0.0, 0.0, -1.0, 1.0],
],
),
(
(1.0, 2.0, 0.0, 100.0, false),
[
[1.0, 0.0, 0.0, 0.0],
[0.0, 2.0, 0.0, 0.0],
[0.0, 0.0, -0.02, 0.0],
[0.0, 0.0, -1.0, 1.0],
],
),
(
(1.0, 2.0, 0.0, 100.0, true),
[
[2.0, 0.0, 0.0, 0.0],
[0.0, 4.0, 0.0, 0.0],
[0.0, 0.0, -0.02, 0.0],
[0.0, 0.0, -1.0, 1.0],
],
),
(
(64.0, 9.0 / 16.0, 0.0, 100.0, false),
[
[(1.0 / 32.0) * (16.0 / 9.0), 0.0, 0.0, 0.0],
[0.0, 1.0 / 32.0, 0.0, 0.0],
[0.0, 0.0, -0.02, 0.0],
[0.0, 0.0, -1.0, 1.0],
],
),
(
(64.0, 9.0 / 16.0, 0.0, 100.0, true),
[
[1.0 / 32.0, 0.0, 0.0, 0.0],
[0.0, (1.0 / 32.0) * (9.0 / 16.0), 0.0, 0.0],
[0.0, 0.0, -0.02, 0.0],
[0.0, 0.0, -1.0, 1.0],
],
),
];
for ((size, aspect, near, far, flip_fov), mat) in TEST_DATA {
assert_eq_approx!(
Projection::create_orthogonal_aspect(size, aspect, near, far, flip_fov),
RMat4::from_cols_array_2d(&mat),
fn = is_matrix_equal_approx,
"orthogonal aspect: size={size} aspect={aspect} near={near} far={far} flip_fov={flip_fov}"
);
}
}
#[test]
fn test_perspective() {
const TEST_DATA: [((real, real, real, real, bool), [[real; 4]; 4]); 5] = [
(
(90.0, 1.0, 1.0, 2.0, false),
[
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, -3.0, -1.0],
[0.0, 0.0, -4.0, 0.0],
],
),
(
(90.0, 1.0, 1.0, 2.0, true),
[
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, -3.0, -1.0],
[0.0, 0.0, -4.0, 0.0],
],
),
(
(45.0, 1.0, 0.05, 100.0, false),
[
[2.414213562373095, 0.0, 0.0, 0.0],
[0.0, 2.414213562373095, 0.0, 0.0],
[0.0, 0.0, -1.001000500250125, -1.0],
[0.0, 0.0, -0.10005002501250625, 0.0],
],
),
(
(90.0, 9.0 / 16.0, 1.0, 2.0, false),
[
[16.0 / 9.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, -3.0, -1.0],
[0.0, 0.0, -4.0, 0.0],
],
),
(
(90.0, 9.0 / 16.0, 1.0, 2.0, true),
[
[1.0, 0.0, 0.0, 0.0],
[0.0, 9.0 / 16.0, 0.0, 0.0],
[0.0, 0.0, -3.0, -1.0],
[0.0, 0.0, -4.0, 0.0],
],
),
];
for ((fov_y, aspect, near, far, flip_fov), mat) in TEST_DATA {
assert_eq_approx!(
Projection::create_perspective(fov_y, aspect, near, far, flip_fov),
RMat4::from_cols_array_2d(&mat),
fn = is_matrix_equal_approx,
"perspective: fov_y={fov_y} aspect={aspect} near={near} far={far} flip_fov={flip_fov}"
);
}
}
#[test]
fn test_frustum() {
const TEST_DATA: [([real; 6], [[real; 4]; 4]); 3] = [
(
[-1.0, 1.0, -1.0, 1.0, 1.0, 2.0],
[
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, -3.0, -1.0],
[0.0, 0.0, -4.0, 0.0],
],
),
(
[0.0, 1.0, 0.0, 1.0, 1.0, 2.0],
[
[2.0, 0.0, 0.0, 0.0],
[0.0, 2.0, 0.0, 0.0],
[1.0, 1.0, -3.0, -1.0],
[0.0, 0.0, -4.0, 0.0],
],
),
(
[-0.1, 0.1, -0.025, 0.025, 0.05, 100.0],
[
[0.5, 0.0, 0.0, 0.0],
[0.0, 2.0, 0.0, 0.0],
[0.0, 0.0, -1.001000500250125, -1.0],
[0.0, 0.0, -0.10005002501250625, 0.0],
],
),
];
for ([left, right, bottom, top, near, far], mat) in TEST_DATA {
assert_eq_approx!(
Projection::create_frustum(left, right, bottom, top, near, far),
RMat4::from_cols_array_2d(&mat),
fn = is_matrix_equal_approx,
"frustum: left={left} right={right} bottom={bottom} top={top} near={near} far={far}"
);
}
}
#[test]
fn test_frustum_aspect() {
const TEST_DATA: [((real, real, Vector2, real, real, bool), [[real; 4]; 4]); 4] = [
(
(2.0, 1.0, Vector2::ZERO, 1.0, 2.0, false),
[
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, -3.0, -1.0],
[0.0, 0.0, -4.0, 0.0],
],
),
(
(2.0, 1.0, Vector2::ZERO, 1.0, 2.0, true),
[
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, -3.0, -1.0],
[0.0, 0.0, -4.0, 0.0],
],
),
(
(1.0, 1.0, Vector2::new(0.5, 0.5), 1.0, 2.0, false),
[
[2.0, 0.0, 0.0, 0.0],
[0.0, 2.0, 0.0, 0.0],
[1.0, 1.0, -3.0, -1.0],
[0.0, 0.0, -4.0, 0.0],
],
),
(
(0.05, 4.0, Vector2::ZERO, 0.05, 100.0, false),
[
[0.5, 0.0, 0.0, 0.0],
[0.0, 2.0, 0.0, 0.0],
[0.0, 0.0, -1.001000500250125, -1.0],
[0.0, 0.0, -0.10005002501250625, 0.0],
],
),
];
for ((size, aspect, offset, near, far, flip_fov), mat) in TEST_DATA {
assert_eq_approx!(
Projection::create_frustum_aspect(size, aspect, offset, near, far, flip_fov),
RMat4::from_cols_array_2d(&mat),
fn = is_matrix_equal_approx,
"frustum aspect: size={size} aspect={aspect} offset=({0} {1}) near={near} far={far} flip_fov={flip_fov}",
offset.x,
offset.y,
);
}
}
#[test]
fn test_is_orthogonal() {
fn f(v: isize) -> real {
(v as real) * 0.5 - 0.5
}
for left_i in 0..20 {
let left = f(left_i);
for right in (left_i + 1..=20).map(f) {
for bottom_i in 0..20 {
let bottom = f(bottom_i);
for top in (bottom_i + 1..=20).map(f) {
for near_i in 0..20 {
let near = f(near_i);
for far in (near_i + 1..=20).map(f) {
assert!(
Projection::create_orthogonal(
left, right, bottom, top, near, far
)
.is_orthogonal(),
"projection should be orthogonal: left={left} right={right} bottom={bottom} top={top} near={near} far={far}",
);
}
}
}
}
}
}
for fov in (0..18).map(|v| (v as real) * 10.0) {
for aspect_x in 1..=10 {
for aspect_y in 1..=10 {
let aspect = (aspect_x as real) / (aspect_y as real);
for near_i in 1..10 {
let near = near_i as real;
for far in (near_i + 1..=20).map(|v| v as real) {
assert!(
!Projection::create_perspective(fov, aspect, near, far, false)
.is_orthogonal(),
"projection should be perspective: fov={fov} aspect={aspect} near={near} far={far}",
);
}
}
}
}
}
for left_i in 0..20 {
let left = f(left_i);
for right in (left_i + 1..=20).map(f) {
for bottom_i in 0..20 {
let bottom = f(bottom_i);
for top in (bottom_i + 1..=20).map(f) {
for near_i in 0..20 {
let near = (near_i as real) * 0.5;
for far in (near_i + 1..=20).map(|v| (v as real) * 0.5) {
assert!(
!Projection::create_frustum(
left, right, bottom, top, near, far
)
.is_orthogonal(),
"projection should be perspective: left={left} right={right} bottom={bottom} top={top} near={near} far={far}",
);
}
}
}
}
}
}
for size in (1..=10).map(|v| v as real) {
for aspect_x in 1..=10 {
for aspect_y in 1..=10 {
let aspect = (aspect_x as real) / (aspect_y as real);
for near_i in 1..10 {
let near = near_i as real;
for far in (near_i + 1..=20).map(|v| v as real) {
assert!(
Projection::create_orthogonal_aspect(
size, aspect, near, far, false
)
.is_orthogonal(),
"projection should be orthogonal: (size={size} aspect={aspect} near={near} far={far}",
);
assert!(
!Projection::create_frustum_aspect(
size,
aspect,
Vector2::ZERO,
near,
far,
false
)
.is_orthogonal(),
"projection should be perspective: (size={size} aspect={aspect} near={near} far={far}",
);
}
}
}
}
}
}
#[cfg(feature = "serde")] #[cfg_attr(published_docs, doc(cfg(feature = "serde")))]
#[test]
fn serde_roundtrip() {
let projection = Projection::IDENTITY;
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}]}";
crate::builtin::test_utils::roundtrip(&projection, expected_json);
}
}