relates to https://github.com/dimforge/nalgebra/issues/769
This commit is contained in:
Philippe Renon 2020-10-11 10:35:26 +02:00
parent ffef8e7f54
commit 0634a0a74e
3 changed files with 184 additions and 49 deletions

View File

@ -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<N: RealField> Matrix4<N> {
/// 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::<OpenGL>::new(aspect, fovy, znear, zfar).into_inner()
// This works:
Perspective3::<OpenGL, N>::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

View File

@ -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};

View File

@ -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<N: Scalar> {
/// 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<S: System = OpenGL, N: Scalar = f32> {
matrix: Matrix4<N>,
/// 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<S>,
}
impl<N: RealField> Copy for Perspective3<N> {}
/// It is possible to avoid the breaking changes by renaming Perspective3 to, lets say, Perspective3S.
/// And then alias Perspective3<N> to Perspective3S<OpenGL, N> and, voilà, PerspectiveS<N> 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<Vulkan>.
//pub type Perspective3<N> = Perspective3S<OpenGL, N>;
impl<N: RealField> Clone for Perspective3<N> {
// Dummy alias to demonstrate that this approach works (see cg.rs)
pub type Perspective3OpenGL<N> = Perspective3<OpenGL, N>;
impl<S: System, N: RealField> Copy for Perspective3<S, N> {}
impl<S: System, N: RealField> Clone for Perspective3<S, N> {
#[inline]
fn clone(&self) -> Self {
Self::from_matrix_unchecked(self.matrix.clone())
}
}
impl<N: RealField> fmt::Debug for Perspective3<N> {
impl<S: System, N: RealField> fmt::Debug for Perspective3<S, N> {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
self.matrix.fmt(f)
}
}
impl<N: RealField> PartialEq for Perspective3<N> {
impl<S: System, N: RealField> PartialEq for Perspective3<S, N> {
#[inline]
fn eq(&self, right: &Self) -> bool {
self.matrix == right.matrix
@ -45,7 +88,7 @@ impl<N: RealField> PartialEq for Perspective3<N> {
}
#[cfg(feature = "serde-serialize")]
impl<N: RealField + Serialize> Serialize for Perspective3<N> {
impl<N: RealField + Serialize> Serialize for Perspective3<S, N> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
@ -55,7 +98,7 @@ impl<N: RealField + Serialize> Serialize for Perspective3<N> {
}
#[cfg(feature = "serde-serialize")]
impl<'a, N: RealField + Deserialize<'a>> Deserialize<'a> for Perspective3<N> {
impl<'a, N: RealField + Deserialize<'a>> Deserialize<'a> for Perspective3<S, N> {
fn deserialize<Des>(deserializer: Des) -> Result<Self, Des::Error>
where
Des: Deserializer<'a>,
@ -66,38 +109,17 @@ impl<'a, N: RealField + Deserialize<'a>> Deserialize<'a> for Perspective3<N> {
}
}
impl<N: RealField> Perspective3<N> {
/// 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<S: System, N: RealField> Perspective3<S, N> {
/// 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<N>) -> Self {
Self { matrix }
Self {
matrix,
phantom: PhantomData,
}
}
/// Retrieves the inverse of the underlying homogeneous matrix.
@ -172,18 +194,13 @@ impl<N: RealField> Perspective3<N> {
/// 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<N: RealField> Perspective3<N> {
);
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<N: RealField> Perspective3<OpenGL, N> {
/// 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<N: RealField> Perspective3<N> {
}
/// 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<N: RealField> Perspective3<N> {
}
/// 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<N: RealField> Perspective3<N> {
}
}
impl<N: RealField> Distribution<Perspective3<N>> for Standard
// Vulkan specialization
impl<N: RealField> Perspective3<Vulkan, N> {
/// 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<N: RealField> Distribution<Perspective3<OpenGL, N>> for Standard
where
Standard: Distribution<N>,
{
fn sample<'a, R: Rng + ?Sized>(&self, r: &'a mut R) -> Perspective3<N> {
fn sample<'a, R: Rng + ?Sized>(&self, r: &'a mut R) -> Perspective3<OpenGL, N> {
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::<OpenGL, N>::new(aspect, r.gen(), znear, zfar)
}
}
#[cfg(feature = "arbitrary")]
impl<N: RealField + Arbitrary> Arbitrary for Perspective3<N> {
impl<S: System, N: RealField + Arbitrary> Arbitrary for Perspective3<S, N> {
fn arbitrary<G: Gen>(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<N: RealField + Arbitrary> Arbitrary for Perspective3<N> {
}
}
impl<N: RealField> From<Perspective3<N>> for Matrix4<N> {
impl<S: System, N: RealField> From<Perspective3<S, N>> for Matrix4<N> {
#[inline]
fn from(pers: Perspective3<N>) -> Self {
fn from(pers: Perspective3<S, N>) -> Self {
pers.into_inner()
}
}