diff --git a/client/src/sim.rs b/client/src/sim.rs index ec36301c..26e72d5b 100644 --- a/client/src/sim.rs +++ b/client/src/sim.rs @@ -6,15 +6,21 @@ use tracing::{debug, error, trace}; use crate::{net, prediction::PredictedMotion, Net}; use common::{ + collision::BoundingBox, + force::{ForceParams, GravityMethod}, graph::{Graph, NodeId}, math, - node::{DualGraph, Node}, + node::{Chunk, DualGraph, Node}, proto::{self, Character, Command, Component, Position}, sanitize_motion_input, + world::Material, worldgen::NodeState, Chunks, EntityId, GraphEntities, Step, }; +const CHARACTER_RADIUS: f64 = 0.10_f64; +const CHARACTER_SLOWDOWN_FACTOR: f32 = 6_f32; + /// Game state pub struct Sim { net: Net, @@ -26,6 +32,7 @@ pub struct Sim { pub world: hecs::World, pub params: Option, pub local_character: Option, + character_velocity: na::Vector4, // velocity of the character controller that carries over from frame to frame. orientation: na::UnitQuaternion, step: Option, @@ -54,6 +61,7 @@ impl Sim { world: hecs::World::new(), params: None, local_character: None, + character_velocity: na::Vector4::zeros(), orientation: na::one(), step: None, @@ -86,6 +94,8 @@ impl Sim { self.handle_net(msg); } + self.handle_forces(dt); + if let Some(step_interval) = self.params.as_ref().map(|x| x.step_interval) { self.since_input_sent += dt; if let Some(overflow) = self.since_input_sent.checked_sub(step_interval) { @@ -135,6 +145,9 @@ impl Sim { chunk_size: msg.chunk_size, meters_to_absolute: msg.meters_to_absolute, movement_speed: msg.movement_speed, + gravity_intensity: msg.gravity_intensity, + drag_factor: msg.drag_factor, + gravity_method: msg.gravity_method, }); // Populate the root node populate_fresh_nodes(&mut self.graph); @@ -146,8 +159,8 @@ impl Sim { return; } self.step = Some(msg.step); - for &(id, new_pos) in &msg.positions { - self.update_position(msg.latest_input, id, new_pos); + for &(id, new_pos, node_transform) in &msg.positions { + self.update_position(msg.latest_input, id, new_pos, node_transform); } for &(id, orientation) in &msg.character_orientations { match self.entity_ids.get(&id) { @@ -166,14 +179,29 @@ impl Sim { } } - fn update_position(&mut self, latest_input: u16, id: EntityId, new_pos: Position) { - if self.params.as_ref().map_or(false, |x| x.character_id == id) { + fn update_position( + &mut self, + latest_input: u16, + id: EntityId, + new_pos: Position, + node_transform: na::Matrix4, + ) { + let id_is_character = self.params.as_ref().map_or(false, |x| x.character_id == id); + + if id_is_character { self.prediction.reconcile(latest_input, new_pos); } + match self.entity_ids.get(&id) { None => debug!(%id, "position update for unknown entity"), Some(&entity) => match self.world.get_mut::(entity) { Ok(mut pos) => { + if id_is_character { + let p0 = node_transform * pos.local * math::origin(); + let p1 = new_pos.local * math::origin(); + self.character_velocity = + math::translate(&p0, &p1) * node_transform * self.character_velocity; + } if pos.node != new_pos.node { self.graph_entities.remove(pos.node, entity); self.graph_entities.insert(new_pos.node, entity); @@ -241,8 +269,24 @@ impl Sim { } fn send_input(&mut self) { - let (direction, speed) = sanitize_motion_input(self.orientation * self.average_velocity); let params = self.params.as_ref().unwrap(); + let local_velocity = self + .world + .get_mut::(self.local_character.unwrap()) + .unwrap() + .local + .try_inverse() + .unwrap() + * self.character_velocity; + let (direction, speed) = sanitize_motion_input( + self.orientation * self.average_velocity / CHARACTER_SLOWDOWN_FACTOR // lower player abilities + + na::Vector3::new( + local_velocity[0], + local_velocity[1], + local_velocity[2], + ) / params.movement_speed, + ); + let generation = self.prediction.push( &direction, speed @@ -263,7 +307,10 @@ impl Sim { result.local *= self.orientation.to_homogeneous(); if let Some(ref params) = self.params { // Apply input that hasn't been sent yet - let (direction, speed) = sanitize_motion_input(self.average_velocity); + let (direction, speed) = sanitize_motion_input( + // TODO: Incorparate the persistent velocity into the view calculations. + self.average_velocity / CHARACTER_SLOWDOWN_FACTOR, + ); // We multiply by the entire timestep rather than the time so far because // self.average_velocity is always over the entire timestep, filling in zeroes for the // future. @@ -292,6 +339,116 @@ impl Sim { .despawn(entity) .expect("destroyed nonexistent entity"); } + + fn handle_forces(&mut self, time: Duration) { + let params = self.params.as_ref().unwrap(); + + let force_system = ForceParams { + gravity_intensity: params.gravity_intensity, + air_drag_factor: params.drag_factor, + gravity_type: params.gravity_method, + float_speed: 0.05_f64, + }; + + // eventually this should be expanded to work on every entity with a physics property, but for now it is just the player + match self.local_character { + Some(entity) => match self.world.get_mut::(entity) { + Ok(character_position) => { + // starting with simpler method for testing purposese + let is_colliding = self.check_collision(*character_position); + + let down_info = &self + .graph + .get(character_position.node) + .as_ref() + .unwrap() + .state; + + let character_v4f64 = na::convert::<_, na::Vector4>( + character_position.local * math::origin(), + ); + + let down = -math::lorentz_normalize(&math::orthogonalize( + down_info.surface().normal(), + &character_v4f64, + )); + + let down_local = self.orientation.conjugate() + * (character_position.local.try_inverse().unwrap() + * na::convert::<_, na::Vector4>(-down)) + .xyz(); + self.orientation *= na::UnitQuaternion::new( + na::Vector3::z() * down_local.x * -3.0 * time.as_secs_f32(), + ); + + let height = down_info.surface().distance_to(&character_v4f64); + + let new_velocity = force_system.apply_forces( + &na::convert::<_, na::Vector4>(self.character_velocity), + &down, + height, + is_colliding, + time.as_secs_f64(), + ); + + self.character_velocity = math::normalize_vector( + character_position.local, + na::convert::<_, na::Vector4>(new_velocity), + ); + + for n in self.character_velocity.iter() { + assert!(!n.is_nan(), "Error, character velocity is not a number"); + } + } + Err(_e) => (), + }, + None => (), + } + } + + /// checks to see if there are any non-Void voxels near Position. + // acts as a helper function for handle_forces + // currently uses a hyperbolic equivilent of an AABB. + fn check_collision(&self, pos: Position) -> bool { + let params = self.params.as_ref().unwrap(); + + let bb = BoundingBox::create_aabb( + pos.node, + na::convert::, na::Vector4>( + pos.local * na::Vector4::new(0_f32, 0_f32, 0_f32, 1_f32), + ), + CHARACTER_RADIUS, + &self.graph, + params.chunk_size, + ); + + for cbb in bb.bounding_boxes { + if match self + .graph + .get(cbb.node) + .as_ref() + .expect("nodes must be populated before collisons can occur") + .chunks[cbb.chunk] + { + Chunk::Generating => true, + Chunk::Fresh => true, + Chunk::Populated { + surface: _, + ref voxels, + } => { + for voxel_address in cbb.every_voxel() { + if !matches!(voxels.get(voxel_address as usize), Material::Void) { + return true; + } + } + false + } + } { + return true; + } + } + false + } } /// Simulation details received on connect @@ -302,6 +459,9 @@ pub struct Parameters { /// Absolute units pub movement_speed: f32, pub character_id: EntityId, + pub gravity_intensity: f64, + pub drag_factor: f64, + pub gravity_method: GravityMethod, } fn populate_fresh_nodes(graph: &mut DualGraph) { diff --git a/common/src/collision.rs b/common/src/collision.rs new file mode 100644 index 00000000..c23a084b --- /dev/null +++ b/common/src/collision.rs @@ -0,0 +1,482 @@ +use crate::node::DualGraph; +use crate::{dodeca::Vertex, graph::NodeId}; +use std::fmt; + +/* +The set of voxels that a collision body covers within a chunk +*/ +#[derive(Clone, Copy)] +pub struct ChunkBoundingBox { + pub node: NodeId, + pub chunk: Vertex, + pub min_xyz: na::Vector3, + pub max_xyz: na::Vector3, + pub dimension: u8, +} + +pub struct VoxelAddress { + pub node: NodeId, + pub chunk: Vertex, + pub index: u32, +} + +/* +The set of voxels that a collision body covers. +*/ +pub struct BoundingBox { + pub bounding_boxes: Vec, +} + +/// from a node coordinate in an arbitrary node, returns the chunk the point would reside in. +/// Does not make any attempt to verify the location is within the node in question. +/// guaranteed to return something if this precondition is met. +fn chunk_from_location(location: na::Vector4) -> Vertex { + let epsilon = 0.001; + for v in Vertex::iter() { + let pos = (v.node_to_chunk() * location).xyz(); + if (pos.x >= -epsilon) && (pos.y >= -epsilon) && (pos.z >= -epsilon) { + return v; + } + } + panic!(); +} + +// The pattern I use to unwrap the results of neighbor is kind of awkward. +impl BoundingBox { + pub fn create_aabb( + start_node: NodeId, + position: na::Vector4, + radius: f64, + graph: &DualGraph, + dimension: u8, + ) -> Self { + assert!( + radius <= 1.0, + "Error: the radius of a bounding box may not exceed 1 absolute unit." + ); + + let start_chunk = chunk_from_location(position); + let mut bounding_boxes = Vec::::new(); + let sides = start_chunk.canonical_sides(); + + // get BBs for the chunks within the node. + for v in Vertex::iter() { + Self::add_sub_bb( + &mut bounding_boxes, + ChunkBoundingBox::get_chunk_bounding_box( + start_node, v, position, radius, dimension, + ), + ); + } + + // get BBs for foreign chunks sharing a dodeca side + for &side in sides.iter() { + if let Some(node) = graph.neighbor(start_node, side) { + let translated_position = side.reflection() * position; + for v in side.vertices().iter() { + Self::add_sub_bb( + &mut bounding_boxes, + ChunkBoundingBox::get_chunk_bounding_box( + node, + *v, + translated_position, + radius, + dimension, + ), + ); + } + } + } + + if let Some(opposite_node) = graph.opposing_node(start_node, start_chunk) { + let opposite_position = + sides[0].reflection() * sides[1].reflection() * sides[2].reflection() * position; + + // get BBs for the chunks sharing an edge + for &side in sides.iter() { + let node = graph.neighbor(opposite_node, side); + let translated_position = side.reflection() * opposite_position; + // the chunk next to the origin chunk + Self::add_sub_bb( + &mut bounding_boxes, + ChunkBoundingBox::get_chunk_bounding_box( + node.unwrap(), + start_chunk, + translated_position, + radius, + dimension, + ), + ); + // the chunk on the opposite side of the edge + Self::add_sub_bb( + &mut bounding_boxes, + ChunkBoundingBox::get_chunk_bounding_box( + node.unwrap(), + side.perpendicular_vertex(start_chunk).unwrap(), + translated_position, + radius, + dimension, + ), + ); + } + + // get BB for the chunk sharing only a vertex. + Self::add_sub_bb( + &mut bounding_boxes, + ChunkBoundingBox::get_chunk_bounding_box( + opposite_node, + start_chunk, + opposite_position, + radius, + dimension, + ), + ); + } + + BoundingBox { bounding_boxes } + } + + // adds a sub-bounding box to a list if it exists + fn add_sub_bb(list: &mut Vec, result: Option) { + if let Some(x) = result { + list.push(x); + } + } + + pub fn every_voxel_address(&self) -> impl Iterator + '_ { + self.bounding_boxes.iter().flat_map(|cbb| { + cbb.every_voxel().map(move |index| VoxelAddress { + node: cbb.node, + chunk: cbb.chunk, + index, + }) + }) + } +} + +// translated_position should be the object position in the node coordinates of the chunk. +// node can be easily factored out if it stops being convienent. +impl ChunkBoundingBox { + pub fn get_chunk_bounding_box( + node: NodeId, + chunk: Vertex, + translated_position: na::Vector4, + radius: f64, + dimension: u8, + ) -> Option { + let euclidean_position = { + let temp = chunk.node_to_chunk() * translated_position; + temp.xyz() / temp[3] + }; + + let mut min_xyz = na::Vector3::::new(0_u32, 0_u32, 0_u32); + let mut max_xyz = na::Vector3::::new(0_u32, 0_u32, 0_u32); + + // It's important to note that euclidean_position is measured as chunk lengths, and radius is measured in absolute units. + // By coincidence, an absolute unit is approximately a chunk's diameter, and only because of that there is no unit conversion here. + + // verify at least one box corner is within the chunk + if euclidean_position + .iter() + .all(|n| n + radius > 0_f64 && n - radius < 1_f64) + { + min_xyz.x = + ((euclidean_position.x - radius).max(0_f64) * dimension as f64).floor() as u32; + max_xyz.x = + ((euclidean_position.x + radius).min(1_f64) * dimension as f64).ceil() as u32; + + min_xyz.y = + ((euclidean_position.y - radius).max(0_f64) * dimension as f64).floor() as u32; + max_xyz.y = + ((euclidean_position.y + radius).min(1_f64) * dimension as f64).ceil() as u32; + + min_xyz.z = + ((euclidean_position.z - radius).max(0_f64) * dimension as f64).floor() as u32; + max_xyz.z = + ((euclidean_position.z + radius).min(1_f64) * dimension as f64).ceil() as u32; + Some(ChunkBoundingBox { + node, + chunk, + min_xyz, + max_xyz, + dimension, + }) + } else { + None + } + } + + pub fn every_voxel(&self) -> impl Iterator + '_ { + let lwm = (self.dimension as u32) + 2; + (self.min_xyz[2]..self.max_xyz[2]).flat_map(move |z| { + (self.min_xyz[1]..self.max_xyz[1]).flat_map(move |y| { + (self.min_xyz[0]..self.max_xyz[0]) + .map(move |x| (x + 1) + lwm * (y + 1) + lwm.pow(2) * (z + 1)) + }) + }) + } +} + +impl fmt::Debug for BoundingBox { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Bounding Box") + .field("Chunk components", &self.bounding_boxes) + .finish() + } +} + +impl fmt::Debug for ChunkBoundingBox { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Chunk Bounding Box") + .field("chunk", &self.chunk) + .field("spanning from", &self.min_xyz.data) + .field("spanning to", &self.max_xyz.data) + .finish() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{graph::Graph, proto::Position, traversal::ensure_nearby}; + + const CHUNK_SIZE: u8 = 12_u8; // might want to test with multiple values in the future. + static CHUNK_SIZE_F: f64 = CHUNK_SIZE as f64; + + fn proper_chunks_generic(x: f64, y: f64, z: f64, expected_chunks: usize) { + let mut graph = Graph::new(); + ensure_nearby(&mut graph, &Position::origin(), 4.0); + let central_chunk = Vertex::B; // arbitrary vertex + let position = central_chunk.chunk_to_node() * na::Vector4::new(x, y, z, 1.0); + + let bb = BoundingBox::create_aabb( + NodeId::ROOT, + position, + 1.6 / CHUNK_SIZE_F, + &graph, + CHUNK_SIZE, + ); + + assert_eq!(bb.bounding_boxes.len(), expected_chunks); + } + + // place a small bounding box near the center of the node. There should be exactly 20 chunks in contact. + #[test] + fn proper_chunks_20() { + proper_chunks_generic( + 0.4 / CHUNK_SIZE_F, + 0.4 / CHUNK_SIZE_F, + 0.4 / CHUNK_SIZE_F, + 20, + ); + } + + // place a small bounding box in the center of a chunk. There should be exactly 1 chunk in contact. + #[test] + fn proper_chunks_1() { + proper_chunks_generic(0.5, 0.5, 0.5, 1) + } + + // place a small bounding box next to a dodecaherdral vertex. There should be exactly 8 chunks in contact. + #[test] + fn proper_chunks_8() { + proper_chunks_generic( + 1.0 - 0.4 / CHUNK_SIZE_F, + 1.0 - 0.4 / CHUNK_SIZE_F, + 1.0 - 0.4 / CHUNK_SIZE_F, + 8, + ); + } + + // place a small bounding box next to a dodecaherdral vertex. All chunks in contact should be of the same index + #[test] + fn proper_vertex_ids_corner() { + let mut graph = Graph::new(); + ensure_nearby(&mut graph, &Position::origin(), 4.0); + + let central_chunk = Vertex::C; // arbitrary vertex + let position = central_chunk.chunk_to_node() + * na::Vector4::new( + 1.0 - 0.4 / CHUNK_SIZE_F, + 1.0 - 0.4 / CHUNK_SIZE_F, + 1.0 - 0.4 / CHUNK_SIZE_F, + 1.0, + ); + + let bb = BoundingBox::create_aabb( + NodeId::ROOT, + position, + 2.0 / CHUNK_SIZE_F, + &graph, + CHUNK_SIZE, + ); + + for cbb in bb.bounding_boxes { + assert!(cbb.chunk == central_chunk); + } + } + + // place a small bounding box on the center of a dodecaherdral face. There should be exactly 10 chunks in contact. + #[test] + fn proper_chunks_10() { + proper_chunks_generic( + 1.0 - 0.5 / CHUNK_SIZE_F, + 0.25 / CHUNK_SIZE_F, + 0.25 / CHUNK_SIZE_F, + 10, + ); + } + + // place a small bounding box right between the center of a dodecaherdral face and the node center. There should be exactly 5 chunks in contact. + #[test] + fn proper_chunks_5() { + proper_chunks_generic(0.4 / CHUNK_SIZE_F, 0.4 / CHUNK_SIZE_F, 0.5, 5); + } + + // place bounding boxes in a variety of places with a variety of sizes and make sure the amount of voxels contained within are roughly what you would + // expect in a euclidean bounding box of the same radius. + #[test] + fn reasonable_voxel_count() { + let margin_of_error = 1.5; // 1.5 times more voxels than what would be expected. + let radi_to_test = CHUNK_SIZE; // higher number means more test precision. Try to keep it a divisor of CHUNK_SIZE_F. + + let mut graph = Graph::new(); + ensure_nearby(&mut graph, &Position::origin(), 4.0); + + let central_chunk = Vertex::B; // arbitrary vertex + + for x in 0..CHUNK_SIZE { + for r in 1..radi_to_test { + let radius = (r as f64) / (radi_to_test as f64); + let x_f64 = x as f64; + // Getting the correct estimation for the number of voxels can be tricky + let expected_voxel_count = (radius * 2.0 * CHUNK_SIZE_F).powf(3.0); // value to display + let minimum_expected_voxel_count = + ((((radius * CHUNK_SIZE_F) - 1_f64).powf(3.0) / margin_of_error).floor() + * 8_f64) as i32; + let maximum_expected_voxel_count = + ((((radius * CHUNK_SIZE_F) + 1_f64).powf(3.0) * margin_of_error).ceil() + * 20_f64) as i32; + + let position = central_chunk.chunk_to_node() + * na::Vector4::new( + x_f64 / CHUNK_SIZE_F, + x_f64 / CHUNK_SIZE_F, + x_f64 / CHUNK_SIZE_F, + 1.0, + ); + + let bb = + BoundingBox::create_aabb(NodeId::ROOT, position, radius, &graph, CHUNK_SIZE); + + let actual_voxels = bb.every_voxel_address().count() as i32; + + println!( + "actual_voxels for reasonable_voxel_count: {} vs {}(min), {}(expected), {}(max)", + actual_voxels, minimum_expected_voxel_count, expected_voxel_count, maximum_expected_voxel_count + ); + println!( + "\tSpans {} chunks; {} voxels per chunk", + bb.bounding_boxes.len(), + actual_voxels / (bb.bounding_boxes.len() as i32) + ); + println!("\tx_f64 is {} radius is {}", x_f64, radius); + assert!(actual_voxels >= minimum_expected_voxel_count); + assert!(actual_voxels <= maximum_expected_voxel_count); + } + } + } + + // ensures that chunk_from_location has expected behavior + #[test] + fn chunk_from_location_proper_chunk() { + for central_chunk in Vertex::iter() { + let chunk_coords = na::Vector3::new(1_u32, 1_u32, 1_u32); + + let position = central_chunk.chunk_to_node() + * na::Vector4::new( + (chunk_coords[0] as f64) / CHUNK_SIZE_F, + (chunk_coords[1] as f64) / CHUNK_SIZE_F, + (chunk_coords[2] as f64) / CHUNK_SIZE_F, + 1.0, + ); + + assert_eq!(central_chunk, chunk_from_location(position)); + } + } + + // places a bounding box at a certain [x,y,z], and verifies that the voxel at [x, y, z] is contained within the bounding box. + #[test] + fn internal_coordinates() { + let mut graph = Graph::new(); + ensure_nearby(&mut graph, &Position::origin(), 4.0); + + let central_chunk = Vertex::B; // arbitrary vertex + let chunk_coords = na::Vector3::new(2_u32, 6_u32, 9_u32); + + let position = central_chunk.chunk_to_node() + * na::Vector4::new( + (chunk_coords[0] as f64) / CHUNK_SIZE_F, + (chunk_coords[1] as f64) / CHUNK_SIZE_F, + (chunk_coords[2] as f64) / CHUNK_SIZE_F, + 1.0, + ); + + let bb = BoundingBox::create_aabb( + NodeId::ROOT, + position, + 3.0 / CHUNK_SIZE_F, + &graph, + CHUNK_SIZE, + ); + + println!("{:?}", bb); + + let lwm = (CHUNK_SIZE + 2_u8) as u32; + + let expected_index = + chunk_coords[0] + 1 + (chunk_coords[1] + 1) * lwm + (chunk_coords[2] + 1) * lwm.pow(2); + + for address in bb.every_voxel_address() { + if expected_index == address.index { + return; + } + } + panic!(); + } + + #[test] + fn internal_coordinates_tiny() { + let mut graph = Graph::new(); + ensure_nearby(&mut graph, &Position::origin(), 4.0); + + let central_chunk = Vertex::B; // arbitrary vertex + let chunk_coords = na::Vector3::new(1_u32, 1_u32, 1_u32); + + let tiny_chunk_size = 1_u8; + + let position = central_chunk.chunk_to_node() + * na::Vector4::new( + (chunk_coords[0] as f64) / 1.0_f64, + (chunk_coords[1] as f64) / 1.0_f64, + (chunk_coords[2] as f64) / 1.0_f64, + 1.0, + ); + + let bb = BoundingBox::create_aabb(NodeId::ROOT, position, 0.3, &graph, 1); + + println!("{:?}", bb); + + let lwm = (tiny_chunk_size + 2_u8) as u32; + + //let expected_index = chunk_coords[0] + 1 + (chunk_coords[1] + 1) * lwm + (chunk_coords[2] + 1)*lwm.pow(2); + let expected_index = chunk_coords[0] + chunk_coords[1] * lwm + chunk_coords[2] * lwm.pow(2); + + for address in bb.every_voxel_address() { + if expected_index == address.index { + return; + } + } + panic!(); + } +} diff --git a/common/src/dodeca.rs b/common/src/dodeca.rs index c0609f39..ebcae784 100644 --- a/common/src/dodeca.rs +++ b/common/src/dodeca.rs @@ -43,6 +43,12 @@ impl Side { ADJACENT[self as usize][other as usize] } + /// All vertices incident on self + #[inline] + pub fn vertices(self) -> [Vertex; 5] { + SIDE_VERTICES[self as usize] + } + /// Reflection across this side #[inline] pub fn reflection(self) -> &'static na::Matrix4 { @@ -55,6 +61,12 @@ impl Side { let r = na::convert::<_, na::RowVector4>(self.reflection().row(3).clone_owned()); (r * p).x < p.w } + + /// given a vertex v, returns a vertex that is adjacent to v but not incident on self, if such a vertex exists + #[inline] + pub fn perpendicular_vertex(self, v: Vertex) -> Option { + PERPENDICULAR_VERTEX[self as usize][v as usize] + } } /// Vertices of a right dodecahedron @@ -137,6 +149,11 @@ impl Vertex { ]) * na::Matrix4::new_scaling(0.5) } + /// Transform from hyperbolic node space to euclidean chunk coordinates + pub fn node_to_chunk(self) -> na::Matrix4 { + self.chunk_to_node().try_inverse().unwrap() + } + /// Convenience method for `self.chunk_to_node().determinant() < 0`. pub fn parity(self) -> bool { CHUNK_TO_NODE_PARITY[self as usize] @@ -238,6 +255,27 @@ lazy_static! { result }; + /// The 5 vertices incident on a side + static ref SIDE_VERTICES: [[Vertex; 5]; SIDE_COUNT] = { + let mut result_list = vec![Vec::new(); SIDE_COUNT]; + + for v in Vertex::iter() { + let sides = v.canonical_sides(); + result_list[sides[0] as usize].push(v); + result_list[sides[1] as usize].push(v); + result_list[sides[2] as usize].push(v); + } + let mut out: [[Vertex; 5]; SIDE_COUNT] = [[Vertex::A; 5]; SIDE_COUNT]; + + for a in 0..5 { + for b in 0..SIDE_COUNT { + out[b][a] = result_list[b][a]; + } + + } + out + }; + /// Whether the determinant of the cube-to-node transform is negative static ref CHUNK_TO_NODE_PARITY: [bool; VERTEX_COUNT] = { let mut result = [false; VERTEX_COUNT]; @@ -248,6 +286,43 @@ lazy_static! { result }; + + /// given a side s and a vertex v, returns a vertex that is adjacent to v but not + /// incident on s, if such a vertex exists + static ref PERPENDICULAR_VERTEX: [[Option; VERTEX_COUNT]; SIDE_COUNT] = { + let mut result = [[None; VERTEX_COUNT]; SIDE_COUNT]; + + for side in Side::iter() { + let incident_vertices = side.vertices(); + + for vertex in incident_vertices { + // let vertex = incident_vertices[v]; + let mut vertex_counts = [0; VERTEX_COUNT]; + + // count the number of times that vertices appear in all incident sides + let sides_to_tally = vertex.canonical_sides(); + for side_to_tally in sides_to_tally { + // five vertices per side + let vertices_to_count = side_to_tally.vertices(); + for vertex_to_count in vertices_to_count { + vertex_counts[vertex_to_count as usize] += 1; + } + } + + // incident corners are not perpendicular + for &c in side.vertices().iter() {vertex_counts[c as usize] = -1} + + for i in Vertex::iter() { + if vertex_counts[i as usize] == 2 { + result[side as usize][vertex as usize] = Some(i); + break; + } + } + } + } + result + }; + } #[cfg(test)] @@ -306,4 +381,27 @@ mod tests { epsilon = 1e-10 ); } + + #[test] + fn perpendicular_vertex_is_complete() { + let mut error_count = 0_i32; + + for s in 0..SIDE_COUNT { + let side = Side::from_index(s); + let incident_vertices = side.vertices(); + + for vertex in incident_vertices { + println!("side of {:?} and vertex of {:?}", side, vertex); + let result = side.perpendicular_vertex(vertex); + if result.is_some() { + println!("\tresults in {:?}", result.unwrap()); + } else { + println!("\tIs not thought to exist."); + error_count += 1; + } + } + } + + assert!(error_count == 0_i32); + } } diff --git a/common/src/force.rs b/common/src/force.rs new file mode 100644 index 00000000..9b46c053 --- /dev/null +++ b/common/src/force.rs @@ -0,0 +1,67 @@ +use serde::{Deserialize, Serialize}; +/** + * GravityMethod encodes different ways of creating gravity in Hyperbolic space + * + * PlanarConstant has gravity oriented in the "down" direction, and the force of gravity is + * constant with respect to height. + * + * PlanarDivergent has gravity oriented in the "down" direction, and the force of gravity respects + * the divergence theorem, which makes it proportial to the ratio of the area of the ground-plane + * to the area of the surface which is equidistant to the ground-plane at the indicated height. + * + * Zero disables gravity. + */ + +#[derive(Debug, Deserialize, Serialize, Copy, Clone)] +pub enum GravityMethod { + PlanarConstant, + PlanarDivergent, + Zero, +} + +pub struct ForceParams { + pub gravity_intensity: f64, + pub air_drag_factor: f64, // lower number means more drag + pub gravity_type: GravityMethod, + pub float_speed: f64, +} + +impl ForceParams { + /// returns the result of applying appropriate forces to "inital_velocity" over "time" + pub fn apply_forces( + &self, + inital_velocity: &na::Vector4, + down: &na::Vector4, + height: f64, + is_colliding: bool, + time: f64, + ) -> na::Vector4 { + let mut velocity = *inital_velocity; + + // with current collision detection tech, we want the player to slowly rise if they are in the ground + if is_colliding { + return down * -self.float_speed; + } + + // otherwise we apply air resistance and gravity + velocity += self.gravity(down, height, time); + velocity = self.air_drag(&velocity, time); + velocity + } + + /// returns the change in velocity that occurs by pulled by gravity at "height" for "time" + fn gravity(&self, down: &na::Vector4, height: f64, time: f64) -> na::Vector4 { + match self.gravity_type { + GravityMethod::PlanarConstant => down * (self.gravity_intensity * time), + GravityMethod::PlanarDivergent => { + down * (self.gravity_intensity * time / height.cosh().powi(2)) + } + GravityMethod::Zero => na::zero(), + } + } + + /// rewrites velocity to account for time spent under drag. + fn air_drag(&self, inital_velocity: &na::Vector4, time: f64) -> na::Vector4 { + inital_velocity * self.air_drag_factor.powf(time) + } +} diff --git a/common/src/graph.rs b/common/src/graph.rs index ee7cbc2b..c2f0177b 100644 --- a/common/src/graph.rs +++ b/common/src/graph.rs @@ -199,6 +199,15 @@ impl Graph { id } + /// find the node opposite "node" along "vertex" + pub fn opposing_node(&self, node: NodeId, vertex: Vertex) -> Option { + let sides = vertex.canonical_sides(); + self.neighbor( + self.neighbor(self.neighbor(node, sides[0])?, sides[1])?, + sides[2], + ) + } + /// Ensure all shorter neighbors of a not-yet-created child node exist and return them, excluding the given parent node fn populate_shorter_neighbors_of_child( &mut self, diff --git a/common/src/lib.rs b/common/src/lib.rs index 003833bd..1cdb8b5e 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -8,8 +8,10 @@ mod id; mod chunks; pub mod codec; +pub mod collision; pub mod cursor; pub mod dodeca; +pub mod force; pub mod graph; mod graph_entities; pub mod lru_slab; diff --git a/common/src/math.rs b/common/src/math.rs index 5434e410..7dcd80b9 100644 --- a/common/src/math.rs +++ b/common/src/math.rs @@ -105,6 +105,25 @@ pub fn renormalize_isometry(m: &na::Matrix4) -> na::Matrix4 translate_along(&direction, boost_length) * rotation.to_homogeneous() } +/// normalizes vector v with respect to translation matrix t +pub fn normalize_vector(t: na::Matrix4, v: na::Vector4) -> na::Vector4 { + let p = t * origin(); + let m = mip(&v, &v); + if m <= na::zero() { + return t * na::Vector4::::new(na::one(), na::zero(), na::zero(), na::zero()); + } + + let v_mag = m.sqrt(); + let v_norm = v / v_mag; + + (v_norm + p * mip(&p, &v_norm)) * v_mag +} + +/// make a orthogonal to b +pub fn orthogonalize(a: &na::Vector4, b: &na::Vector4) -> na::Vector4 { + a - b * (mip(a, b) / mip(b, b)) +} + #[rustfmt::skip] fn renormalize_rotation_reflection(m: &na::Matrix3) -> na::Matrix3 { let zv = m.index((.., 2)).normalize(); @@ -230,6 +249,12 @@ mod tests { assert_abs_diff_eq!(distance(&p, &m) * 2.0, distance(&p, &q), epsilon = 1e-5); } + #[test] + fn distance_identity() { + let a = na::Vector4::new(0.2, 0.0, 0.3, 1.0); + assert_abs_diff_eq!(distance(&a, &a), 0.0, epsilon = 1e-5); + } + #[test] fn renormalize_translation() { let mat = translate( @@ -248,4 +273,13 @@ mod tests { 0.0, 0.0, 0.0, 1.0); assert_abs_diff_eq!(renormalize_isometry(&mat), mat, epsilon = 1e-5); } + + #[test] + fn orthogonalize_example() { + let vec1 = na::Vector4::new(-0.5, -0.5, 0.0, 1.0); + let vec2 = na::Vector4::new(0.3, -0.7, 0.0, 1.0); + let orth = orthogonalize(&vec1, &vec2); + + assert_abs_diff_eq!(mip(&vec2, &orth), 0.0, epsilon = 1e-5); + } } diff --git a/common/src/proto.rs b/common/src/proto.rs index 208b12b5..a875f240 100644 --- a/common/src/proto.rs +++ b/common/src/proto.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::{dodeca, graph::NodeId, EntityId, Step}; +use crate::{dodeca, force::GravityMethod, graph::NodeId, EntityId, Step}; #[derive(Debug, Serialize, Deserialize)] pub struct ClientHello { @@ -17,6 +17,9 @@ pub struct ServerHello { pub movement_speed: f32, /// Unit conversion factor pub meters_to_absolute: f32, + pub gravity_intensity: f64, + pub drag_factor: f64, + pub gravity_method: GravityMethod, } #[derive(Debug, Serialize, Deserialize, Copy, Clone)] @@ -39,7 +42,8 @@ pub struct StateDelta { pub step: Step, /// Highest input generation received prior to `step` pub latest_input: u16, - pub positions: Vec<(EntityId, Position)>, + /// the last field is the transform required to go from the previous node to the current one. + pub positions: Vec<(EntityId, Position, na::Matrix4)>, pub character_orientations: Vec<(EntityId, na::UnitQuaternion)>, } diff --git a/common/src/sim_config.rs b/common/src/sim_config.rs index 0c245577..05e0112c 100644 --- a/common/src/sim_config.rs +++ b/common/src/sim_config.rs @@ -4,6 +4,8 @@ use serde::{Deserialize, Serialize}; use crate::{dodeca, math}; +use crate::force::GravityMethod; + /// Manually specified simulation config parameters #[derive(Serialize, Deserialize, Default)] #[serde(deny_unknown_fields)] @@ -24,6 +26,9 @@ pub struct SimConfigRaw { pub voxel_size: Option, /// Character movement speed in m/s pub movement_speed: Option, + pub gravity_intensity: Option, + pub drag_factor: Option, + pub gravity_method: Option, } /// Complete simulation config parameters @@ -34,6 +39,9 @@ pub struct SimConfig { pub input_queue_size: Duration, pub chunk_size: u8, pub movement_speed: f32, + pub gravity_intensity: f64, + pub drag_factor: f64, + pub gravity_method: GravityMethod, /// Scaling factor converting meters to absolute units pub meters_to_absolute: f32, } @@ -49,6 +57,10 @@ impl SimConfig { input_queue_size: Duration::from_millis(x.input_queue_size_ms.unwrap_or(50).into()), chunk_size, movement_speed: x.movement_speed.unwrap_or(12.0) * meters_to_absolute, + gravity_intensity: x.gravity_intensity.unwrap_or(0.5), + drag_factor: x.drag_factor.unwrap_or(0.90), + gravity_method: x.gravity_method.unwrap_or(GravityMethod::PlanarConstant), + meters_to_absolute, } } diff --git a/common/src/worldgen.rs b/common/src/worldgen.rs index 1b785f7f..8261acf4 100644 --- a/common/src/worldgen.rs +++ b/common/src/worldgen.rs @@ -128,6 +128,11 @@ impl NodeState { enviro, } } + + /// Returns a Plane representing the ground relative to this Node. + pub fn surface(&self) -> Plane { + self.surface + } } struct VoxelCoords { diff --git a/server/src/lib.rs b/server/src/lib.rs index 2b818e2e..7f98a140 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -150,6 +150,9 @@ impl Server { chunk_size: self.cfg.chunk_size, meters_to_absolute: self.cfg.meters_to_absolute, movement_speed: self.cfg.movement_speed, + gravity_intensity: self.cfg.gravity_intensity, + drag_factor: self.cfg.drag_factor, + gravity_method: self.cfg.gravity_method, }; tokio::spawn(async move { // Errors will be handled by recv task diff --git a/server/src/sim.rs b/server/src/sim.rs index 565a9e0e..e3cf5117 100644 --- a/server/src/sim.rs +++ b/server/src/sim.rs @@ -109,7 +109,12 @@ impl Sim { let _guard = span.enter(); // Simulate - for (_, (ch, pos)) in self.world.query::<(&Character, &mut Position)>().iter() { + let mut node_transforms = FxHashMap::>::default(); + for (_, (ch, pos, id)) in self + .world + .query::<(&Character, &mut Position, &EntityId)>() + .iter() + { let next_xf = pos.local * math::translate_along(&ch.direction, ch.speed / self.cfg.rate as f32); pos.local = math::renormalize_isometry(&next_xf); @@ -117,6 +122,7 @@ impl Sim { if next_node != pos.node { pos.node = next_node; pos.local = transition_xf * pos.local; + node_transforms.insert(*id, transition_xf); } ensure_nearby(&mut self.graph, pos, f64::from(self.cfg.view_distance)); } @@ -157,7 +163,13 @@ impl Sim { .world .query::<(&EntityId, &Position)>() .iter() - .map(|(_, (&id, &position))| (id, position)) + .map(|(_, (&id, &position))| { + ( + id, + position, + *node_transforms.get(&id).unwrap_or(&na::Matrix4::identity()), + ) + }) .collect(), character_orientations: self .world