Skip to content

Commit

Permalink
Fix up margins for newly-generated chunks
Browse files Browse the repository at this point in the history
  • Loading branch information
patowen committed Apr 26, 2024
1 parent e1a7f81 commit 7b1b3f5
Show file tree
Hide file tree
Showing 4 changed files with 259 additions and 25 deletions.
1 change: 1 addition & 0 deletions common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
224 changes: 224 additions & 0 deletions common/src/margins.rs
Original file line number Diff line number Diff line change
@@ -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<Coords> 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<CoordAxis> for CoordsWithMargins {
type Output = u8;

#[inline]
fn index(&self, coord_axis: CoordAxis) -> &u8 {
self.0.index(coord_axis as usize)
}
}

impl std::ops::IndexMut<CoordAxis> 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<CoordsWithMargins> 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
);
}
}
56 changes: 32 additions & 24 deletions common/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion common/src/worldgen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -246,6 +246,7 @@ impl ChunkParams {
self.generate_trees(&mut voxels, &mut rng);
}

margins::initialize_margins(self.dimension, &mut voxels);
voxels
}

Expand Down

0 comments on commit 7b1b3f5

Please sign in to comment.