diff --git a/common/Cargo.toml b/common/Cargo.toml index b9f5912c..8b92e829 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -9,6 +9,7 @@ license = "Apache-2.0 OR Zlib" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +arrayvec = "0.7.6" blake3 = "1.3.3" serde = { version = "1.0.104", features = ["derive"] } nalgebra = { workspace = true, features = ["serde-serialize"] } diff --git a/common/src/graph.rs b/common/src/graph.rs index f9b35340..d7567493 100644 --- a/common/src/graph.rs +++ b/common/src/graph.rs @@ -259,7 +259,7 @@ impl std::ops::IndexMut for Graph { /// A cryptographic hash function is used to ensure uniqueness, making it /// astronomically unlikely to be able to find two different nodes in the graph /// with the same ID. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub struct NodeId(u128); impl NodeId { diff --git a/common/src/lib.rs b/common/src/lib.rs index 68f70279..ea7de222 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -25,10 +25,9 @@ pub mod lru_slab; mod margins; pub mod math; pub mod node; -mod plane; +pub mod peer_traverser; pub mod proto; mod sim_config; -pub mod terraingen; pub mod traversal; pub mod voxel_math; pub mod world; diff --git a/common/src/node.rs b/common/src/node.rs index 65f20adc..36df04ce 100644 --- a/common/src/node.rs +++ b/common/src/node.rs @@ -11,8 +11,8 @@ use crate::lru_slab::SlotId; use crate::proto::{BlockUpdate, Position, SerializedVoxelData}; use crate::voxel_math::{ChunkDirection, CoordAxis, CoordSign, Coords}; use crate::world::Material; -use crate::worldgen::NodeState; -use crate::{Chunks, margins}; +use crate::worldgen::{NodeState, PartialNodeState}; +use crate::{Chunks, margins, peer_traverser}; /// Unique identifier for a single chunk (1/20 of a dodecahedron) in the graph #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] @@ -28,30 +28,46 @@ impl ChunkId { } impl Graph { + /// Returns the PartialNodeState for the given node, panicking if it isn't initialized. + #[inline] + pub fn partial_node_state(&self, node_id: NodeId) -> &PartialNodeState { + self[node_id].partial_state.as_ref().unwrap() + } + + /// Initializes the PartialNodeState for the given node if not already initialized, + /// initializing other nodes' NodeState and PartialNodeState as necessary + pub fn ensure_partial_node_state(&mut self, node_id: NodeId) { + if self[node_id].partial_state.is_some() { + return; + } + + for (_, parent) in self.parents(node_id) { + self.ensure_node_state(parent); + } + + let partial_node_state = PartialNodeState::new(self, node_id); + self[node_id].partial_state = Some(partial_node_state); + } + /// Returns the NodeState for the given node, panicking if it isn't initialized. #[inline] pub fn node_state(&self, node_id: NodeId) -> &NodeState { self[node_id].state.as_ref().unwrap() } - /// Initializes the NodeState for the given node and all its ancestors if not - /// already initialized. + /// Initializes the NodeState for the given node if not already initialized, + /// initializing other nodes' NodeState and PartialNodeState as necessary pub fn ensure_node_state(&mut self, node_id: NodeId) { if self[node_id].state.is_some() { return; } - for (_, parent) in self.parents(node_id) { - self.ensure_node_state(parent); + self.ensure_partial_node_state(node_id); + for peer in peer_traverser::ensure_peer_nodes(self, node_id) { + self.ensure_partial_node_state(peer.node()); } - let node_state = self - .primary_parent_side(node_id) - .map(|i| { - let parent_state = self.node_state(self.neighbor(node_id, i).unwrap()); - parent_state.child(self, node_id, i) - }) - .unwrap_or_else(NodeState::root); + let node_state = NodeState::new(self, node_id); self[node_id].state = Some(node_state); } @@ -217,6 +233,7 @@ impl IndexMut for Graph { /// used for rendering, is stored here. #[derive(Default)] pub struct Node { + pub partial_state: Option, pub state: Option, /// We can only populate chunks which lie within a cube of populated nodes, so nodes on the edge /// of the graph always have some `Fresh` chunks. diff --git a/common/src/peer_traverser.rs b/common/src/peer_traverser.rs new file mode 100644 index 00000000..415d6552 --- /dev/null +++ b/common/src/peer_traverser.rs @@ -0,0 +1,324 @@ +use std::sync::LazyLock; + +use arrayvec::ArrayVec; + +use crate::{ + dodeca::Side, + graph::{Graph, NodeId}, +}; + +/// Assumes the graph is expanded enough to traverse peer nodes and returns all peer nodes +/// for the given base node. Panics if this assumption is false. See documentation of `PeerNode` +/// for a definition of what a "peer node" is. +pub fn expect_peer_nodes(graph: &Graph, base_node: NodeId) -> Vec { + peer_nodes_impl(AssertingGraphRef { graph }, base_node) +} + +/// Returns all peer nodes for the given base node, expanding the graph if necessary. See +/// documentation of `PeerNode` for a definition of what a "peer node" is. +pub fn ensure_peer_nodes(graph: &mut Graph, base_node: NodeId) -> Vec { + peer_nodes_impl(ExpandingGraphRef { graph }, base_node) +} + +/// Internal implementation of peer node traversal, using a `GraphRef` to be generic over +/// whether a mutable or immutable graph reference is available +fn peer_nodes_impl(mut graph: impl GraphRef, base_node: NodeId) -> Vec { + let mut nodes = Vec::new(); + + // Depth 1 paths + for parent_side in Side::iter() { + let parent_node = graph.neighbor(base_node, parent_side); + if graph.depth(parent_node) >= graph.depth(base_node) { + continue; + } + for &child_side in &DEPTH1_CHILD_PATHS[parent_side as usize] { + let peer_node = graph.neighbor(parent_node, child_side); + if graph.depth(peer_node) == graph.depth(base_node) { + nodes.push(PeerNode { + node_id: peer_node, + parent_path: ArrayVec::from_iter([parent_side]), + child_path: ArrayVec::from_iter([child_side]), + }); + } + } + } + + // Depth 2 paths + for (parent_side0, parent_node0) in graph.parents(base_node) { + for (parent_side1, parent_node1) in graph.parents(parent_node0) { + // Avoid redundancies by enforcing shortlex order + if parent_side1.adjacent_to(parent_side0) + && (parent_side1 as usize) < (parent_side0 as usize) + { + continue; + } + for &child_sides in &DEPTH2_CHILD_PATHS[parent_side0 as usize][parent_side1 as usize] { + let peer_node_parent = graph.neighbor(parent_node1, child_sides[0]); + let peer_node = graph.neighbor(peer_node_parent, child_sides[1]); + if graph.depth(peer_node) == graph.depth(base_node) { + nodes.push(PeerNode { + node_id: peer_node, + parent_path: ArrayVec::from_iter([parent_side0, parent_side1]), + child_path: ArrayVec::from_iter(child_sides), + }); + } + } + } + } + + nodes +} + +/// Details relating to a specific peer node. For a given base node, a peer node is +/// any node of the same depth as the base node where it is possible to reach the +/// same node from both the base and the peer node without "going backwards". Going backwards +/// in this sense means going from a node with a higher depth to a node with a lower depth. +/// +/// Peer nodes are important because if worldgen produces a structure at a given base node +/// and another structure at a given peer node, those two structures could potentially intersect +/// if care is not taken. Checking the peer nodes in advance will prevent this. +pub struct PeerNode { + node_id: NodeId, + parent_path: ArrayVec, + child_path: ArrayVec, +} + +impl PeerNode { + /// The ID of the peer node + #[inline] + pub fn node(&self) -> NodeId { + self.node_id + } + + /// The sequence of sides that takes you from the peer node to the shared child node + #[inline] + pub fn peer_to_shared(&self) -> impl ExactSizeIterator + Clone + use<> { + self.parent_path.clone().into_iter().rev() + } + + /// The sequence of sides that takes you from the base node to the shared child node + #[inline] + pub fn base_to_shared(&self) -> impl ExactSizeIterator + Clone + use<> { + self.child_path.clone().into_iter() + } +} + +/// All paths that can potentially lead to a peer node after following the given parent path of length 1 +static DEPTH1_CHILD_PATHS: LazyLock<[ArrayVec; Side::VALUES.len()]> = + LazyLock::new(|| { + Side::VALUES.map(|parent_side| { + // The main constraint is that all parent sides need to be adjacent to all child sides. + let mut path_list: ArrayVec = ArrayVec::new(); + for child_side in Side::iter() { + if !child_side.adjacent_to(parent_side) { + continue; + } + path_list.push(child_side); + } + path_list + }) + }); + +/// All paths that can potentially lead to a peer node after following the given parent path of length 2 +static DEPTH2_CHILD_PATHS: LazyLock< + [[ArrayVec<[Side; 2], 2>; Side::VALUES.len()]; Side::VALUES.len()], +> = LazyLock::new(|| { + Side::VALUES.map(|parent_side0| { + Side::VALUES.map(|parent_side1| { + let mut path_list: ArrayVec<[Side; 2], 2> = ArrayVec::new(); + if parent_side0 == parent_side1 { + // Backtracking parent paths are irrelevant and may result in more child paths than + // can fit in the ArrayVec, so skip these. + return path_list; + } + // The main constraint is that all parent sides need to be adjacent to all child sides. + for child_side0 in Side::iter() { + if !child_side0.adjacent_to(parent_side0) || !child_side0.adjacent_to(parent_side1) + { + // Child paths need to have both parts adjacent to parent paths. + continue; + } + for child_side1 in Side::iter() { + // To avoid redundancies, only look at child paths that obey shortlex rules. + if child_side0 == child_side1 { + // Child path backtracks and should be discounted. + continue; + } + if child_side0.adjacent_to(child_side1) + && (child_side0 as usize) > (child_side1 as usize) + { + // There is a lexicographically earlier child path, so this should be discounted. + continue; + } + if !child_side1.adjacent_to(parent_side0) + || !child_side1.adjacent_to(parent_side1) + { + // Child paths need to have both parts adjacent to parent paths. + continue; + } + path_list.push([child_side0, child_side1]); + } + } + path_list + }) + }) +}); + +/// A reference to the graph used by `PeerTraverser` to decide how to handle not-yet-created nodes +trait GraphRef: AsRef { + fn depth(&self, node: NodeId) -> u32; + fn neighbor(&mut self, node: NodeId, side: Side) -> NodeId; + fn parents(&self, node: NodeId) -> impl ExactSizeIterator + use; +} + +/// A `GraphRef` that asserts that all the nodes it needs already exist +struct AssertingGraphRef<'a> { + graph: &'a Graph, +} + +impl AsRef for AssertingGraphRef<'_> { + fn as_ref(&self) -> &Graph { + self.graph + } +} + +impl<'a> GraphRef for AssertingGraphRef<'a> { + fn depth(&self, node: NodeId) -> u32 { + self.graph.depth(node) + } + + fn neighbor(&mut self, node: NodeId, side: Side) -> NodeId { + self.graph.neighbor(node, side).unwrap() + } + + fn parents(&self, node: NodeId) -> impl ExactSizeIterator + use<'a> { + self.graph.parents(node) + } +} + +/// A `GraphRef` that expands the graph as necessary +struct ExpandingGraphRef<'a> { + graph: &'a mut Graph, +} + +impl<'a> GraphRef for ExpandingGraphRef<'a> { + fn depth(&self, node: NodeId) -> u32 { + self.graph.depth(node) + } + + fn neighbor(&mut self, node: NodeId, side: Side) -> NodeId { + self.graph.ensure_neighbor(node, side) + } + + fn parents(&self, node: NodeId) -> impl ExactSizeIterator + use<'a> { + self.graph.parents(node) + } +} + +impl AsRef for ExpandingGraphRef<'_> { + fn as_ref(&self) -> &Graph { + self.graph + } +} + +#[cfg(test)] +mod tests { + use fxhash::FxHashSet; + + use super::*; + + // Returns the `NodeId` corresponding to the given path + fn node_from_path( + graph: &mut Graph, + start_node: NodeId, + path: impl IntoIterator, + ) -> NodeId { + let mut current_node = start_node; + for side in path { + current_node = graph.ensure_neighbor(current_node, side); + } + current_node + } + + #[test] + fn peer_traverser_example() { + let mut graph = Graph::new(1); + let base_node = node_from_path( + &mut graph, + NodeId::ROOT, + [Side::B, Side::D, Side::C, Side::A], + ); + + let expected_paths: &[(&[Side], &[Side])] = &[ + (&[Side::A], &[Side::B]), + (&[Side::A], &[Side::E]), + (&[Side::A], &[Side::I]), + (&[Side::C], &[Side::B]), + (&[Side::C], &[Side::F]), + (&[Side::C], &[Side::H]), + (&[Side::D], &[Side::H]), + (&[Side::D], &[Side::I]), + (&[Side::D], &[Side::K]), + (&[Side::C, Side::A], &[Side::B, Side::D]), + (&[Side::D, Side::A], &[Side::I, Side::C]), + (&[Side::D, Side::C], &[Side::H, Side::A]), + ]; + + let peers = ensure_peer_nodes(&mut graph, base_node); + assert_eq!(peers.len(), expected_paths.len()); + for (peer, expected_path) in peers.into_iter().zip(expected_paths) { + assert_eq!( + peer.peer_to_shared().collect::>(), + expected_path.0.to_vec(), + ); + assert_eq!( + peer.base_to_shared().collect::>(), + expected_path.1.to_vec(), + ); + } + } + + #[test] + fn peer_definition_holds() { + let mut graph = Graph::new(1); + let base_node = node_from_path( + &mut graph, + NodeId::ROOT, + [Side::B, Side::D, Side::C, Side::A], + ); + let mut found_peer_nodes = FxHashSet::default(); + for peer in ensure_peer_nodes(&mut graph, base_node) { + let peer_node = peer.node(); + + assert!( + found_peer_nodes.insert(peer_node), + "The same peer node must not be returned more than once." + ); + + let destination_from_base = + node_from_path(&mut graph, base_node, peer.base_to_shared()); + let destination_from_peer = + node_from_path(&mut graph, peer_node, peer.peer_to_shared()); + + assert_eq!( + graph.depth(base_node), + graph.depth(peer_node), + "The base and peer nodes must have the same depth in the graph." + ); + assert_eq!( + graph.depth(base_node) + peer.base_to_shared().len() as u32, + graph.depth(destination_from_base), + "path_from_base must not backtrack to a parent node." + ); + assert_eq!( + graph.depth(peer_node) + peer.peer_to_shared().len() as u32, + graph.depth(destination_from_peer), + "path_from_peer must not backtrack to a parent node." + ); + assert_eq!( + destination_from_base, destination_from_peer, + "path_from_base and path_from_peer must lead to the same node." + ); + } + } +} diff --git a/common/src/worldgen/horosphere.rs b/common/src/worldgen/horosphere.rs new file mode 100644 index 00000000..e9433927 --- /dev/null +++ b/common/src/worldgen/horosphere.rs @@ -0,0 +1,533 @@ +use libm::{cosf, sinf, sqrtf}; +use rand::{Rng, SeedableRng}; +use rand_distr::Poisson; +use rand_pcg::Pcg64Mcg; + +use crate::{ + dodeca::{Side, Vertex}, + graph::{Graph, NodeId}, + math::{MDirection, MIsometry, MPoint, MVector}, + node::VoxelData, + peer_traverser, + voxel_math::Coords, + world::Material, + worldgen::hash, +}; + +/// Whether an assortment of random horospheres should be added to world generation. This is a temporary +/// option until large structures that fit with the theme of the world are introduced. +/// For code simplicity, this is made into a constant instead of a configuration option. +const HOROSPHERES_ENABLED: bool = true; + +/// Value to mix into the node's spice for generating horospheres. Chosen randomly. +const HOROSPHERE_SEED: u64 = 6046133366614030452; + +/// Represents a node's reference to a particular horosphere. As a general rule, for any give horosphere, +/// every node in the convex hull of nodes containing the horosphere will have a `HorosphereNode` +/// referencing it. The unique node in this convex hull with the smallest depth in the graph is the owner +/// of the horosphere, where it is originally generated. +#[derive(Copy, Clone)] +pub struct HorosphereNode { + /// The node that originally created the horosphere. All parts of the horosphere will + /// be in a node with this as an ancestor, and all HorosphereNodes with the same `owner` correspond + /// to the same horosphere. + owner: NodeId, + + /// The horosphere's location relative to the node containing this `HorosphereNode` + horosphere: Horosphere, + + /// A region that bounds the `HorosphereNode`'s descendents. A `HorosphereNode` will never propagate beyond + /// this region, and the region's bounds will be as tight as possible. Note that this region does note necessarily + /// contain the whole horosphere because parts of the horosphere that require backtracking towards the origin + /// are ignored. + // Note: All public constructors generate a `HorosphereNode` with tight bounds, but some `HorosphereNode`s + // might not have tight bounds because a `HorosphereNode` is used in an intermediate calculations, averaged + // together with other `HorosphereNode`s before the bounds are tightened. + region: NodeBoundedRegion, +} + +impl HorosphereNode { + /// Returns the `HorosphereNode` for the given node, either by propagating an existing parent + /// `HorosphereNode` or by randomly generating a new one. + pub fn new(graph: &Graph, node_id: NodeId) -> Option { + if !HOROSPHERES_ENABLED { + return None; + } + HorosphereNode::create_from_parents(graph, node_id) + .or_else(|| HorosphereNode::maybe_create_fresh(graph, node_id)) + } + + /// Propagates `HorosphereNode` information from the given parent nodes to this child node. Returns + /// `None` if there's no horosphere to propagate, either because none of the parent nodes have a + /// horosphere associated with them, or because any existing horosphere is outside the range + /// of this node. + fn create_from_parents(graph: &Graph, node_id: NodeId) -> Option { + // Rather than selecting an arbitrary parent HorosphereNode, we average all of them. This + // is important because otherwise, the propagation of floating point precision errors could + // create a seam. This ensures that all errors average out, keeping the horosphere smooth. + let mut horospheres_to_average_iter = + graph + .parents(node_id) + .filter_map(|(parent_side, parent_id)| { + graph + .node_state(parent_id) + .horosphere + .as_ref() + .and_then(|h| h.propagate(parent_side)) + }); + + let mut horosphere_node = horospheres_to_average_iter.next()?; + let mut count = 1; + for other in horospheres_to_average_iter { + // Take an average of all HorosphereNodes in this iterator, giving each of them equal weight + // by keeping track of a moving average with a weight that changes over time to make the + // numbers work out the same way. + count += 1; + horosphere_node.average_with(other, 1.0 / count as f32); + } + + horosphere_node.horosphere.renormalize(); + horosphere_node.tighten_region_bounds(); + Some(horosphere_node) + } + + /// Create a `HorosphereNode` corresponding to a freshly created horosphere with the given node as its owner, + /// if one should be created. This function is called on every node that doesn't already have a horosphere + /// associated with it, so this function has control over how frequent the horospheres should be. + fn maybe_create_fresh(graph: &Graph, node_id: NodeId) -> Option { + const HOROSPHERE_DENSITY: f32 = 6.0; + + let spice = graph.hash_of(node_id) as u64; + let mut rng = rand_pcg::Pcg64Mcg::seed_from_u64(hash(spice, HOROSPHERE_SEED)); + for _ in 0..rng.sample(Poisson::new(HOROSPHERE_DENSITY).unwrap()) as u32 { + // This logic is designed to create an average of "HOROSPHERE_DENSITY" horosphere candiates + // in the region determined by `random_horosphere_pos` and then filters the resulting + // list of candiates to only ones where the current node is the suitable owner for them. + // Filtering instead of rejection sampling ensures a uniform distribution of horosphere + // even though different nodes have different-sized regions for valid horospheres. + + // However, we do return early to ensure that after filtering, we only take the first + // horosphere if there is one, since a node can have at most one horosphere. + let horosphere = Horosphere::new_random(&mut rng, MAX_OWNED_HOROSPHERE_W); + if is_horosphere_valid(graph, node_id, &horosphere) { + let mut horosphere_node = HorosphereNode { + owner: node_id, + horosphere, + region: NodeBoundedRegion::node_and_descendents(graph, node_id), + }; + horosphere_node.tighten_region_bounds(); + return Some(horosphere_node); + } + } + None + } + + /// Updates the region associated with the `HorosphereNode` to have bounds that are as tight as possible. + fn tighten_region_bounds(&mut self) { + for side in Side::iter() { + if !self.region.is_bounded_by(side) && self.can_tighten_region_bound(side) { + self.region.add_bound(side); + } + } + } + + /// Computes whether propagation can stop at a particular side due to no part of the horosphere + /// being behind it. This function is used to tighten region bounds. + fn can_tighten_region_bound(&self, side: Side) -> bool { + !self.horosphere.intersects_half_space(side.normal()) + } + + /// Returns an estimate of the `HorosphereNode` corresponding to the node adjacent to the current node + /// at the given side, or `None` if the horosphere is no longer relevant after crossing the given side. + /// The estimates given by multiple nodes may be used to produce the actual `HorosphereNode`. + fn propagate(&self, side: Side) -> Option { + // Don't propagate beyond the already-computed bounds of the `HorosphereNode`. + if self.region.is_bounded_by(side) { + return None; + } + + Some(HorosphereNode { + owner: self.owner, + horosphere: side.reflection() * self.horosphere, + region: self.region.neighbor(side), + }) + } + + /// Takes the weighted average of the coordinates of this horosphere with the coordinates of the other horosphere. + fn average_with(&mut self, other: HorosphereNode, other_weight: f32) { + if self.owner != other.owner { + // If this panic is triggered, it may mean that two horospheres were generated that interfere + // with each other. The logic in `should_generate` should prevent this, so this would be a sign + // of a bug in that function's implementation. + panic!("Tried to average two unrelated HorosphereNodes"); + } + self.horosphere.pos = + self.horosphere.pos * (1.0 - other_weight) + other.horosphere.pos * other_weight; + self.region = self.region.intersect(other.region); + } + + /// Returns whether the horosphere is freshly created, instead of a + /// reference to a horosphere created earlier on in the node graph. + fn is_fresh(&self, node_id: NodeId) -> bool { + self.owner == node_id + } + + /// If `self` and `other` would propagate to the same node, to avoid interference, only one of these + /// two horospheres can generate. This function determines whether `self` should be the one to generate. + fn has_priority(&self, other: &HorosphereNode, node_id: NodeId) -> bool { + // If both horospheres are fresh, use the owner's NodeId as an arbitrary + // tie-breaker to decide which horosphere should win. + !self.is_fresh(node_id) || (other.is_fresh(node_id) && self.owner < other.owner) + } + + /// Based on other nodes in the graph, determines whether the horosphere + /// should generate. If false, it means that another horosphere elsewhere + /// would interfere, and generation should not proceed. + pub fn should_generate(&self, graph: &Graph, node_id: NodeId) -> bool { + if !self.is_fresh(node_id) { + // The horosphere is propagated and so is already proven to exist. + return true; + } + + for peer in peer_traverser::expect_peer_nodes(graph, node_id) { + let Some(peer_horosphere) = graph + .partial_node_state(peer.node()) + .candidate_horosphere + .as_ref() + else { + continue; + }; + if !self.has_priority(peer_horosphere, node_id) + // Check that these horospheres can interfere by seeing if their regions share a node in common. + && peer_horosphere.region.contains_node(peer.peer_to_shared()) + && self.region.contains_node(peer.base_to_shared()) + { + return false; + } + } + true + } +} + +/// Returns whether the given horosphere position could represent a horosphere generated by the +/// given node. The requirement is that a horosphere must be bounded by all of the node's parent sides +/// (as otherwise, a parent node would own the horosphere), and the horosphere must not be fully +/// behind any of the other dodeca sides (as otherwise, a child node would own the horosphere). Note +/// that the horosphere does not necessarily need to intersect the dodeca to be valid. +fn is_horosphere_valid(graph: &Graph, node_id: NodeId, horosphere: &Horosphere) -> bool { + Side::iter().all(|s| !horosphere.is_inside_half_space(s.normal())) + && (graph.parents(node_id)).all(|(s, _)| !horosphere.intersects_half_space(s.normal())) +} + +/// The maximum node-centric w-coordinate a horosphere can have such that the node in question +/// is still the owner of the horosphere. +// See `test_max_owned_horosphere_w()` for how this is computed. +const MAX_OWNED_HOROSPHERE_W: f32 = 5.9047837; + +/// Represents a chunks's reference to a particular horosphere. +pub struct HorosphereChunk { + /// The horosphere's location relative to the chunk containing this `HorosphereChunk`. + pub horosphere: Horosphere, +} + +impl HorosphereChunk { + /// Creates a `HorosphereChunk` based on a `HorosphereNode` + pub fn new(horosphere_node: &HorosphereNode, vertex: Vertex) -> Self { + HorosphereChunk { + horosphere: vertex.node_to_dual() * horosphere_node.horosphere, + } + } + + /// Rasterizes the horosphere chunk into the given `VoxelData` + pub fn generate(&self, voxels: &mut VoxelData, chunk_size: u8) { + for z in 0..chunk_size { + for y in 0..chunk_size { + for x in 0..chunk_size { + let pos = MVector::new( + x as f32 + 0.5, + y as f32 + 0.5, + z as f32 + 0.5, + chunk_size as f32 * Vertex::dual_to_chunk_factor(), + ) + .normalized_point(); + if self.horosphere.contains_point(&pos) { + voxels.data_mut(chunk_size)[Coords([x, y, z]).to_index(chunk_size)] = + Material::RedSandstone; + } + } + } + } + } +} + +/// A horosphere in hyperbolic space. Contains helper functions for common operations +#[derive(Copy, Clone)] +pub struct Horosphere { + /// This vector, `pos`, entirely represents the horosphere using the following rule: A vector + /// `point` is in this horosphere exactly when `point.mip(&self.pos) == -1`. This vector should + /// always have the invariant `self.pos.mip(&self.pos) == 0`, behaving much like a "light-like" + /// vector in Minkowski space. + /// + /// One recommended way to gain an intuition of this vector is to consider its direction separately. + /// The vector points in the direction of an ideal point in the hyperboloid model because it is on + /// the cone that the hyperboloid approaches. This ideal point is the center of the horosphere. + /// This determines `self.pos` up to a scalar multiple, and the remaining degree of freedom can + /// be pinned down by analyzing the w-coordinate. + /// + /// The w-coordinate determines which of the concentric horospheres with that ideal point is represented. + /// A larger w-coordinate represents a horosphere that is farther away from the origin. + /// If the w-coordinate is 1, the origin is on the horosphere's surface. + /// If it's less than 1, the origin is in the horosphere's interior, and if it's greater than 1, the origin + /// is outside the horosphere. + /// + /// TODO: If a player traverses too far inside a horosphere, this vector will underflow, preventing + /// the horosphere from generating properly. Fixing this requires using logic similar to `Plane` to + /// increase the range of magnitudes the vector can take. + pos: MVector, +} + +impl Horosphere { + /// Returns whether the point is inside the horosphere + pub fn contains_point(&self, point: &MPoint) -> bool { + self.pos.mip(point) >= -1.0 + } + + /// Returns whether the horosphere is entirely inside the half space in front of the plane defined by `normal` + pub fn is_inside_half_space(&self, normal: &MDirection) -> bool { + self.pos.mip(normal) >= 1.0 + } + + /// Returns whether any part of the horosphere intersects the half space in front of the plane defined by `normal` + pub fn intersects_half_space(&self, normal: &MDirection) -> bool { + self.pos.mip(normal) >= -1.0 + } + + /// Ensures that the horosphere invariant holds (`pos.mip(&pos) == 0`), as numerical error can otherwise propagate, + /// potentially making the surface behave more like a sphere or an equidistant surface. + pub fn renormalize(&mut self) { + self.pos.w = self.pos.xyz().norm(); + } + + /// Returns a uniformly random horosphere, restricted so that the w-coordinate of its representing vector + /// must be at most `max_w`. + pub fn new_random(rng: &mut Pcg64Mcg, max_w: f32) -> Self { + // Pick a w-coordinate whose probability density function is `p(w) = w`. By trial and error, + // this seems to produce horospheres with a uniform and isotropic distribution. + // TODO: Find a rigorous explanation for this. We would want to show that the probability density is unchanged + // when an isometry is applied. + let w = sqrtf(rng.random::()) * max_w; + + // Uniformly pick spherical coordinates from a unit sphere + let cos_phi = rng.random::() * 2.0 - 1.0; + let sin_phi = sqrtf(1.0 - cos_phi * cos_phi); + let theta = rng.random::() * std::f32::consts::TAU; + + // Construct the resulting vector. + Horosphere { + pos: MVector::new( + w * sin_phi * cosf(theta), + w * sin_phi * sinf(theta), + w * cos_phi, + w, + ), + } + } +} + +impl std::ops::Mul for &MIsometry { + type Output = Horosphere; + + fn mul(self, rhs: Horosphere) -> Self::Output { + Horosphere { + pos: self * rhs.pos, + } + } +} + +/// Represents a region of space bounded by planes corresponding to a subset of a node's sides in that node's perspective. +#[derive(Clone, Copy)] +struct NodeBoundedRegion { + /// A bit-array with 12 elements, one for each side. A 1 means that that side is a bound, and a 0 means it is not. + bounded_sides: u16, +} + +impl NodeBoundedRegion { + /// Creates a region with no bounds + #[cfg(test)] + fn unbounded() -> Self { + NodeBoundedRegion { bounded_sides: 0 } + } + + /// Creates a region that contains the given node and all its descendents + fn node_and_descendents(graph: &Graph, node_id: NodeId) -> Self { + let mut bounded_sides = 0; + for (parent_side, _) in graph.parents(node_id) { + bounded_sides |= 1 << (parent_side as u8); + } + NodeBoundedRegion { bounded_sides } + } + + /// Produces the set intersection of the `self` and `other` regions + fn intersect(self, other: NodeBoundedRegion) -> NodeBoundedRegion { + NodeBoundedRegion { + bounded_sides: self.bounded_sides | other.bounded_sides, + } + } + + /// Returns the sub-region consisting of everything beyond the plane containing + /// the given side (in the perspective of the corresponding neighboring node). + /// As a precondition, the given side cannot be an existing bound, as that would + /// make the sub-region empty (which is non-representable in `NodeBoundedRegion`). + fn neighbor(self, neighbor_side: Side) -> NodeBoundedRegion { + debug_assert!(!self.is_bounded_by(neighbor_side)); + + let mut bounded_sides = self.bounded_sides; + + // Don't allow backtracking + bounded_sides |= 1 << (neighbor_side as u8); + + // As we're shifting perspective to a neighboring node, most sides now refer + // to different planes and no longer bound the region. The only exceptions to this + // are the shared side and its neighbors. + for side in Side::iter() { + if !side.adjacent_to(neighbor_side) && side != neighbor_side { + bounded_sides &= !(1 << (side as u8)); + } + } + NodeBoundedRegion { bounded_sides } + } + + /// Returns whether the node reachable via the given path is within the region. + /// Note that this path is required to be one of the shortest paths that can reach + /// that node. + fn contains_node(self, path: impl Iterator) -> bool { + let mut current_region = self; + for side in path { + if current_region.is_bounded_by(side) { + return false; + } + current_region = current_region.neighbor(side); + } + true + } + + /// Returns whether the given side bounds the region + fn is_bounded_by(self, side: Side) -> bool { + self.bounded_sides & (1 << (side as u8)) != 0 + } + + /// Adds the given side as a bound for the region + fn add_bound(&mut self, side: Side) { + self.bounded_sides |= 1 << (side as u8); + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::math::MPoint; + use approx::assert_abs_diff_eq; + + #[test] + fn test_max_owned_horosphere_w() { + // This tests that `MAX_OWNED_HOROSPHERE_W` is set to the correct value. + + // The worst case scenario would be a horosphere located directly in the direction of a dodeca's vertex. + // This is because the horosphere can be outside the dodeca, tangent to each of the planes that extend the + // dodeca's sides adjancent to that vertex. If that horosphere were brought any closer, it would intersect + // all three of those planes, making it impossible for any child node to own the dodeca and forcing the node + // in focus to own it. + + // First, find an arbitrary horosphere in the direction of a vertex. + let example_vertex = Vertex::A; + let example_vertex_pos = example_vertex.dual_to_node() * MPoint::origin(); + let mut horosphere_pos = MVector::from(example_vertex_pos); + horosphere_pos.w = horosphere_pos.xyz().norm(); + + // Then, scale the horosphere so that it's mip with each of the sides of the vertex is 1, making it tangent. + horosphere_pos /= horosphere_pos.mip(example_vertex.canonical_sides()[0].normal()); + for side in example_vertex.canonical_sides() { + assert_abs_diff_eq!(horosphere_pos.mip(side.normal()), 1.0, epsilon = 1.0e-6); + } + + // Finally, compare that horosphere's w-coordinate to `MAX_OWNED_HOROSPHERE_W` + assert_abs_diff_eq!(horosphere_pos.w, MAX_OWNED_HOROSPHERE_W, epsilon = 1.0e-6); + } + + #[test] + fn node_bounded_region_intersect_example() { + let mut region0 = NodeBoundedRegion::unbounded(); + region0.add_bound(Side::A); + region0.add_bound(Side::B); + + let mut region1 = NodeBoundedRegion::unbounded(); + region1.add_bound(Side::B); + region1.add_bound(Side::C); + + let intersection = region0.intersect(region1); + for side in Side::iter() { + assert_eq!( + intersection.is_bounded_by(side), + [Side::A, Side::B, Side::C].contains(&side), + "testing side {side:?}", + ); + } + } + + #[test] + fn node_bounded_region_neighbor_example() { + let mut region = NodeBoundedRegion::unbounded(); + // Sides adjacent to A + region.add_bound(Side::B); + region.add_bound(Side::C); + region.add_bound(Side::D); + + // Sides not adjacent to A + region.add_bound(Side::F); + region.add_bound(Side::G); + region.add_bound(Side::J); + + let neighbor = region.neighbor(Side::A); + + for side in Side::iter() { + assert_eq!( + neighbor.is_bounded_by(side), + [Side::A, Side::B, Side::C, Side::D].contains(&side), + "testing side {side:?}", + ); + } + } + + #[test] + fn node_bounded_region_node_and_descendents_example() { + let mut graph = Graph::new(1); + let node_id = graph.ensure_neighbor(NodeId::ROOT, Side::A); + let region = NodeBoundedRegion::node_and_descendents(&graph, node_id); + + for side in Side::iter() { + assert_eq!( + region.is_bounded_by(side), + [Side::A].contains(&side), + "testing side {side:?}", + ); + } + } + + #[test] + fn node_bounded_region_contains_node_example() { + let mut region = NodeBoundedRegion::unbounded(); + // Sides adjacent to A + region.add_bound(Side::B); + region.add_bound(Side::C); + + // Sides not adjacent to A + region.add_bound(Side::F); + region.add_bound(Side::G); + region.add_bound(Side::J); + + assert!(region.contains_node([Side::A].into_iter())); + assert!(!region.contains_node([Side::B].into_iter())); + assert!(region.contains_node([Side::A, Side::F].into_iter())); + assert!(!region.contains_node([Side::A, Side::B].into_iter())); + } +} diff --git a/common/src/worldgen.rs b/common/src/worldgen/mod.rs similarity index 87% rename from common/src/worldgen.rs rename to common/src/worldgen/mod.rs index 9cd9fdf7..98aa345c 100644 --- a/common/src/worldgen.rs +++ b/common/src/worldgen/mod.rs @@ -1,5 +1,8 @@ +use horosphere::{HorosphereChunk, HorosphereNode}; +use plane::Plane; use rand::{Rng, SeedableRng, distr::Uniform}; use rand_distr::Normal; +use terraingen::VoronoiInfo; use crate::{ dodeca::{Side, Vertex}, @@ -7,11 +10,13 @@ use crate::{ margins, math::{self, MVector}, node::{ChunkId, VoxelData}, - plane::Plane, - terraingen::VoronoiInfo, world::Material, }; +mod horosphere; +mod plane; +mod terraingen; + #[derive(Clone, Copy, PartialEq, Debug)] enum NodeStateKind { Sky, @@ -62,61 +67,88 @@ impl NodeStateRoad { } } +/// Contains a minimal amount of information about a node that can be deduced entirely from +/// the NodeState of its parents. +pub struct PartialNodeState { + /// This becomes a real horosphere only if it doesn't interfere with another higher-priority horosphere. + /// See `HorosphereNode::has_priority` for the definition of priority. + candidate_horosphere: Option, +} + +impl PartialNodeState { + pub fn new(graph: &Graph, node: NodeId) -> Self { + Self { + candidate_horosphere: HorosphereNode::new(graph, node), + } + } +} + /// Contains all information about a node used for world generation. Most world -/// generation logic uses this information as a starting point. +/// generation logic uses this information as a starting point. The `NodeState` is deduced +/// from the `NodeState` of the node's parents, along with the `PartialNodeState` of the node +/// itself and its "peer" nodes (See `peer_traverser`). pub struct NodeState { kind: NodeStateKind, surface: Plane, road_state: NodeStateRoad, enviro: EnviroFactors, + horosphere: Option, } impl NodeState { - pub fn root() -> Self { - Self { - kind: NodeStateKind::ROOT, - surface: Plane::from(Side::A), - road_state: NodeStateRoad::ROOT, - enviro: EnviroFactors { + pub fn new(graph: &Graph, node: NodeId) -> Self { + let mut parents = graph + .parents(node) + .map(|(s, n)| ParentInfo { + node_id: n, + side: s, + node_state: graph.node_state(n), + }) + .fuse(); + let parents = [parents.next(), parents.next(), parents.next()]; + + let enviro = match (parents[0], parents[1]) { + (None, None) => EnviroFactors { max_elevation: 0.0, temperature: 0.0, rainfall: 0.0, blockiness: 0.0, }, - } - } - - pub fn child(&self, graph: &Graph, node: NodeId, side: Side) -> Self { - let mut d = graph.parents(node).map(|(s, n)| (s, graph.node_state(n))); - let enviro = match (d.next(), d.next()) { - (Some(_), None) => { - let parent_side = graph.primary_parent_side(node).unwrap(); - let parent_node = graph.neighbor(node, parent_side).unwrap(); - let parent_state = graph.node_state(parent_node); + (Some(parent), None) => { let spice = graph.hash_of(node) as u64; - EnviroFactors::varied_from(parent_state.enviro, spice) + EnviroFactors::varied_from(parent.node_state.enviro, spice) } - (Some((a_side, a_state)), Some((b_side, b_state))) => { - let ab_node = graph - .neighbor(graph.neighbor(node, a_side).unwrap(), b_side) - .unwrap(); - let ab_state = graph.node_state(ab_node); - EnviroFactors::continue_from(a_state.enviro, b_state.enviro, ab_state.enviro) + (Some(parent_a), Some(parent_b)) => { + let ab_node = graph.neighbor(parent_a.node_id, parent_b.side).unwrap(); + let ab_state = &graph.node_state(ab_node); + EnviroFactors::continue_from( + parent_a.node_state.enviro, + parent_b.node_state.enviro, + ab_state.enviro, + ) } _ => unreachable!(), }; - let child_kind = self.kind.child(side); - let child_road = self.road_state.child(side); + let kind = parents[0].map_or(NodeStateKind::ROOT, |p| p.node_state.kind.child(p.side)); + let road_state = parents[0].map_or(NodeStateRoad::ROOT, |p| { + p.node_state.road_state.child(p.side) + }); + + let horosphere = graph + .partial_node_state(node) + .candidate_horosphere + .filter(|h| h.should_generate(graph, node)); Self { - kind: child_kind, - surface: match child_kind { + kind, + surface: match kind { Land => Plane::from(Side::A), Sky => -Plane::from(Side::A), - _ => side * self.surface, + _ => parents[0].map(|p| p.side * p.node_state.surface).unwrap(), }, - road_state: child_road, + road_state, enviro, + horosphere, } } @@ -125,6 +157,13 @@ impl NodeState { } } +#[derive(Clone, Copy)] +struct ParentInfo<'a> { + node_id: NodeId, + side: Side, + node_state: &'a NodeState, +} + struct VoxelCoords { counter: u32, dimension: u8, @@ -176,6 +215,8 @@ pub struct ChunkParams { is_road_support: bool, /// Random quantity used to seed terrain gen node_spice: u64, + /// Horosphere to place in the chunk + horosphere: Option, } impl ChunkParams { @@ -194,6 +235,10 @@ impl ChunkParams { is_road_support: ((state.kind == Land) || (state.kind == DeepLand)) && ((state.road_state == East) || (state.road_state == West)), node_spice: graph.hash_of(chunk.node) as u64, + horosphere: state + .horosphere + .as_ref() + .map(|h| HorosphereChunk::new(h, chunk.vertex)), } } @@ -203,33 +248,6 @@ impl ChunkParams { /// Generate voxels making up the chunk pub fn generate_voxels(&self) -> VoxelData { - // Determine whether this chunk might contain a boundary between solid and void - let mut me_min = self.env.max_elevations[0]; - let mut me_max = self.env.max_elevations[0]; - for &me in &self.env.max_elevations[1..] { - me_min = me_min.min(me); - me_max = me_max.max(me); - } - // Maximum difference between elevations at the center of a chunk and any other point in the chunk - // TODO: Compute what this actually is, current value is a guess! Real one must be > 0.6 - // empirically. - const ELEVATION_MARGIN: f32 = 0.7; - let center_elevation = self - .surface - .distance_to_chunk(self.chunk, &na::Vector3::repeat(0.5)); - if (center_elevation - ELEVATION_MARGIN > me_max / TERRAIN_SMOOTHNESS) - && !(self.is_road || self.is_road_support) - { - // The whole chunk is above ground and not part of the road - return VoxelData::Solid(Material::Void); - } - - if (center_elevation + ELEVATION_MARGIN < me_min / TERRAIN_SMOOTHNESS) && !self.is_road { - // The whole chunk is underground - // TODO: More accurate VoxelData - return VoxelData::Solid(Material::Dirt); - } - let mut voxels = VoxelData::Solid(Material::Void); let mut rng = rand_pcg::Pcg64Mcg::seed_from_u64(hash(self.node_spice, self.chunk as u64)); @@ -241,11 +259,13 @@ impl ChunkParams { self.generate_road_support(&mut voxels); } + if let Some(horosphere) = &self.horosphere { + horosphere.generate(&mut voxels, self.dimension); + } + // TODO: Don't generate detailed data for solid chunks with no neighboring voids - if self.dimension > 4 && matches!(voxels, VoxelData::Dense(_)) { - self.generate_trees(&mut voxels, &mut rng); - } + self.generate_trees(&mut voxels, &mut rng); margins::initialize_margins(self.dimension, &mut voxels); voxels @@ -254,6 +274,33 @@ impl ChunkParams { /// Performs all terrain generation that can be done one voxel at a time and with /// only the containing chunk's surrounding nodes' envirofactors. fn generate_terrain(&self, voxels: &mut VoxelData, rng: &mut Pcg64Mcg) { + // Determine whether this chunk might contain a boundary between solid and void + let mut me_min = self.env.max_elevations[0]; + let mut me_max = self.env.max_elevations[0]; + for &me in &self.env.max_elevations[1..] { + me_min = me_min.min(me); + me_max = me_max.max(me); + } + // Maximum difference between elevations at the center of a chunk and any other point in the chunk + // TODO: Compute what this actually is, current value is a guess! Real one must be > 0.6 + // empirically. + const ELEVATION_MARGIN: f32 = 0.7; + let center_elevation = self + .surface + .distance_to_chunk(self.chunk, &na::Vector3::repeat(0.5)); + if center_elevation - ELEVATION_MARGIN > me_max / TERRAIN_SMOOTHNESS { + // The whole chunk is above ground + *voxels = VoxelData::Solid(Material::Void); + return; + } + if center_elevation + ELEVATION_MARGIN < me_min / TERRAIN_SMOOTHNESS { + // The whole chunk is underground + *voxels = VoxelData::Solid(Material::Dirt); + return; + } + + // Otherwise, the chunk might contain a solid/void boundary, so the full terrain generation + // code should run. let normal = Normal::new(0.0, 0.03).unwrap(); for (x, y, z) in VoxelCoords::new(self.dimension) { @@ -337,6 +384,12 @@ impl ChunkParams { /// Fills the half-plane below the road with wooden supports. fn generate_road_support(&self, voxels: &mut VoxelData) { + if voxels.is_solid() && voxels.get(0) != Material::Void { + // There is guaranteed no void to fill with the road supports, so + // nothing to do here. + return; + } + let plane = -Plane::from(Side::B); for (x, y, z) in VoxelCoords::new(self.dimension) { @@ -386,6 +439,16 @@ impl ChunkParams { /// and a block of leaves. The leaf block is on the opposite face of the /// wood block as the ground block. fn generate_trees(&self, voxels: &mut VoxelData, rng: &mut Pcg64Mcg) { + if voxels.is_solid() { + // No trees can be generated unless there's both land and air. + return; + } + + if self.dimension <= 4 { + // The tree generation algorithm can crash when the chunk size is too small. + return; + } + // margins are added to keep voxels outside the chunk from being read/written let random_position = Uniform::new(1, self.dimension - 1).unwrap(); diff --git a/common/src/plane.rs b/common/src/worldgen/plane.rs similarity index 100% rename from common/src/plane.rs rename to common/src/worldgen/plane.rs diff --git a/common/src/terraingen.rs b/common/src/worldgen/terraingen.rs similarity index 100% rename from common/src/terraingen.rs rename to common/src/worldgen/terraingen.rs