use cgmath::prelude::*; use cgmath::{Matrix4, Point2, Point3, Vector2, Vector3, Vector4}; use crate::render::shaders::ShaderCamera; use crate::util::math::{Aabb2, Aabb3, Plane}; #[rustfmt::skip] pub const OPENGL_TO_WGPU_MATRIX: cgmath::Matrix4 = cgmath::Matrix4::new( 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5, 1.0, ); #[rustfmt::skip] pub const FLIP_Y: cgmath::Matrix4 = cgmath::Matrix4::new( 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, ); pub struct ViewProjection(Matrix4); impl ViewProjection { pub fn invert(&self) -> InvertedViewProjection { InvertedViewProjection(self.0.invert().expect("Unable to invert view projection")) } pub fn project(&self, vector: Vector4) -> Vector4 { self.0 * vector } pub fn to_model_view_projection(&self, projection: Matrix4) -> ModelViewProjection { ModelViewProjection(self.0 * projection) } pub fn downcast(&self) -> Matrix4 { self.0 .cast::() .expect("Unable to cast view projection to f32") } } pub struct InvertedViewProjection(Matrix4); impl InvertedViewProjection { pub fn project(&self, vector: Vector4) -> Vector4 { self.0 * vector } } pub struct ModelViewProjection(Matrix4); impl ModelViewProjection { pub fn downcast(&self) -> Matrix4 { self.0 .cast::() .expect("Unable to cast view projection to f32") } } #[derive(Debug, Clone)] pub struct Camera { pub position: cgmath::Point3, pub yaw: cgmath::Rad, pub pitch: cgmath::Rad, pub width: f64, pub height: f64, } impl Camera { pub fn new< V: Into>, Y: Into>, P: Into>, >( position: V, yaw: Y, pitch: P, width: u32, height: u32, ) -> Self { Self { position: position.into(), yaw: yaw.into(), pitch: pitch.into(), width: width as f64, height: height as f64, } } pub fn resize(&mut self, width: u32, height: u32) { self.width = width as f64; self.height = height as f64; } fn calc_matrix(&self) -> cgmath::Matrix4 { cgmath::Matrix4::look_to_rh( self.position, cgmath::Vector3::new(self.yaw.cos(), self.pitch.sin(), self.yaw.sin()).normalize(), cgmath::Vector3::unit_y(), ) } pub fn calc_view_proj(&self, perspective: &Perspective) -> ViewProjection { ViewProjection(FLIP_Y * perspective.calc_matrix() * self.calc_matrix()) } pub fn create_camera_uniform(&self, perspective: &Perspective) -> ShaderCamera { let view_proj = self.calc_view_proj(perspective); ShaderCamera::new( view_proj.downcast().into(), self.position.to_homogeneous().cast::().unwrap().into(), ) } /// A transform which can be used to transfrom between clip and window space. /// Adopted from [here](https://docs.microsoft.com/en-us/windows/win32/direct3d9/viewports-and-clipping#viewport-rectangle) (Direct3D). fn clip_to_window_transform(&self) -> Matrix4 { let min_depth = 0.0; let max_depth = 1.0; let x = 0.0; let y = 0.0; let ox = x + self.width / 2.0; let oy = y + self.height / 2.0; let oz = min_depth; let pz = max_depth - min_depth; Matrix4::from_cols( Vector4::new(self.width as f64 / 2.0, 0.0, 0.0, 0.0), Vector4::new(0.0, -self.height as f64 / 2.0, 0.0, 0.0), Vector4::new(0.0, 0.0, pz, 0.0), Vector4::new(ox, oy, oz, 1.0), ) } /// Transforms coordinates in clip space to window coordinates. /// /// Adopted from [here](https://docs.microsoft.com/en-us/windows/win32/dxtecharts/the-direct3d-transformation-pipeline) (Direct3D). fn clip_to_window(&self, clip: &Vector4) -> Vector4 { #[rustfmt::skip] let ndc = Vector4::new( clip.x / clip.w, clip.y / clip.w, clip.z / clip.w, 1.0 ); self.clip_to_window_transform() * ndc } /// Alternative implementation to `clip_to_window`. Transforms coordinates in clip space to /// window coordinates. /// /// Adopted from [here](https://www.khronos.org/registry/vulkan/specs/1.2-extensions/man/html/VkViewport.html) /// and [here](https://matthewwellings.com/blog/the-new-vulkan-coordinate-system/) (Vulkan). fn clip_to_window_vulkan(&self, clip: &Vector4) -> Vector3 { #[rustfmt::skip] let ndc = Vector4::new( clip.x / clip.w, clip.y / clip.w, clip.z / clip.w, 1.0 ); let min_depth = 0.0; let max_depth = 1.0; let x = 0.0; let y = 0.0; let ox = x + self.width as f64 / 2.0; let oy = y + self.height as f64 / 2.0; let oz = min_depth; let px = self.width as f64; let py = self.height as f64; let pz = max_depth - min_depth; let xd = ndc.x; let yd = ndc.y; let zd = ndc.z; Vector3::new(px / 2.0 * xd + ox, py / 2.0 * yd + oy, pz * zd + oz) } /// Order of transformations reversed: https://computergraphics.stackexchange.com/questions/6087/screen-space-coordinates-to-eye-space-conversion/6093 /// `w` is lost. /// /// OpenGL explanation: https://www.khronos.org/opengl/wiki/Compute_eye_space_from_window_space#From_window_to_ndc fn window_to_world( &self, window: &Vector3, inverted_view_proj: &InvertedViewProjection, ) -> Vector3 { #[rustfmt::skip] let fixed_window = Vector4::new( window.x, window.y, window.z, 1.0 ); let ndc = self.clip_to_window_transform().invert().unwrap() * fixed_window; let unprojected = inverted_view_proj.project(ndc); Vector3::new( unprojected.x / unprojected.w, unprojected.y / unprojected.w, unprojected.z / unprojected.w, ) } /// Alternative implementation to `window_to_world` /// /// Adopted from [here](https://docs.rs/nalgebra-glm/latest/src/nalgebra_glm/ext/matrix_projection.rs.html#164-181). fn window_to_world_nalgebra( window: &Vector3, inverted_view_proj: &InvertedViewProjection, width: f64, height: f64, ) -> Vector3 { let pt = Vector4::new( 2.0 * (window.x - 0.0) / width - 1.0, 2.0 * (height - window.y - 0.0) / height - 1.0, window.z, 1.0, ); let unprojected = inverted_view_proj.project(pt); Vector3::new( unprojected.x / unprojected.w, unprojected.y / unprojected.w, unprojected.z / unprojected.w, ) } /// Gets the world coordinates for the specified `window` coordinates on the `z=0` plane. pub fn window_to_world_at_ground( &self, window: &Vector2, inverted_view_proj: &InvertedViewProjection, ) -> Option> { let near_world = self.window_to_world(&Vector3::new(window.x, window.y, 0.0), inverted_view_proj); let far_world = self.window_to_world(&Vector3::new(window.x, window.y, 1.0), inverted_view_proj); // for z = 0 in world coordinates // Idea comes from: https://dondi.lmu.build/share/cg/unproject-explained.pdf let u = -near_world.z / (far_world.z - near_world.z); if (0.0..=1.0).contains(&u) { Some(near_world + u * (far_world - near_world)) } else { None } } /// Calculates an [`Aabb2`] bounding box which contains at least the visible area on the `z=0` /// plane. One can think of it as being the bounding box of the geometry which forms the /// intersection between the viewing frustum and the `z=0` plane. /// /// This implementation works in the world 3D space. It casts rays from the corners of the /// window to calculate intersections points with the `z=0` plane. Then a bounding box is /// calculated. /// /// *Note:* It is possible that no such bounding box exists. This is the case if the `z=0` plane /// is not in view. pub fn view_region_bounding_box( &self, inverted_view_proj: &InvertedViewProjection, ) -> Option> { let screen_bounding_box = [ Vector2::new(0.0, 0.0), Vector2::new(self.width, 0.0), Vector2::new(self.width, self.height), Vector2::new(0.0, self.height), ] .map(|point| self.window_to_world_at_ground(&point, inverted_view_proj)); let mut min: Option> = None; let mut max: Option> = None; for vector in screen_bounding_box.into_iter().flatten() { if let Some(current_min) = &mut min { if vector.x < current_min.x { current_min.x = vector.x; } if vector.y < current_min.y { current_min.y = vector.y; } } else { min = Some(Point2::new(vector.x, vector.y)) } if let Some(current_max) = &mut max { if vector.x > current_max.x { current_max.x = vector.x; } if vector.y > current_max.y { current_max.y = vector.y; } } else { max = Some(Point2::new(vector.x, vector.y)) } } Some(Aabb2::new(min?, max?)) } /// An alternative implementation for `view_bounding_box`. /// /// This implementation works in the NDC space. We are creating a plane in the world 3D space. /// Then we are transforming it to the NDC space. In NDC space it is easy to calculate /// the intersection points between an Aabb3 and a plane. The resulting Aabb2 is returned. pub fn view_region_bounding_box_ndc(&self, perspective: &Perspective) -> Option> { let view_proj = self.calc_view_proj(perspective); let a = view_proj.project(Vector4::new(0.0, 0.0, 0.0, 1.0)); let b = view_proj.project(Vector4::new(1.0, 0.0, 0.0, 1.0)); let c = view_proj.project(Vector4::new(1.0, 1.0, 0.0, 1.0)); let a_ndc = self.clip_to_window(&a).truncate(); let b_ndc = self.clip_to_window(&b).truncate(); let c_ndc = self.clip_to_window(&c).truncate(); let to_ndc = Vector3::new(1.0 / self.width, 1.0 / self.height, 1.0); let plane: Plane = Plane::from_points( Point3::from_vec(a_ndc.mul_element_wise(to_ndc)), Point3::from_vec(b_ndc.mul_element_wise(to_ndc)), Point3::from_vec(c_ndc.mul_element_wise(to_ndc)), )?; let points = plane.intersection_points_aabb3(&Aabb3::new( Point3::new(0.0, 0.0, 0.0), Point3::new(1.0, 1.0, 1.0), )); let inverted_view_proj = view_proj.invert(); let from_ndc = Vector3::new(self.width, self.height, 1.0); let vec = points .iter() .map(|point| { self.window_to_world(&point.mul_element_wise(from_ndc), &inverted_view_proj) }) .collect::>(); let min_x = vec .iter() .map(|point| point.x) .min_by(|a, b| a.partial_cmp(b).unwrap())?; let min_y = vec .iter() .map(|point| point.y) .min_by(|a, b| a.partial_cmp(b).unwrap())?; let max_x = vec .iter() .map(|point| point.x) .max_by(|a, b| a.partial_cmp(b).unwrap())?; let max_y = vec .iter() .map(|point| point.y) .max_by(|a, b| a.partial_cmp(b).unwrap())?; Some(Aabb2::new( Point2::new(min_x, min_y), Point2::new(max_x, max_y), )) } } pub struct Perspective { aspect: f64, fovy: cgmath::Rad, znear: f64, zfar: f64, } impl Perspective { pub fn new>>( width: u32, height: u32, fovy: F, znear: f64, zfar: f64, ) -> Self { Self { aspect: width as f64 / height as f64, fovy: fovy.into(), znear, zfar, } } pub fn resize(&mut self, width: u32, height: u32) { self.aspect = width as f64 / height as f64; } pub fn calc_matrix(&self) -> cgmath::Matrix4 { OPENGL_TO_WGPU_MATRIX * cgmath::perspective(self.fovy, self.aspect, self.znear, self.zfar) } } #[cfg(test)] mod tests { use crate::render::camera::{InvertedViewProjection, ViewProjection}; use cgmath::{AbsDiffEq, Vector2, Vector3, Vector4}; use super::{Camera, Perspective}; #[test] fn test() { let width = 1920.0; let height = 1080.0; let camera = Camera::new( (0.0, 5.0, 5000.0), cgmath::Deg(-90.0), cgmath::Deg(45.0), width as u32, height as u32, ); // 4732.561319582916 let perspective = Perspective::new( width as u32, height as u32, cgmath::Deg(45.0), 0.1, 100000.0, ); let view_proj: ViewProjection = camera.calc_view_proj(&perspective); let inverted_view_proj: InvertedViewProjection = view_proj.invert(); let world_pos: Vector4 = Vector4::new(0.0, 0.0, 0.0, 1.0); let clip = view_proj.project(world_pos); let origin_clip_space = view_proj.project(Vector4::new(0.0, 0.0, 0.0, 1.0)); println!("origin w in clip space: {:?}", origin_clip_space.w); println!("world_pos: {:?}", world_pos); println!("clip: {:?}", clip); println!("world_pos: {:?}", view_proj.invert().project(clip)); println!("window: {:?}", camera.clip_to_window_vulkan(&clip)); let window = camera.clip_to_window(&clip); println!("window (matrix): {:?}", window); // --------- nalgebra println!( "r world (nalgebra): {:?}", Camera::window_to_world_nalgebra( &window.truncate(), &inverted_view_proj, width, height ) ); // -------- far vs. near plane trick let near_world = Camera::window_to_world_nalgebra( &Vector3::new(window.x, window.y, 0.0), &inverted_view_proj, width, height, ); let far_world = Camera::window_to_world_nalgebra( &Vector3::new(window.x, window.y, 1.0), &inverted_view_proj, width, height, ); // for z = 0 in world coordinates let u = -near_world.z / (far_world.z - near_world.z); println!("u: {:?}", u); let unprojected = near_world + u * (far_world - near_world); println!("unprojected: {:?}", unprojected); assert!(Vector3::new(world_pos.x, world_pos.y, world_pos.z).abs_diff_eq(&unprojected, 0.05)); // ---- test for unproject let window = Vector2::new(960.0, 631.0); // 0, 4096: passt nicht //let window = Vector2::new(962.0, 1.0); // 0, 300: passt nicht //let window = Vector2::new(960.0, 540.0); // 0, 0 passt let near_world = camera.window_to_world(&Vector3::new(window.x, window.y, 0.0), &inverted_view_proj); let far_world = camera.window_to_world(&Vector3::new(window.x, window.y, 1.0), &inverted_view_proj); // for z = 0 in world coordinates let u = -near_world.z / (far_world.z - near_world.z); println!("u: {:?}", u); let unprojected = near_world + u * (far_world - near_world); println!("unprojected: {:?}", unprojected); // ---- //assert!(reverse_world.abs_diff_eq(&world_pos, 0.05)) } }