Skip to content

Commit

Permalink
Add graph traversal to collision checking so it works for all chunks
Browse files Browse the repository at this point in the history
  • Loading branch information
patowen committed Feb 18, 2023
1 parent f79641c commit d27ef8e
Show file tree
Hide file tree
Showing 3 changed files with 162 additions and 45 deletions.
10 changes: 9 additions & 1 deletion common/src/character_controller.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use tracing::info;
use tracing::{error, info};

use crate::{
collision,
Expand Down Expand Up @@ -118,6 +118,14 @@ impl CharacterControllerPass<'_> {
displacement_norm.tanh(),
);

let ray_endpoint = match ray_endpoint {
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(ray_endpoint.tanh_distance)),
Expand Down
155 changes: 113 additions & 42 deletions common/src/collision.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use std::collections::{HashSet, VecDeque};

use crate::{
dodeca::Vertex,
dodeca::{self, Vertex},
math,
node::{Chunk, ChunkId, DualGraph, VoxelData},
sphere_collider::chunk_shape_cast,
Expand All @@ -13,8 +15,6 @@ use crate::{
/// system of `start_chunk`'s node.
///
/// The `tanh_distance` is the hyperbolic tangent of the distance along the ray to check for hits.
///
/// This implementaion is incomplete, and it will only detect intersection with voxels in `start_chunk`.
pub fn shape_cast(
graph: &DualGraph,
dimension: usize,
Expand All @@ -23,54 +23,122 @@ pub fn shape_cast(
start_node_transform: na::Matrix4<f32>,
ray: &Ray,
tanh_distance: f32,
) -> RayEndpoint {
) -> Result<RayEndpoint, RayTracingError> {
// A collision check is assumed to be a miss until a collision is found.
// This `endpoint` variable gets updated over time before being returned.
let mut endpoint = RayEndpoint {
tanh_distance,
hit: None,
};

// Start a breadth-first search of the graph's chunks, performing collision checks in each relevant chunk.
// The `chunk_queue` contains ordered pairs containing the `ChunkId` and the transformation needed to switch
// from the original node coordinates to the current chunk's node coordinates.
let mut visited_chunks: HashSet<ChunkId> = HashSet::new();
let mut chunk_queue: VecDeque<(ChunkId, na::Matrix4<f32>)> = VecDeque::new();
chunk_queue.push_back((start_chunk, start_node_transform));

// Precalculate the chunk boundaries for collision purposes. If the collider goes outside these bounds,
// the corresponding neighboring chunk will also be used for collision checking.
let klein_lower_boundary = collider_radius.tanh();
let klein_upper_boundary =
((Vertex::chunk_to_dual_factor() as f32).atanh() - collider_radius).tanh();

// Breadth-first search loop
let node = graph.get(start_chunk.node).as_ref().unwrap();
let Chunk::Populated {
voxels: ref voxel_data,
..
} = node.chunks[start_chunk.vertex] else {
// Collision checking on unpopulated chunk
panic!("Collision checking on unpopulated chunk");
};
let local_ray = start_chunk.vertex.node_to_dual().cast::<f32>() * start_node_transform * ray;

let dual_to_grid_factor = Vertex::dual_to_chunk_factor() as f32 * dimension as f32;

let bounding_box = VoxelAABB::from_ray_segment_and_radius(
dimension,
dual_to_grid_factor,
&local_ray,
endpoint.tanh_distance,
collider_radius,
);

// Check collision within a single chunk
if let Some(bounding_box) = bounding_box {
chunk_shape_cast(
&ChunkShapeCastingContext {
dimension,
dual_to_grid_factor,
chunk: start_chunk,
transform: math::mtranspose(&start_node_transform)
* start_chunk.vertex.dual_to_node().cast(),
voxel_data,
collider_radius,
ray: &local_ray,
bounding_box,
},
&mut endpoint,
while let Some((chunk, node_transform)) = chunk_queue.pop_front() {
let node = graph.get(chunk.node).as_ref().unwrap();
let Chunk::Populated {
voxels: ref voxel_data,
..
} = node.chunks[chunk.vertex] else {
// Collision checking on unpopulated chunk
return Err(RayTracingError::OutOfBounds);
};
let local_ray = chunk.vertex.node_to_dual().cast::<f32>() * node_transform * ray;

let dual_to_grid_factor = Vertex::dual_to_chunk_factor() as f32 * dimension as f32;

let bounding_box = VoxelAABB::from_ray_segment_and_radius(
dimension,
dual_to_grid_factor,
&local_ray,
endpoint.tanh_distance,
collider_radius,
);

// Check collision within a single chunk
if let Some(bounding_box) = bounding_box {
chunk_shape_cast(
&ChunkShapeCastingContext {
dimension,
dual_to_grid_factor,
chunk,
transform: math::mtranspose(&node_transform)
* chunk.vertex.dual_to_node().cast(),
voxel_data,
collider_radius,
ray: &local_ray,
bounding_box,
},
&mut endpoint,
);
}

// Compute the Klein-Beltrami coordinates of the ray segment's endpoints. To check whether neighboring chunks
// are needed, we need to check whether the endpoints of the line segments lie outside the boundaries of the square
// bounded by `klein_lower_boundary` and `klein_upper_boundary`.
let klein_ray_start = na::Point3::from_homogeneous(local_ray.position).unwrap();
let klein_ray_end =
na::Point3::from_homogeneous(local_ray.ray_point(endpoint.tanh_distance)).unwrap();

// Add neighboring chunks as necessary, using one coordinate at a time.
for axis in 0..3 {
// Check for neighboring nodes
if klein_ray_start[axis] <= klein_lower_boundary
|| klein_ray_end[axis] <= klein_lower_boundary
{
let side = chunk.vertex.canonical_sides()[axis];
let next_node_transform = side.reflection().cast::<f32>() * node_transform;
// Crude check to ensure that the neighboring chunk's node can be in the path of the ray. For simplicity, this
// check treats each node as a sphere and assumes the ray is pointed directly towards its center. The check is
// needed because chunk generation uses this approximation, and this check is not guaranteed to pass near corners.
let ray_node_distance = (next_node_transform * ray.position)
.xyz()
.magnitude()
.acosh();
let ray_length = endpoint.tanh_distance.atanh();
if ray_node_distance - ray_length
> dodeca::BOUNDING_SPHERE_RADIUS as f32 + collider_radius
{
// Ray cannot intersect node
continue;
}
// If we have to do collision checking on nodes that don't exist in the graph, we cannot have a conclusive result.
let Some(neighbor) = graph.neighbor(chunk.node, side) else {
// Collision checking on nonexistent node
return Err(RayTracingError::OutOfBounds);
};
// Assuming everything goes well, add the new chunk to the queue.
let next_chunk = ChunkId::new(neighbor, chunk.vertex);
if visited_chunks.insert(next_chunk) {
chunk_queue.push_back((next_chunk, next_node_transform));
}
}

// Check for neighboring chunks within the same node
if klein_ray_start[axis] >= klein_upper_boundary
|| klein_ray_end[axis] >= klein_upper_boundary
{
let vertex = chunk.vertex.adjacent_vertices()[axis];
let next_chunk = ChunkId::new(chunk.node, vertex);
if visited_chunks.insert(next_chunk) {
chunk_queue.push_back((next_chunk, node_transform));
}
}
}
}

endpoint
Ok(endpoint)
}

/// Contains all the immutable data needed for `ChunkShapeCaster` to perform its logic
Expand Down Expand Up @@ -114,6 +182,11 @@ pub struct RayEndpoint {
pub hit: Option<RayHit>,
}

#[derive(Debug)]
pub enum RayTracingError {
OutOfBounds,
}

/// Information about the intersection at the end of a ray segment.
#[derive(Debug)]
pub struct RayHit {
Expand Down Expand Up @@ -252,8 +325,6 @@ impl VoxelAABB {

#[cfg(test)]
mod tests {
use std::collections::HashSet;

use crate::dodeca::Vertex;

use super::*;
Expand Down
42 changes: 40 additions & 2 deletions common/src/dodeca.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,12 @@ impl Vertex {
VERTEX_SIDES[self as usize]
}

/// Vertices adjacent to this vertex, opposite the sides in canonical order
#[inline]
pub fn adjacent_vertices(self) -> [Vertex; 3] {
ADJACENT_VERTICES[self as usize]
}

/// For each vertex of the cube dual to this dodecahedral vertex, provides an iterator of at
/// most 3 steps to reach the corresponding graph node, and binary coordinates of the vertex in
/// question with respect to the origin vertex of the cube.
Expand Down Expand Up @@ -155,7 +161,14 @@ impl Vertex {
/// Scaling the x, y, and z components of a vector in cube-centric coordinates by this value
/// and dividing them by the w coordinate will yield euclidean chunk coordinates.
pub fn dual_to_chunk_factor() -> f64 {
2.0581710272714924 // sqrt(2 + sqrt(5))
*DUAL_TO_CHUNK_FACTOR
}

/// Scale factor used in conversion from euclidean chunk coordinates to cube-centric coordinates.
/// Scaling the x, y, and z components of a vector in homogeneous euclidean chunk coordinates by this value
/// and lorentz-normalizing the result will yield cube-centric coordinates.
pub fn chunk_to_dual_factor() -> f64 {
*CHUNK_TO_DUAL_FACTOR
}

/// Convenience method for `self.chunk_to_node().determinant() < 0`.
Expand All @@ -166,7 +179,6 @@ impl Vertex {

pub const VERTEX_COUNT: usize = 20;
pub const SIDE_COUNT: usize = 12;
#[allow(clippy::unreadable_literal)]
pub const BOUNDING_SPHERE_RADIUS: f64 = 1.2264568712514068;

lazy_static! {
Expand Down Expand Up @@ -231,6 +243,29 @@ lazy_static! {
result
};

// Which vertices are adjacent to other vertices and opposite the canonical sides
static ref ADJACENT_VERTICES: [[Vertex; 3]; VERTEX_COUNT] = {
let mut result = [[Vertex::A; 3]; VERTEX_COUNT];

for vertex in 0..VERTEX_COUNT {
for result_index in 0..3 {
let mut test_sides = VERTEX_SIDES[vertex];
// Keep modifying the result_index'th element of test_sides until its three elements are all
// adjacent to a single vertex. That vertex is the vertex we're looking for.
for side in Side::iter() {
if side == VERTEX_SIDES[vertex][result_index] {
continue;
}
test_sides[result_index] = side;
if let Some(adjacent_vertex) = Vertex::from_sides(test_sides[0], test_sides[1], test_sides[2]) {
result[vertex][result_index] = adjacent_vertex;
}
}
}
}
result
};

/// Transform that converts from cube-centric coordinates to dodeca-centric coordinates
static ref DUAL_TO_NODE: [na::Matrix4<f64>; VERTEX_COUNT] = {
let mip_origin_normal = math::mip(&math::origin(), &SIDE_NORMALS[0]); // This value is the same for every side
Expand All @@ -250,6 +285,9 @@ lazy_static! {
DUAL_TO_NODE.map(|m| math::mtranspose(&m))
};

static ref DUAL_TO_CHUNK_FACTOR: f64 = (2.0 + 5.0f64.sqrt()).sqrt();
static ref CHUNK_TO_DUAL_FACTOR: f64 = 1.0 / *DUAL_TO_CHUNK_FACTOR;

/// Vertex shared by 3 sides
static ref SIDES_TO_VERTEX: [[[Option<Vertex>; SIDE_COUNT]; SIDE_COUNT]; SIDE_COUNT] = {
let mut result = [[[None; SIDE_COUNT]; SIDE_COUNT]; SIDE_COUNT];
Expand Down

0 comments on commit d27ef8e

Please sign in to comment.