From 0634a0a74eb4bf95e384f827361f6a100116b63c Mon Sep 17 00:00:00 2001 From: Philippe Renon Date: Sun, 11 Oct 2020 10:35:26 +0200 Subject: [PATCH] wip relates to https://github.com/dimforge/nalgebra/issues/769 --- src/base/cg.rs | 31 +++++- src/geometry/mod.rs | 2 + src/geometry/perspective.rs | 200 +++++++++++++++++++++++++++--------- 3 files changed, 184 insertions(+), 49 deletions(-) diff --git a/src/base/cg.rs b/src/base/cg.rs index ba319c7e..dc3dba48 100644 --- a/src/base/cg.rs +++ b/src/base/cg.rs @@ -15,8 +15,8 @@ use crate::base::{ Vector3, VectorN, }; use crate::geometry::{ - Isometry, IsometryMatrix3, Orthographic3, Perspective3, Point, Point2, Point3, Rotation2, - Rotation3, + Isometry, IsometryMatrix3, OpenGL, Orthographic3, Perspective3, Perspective3OpenGL, Point, + Point2, Point3, Rotation2, Rotation3, }; use simba::scalar::{ClosedAdd, ClosedMul, RealField}; @@ -171,7 +171,32 @@ impl Matrix4 { /// Creates a new homogeneous matrix for a perspective projection. #[inline] pub fn new_perspective(aspect: N, fovy: N, znear: N, zfar: N) -> Self { - Perspective3::new(aspect, fovy, znear, zfar).into_inner() + // Example of a breaking change appears here. The same will happen in user code. + // It is currently not possible to omit the S type parameter. + // Current code, if left unchanged, fails to compile with : + // multiple applicable items in scope + // multiple `new` found + // + // In theory, S should default to OpenGL and N should be infered from the new() parameters. + // + // Interestingly, if only the OpenGL specialization is provided then this code compiles.abomonation + // This means defaulting to OpenGL works. But adding an additional specialization seems to break defaulting. + //Perspective3::new(aspect, fovy, znear, zfar).into_inner() + // The following line should work but fails with : + // mismatched types + // expected type parameter `N`, found `f32` + // Seems that N is not infered anymore and defaults to f32 (as now specified in the Perspective3 struct declaration). + //Perspective3::::new(aspect, fovy, znear, zfar).into_inner() + // This works: + Perspective3::::new(aspect, fovy, znear, zfar).into_inner() + } + + /// Dummy functions to show the use of a Perspective3 alias. + #[inline] + pub fn new_perspective2(aspect: N, fovy: N, znear: N, zfar: N) -> Self { + // Using the alias works: no need to specify type parameters. + // If we go that route then Perspective3 would be renamed to something and the alias would be Perspective3. + Perspective3OpenGL::new(aspect, fovy, znear, zfar).into_inner() } /// Creates an isometry that corresponds to the local frame of an observer standing at the diff --git a/src/geometry/mod.rs b/src/geometry/mod.rs index 5701aa8f..a339e141 100644 --- a/src/geometry/mod.rs +++ b/src/geometry/mod.rs @@ -114,3 +114,5 @@ pub use self::reflection::*; pub use self::orthographic::Orthographic3; pub use self::perspective::Perspective3; +pub use self::perspective::Perspective3OpenGL; +pub use self::perspective::{OpenGL, Vulkan, VulkanX}; diff --git a/src/geometry/perspective.rs b/src/geometry/perspective.rs index c11b7632..d9cd5fee 100644 --- a/src/geometry/perspective.rs +++ b/src/geometry/perspective.rs @@ -6,6 +6,7 @@ use rand::Rng; #[cfg(feature = "serde-serialize")] use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::fmt; +use std::marker::PhantomData; use std::mem; use simba::scalar::RealField; @@ -17,27 +18,69 @@ use crate::base::{Matrix4, Scalar, Vector, Vector3}; use crate::geometry::{Point3, Projective3}; +/// Normalized device coordinates systems +pub trait System {} + +/// OpenGL +/// Note that we will probably want to go with more generic names that encode the handedness and depth range +/// Please consider all names as placeholders +#[derive(Default)] +pub struct OpenGL {} + +/// OpenGL is a System +impl System for OpenGL {} + +/// Vulkan is also a System +#[derive(Default)] +pub struct Vulkan {} + +/// Vulkan is also a System +impl System for Vulkan {} + +/// Note that it is possible to alias systems (OpenGL and Vulkan would be aliases of generic systems) +//pub type OpenGL = RHS_NO; +//pub type OpenGL = LHS_ZO; +pub type VulkanX = Vulkan; + /// A 3D perspective projection stored as a homogeneous 4x4 matrix. -pub struct Perspective3 { +/// Perspective3 is now generic over System +/// Note that : +/// - S was put in first place to avoid having to specify N when specifying S +/// - S defaults to OpenGL and rust requires N to have a default too (only trailing type parameters can have defaults) +/// But unfortunately default type parameters have some limitations and don't fully work as one would expect. +/// See cg.rs for the issue at hand. +/// And [RFC 0213-defaulted-type-params](@ https://github.com/rust-lang/rfcs/blob/master/text/0213-defaulted-type-params.md) for more details on the issue. +pub struct Perspective3 { matrix: Matrix4, + /// See [PhantomData](https://doc.rust-lang.org/std/marker/struct.PhantomData.html#unused-type-parameters) + /// TODO add above comment to all other PhantomData uses. + phantom: PhantomData, } -impl Copy for Perspective3 {} +/// It is possible to avoid the breaking changes by renaming Perspective3 to, lets say, Perspective3S. +/// And then alias Perspective3 to Perspective3S and, voilĂ , PerspectiveS is still a thing and no code breaks. +/// But it is ugly and if you want to use another NDC you end up with the Perspective3S. +//pub type Perspective3 = Perspective3S; -impl Clone for Perspective3 { +// Dummy alias to demonstrate that this approach works (see cg.rs) +pub type Perspective3OpenGL = Perspective3; + +impl Copy for Perspective3 {} + +impl Clone for Perspective3 { #[inline] fn clone(&self) -> Self { Self::from_matrix_unchecked(self.matrix.clone()) } } -impl fmt::Debug for Perspective3 { +impl fmt::Debug for Perspective3 { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { self.matrix.fmt(f) } } -impl PartialEq for Perspective3 { +impl PartialEq for Perspective3 { #[inline] fn eq(&self, right: &Self) -> bool { self.matrix == right.matrix @@ -45,7 +88,7 @@ impl PartialEq for Perspective3 { } #[cfg(feature = "serde-serialize")] -impl Serialize for Perspective3 { +impl Serialize for Perspective3 { fn serialize(&self, serializer: S) -> Result where S: Serializer, @@ -55,7 +98,7 @@ impl Serialize for Perspective3 { } #[cfg(feature = "serde-serialize")] -impl<'a, N: RealField + Deserialize<'a>> Deserialize<'a> for Perspective3 { +impl<'a, N: RealField + Deserialize<'a>> Deserialize<'a> for Perspective3 { fn deserialize(deserializer: Des) -> Result where Des: Deserializer<'a>, @@ -66,38 +109,17 @@ impl<'a, N: RealField + Deserialize<'a>> Deserialize<'a> for Perspective3 { } } -impl Perspective3 { - /// Creates a new perspective matrix from the aspect ratio, y field of view, and near/far planes. - pub fn new(aspect: N, fovy: N, znear: N, zfar: N) -> Self { - assert!( - !relative_eq!(zfar - znear, N::zero()), - "The near-plane and far-plane must not be superimposed." - ); - assert!( - !relative_eq!(aspect, N::zero()), - "The aspect ratio must not be zero." - ); - - let matrix = Matrix4::identity(); - let mut res = Self::from_matrix_unchecked(matrix); - - res.set_fovy(fovy); - res.set_aspect(aspect); - res.set_znear_and_zfar(znear, zfar); - - res.matrix[(3, 3)] = N::zero(); - res.matrix[(3, 2)] = -N::one(); - - res - } - +impl Perspective3 { /// Wraps the given matrix to interpret it as a 3D perspective matrix. /// /// It is not checked whether or not the given matrix actually represents a perspective /// projection. #[inline] pub fn from_matrix_unchecked(matrix: Matrix4) -> Self { - Self { matrix } + Self { + matrix, + phantom: PhantomData, + } } /// Retrieves the inverse of the underlying homogeneous matrix. @@ -172,18 +194,13 @@ impl Perspective3 { /// Gets the near plane offset of the view frustum. #[inline] pub fn znear(&self) -> N { - let ratio = (-self.matrix[(2, 2)] + N::one()) / (-self.matrix[(2, 2)] - N::one()); - - self.matrix[(2, 3)] / (ratio * crate::convert(2.0)) - - self.matrix[(2, 3)] / crate::convert(2.0) + self.matrix[(2, 3)] / self.matrix[(2, 2)] } /// Gets the far plane offset of the view frustum. #[inline] pub fn zfar(&self) -> N { - let ratio = (-self.matrix[(2, 2)] + N::one()) / (-self.matrix[(2, 2)] - N::one()); - - (self.matrix[(2, 3)] - ratio * self.matrix[(2, 3)]) / crate::convert(2.0) + self.matrix[(2, 3)] / (N::one() + self.matrix[(2, 2)]) } // TODO: add a method to retrieve znear and zfar simultaneously? @@ -237,7 +254,36 @@ impl Perspective3 { ); self.matrix[(0, 0)] = self.matrix[(1, 1)] / aspect; } +} +// OpenGL specialization +// For now not all required functions are specialized for sake of illustration. +// Specializating the other functions should be trivial. + +impl Perspective3 { + /// Implementation note: new() must be specialized because it calls other specialized functions. + pub fn new(aspect: N, fovy: N, znear: N, zfar: N) -> Self { + assert!( + !relative_eq!(zfar - znear, N::zero()), + "The near-plane and far-plane must not be superimposed." + ); + assert!( + !relative_eq!(aspect, N::zero()), + "The aspect ratio must not be zero." + ); + + let matrix = Matrix4::identity(); + let mut res = Self::from_matrix_unchecked(matrix); + + res.set_fovy(fovy); + res.set_aspect(aspect); + res.set_znear_and_zfar(znear, zfar); + + res.matrix[(3, 3)] = N::zero(); + res.matrix[(3, 2)] = -N::one(); + + res + } /// Updates this perspective with a new y field of view of the view frustum. #[inline] pub fn set_fovy(&mut self, fovy: N) { @@ -247,6 +293,7 @@ impl Perspective3 { } /// Updates this perspective matrix with a new near plane offset of the view frustum. + /// Implementation note: set_znear() must be specialized because it calls other specialized functions. #[inline] pub fn set_znear(&mut self, znear: N) { let zfar = self.zfar(); @@ -254,6 +301,7 @@ impl Perspective3 { } /// Updates this perspective matrix with a new far plane offset of the view frustum. + /// Implementation note: set_zfar() must be specialized because it calls other specialized functions. #[inline] pub fn set_zfar(&mut self, zfar: N) { let znear = self.znear(); @@ -268,21 +316,81 @@ impl Perspective3 { } } -impl Distribution> for Standard +// Vulkan specialization + +impl Perspective3 { + /// Implementation note: new() must be specialized because it calls other specialized functions. + pub fn new(aspect: N, fovy: N, znear: N, zfar: N) -> Self { + assert!( + !relative_eq!(zfar - znear, N::zero()), + "The near-plane and far-plane must not be superimposed." + ); + assert!( + !relative_eq!(aspect, N::zero()), + "The aspect ratio must not be zero." + ); + + let matrix = Matrix4::identity(); + let mut res = Self::from_matrix_unchecked(matrix); + + res.set_fovy(fovy); + res.set_aspect(aspect); + res.set_znear_and_zfar(znear, zfar); + + res.matrix[(3, 3)] = N::zero(); + res.matrix[(3, 2)] = -N::one(); + + res + } + + /// Updates this perspective with a new y field of view of the view frustum. + #[inline] + pub fn set_fovy(&mut self, fovy: N) { + let old_m22 = self.matrix[(1, 1)]; + let f = N::one() / (fovy / crate::convert(2.0)).tan(); + self.matrix[(1, 1)] = -f; + self.matrix[(0, 0)] *= f / old_m22; + } + + /// Updates this perspective matrix with a new near plane offset of the view frustum. + /// Implementation note: set_znear() must be specialized because it calls other specialized functions. + #[inline] + pub fn set_znear(&mut self, znear: N) { + let zfar = self.zfar(); + self.set_znear_and_zfar(znear, zfar); + } + + /// Updates this perspective matrix with a new far plane offset of the view frustum. + /// Implementation note: set_zfar() must be specialized because it calls other specialized functions. + #[inline] + pub fn set_zfar(&mut self, zfar: N) { + let znear = self.znear(); + self.set_znear_and_zfar(znear, zfar); + } + + /// Updates this perspective matrix with new near and far plane offsets of the view frustum. + #[inline] + pub fn set_znear_and_zfar(&mut self, znear: N, zfar: N) { + self.matrix[(2, 2)] = -zfar / (zfar - znear); + self.matrix[(2, 3)] = -(zfar * znear) / (zfar - znear); + } +} + +impl Distribution> for Standard where Standard: Distribution, { - fn sample<'a, R: Rng + ?Sized>(&self, r: &'a mut R) -> Perspective3 { + fn sample<'a, R: Rng + ?Sized>(&self, r: &'a mut R) -> Perspective3 { let znear = r.gen(); let zfar = helper::reject_rand(r, |&x: &N| !(x - znear).is_zero()); let aspect = helper::reject_rand(r, |&x: &N| !x.is_zero()); - Perspective3::new(aspect, r.gen(), znear, zfar) + Perspective3::::new(aspect, r.gen(), znear, zfar) } } #[cfg(feature = "arbitrary")] -impl Arbitrary for Perspective3 { +impl Arbitrary for Perspective3 { fn arbitrary(g: &mut G) -> Self { let znear = Arbitrary::arbitrary(g); let zfar = helper::reject(g, |&x: &N| !(x - znear).is_zero()); @@ -292,9 +400,9 @@ impl Arbitrary for Perspective3 { } } -impl From> for Matrix4 { +impl From> for Matrix4 { #[inline] - fn from(pers: Perspective3) -> Self { + fn from(pers: Perspective3) -> Self { pers.into_inner() } }