diff --git a/Cargo.toml b/Cargo.toml index 12def8d9..1f94d986 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ members = ["client", "server", "common"] [profile.dev] opt-level = 1 +debug-assertions = true [profile.dev.package."*"] opt-level = 2 diff --git a/client/Cargo.toml b/client/Cargo.toml index bd0fb882..ffc99e34 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -14,11 +14,12 @@ server = { path = "../server" } tracing = "0.1.10" ash = { version = "0.37.0", features = ["loaded"] } lahar = { git = "https://github.com/Ralith/lahar", rev = "88abd75e41d04c3a4199d95f581cb135f5962844" } -winit = "0.26.1" -ash-window = "0.10.0" +winit = "0.27.2" +ash-window = "0.12.0" +raw-window-handle = "0.5.0" directories = "4.0.1" vk-shader-macros = "0.2.5" -na = { package = "nalgebra", version = "0.19" } +nalgebra = "0.31.2" tokio = { version = "1.18.2", features = ["rt-multi-thread", "sync", "macros"] } png = "0.17.5" anyhow = "1.0.26" @@ -30,13 +31,12 @@ downcast-rs = "1.1.1" quinn = "0.8.3" futures-util = "0.3.1" rustls = { version = "0.20.6", features = ["dangerous_configuration"] } -webpki = "0.21.0" -hecs = "0.7.6" -rcgen = { version = "0.9.2", default-features = false } +webpki = "0.22.0" +hecs = "0.9.0" +rcgen = { version = "0.10.0", default-features = false } memoffset = "0.6" gltf = { version = "1.0.0", default-features = false, features = ["utils"] } -metrics = { version = "0.12.1", features = ["std"] } -metrics-core = "0.5.2" +metrics = { version = "0.20.1" } hdrhistogram = { version = "7", default-features = false } [features] @@ -44,7 +44,7 @@ default = ["use-repo-assets"] use-repo-assets = [] [dev-dependencies] -approx = "0.3.2" +approx = "0.5.1" bencher = "0.1.5" renderdoc = "0.10.1" diff --git a/client/src/graphics/draw.rs b/client/src/graphics/draw.rs index abb4fe45..00ccde7a 100644 --- a/client/src/graphics/draw.rs +++ b/client/src/graphics/draw.rs @@ -4,7 +4,7 @@ use std::time::Instant; use ash::vk; use fxhash::FxHashSet; use lahar::Staged; -use metrics::timing; +use metrics::histogram; use super::{fog, voxels, Base, Fog, Frustum, GltfScene, Meshes, Voxels}; use crate::{sim, Asset, Config, Loader, Sim}; @@ -312,10 +312,12 @@ impl Draw { vk::QueryResultFlags::TYPE_64 | vk::QueryResultFlags::WAIT, ) .unwrap(); - let draw_nanos = self.gfx.limits.timestamp_period * (queries[1] - queries[0]) as f32; - let after_nanos = self.gfx.limits.timestamp_period * (queries[2] - queries[1]) as f32; - timing!("frame.gpu.draw", draw_nanos as u64); - timing!("frame.gpu.after_draw", after_nanos as u64); + let draw_seconds = + self.gfx.limits.timestamp_period as f64 * 1e-9 * (queries[1] - queries[0]) as f64; + let after_seconds = + self.gfx.limits.timestamp_period as f64 * 1e-9 * (queries[2] - queries[1]) as f64; + histogram!("frame.gpu.draw", draw_seconds); + histogram!("frame.gpu.after_draw", after_seconds); } device @@ -447,10 +449,10 @@ impl Draw { } let pos = sim .world - .get::(entity) + .get::<&Position>(entity) .expect("positionless entity in graph"); if let Some(character_model) = self.loader.get(self.character_model) { - if let Ok(ch) = sim.world.get::(entity) { + if let Ok(ch) = sim.world.get::<&Character>(entity) { let transform = transform * pos.local * na::Matrix4::new_scaling(params.meters_to_absolute) @@ -516,7 +518,7 @@ impl Draw { .unwrap(); state.used = true; state.in_flight = true; - timing!("frame.cpu", draw_started.elapsed()); + histogram!("frame.cpu", draw_started.elapsed()); } /// Wait for all drawing to complete diff --git a/client/src/graphics/voxels/mod.rs b/client/src/graphics/voxels/mod.rs index 417f2d00..b93d8ef3 100644 --- a/client/src/graphics/voxels/mod.rs +++ b/client/src/graphics/voxels/mod.rs @@ -7,7 +7,7 @@ mod tests; use std::{sync::Arc, time::Instant}; use ash::{vk, Device}; -use metrics::timing; +use metrics::histogram; use tracing::warn; use super::draw::nearby_nodes; @@ -120,7 +120,7 @@ impl Voxels { &view, f64::from(self.config.local_simulation.view_distance), ); - timing!( + histogram!( "frame.cpu.voxels.graph_traversal", graph_traversal_started.elapsed() ); @@ -240,7 +240,7 @@ impl Voxels { cmd, &extractions, ); - timing!("frame.cpu.voxels.node_scan", node_scan_started.elapsed()); + histogram!("frame.cpu.voxels.node_scan", node_scan_started.elapsed()); } pub unsafe fn draw( @@ -265,7 +265,7 @@ impl Voxels { for chunk in &frame.drawn { self.draw.draw(device, cmd, &self.surfaces, chunk.0); } - timing!("frame.cpu.voxels.draw", started.elapsed()); + histogram!("frame.cpu.voxels.draw", started.elapsed()); } pub unsafe fn destroy(&mut self, device: &Device) { diff --git a/client/src/graphics/window.rs b/client/src/graphics/window.rs index 2544a778..fb48fc81 100644 --- a/client/src/graphics/window.rs +++ b/client/src/graphics/window.rs @@ -4,6 +4,7 @@ use std::{f32, os::raw::c_char}; use ash::{extensions::khr, vk}; use lahar::DedicatedImage; +use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; use tracing::info; use winit::{ dpi::PhysicalSize, @@ -11,7 +12,7 @@ use winit::{ DeviceEvent, ElementState, Event, KeyboardInput, MouseButton, VirtualKeyCode, WindowEvent, }, event_loop::{ControlFlow, EventLoop}, - window::{Window as WinitWindow, WindowBuilder}, + window::{CursorGrabMode, Window as WinitWindow, WindowBuilder}, }; use super::{Base, Core, Draw, Frustum}; @@ -35,7 +36,8 @@ impl EarlyWindow { /// Identify the Vulkan extension needed to render to this window pub fn required_extensions(&self) -> &'static [*const c_char] { - ash_window::enumerate_required_extensions(&self.window).expect("unsupported platform") + ash_window::enumerate_required_extensions(self.event_loop.raw_display_handle()) + .expect("unsupported platform") } } @@ -64,7 +66,14 @@ impl Window { sim: Sim, ) -> Self { let surface = unsafe { - ash_window::create_surface(&core.entry, &core.instance, &early.window, None).unwrap() + ash_window::create_surface( + &core.entry, + &core.instance, + early.window.raw_display_handle(), + early.window.raw_window_handle(), + None, + ) + .unwrap() }; let surface_fn = khr::Surface::new(&core.entry, &core.instance); @@ -180,7 +189,10 @@ impl Window { state: ElementState::Pressed, .. } => { - let _ = self.window.set_cursor_grab(true); + let _ = self + .window + .set_cursor_grab(CursorGrabMode::Confined) + .or_else(|_e| self.window.set_cursor_grab(CursorGrabMode::Locked)); self.window.set_cursor_visible(false); mouse_captured = true; } @@ -218,7 +230,7 @@ impl Window { down = state == ElementState::Pressed; } VirtualKeyCode::Escape => { - let _ = self.window.set_cursor_grab(false); + let _ = self.window.set_cursor_grab(CursorGrabMode::None); self.window.set_cursor_visible(true); mouse_captured = false; } @@ -226,7 +238,7 @@ impl Window { }, WindowEvent::Focused(focused) => { if !focused { - let _ = self.window.set_cursor_grab(false); + let _ = self.window.set_cursor_grab(CursorGrabMode::None); self.window.set_cursor_visible(true); mouse_captured = false; } @@ -314,7 +326,7 @@ impl SwapchainMgr { /// Construct a swapchain manager for a certain window fn new(window: &Window, gfx: Arc, fallback_size: PhysicalSize) -> Self { let device = &*gfx.device; - let swapchain_fn = khr::Swapchain::new(&gfx.core.instance, &*device); + let swapchain_fn = khr::Swapchain::new(&gfx.core.instance, device); let surface_formats = unsafe { window .surface_fn diff --git a/client/src/lahar_deprecated/transfer.rs b/client/src/lahar_deprecated/transfer.rs index 445f3ccb..99d1e687 100644 --- a/client/src/lahar_deprecated/transfer.rs +++ b/client/src/lahar_deprecated/transfer.rs @@ -52,6 +52,7 @@ impl fmt::Display for ShutDown { impl std::error::Error for ShutDown {} +#[allow(clippy::type_complexity)] struct Message { sender: oneshot::Sender<()>, op: Box, diff --git a/client/src/lib.rs b/client/src/lib.rs index aada2c8e..5e81b3ce 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -9,6 +9,7 @@ macro_rules! cstr { }}; } +extern crate nalgebra as na; mod config; pub mod graphics; mod lahar_deprecated; diff --git a/client/src/metrics.rs b/client/src/metrics.rs index 24631803..a54bd795 100644 --- a/client/src/metrics.rs +++ b/client/src/metrics.rs @@ -5,7 +5,6 @@ use std::{ }; use hdrhistogram::Histogram; -use metrics_core::Key; use tracing::info; pub fn init() -> Arc { @@ -17,11 +16,13 @@ pub fn init() -> Arc { } pub struct Recorder { - histograms: RwLock>>>, + histograms: RwLock>>>, } impl Recorder { pub fn report(&self) { + // metrics crate documentation assures us that Key's interior mutability does not affect the hash code. + #[allow(clippy::mutable_key_type)] let histograms = &*self.histograms.read().unwrap(); for (key, histogram) in histograms { let histogram = histogram.lock().unwrap(); @@ -40,29 +41,70 @@ impl Recorder { struct ArcRecorder(Arc); impl metrics::Recorder for ArcRecorder { - fn increment_counter(&self, _key: Key, _value: u64) { + fn describe_counter( + &self, + _key: metrics::KeyName, + _unit: Option, + _description: metrics::SharedString, + ) { todo!() } - fn update_gauge(&self, _key: Key, _value: i64) { + fn describe_gauge( + &self, + _key: metrics::KeyName, + _unit: Option, + _description: metrics::SharedString, + ) { todo!() } - fn record_histogram(&self, key: Key, value: u64) { - let mut histograms = self.0.histograms.read().unwrap(); - let mut histogram = match histograms.get(&key) { + fn describe_histogram( + &self, + _key: metrics::KeyName, + _unit: Option, + _description: metrics::SharedString, + ) { + todo!() + } + + fn register_counter(&self, _key: &metrics::Key) -> metrics::Counter { + todo!() + } + + fn register_gauge(&self, _key: &metrics::Key) -> metrics::Gauge { + todo!() + } + + fn register_histogram(&self, key: &metrics::Key) -> metrics::Histogram { + metrics::Histogram::from_arc(Arc::new(Handle { + recorder: self.0.clone(), + key: key.clone(), + })) + } +} + +struct Handle { + recorder: Arc, + key: metrics::Key, +} + +impl metrics::HistogramFn for Handle { + fn record(&self, value: f64) { + let mut histograms = self.recorder.histograms.read().unwrap(); + let mut histogram = match histograms.get(&self.key) { Some(x) => x.lock().unwrap(), None => { drop(histograms); - self.0 + self.recorder .histograms .write() .unwrap() - .insert(key.clone(), Mutex::new(Histogram::new(3).unwrap())); - histograms = self.0.histograms.read().unwrap(); - histograms.get(&key).unwrap().lock().unwrap() + .insert(self.key.clone(), Mutex::new(Histogram::new(3).unwrap())); + histograms = self.recorder.histograms.read().unwrap(); + histograms.get(&self.key).unwrap().lock().unwrap() } }; - histogram.record(value).unwrap(); + histogram.record((value * 1e9) as u64).unwrap(); } } diff --git a/client/src/sim.rs b/client/src/sim.rs index 26e72d5b..ace3a688 100644 --- a/client/src/sim.rs +++ b/client/src/sim.rs @@ -165,7 +165,7 @@ impl Sim { for &(id, orientation) in &msg.character_orientations { match self.entity_ids.get(&id) { None => debug!(%id, "character orientation update for unknown entity"), - Some(&entity) => match self.world.get_mut::(entity) { + Some(&entity) => match self.world.get::<&mut Character>(entity) { Ok(mut ch) => { ch.orientation = orientation; } @@ -194,7 +194,7 @@ impl Sim { match self.entity_ids.get(&id) { None => debug!(%id, "position update for unknown entity"), - Some(&entity) => match self.world.get_mut::(entity) { + Some(&entity) => match self.world.get::<&mut Position>(entity) { Ok(mut pos) => { if id_is_character { let p0 = node_transform * pos.local * math::origin(); @@ -272,7 +272,7 @@ impl Sim { let params = self.params.as_ref().unwrap(); let local_velocity = self .world - .get_mut::(self.local_character.unwrap()) + .get::<&mut Position>(self.local_character.unwrap()) .unwrap() .local .try_inverse() @@ -324,7 +324,7 @@ impl Sim { fn destroy(&mut self, entity: Entity) { let id = *self .world - .get::(entity) + .get::<&EntityId>(entity) .expect("destroyed nonexistent entity"); self.entity_ids.remove(&id); self.destroy_idless(entity); @@ -332,7 +332,7 @@ impl Sim { /// Destroy an entity without an EntityId mapped fn destroy_idless(&mut self, entity: Entity) { - if let Ok(position) = self.world.get::(entity) { + if let Ok(position) = self.world.get::<&Position>(entity) { self.graph_entities.remove(position.node, entity); } self.world @@ -352,7 +352,7 @@ impl Sim { // 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) { + Some(entity) => match self.world.get::<&mut Position>(entity) { Ok(character_position) => { // starting with simpler method for testing purposese let is_colliding = self.check_collision(*character_position); diff --git a/common/Cargo.toml b/common/Cargo.toml index 7aca1658..e72cab4e 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -10,18 +10,18 @@ license = "Apache-2.0 OR Zlib" [dependencies] serde = { version = "1.0.104", features = ["derive"] } -na = { package = "nalgebra", version = "0.19", features = ["serde-serialize"] } +nalgebra = { version = "0.31.2", features = ["rand", "serde-serialize"] } bincode = "1.2.1" anyhow = "1.0.26" quinn = "0.8.3" lazy_static = "1.4.0" fxhash = "0.2.1" tracing = "0.1.10" -hecs = "0.7.6" -tracing-subscriber = { version = "0.2.5", default-features = false, features = ["env-filter", "smallvec", "fmt", "ansi", "chrono", "parking_lot"] } -rand = "0.7.3" -rand_pcg = "0.2.1" -rand_distr = "0.3.0" +hecs = "0.9.0" +tracing-subscriber = { version = "0.3.15", default-features = false, features = ["env-filter", "smallvec", "fmt", "ansi", "time", "parking_lot"] } +rand = "0.8.5" +rand_pcg = "0.3.1" +rand_distr = "0.4.3" [dev-dependencies] -approx = "0.3.2" +approx = "0.5.1" diff --git a/common/src/collision.rs b/common/src/collision.rs index c23a084b..5318d016 100644 --- a/common/src/collision.rs +++ b/common/src/collision.rs @@ -33,8 +33,8 @@ pub struct BoundingBox { 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) { + let pos = v.node_to_chunk() * location; + if (pos.x <= pos.w + epsilon) && (pos.y <= pos.w + epsilon) && (pos.z <= pos.w + epsilon) { return v; } } @@ -264,9 +264,9 @@ mod tests { #[test] fn proper_chunks_20() { proper_chunks_generic( - 0.4 / CHUNK_SIZE_F, - 0.4 / CHUNK_SIZE_F, - 0.4 / CHUNK_SIZE_F, + 1.0 - 0.4 / CHUNK_SIZE_F, + 1.0 - 0.4 / CHUNK_SIZE_F, + 1.0 - 0.4 / CHUNK_SIZE_F, 20, ); } @@ -281,9 +281,9 @@ mod tests { #[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, + 0.4 / CHUNK_SIZE_F, + 0.4 / CHUNK_SIZE_F, + 0.4 / CHUNK_SIZE_F, 8, ); } @@ -297,9 +297,9 @@ mod tests { 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, + 0.4 / CHUNK_SIZE_F, + 0.4 / CHUNK_SIZE_F, + 0.4 / CHUNK_SIZE_F, 1.0, ); @@ -320,9 +320,9 @@ mod tests { #[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, + 0.5 / CHUNK_SIZE_F, + 1.0 - 0.25 / CHUNK_SIZE_F, + 1.0 - 0.25 / CHUNK_SIZE_F, 10, ); } @@ -330,7 +330,7 @@ mod tests { // 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); + proper_chunks_generic(1.0 - 0.4 / CHUNK_SIZE_F, 1.0 - 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 @@ -391,7 +391,7 @@ mod tests { #[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 chunk_coords = na::Vector3::new(0_u32, 0_u32, 0_u32); let position = central_chunk.chunk_to_node() * na::Vector4::new( diff --git a/common/src/dodeca.rs b/common/src/dodeca.rs index ebcae784..0a5d884c 100644 --- a/common/src/dodeca.rs +++ b/common/src/dodeca.rs @@ -49,6 +49,12 @@ impl Side { SIDE_VERTICES[self as usize] } + /// Outward normal vector of this side + #[inline] + pub fn normal(self) -> &'static na::Vector4 { + &SIDE_NORMALS[self as usize] + } + /// Reflection across this side #[inline] pub fn reflection(self) -> &'static na::Matrix4 { @@ -57,7 +63,7 @@ impl Side { /// Whether `p` is opposite the dodecahedron across the plane containing `self` #[inline] - pub fn faces(self, p: &na::Vector4) -> bool { + pub fn is_facing(self, p: &na::Vector4) -> bool { let r = na::convert::<_, na::RowVector4>(self.reflection().row(3).clone_owned()); (r * p).x < p.w } @@ -139,19 +145,29 @@ impl Vertex { /// Transform from euclidean chunk coordinates to hyperbolic node space pub fn chunk_to_node(self) -> na::Matrix4 { - let origin = na::Vector4::new(0.0, 0.0, 0.0, 1.0); - let [a, b, c] = self.canonical_sides(); - na::Matrix4::from_columns(&[ - a.reflection().column(3) - origin, - b.reflection().column(3) - origin, - c.reflection().column(3) - origin, - origin, - ]) * na::Matrix4::new_scaling(0.5) + self.dual_to_node() * na::Matrix4::new_scaling(1.0 / Self::dual_to_chunk_factor()) } /// Transform from hyperbolic node space to euclidean chunk coordinates pub fn node_to_chunk(self) -> na::Matrix4 { - self.chunk_to_node().try_inverse().unwrap() + na::Matrix4::new_scaling(Self::dual_to_chunk_factor()) * self.node_to_dual() + } + + /// Transform from cube-centric coordinates to dodeca-centric coordinates + pub fn dual_to_node(self) -> &'static na::Matrix4 { + &DUAL_TO_NODE[self as usize] + } + + /// Transform from dodeca-centric coordinates to cube-centric coordinates + pub fn node_to_dual(self) -> &'static na::Matrix4 { + &NODE_TO_DUAL[self as usize] + } + + /// Scale factor used in conversion from cube-centric coordinates to euclidean chunk coordinates. + /// 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.0 + 5.0f64.sqrt()).sqrt() } /// Convenience method for `self.chunk_to_node().determinant() < 0`. @@ -180,13 +196,12 @@ lazy_static! { result }; - /// Transform that moves from a neighbor to a reference node, for each side - static ref REFLECTIONS: [na::Matrix4; SIDE_COUNT] = { + /// Vector corresponding to the outer normal of each side + static ref SIDE_NORMALS: [na::Vector4; SIDE_COUNT] = { let phi = 1.25f64.sqrt() + 0.5; // golden ratio - let root_phi = phi.sqrt(); - let f = math::lorentz_normalize(&na::Vector4::new(root_phi, phi * root_phi, 0.0, phi + 2.0)); + let f = math::lorentz_normalize(&na::Vector4::new(1.0, phi, 0.0, phi.sqrt())); - let mut result = [na::zero(); SIDE_COUNT]; + let mut result: [na::Vector4; SIDE_COUNT] = [na::zero(); SIDE_COUNT]; let mut i = 0; for (x, y, z, w) in [ (f.x, f.y, f.z, f.w), @@ -194,19 +209,20 @@ lazy_static! { (f.x, -f.y, -f.z, f.w), (-f.x, -f.y, f.z, f.w), ] - .iter() - .cloned() { - for (x, y, z, w) in [(x, y, z, w), (y, z, x, w), (z, x, y, w)].iter().cloned() { - result[i] = math::translate(&math::origin(), &na::Vector4::new(x, y, z, w)) - * math::euclidean_reflect(&na::Vector4::new(x, y, z, 0.0)) - * math::translate(&math::origin(), &na::Vector4::new(-x, -y, -z, w)); + for (x, y, z, w) in [(x, y, z, w), (y, z, x, w), (z, x, y, w)] { + result[i] = na::Vector4::new(x, y, z, w); i += 1; } } result }; + /// Transform that moves from a neighbor to a reference node, for each side + static ref REFLECTIONS: [na::Matrix4; SIDE_COUNT] = { + SIDE_NORMALS.map(|r| math::reflect(&r)) + }; + /// Sides incident to a vertex, in canonical order static ref VERTEX_SIDES: [[Side; 3]; VERTEX_COUNT] = { let mut result = [[Side::A; 3]; VERTEX_COUNT]; @@ -227,6 +243,25 @@ lazy_static! { result }; + /// Transform that converts from cube-centric coordinates to dodeca-centric coordinates + static ref DUAL_TO_NODE: [na::Matrix4; VERTEX_COUNT] = { + let mip_origin_normal = math::mip(&math::origin(), &SIDE_NORMALS[0]); // This value is the same for every side + let mut result = [na::zero(); VERTEX_COUNT]; + for i in 0..VERTEX_COUNT { + let [a, b, c] = VERTEX_SIDES[i]; + let vertex_position = math::lorentz_normalize( + &(math::origin() - (a.normal() + b.normal() + c.normal()) * mip_origin_normal), + ); + result[i] = na::Matrix4::from_columns(&[-a.normal(), -b.normal(), -c.normal(), vertex_position]); + } + result + }; + + /// Transform that converts from dodeca-centric coordinates to cube-centric coordinates + static ref NODE_TO_DUAL: [na::Matrix4; VERTEX_COUNT] = { + DUAL_TO_NODE.map(|m| m.try_inverse().unwrap()) + }; + /// Vertex shared by 3 sides static ref SIDES_TO_VERTEX: [[[Option; SIDE_COUNT]; SIDE_COUNT]; SIDE_COUNT] = { let mut result = [[[None; SIDE_COUNT]; SIDE_COUNT]; SIDE_COUNT]; @@ -359,16 +394,16 @@ mod tests { } #[test] - fn side_faces() { + fn side_is_facing() { for side in Side::iter() { - assert!(!side.faces::(&math::origin())); - assert!(side.faces(&(side.reflection() * math::origin()))); + assert!(!side.is_facing::(&math::origin())); + assert!(side.is_facing(&(side.reflection() * math::origin()))); } } #[test] fn radius() { - let corner = Vertex::A.chunk_to_node() * na::Vector4::repeat(1.0); + let corner = Vertex::A.chunk_to_node() * math::origin(); assert_abs_diff_eq!( BOUNDING_SPHERE_RADIUS, math::distance(&corner, &math::origin()), @@ -404,4 +439,26 @@ mod tests { assert!(error_count == 0_i32); } + + #[test] + fn chunk_to_node() { + // Chunk coordinates of (1, 1, 1) should be at the center of a dodecahedron. + let mut chunk_corner_in_node_coordinates = + Vertex::A.chunk_to_node() * na::Vector4::new(1.0, 1.0, 1.0, 1.0); + chunk_corner_in_node_coordinates /= chunk_corner_in_node_coordinates.w; + assert_abs_diff_eq!( + chunk_corner_in_node_coordinates, + na::Vector4::new(0.0, 0.0, 0.0, 1.0), + epsilon = 1e-10 + ); + } + + #[test] + fn node_to_chunk() { + assert_abs_diff_eq!( + Vertex::A.chunk_to_node().try_inverse().unwrap(), + Vertex::A.node_to_chunk(), + epsilon = 1e-10 + ); + } } diff --git a/common/src/graph.rs b/common/src/graph.rs index c2f0177b..36baa867 100644 --- a/common/src/graph.rs +++ b/common/src/graph.rs @@ -111,7 +111,7 @@ impl Graph { /// Given a `transform` relative to a `reference` node, computes the node /// that it's closest to and the transform that moves it there - pub fn normalize_transform( + pub fn normalize_transform( &self, mut reference: NodeId, original: &na::Matrix4, @@ -120,7 +120,7 @@ impl Graph { let mut location = original * math::origin(); 'outer: loop { for side in Side::iter() { - if !side.faces(&location) { + if !side.is_facing(&location) { continue; } reference = match self.neighbor(reference, side) { diff --git a/common/src/lib.rs b/common/src/lib.rs index 1cdb8b5e..6a17b48d 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -6,6 +6,7 @@ use rand::{ #[macro_use] mod id; +extern crate nalgebra as na; mod chunks; pub mod codec; pub mod collision; diff --git a/common/src/math.rs b/common/src/math.rs index 7dcd80b9..5d7af597 100644 --- a/common/src/math.rs +++ b/common/src/math.rs @@ -22,7 +22,7 @@ impl HPoint { } } -impl HPoint { +impl HPoint { pub fn origin() -> Self { Self::new(na::zero(), na::zero(), na::zero()) } @@ -37,18 +37,18 @@ impl HPoint { } /// Point reflection around `p` -pub fn reflect(p: &na::Vector4) -> na::Matrix4 { +pub fn reflect(p: &na::Vector4) -> na::Matrix4 { na::Matrix4::::identity() - (*p * p.transpose() * i31::()) * na::convert::<_, N>(2.0) / mip(p, p) } /// Transform that translates `a` to `b` -pub fn translate(a: &na::Vector4, b: &na::Vector4) -> na::Matrix4 { +pub fn translate(a: &na::Vector4, b: &na::Vector4) -> na::Matrix4 { reflect(&midpoint(a, b)) * reflect(a) } #[rustfmt::skip] -pub fn translate_along(v: &na::Unit>, distance: N) -> na::Matrix4 { +pub fn translate_along(v: &na::Unit>, distance: N) -> na::Matrix4 { if distance == na::zero() { return na::Matrix4::identity(); } @@ -66,23 +66,23 @@ pub fn translate_along(v: &na::Unit>, distance: N) } /// 4D reflection around a normal vector; length is not significant (so long as it's nonzero) -pub fn euclidean_reflect(v: &na::Vector4) -> na::Matrix4 { +pub fn euclidean_reflect(v: &na::Vector4) -> na::Matrix4 { na::Matrix4::identity() - v * v.transpose() * (na::convert::<_, N>(2.0) / v.norm_squared()) } -pub fn midpoint(a: &na::Vector4, b: &na::Vector4) -> na::Vector4 { +pub fn midpoint(a: &na::Vector4, b: &na::Vector4) -> na::Vector4 { a * (mip(b, b) * mip(a, b)).sqrt() + b * (mip(a, a) * mip(a, b)).sqrt() } -pub fn distance(a: &na::Vector4, b: &na::Vector4) -> N { +pub fn distance(a: &na::Vector4, b: &na::Vector4) -> N { (mip(a, b).powi(2) / (mip(a, a) * mip(b, b))).sqrt().acosh() } -pub fn origin() -> na::Vector4 { +pub fn origin() -> na::Vector4 { na::Vector4::new(na::zero(), na::zero(), na::zero(), na::one()) } -pub fn lorentz_normalize(v: &na::Vector4) -> na::Vector4 { +pub fn lorentz_normalize(v: &na::Vector4) -> na::Vector4 { let sf2 = mip(v, v); if sf2 == na::zero() { return origin(); @@ -91,22 +91,20 @@ pub fn lorentz_normalize(v: &na::Vector4) -> na::Vector4 { v / sf } -pub fn renormalize_isometry(m: &na::Matrix4) -> na::Matrix4 { +pub fn renormalize_isometry(m: &na::Matrix4) -> na::Matrix4 { let dest = m.index((.., 3)); let norm = dest.xyz().norm(); let boost_length = (dest.w + norm).ln(); let direction = na::Unit::new_unchecked(dest.xyz() / norm); let inverse_boost = translate_along(&direction, -boost_length); let rotation = renormalize_rotation_reflection( - &(inverse_boost * m) - .fixed_slice::(0, 0) - .clone_owned(), + &(inverse_boost * m).fixed_slice::<3, 3>(0, 0).clone_owned(), ); 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 { +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() { @@ -120,12 +118,12 @@ pub fn normalize_vector(t: na::Matrix4, v: na::Vector4) -> n } /// make a orthogonal to b -pub fn orthogonalize(a: &na::Vector4, b: &na::Vector4) -> na::Vector4 { +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 { +fn renormalize_rotation_reflection(m: &na::Matrix3) -> na::Matrix3 { let zv = m.index((.., 2)).normalize(); let yv = m.index((.., 1)); let dot = zv.dot(&yv); @@ -139,17 +137,17 @@ fn renormalize_rotation_reflection(m: &na::Matrix3) -> na::Matr } /// Whether an isometry reverses winding with respect to the norm -pub fn parity(m: &na::Matrix4) -> bool { - m.fixed_slice::(0, 0).determinant() < na::zero::() +pub fn parity(m: &na::Matrix4) -> bool { + m.fixed_slice::<3, 3>(0, 0).determinant() < na::zero::() } /// Minkowski inner product, aka _h -pub fn mip(a: &na::Vector4, b: &na::Vector4) -> N { +pub fn mip(a: &na::Vector4, b: &na::Vector4) -> N { a.x * b.x + a.y * b.y + a.z * b.z - a.w * b.w } #[rustfmt::skip] -fn i31() -> na::Matrix4 { +fn i31() -> na::Matrix4 { na::convert::<_, na::Matrix4>(na::Matrix4::::new( 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, diff --git a/common/src/node.rs b/common/src/node.rs index 50cbe4ca..a65316c9 100644 --- a/common/src/node.rs +++ b/common/src/node.rs @@ -30,7 +30,6 @@ impl Default for Chunk { } } -#[derive(PartialEq)] pub enum VoxelData { Solid(Material), Dense(Box<[Material]>), diff --git a/common/src/plane.rs b/common/src/plane.rs index 96faa9ca..18e2ee2b 100644 --- a/common/src/plane.rs +++ b/common/src/plane.rs @@ -2,7 +2,7 @@ use std::ops::{Mul, Neg}; use crate::{ dodeca::{Side, Vertex}, - math::{lorentz_normalize, mip, origin}, + math::{lorentz_normalize, mip}, }; /// A hyperbolic plane @@ -14,14 +14,13 @@ pub struct Plane { impl From for Plane { /// A surface overlapping with a particular dodecahedron side fn from(side: Side) -> Self { - let n = side.reflection().column(3) - origin(); Self { - normal: lorentz_normalize(&n), + normal: *side.normal(), } } } -impl From>> for Plane { +impl From>> for Plane { /// A plane passing through the origin fn from(x: na::Unit>) -> Self { Self { @@ -30,7 +29,7 @@ impl From>> for Plane { } } -impl Neg for Plane { +impl Neg for Plane { type Output = Self; fn neg(self) -> Self { Self { @@ -47,7 +46,7 @@ impl Mul> for Side { } } -impl<'a, N: na::RealField> Mul> for &'a na::Matrix4 { +impl<'a, N: na::RealField + Copy> Mul> for &'a na::Matrix4 { type Output = Plane; fn mul(self, rhs: Plane) -> Plane { Plane { @@ -56,7 +55,7 @@ impl<'a, N: na::RealField> Mul> for &'a na::Matrix4 { } } -impl Plane { +impl Plane { /// Hyperbolic normal vector identifying the plane pub fn normal(&self) -> &na::Vector4 { &self.normal @@ -81,7 +80,7 @@ impl Plane { #[cfg(test)] mod tests { use super::*; - use crate::math::translate_along; + use crate::math::{origin, translate_along}; use approx::*; #[test] @@ -105,8 +104,8 @@ mod tests { fn check_surface_flipped() { let root = Plane::from(Side::A); assert_abs_diff_eq!( - root.distance_to_chunk(Vertex::A, &(na::Vector3::x() * 2.0)), - root.distance_to_chunk(Vertex::J, &(na::Vector3::x() * 2.0)) * -1.0, + root.distance_to_chunk(Vertex::A, &na::Vector3::new(-1.0, 1.0, 1.0)), + root.distance_to_chunk(Vertex::J, &na::Vector3::new(-1.0, 1.0, 1.0)) * -1.0, epsilon = 1e-5 ); } @@ -116,7 +115,7 @@ mod tests { assert_abs_diff_eq!( Plane::from(Side::A).distance_to_chunk( Vertex::from_sides(Side::A, Side::B, Side::C).unwrap(), - &na::Vector3::new(1.0, 0.3, 0.9), // The first 1.0 is important, the plane is the midplane of the cube in Side::A direction + &na::Vector3::new(0.0, 0.7, 0.1), // The first 0.0 is important, the plane is the midplane of the cube in Side::A direction ), 0.0, epsilon = 1e-8, @@ -129,33 +128,33 @@ mod tests { // A cube corner should have the same elevation seen from different cubes assert_abs_diff_eq!( - Plane::from(Side::A).distance_to_chunk(abc, &na::Vector3::new(0.0, 0.0, 0.0)), + Plane::from(Side::A).distance_to_chunk(abc, &na::Vector3::new(1.0, 1.0, 1.0)), Plane::from(Side::A).distance_to_chunk( Vertex::from_sides(Side::F, Side::H, Side::J).unwrap(), - &na::Vector3::new(0.0, 0.0, 0.0), + &na::Vector3::new(1.0, 1.0, 1.0), ), epsilon = 1e-8, ); // The same corner should have the same distance_to_chunk when represented from the same cube at different corners assert_abs_diff_eq!( - Plane::from(Side::A).distance_to_chunk(abc, &na::Vector3::new(1.0, 0.0, 0.0)), + Plane::from(Side::A).distance_to_chunk(abc, &na::Vector3::new(0.0, 1.0, 1.0)), (Side::A * Plane::from(Side::A)) - .distance_to_chunk(abc, &na::Vector3::new(1.0, 0.0, 0.0),), + .distance_to_chunk(abc, &na::Vector3::new(0.0, 1.0, 1.0),), epsilon = 1e-8, ); // Corners of midplane cubes separated by the midplane should have the same distance_to_chunk with a different sign assert_abs_diff_eq!( - Plane::from(Side::A).distance_to_chunk(abc, &na::Vector3::new(0.0, 0.0, 0.0)), - -Plane::from(Side::A).distance_to_chunk(abc, &na::Vector3::new(2.0, 0.0, 0.0)), + Plane::from(Side::A).distance_to_chunk(abc, &na::Vector3::new(1.0, 1.0, 1.0)), + -Plane::from(Side::A).distance_to_chunk(abc, &na::Vector3::new(-1.0, 1.0, 1.0)), epsilon = 1e-8, ); // Corners of midplane cubes not separated by the midplane should have the same distance_to_chunk assert_abs_diff_eq!( - Plane::from(Side::A).distance_to_chunk(abc, &na::Vector3::new(0.0, 0.0, 0.0)), - Plane::from(Side::A).distance_to_chunk(abc, &na::Vector3::new(0.0, 0.0, 2.0)), + Plane::from(Side::A).distance_to_chunk(abc, &na::Vector3::new(1.0, 1.0, 1.0)), + Plane::from(Side::A).distance_to_chunk(abc, &na::Vector3::new(1.0, 1.0, -1.0)), epsilon = 1e-8, ); } diff --git a/common/src/worldgen.rs b/common/src/worldgen.rs index 8261acf4..93df0e1f 100644 --- a/common/src/worldgen.rs +++ b/common/src/worldgen.rs @@ -268,15 +268,16 @@ impl ChunkParams { for (x, y, z) in VoxelCoords::new(self.dimension) { let coords = na::Vector3::new(x, y, z); let center = voxel_center(self.dimension, coords); - let cube_coords = center * 0.5; + let trilerp_coords = center.map(|x| (1.0 - x) * 0.5); - let rain = trilerp(&self.env.rainfalls, cube_coords) + rng.sample(&normal.unwrap()); - let temp = trilerp(&self.env.temperatures, cube_coords) + rng.sample(&normal.unwrap()); + let rain = trilerp(&self.env.rainfalls, trilerp_coords) + rng.sample(&normal.unwrap()); + let temp = + trilerp(&self.env.temperatures, trilerp_coords) + rng.sample(&normal.unwrap()); // elev is calculated in multiple steps. The initial value elev_pre_terracing // is used to calculate elev_pre_noise which is used to calculate elev. - let elev_pre_terracing = trilerp(&self.env.max_elevations, cube_coords); - let block = trilerp(&self.env.blockinesses, cube_coords); + let elev_pre_terracing = trilerp(&self.env.max_elevations, trilerp_coords); + let block = trilerp(&self.env.blockinesses, trilerp_coords); let voxel_elevation = self.surface.distance_to_chunk(self.chunk, ¢er); let strength = 0.4 / (1.0 + voxel_elevation.powi(2)); let terracing_small = terracing_diff(elev_pre_terracing, block, 5.0, strength, 2.0); @@ -376,7 +377,7 @@ impl ChunkParams { let x = coords[0]; let y = coords[1]; let z = coords[2]; - let offset = 2 * self.dimension / 3; + let offset = self.dimension / 3; // straight lines. criteria_met += u32::from(x == offset); @@ -556,14 +557,14 @@ fn chunk_incident_enviro_factors( } /// Linearly interpolate at interior and boundary of a cube given values at the eight corners. -fn trilerp( +fn trilerp( &[v000, v001, v010, v011, v100, v101, v110, v111]: &[N; 8], t: na::Vector3, ) -> N { - fn lerp(v0: N, v1: N, t: N) -> N { + fn lerp(v0: N, v1: N, t: N) -> N { v0 * (N::one() - t) + v1 * t } - fn bilerp(v00: N, v01: N, v10: N, v11: N, t: na::Vector2) -> N { + fn bilerp(v00: N, v01: N, v10: N, v11: N, t: na::Vector2) -> N { lerp(lerp(v00, v01, t.x), lerp(v10, v11, t.x), t.y) } @@ -578,7 +579,7 @@ fn trilerp( /// v0 for [0, threshold], v1 for [1-threshold, 1], and linear interpolation in between /// such that the overall shape is an S-shaped piecewise function. /// threshold should be between 0 and 0.5. -fn serp(v0: N, v1: N, t: N, threshold: N) -> N { +fn serp(v0: N, v1: N, t: N, threshold: N) -> N { if t < threshold { v0 } else if t < (N::one() - threshold) { diff --git a/server/Cargo.toml b/server/Cargo.toml index 3814eec2..2e8c5be5 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -17,13 +17,13 @@ quinn = { version = "0.8.3", features = ["rustls"] } serde = { version = "1.0.104", features = ["derive", "rc"] } toml = "0.5.5" anyhow = "1.0.26" -rcgen = { version = "0.9.2", default-features = false } +rcgen = { version = "0.10.0", default-features = false } hostname = "0.3.0" futures = "0.3.1" -hecs = "0.7.6" -rand = { version = "0.7.2", features = [ "small_rng" ] } +hecs = "0.9.0" +rand = { version = "0.8.5", features = [ "small_rng" ] } fxhash = "0.2.1" -na = { package = "nalgebra", version = "0.19" } +nalgebra = "0.31.2" slotmap = "1.0.6" rustls = "0.20.6" rustls-pemfile = "1.0.0" diff --git a/server/src/lib.rs b/server/src/lib.rs index 7f98a140..b89ee3d6 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -1,3 +1,4 @@ +extern crate nalgebra as na; mod input_queue; mod sim; diff --git a/server/src/sim.rs b/server/src/sim.rs index e3cf5117..b6de6bb9 100644 --- a/server/src/sim.rs +++ b/server/src/sim.rs @@ -71,7 +71,7 @@ impl Sim { entity: Entity, command: Command, ) -> Result<(), hecs::ComponentError> { - let mut ch = self.world.get_mut::(entity)?; + let mut ch = self.world.get::<&mut Character>(entity)?; let (direction, speed) = sanitize_motion_input(command.velocity); ch.direction = direction; ch.speed = speed * self.cfg.movement_speed; @@ -80,7 +80,7 @@ impl Sim { } pub fn destroy(&mut self, entity: Entity) { - let id = *self.world.get::(entity).unwrap(); + let id = *self.world.get::<&EntityId>(entity).unwrap(); self.entity_ids.remove(&id); self.world.despawn(entity).unwrap(); self.despawns.push(id); @@ -130,7 +130,7 @@ impl Sim { // Capture state changes for broadcast to clients let mut spawns = Vec::with_capacity(self.spawns.len()); for entity in self.spawns.drain(..) { - let id = *self.world.get::(entity).unwrap(); + let id = *self.world.get::<&EntityId>(entity).unwrap(); spawns.push((id, dump_entity(&self.world, entity))); } if !self.graph.fresh().is_empty() { @@ -197,10 +197,10 @@ enum Empty {} fn dump_entity(world: &hecs::World, entity: Entity) -> Vec { let mut components = Vec::new(); - if let Ok(x) = world.get::(entity) { + if let Ok(x) = world.get::<&Position>(entity) { components.push(Component::Position(*x)); } - if let Ok(x) = world.get::(entity) { + if let Ok(x) = world.get::<&Character>(entity) { components.push(Component::Character(proto::Character { name: x.name.clone(), orientation: x.orientation,