From 974f7eb2c934da891fb2e7bbdea15790d1a60a01 Mon Sep 17 00:00:00 2001 From: Maximilian Ammann Date: Fri, 21 Jan 2022 12:27:56 +0100 Subject: [PATCH] Add two ways of determining view bounding box: view_bounding_box2 and view_bounding_box --- src/input/mod.rs | 7 +- src/input/tilt_handler.rs | 2 - src/render/camera.rs | 145 +++++++++++++----- src/render/render_state.rs | 3 + src/util/math.rs | 305 +++++++++++++++++++++++++++++++++++++ src/util/mod.rs | 1 + 6 files changed, 420 insertions(+), 43 deletions(-) create mode 100644 src/util/math.rs diff --git a/src/input/mod.rs b/src/input/mod.rs index a9c58efb..bb38a400 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -2,16 +2,13 @@ use std::time::Duration; -use cgmath::Vector2; -use winit::event::{ - DeviceEvent, ElementState, KeyboardInput, MouseButton, TouchPhase, WindowEvent, -}; +use cgmath::{ElementWise, EuclideanSpace, InnerSpace, Point2, Point3, Vector2, Vector3, Vector4}; +use winit::event::{DeviceEvent, ElementState, KeyboardInput, TouchPhase, WindowEvent}; use crate::input::pan_handler::PanHandler; use crate::input::pinch_handler::PinchHandler; use crate::input::shift_handler::ShiftHandler; use crate::input::tilt_handler::TiltHandler; -use crate::render::camera::Camera; use crate::render::render_state::RenderState; mod pan_handler; diff --git a/src/input/tilt_handler.rs b/src/input/tilt_handler.rs index c7ab4ae7..aa7bf28b 100644 --- a/src/input/tilt_handler.rs +++ b/src/input/tilt_handler.rs @@ -16,8 +16,6 @@ impl UpdateState for TiltHandler { let delta = self.delta_pitch * dt; state.camera.pitch += Rad::from(delta); - let x: Deg = state.camera.pitch.into(); - println!("{:?}", x); self.delta_pitch -= delta; } } diff --git a/src/render/camera.rs b/src/render/camera.rs index 58d3cb2a..a4d207a6 100644 --- a/src/render/camera.rs +++ b/src/render/camera.rs @@ -1,7 +1,8 @@ use cgmath::prelude::*; -use cgmath::{Matrix4, Vector2, Vector3, Vector4}; +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( @@ -124,8 +125,7 @@ impl Camera { } // https://docs.microsoft.com/en-us/windows/win32/dxtecharts/the-direct3d-transformation-pipeline - // https://docs.microsoft.com/en-us/windows/win32/dxtecharts/the-direct3d-transformation-pipeline - fn clip_to_window_matrix(clip: &Vector4, width: f64, height: f64) -> Vector4 { + fn clip_to_window_via_transform(&self, clip: &Vector4) -> Vector4 { #[rustfmt::skip] let ndc = Vector4::new( clip.x / clip.w, @@ -134,16 +134,15 @@ impl Camera { clip.w / clip.w ); - let window = Self::clip_to_window_transform(width, height) * ndc; + let window = Self::clip_to_window_transform(self.width, self.height) * ndc; window } - fn window_to_world( - window: &Vector3, - view_proj: &Matrix4, - width: f64, - height: f64, - ) -> Vector3 { + /// 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, view_proj: &Matrix4) -> Vector3 { #[rustfmt::skip] let fixed_window = Vector4::new( window.x, @@ -152,7 +151,7 @@ impl Camera { 1.0 ); - let ndc = Self::clip_to_window_transform(width, height) + let ndc = Self::clip_to_window_transform(self.width, self.height) .invert() .unwrap() * fixed_window; @@ -166,6 +165,8 @@ impl Camera { } /// Alternative implementation to `window_to_world` + /// + /// https://docs.rs/nalgebra-glm/latest/src/nalgebra_glm/ext/matrix_projection.rs.html#164-181 fn window_to_world_nalgebra( window: &Vector3, view_proj: &Matrix4, @@ -187,29 +188,110 @@ impl Camera { world } + /// Idea comes from: https://dondi.lmu.build/share/cg/unproject-explained.pdf pub fn window_to_world_z0( &self, window: &Vector2, view_proj: &Matrix4, ) -> Vector3 { - let near_world = Camera::window_to_world( - &Vector3::new(window.x, window.y, 0.0), - &view_proj, - self.width, - self.height, - ); + let near_world = self.window_to_world(&Vector3::new(window.x, window.y, 0.0), &view_proj); - let far_world = Camera::window_to_world( - &Vector3::new(window.x, window.y, 1.0), - &view_proj, - self.width, - self.height, - ); + let far_world = self.window_to_world(&Vector3::new(window.x, window.y, 1.0), &view_proj); // for z = 0 in world coordinates let u = -near_world.z / (far_world.z - near_world.z); + if u < 0.0 || u > 1.0 { + panic!("interpolation factor is out of bounds") + } near_world + u * (far_world - near_world) } + + pub fn view_bounding_box2(&self, perspective: &Perspective) -> Option> { + let view_proj = self.calc_view_proj(perspective); + + let vec = vec![ + Vector2::new(0.0, 0.0), + Vector2::new(self.width, 0.0), + Vector2::new(self.width, self.height), + Vector2::new(0.0, self.height), + ] + .iter() + .map(|point| self.window_to_world_z0(point, &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 fn view_bounding_box(&self, perspective: &Perspective) -> Option> { + let view_proj = self.calc_view_proj(perspective); + let a = view_proj * Vector4::new(0.0, 0.0, 0.0, 1.0); + let b = view_proj * Vector4::new(1.0, 0.0, 0.0, 1.0); + let c = view_proj * Vector4::new(1.0, 1.0, 0.0, 1.0); + + let a = self.clip_to_window_via_transform(&a); + let a_ndc = a.truncate(); + let b_ndc = self.clip_to_window_via_transform(&b).truncate(); + let c_ndc = self.clip_to_window_via_transform(&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)), + ) + .unwrap(); + println!("{:?}", &plane); + + let mut points = plane.intersection_points_aabb3(&Aabb3::new( + Point3::new(0.0, 0.0, 0.0).into(), + Point3::new(1.0, 1.0, 1.0).into(), + )); + + 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), &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 { @@ -246,6 +328,7 @@ impl Perspective { #[cfg(test)] mod tests { + use crate::render::camera; use cgmath::{AbsDiffEq, ElementWise, Matrix4, SquareMatrix, Vector2, Vector3, Vector4}; use super::{Camera, Perspective}; @@ -282,7 +365,7 @@ mod tests { println!("world_pos: {:?}", view_proj.invert().unwrap() * clip); println!("window: {:?}", Camera::clip_to_window(&clip, width, height)); - let window = Camera::clip_to_window_matrix(&clip, width, height); + let window = camera.clip_to_window_via_transform(&clip); println!("window (matrix): {:?}", window); // --------- nalgebra @@ -320,19 +403,9 @@ mod tests { 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), - &view_proj, - width, - height, - ); + let near_world = camera.window_to_world(&Vector3::new(window.x, window.y, 0.0), &view_proj); - let far_world = Camera::window_to_world( - &Vector3::new(window.x, window.y, 1.0), - &view_proj, - width, - height, - ); + let far_world = camera.window_to_world(&Vector3::new(window.x, window.y, 1.0), &view_proj); // for z = 0 in world coordinates let u = -near_world.z / (far_world.z - near_world.z); diff --git a/src/render/render_state.rs b/src/render/render_state.rs index 12ee9915..036d0b55 100644 --- a/src/render/render_state.rs +++ b/src/render/render_state.rs @@ -350,6 +350,9 @@ impl RenderState { // TODO: Could we draw inspiration from StagingBelt (https://docs.rs/wgpu/latest/wgpu/util/struct.StagingBelt.html)? // TODO: What is StagingBelt for? pub fn upload_tile_geometry(&mut self, worker_loop: &WorkerLoop) { + println!("1: {:?}", self.camera.view_bounding_box(&self.perspective)); + println!("2: {:?}", self.camera.view_bounding_box2(&self.perspective)); + let upload = worker_loop.pop_all(); for layer in upload.iter() { diff --git a/src/util/math.rs b/src/util/math.rs new file mode 100644 index 00000000..41b99a31 --- /dev/null +++ b/src/util/math.rs @@ -0,0 +1,305 @@ +use cgmath::{ + ulps_eq, BaseFloat, BaseNum, EuclideanSpace, InnerSpace, Point2, Point3, Vector3, Vector4, Zero, +}; +use std::cmp::Ordering; +use std::fmt; + +/// A 3-dimensional plane formed from the equation: `A*x + B*y + C*z - D = 0`. +/// +/// # Fields +/// +/// - `n`: a unit vector representing the normal of the plane where: +/// - `n.x`: corresponds to `A` in the plane equation +/// - `n.y`: corresponds to `B` in the plane equation +/// - `n.z`: corresponds to `C` in the plane equation +/// - `d`: the distance value, corresponding to `D` in the plane equation +/// +/// # Notes +/// +/// The `A*x + B*y + C*z - D = 0` form is preferred over the other common +/// alternative, `A*x + B*y + C*z + D = 0`, because it tends to avoid +/// superfluous negations (see _Real Time Collision Detection_, p. 55). +/// +/// Copied from: https://github.com/rustgd/collision-rs +pub struct Plane { + /// Plane normal + pub n: Vector3, + /// Plane distance value + pub d: S, +} + +impl Plane { + /// Construct a plane from a normal vector and a scalar distance. The + /// plane will be perpendicular to `n`, and `d` units offset from the + /// origin. + pub fn new(n: Vector3, d: S) -> Plane { + Plane { n, d } + } + + /// Constructs a plane that passes through the the three points `a`, `b` and `c` + pub fn from_points(a: Point3, b: Point3, c: Point3) -> Option> { + // create two vectors that run parallel to the plane + let v0 = b - a; + let v1 = c - a; + + // find the normal vector that is perpendicular to v1 and v2 + let n = v0.cross(v1); + if ulps_eq!(n, &Vector3::zero()) { + None + } else { + // compute the normal and the distance to the plane + let n = n.normalize(); + let d = -a.dot(n); + + Some(Plane::new(n, d)) + } + } + + /// Construct a plane from a point and a normal vector. + /// The plane will contain the point `p` and be perpendicular to `n`. + pub fn from_point_normal(p: Point3, n: Vector3) -> Plane { + Plane { n, d: p.dot(n) } + } + + fn intersection_distance_ray( + &self, + ray_origin: &Vector3, + ray_direction: &Vector3, + ) -> Option { + let vd: S = + self.n.x * ray_direction.x + self.n.y * ray_direction.y + self.n.z * ray_direction.z; + if vd == S::zero() { + return None; + } + let t: S = + -(self.n.x * ray_origin.x + self.n.y * ray_origin.y + self.n.z * ray_origin.z + self.d) + / vd; + + return Some(t); + } + + /// Returns unsorted intersection points with an Aabb3 + /// Adopted from: https://www.asawicki.info/news_1428_finding_polygon_of_plane-aabb_intersection + /// Inspired by: https://godotengine.org/qa/54688/camera-frustum-intersection-with-plane + pub fn intersection_points_aabb3(&self, aabb: &Aabb3) -> Vec> { + let mut out_points: Vec> = Vec::new(); + let aabb_min: Vector3 = aabb.min.to_vec().into(); + let aabb_max: Vector3 = aabb.max.to_vec().into(); + + // Test edges along X axis, pointing right. + let mut dir: Vector3 = Vector3::new(aabb_max.x - aabb_min.x, S::zero(), S::zero()); + let mut orig = aabb_min; + if let Some(t) = self.intersection_distance_ray(&orig, &dir) { + if t >= S::zero() && t <= S::one() { + out_points.push(orig + dir * t); + } + } + + orig = Vector3::new(aabb_min.x, aabb_max.y, aabb_min.z); + if let Some(t) = self.intersection_distance_ray(&orig, &dir) { + if t >= S::zero() && t <= S::one() { + out_points.push(orig + dir * t); + } + } + + orig = Vector3::new(aabb_min.x, aabb_min.y, aabb_max.z); + if let Some(t) = self.intersection_distance_ray(&orig, &dir) { + if t >= S::zero() && t <= S::one() { + out_points.push(orig + dir * t); + } + } + + orig = Vector3::new(aabb_min.x, aabb_max.y, aabb_max.z); + if let Some(t) = self.intersection_distance_ray(&orig, &dir) { + if t >= S::zero() && t <= S::one() { + out_points.push(orig + dir * t); + } + } + + // Test edges along Y axis, pointing up. + dir = Vector3::new(S::zero(), aabb_max.y - aabb_min.y, S::zero()); + orig = Vector3::new(aabb_min.x, aabb_min.y, aabb_min.z); + if let Some(t) = self.intersection_distance_ray(&orig, &dir) { + if t >= S::zero() && t <= S::one() { + out_points.push(orig + dir * t); + } + } + + orig = Vector3::new(aabb_max.x, aabb_min.y, aabb_min.z); + if let Some(t) = self.intersection_distance_ray(&orig, &dir) { + if t >= S::zero() && t <= S::one() { + out_points.push(orig + dir * t); + } + } + + orig = Vector3::new(aabb_min.x, aabb_min.y, aabb_max.z); + if let Some(t) = self.intersection_distance_ray(&orig, &dir) { + if t >= S::zero() && t <= S::one() { + out_points.push(orig + dir * t); + } + } + + orig = Vector3::new(aabb_max.x, aabb_min.y, aabb_max.z); + if let Some(t) = self.intersection_distance_ray(&orig, &dir) { + if t >= S::zero() && t <= S::one() { + out_points.push(orig + dir * t); + } + } + + // Test edges along Z axis, pointing forward. + dir = Vector3::new(S::zero(), S::zero(), aabb_max.z - aabb_min.z); + orig = Vector3::new(aabb_min.x, aabb_min.y, aabb_min.z); + if let Some(t) = self.intersection_distance_ray(&orig, &dir) { + if t >= S::zero() && t <= S::one() { + out_points.push(orig + dir * t); + } + } + + orig = Vector3::new(aabb_max.x, aabb_min.y, aabb_min.z); + if let Some(t) = self.intersection_distance_ray(&orig, &dir) { + if t >= S::zero() && t <= S::one() { + out_points.push(orig + dir * t); + } + } + + orig = Vector3::new(aabb_min.x, aabb_max.y, aabb_min.z); + if let Some(t) = self.intersection_distance_ray(&orig, &dir) { + if t >= S::zero() && t <= S::one() { + out_points.push(orig + dir * t); + } + } + + orig = Vector3::new(aabb_max.x, aabb_max.y, aabb_min.z); + if let Some(t) = self.intersection_distance_ray(&orig, &dir) { + if t >= S::zero() && t <= S::one() { + out_points.push(orig + dir * t); + } + } + + out_points + } + + pub fn intersection_polygon_aabb3(&self, aabb: &Aabb3) -> Vec> { + let mut points = self.intersection_points_aabb3(aabb); + + if points.is_empty() { + return points; + }; + + let plane_normal = Vector3::new(self.n.x, self.n.y, self.n.z); + let origin = points[0]; + + points.sort_by(|a, b| { + let cmp = (a - origin).cross(b - origin).dot(plane_normal); + if cmp < S::zero() { + Ordering::Less + } else if cmp == S::zero() { + Ordering::Equal + } else { + Ordering::Greater + } + }); + + points + } +} + +impl fmt::Debug for Plane { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{:?}x + {:?}y + {:?}z - {:?} = 0", + self.n.x, self.n.y, self.n.z, self.d + ) + } +} + +pub(crate) fn min(lhs: S, rhs: S) -> S { + match lhs.partial_cmp(&rhs) { + Some(Ordering::Less) | Some(Ordering::Equal) | None => lhs, + _ => rhs, + } +} + +pub(crate) fn max(lhs: S, rhs: S) -> S { + match lhs.partial_cmp(&rhs) { + Some(Ordering::Greater) | Some(Ordering::Equal) | None => lhs, + _ => rhs, + } +} + +/// A two-dimensional AABB, aka a rectangle. +pub struct Aabb2 { + /// Minimum point of the AABB + pub min: Point2, + /// Maximum point of the AABB + pub max: Point2, +} + +impl Aabb2 { + /// Construct a new axis-aligned bounding box from two points. + #[inline] + pub fn new(p1: Point2, p2: Point2) -> Aabb2 { + Aabb2 { + min: Point2::new(min(p1.x, p2.x), min(p1.y, p2.y)), + max: Point2::new(max(p1.x, p2.x), max(p1.y, p2.y)), + } + } + + /// Compute corners. + #[inline] + pub fn to_corners(&self) -> [Point2; 4] { + [ + self.min, + Point2::new(self.max.x, self.min.y), + Point2::new(self.min.x, self.max.y), + self.max, + ] + } +} + +impl fmt::Debug for Aabb2 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "[{:?} - {:?}]", self.min, self.max) + } +} + +/// A three-dimensional AABB, aka a rectangular prism. +pub struct Aabb3 { + /// Minimum point of the AABB + pub min: Point3, + /// Maximum point of the AABB + pub max: Point3, +} + +impl Aabb3 { + /// Construct a new axis-aligned bounding box from two points. + #[inline] + pub fn new(p1: Point3, p2: Point3) -> Aabb3 { + Aabb3 { + min: Point3::new(min(p1.x, p2.x), min(p1.y, p2.y), min(p1.z, p2.z)), + max: Point3::new(max(p1.x, p2.x), max(p1.y, p2.y), max(p1.z, p2.z)), + } + } + + /// Compute corners. + #[inline] + pub fn to_corners(&self) -> [Point3; 8] { + [ + self.min, + Point3::new(self.max.x, self.min.y, self.min.z), + Point3::new(self.min.x, self.max.y, self.min.z), + Point3::new(self.max.x, self.max.y, self.min.z), + Point3::new(self.min.x, self.min.y, self.max.z), + Point3::new(self.max.x, self.min.y, self.max.z), + Point3::new(self.min.x, self.max.y, self.max.z), + self.max, + ] + } +} + +impl fmt::Debug for Aabb3 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "[{:?} - {:?}]", self.min, self.max) + } +} diff --git a/src/util/mod.rs b/src/util/mod.rs index 0e4247de..7298d474 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -1,6 +1,7 @@ //! Utils which are used internally mod fps_meter; +pub mod math; mod measure; pub use fps_meter::FPSMeter;