Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add initial sphere-voxel collision detection #262

Merged
merged 4 commits into from
Feb 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 8 additions & 11 deletions client/src/graphics/voxels/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use common::{
graph::NodeId,
lru_slab::SlotId,
math,
node::{Chunk, VoxelData},
node::{Chunk, ChunkId, VoxelData},
traversal::nearby_nodes,
LruSlab,
};
Expand Down Expand Up @@ -146,27 +146,24 @@ impl Voxels {
}

use Chunk::*;
for chunk in Vertex::iter() {
for vertex in Vertex::iter() {
let chunk = ChunkId::new(node, vertex);
// Fetch existing chunk, or extract surface of new chunk
match sim
.graph
.get_mut(node)
.as_mut()
.get_chunk_mut(chunk)
.expect("all nodes must be populated before rendering")
.chunks[chunk]
{
Generating => continue,
Fresh => {
// Generate voxel data
if let Some(params) = common::worldgen::ChunkParams::new(
self.surfaces.dimension() as u8,
&sim.graph,
node,
chunk,
) {
if self.worldgen.load(ChunkDesc { node, params }).is_ok() {
sim.graph.get_mut(node).as_mut().unwrap().chunks[chunk] =
Generating;
sim.graph[chunk] = Generating;
}
}
continue;
Expand All @@ -181,7 +178,7 @@ impl Voxels {
frame.drawn.push(slot);
// Transfer transform
frame.surface.transforms_mut()[slot.0 as usize] =
node_transform * chunk.chunk_to_node().map(|x| x as f32);
node_transform * vertex.chunk_to_node().map(|x| x as f32);
}
(&mut ref mut surface @ None, &VoxelData::Dense(ref data)) => {
// Extract a surface so it can be drawn in future frames
Expand All @@ -203,7 +200,7 @@ impl Voxels {
frame.extracted.push(scratch_slot);
let slot = self.states.insert(SurfaceState {
node,
chunk,
chunk: vertex,
refcount: 0,
});
*surface = Some(slot);
Expand All @@ -224,7 +221,7 @@ impl Voxels {
indirect_offset: self.surfaces.indirect_offset(slot.0),
face_offset: self.surfaces.face_offset(slot.0),
draw_id: slot.0,
reverse_winding: chunk.parity() ^ node_is_odd,
reverse_winding: vertex.parity() ^ node_is_odd,
});
}
(None, &VoxelData::Solid(_)) => continue,
Expand Down
112 changes: 104 additions & 8 deletions common/src/character_controller.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use tracing::{error, info};

use crate::{
math,
node::DualGraph,
graph_collision, math,
node::{ChunkLayout, DualGraph},
proto::{CharacterInput, Position},
sanitize_motion_input, SimConfig,
};
Expand Down Expand Up @@ -56,15 +58,25 @@ impl CharacterControllerPass<'_> {
*self.velocity += current_to_target_velocity;
}

// Update position by using the average of the old velocity and new velocity, which has
// the effect of modeling a velocity that changes linearly over the timestep. This is
// necessary to avoid the following two issues:
// Set expected displacement by using the average of the old velocity and new velocity,
// which has the effect of modeling a velocity that changes linearly over the timestep.
// This is necessary to avoid the following two issues:
// 1. Input lag, which would occur if only the old velocity was used
// 2. Movement artifacts, which would occur if only the new velocity was used. One
// example of such an artifact is the player moving backwards slightly when they
// example of such an artifact is the character moving backwards slightly when they
// stop moving after releasing a direction key.
self.position.local *=
math::translate_along(&((*self.velocity + old_velocity) * 0.5 * self.dt_seconds));
let expected_displacement = (*self.velocity + old_velocity) * 0.5 * self.dt_seconds;

// Update position with collision checking
let collision_checking_result = self.check_collision(&expected_displacement);
self.position.local *= collision_checking_result.allowed_displacement;
if let Some(collision) = collision_checking_result.collision {
*self.velocity = na::Vector3::zeros();
// We are not using collision normals yet, so print them to the console to allow
// sanity checking. Note that the "orientation" quaternion is not used here, so the
// numbers will only make sense if the character doesn't look around.
info!("Collision: normal = {:?}", collision.normal);
}
}

// Renormalize
Expand All @@ -77,4 +89,88 @@ impl CharacterControllerPass<'_> {
self.position.local = transition_xf * self.position.local;
}
}

/// Checks for collisions when a character moves with a character-relative displacement vector of `relative_displacement`.
fn check_collision(&self, relative_displacement: &na::Vector3<f32>) -> CollisionCheckingResult {
// Split relative_displacement into its norm and a unit vector
let relative_displacement = relative_displacement.to_homogeneous();
let displacement_sqr = relative_displacement.norm_squared();
if displacement_sqr < 1e-16 {
// Fallback for if the displacement vector isn't large enough to reliably be normalized.
// Any value that is sufficiently large compared to f32::MIN_POSITIVE should work as the cutoff.
return CollisionCheckingResult::stationary();
}

let displacement_norm = displacement_sqr.sqrt();
let displacement_normalized = relative_displacement / displacement_norm;

let ray = graph_collision::Ray::new(math::origin(), displacement_normalized);
let tanh_distance = displacement_norm.tanh();

let cast_hit = graph_collision::sphere_cast(
self.cfg.character_radius,
self.graph,
&ChunkLayout::new(self.cfg.chunk_size as usize),
Ralith marked this conversation as resolved.
Show resolved Hide resolved
self.position,
&ray,
tanh_distance,
);

let cast_hit = match cast_hit {
Ok(r) => r,
Err(e) => {
error!("Collision checking returned {:?}", e);
return CollisionCheckingResult::stationary();
}
};

let allowed_displacement = math::translate(
&ray.position,
&math::lorentz_normalize(
&ray.ray_point(
cast_hit
.as_ref()
.map_or(tanh_distance, |hit| hit.tanh_distance),
),
),
);

CollisionCheckingResult {
allowed_displacement,
collision: cast_hit.map(|hit| Collision {
// `CastEndpoint` has its `normal` given relative to the character's original position,
// but we want the normal relative to the character after the character moves to meet the wall.
// This normal now represents a contact point at the origin, so we omit the w-coordinate
// to ensure that it's orthogonal to the origin.
normal: na::UnitVector3::new_normalize(
(math::mtranspose(&allowed_displacement) * hit.normal).xyz(),
),
}),
}
}
}

struct CollisionCheckingResult {
/// Multiplying the character's position by this matrix will move the character as far as it can up to its intended
/// displacement until it hits the wall.
allowed_displacement: na::Matrix4<f32>,
collision: Option<Collision>,
}

struct Collision {
/// This collision normal faces away from the collision surface and is given in the perspective of the character
/// _after_ it is transformed by `allowed_displacement`. The 4th coordinate of this normal vector is assumed to be
/// 0.0 and is therefore omitted.
normal: na::UnitVector3<f32>,
}

impl CollisionCheckingResult {
/// Return a CollisionCheckingResult with no movement and no collision; useful if the character is not moving
/// and has nothing to check collision against. Also useful as a last resort fallback if an unexpected error occurs.
fn stationary() -> CollisionCheckingResult {
CollisionCheckingResult {
allowed_displacement: na::Matrix4::identity(),
collision: None,
}
}
}
Loading