diff --git a/common/src/lib.rs b/common/src/lib.rs index c84b2bbe..a8d8e097 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -22,6 +22,7 @@ pub mod graph_collision; mod graph_entities; pub mod graph_ray_casting; pub mod lru_slab; +mod margins; pub mod math; pub mod node; mod plane; diff --git a/common/src/margins.rs b/common/src/margins.rs new file mode 100644 index 00000000..25402743 --- /dev/null +++ b/common/src/margins.rs @@ -0,0 +1,224 @@ +use crate::{ + dodeca::Vertex, + math, + node::VoxelData, + voxel_math::{ChunkAxisPermutation, ChunkDirection, CoordAxis, CoordSign, Coords}, +}; + +/// Updates the margins of both `voxels` and `neighbor_voxels` at the side they meet at. +/// It is assumed that `voxels` corresponds to a chunk that lies at `vertex` and that +/// `neighbor_voxels` is at direction `direction` from `voxels`. +pub fn fix_margins( + dimension: u8, + vertex: Vertex, + voxels: &mut VoxelData, + direction: ChunkDirection, + neighbor_voxels: &mut VoxelData, +) { + let neighbor_axis_permutation = neighbor_axis_permutation(vertex, direction); + + let margin_coord = CoordsWithMargins::margin_coord(dimension, direction.sign); + let edge_coord = CoordsWithMargins::edge_coord(dimension, direction.sign); + let voxel_data = voxels.data_mut(dimension); + let neighbor_voxel_data = neighbor_voxels.data_mut(dimension); + for j in 0..dimension { + for i in 0..dimension { + // Determine coordinates of the edge voxel (to read from) and the margin voxel (to write to) + // in voxel_data's perspective. To convert to neighbor_voxel_data's perspective, left-multiply + // by neighbor_axis_permutation. + let coords_of_edge_voxel = CoordsWithMargins(math::tuv_to_xyz( + direction.axis as usize, + [edge_coord, i + 1, j + 1], + )); + let coords_of_margin_voxel = CoordsWithMargins(math::tuv_to_xyz( + direction.axis as usize, + [margin_coord, i + 1, j + 1], + )); + + // Use neighbor_voxel_data to set margins of voxel_data + voxel_data[coords_of_margin_voxel.to_index(dimension)] = neighbor_voxel_data + [(neighbor_axis_permutation * coords_of_edge_voxel).to_index(dimension)]; + + // Use voxel_data to set margins of neighbor_voxel_data + neighbor_voxel_data + [(neighbor_axis_permutation * coords_of_margin_voxel).to_index(dimension)] = + voxel_data[coords_of_edge_voxel.to_index(dimension)]; + } + } +} + +/// Updates the margins of a given VoxelData to match the voxels they're next to. This is a good assumption to start +/// with before taking into account neighboring chunks because it means that no surface will be present on the boundaries +/// of the chunk, resulting in the least rendering. This is also generally accurate when the neighboring chunks are solid. +pub fn initialize_margins(dimension: u8, voxels: &mut VoxelData) { + // If voxels is solid, the margins are already set up the way they should be. + if voxels.is_solid() { + return; + } + + for direction in ChunkDirection::iter() { + let margin_coord = CoordsWithMargins::margin_coord(dimension, direction.sign); + let edge_coord = CoordsWithMargins::edge_coord(dimension, direction.sign); + let chunk_data = voxels.data_mut(dimension); + for j in 0..dimension { + for i in 0..dimension { + // Determine coordinates of the edge voxel (to read from) and the margin voxel (to write to). + let coords_of_edge_voxel = CoordsWithMargins(math::tuv_to_xyz( + direction.axis as usize, + [edge_coord, i + 1, j + 1], + )); + let coords_of_margin_voxel = CoordsWithMargins(math::tuv_to_xyz( + direction.axis as usize, + [margin_coord, i + 1, j + 1], + )); + + chunk_data[coords_of_margin_voxel.to_index(dimension)] = + chunk_data[coords_of_edge_voxel.to_index(dimension)]; + } + } + } +} + +fn neighbor_axis_permutation(vertex: Vertex, direction: ChunkDirection) -> ChunkAxisPermutation { + match direction.sign { + CoordSign::Plus => vertex.chunk_axis_permutations()[direction.axis as usize], + CoordSign::Minus => ChunkAxisPermutation::IDENTITY, + } +} + +/// Coordinates for a discrete voxel within a chunk, including margins +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct CoordsWithMargins(pub [u8; 3]); + +impl CoordsWithMargins { + /// Returns the array index in `VoxelData` corresponding to these coordinates + pub fn to_index(self, chunk_size: u8) -> usize { + let chunk_size_with_margin = chunk_size as usize + 2; + (self.0[0] as usize) + + (self.0[1] as usize) * chunk_size_with_margin + + (self.0[2] as usize) * chunk_size_with_margin.pow(2) + } + + /// Returns the x, y, or z coordinate that would correspond to the margin in the direction of `sign` + pub fn margin_coord(chunk_size: u8, sign: CoordSign) -> u8 { + match sign { + CoordSign::Plus => chunk_size + 1, + CoordSign::Minus => 0, + } + } + + /// Returns the x, y, or z coordinate that would correspond to the voxel meeting the chunk boundary in the direction of `sign` + pub fn edge_coord(chunk_size: u8, sign: CoordSign) -> u8 { + match sign { + CoordSign::Plus => chunk_size, + CoordSign::Minus => 1, + } + } +} + +impl From for CoordsWithMargins { + #[inline] + fn from(value: Coords) -> Self { + CoordsWithMargins([value.0[0] + 1, value.0[1] + 1, value.0[2] + 1]) + } +} + +impl std::ops::Index for CoordsWithMargins { + type Output = u8; + + #[inline] + fn index(&self, coord_axis: CoordAxis) -> &u8 { + self.0.index(coord_axis as usize) + } +} + +impl std::ops::IndexMut for CoordsWithMargins { + #[inline] + fn index_mut(&mut self, coord_axis: CoordAxis) -> &mut u8 { + self.0.index_mut(coord_axis as usize) + } +} + +impl std::ops::Mul for ChunkAxisPermutation { + type Output = CoordsWithMargins; + + fn mul(self, rhs: CoordsWithMargins) -> Self::Output { + let mut result = CoordsWithMargins([0; 3]); + for axis in CoordAxis::iter() { + result[self[axis]] = rhs[axis]; + } + result + } +} + +#[cfg(test)] +mod tests { + use crate::{dodeca::Vertex, voxel_math::Coords, world::Material}; + + use super::*; + + #[test] + fn test_fix_margins() { + // This test case can set up empirically by placing blocks and printing their coordinates to confirm which + // coordinates are adjacent to each other. + + // `voxels` lives at vertex F + let mut voxels = VoxelData::Solid(Material::Void); + voxels.data_mut(12)[Coords([11, 2, 10]).to_index(12)] = Material::WoodPlanks; + + // `neighbor_voxels` lives at vertex J + let mut neighbor_voxels = VoxelData::Solid(Material::Void); + neighbor_voxels.data_mut(12)[Coords([2, 10, 11]).to_index(12)] = Material::Grass; + + // Sanity check that voxel adjacencies are as expected. If the test fails here, it's likely that "dodeca.rs" was + // redesigned, and the test itself will have to be fixed, rather than the code being tested. + assert_eq!(Vertex::F.adjacent_vertices()[0], Vertex::J); + assert_eq!(Vertex::J.adjacent_vertices()[2], Vertex::F); + + // Sanity check that voxels are populated as expected, using `CoordsWithMargins` for consistency with the actual + // test case. + assert_eq!( + voxels.get(CoordsWithMargins([12, 3, 11]).to_index(12)), + Material::WoodPlanks + ); + assert_eq!( + neighbor_voxels.get(CoordsWithMargins([3, 11, 12]).to_index(12)), + Material::Grass + ); + + fix_margins( + 12, + Vertex::F, + &mut voxels, + ChunkDirection::PLUS_X, + &mut neighbor_voxels, + ); + + // Actual verification: Check that the margins were set correctly + assert_eq!( + voxels.get(CoordsWithMargins([13, 3, 11]).to_index(12)), + Material::Grass + ); + assert_eq!( + neighbor_voxels.get(CoordsWithMargins([3, 11, 13]).to_index(12)), + Material::WoodPlanks + ); + } + + #[test] + fn test_initialize_margins() { + let mut voxels = VoxelData::Solid(Material::Void); + voxels.data_mut(12)[Coords([11, 2, 10]).to_index(12)] = Material::WoodPlanks; + assert_eq!( + voxels.get(CoordsWithMargins([12, 3, 11]).to_index(12)), + Material::WoodPlanks + ); + + initialize_margins(12, &mut voxels); + + assert_eq!( + voxels.get(CoordsWithMargins([13, 3, 11]).to_index(12)), + Material::WoodPlanks + ); + } +} diff --git a/common/src/node.rs b/common/src/node.rs index 3afac4a2..20852644 100644 --- a/common/src/node.rs +++ b/common/src/node.rs @@ -12,7 +12,7 @@ use crate::proto::{BlockUpdate, Position, SerializedVoxelData}; use crate::voxel_math::{ChunkDirection, CoordAxis, CoordSign, Coords}; use crate::world::Material; use crate::worldgen::NodeState; -use crate::{math, Chunks}; +use crate::{margins, math, Chunks}; #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct ChunkId { @@ -92,34 +92,42 @@ impl Graph { Some((chunk, coords)) } - /// Populates a chunk with the given voxel data and ensures that margins are correctly cleared if necessary. - pub fn populate_chunk(&mut self, chunk: ChunkId, mut new_data: VoxelData, modified: bool) { - // New solid chunks should have their margin cleared if they are adjacent to any modified chunks. - // See the function description of VoxelData::clear_margin for why this is necessary. - if new_data.is_solid() { - // Loop through all six potential chunk neighbors. If any are modified, the `new_data` should have - // its margin cleared. - for chunk_direction in ChunkDirection::iter() { - if let Some(chunk_id) = - self.get_chunk_neighbor(chunk, chunk_direction.axis, chunk_direction.sign) - { - if let Chunk::Populated { modified: true, .. } = self[chunk_id] { - new_data.clear_margin(self.layout().dimension); - break; - } - } + /// Populates a chunk with the given voxel data and ensures that margins are correctly fixed up if necessary. + pub fn populate_chunk(&mut self, chunk: ChunkId, mut voxels: VoxelData, modified: bool) { + let dimension = self.layout().dimension; + // Fix up margins for the chunk we're inserting along with any neighboring chunks + for chunk_direction in ChunkDirection::iter() { + let Some(Chunk::Populated { + modified: neighbor_modified, + voxels: neighbor_voxels, + surface: neighbor_surface, + old_surface: neighbor_old_surface, + }) = self + .get_chunk_neighbor(chunk, chunk_direction.axis, chunk_direction.sign) + .map(|chunk_id| &mut self[chunk_id]) + else { + continue; + }; + // We need to fix up margins between the current chunk and the neighboring chunk if and only if + // there's a potential surface between them. This can occur if either is modified or if neither + // is designated as solid. Note that if one is designated as solid, that means that it's deep enough + // in the terrain or up in the air that there will be no surface between them. + if (!voxels.is_solid() && !neighbor_voxels.is_solid()) || modified || *neighbor_modified + { + margins::fix_margins( + dimension, + chunk.vertex, + &mut voxels, + chunk_direction, + neighbor_voxels, + ); + *neighbor_old_surface = neighbor_surface.take().or(*neighbor_old_surface); } } - // Existing adjacent solid chunks should have their margins cleared if the chunk we're populating is modified. - // See the function description of VoxelData::clear_margin for why this is necessary. - if modified { - self.clear_adjacent_solid_chunk_margins(chunk); - } - // After clearing any margins we needed to clear, we can now insert the data into the graph *self.get_chunk_mut(chunk).unwrap() = Chunk::Populated { - voxels: new_data, + voxels, modified, surface: None, old_surface: None, diff --git a/common/src/worldgen.rs b/common/src/worldgen.rs index 15fe148a..57f14e2a 100644 --- a/common/src/worldgen.rs +++ b/common/src/worldgen.rs @@ -4,7 +4,7 @@ use rand_distr::Normal; use crate::{ dodeca::{Side, Vertex}, graph::{Graph, NodeId}, - math, + margins, math, node::{ChunkId, VoxelData}, terraingen::VoronoiInfo, world::Material, @@ -246,6 +246,7 @@ impl ChunkParams { self.generate_trees(&mut voxels, &mut rng); } + margins::initialize_margins(self.dimension, &mut voxels); voxels }