From b5e01c59b2905d402e9b260bf232afc26bd6a925 Mon Sep 17 00:00:00 2001 From: Paris DOUADY Date: Thu, 14 Dec 2023 22:14:20 +0100 Subject: [PATCH] implement raycast on heightmap and aabb --- engine_demo/src/helmet.rs | 2 +- engine_demo/src/main.rs | 4 +- engine_demo/src/spheres.rs | 2 +- engine_demo/src/terrain.rs | 68 +++++++-- geom/src/aabb3.rs | 20 ++- geom/src/heightmap.rs | 171 +++++++++++++++++++++-- geom/src/perp_camera.rs | 25 +++- native_app/src/rendering/orbit_camera.rs | 23 +-- 8 files changed, 266 insertions(+), 49 deletions(-) diff --git a/engine_demo/src/helmet.rs b/engine_demo/src/helmet.rs index 2fe736c1..ead66cc5 100644 --- a/engine_demo/src/helmet.rs +++ b/engine_demo/src/helmet.rs @@ -29,7 +29,7 @@ impl DemoElement for Helmet { Self { mesh: Some(mesh) } } - fn update(&mut self, _ctx: &mut Context) {} + fn update(&mut self, _ctx: &mut Context, _cam: &Camera) {} fn render(&mut self, fc: &mut FrameContext, _cam: &Camera, _frustrum: &InfiniteFrustrum) { fc.draw(self.mesh.clone()); diff --git a/engine_demo/src/main.rs b/engine_demo/src/main.rs index 6c515f2e..6a76da95 100644 --- a/engine_demo/src/main.rs +++ b/engine_demo/src/main.rs @@ -21,7 +21,7 @@ trait DemoElement { fn init(ctx: &mut Context) -> Self where Self: Sized; - fn update(&mut self, ctx: &mut Context); + fn update(&mut self, ctx: &mut Context, cam: &Camera); fn render(&mut self, fc: &mut FrameContext, cam: &Camera, frustrum: &InfiniteFrustrum); fn render_gui(&mut self, _ui: &mut egui::Ui) {} } @@ -144,7 +144,7 @@ impl engine::framework::State for State { if !*enabled { continue; } - de.update(ctx); + de.update(ctx, &self.camera); } for v in self.play_queue.drain(..) { diff --git a/engine_demo/src/spheres.rs b/engine_demo/src/spheres.rs index 114e480e..4ab75d13 100644 --- a/engine_demo/src/spheres.rs +++ b/engine_demo/src/spheres.rs @@ -54,7 +54,7 @@ impl DemoElement for Spheres { Self { meshes } } - fn update(&mut self, _ctx: &mut Context) {} + fn update(&mut self, _ctx: &mut Context, _cam: &Camera) {} fn render(&mut self, fc: &mut FrameContext, _cam: &Camera, _frustrum: &InfiniteFrustrum) { fc.draw(self.meshes.clone()); diff --git a/engine_demo/src/terrain.rs b/engine_demo/src/terrain.rs index 0029601c..2d5ee111 100644 --- a/engine_demo/src/terrain.rs +++ b/engine_demo/src/terrain.rs @@ -1,7 +1,8 @@ use crate::DemoElement; +use engine::meshload::load_mesh; use engine::terrain::TerrainRender as EngineTerrainRender; -use engine::{Context, FrameContext}; -use geom::{vec2, Camera, InfiniteFrustrum}; +use engine::{Context, FrameContext, InstancedMeshBuilder, MeshInstance}; +use geom::{vec2, Camera, Heightmap, HeightmapChunk, InfiniteFrustrum, LinearColor, Vec3}; const CSIZE: usize = 512; const CRESO: usize = 16; @@ -9,8 +10,12 @@ const MAP_SIZE: usize = 50; pub struct Terrain { terrain: EngineTerrainRender, - _heights: Box<[[[[f32; CRESO]; CRESO]; MAP_SIZE]; MAP_SIZE]>, + heights: Heightmap, reload: bool, + + last_hitpos: Option, + plane_hitpos: Option, + hitmesh: InstancedMeshBuilder, } impl DemoElement for Terrain { @@ -21,17 +26,16 @@ impl DemoElement for Terrain { fn init(ctx: &mut Context) -> Self { let gfx = &mut ctx.gfx; - let mut heights: Box<[[[[f32; CRESO]; CRESO]; MAP_SIZE]; MAP_SIZE]> = - vec![[[[0.0; CRESO]; CRESO]; MAP_SIZE]; MAP_SIZE] - .into_boxed_slice() - .try_into() - .unwrap(); + let hitmesh = load_mesh(gfx, "sphere.glb").unwrap(); + + let mut h = Heightmap::new(MAP_SIZE as u16, MAP_SIZE as u16); for y in 0..MAP_SIZE { for x in 0..MAP_SIZE { + let mut c = [[0.0; CRESO]; CRESO]; for i in 0..CRESO { for j in 0..CRESO { - heights[y][x][i][j] = 3000.0 + c[i][j] = 3000.0 * geom::fnoise::<6>( 0.002 * vec2((x * CRESO + j) as f32, (y * CRESO + i) as f32), ) @@ -41,6 +45,7 @@ impl DemoElement for Terrain { // (CSIZE / CRESO * i) as f32 + 0.5 * (CSIZE / CRESO * j) as f32; } } + h.set_chunk((x as u16, y as u16), HeightmapChunk::new(c)); } } @@ -50,7 +55,11 @@ impl DemoElement for Terrain { for x in 0..MAP_SIZE { for y in 0..MAP_SIZE { - terrain.update_chunk(gfx, (x as u32, y as u32), &heights[y][x]); + terrain.update_chunk( + gfx, + (x as u32, y as u32), + &h.get_chunk((x as u16, y as u16)).unwrap().heights(), + ); } } @@ -58,20 +67,55 @@ impl DemoElement for Terrain { Self { terrain, - _heights: heights, + heights: h, reload: false, + last_hitpos: None, + plane_hitpos: None, + hitmesh: InstancedMeshBuilder::new(hitmesh), } } - fn update(&mut self, ctx: &mut Context) { + fn update(&mut self, ctx: &mut Context, cam: &Camera) { if self.reload { self.reload = false; self.terrain.invalidate_height_normals(&ctx.gfx); } + + self.last_hitpos = None; + self.plane_hitpos = None; + if let Some(unproj) = cam.unproj_ray(ctx.input.mouse.screen) { + let p = geom::Plane { n: Vec3::Z, o: 0.0 }; + if let Some(mut v) = unproj.intersection_plane(&p) { + v.z = self.heights.height(v.xy()).unwrap_or(0.0); + self.plane_hitpos = Some(v); + } + + if let Some((hitpos, _hitnormal)) = self.heights.raycast(unproj) { + self.last_hitpos = Some(hitpos); + } + } } fn render(&mut self, fc: &mut FrameContext, cam: &Camera, frustrum: &InfiniteFrustrum) { self.terrain.draw_terrain(cam, frustrum, fc); + + self.hitmesh.instances.clear(); + if let Some(pos) = self.last_hitpos { + self.hitmesh.instances.push(MeshInstance { + pos, + dir: Vec3::X * 20.0, + tint: LinearColor::WHITE, + }); + } + if let Some(pos) = self.plane_hitpos { + self.hitmesh.instances.push(MeshInstance { + pos, + dir: Vec3::X * 10.0, + tint: LinearColor::RED, + }); + } + + fc.draw(self.hitmesh.build(fc.gfx)); } fn render_gui(&mut self, ui: &mut egui::Ui) { diff --git a/geom/src/aabb3.rs b/geom/src/aabb3.rs index f625b4ea..60bd252c 100644 --- a/geom/src/aabb3.rs +++ b/geom/src/aabb3.rs @@ -1,4 +1,4 @@ -use super::Vec3; +use super::{Ray3, Vec3}; use crate::{Intersect3, Shape3, AABB}; use serde::{Deserialize, Serialize}; @@ -106,6 +106,24 @@ impl AABB3 { && point.z <= self.ur.z + tolerance && point.z >= self.ll.z - tolerance } + + /// as ray is defined by O + tD, return the t values for the entering and exiting intersections + /// Returns a 2-tuple of (t_near, t_far) + /// Adapted from https://gist.github.com/DomNomNom/46bb1ce47f68d255fd5d + /// If the ray origin is inside the box, t_near will be zero + #[inline] + pub fn raycast(&self, ray: Ray3) -> Option<(f32, f32)> { + let t_min = (self.ll - ray.from) / ray.dir; + let t_max = (self.ur - ray.from) / ray.dir; + let t1 = t_min.min(t_max); + let t2 = t_min.max(t_max); + let t_near = f32::max(f32::max(t1.x, t1.y), t1.z); + let t_far = f32::min(f32::min(t2.x, t2.y), t2.z); + if t_near >= t_far || t_far < 0.0 { + return None; + } + Some((t_near.max(0.0), t_far)) + } } impl Intersect3 for AABB3 { diff --git a/geom/src/heightmap.rs b/geom/src/heightmap.rs index f06df586..58492fc1 100644 --- a/geom/src/heightmap.rs +++ b/geom/src/heightmap.rs @@ -1,25 +1,39 @@ -use crate::{vec2, Vec2, AABB}; +use crate::{vec2, vec3, Ray3, Vec2, Vec3, AABB, AABB3}; use serde::ser::SerializeSeq; use serde::{Deserialize, Serialize}; pub type HeightmapChunkID = (u16, u16); +const MIN_HEIGHT: f32 = -40.0; + #[derive(Clone)] pub struct HeightmapChunk { heights: [[f32; RESOLUTION]; RESOLUTION], // TODO: change to RESOLUTION * RESOLUTION when generic_const_exprs is stabilized + max_height: f32, } impl Default for HeightmapChunk { fn default() -> Self { Self { heights: [[0.0; RESOLUTION]; RESOLUTION], + max_height: 0.0, } } } impl HeightmapChunk { pub fn new(heights: [[f32; RESOLUTION]; RESOLUTION]) -> Self { - Self { heights } + let mut max_height = heights[0][0]; + for row in &heights { + for height in row { + max_height = max_height.max(*height); + } + } + + Self { + heights, + max_height, + } } pub fn rect(id: HeightmapChunkID) -> AABB { @@ -29,10 +43,22 @@ impl HeightmapChunk } pub fn id(v: Vec2) -> HeightmapChunkID { - if v.x < 0.0 || v.y < 0.0 { - return (0, 0); - } - ((v.x / SIZE as f32) as u16, (v.y / SIZE as f32) as u16) + let x = v.x / SIZE as f32; + let x = x.clamp(0.0, u16::MAX as f32) as u16; + let y = v.y / SIZE as f32; + let y = y.clamp(0.0, u16::MAX as f32) as u16; + (x, y) + } + + pub fn bbox(&self, origin: Vec2) -> AABB3 { + AABB3::new( + vec3(origin.x, origin.y, MIN_HEIGHT), + vec3( + origin.x + SIZE as f32, + origin.y + SIZE as f32, + self.max_height, + ), + ) } /// assume p is in chunk-space and in-bounds @@ -42,6 +68,12 @@ impl HeightmapChunk self.heights[v.y as usize][v.x as usize] } + pub fn height(&self, p: Vec2) -> Option { + let v = p / SIZE as f32; + let v = v * RESOLUTION as f32; + self.heights.get(v.y as usize)?.get(v.x as usize).copied() + } + pub fn heights(&self) -> &[[f32; RESOLUTION]; RESOLUTION] { &self.heights } @@ -134,11 +166,123 @@ impl Heightmap { } exact } + + /// Casts a ray on the heightmap, returning the point of intersection and the normal at that point + /// We assume height is between [-40.0; 2008] + pub fn raycast(&self, ray: Ray3) -> Option<(Vec3, Vec3)> { + // Let's build an iterator over the chunks that intersect the ray (from nearest to furthest) + let start = ray.from.xy() / SIZE as f32; + let end = start + ray.dir.xy().normalize() * self.w.max(self.h) as f32 * 2.0; + + let diff = end - start; + let l = diff.mag(); + let speed = diff / l; + + let mut t = 0.0; + + let mut cur = start; + + let intersecting_chunks = std::iter::once((start.x as isize, start.y as isize)) + .chain(std::iter::from_fn(|| { + let x = cur.x - cur.x.floor(); + let y = cur.y - cur.y.floor(); + + let t_x; + let t_y; + + if speed.x >= 0.0 { + t_x = (1.0 - x) / speed.x; + } else { + t_x = -x / speed.x; + } + if speed.y >= 0.0 { + t_y = (1.0 - y) / speed.y; + } else { + t_y = -y / speed.y; + } + + let min_t = t_x.min(t_y) + 0.0001; + t += min_t; + if !(t < l) { + // reverse the condition to avoid infinite loop in case of NaN + return None; + } + cur += min_t * speed; + Some((cur.x as isize, cur.y as isize)) + })) + .filter(|&(x, y)| x < self.w as isize && y < self.h as isize && x >= 0 && y >= 0) + .filter_map(|(x, y)| { + let chunk_id = (x as u16, y as u16); + let corner = vec2(x as f32, y as f32) * SIZE as f32; + let (t_min, t_max) = self.get_chunk(chunk_id)?.bbox(corner).raycast(ray)?; + Some((t_min, t_max)) + }); + + // Now within those chunks, let's try to find the intersection point + // h < t * ray.dir.z + ray.from.z + for (t_min, t_max) in intersecting_chunks { + let mut t = t_min; + let t_step = Self::CELL_SIZE; + + loop { + let p = ray.from + ray.dir * t; + let Some(h) = self.height(p.xy()) else { + if t >= t_max { + break; + } + t += t_step; + continue; + }; + if p.z < h { + // we found a good candidate but we're not there yet + // we still need to do one last binary search + // to find the bilinear-filtered-corrected location + + let t = binary_search(t - t_step * 2.0, t, |t| { + let p = ray.from + ray.dir * t; + let Some(h) = self.height(p.xy()) else { + return false; + }; + p.z < h + }); + + return Some((ray.from + ray.dir * t, vec3(0.0, 0.0, 1.0))); + } + if t >= t_max { + break; + } + t += t_step; + } + } + + None + } +} + +/// Does a binary search on the interval [min; max] to find the first value for which f returns true +fn binary_search(min: f32, max: f32, mut f: impl FnMut(f32) -> bool) -> f32 { + let mut min = min; + let mut max = max; + let mut mid = min + (max - min) * 0.5; + loop { + if f(mid) { + max = mid; + } else { + min = mid; + } + let new_mid = min + (max - min) * 0.5; + if (new_mid - mid).abs() < 0.0001 { + break; + } + mid = new_mid; + } + mid } impl Serialize for HeightmapChunk { fn serialize(&self, serializer: S) -> Result { let mut seq = serializer.serialize_seq(Some(RESOLUTION * RESOLUTION))?; + seq.serialize_element(&self.max_height)?; for row in &self.heights { for height in row { seq.serialize_element(height)?; @@ -152,15 +296,19 @@ impl<'de, const RESOLUTION: usize, const SIZE: u32> Deserialize<'de> for HeightmapChunk { fn deserialize>(deserializer: D) -> Result { - let heights = deserializer.deserialize_seq(HeightmapChunkVisitor::)?; - Ok(Self { heights }) + let (heights, max_height) = + deserializer.deserialize_seq(HeightmapChunkVisitor::)?; + Ok(Self { + heights, + max_height, + }) } } struct HeightmapChunkVisitor; impl<'de, const RESOLUTION: usize> serde::de::Visitor<'de> for HeightmapChunkVisitor { - type Value = [[f32; RESOLUTION]; RESOLUTION]; + type Value = ([[f32; RESOLUTION]; RESOLUTION], f32); fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { formatter.write_str("a sequence of floats") @@ -171,6 +319,9 @@ impl<'de, const RESOLUTION: usize> serde::de::Visitor<'de> for HeightmapChunkVis if len != RESOLUTION * RESOLUTION { return Err(serde::de::Error::invalid_length(len, &"")); } + let max_height = seq + .next_element()? + .ok_or_else(|| serde::de::Error::invalid_length(0, &""))?; let mut heights = [[0.0; RESOLUTION]; RESOLUTION]; for row in &mut heights { for height in row { @@ -179,6 +330,6 @@ impl<'de, const RESOLUTION: usize> serde::de::Visitor<'de> for HeightmapChunkVis .ok_or_else(|| serde::de::Error::invalid_length(0, &""))?; } } - Ok(heights) + Ok((heights, max_height)) } } diff --git a/geom/src/perp_camera.rs b/geom/src/perp_camera.rs index 9ff9f06d..94427ed1 100644 --- a/geom/src/perp_camera.rs +++ b/geom/src/perp_camera.rs @@ -15,7 +15,7 @@ // Modified for the Egregoria project by the Egregoria developers. use crate::matrix4::Matrix4; -use crate::{vec2, vec3, InfiniteFrustrum, Radians, Vec3, Vec4}; +use crate::{vec2, vec3, vec4, InfiniteFrustrum, Radians, Ray3, Vec2, Vec3, Vec4}; use serde::{Deserialize, Serialize}; #[derive(Copy, Clone, Serialize, Deserialize)] @@ -67,6 +67,29 @@ impl Camera { self.pos + self.offset() } + pub fn unproj_ray(&self, pos: Vec2) -> Option { + let proj = self.build_view_projection_matrix(); + let inv = proj.invert()?; + + let v = inv + * vec4( + 2.0 * pos.x / self.viewport_w - 1.0, + -(2.0 * pos.y / self.viewport_h - 1.0), + 1.0, + 1.0, + ); + + let v = Vec3 { + x: v.x / v.w, + y: v.y / v.w, + z: v.z / v.w, + } - self.eye(); + Some(Ray3 { + from: self.eye(), + dir: v.normalize(), + }) + } + pub fn build_view_projection_matrix(&self) -> Matrix4 { let eye = self.eye(); let view = look_to_rh(eye, -self.dir(), self.up); diff --git a/native_app/src/rendering/orbit_camera.rs b/native_app/src/rendering/orbit_camera.rs index 9525a3a3..a0ecbbf7 100644 --- a/native_app/src/rendering/orbit_camera.rs +++ b/native_app/src/rendering/orbit_camera.rs @@ -3,7 +3,7 @@ use crate::gui::windows::settings::Settings; use crate::inputmap::{InputAction, InputMap}; use common::saveload::Encoder; use engine::{Context, Tesselator}; -use geom::{vec4, Camera, InfiniteFrustrum, Matrix4, Plane, Radians, Ray3, Vec2, Vec3, AABB}; +use geom::{Camera, InfiniteFrustrum, Matrix4, Plane, Radians, Vec2, Vec3, AABB}; use simulation::map::pathfinding_crate::num_traits::Pow; /// CameraHandler3D is the camera handler for the 3D view @@ -58,26 +58,7 @@ impl OrbitCamera { } pub fn unproject(&self, pos: Vec2, height: impl Fn(Vec2) -> Option) -> Option { - let proj = self.camera.build_view_projection_matrix(); - let inv = proj.invert()?; - - let v = inv - * vec4( - 2.0 * pos.x / self.camera.viewport_w - 1.0, - -(2.0 * pos.y / self.camera.viewport_h - 1.0), - 1.0, - 1.0, - ); - - let v = Vec3 { - x: v.x / v.w, - y: v.y / v.w, - z: v.z / v.w, - } - self.camera.eye(); - let r = Ray3 { - from: self.camera.eye(), - dir: v.normalize(), - }; + let r = self.camera.unproj_ray(pos)?; let p = Plane { n: Vec3::Z, o: 0.0 };