diff --git a/src/camera.rs b/src/camera.rs index 6eb9425..dc15680 100644 --- a/src/camera.rs +++ b/src/camera.rs @@ -369,7 +369,7 @@ impl Camera { } else if focal > 0.0 && z_far > focal { z_far = focal - 0.001; } - self.projection = cgmath::planar( + self.projection = planar( field_of_view_y, self.viewport.aspect(), height, diff --git a/src/prelude/math.rs b/src/prelude/math.rs index 13e6197..b925331 100644 --- a/src/prelude/math.rs +++ b/src/prelude/math.rs @@ -3,8 +3,8 @@ //! pub use cgmath::{ - dot, frustum, ortho, perspective, planar, vec2, vec3, vec4, Deg, Matrix2, Matrix3, Matrix4, - Point2, Point3, Quaternion, Rad, Vector2, Vector3, Vector4, + dot, frustum, ortho, perspective, vec2, vec3, vec4, Deg, Matrix2, Matrix3, Matrix4, Point2, + Point3, Quaternion, Rad, Vector2, Vector3, Vector4, }; pub use cgmath::{ Angle, EuclideanSpace, InnerSpace, Matrix, MetricSpace, One, Rotation, Rotation2, Rotation3, @@ -78,3 +78,109 @@ pub fn rotation_matrix_from_dir_to_dir(source_dir: Vec3, target_dir: Vec3) -> Ma source_dir, target_dir, ))) } + +/// Create a planar projection matrix, which can be either perspective or orthographic. +/// +/// The projection frustum is always `height` units high at the origin along the view direction, +/// making the focal point located at `(0.0, 0.0, cot(fovy / 2.0)) * height / 2.0`. Unlike +/// a standard perspective projection, this allows `fovy` to be zero or negative. +pub fn planar>>( + fovy: A, + aspect: S, + height: S, + near: S, + far: S, +) -> Matrix4 { + PlanarFov { + fovy: fovy.into(), + aspect, + height, + near, + far, + } + .into() +} + +/// A planar projection based on a vertical field-of-view angle. +#[derive(Copy, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +struct PlanarFov { + pub fovy: Rad, + pub aspect: S, + pub height: S, + pub near: S, + pub far: S, +} + +impl From> for Matrix4 { + fn from(persp: PlanarFov) -> Matrix4 { + assert!( + persp.fovy > -Rad::turn_div_2(), + "The vertical field of view cannot be less than a negative half turn, found: {:?}", + persp.fovy + ); + assert!( + persp.fovy < Rad::turn_div_2(), + "The vertical field of view cannot be greater than a half turn, found: {:?}", + persp.fovy + ); + assert! { + persp.height >= S::zero(), + "The projection plane height cannot be negative, found: {:?}", + persp.height + } + + let two: S = cgmath::num_traits::cast(2).unwrap(); + let inv_f = Rad::tan(persp.fovy / two) * two / persp.height; + + let focal_point = -inv_f.recip(); + + assert!( + cgmath::abs_diff_ne!(persp.aspect.abs(), S::zero()), + "The absolute aspect ratio cannot be zero, found: {:?}", + persp.aspect.abs() + ); + assert!( + cgmath::abs_diff_ne!(persp.far, persp.near), + "The far plane and near plane are too close, found: far: {:?}, near: {:?}", + persp.far, + persp.near + ); + assert!( + focal_point < S::min(persp.far, persp.near) || focal_point > S::max(persp.far, persp.near), + "The focal point cannot be between the far and near planes, found: focal: {:?}, far: {:?}, near: {:?}", + focal_point, + persp.far, + persp.near, + ); + + let c0r0 = two / (persp.aspect * persp.height); + let c0r1 = S::zero(); + let c0r2 = S::zero(); + let c0r3 = S::zero(); + + let c1r0 = S::zero(); + let c1r1 = two / persp.height; + let c1r2 = S::zero(); + let c1r3 = S::zero(); + + let c2r0 = S::zero(); + let c2r1 = S::zero(); + let c2r2 = ((persp.far + persp.near) * inv_f + two) / (persp.near - persp.far); + let c2r3 = -inv_f; + + let c3r0 = S::zero(); + let c3r1 = S::zero(); + let c3r2 = (two * persp.far * persp.near * inv_f + (persp.far + persp.near)) + / (persp.near - persp.far); + let c3r3 = S::one(); + + #[cfg_attr(rustfmt, rustfmt_skip)] + Matrix4::new( + c0r0, c0r1, c0r2, c0r3, + c1r0, c1r1, c1r2, c1r3, + c2r0, c2r1, c2r2, c2r3, + c3r0, c3r1, c3r2, c3r3, + ) + } +}