Initial implementation of indoor camera. Enclosure detection and camera switch

This commit is contained in:
Joey Maher 2020-06-09 09:39:58 -05:00 committed by Monty Marz
parent 98c8240879
commit 7f6c310d38
4 changed files with 225 additions and 37 deletions

View File

@ -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

View File

@ -96,4 +96,71 @@ impl<'a, V: ReadVol, F: RayUntil<V::Vox>, G: RayForEach<V::Vox>> 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<f32, V::Error> {
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)
}
}
}

View File

@ -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<f64>,
enclosed: bool,
enclosed_last_checked: Option<Vec3<f32>>,
dependents: Dependents,
frustum: Frustum<f32>,
}
@ -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,13 +87,146 @@ impl Camera {
}
}
///
/// 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(raycast_resolution)
.until(|b| b.is_solid())
.cast()
{
(_, 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::<f32>::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: &impl ReadVol) {
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()
@ -97,6 +239,7 @@ impl Camera {
(_, Err(_)) => self.dist,
}
.max(0.0)
}
};
self.dependents.view_mat = Mat4::<f32>::identity()

View File

@ -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<i32>) -> Result<&'a Self::Vox, Self::Error> { Ok(&VoidVox) }
}
fn generate_mesh<'a>(
greedy: &mut GreedyMesh<'a>,
mesh: &mut Mesh<TerrainPipeline>,
@ -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,