diff --git a/client/shaders/surface-extraction/extract.comp b/client/shaders/surface-extraction/extract.comp index bab68ef6..5a969030 100644 --- a/client/shaders/surface-extraction/extract.comp +++ b/client/shaders/surface-extraction/extract.comp @@ -76,6 +76,9 @@ bool find_face(out Face info) { // Flip face around if the neighbor is the solid one info.inward = self_mat == 0; info.material = self_mat | neighbor_mat; + // If self or neighbor is a void margin, then no surface should be generated, as any surface + // that would be rendered is the responsibility of the adjacent chunk. + if ((self_mat == 0 && info.voxel[info.axis] == dimension) || (neighbor_mat == 0 && neighbor[info.axis] == -1)) return false; return (neighbor_mat == 0) != (self_mat == 0); } diff --git a/client/src/sim.rs b/client/src/sim.rs index 78b9dff7..09ddad34 100644 --- a/client/src/sim.rs +++ b/client/src/sim.rs @@ -495,7 +495,7 @@ impl Sim { hit.chunk, hit.voxel_coords, hit.face_axis, - hit.face_direction, + hit.face_sign, )? } else { (hit.chunk, hit.voxel_coords) diff --git a/common/src/chunk_collision.rs b/common/src/chunk_collision.rs index 3fbcbe8f..3a7f3a0c 100644 --- a/common/src/chunk_collision.rs +++ b/common/src/chunk_collision.rs @@ -1,7 +1,8 @@ use crate::{ collision_math::Ray, math, - node::{ChunkLayout, Coords, VoxelAABB, VoxelData}, + node::{ChunkLayout, VoxelAABB, VoxelData}, + voxel_math::Coords, world::Material, }; diff --git a/common/src/chunk_ray_casting.rs b/common/src/chunk_ray_casting.rs index 61b8abc8..b764ed25 100644 --- a/common/src/chunk_ray_casting.rs +++ b/common/src/chunk_ray_casting.rs @@ -1,7 +1,8 @@ use crate::{ collision_math::Ray, math, - node::{ChunkLayout, CoordAxis, CoordDirection, Coords, VoxelAABB, VoxelData}, + node::{ChunkLayout, VoxelAABB, VoxelData}, + voxel_math::{CoordAxis, CoordSign, Coords}, world::Material, }; @@ -16,7 +17,7 @@ pub struct ChunkCastHit { pub face_axis: CoordAxis, /// The direction along `face_axis` corresponding to the outside of the face that was hit. - pub face_direction: CoordDirection, + pub face_sign: CoordSign, } /// Performs ray casting against the voxels in the chunk with the given `voxel_data` @@ -84,16 +85,16 @@ fn find_face_collision( // Which side we approach the plane from affects which voxel we want to use for hit detection. // If exiting a chunk via a chunk boundary, hit detection is handled by a different chunk. // We also want to retain this face_direction for reporting the hit result later. - let (face_direction, voxel_t) = if math::mip(&ray.direction, &normal) < 0.0 { + let (face_sign, voxel_t) = if math::mip(&ray.direction, &normal) < 0.0 { if t == 0 { continue; } - (CoordDirection::Plus, t - 1) + (CoordSign::Plus, t - 1) } else { if t == layout.dimension() { continue; } - (CoordDirection::Minus, t) + (CoordSign::Minus, t) }; let ray_endpoint = ray.ray_point(new_tanh_distance); @@ -121,7 +122,7 @@ fn find_face_collision( tanh_distance: new_tanh_distance, voxel_coords: Coords(math::tuv_to_xyz(t_axis, [voxel_t, voxel_u, voxel_v])), face_axis: CoordAxis::try_from(t_axis).unwrap(), - face_direction, + face_sign, }); } @@ -221,13 +222,13 @@ mod tests { ray: &Ray, tanh_distance: f32, expected_face_axis: CoordAxis, - expected_face_direction: CoordDirection, + expected_face_sign: CoordSign, ) { let hit = chunk_ray_cast_wrapper(ctx, ray, tanh_distance); let hit = hit.expect("collision expected"); assert_eq!(hit.voxel_coords, Coords([1, 1, 1])); assert_eq!(hit.face_axis, expected_face_axis); - assert_eq!(hit.face_direction, expected_face_direction); + assert_eq!(hit.face_sign, expected_face_sign); // sanity_check_normal(ray, &hit.unwrap()); TODO: Check other results } @@ -245,13 +246,7 @@ mod tests { [0.0, 1.5, 1.5], [1.5, 1.5, 1.5], |ray, tanh_distance| { - test_face_collision( - &ctx, - ray, - tanh_distance, - CoordAxis::X, - CoordDirection::Minus, - ); + test_face_collision(&ctx, ray, tanh_distance, CoordAxis::X, CoordSign::Minus); }, ); @@ -260,7 +255,7 @@ mod tests { [1.5, 1.5, 3.0], [1.5, 1.5, 1.5], |ray, tanh_distance| { - test_face_collision(&ctx, ray, tanh_distance, CoordAxis::Z, CoordDirection::Plus); + test_face_collision(&ctx, ray, tanh_distance, CoordAxis::Z, CoordSign::Plus); }, ); diff --git a/common/src/dodeca.rs b/common/src/dodeca.rs index 6df0b310..d4f9208a 100644 --- a/common/src/dodeca.rs +++ b/common/src/dodeca.rs @@ -1,7 +1,9 @@ //! Tools for processing the geometry of a right dodecahedron + +use data::*; use serde::{Deserialize, Serialize}; -use crate::dodeca::data::*; +use crate::voxel_math::ChunkAxisPermutation; /// Sides of a right dodecahedron #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] @@ -112,18 +114,45 @@ impl Vertex { sides_to_vertex()[a as usize][b as usize][c as usize] } - /// Sides incident to this vertex, in canonical order + /// Sides incident to this vertex, in canonical order. + /// + /// This canonical order determines the X, Y, and Z axes of the chunk + /// corresponding to the vertex. #[inline] pub fn canonical_sides(self) -> [Side; 3] { vertex_sides()[self as usize] } - /// Vertices adjacent to this vertex, opposite the sides in canonical order + /// Vertices adjacent to this vertex in canonical order. + /// + /// The canonical order of adjacent vertices is based on the canonical order + /// of sides incident to the vertex, as each of the three adjacent vertices + /// corresponds to one of the three sides. As for which side, when two + /// vertices are adjacent, they share two out of three sides of the + /// dodecahedron. The side they do _not_ share is the side they correspond + /// to. + /// + /// Put another way, anything leaving a chunk in the negative-X direction + /// will end up crossing `canonical_sides()[0]`, while anything leaving a + /// chunk in the positive-X direction will end up arriving at + /// `adjacent_vertices()[0]`. #[inline] pub fn adjacent_vertices(self) -> [Vertex; 3] { adjacent_vertices()[self as usize] } + /// Chunk axes permutations for vertices adjacent to this vertex in + /// canonical order. + /// + /// The chunks of two adjacent vertices meet at a plane. When swiching + /// reference frames from one vertex to another, it is necessary to reflect + /// about this plane and then apply the permutation returned by this + /// function. + #[inline] + pub fn chunk_axis_permutations(self) -> &'static [ChunkAxisPermutation; 3] { + &chunk_axis_permutations()[self as usize] + } + /// For each vertex of the cube dual to this dodecahedral vertex, provides an iterator of at /// most 3 steps to reach the corresponding graph node, and binary coordinates of the vertex in /// question with respect to the origin vertex of the cube. @@ -227,10 +256,12 @@ pub const BOUNDING_SPHERE_RADIUS_F64: f64 = 1.2264568712514068; pub const BOUNDING_SPHERE_RADIUS: f32 = BOUNDING_SPHERE_RADIUS_F64 as f32; mod data { + use std::array; use std::sync::OnceLock; use crate::dodeca::{Side, Vertex, SIDE_COUNT, VERTEX_COUNT}; use crate::math; + use crate::voxel_math::ChunkAxisPermutation; /// Whether two sides share an edge pub fn adjacent() -> &'static [[bool; SIDE_COUNT]; SIDE_COUNT] { @@ -334,6 +365,39 @@ mod data { }) } + // Which transformations have to be done after a reflection to switch reference frames from one vertex + // to one of its adjacent vertices (ordered similarly to ADJACENT_VERTICES) + pub fn chunk_axis_permutations() -> &'static [[ChunkAxisPermutation; 3]; VERTEX_COUNT] { + static LOCK: OnceLock<[[ChunkAxisPermutation; 3]; VERTEX_COUNT]> = OnceLock::new(); + LOCK.get_or_init(|| { + array::from_fn(|vertex| { + array::from_fn(|result_index| { + let mut test_sides = vertex_sides()[vertex]; + // Keep modifying the result_index'th element of test_sides until its three elements are all + // adjacent to a single vertex (determined using `Vertex::from_sides`). + for side in Side::iter() { + if side == vertex_sides()[vertex][result_index] { + continue; + } + test_sides[result_index] = side; + let Some(adjacent_vertex) = + Vertex::from_sides(test_sides[0], test_sides[1], test_sides[2]) + else { + continue; + }; + // Compare the natural permutation of sides after a reflection from `vertex` to `adjacent_vertex` + // to the canonical permutation of the sides for `adjacent_vertex`. + return ChunkAxisPermutation::from_permutation( + test_sides, + adjacent_vertex.canonical_sides(), + ); + } + panic!("No suitable vertex found"); + }) + }) + }) + } + /// Transform that converts from cube-centric coordinates to dodeca-centric coordinates pub fn dual_to_node_f64() -> &'static [na::Matrix4; VERTEX_COUNT] { static LOCK: OnceLock<[na::Matrix4; VERTEX_COUNT]> = OnceLock::new(); @@ -484,6 +548,57 @@ mod tests { } } + #[test] + fn adjacent_chunk_axis_permutations() { + // Assumptions for this test to be valid. If any assertions in this section fail, the test itself + // needs to be modified + assert_eq!(Vertex::A.canonical_sides(), [Side::A, Side::B, Side::C]); + assert_eq!(Vertex::B.canonical_sides(), [Side::A, Side::B, Side::E]); + + assert_eq!(Vertex::F.canonical_sides(), [Side::B, Side::C, Side::F]); + assert_eq!(Vertex::J.canonical_sides(), [Side::C, Side::F, Side::H]); + + // Test cases + + // Variables with name vertex_?_canonical_sides_reflected refer to the canonical sides + // of a particular vertex after a reflection that moves it to another vertex. + // For instance, vertex_a_canonical_sides_reflected is similar to Vertex::A.canonical_sides(), + // but one of the sides is changed to match Vertex B, but the order of the other two sides is left alone. + let vertex_a_canonical_sides_reflected = [Side::A, Side::B, Side::E]; + let vertex_b_canonical_sides_reflected = [Side::A, Side::B, Side::C]; + assert_eq!( + Vertex::A.chunk_axis_permutations()[2], + ChunkAxisPermutation::from_permutation( + vertex_a_canonical_sides_reflected, + Vertex::B.canonical_sides() + ) + ); + assert_eq!( + Vertex::B.chunk_axis_permutations()[2], + ChunkAxisPermutation::from_permutation( + vertex_b_canonical_sides_reflected, + Vertex::A.canonical_sides() + ) + ); + + let vertex_f_canonical_sides_reflected = [Side::H, Side::C, Side::F]; + let vertex_j_canonical_sides_reflected = [Side::C, Side::F, Side::B]; + assert_eq!( + Vertex::F.chunk_axis_permutations()[0], + ChunkAxisPermutation::from_permutation( + vertex_f_canonical_sides_reflected, + Vertex::J.canonical_sides() + ) + ); + assert_eq!( + Vertex::J.chunk_axis_permutations()[2], + ChunkAxisPermutation::from_permutation( + vertex_j_canonical_sides_reflected, + Vertex::F.canonical_sides() + ) + ); + } + #[test] fn side_is_facing() { for side in Side::iter() { diff --git a/common/src/graph_collision.rs b/common/src/graph_collision.rs index cf0c8a1a..1f842aa9 100644 --- a/common/src/graph_collision.rs +++ b/common/src/graph_collision.rs @@ -88,9 +88,10 @@ mod tests { collision_math::Ray, dodeca::{self, Side, Vertex}, graph::{Graph, NodeId}, - node::{populate_fresh_nodes, Coords, VoxelData}, + node::{populate_fresh_nodes, VoxelData}, proto::Position, traversal::{ensure_nearby, nearby_nodes}, + voxel_math::Coords, world::Material, }; diff --git a/common/src/graph_ray_casting.rs b/common/src/graph_ray_casting.rs index d8246abc..933641bb 100644 --- a/common/src/graph_ray_casting.rs +++ b/common/src/graph_ray_casting.rs @@ -2,9 +2,10 @@ use crate::{ chunk_ray_casting::chunk_ray_cast, collision_math::Ray, graph::Graph, - node::{Chunk, ChunkId, CoordAxis, CoordDirection, Coords}, + node::{Chunk, ChunkId}, proto::Position, traversal::RayTraverser, + voxel_math::{CoordAxis, CoordSign, Coords}, }; /// Performs ray casting against the voxels in the `DualGraph` @@ -54,7 +55,7 @@ pub fn ray_cast( chunk, voxel_coords: hit.voxel_coords, face_axis: hit.face_axis, - face_direction: hit.face_direction, + face_sign: hit.face_sign, }) }); } @@ -81,5 +82,5 @@ pub struct GraphCastHit { pub face_axis: CoordAxis, /// The direction along `face_axis` corresponding to the outside of the face that was hit. - pub face_direction: CoordDirection, + pub face_sign: CoordSign, } diff --git a/common/src/lib.rs b/common/src/lib.rs index 0d17e658..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; @@ -29,6 +30,7 @@ pub mod proto; mod sim_config; pub mod terraingen; pub mod traversal; +pub mod voxel_math; pub mod world; pub mod worldgen; diff --git a/common/src/margins.rs b/common/src/margins.rs new file mode 100644 index 00000000..1329d09f --- /dev/null +++ b/common/src/margins.rs @@ -0,0 +1,337 @@ +use crate::{ + dodeca::Vertex, + graph::Graph, + math, + node::{Chunk, ChunkId, VoxelData}, + voxel_math::{ChunkAxisPermutation, ChunkDirection, CoordAxis, CoordSign, Coords}, + world::Material, +}; + +/// 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)]; + } + } + } +} + +/// Assuming that the voxel at `chunk` and `coords` is set to `material`, updates the cooresponding +/// margin in the chunk at direction `direction` from `chunk` if such a margin exists. Unpopulated chunks +/// are ignored. +pub fn update_margin_voxel( + graph: &mut Graph, + chunk: ChunkId, + coords: Coords, + direction: ChunkDirection, + material: Material, +) { + let coords: CoordsWithMargins = coords.into(); + let dimension = graph.layout().dimension(); + let edge_coord = match direction.sign { + CoordSign::Plus => dimension, + CoordSign::Minus => 1, + }; + if coords[direction.axis] != edge_coord { + // There is nothing to do if we're not on an edge voxel. + return; + } + let Some(Chunk::Populated { + voxels: neighbor_voxels, + surface: neighbor_surface, + old_surface: neighbor_old_surface, + .. + }) = graph + .get_chunk_neighbor(chunk, direction.axis, direction.sign) + .map(|chunk_id| &mut graph[chunk_id]) + else { + // If the neighboring chunk to check is not populated, there is nothing to do. + return; + }; + + let margin_coord = match direction.sign { + CoordSign::Plus => dimension + 1, + CoordSign::Minus => 0, + }; + let neighbor_axis_permutation = neighbor_axis_permutation(chunk.vertex, direction); + let mut neighbor_coords = coords; + neighbor_coords[direction.axis] = margin_coord; + neighbor_coords = neighbor_axis_permutation * neighbor_coords; + + neighbor_voxels.data_mut(dimension)[neighbor_coords.to_index(dimension)] = material; + *neighbor_old_surface = neighbor_surface.take().or(*neighbor_old_surface); +} + +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, graph::NodeId, node, 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 + ); + } + + #[test] + fn test_update_margin_voxel() { + let mut graph = Graph::new(12); + let current_vertex = Vertex::A; + let neighbor_vertex = current_vertex.adjacent_vertices()[1]; + let neighbor_node = + graph.ensure_neighbor(NodeId::ROOT, current_vertex.canonical_sides()[0]); + node::populate_fresh_nodes(&mut graph); + + // These are the chunks this test will work with. + let current_chunk = ChunkId::new(NodeId::ROOT, current_vertex); + let node_neighbor_chunk = ChunkId::new(neighbor_node, current_vertex); + let vertex_neighbor_chunk = ChunkId::new(NodeId::ROOT, neighbor_vertex); + + // Populate relevant chunks with void + for chunk in [current_chunk, node_neighbor_chunk, vertex_neighbor_chunk] { + *graph.get_chunk_mut(chunk).unwrap() = Chunk::Populated { + voxels: VoxelData::Solid(Material::Void), + modified: false, + surface: None, + old_surface: None, + }; + } + + // Update and check the margins of node_neighbor_chunk + update_margin_voxel( + &mut graph, + current_chunk, + Coords([0, 7, 9]), + ChunkDirection::MINUS_X, + Material::WoodPlanks, + ); + let Chunk::Populated { + voxels: node_neighbor_voxels, + .. + } = graph.get_chunk_mut(node_neighbor_chunk).unwrap() + else { + panic!("node_neighbor_chunk should have just been populated by this test"); + }; + assert_eq!( + node_neighbor_voxels.get(CoordsWithMargins([0, 8, 10]).to_index(12)), + Material::WoodPlanks + ); + + // Update and check the margins of vertex_neighbor_chunk + update_margin_voxel( + &mut graph, + current_chunk, + Coords([5, 11, 9]), + ChunkDirection::PLUS_Y, + Material::Grass, + ); + let Chunk::Populated { + voxels: vertex_neighbor_voxels, + .. + } = graph.get_chunk_mut(vertex_neighbor_chunk).unwrap() + else { + panic!("vertex_neighbor_chunk should have just been populated by this test"); + }; + assert_eq!( + vertex_neighbor_voxels.get(CoordsWithMargins([6, 10, 13]).to_index(12)), + Material::Grass + ); + } +} diff --git a/common/src/node.rs b/common/src/node.rs index a6e31dca..45045fec 100644 --- a/common/src/node.rs +++ b/common/src/node.rs @@ -9,9 +9,10 @@ use crate::dodeca::Vertex; use crate::graph::{Graph, NodeId}; 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::{math, Chunks}; +use crate::{margins, math, Chunks}; #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct ChunkId { @@ -47,14 +48,14 @@ impl Graph { &self, chunk: ChunkId, coord_axis: CoordAxis, - coord_direction: CoordDirection, + coord_sign: CoordSign, ) -> Option { - match coord_direction { - CoordDirection::Plus => Some(ChunkId::new( + match coord_sign { + CoordSign::Plus => Some(ChunkId::new( chunk.node, chunk.vertex.adjacent_vertices()[coord_axis as usize], )), - CoordDirection::Minus => Some(ChunkId::new( + CoordSign::Minus => Some(ChunkId::new( self.neighbor( chunk.node, chunk.vertex.canonical_sides()[coord_axis as usize], @@ -69,73 +70,64 @@ impl Graph { mut chunk: ChunkId, mut coords: Coords, coord_axis: CoordAxis, - coord_direction: CoordDirection, + coord_sign: CoordSign, ) -> Option<(ChunkId, Coords)> { - if coords[coord_axis] == self.layout().dimension - 1 - && coord_direction == CoordDirection::Plus - { - let new_vertex = chunk.vertex.adjacent_vertices()[coord_axis as usize]; - // Permute coordinates based on differences in the canonical orders between the old - // and new vertex - let [coord_plane0, coord_plane1] = coord_axis.other_axes(); - let mut new_coords = Coords([0; 3]); - for current_axis in CoordAxis::iter() { - if new_vertex.canonical_sides()[current_axis as usize] - == chunk.vertex.canonical_sides()[coord_plane0 as usize] - { - new_coords[current_axis] = coords[coord_plane0]; - } else if new_vertex.canonical_sides()[current_axis as usize] - == chunk.vertex.canonical_sides()[coord_plane1 as usize] - { - new_coords[current_axis] = coords[coord_plane1]; - } else { - new_coords[current_axis] = coords[coord_axis]; + if coords[coord_axis] == Coords::edge_coord(self.layout().dimension, coord_sign) { + match coord_sign { + CoordSign::Plus => { + coords = chunk.vertex.chunk_axis_permutations()[coord_axis as usize] * coords; + chunk.vertex = chunk.vertex.adjacent_vertices()[coord_axis as usize]; + } + CoordSign::Minus => { + chunk.node = self.neighbor( + chunk.node, + chunk.vertex.canonical_sides()[coord_axis as usize], + )?; } } - coords = new_coords; - chunk.vertex = new_vertex; - } else if coords[coord_axis] == 0 && coord_direction == CoordDirection::Minus { - chunk.node = self.neighbor( - chunk.node, - chunk.vertex.canonical_sides()[coord_axis as usize], - )?; } else { - coords[coord_axis] = coords[coord_axis].wrapping_add_signed(coord_direction as i8); + coords[coord_axis] = coords[coord_axis].wrapping_add_signed(coord_sign as i8); } 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. - 'outer: for coord_axis in CoordAxis::iter() { - for coord_direction in CoordDirection::iter() { - if let Some(chunk_id) = - self.get_chunk_neighbor(chunk, coord_axis, coord_direction) - { - if let Chunk::Populated { modified: true, .. } = self[chunk_id] { - new_data.clear_margin(self.layout().dimension); - break 'outer; - } - } - } + /// 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, @@ -158,9 +150,6 @@ impl Graph { else { return false; }; - if voxels.is_solid() { - voxels.clear_margin(dimension); - } let voxel = voxels .data_mut(dimension) .get_mut(block_update.coords.to_index(dimension)) @@ -170,44 +159,14 @@ impl Graph { *modified = true; *old_surface = surface.take().or(*old_surface); - self.clear_adjacent_solid_chunk_margins(block_update.chunk_id); - true - } - - /// Clears margins from any populated and solid adjacent chunks. When a chunk is modified, this function should - /// be called on that chunk to ensure that adjacent chunks are rendered, since they can no longer be assumed to be - /// hidden by world generation. - fn clear_adjacent_solid_chunk_margins(&mut self, chunk: ChunkId) { - for coord_axis in CoordAxis::iter() { - for coord_direction in CoordDirection::iter() { - if let Some(chunk_id) = self.get_chunk_neighbor(chunk, coord_axis, coord_direction) - { - // We only need to clear margins from populated chunks. - let _ = self.clear_solid_chunk_margin(chunk_id); - } - } - } - } - - /// Tries to clear the margins of the given chunk. Fails and returns false if the - /// chunk is not populated yet. Succeeds and returns true if the chunk is not Solid, as the - /// chunk is assumed to have empty margins already. - #[must_use] - fn clear_solid_chunk_margin(&mut self, chunk: ChunkId) -> bool { - let dimension = self.layout().dimension; - let Some(Chunk::Populated { - voxels, - surface, - old_surface, - .. - }) = self.get_chunk_mut(chunk) - else { - return false; - }; - - if voxels.is_solid() { - voxels.clear_margin(dimension); - *old_surface = surface.take().or(*old_surface); + for chunk_direction in ChunkDirection::iter() { + margins::update_margin_voxel( + self, + block_update.chunk_id, + block_update.coords, + chunk_direction, + block_update.new_material, + ) } true } @@ -227,34 +186,6 @@ impl IndexMut for Graph { } } -/// Coordinates for a discrete voxel within a chunk, not including margins -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub struct Coords(pub [u8; 3]); - -impl Coords { - /// 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 + 1) - + (self.0[1] as usize + 1) * chunk_size_with_margin - + (self.0[2] as usize + 1) * chunk_size_with_margin.pow(2) - } -} - -impl Index for Coords { - type Output = u8; - - fn index(&self, coord_axis: CoordAxis) -> &u8 { - self.0.index(coord_axis as usize) - } -} - -impl IndexMut for Coords { - fn index_mut(&mut self, coord_axis: CoordAxis) -> &mut u8 { - self.0.index_mut(coord_axis as usize) - } -} - pub struct Node { pub state: NodeState, /// We can only populate chunks which lie within a cube of populated nodes, so nodes on the edge @@ -298,27 +229,6 @@ impl VoxelData { } } - /// Replaces all voxels in the margin of this chunk with the "Void" material. This function is a coarse - /// way to ensure that chunks are fully rendered when they need to be, avoiding a rendering bug caused - /// by a voxel's surface failing to render because of a margin being solid. - /// Until margins are fully implemented, any solid chunk produced by world generation should have its - /// margins cleared if it, or any chunk adjacent to it, is edited, since otherwise, the margins could - /// be inaccurate. - pub fn clear_margin(&mut self, dimension: u8) { - let data = self.data_mut(dimension); - let lwm = usize::from(dimension) + 2; - for z in 0..lwm { - for y in 0..lwm { - for x in 0..lwm { - if x == 0 || x == lwm - 1 || y == 0 || y == lwm - 1 || z == 0 || z == lwm - 1 { - // The current coordinates correspond to a margin point. Set it to void. - data[x + y * lwm + z * lwm.pow(2)] = Material::Void; - } - } - } - } - } - pub fn is_solid(&self) -> bool { match *self { VoxelData::Dense(_) => false, @@ -449,66 +359,6 @@ fn populate_node(graph: &mut Graph, node: NodeId) { }); } -/// Represents a particular axis in a voxel grid. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum CoordAxis { - X = 0, - Y = 1, - Z = 2, -} - -/// Trying to convert a `usize` to a `CoordAxis` returns this struct if the provided -/// `usize` is out-of-bounds -#[derive(Debug, Clone, Copy)] -pub struct CoordAxisOutOfBounds; - -impl CoordAxis { - /// Iterates through the the axes in ascending order - pub fn iter() -> impl ExactSizeIterator { - [Self::X, Self::Y, Self::Z].into_iter() - } - - /// Returns the pair axes orthogonal to the current axis - pub fn other_axes(self) -> [Self; 2] { - match self { - Self::X => [Self::Y, Self::Z], - Self::Y => [Self::Z, Self::X], - Self::Z => [Self::X, Self::Y], - } - } -} - -impl TryFrom for CoordAxis { - type Error = CoordAxisOutOfBounds; - - fn try_from(value: usize) -> Result { - match value { - 0 => Ok(Self::X), - 1 => Ok(Self::Y), - 2 => Ok(Self::Z), - _ => Err(CoordAxisOutOfBounds), - } - } -} - -/// Represents a direction in a particular axis. This struct is meant to be used with a coordinate axis, -/// so when paired with the X-axis, it represents the postitive X-direction when set to Plus and the -/// negative X-direction when set to Minus. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum CoordDirection { - Plus = 1, - Minus = -1, -} - -impl CoordDirection { - /// Iterates through the two possible coordinate directions - pub fn iter() -> impl ExactSizeIterator { - [CoordDirection::Plus, CoordDirection::Minus] - .iter() - .copied() - } -} - /// Represents a discretized region in the voxel grid contained by an axis-aligned bounding box. pub struct VoxelAABB { // The bounds are of the form [[x_min, x_max], [y_min, y_max], [z_min, z_max]], using voxel coordinates with a one-block diff --git a/common/src/proto.rs b/common/src/proto.rs index 2c63758b..d051f5eb 100644 --- a/common/src/proto.rs +++ b/common/src/proto.rs @@ -1,11 +1,8 @@ use serde::{Deserialize, Serialize}; use crate::{ - dodeca, - graph::NodeId, - node::{ChunkId, Coords}, - world::Material, - EntityId, SimConfig, Step, + dodeca, graph::NodeId, node::ChunkId, voxel_math::Coords, world::Material, EntityId, SimConfig, + Step, }; #[derive(Debug, Serialize, Deserialize)] diff --git a/common/src/voxel_math.rs b/common/src/voxel_math.rs new file mode 100644 index 00000000..90eb8a2b --- /dev/null +++ b/common/src/voxel_math.rs @@ -0,0 +1,317 @@ +use std::ops::{Index, IndexMut}; + +use serde::{Deserialize, Serialize}; + +use crate::dodeca::Side; + +/// Represents a particular axis in a voxel grid +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CoordAxis { + X = 0, + Y = 1, + Z = 2, +} + +/// Trying to convert a `usize` to a `CoordAxis` returns this struct if the provided +/// `usize` is out-of-bounds +#[derive(Debug, Clone, Copy)] +pub struct CoordAxisOutOfBounds; + +impl CoordAxis { + /// Iterates through the the axes in ascending order + pub fn iter() -> impl ExactSizeIterator { + [Self::X, Self::Y, Self::Z].into_iter() + } + + /// Returns the pair axes orthogonal to the current axis + pub fn other_axes(self) -> [Self; 2] { + match self { + Self::X => [Self::Y, Self::Z], + Self::Y => [Self::Z, Self::X], + Self::Z => [Self::X, Self::Y], + } + } +} + +impl TryFrom for CoordAxis { + type Error = CoordAxisOutOfBounds; + + fn try_from(value: usize) -> Result { + match value { + 0 => Ok(Self::X), + 1 => Ok(Self::Y), + 2 => Ok(Self::Z), + _ => Err(CoordAxisOutOfBounds), + } + } +} + +/// Represents a direction in a particular axis. This struct is meant to be used with a coordinate axis, +/// so when paired with the X-axis, it represents the postitive X-direction when set to Plus and the +/// negative X-direction when set to Minus. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CoordSign { + Plus = 1, + Minus = -1, +} + +impl CoordSign { + /// Iterates through the two possible coordinate directions + pub fn iter() -> impl ExactSizeIterator { + [CoordSign::Plus, CoordSign::Minus].into_iter() + } +} + +impl std::ops::Mul for CoordSign { + type Output = CoordSign; + + fn mul(self, rhs: Self) -> Self::Output { + match self == rhs { + true => CoordSign::Plus, + false => CoordSign::Minus, + } + } +} + +impl std::ops::MulAssign for CoordSign { + fn mul_assign(&mut self, rhs: Self) { + *self = *self * rhs; + } +} + +/// Coordinates for a discrete voxel within a chunk, not including margins +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct Coords(pub [u8; 3]); + +impl Coords { + /// 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 + 1) + + (self.0[1] as usize + 1) * chunk_size_with_margin + + (self.0[2] as usize + 1) * chunk_size_with_margin.pow(2) + } + + /// 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 - 1, + CoordSign::Minus => 0, + } + } +} + +impl Index for Coords { + type Output = u8; + + #[inline] + fn index(&self, coord_axis: CoordAxis) -> &u8 { + self.0.index(coord_axis as usize) + } +} + +impl IndexMut for Coords { + #[inline] + fn index_mut(&mut self, coord_axis: CoordAxis) -> &mut u8 { + self.0.index_mut(coord_axis as usize) + } +} + +/// Represents one of the six main directions within a chunk: positive or negative x, y, and z. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ChunkDirection { + pub axis: CoordAxis, + pub sign: CoordSign, +} + +impl ChunkDirection { + pub const PLUS_X: Self = ChunkDirection { + axis: CoordAxis::X, + sign: CoordSign::Plus, + }; + pub const PLUS_Y: Self = ChunkDirection { + axis: CoordAxis::Y, + sign: CoordSign::Plus, + }; + pub const PLUS_Z: Self = ChunkDirection { + axis: CoordAxis::Z, + sign: CoordSign::Plus, + }; + pub const MINUS_X: Self = ChunkDirection { + axis: CoordAxis::X, + sign: CoordSign::Minus, + }; + pub const MINUS_Y: Self = ChunkDirection { + axis: CoordAxis::Y, + sign: CoordSign::Minus, + }; + pub const MINUS_Z: Self = ChunkDirection { + axis: CoordAxis::Z, + sign: CoordSign::Minus, + }; + + pub fn iter() -> impl ExactSizeIterator { + [ + Self::PLUS_X, + Self::PLUS_Y, + Self::PLUS_Z, + Self::MINUS_X, + Self::MINUS_Y, + Self::MINUS_Z, + ] + .into_iter() + } +} + +/// Represents one of the 6 possible permutations a chunk's axes can have, useful for comparing the canonical sides of one chunk to an adjacent chunk. +/// This is analogous to a 3x3 rotation/reflection matrix with a restricted domain. +/// Note that it may make sense to define a more general `ChunkOrientation` class that takes three `ChunkDirection`s, to represent +/// any cube rotation/reflection, but no use exists for it yet, so it has not yet been implemented. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ChunkAxisPermutation { + axes: [CoordAxis; 3], +} + +impl ChunkAxisPermutation { + pub const IDENTITY: Self = ChunkAxisPermutation { + axes: [CoordAxis::X, CoordAxis::Y, CoordAxis::Z], + }; + + /// Constructs a `ChunkAxisPermutation` that, when left-multiplying a set of coordinates, moves from `from`'s reference + /// frame to `to`'s reference frame, where `from` and `to` are represented as three dodeca sides incident to a vertex + /// that determine the orientation of a chunk. + pub fn from_permutation(from: [Side; 3], to: [Side; 3]) -> Self { + assert!(from[0] != from[1] && from[0] != from[2] && from[1] != from[2]); + assert!(to[0] != to[1] && to[0] != to[2] && to[1] != to[2]); + ChunkAxisPermutation { + axes: from.map(|f| { + CoordAxis::try_from( + to.iter() + .position(|&t| f == t) + .expect("from and to must have same set of sides"), + ) + .unwrap() + }), + } + } +} + +impl Index for ChunkAxisPermutation { + type Output = CoordAxis; + + fn index(&self, index: CoordAxis) -> &Self::Output { + &self.axes[index as usize] + } +} + +impl std::ops::Mul for ChunkAxisPermutation { + type Output = Coords; + + fn mul(self, rhs: Coords) -> Self::Output { + let mut result = Coords([0; 3]); + for axis in CoordAxis::iter() { + result[self[axis]] = rhs[axis]; + } + result + } +} + +impl std::ops::Mul for ChunkAxisPermutation { + type Output = ChunkDirection; + + fn mul(self, rhs: ChunkDirection) -> Self::Output { + ChunkDirection { + axis: self[rhs.axis], + sign: rhs.sign, + } + } +} + +#[cfg(test)] +mod tests { + use crate::dodeca::Vertex; + + use super::*; + + fn coords_to_vector3(coords: Coords) -> na::Vector3 { + na::Vector3::new( + coords[CoordAxis::X] as i32, + coords[CoordAxis::Y] as i32, + coords[CoordAxis::Z] as i32, + ) + } + + fn coord_axis_to_vector3(coord_axis: CoordAxis) -> na::Vector3 { + let mut vector = na::Vector3::new(0, 0, 0); + vector[coord_axis as usize] = 1; + vector + } + + fn chunk_direction_to_vector3(chunk_direction: ChunkDirection) -> na::Vector3 { + let mut vector = na::Vector3::new(0, 0, 0); + vector[chunk_direction.axis as usize] = chunk_direction.sign as i32; + vector + } + + fn chunk_axis_permutation_to_matrix3( + chunk_axis_permutation: ChunkAxisPermutation, + ) -> na::Matrix3 { + na::Matrix::from_columns(&chunk_axis_permutation.axes.map(coord_axis_to_vector3)) + } + + // Helper function to return all permutations as a list of ordered triples + fn get_all_permutations() -> Vec<(usize, usize, usize)> { + let mut permutations = vec![]; + for i in 0..3 { + for j in 0..3 { + if j == i { + continue; + } + for k in 0..3 { + if k == i || k == j { + continue; + } + permutations.push((i, j, k)); + } + } + } + permutations + } + + #[test] + fn test_chunk_axis_permutation() { + let sides = Vertex::A.canonical_sides(); + + let example_coords = Coords([3, 5, 9]); + + for (i, j, k) in get_all_permutations() { + let permutation = ChunkAxisPermutation::from_permutation( + [sides[0], sides[1], sides[2]], + [sides[i], sides[j], sides[k]], + ); + + // Test that the permutation goes in the expected direction + assert_eq!( + permutation * example_coords, + Coords([ + example_coords.0[i], + example_coords.0[j], + example_coords.0[k] + ]) + ); + + // Test that the multiplication operations are consistent with matrix multiplication + assert_eq!( + coords_to_vector3(permutation * example_coords), + chunk_axis_permutation_to_matrix3(permutation) * coords_to_vector3(example_coords) + ); + for chunk_direction in ChunkDirection::iter() { + assert_eq!( + chunk_direction_to_vector3(permutation * chunk_direction), + chunk_axis_permutation_to_matrix3(permutation) + * chunk_direction_to_vector3(chunk_direction) + ) + } + } + } +} 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 }