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/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 3e4d2826..5aa0952b 100644 --- a/common/src/node.rs +++ b/common/src/node.rs @@ -8,10 +8,11 @@ use crate::collision_math::Ray; use crate::dodeca::Vertex; use crate::graph::{Graph, NodeId}; use crate::lru_slab::SlotId; +use crate::peer_traverser::PeerTraverser; use crate::proto::{BlockUpdate, Position, SerializedVoxelData}; use crate::voxel_math::{ChunkDirection, CoordAxis, CoordSign, Coords}; use crate::world::Material; -use crate::worldgen::NodeState; +use crate::worldgen::{NodeState, PartialNodeState}; use crate::{Chunks, margins}; /// Unique identifier for a single chunk (1/20 of a dodecahedron) in the graph @@ -28,30 +29,47 @@ 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.descenders(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.descenders(node_id) { - self.ensure_node_state(parent); + self.ensure_partial_node_state(node_id); + let mut peers = PeerTraverser::new(node_id); + while let Some(peer) = peers.ensure_next(self) { + self.ensure_partial_node_state(peer.node()); } - let node_state = self - .parent(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 +235,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..3678be26 --- /dev/null +++ b/common/src/peer_traverser.rs @@ -0,0 +1,544 @@ +use std::sync::LazyLock; + +use arrayvec::ArrayVec; + +use crate::{ + dodeca::Side, + graph::{Graph, NodeId}, +}; + +/// 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 path_from_peer(&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 path_from_base(&self) -> impl ExactSizeIterator + Clone + use<> { + self.child_path.clone().into_iter() + } +} + +/// Allows traversal through all `PeerNode`s for a given base node. +pub struct PeerTraverser { + /// The path leading from the base node to the ancestor node we're currently looking at + parent_path: ArrayVec, + + /// The nodes traversed by `parent_path`, including the start and end nodes. The length of `parent_path_nodes` + /// is always one greater than the length of `parent_path`. + parent_path_nodes: ArrayVec, + + /// Index into the appropriate element of `DEPTH1_CHILD_PATHS` or `DEPTH2_CHILD_PATHS` + /// that the iterator is currently on + child_path_index: usize, +} + +// This implementation is rather complicated and can be difficult to follow. It is recommended to see the +// `alternative_implementation` test for an equivalent algorithm. Most of the logic here is just maintaining +// state to allow `PeerTraverser` to work much like an iterator instead of storing everything into an array or vec. +impl PeerTraverser { + pub fn new(base_node: NodeId) -> Self { + PeerTraverser { + parent_path: ArrayVec::new(), + parent_path_nodes: ArrayVec::from_iter([base_node]), + child_path_index: 0, + } + } + + /// Assuming `parent_path` obeys shortlex rules up to right before the last + /// element for a given depth, returns whether it still obeys shortlex rules. + fn parent_path_end_is_shortlex(&self, depth: usize) -> bool { + if depth <= 1 { + // One-element node strings are always shortlex. + return true; + }; + let last = self.parent_path[depth - 1]; + let second_last = self.parent_path[depth - 2]; + if last == second_last { + // Backtracking is not valid (short part of shortlex) + return false; + } + if last.adjacent_to(second_last) && (last as usize) < (second_last as usize) { + // Unnecessarily having a higher side index first is not valid (lex part of shortlex). + // This is because adjacent sides in a path can always be swapped, still leading to the same node. + return false; + } + true + } + + /// Assuming `parent_path` and `parent_path_nodes` is already valid apart from possibly the last node, + /// increments to the next valid `parent_path` for the given depth and updates `parent_path_nodes`. + /// If `allow_unchanged_path` is true, the `parent_path` will not be incremented if it is already valid, but + /// `parent_path_nodes` will still be updated. + /// Returns `false` if no such valid path exists. + #[must_use] + fn increment_parent_path_for_depth( + &mut self, + graph: &Graph, + depth: usize, + mut allow_unchanged_path: bool, + ) -> bool { + if depth == 0 { + // Empty paths are always valid, but they cannot be incremented. + return allow_unchanged_path; + } + loop { + // Check if we're done incrementing and exit if so. + if allow_unchanged_path && self.parent_path_end_is_shortlex(depth) { + if let Some(node) = graph.neighbor( + self.parent_path_nodes[depth - 1], + self.parent_path[depth - 1], + ) { + if graph.length(self.parent_path_nodes[depth - 1]) == graph.length(node) + 1 { + self.parent_path_nodes[depth] = node; + return true; + } + } + } + + // Otherwise, increment. + let mut current_side = self.parent_path[depth - 1]; + current_side = Side::VALUES[(current_side as usize + 1) % Side::VALUES.len()]; // Cycle the current side + if current_side == Side::A { + // We looped, so make sure to increment an earlier part of the path. + if !self.increment_parent_path_for_depth(graph, depth - 1, false) { + return false; + } + } + self.parent_path[depth - 1] = current_side; + + allow_unchanged_path = true; // The path has changed, so it won't necessarily need to be changed again. + } + } + + /// Increments to the next valid `parent_path` for the given depth and updates `parent_path_nodes`. + /// Returns `false` if no such valid path exists. + #[must_use] + fn increment_parent_path(&mut self, graph: &Graph) -> bool { + let depth = self.parent_path.len(); + if depth == 0 { + // We're on the first iteration. If there are any peers at all, there will be one at depth 1. + self.parent_path.push(Side::A); + self.parent_path_nodes.push(NodeId::ROOT); + return self.increment_parent_path_for_depth(graph, 1, true); + } else if depth == 1 { + // Last time this was called, we were on a depth 1 path. Check if there's another depth 1 path to use. + if self.increment_parent_path_for_depth(graph, 1, false) { + return true; + } + // Otherwise, switch to depth 2 paths. + self.parent_path.fill(Side::A); + self.parent_path.push(Side::A); + self.parent_path_nodes.push(NodeId::ROOT); + // We're on the first depth 2 path, so we cannot make any assumptions. Therefore, to assure the resulting + // path is valid, we need to call `increment_parent_path_for_depth` for its depth 1 subpath first before + // calling it for the whole path. + return self.increment_parent_path_for_depth(graph, 1, true) + && self.increment_parent_path_for_depth(graph, 2, true); + } else if depth == 2 { + // Last time this was called, we were on a depth 2 path. Check if there's another depth 2 path to use. + return self.increment_parent_path_for_depth(graph, 2, false); + } + false + } + + /// Increments `child_path_index` to the index of the next valid path and returns the associated `PeerNode`. + /// If `allow_unchanged_path` is true, the `child_path_index` will not be incremented if it is already valid. + /// Returns `None` if no such valid path exists. + #[must_use] + fn increment_child_path( + &mut self, + graph: &mut impl GraphRef, + mut allow_unchanged_path: bool, + ) -> Option { + if self.parent_path.len() == 1 { + let child_paths = &DEPTH1_CHILD_PATHS[self.parent_path[0] as usize]; + loop { + if allow_unchanged_path { + if self.child_path_index >= child_paths.len() { + return None; + } + let child_side = child_paths[self.child_path_index]; + let mut current_node = self.parent_path_nodes[1]; + current_node = graph.neighbor(current_node, child_side); + if graph.length(current_node) == graph.length(self.parent_path_nodes[0]) { + return Some(PeerNode { + node_id: current_node, + parent_path: self.parent_path.clone(), + child_path: ArrayVec::from_iter([child_side]), + }); + } + } + self.child_path_index += 1; + allow_unchanged_path = true; + } + } else if self.parent_path.len() == 2 { + let child_paths = + &DEPTH2_CHILD_PATHS[self.parent_path[0] as usize][self.parent_path[1] as usize]; + loop { + if allow_unchanged_path { + if self.child_path_index >= child_paths.len() { + return None; + } + let child_path = &child_paths[self.child_path_index]; + let mut current_node = self.parent_path_nodes[2]; + for &side in child_path { + current_node = graph.neighbor(current_node, side); + } + if graph.length(current_node) == graph.length(self.parent_path_nodes[0]) { + let mut result_child_path = ArrayVec::new(); + result_child_path.push(child_path[0]); + result_child_path.push(child_path[1]); + return Some(PeerNode { + node_id: current_node, + parent_path: self.parent_path.clone(), + child_path: result_child_path, + }); + } + } + self.child_path_index += 1; + allow_unchanged_path = true; + } + } + None + } + + /// Returns the next peer node, or `None` if there are no peer nodes left. The implementation of `GraphRef` + /// is used to decide how to handle traversing through potentially not-yet-created nodes. + fn next_impl(&mut self, mut graph: impl GraphRef) -> Option { + let mut allow_unchanged_path = false; + loop { + // The parent path is guaranteed to be valid here. It starts valid before iteration because empty + // paths are valid, and it remains valid because it is only modified with `increment_parent_path`. + // Therefore, if we can increment the child path here, that is all we need to do. + if let Some(node) = self.increment_child_path(&mut graph, allow_unchanged_path) { + return Some(node); + } + // If there is no valid child path, we need to increment the parent path. + if !self.increment_parent_path(graph.as_ref()) { + return None; + } + allow_unchanged_path = true; // The path has changed, so it won't necessarily need to be changed again. + self.child_path_index = 0; // Since we incremented the parent, we should make sure to reset the child path index. + } + } + + /// Assumes the graph is expanded enough to traverse peer nodes and returns the next peer node, + /// or `None` if there are no peer nodes left. Panics if this assumption is false. + pub fn next(&mut self, graph: &Graph) -> Option { + self.next_impl(AssertingGraphRef { graph }) + } + + /// Returns the next peer node, expanding the graph if necessary, or `None` if there are no peer nodes left. + pub fn ensure_next(&mut self, graph: &mut Graph) -> Option { + self.next_impl(ExpandingGraphRef { graph }) + } +} + +/// All paths that are compatible with 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 are compatible with 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 length(&self, node: NodeId) -> u32; + fn neighbor(&mut self, node: NodeId, side: Side) -> NodeId; +} + +/// A `GraphRef` that asserts that all the nodes it needs already exist +struct AssertingGraphRef<'a> { + graph: &'a Graph, +} + +impl AsRef for AssertingGraphRef<'_> { + #[inline] + fn as_ref(&self) -> &Graph { + self.graph + } +} + +impl GraphRef for AssertingGraphRef<'_> { + #[inline] + fn length(&self, node: NodeId) -> u32 { + self.graph.length(node) + } + + #[inline] + fn neighbor(&mut self, node: NodeId, side: Side) -> NodeId { + self.graph.neighbor(node, side).unwrap() + } +} + +/// A `GraphRef` that expands the graph as necessary +struct ExpandingGraphRef<'a> { + graph: &'a mut Graph, +} + +impl GraphRef for ExpandingGraphRef<'_> { + #[inline] + fn length(&self, node: NodeId) -> u32 { + self.graph.length(node) + } + + #[inline] + fn neighbor(&mut self, node: NodeId, side: Side) -> NodeId { + self.graph.ensure_neighbor(node, side) + } +} + +impl AsRef for ExpandingGraphRef<'_> { + #[inline] + 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 mut traverser = PeerTraverser::new(base_node); + for expected_path in expected_paths { + let peer = traverser.ensure_next(&mut graph).unwrap(); + assert_eq!( + peer.path_from_peer().collect::>(), + expected_path.0.to_vec(), + ); + assert_eq!( + peer.path_from_base().collect::>(), + expected_path.1.to_vec(), + ); + } + + assert!(traverser.ensure_next(&mut graph).is_none()); + } + + #[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(); + let mut traverser = PeerTraverser::new(base_node); + while let Some(peer) = traverser.ensure_next(&mut graph) { + 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.path_from_base()); + let destination_from_peer = + node_from_path(&mut graph, peer_node, peer.path_from_peer()); + + assert_eq!( + graph.length(base_node), + graph.length(peer_node), + "The base and peer nodes must have the same depth in the graph." + ); + assert_eq!( + graph.length(base_node) + peer.path_from_base().len() as u32, + graph.length(destination_from_base), + "path_from_base must not backtrack to a parent node." + ); + assert_eq!( + graph.length(peer_node) + peer.path_from_peer().len() as u32, + graph.length(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." + ); + } + } + + #[test] + fn alternative_implementation() { + // Tests that the traverser's implementation is equivalent to a much simpler implementation that returns + // everything at once instead of maintaining a state machine. + 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 traverser = PeerTraverser::new(base_node); + + // Depth 1 paths + for (parent_side, parent_node) in graph.descenders(base_node) { + for &child_side in &DEPTH1_CHILD_PATHS[parent_side as usize] { + let peer_node = graph.ensure_neighbor(parent_node, child_side); + if graph.length(peer_node) == graph.length(base_node) { + assert_peer_node_eq( + PeerNode { + node_id: peer_node, + parent_path: ArrayVec::from_iter([parent_side]), + child_path: ArrayVec::from_iter([child_side]), + }, + traverser.ensure_next(&mut graph).unwrap(), + ); + } + } + } + + // Depth 2 paths + for (parent_side0, parent_node0) in graph.descenders(base_node) { + for (parent_side1, parent_node1) in graph.descenders(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.ensure_neighbor(parent_node1, child_sides[0]); + let peer_node = graph.ensure_neighbor(peer_node_parent, child_sides[1]); + if graph.length(peer_node) == graph.length(base_node) { + assert_peer_node_eq( + PeerNode { + node_id: peer_node, + parent_path: ArrayVec::from_iter([parent_side0, parent_side1]), + child_path: ArrayVec::from_iter(child_sides), + }, + traverser.ensure_next(&mut graph).unwrap(), + ); + } + } + } + } + + assert!(traverser.ensure_next(&mut graph).is_none()); + } + + fn assert_peer_node_eq(left: PeerNode, right: PeerNode) { + assert_eq!(left.node_id, right.node_id); + assert_eq!(left.parent_path, right.parent_path); + assert_eq!(left.child_path, right.child_path); + } +} diff --git a/common/src/worldgen/horosphere.rs b/common/src/worldgen/horosphere.rs new file mode 100644 index 00000000..37cb34c0 --- /dev/null +++ b/common/src/worldgen/horosphere.rs @@ -0,0 +1,324 @@ +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::MVector, + node::VoxelData, + peer_traverser::PeerTraverser, + voxel_math::Coords, + world::Material, +}; + +/// 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; + +/// 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(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. + owner: NodeId, + + /// The vector representing the horosphere in the perspective of the relevant node. A vector + /// `point` is in this horosphere if `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 consequence of this invariant is that this vector's length is always + /// proportional to its w-coordinate. If the w-coordinate is 1, the horosphere intersects the origin. + /// If it's less than 1, the horosphere contains the origin, and if it's greater than 1, the origin + /// is outside the horosphere. The vector points in the direction of the horosphere's ideal point. + /// + /// 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 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 horosphere, 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 + .descenders(node_id) + .filter_map(|(parent_side, parent_id)| { + (graph.node_state(parent_id).horosphere.as_ref()) + .filter(|h| h.should_propagate(parent_side)) + .map(|h| h.propagate(parent_side)) + }); + + let mut horosphere = horospheres_to_average_iter.next()?; + let mut count = 1; + for other in horospheres_to_average_iter { + // Take an average of all horospheres 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.average_with(other, 1.0 / count as f32); + } + + horosphere.renormalize(); + Some(horosphere) + } + + /// 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(spice.wrapping_add(42)); + for _ in 0..rng.sample(Poisson::new(HOROSPHERE_DENSITY).unwrap()) as u32 { + let horosphere_pos = Self::random_horosphere_pos(&mut rng); + if Self::is_horosphere_pos_valid(graph, node_id, &horosphere_pos) { + return Some(HorosphereNode { + owner: node_id, + pos: horosphere_pos, + }); + } + } + None + } + + /// Whether the horosphere will still be relevant after crossing the given side of the current node. + fn should_propagate(&self, side: Side) -> bool { + // TODO: Consider adding epsilon to ensure floating point precision + // doesn't cause `average_with` to fail + + // If the horosphere is entirely behind the plane bounded by the given side, it is no longer relevant. + // The relationship between a horosphere and a directed plane given by a normal vector can be determined with + // the Minkowski inner product (mip) between their respective vectors. If it's positive, the ideal point is in front + // of the plane, and if it's negative, the ideal point is behind it. The horosphere intersects the plane + // exactly when the mip is between -1 and 1. Therefore, if it's less than -1, the horosphere is entirely + // behind the plane. + self.pos.mip(side.normal()) > -1.0 + } + + /// Returns an estimate of the `HorosphereNode` corresponding to the node adjacent to the current node + /// at the given side. The estimates given by multiple nodes may be used to produce the actual `HorosphereNode`. + fn propagate(&self, side: Side) -> HorosphereNode { + HorosphereNode { + owner: self.owner, + pos: side.reflection() * self.pos, + } + } + + /// 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.pos = self.pos * (1.0 - other_weight) + other.pos * other_weight; + } + + /// 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. + fn renormalize(&mut self) { + self.pos.w = self.pos.xyz().norm(); + } + + /// 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` have to compete to exist as an actual horosphere, returns whether `self` wins. + fn has_priority(&self, other: &HorosphereNode, node_id: NodeId) -> bool { + // If both horospheres are fresh, use the w-coordinate as an arbitrary + // tie-breaker to decide which horosphere should win. + !self.is_fresh(node_id) || (other.is_fresh(node_id) && self.pos.w < other.pos.w) + } + + /// 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; + } + + let mut peers = PeerTraverser::new(node_id); + while let Some(peer) = peers.next(graph) { + 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 they share a node in common. + && peer_horosphere.should_propagate_through_path(peer.path_from_peer()) + && self.should_propagate_through_path(peer.path_from_base()) + { + return false; + } + } + true + } + + /// This function is much like `should_propagate`, but it takes in a sequence of sides instead + /// of a single side. + fn should_propagate_through_path(&self, mut path: impl ExactSizeIterator) -> bool { + let mut current_horosphere = self.clone(); + while let Some(side) = path.next() { + if !current_horosphere.should_propagate(side) { + return false; + } + if path.len() == 0 { + return true; + } + current_horosphere = current_horosphere.propagate(side); + } + 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 descenders + /// (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_pos_valid( + graph: &Graph, + node_id: NodeId, + horosphere_pos: &MVector, + ) -> bool { + // See `should_propagate` for an explanation of what the mip between a horosphere position and + // a plane's normal signifies. + Side::iter().all(|s| s.normal().mip(&horosphere_pos) < 1.0) + && (graph.descenders(node_id)).all(|(s, _)| s.normal().mip(horosphere_pos) < -1.0) + } + + /// Returns a vector representing a uniformly random horosphere within a certain distance to the origin. + /// This distance is set up to ensure that `is_horosphere_pos_valid` would always return false if it were any futher, + /// ensuring that this function does not artificially restrict which horospheres can be created. Rejection + /// sampling is used to more precisely finetune the list of allowed horospheres. + fn random_horosphere_pos(rng: &mut Pcg64Mcg) -> MVector { + // 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::()) * Self::MAX_OWNED_HOROSPHERE_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. + MVector::new( + w * sin_phi * cosf(theta), + w * sin_phi * sinf(theta), + w * cos_phi, + w, + ) + } + + /// 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 vector representing the horosphere in the perspective of the relevant chunk. + /// See `HorosphereNode::pos` for details. + pub pos: MVector, +} + +impl HorosphereChunk { + /// Creates a `HorosphereChunk` based on a `HorosphereNode` + pub fn new(horosphere_node: &HorosphereNode, vertex: Vertex) -> Self { + HorosphereChunk { + pos: vertex.node_to_dual() * horosphere_node.pos, + } + } + + /// Rasterizes the horosphere chunk into the given `VoxelData` + pub fn generate(&self, voxels: &mut VoxelData, chunk_size: u8) { + for x in 0..chunk_size { + for y in 0..chunk_size { + for z 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 pos.mip(&self.pos) > -1.0 { + voxels.data_mut(chunk_size)[Coords([x, y, z]).to_index(chunk_size)] = + Material::RedSandstone; + } + } + } + } + } +} + +#[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, + HorosphereNode::MAX_OWNED_HOROSPHERE_W, + epsilon = 1.0e-6 + ); + } +} 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 63804813..9d7d2af1 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,63 +67,91 @@ 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. + 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 + .descenders(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 - .descenders(node) - .map(|(s, n)| (s, graph.node_state(n))); - let enviro = match (d.next(), d.next()) { - (Some(_), None) => { - let parent_side = graph.parent(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.as_ref()).and_then(|h| { + if h.should_generate(graph, node) { + Some(h.clone()) + } else { + None + } + }); 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, } } @@ -127,6 +160,13 @@ impl NodeState { } } +#[derive(Clone, Copy)] +struct ParentInfo<'a> { + node_id: NodeId, + side: Side, + node_state: &'a NodeState, +} + struct VoxelCoords { counter: u32, dimension: u8, @@ -178,6 +218,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 { @@ -196,6 +238,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)), } } @@ -205,33 +251,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)); @@ -243,11 +262,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 @@ -256,6 +277,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) { @@ -339,6 +387,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) { @@ -388,6 +442,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