diff --git a/src/camera.rs b/src/camera.rs index 64c9b3b..cb59647 100644 --- a/src/camera.rs +++ b/src/camera.rs @@ -223,6 +223,11 @@ pub enum ProjectionType { /// The field of view angle in the vertical direction. field_of_view_y: Radians, }, + /// General planar projection + Planar { + /// The field of view angle in the vertical direction. + field_of_view_y: Radians, + }, } /// @@ -279,6 +284,24 @@ impl Camera { camera } + /// + /// New camera which projects the world with a general planar projection. + /// + pub fn new_planar( + viewport: Viewport, + position: Vec3, + target: Vec3, + up: Vec3, + field_of_view_y: impl Into, + z_near: f32, + z_far: f32, + ) -> Self { + let mut camera = Camera::new(viewport); + camera.set_view(position, target, up); + camera.set_planar_projection(field_of_view_y, z_near, z_far); + camera + } + /// /// Specify the camera to use perspective projection with the given field of view in the y-direction and near and far plane. /// @@ -320,6 +343,40 @@ impl Camera { ); } + /// + /// Specify the camera to use planar projection with the given field of view in the y-direction and near and far plane. + /// This can be either a planar or perspective projection depending on the field of view provided, which is permitted to be zero or negative. + /// + pub fn set_planar_projection( + &mut self, + field_of_view_y: impl Into, + mut z_near: f32, + mut z_far: f32, + ) { + self.z_near = z_near; + self.z_far = z_far; + let field_of_view_y = field_of_view_y.into(); + self.projection_type = ProjectionType::Planar { field_of_view_y }; + let depth = self.position.distance(self.target); + let height = 2.0 * depth; + let focal = -Rad::cot(field_of_view_y / 2.0) * depth; + z_near -= depth; + z_far -= depth; + // Required to ensure near/far plane does not cross focal point when at close zoom levels + if focal < 0.0 && z_near < focal { + z_near = focal + 0.001; + } else if focal > 0.0 && z_far > focal { + z_far = focal - 0.001; + } + self.projection = planar( + field_of_view_y, + self.viewport.aspect(), + height, + z_near, + z_far, + ) * Mat4::from_translation(vec3(0.0, 0.0, depth)); + } + /// /// Set the current viewport. /// Returns whether or not the viewport actually changed. @@ -334,6 +391,9 @@ impl Camera { ProjectionType::Perspective { field_of_view_y } => { self.set_perspective_projection(field_of_view_y, self.z_near, self.z_far); } + ProjectionType::Planar { field_of_view_y } => { + self.set_planar_projection(field_of_view_y, self.z_near, self.z_far); + } } true } else { @@ -354,9 +414,15 @@ impl Camera { Point3::from_vec(self.target), self.up, ); - if let ProjectionType::Orthographic { height } = self.projection_type { - self.set_orthographic_projection(height, self.z_near, self.z_far); - } + match self.projection_type { + ProjectionType::Orthographic { height } => { + self.set_orthographic_projection(height, self.z_near, self.z_far) + } + ProjectionType::Planar { field_of_view_y } => { + self.set_planar_projection(field_of_view_y, self.z_near, self.z_far) + } + _ => {} + }; } /// Returns the [Frustum] for this camera. @@ -369,7 +435,7 @@ impl Camera { /// pub fn position_at_pixel(&self, pixel: impl Into) -> Vec3 { match self.projection_type() { - ProjectionType::Orthographic { .. } => { + ProjectionType::Orthographic { .. } | ProjectionType::Planar { .. } => { let coords = self.uv_coordinates_at_pixel(pixel); self.position_at_uv_coordinates(coords) } @@ -382,10 +448,10 @@ impl Camera { /// pub fn position_at_uv_coordinates(&self, coords: impl Into) -> Vec3 { match self.projection_type() { - ProjectionType::Orthographic { .. } => { + ProjectionType::Orthographic { .. } | ProjectionType::Planar { .. } => { let coords = coords.into(); - let screen_pos = vec4(2. * coords.u - 1., 2. * coords.v - 1.0, -1.0, 1.); - (self.screen2ray() * screen_pos).truncate() + let screen_pos = Point3::new(2. * coords.u - 1., 2. * coords.v - 1.0, -1.0); + self.screen2ray().transform_point(screen_pos).to_vec() } ProjectionType::Perspective { .. } => self.position, } @@ -397,7 +463,7 @@ impl Camera { pub fn view_direction_at_pixel(&self, pixel: impl Into) -> Vec3 { match self.projection_type() { ProjectionType::Orthographic { .. } => self.view_direction(), - ProjectionType::Perspective { .. } => { + ProjectionType::Perspective { .. } | ProjectionType::Planar { .. } => { let coords = self.uv_coordinates_at_pixel(pixel); self.view_direction_at_uv_coordinates(coords) } @@ -415,6 +481,14 @@ impl Camera { let screen_pos = vec4(2. * coords.u - 1., 2. * coords.v - 1.0, 0., 1.); (self.screen2ray() * screen_pos).truncate().normalize() } + ProjectionType::Planar { .. } => { + let coords = coords.into(); + let start_pos = Point3::new(2. * coords.u - 1., 2. * coords.v - 1.0, -0.5); + let end_pos = Point3::new(2. * coords.u - 1., 2. * coords.v - 1.0, 0.5); + (self.screen2ray().transform_point(end_pos) + - self.screen2ray().transform_point(start_pos)) + .normalize() + } } } diff --git a/src/prelude/math.rs b/src/prelude/math.rs index 7da528c..24c0e68 100644 --- a/src/prelude/math.rs +++ b/src/prelude/math.rs @@ -78,3 +78,108 @@ 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)] +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, + ) + } +}