diff --git a/all-is-cubes-mesh/src/block_mesh/analyze.rs b/all-is-cubes-mesh/src/block_mesh/analyze.rs index 4a4a1ca1f..734158754 100644 --- a/all-is-cubes-mesh/src/block_mesh/analyze.rs +++ b/all-is-cubes-mesh/src/block_mesh/analyze.rs @@ -162,7 +162,7 @@ pub(crate) fn analyze(resolution: Resolution, voxels: Vol<&[Evoxel]>, viz: &mut for (center, window_voxels) in windows(voxels) { viz.window(center, voxels); - analyze_one_window(&mut analysis, center, window_voxels); + analyze_one_window(&mut analysis, center, window_voxels, viz); viz.analysis_in_progress(&analysis); } viz.clear_window(); @@ -172,7 +172,12 @@ pub(crate) fn analyze(resolution: Resolution, voxels: Vol<&[Evoxel]>, viz: &mut /// Take one of the outputs of [`windows()`] and compute its contribution to [`analysis`]. #[inline] -fn analyze_one_window(analysis: &mut Analysis, center: GridPoint, window: OctantMap<&Evoxel>) { +fn analyze_one_window( + analysis: &mut Analysis, + center: GridPoint, + window: OctantMap<&Evoxel>, + viz: &mut Viz, +) { use Face6::*; const ALL: OctantMask = OctantMask::ALL; const NONE: OctantMask = OctantMask::NONE; @@ -195,29 +200,54 @@ fn analyze_one_window(analysis: &mut Analysis, center: GridPoint, window: Octant .map(|voxel| (voxel.color, voxel.emission)) .all_equal(); + // Bitmask of which axes are going to have a visible surface + let mut axes_involved = 0; + // For each direction, check if any of the voxels in the deeper side are visible // and not covered by a voxel of the same or stronger category in the shallower side, // and mark that plane as occupied if so. // // `unflatten()` undoes the axis dropping/swapping this code does. if uncovered(opaque, PX) || uncovered(renderable, PX) { + axes_involved |= 0b1; analysis.expand_rect(NX, center.x, center.yz()); } if uncovered(opaque, PY) || uncovered(renderable, PY) { + axes_involved |= 0b10; analysis.expand_rect(NY, center.y, center.xz()); } if uncovered(opaque, PZ) || uncovered(renderable, PZ) { + axes_involved |= 0b100; analysis.expand_rect(NZ, center.z, center.xy()); } if uncovered(opaque, NX) || uncovered(renderable, NX) { + axes_involved |= 0b1; analysis.expand_rect(PX, resolution_coord - center.x, center.yz()); } if uncovered(opaque, NY) || uncovered(renderable, NY) { + axes_involved |= 0b10; analysis.expand_rect(PY, resolution_coord - center.y, center.xz()); } if uncovered(opaque, NZ) || uncovered(renderable, NZ) { + axes_involved |= 0b100; analysis.expand_rect(PZ, resolution_coord - center.z, center.xy()); } + + if axes_involved == 0b111 { + // If all three axes have visible surfaces at this point, then this must become a + // vertex of the mesh. We need to record such vertices as part of the analysis, + // because not all such vertices are identifiable by being corners in a 2D slice of + // the volume — there are also vertices where an edge on one plane meets a corner on + // another plane, and we must consistently treat them as vertices to avoid + // “T-junctions”: places where an edge of one triangle meets a vertex of other + // triangles, which are subject to numerical error during rendering that causes visible + // gaps. + // + // TODO: Currently, this information is not used (except by `Viz`). + // We should store it in `analysis` or return it in a separate buffer, + // but only once it is actually going to be used. + viz.add_analysis_vertex(center); + } } } @@ -388,7 +418,8 @@ mod tests { let mut u = Universe::new(); let block = Block::builder() - .voxels_fn(Resolution::R4, |_| color_block!(0.0, 0.0, 0.0, 0.5)).unwrap() + .voxels_fn(Resolution::R4, |_| color_block!(0.0, 0.0, 0.0, 0.5)) + .unwrap() .build_into(&mut u); let ev = block.evaluate().unwrap(); let analysis = analyze( diff --git a/all-is-cubes-mesh/src/block_mesh/viz.rs b/all-is-cubes-mesh/src/block_mesh/viz.rs index 1a2065573..c58ef0e1a 100644 --- a/all-is-cubes-mesh/src/block_mesh/viz.rs +++ b/all-is-cubes-mesh/src/block_mesh/viz.rs @@ -16,6 +16,7 @@ use { all_is_cubes::rerun_glue as rg, alloc::vec::Vec, core::iter, + core::mem, itertools::Itertools as _, }; @@ -45,6 +46,10 @@ pub struct Inner { data_bounds: Option, analysis: Analysis, + analysis_vertices: Vec, + /// Tracks whether we have new vertices, to reduce the amount of logged data + /// TODO: Do this for the other things too. + analysis_vertices_dirty: bool, destination: rg::Destination, window_voxels_path: rg::EntityPath, @@ -52,6 +57,7 @@ pub struct Inner { layer_path: rg::EntityPath, mesh_surface_path: rg::EntityPath, mesh_edges_path: rg::EntityPath, + analysis_vertices_path: rg::EntityPath, // These two together make up the mesh edge display entity's data mesh_edge_positions: Vec, @@ -88,9 +94,12 @@ impl Viz { layer_path: rg::entity_path!["progress", "mesh_plane"], mesh_surface_path: rg::entity_path!["mesh", "surface"], mesh_edges_path: rg::entity_path!["mesh", "edges"], + analysis_vertices_path: rg::entity_path!["analysis_vertices"], resolution: None, data_bounds: None, analysis: Analysis::empty(), + analysis_vertices: Vec::new(), + analysis_vertices_dirty: false, mesh_edge_positions: Vec::new(), mesh_edge_classes: Vec::new(), mesh_vertex_positions: Vec::new(), @@ -273,6 +282,16 @@ impl Viz { ); } } + + pub(crate) fn add_analysis_vertex(&mut self, #[allow(unused)] point: GridPoint) { + #[cfg(feature = "rerun")] + if let Self::Enabled(state) = self { + state + .analysis_vertices + .push(rg::convert_vec(point.to_vector())); + state.analysis_vertices_dirty = true; + } + } } #[cfg(feature = "rerun")] @@ -281,18 +300,38 @@ impl Inner { self.analysis.occupied_plane_box(face, layer).unwrap() } - fn log_analysis(&self) { - let iter = Face6::ALL.into_iter().flat_map(|face| { - self.analysis - .occupied_planes(face) - .map(move |(layer, _)| self.layer_box(face, layer)) - }); - self.destination.log( - &self.occupied_path, - &rg::convert_grid_aabs(iter) - .with_class_ids([rg::ClassId::MeshVizOccupiedPlane]) - .with_radii([OCCUPIED_RADIUS]), - ); + /// Logs the current contents of the [`Analysis`], replacing any prior data. + fn log_analysis(&mut self) { + // Log occupied_planes + { + let iter = Face6::ALL.into_iter().flat_map(|face| { + self.analysis.occupied_planes(face).map({ + let this = &*self; + move |(layer, _)| this.layer_box(face, layer) + }) + }); + self.destination.log( + &self.occupied_path, + &rg::convert_grid_aabs(iter) + .with_class_ids([rg::ClassId::MeshVizOccupiedPlane]) + .with_radii([OCCUPIED_RADIUS]), + ); + } + + // Log analysis_vertices (not technically part of `Analysis` *yet*) + if mem::take(&mut self.analysis_vertices_dirty) { + self.destination.log( + &self.analysis_vertices_path, + // We use `Ellipsoids3D` instead of `Points3D`, even though these are semantically + // points, to get better rendering. + &rg::archetypes::Ellipsoids3D::from_centers_and_radii( + self.analysis_vertices.iter().copied(), + [0.15], + ) + .with_colors([rg::components::Color::from_rgb(80, 80, 255)]) + .with_fill_mode(rg::components::FillMode::Solid), + ) + } } fn log_voxels(