From 7f6c310d38c0b3e6f4a0032cbb6d27c5daa65110 Mon Sep 17 00:00:00 2001 From: Joey Maher Date: Tue, 9 Jun 2020 09:39:58 -0500 Subject: [PATCH] Initial implementation of indoor camera. Enclosure detection and camera switch --- CHANGELOG.md | 7 +- common/src/ray.rs | 67 +++++++++++++++ voxygen/src/scene/camera.rs | 167 +++++++++++++++++++++++++++++++++--- voxygen/src/scene/simple.rs | 21 +---- 4 files changed, 225 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac65bf0246..2ab25ac7e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added + - New level of detail feature, letting you see all the world's terrain at any view distance. - Point and directional lights now cast realistic shadows, using shadow mapping. - Added leaf and chimney particles @@ -20,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Adaptive stride setup for more dynamic run behavior - Theropod body - Several new animals +- Added a separate camera mode for when the player is indoors ### Changed @@ -95,11 +97,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add detection of entities under the cursor - Functional group-system with exp-sharing and disabled damage to group members - Some Campfire, fireball & bomb; particle, light & sound effects. -- Added firework recipe -- Added setting to change resolution -- Rare (unfinished) castles -- Caves with monsters and treasure -- Furniture and decals in towns ### Changed diff --git a/common/src/ray.rs b/common/src/ray.rs index 7f58dc3926..3d667df317 100644 --- a/common/src/ray.rs +++ b/common/src/ray.rs @@ -96,4 +96,71 @@ impl<'a, V: ReadVol, F: RayUntil, G: RayForEach> Ray<'a, V, F, G (dist, Ok(None)) } + + /// + /// Unlike cast(), this method traverses the whole ray identifying + /// "edges". Edges are defined as the point where the until condition + /// switches from false to true, or true to false. + /// + /// It then returns the last edge's distance. + pub fn last_edge_cast(mut self) -> Result { + const PLANCK: f32 = 0.001; + + let mut dist = 0.0; + let dir = (self.to - self.from).normalized(); + let max = (self.to - self.from).magnitude(); + + let mut until_condition_met = true; + + let mut distances = vec![]; + + for _ in 0..self.max_iter { + let pos = self.from + dir * dist; + let ipos = pos.map(|e| e.floor() as i32); + + // Allow one iteration above max. + if dist > max { + break; + } + + let vox = self.vol.get(ipos); + + // for_each + if let Some(g) = &mut self.for_each { + if let Ok(vox) = vox { + g(vox, ipos); + } + } + + match vox.map(|vox| (vox, (self.until)(vox))) { + Ok((_vox, true)) => { + if !until_condition_met { + // This is an edge + distances.push(dist); + until_condition_met = true; + } + }, + Ok((_vox, false)) => { + if until_condition_met { + // This is also an edge + distances.push(dist); + until_condition_met = false; + } + }, + Err(err) if !self.ignore_error => return Err(err), + _ => {}, + } + + let deltas = + (dir.map(|e| if e < 0.0 { 0.0 } else { 1.0 }) - pos.map(|e| e.abs().fract())) / dir; + + dist += deltas.reduce(f32::min).max(PLANCK); + } + + if !distances.is_empty() { + Ok(distances[distances.len() - 1]) + } else { + Ok(dist) + } + } } diff --git a/voxygen/src/scene/camera.rs b/voxygen/src/scene/camera.rs index f48db8a7f3..2453689e3d 100644 --- a/voxygen/src/scene/camera.rs +++ b/voxygen/src/scene/camera.rs @@ -1,5 +1,6 @@ use common::{ span, + terrain::TerrainGrid, vol::{ReadVol, Vox}, }; use std::f32::consts::PI; @@ -48,6 +49,9 @@ pub struct Camera { last_time: Option, + enclosed: bool, + enclosed_last_checked: Option>, + dependents: Dependents, frustum: Frustum, } @@ -68,6 +72,11 @@ impl Camera { last_time: None, + // Whether the camera was detected to be inside of a structure. Switches us to indoor + // camera. + enclosed: false, + enclosed_last_checked: None, + dependents: Dependents { view_mat: Mat4::identity(), proj_mat: Mat4::identity(), @@ -78,25 +87,159 @@ impl Camera { } } - /// Compute the transformation matrices (view matrix and projection matrix) - /// and position of the camera. - pub fn compute_dependents(&mut self, terrain: &impl ReadVol) { - span!(_guard, "compute_dependents", "Camera::compute_dependents"); - let dist = { - let (start, end) = (self.focus - self.forward() * self.dist, self.focus); + /// + /// Estimate whether the player is indoors with rays from their position + /// The player is marked "enclosed" if >= 75% of the rays hit a solid block + pub fn check_enclosure(&mut self, terrain: &TerrainGrid) { + // We can expose these values in the settings at some point if they become a + // performance concern + let vertical_resolution = 3; + let horizontal_resolution = 12; + let ray_distance = 30.0; + let raycast_resolution = 50; + + let mut total = 0; + let mut hit = 0; + + for horizontal in 0..horizontal_resolution { + // Do a ray horizontally out from the player to check for things + let horizontal_angle = (horizontal as f32 * PI) / (horizontal_resolution as f32 / 2.0); + let (start, end) = ( + self.focus, + self.focus + + (Vec3::new( + -f32::sin(horizontal as f32), + -f32::cos(horizontal as f32), + 0.0, + ) * ray_distance), + ); match terrain .ray(start, end) .ignore_error() - .max_iter(500) - .until(|b| b.is_empty()) + .max_iter(raycast_resolution) + .until(|b| b.is_solid()) .cast() { - (d, Ok(Some(_))) => f32::min(self.dist - d - 0.03, self.dist), - (_, Ok(None)) => self.dist, - (_, Err(_)) => self.dist, + (_, Ok(Some(_))) => { + total += 1; + hit += 1; + }, + (_, Ok(None)) => total += 1, + (_, Err(_)) => total += 1, + } + + if horizontal % vertical_resolution == 0 { + for vertical in 0..3 { + let vertical_angle = (vertical as f32 * PI) / 6.0; + let (vertical_start, vertical_end) = ( + self.focus, + self.focus + + (Vec3::new( + -f32::sin(horizontal_angle) * f32::cos(vertical_angle), + -f32::cos(horizontal_angle) * f32::cos(vertical_angle), + f32::sin(vertical_angle), + ) * ray_distance), + ); + + match terrain + .ray(vertical_start, vertical_end) + .ignore_error() + .max_iter(raycast_resolution) + .until(|b| b.is_solid()) + .cast() + { + (_, Ok(Some(_))) => { + total += 1; + hit += 1; + }, + (_, Ok(None)) => total += 1, + (_, Err(_)) => total += 1, + } + } + } + } + + let percentage = hit as f32 / total as f32; + self.enclosed = percentage >= 0.75; + } + + /// Compute_dependents adjusted for when there is no terrain data (character + /// selection) + pub fn compute_dependents_no_terrain(&mut self) { + self.dependents.view_mat = Mat4::::identity() + * Mat4::translation_3d(-Vec3::unit_z() * self.dist) + * Mat4::rotation_z(self.ori.z) + * Mat4::rotation_x(self.ori.y) + * Mat4::rotation_y(self.ori.x) + * Mat4::rotation_3d(PI / 2.0, -Vec4::unit_x()) + * Mat4::translation_3d(-self.focus); + + self.dependents.proj_mat = + Mat4::perspective_rh_no(self.fov, self.aspect, NEAR_PLANE, FAR_PLANE); + + // TODO: Make this more efficient. + self.dependents.cam_pos = Vec3::from(self.dependents.view_mat.inverted() * Vec4::unit_w()); + } + + /// Compute the transformation matrices (view matrix and projection matrix) + /// and position of the camera. + pub fn compute_dependents(&mut self, terrain: &TerrainGrid) { + span!(_guard, "compute_dependents", "Camera::compute_dependents"); + // Check enclosure if necessary + let mut should_check_enclosure = false; + match self.enclosed_last_checked { + Some(vec) => { + // We don't need to check enclosure every frame. Only check if we have moved. + if vec.distance_squared(self.focus) >= 1.2 { + should_check_enclosure = true; + self.enclosed_last_checked = Some(self.focus); + } + }, + None => { + should_check_enclosure = true; + self.enclosed_last_checked = Some(self.focus); + }, + } + + if should_check_enclosure { + self.check_enclosure(&*terrain); + } + + let dist = { + let (start, end) = (self.focus - self.forward() * self.dist, self.focus); + + if self.enclosed { + match terrain + .ray(start, end) + .ignore_error() + .max_iter(500) + .until(|vox| !vox.is_solid()) + .last_edge_cast() + { + Ok(d) => { + if d >= self.dist { + self.dist + } else { + f32::min(self.dist - d - 0.1, self.dist) + } + }, + Err(_) => self.dist, + } + } else { + match terrain + .ray(start, end) + .ignore_error() + .max_iter(500) + .until(|b| b.is_empty()) + .cast() + { + (d, Ok(Some(_))) => f32::min(self.dist - d - 0.03, self.dist), + (_, Ok(None)) => self.dist, + (_, Err(_)) => self.dist, + } + .max(0.0) } - .max(0.0) }; self.dependents.view_mat = Mat4::::identity() diff --git a/voxygen/src/scene/simple.rs b/voxygen/src/scene/simple.rs index 64f0167117..ba21fdd980 100644 --- a/voxygen/src/scene/simple.rs +++ b/voxygen/src/scene/simple.rs @@ -22,29 +22,10 @@ use common::{ comp::{humanoid, item::ItemKind, Loadout}, figure::Segment, terrain::BlockKind, - vol::{BaseVol, ReadVol, Vox}, }; use tracing::error; use vek::*; -#[derive(PartialEq, Eq, Copy, Clone)] -struct VoidVox; -impl Vox for VoidVox { - fn empty() -> Self { VoidVox } - - fn is_empty(&self) -> bool { true } - - fn or(self, _other: Self) -> Self { VoidVox } -} -struct VoidVol; -impl BaseVol for VoidVol { - type Error = (); - type Vox = VoidVox; -} -impl ReadVol for VoidVol { - fn get<'a>(&'a self, _pos: Vec3) -> Result<&'a Self::Vox, Self::Error> { Ok(&VoidVox) } -} - fn generate_mesh<'a>( greedy: &mut GreedyMesh<'a>, mesh: &mut Mesh, @@ -234,7 +215,7 @@ impl Scene { scene_data.mouse_smoothing, ); - self.camera.compute_dependents(&VoidVol); + self.camera.compute_dependents_no_terrain(); let camera::Dependents { view_mat, proj_mat,