From 277f5fc5730f2702503d5c8d68c19f6ea96fbbcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20L=C3=B6schner?= Date: Tue, 18 Jul 2023 18:57:20 +0200 Subject: [PATCH 01/22] Update vtkio traits, Real vec/mat conversion trait --- splashsurf/src/io.rs | 10 +- splashsurf/src/reconstruction.rs | 12 +- splashsurf_lib/src/aabb.rs | 6 +- splashsurf_lib/src/io/json_format.rs | 2 +- splashsurf_lib/src/io/obj_format.rs | 129 ++++++- splashsurf_lib/src/io/ply_format.rs | 24 +- splashsurf_lib/src/io/vtk_format.rs | 12 +- splashsurf_lib/src/kernel.rs | 2 +- splashsurf_lib/src/lib.rs | 2 +- splashsurf_lib/src/mesh.rs | 321 ++++++++++++------ splashsurf_lib/src/traits.rs | 57 +++- splashsurf_lib/src/utils.rs | 8 + .../tests/integration_tests/test_octree.rs | 2 +- 13 files changed, 424 insertions(+), 163 deletions(-) diff --git a/splashsurf/src/io.rs b/splashsurf/src/io.rs index 9232c73..c58f805 100644 --- a/splashsurf/src/io.rs +++ b/splashsurf/src/io.rs @@ -1,14 +1,12 @@ use crate::io::vtk_format::VtkFile; use anyhow::{anyhow, Context}; use log::{info, warn}; -use splashsurf_lib::mesh::MeshAttribute; +use splashsurf_lib::mesh::{ + IntoVtkUnstructuredGridPiece, Mesh3d, MeshAttribute, MeshWithData, TriMesh3d, +}; use splashsurf_lib::nalgebra::Vector3; use splashsurf_lib::Real; use splashsurf_lib::{io, profile}; -use splashsurf_lib::{ - mesh::{Mesh3d, MeshWithData, TriMesh3d}, - vtkio::model::DataSet, -}; use std::collections::HashSet; use std::fs::File; use std::io::{BufWriter, Write}; @@ -245,7 +243,7 @@ pub fn write_mesh<'a, R: Real, MeshT: Mesh3d, P: AsRef>( _format_params: &OutputFormatParameters, ) -> Result<(), anyhow::Error> where - &'a MeshWithData: Into, + for<'b> &'b MeshWithData: IntoVtkUnstructuredGridPiece, { let output_file = output_file.as_ref(); info!( diff --git a/splashsurf/src/reconstruction.rs b/splashsurf/src/reconstruction.rs index b155c99..85e8e7f 100644 --- a/splashsurf/src/reconstruction.rs +++ b/splashsurf/src/reconstruction.rs @@ -998,11 +998,7 @@ pub(crate) fn reconstruction_pipeline_generic( if let Some(output_octree_file) = &paths.output_octree_file { info!("Writing octree to \"{}\"...", output_octree_file.display()); io::vtk_format::write_vtk( - reconstruction - .octree() - .unwrap() - .hexmesh(grid, true) - .to_unstructured_grid(), + reconstruction.octree().unwrap().hexmesh(grid, true), output_octree_file, "mesh", ) @@ -1061,11 +1057,7 @@ pub(crate) fn reconstruction_pipeline_generic( output_density_map_grid_file.display() ); - io::vtk_format::write_vtk( - density_mesh.to_unstructured_grid(), - output_density_map_grid_file, - "density_map", - )?; + io::vtk_format::write_vtk(density_mesh, output_density_map_grid_file, "density_map")?; info!("Done."); } diff --git a/splashsurf_lib/src/aabb.rs b/splashsurf_lib/src/aabb.rs index c149c2d..18db882 100644 --- a/splashsurf_lib/src/aabb.rs +++ b/splashsurf_lib/src/aabb.rs @@ -6,7 +6,7 @@ use std::fmt::Debug; use nalgebra::SVector; use rayon::prelude::*; -use crate::{Real, ThreadSafe}; +use crate::{Real, RealConvert, ThreadSafe}; /// Type representing an axis aligned bounding box in arbitrary dimensions #[derive(Clone, Eq, PartialEq)] @@ -119,8 +119,8 @@ where T: Real, { Some(AxisAlignedBoundingBox::new( - T::try_convert_vec_from(&self.min)?, - T::try_convert_vec_from(&self.max)?, + self.min.try_convert()?, + self.max.try_convert()?, )) } diff --git a/splashsurf_lib/src/io/json_format.rs b/splashsurf_lib/src/io/json_format.rs index 93b9a5f..6493768 100644 --- a/splashsurf_lib/src/io/json_format.rs +++ b/splashsurf_lib/src/io/json_format.rs @@ -1,7 +1,7 @@ //! Helper functions for the JSON file format use crate::utils::IteratorExt; -use crate::Real; +use crate::{Real, RealConvert}; use anyhow::{anyhow, Context}; use nalgebra::Vector3; use std::fs::{File, OpenOptions}; diff --git a/splashsurf_lib/src/io/obj_format.rs b/splashsurf_lib/src/io/obj_format.rs index 8c611cd..af7e1c1 100644 --- a/splashsurf_lib/src/io/obj_format.rs +++ b/splashsurf_lib/src/io/obj_format.rs @@ -1,11 +1,15 @@ //! Helper functions for the OBJ file format -use crate::mesh::{AttributeData, CellConnectivity, Mesh3d, MeshWithData}; -use crate::Real; +use crate::mesh::{ + AttributeData, CellConnectivity, Mesh3d, MeshAttribute, MeshWithData, TriMesh3d, +}; +use crate::{utils, Real}; use anyhow::Context; +use nalgebra::Vector3; use std::fs; -use std::io::{BufWriter, Write}; +use std::io::{BufRead, BufReader, BufWriter, Write}; use std::path::Path; +use std::str::FromStr; // TODO: Support for other mesh data (interpolated fields)? @@ -61,3 +65,122 @@ pub fn mesh_to_obj, P: AsRef>( Ok(()) } + +pub fn surface_mesh_from_obj>( + obj_path: P, +) -> Result>, anyhow::Error> { + let file = fs::File::open(obj_path).context("Failed to open file for reading")?; + let mut reader = BufReader::with_capacity(1000000, file); + + let mut vertices = Vec::new(); + let mut triangles = Vec::new(); + let mut normals = Vec::new(); + + let buffer_to_vec3 = |buffer: &[&str]| -> Result, anyhow::Error> { + Ok(Vector3::new( + R::from_f64(f64::from_str(buffer[0])?).unwrap(), + R::from_f64(f64::from_str(buffer[1])?).unwrap(), + R::from_f64(f64::from_str(buffer[2])?).unwrap(), + )) + }; + + let mut outer_buffer: Vec<&'static str> = Vec::new(); + let mut buffer_string = String::new(); + + loop { + let mut buffer = utils::recycle(outer_buffer); + + let read = reader.read_line(&mut buffer_string)?; + if read == 0 { + break; + } + + let line = buffer_string.trim(); + + if let Some(vert_string) = line.strip_prefix("v ") { + buffer.extend(vert_string.split(' ')); + assert_eq!(buffer.len(), 3, "expected three coordinates per vertex"); + vertices.push(buffer_to_vec3(&buffer)?); + } else if let Some(face_string) = line.strip_prefix("f ") { + // TODO: Support mixed tri/quad meshes? + buffer.extend( + face_string + .split(' ') + // Support "v1/vt1", "v1/vt1/vn1" and "v1//vn1" formats (ignore everything after '/') + .map(|f| f.split_once('/').map(|(f, _)| f).unwrap_or(f)), + ); + assert_eq!( + buffer.len(), + 3, + "expected three indices per faces (only triangles supported at the moment)" + ); + let tri = [ + usize::from_str(buffer[0])? - 1, + usize::from_str(buffer[1])? - 1, + usize::from_str(buffer[2])? - 1, + ]; + triangles.push(tri); + } else if let Some(normal_string) = line.strip_prefix("vn ") { + buffer.extend(normal_string.split(' ')); + assert_eq!( + buffer.len(), + 3, + "expected three normal components per vertex" + ); + normals.push(buffer_to_vec3(&buffer)?); + } + + outer_buffer = utils::recycle(buffer); + buffer_string.clear(); + } + + let mut mesh = MeshWithData::new(TriMesh3d { + vertices, + triangles, + }); + + if !normals.is_empty() { + assert_eq!( + mesh.vertices().len(), + normals.len(), + "length of vertex and vertex normal array doesn't match" + ); + mesh.point_attributes.push(MeshAttribute::new( + "normals", + AttributeData::Vector3Real(normals), + )); + } + + Ok(mesh) +} + +#[cfg(test)] +pub mod test { + use super::*; + + #[test] + fn test_obj_read_icosphere() -> Result<(), anyhow::Error> { + let mesh = surface_mesh_from_obj::("../data/icosphere.obj")?; + + assert_eq!(mesh.vertices().len(), 42); + assert_eq!(mesh.cells().len(), 80); + + Ok(()) + } + + #[test] + fn test_obj_read_icosphere_with_normals() -> Result<(), anyhow::Error> { + let mesh = surface_mesh_from_obj::("../data/icosphere.obj")?; + + assert_eq!(mesh.vertices().len(), 42); + assert_eq!(mesh.cells().len(), 80); + let normals = mesh.point_attributes.iter().find(|a| a.name == "normals"); + if let Some(MeshAttribute { data, .. }) = normals { + if let AttributeData::Vector3Real(normals) = data { + assert_eq!(normals.len(), 42) + } + } + + Ok(()) + } +} diff --git a/splashsurf_lib/src/io/ply_format.rs b/splashsurf_lib/src/io/ply_format.rs index 591b1f2..bd6c095 100644 --- a/splashsurf_lib/src/io/ply_format.rs +++ b/splashsurf_lib/src/io/ply_format.rs @@ -202,7 +202,7 @@ pub fn mesh_to_ply, P: AsRef>( .truncate(true) .open(filename) .context("Failed to open file handle for writing PLY file")?; - let mut writer = BufWriter::with_capacity(100000, file); + let mut writer = BufWriter::with_capacity(1000000, file); write!(&mut writer, "ply\n")?; write!(&mut writer, "format binary_little_endian 1.0\n")?; @@ -257,7 +257,7 @@ pub fn mesh_to_ply, P: AsRef>( } for c in mesh.cells() { - let num_verts = M::Cell::num_vertices().to_u8().expect("failed to convert cell vertex count to u8"); + let num_verts = c.num_vertices().to_u8().expect("failed to convert cell vertex count to u8"); writer.write_all(&num_verts.to_le_bytes())?; c.try_for_each_vertex(|v| { let idx = v.to_u32().expect("failed to convert vertex index to u32"); @@ -273,8 +273,8 @@ pub mod test { use super::*; #[test] - fn test_ply_read_cube_with_normals() -> Result<(), anyhow::Error> { - let input_file = Path::new("../data/cube_normals.ply"); + fn test_ply_read_cube() -> Result<(), anyhow::Error> { + let input_file = Path::new("../data/cube.ply"); let mesh: MeshWithData = surface_mesh_from_ply(input_file).with_context(|| { format!( @@ -285,19 +285,13 @@ pub mod test { assert_eq!(mesh.mesh.vertices.len(), 24); assert_eq!(mesh.mesh.triangles.len(), 12); - let normals = mesh.point_attributes.iter().find(|a| a.name == "normals"); - if let Some(MeshAttribute { data, .. }) = normals { - if let AttributeData::Vector3Real(normals) = data { - assert_eq!(normals.len(), 24) - } - } Ok(()) } #[test] - fn test_ply_read_cube() -> Result<(), anyhow::Error> { - let input_file = Path::new("../data/cube.ply"); + fn test_ply_read_cube_with_normals() -> Result<(), anyhow::Error> { + let input_file = Path::new("../data/cube_normals.ply"); let mesh: MeshWithData = surface_mesh_from_ply(input_file).with_context(|| { format!( @@ -308,6 +302,12 @@ pub mod test { assert_eq!(mesh.mesh.vertices.len(), 24); assert_eq!(mesh.mesh.triangles.len(), 12); + let normals = mesh.point_attributes.iter().find(|a| a.name == "normals"); + if let Some(MeshAttribute { data, .. }) = normals { + if let AttributeData::Vector3Real(normals) = data { + assert_eq!(normals.len(), 24) + } + } Ok(()) } diff --git a/splashsurf_lib/src/io/vtk_format.rs b/splashsurf_lib/src/io/vtk_format.rs index fdd83b3..d3acfa8 100644 --- a/splashsurf_lib/src/io/vtk_format.rs +++ b/splashsurf_lib/src/io/vtk_format.rs @@ -1,8 +1,8 @@ //! Helper functions for the VTK file format -use crate::mesh::{AttributeData, MeshAttribute, MeshWithData, TriMesh3d}; +use crate::mesh::{AttributeData, IntoVtkDataSet, MeshAttribute, MeshWithData, TriMesh3d}; use crate::utils::IteratorExt; -use crate::Real; +use crate::{Real, RealConvert}; use anyhow::{anyhow, Context}; use nalgebra::Vector3; use std::borrow::Cow; @@ -183,7 +183,7 @@ pub fn surface_mesh_from_vtk>( /// Tries to write `data` that is convertible to a VTK `DataSet` into a big endian VTK file pub fn write_vtk>( - data: impl Into, + data: impl IntoVtkDataSet, filename: P, title: &str, ) -> Result<(), anyhow::Error> { @@ -192,7 +192,7 @@ pub fn write_vtk>( title: title.to_string(), file_path: None, byte_order: ByteOrder::BigEndian, - data: data.into(), + data: data.into_dataset(), }; let filename = filename.as_ref(); @@ -279,6 +279,10 @@ fn surface_mesh_from_unstructured_grid( } }; + // Sometimes VTK files from paraview start with an empty cell + let has_empty_cell = cell_verts.first().map(|c| *c == 0).unwrap_or(false); + let cell_verts = &cell_verts[cell_verts.len().min(has_empty_cell as usize)..]; + if cell_verts.len() % 4 != 0 { return Err(anyhow!("Length of cell vertex array is invalid. Expected 4 values per cell (3 for each triangle vertex index + 1 for vertex count). There are {} values for {} cells.", cell_verts.len(), num_cells)); } diff --git a/splashsurf_lib/src/kernel.rs b/splashsurf_lib/src/kernel.rs index 4dae19e..42c8965 100644 --- a/splashsurf_lib/src/kernel.rs +++ b/splashsurf_lib/src/kernel.rs @@ -1,6 +1,6 @@ //! SPH kernel function implementations -use crate::Real; +use crate::{Real, RealConvert}; use nalgebra::Vector3; use numeric_literals::replace_float_literals; diff --git a/splashsurf_lib/src/lib.rs b/splashsurf_lib/src/lib.rs index 1c53368..0814039 100644 --- a/splashsurf_lib/src/lib.rs +++ b/splashsurf_lib/src/lib.rs @@ -37,7 +37,7 @@ pub use vtkio; pub use crate::aabb::{Aabb2d, Aabb3d, AxisAlignedBoundingBox}; pub use crate::density_map::DensityMap; pub use crate::octree::SubdivisionCriterion; -pub use crate::traits::{Index, Real, ThreadSafe}; +pub use crate::traits::{Index, Real, RealConvert, ThreadSafe}; pub use crate::uniform_grid::UniformGrid; use crate::density_map::DensityMapError; diff --git a/splashsurf_lib/src/mesh.rs b/splashsurf_lib/src/mesh.rs index af27088..6749f3b 100644 --- a/splashsurf_lib/src/mesh.rs +++ b/splashsurf_lib/src/mesh.rs @@ -10,13 +10,11 @@ //! attached to the vertices (e.g. normals) or cells (e.g. some identifiers) of the mesh. //! //! If the `vtk_extras` feature is enabled, this module also provides features for conversion of these -//! meshes to [`vtkio`](https://docs.rs/vtkio/0.6.*/vtkio/index.html) data structures. For example: -//! - [`MeshWithData::to_unstructured_grid`] to convert a mesh together with all attached attributes -//! - [`vtk_helper::mesh_to_unstructured_grid`] to convert a basic mesh without additional data -//! - `From for UnstructuredGridPiece` implementations for the basic mesh types -//! - `Into` implementations for the basic mesh types +//! meshes to [`vtkio`] data structures. For example: +//! - [`IntoVtkUnstructuredGridPiece`] to convert basic meshes and meshes with attached attributes to the +//! - [`IntoVtkDataSet`] for all meshes implementing [`IntoVtkUnstructuredGridPiece`] to directly save a mesh as a VTK file -use crate::{new_map, Aabb3d, MapType, Real}; +use crate::{new_map, profile, Aabb3d, MapType, Real, RealConvert}; use bytemuck_derive::{Pod, Zeroable}; use nalgebra::{Unit, Vector3}; use rayon::prelude::*; @@ -24,9 +22,10 @@ use std::cell::RefCell; use std::fmt::Debug; use thread_local::ThreadLocal; #[cfg(feature = "vtk_extras")] -use vtkio::model::{Attribute, DataSet, UnstructuredGridPiece}; +use vtkio::model::{Attribute, UnstructuredGridPiece}; -// TODO: Rename/restructure VTK helper implementations +#[cfg(feature = "vtk_extras")] +pub use crate::mesh::vtk_helper::{IntoVtkDataSet, IntoVtkUnstructuredGridPiece}; /// A named attribute with data that can be attached to the vertices or cells of a mesh #[derive(Clone, Debug)] @@ -94,12 +93,31 @@ pub trait Mesh3d { fn vertices(&self) -> &[Vector3]; /// Returns a slice of all cells of the mesh fn cells(&self) -> &[Self::Cell]; + + /// Returns a mapping of all mesh vertices to the set of the cells they belong to + fn vertex_cell_connectivity(&self) -> Vec> { + profile!("vertex_cell_connectivity"); + let mut connectivity_map: Vec> = vec![Vec::new(); self.vertices().len()]; + for (cell_idx, cell) in self.cells().iter().enumerate() { + cell.for_each_vertex(|v_i| { + if !connectivity_map[v_i].contains(&cell_idx) { + connectivity_map[v_i].push(cell_idx); + } + }) + } + + connectivity_map + } } /// Basic interface for mesh cells consisting of a collection of vertex indices pub trait CellConnectivity { /// Returns the number of vertices per cell - fn num_vertices() -> usize; + fn num_vertices(&self) -> usize { + Self::expected_num_vertices() + } + /// Returns the expected number of vertices per cell (helpful for connectivities with a constant number of vertices to reserve storage) + fn expected_num_vertices() -> usize; /// Calls the given closure with each vertex index that is part of this cell, stopping at the first error and returning that error fn try_for_each_vertex Result<(), E>>(&self, f: F) -> Result<(), E>; /// Calls the given closure with each vertex index that is part of this cell @@ -126,7 +144,7 @@ pub struct HexCell(pub [usize; 8]); pub struct PointCell(pub usize); impl CellConnectivity for TriangleCell { - fn num_vertices() -> usize { + fn expected_num_vertices() -> usize { 3 } @@ -136,7 +154,7 @@ impl CellConnectivity for TriangleCell { } impl CellConnectivity for HexCell { - fn num_vertices() -> usize { + fn expected_num_vertices() -> usize { 8 } @@ -146,7 +164,7 @@ impl CellConnectivity for HexCell { } impl CellConnectivity for PointCell { - fn num_vertices() -> usize { + fn expected_num_vertices() -> usize { 1 } @@ -163,7 +181,7 @@ impl Mesh3d for TriMesh3d { } fn cells(&self) -> &[TriangleCell] { - bytemuck::cast_slice::<[usize; 3], TriangleCell>(self.triangles.as_slice()) + self.triangle_cells() } } @@ -191,7 +209,39 @@ impl Mesh3d for PointCloud3d { } } +impl> Mesh3d for &MeshT { + type Cell = MeshT::Cell; + fn vertices(&self) -> &[Vector3] { + (*self).vertices() + } + fn cells(&self) -> &[MeshT::Cell] { + (*self).cells() + } +} + +impl<'a, R: Real, MeshT: Mesh3d + ToOwned> Mesh3d for std::borrow::Cow<'a, MeshT> { + type Cell = MeshT::Cell; + fn vertices(&self) -> &[Vector3] { + (*self.as_ref()).vertices() + } + fn cells(&self) -> &[MeshT::Cell] { + (*self.as_ref()).cells() + } +} + +impl TriangleCell { + /// Returns an iterator over all edges of this triangle + pub fn edges<'a>(&'a self) -> impl Iterator + 'a { + (0..3).map(|i| (self.0[i], self.0[(i + 1) % 3])) + } +} + impl TriMesh3d { + /// Returns a slice of all triangles of the mesh as `TriangleCell`s + pub fn triangle_cells(&self) -> &[TriangleCell] { + bytemuck::cast_slice::<[usize; 3], TriangleCell>(self.triangles.as_slice()) + } + /// Clears the vertex and triangle storage, preserves allocated memory pub fn clear(&mut self) { self.vertices.clear(); @@ -306,6 +356,25 @@ impl TriMesh3d { }) } + /// Returns a mapping of all mesh vertices to the set of their connected neighbor vertices + pub fn vertex_vertex_connectivity(&self) -> Vec> { + profile!("vertex_vertex_connectivity"); + + let mut connectivity_map: Vec> = + vec![Vec::with_capacity(4); self.vertices().len()]; + for tri in &self.triangles { + for &i in tri { + for &j in tri { + if i != j && !connectivity_map[i].contains(&j) { + connectivity_map[i].push(j); + } + } + } + } + + connectivity_map + } + /// Same as [`Self::vertex_normal_directions_inplace`] but assumes that the output is already zeroed fn vertex_normal_directions_inplace_assume_zeroed(&self, normal_directions: &mut [Vector3]) { assert_eq!(normal_directions.len(), self.vertices.len()); @@ -595,6 +664,29 @@ impl> MeshWithData { } } + /// Replaces the mesh but keeps the data + pub fn with_mesh>(self, new_mesh: NewMeshT) -> MeshWithData { + if !self.point_attributes.is_empty() { + assert_eq!( + self.mesh.vertices().len(), + new_mesh.vertices().len(), + "number of vertices should match if there are point attributes" + ); + } + if !self.cell_attributes.is_empty() { + assert_eq!( + self.mesh.cells().len(), + new_mesh.cells().len(), + "number of cells should match if there are cell attributes" + ) + } + MeshWithData { + mesh: new_mesh, + point_attributes: self.point_attributes, + cell_attributes: self.cell_attributes, + } + } + /// Attaches an attribute to the points of the mesh, panics if the length of the data does not match the mesh's number of points pub fn with_point_data(mut self, point_attribute: impl Into>) -> Self { let point_attribute = point_attribute.into(); @@ -637,7 +729,7 @@ impl MeshAttribute { } } - /// Converts the mesh attribute to a [`vtkio::model::Attribute`](https://docs.rs/vtkio/0.6.*/vtkio/model/enum.Attribute.html) + /// Converts the mesh attribute to a [`vtkio::model::Attribute`]) #[cfg(feature = "vtk_extras")] #[cfg_attr(doc_cfg, doc(cfg(feature = "vtk_extras")))] fn to_vtk_attribute(&self) -> Attribute { @@ -676,12 +768,12 @@ impl MeshWithData where R: Real, MeshT: Mesh3d, - for<'a> &'a MeshT: Into, + for<'a> &'a MeshT: IntoVtkUnstructuredGridPiece, { - /// Creates a [`vtkio::model::UnstructuredGridPiece`](https://docs.rs/vtkio/0.6.*/vtkio/model/struct.UnstructuredGridPiece.html) representing this mesh including its attached [`MeshAttribute`]s + /// Creates a [`vtkio::model::UnstructuredGridPiece`] representing this mesh including its attached [`MeshAttribute`]s #[cfg_attr(doc_cfg, doc(cfg(feature = "vtk_extras")))] - pub fn to_unstructured_grid(&self) -> UnstructuredGridPiece { - let mut grid_piece: UnstructuredGridPiece = (&self.mesh).into(); + fn unstructured_grid(&self) -> UnstructuredGridPiece { + let mut grid_piece: UnstructuredGridPiece = (&self.mesh).into_unstructured_grid(); for point_attribute in &self.point_attributes { grid_piece .data @@ -695,21 +787,73 @@ where } } -/// Creates a [`vtkio::model::UnstructuredGridPiece`](https://docs.rs/vtkio/0.6.*/vtkio/model/struct.UnstructuredGridPiece.html) representing this mesh with all its attributes and wraps it into a [`vtkio::model::DataSet`](https://docs.rs/vtkio/0.6.*/vtkio/model/enum.DataSet.html) -#[cfg(feature = "vtk_extras")] -#[cfg_attr(doc_cfg, doc(cfg(feature = "vtk_extras")))] -impl Into for &MeshWithData -where - R: Real, - MeshT: Mesh3d, - for<'a> &'a MeshT: Into, -{ - fn into(self) -> DataSet { - DataSet::inline(self.to_unstructured_grid()) - } +macro_rules! impl_into_vtk { + ($name:tt) => { + #[cfg(feature = "vtk_extras")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "vtk_extras")))] + impl IntoVtkUnstructuredGridPiece for $name { + fn into_unstructured_grid(self) -> UnstructuredGridPiece { + vtk_helper::mesh_to_unstructured_grid(&self) + } + } + + #[cfg(feature = "vtk_extras")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "vtk_extras")))] + impl IntoVtkUnstructuredGridPiece for &$name { + fn into_unstructured_grid(self) -> UnstructuredGridPiece { + vtk_helper::mesh_to_unstructured_grid(self) + } + } + + #[cfg(feature = "vtk_extras")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "vtk_extras")))] + impl<'a, R: Real> IntoVtkUnstructuredGridPiece for std::borrow::Cow<'a, $name> { + fn into_unstructured_grid(self) -> UnstructuredGridPiece { + vtk_helper::mesh_to_unstructured_grid(&self) + } + } + + #[cfg(feature = "vtk_extras")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "vtk_extras")))] + impl<'a, R: Real> IntoVtkUnstructuredGridPiece for &std::borrow::Cow<'a, $name> { + fn into_unstructured_grid(self) -> UnstructuredGridPiece { + vtk_helper::mesh_to_unstructured_grid(&self) + } + } + + #[cfg(feature = "vtk_extras")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "vtk_extras")))] + impl IntoVtkUnstructuredGridPiece for &MeshWithData> { + fn into_unstructured_grid(self) -> UnstructuredGridPiece { + self.unstructured_grid() + } + } + + #[cfg(feature = "vtk_extras")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "vtk_extras")))] + impl IntoVtkUnstructuredGridPiece for MeshWithData> { + fn into_unstructured_grid(self) -> UnstructuredGridPiece { + self.unstructured_grid() + } + } + + #[cfg(feature = "vtk_extras")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "vtk_extras")))] + impl<'a, R: Real> IntoVtkUnstructuredGridPiece + for &MeshWithData>> + { + fn into_unstructured_grid(self) -> UnstructuredGridPiece { + self.unstructured_grid() + } + } + }; } -/// Trait implementations to convert meshes into types supported by [`vtkio`](https://github.com/elrnv/vtkio) +impl_into_vtk!(TriMesh3d); +impl_into_vtk!(HexMesh3d); +impl_into_vtk!(PointCloud3d); + +/// Trait implementations to convert meshes into types supported by [`vtkio`] #[cfg(feature = "vtk_extras")] #[cfg_attr(doc_cfg, doc(cfg(feature = "vtk_extras")))] pub mod vtk_helper { @@ -718,40 +862,67 @@ pub mod vtk_helper { }; use vtkio::IOBuffer; - use super::{ - CellConnectivity, HexCell, HexMesh3d, Mesh3d, PointCell, PointCloud3d, Real, TriMesh3d, - TriangleCell, - }; + use super::{CellConnectivity, HexCell, Mesh3d, PointCell, Real, TriangleCell}; - /// Trait that can be implemented by mesh cells to return the corresponding [`vtkio::model::CellType`](https://docs.rs/vtkio/0.6.*/vtkio/model/enum.CellType.html) + /// Trait that can be implemented by mesh cells to return the corresponding [`vtkio::model::CellType`] #[cfg_attr(doc_cfg, doc(cfg(feature = "vtk_extras")))] pub trait HasVtkCellType { - /// Returns the corresponding [`vtkio::model::CellType`](https://docs.rs/vtkio/0.6.*/vtkio/model/enum.CellType.html) of the cell - fn vtk_cell_type() -> CellType; + /// Returns the corresponding [`vtkio::model::CellType`] of the cell + fn vtk_cell_type(&self) -> CellType; } #[cfg_attr(doc_cfg, doc(cfg(feature = "vtk_extras")))] impl HasVtkCellType for TriangleCell { - fn vtk_cell_type() -> CellType { + fn vtk_cell_type(&self) -> CellType { CellType::Triangle } } #[cfg_attr(doc_cfg, doc(cfg(feature = "vtk_extras")))] impl HasVtkCellType for HexCell { - fn vtk_cell_type() -> CellType { + fn vtk_cell_type(&self) -> CellType { CellType::Hexahedron } } #[cfg_attr(doc_cfg, doc(cfg(feature = "vtk_extras")))] impl HasVtkCellType for PointCell { - fn vtk_cell_type() -> CellType { + fn vtk_cell_type(&self) -> CellType { CellType::Vertex } } - /// Converts any supported mesh to a [`vtkio::model::UnstructuredGridPiece`](https://docs.rs/vtkio/0.6.*/vtkio/model/struct.UnstructuredGridPiece.html) + /// Conversion of meshes into a [`vtkio::model::UnstructuredGridPiece`] + #[cfg_attr(doc_cfg, doc(cfg(feature = "vtk_extras")))] + pub trait IntoVtkUnstructuredGridPiece { + fn into_unstructured_grid(self) -> UnstructuredGridPiece; + } + + /// Direct conversion of meshes into a full [`vtkio::model::DataSet`] + #[cfg_attr(doc_cfg, doc(cfg(feature = "vtk_extras")))] + pub trait IntoVtkDataSet { + fn into_dataset(self) -> DataSet; + } + + impl IntoVtkUnstructuredGridPiece for UnstructuredGridPiece { + fn into_unstructured_grid(self) -> UnstructuredGridPiece { + self + } + } + + impl IntoVtkDataSet for T { + fn into_dataset(self) -> DataSet { + DataSet::inline(self.into_unstructured_grid()) + } + } + + impl IntoVtkDataSet for DataSet { + fn into_dataset(self) -> DataSet { + self + } + } + + /// Converts any supported mesh to a [`vtkio::model::UnstructuredGridPiece`] #[cfg_attr(doc_cfg, doc(cfg(feature = "vtk_extras")))] pub fn mesh_to_unstructured_grid<'a, R, MeshT>(mesh: &'a MeshT) -> UnstructuredGridPiece where @@ -765,78 +936,22 @@ pub mod vtk_helper { points }; - let vertices_per_cell = MeshT::Cell::num_vertices(); let vertices = { - let mut vertices = Vec::with_capacity(mesh.cells().len() * (vertices_per_cell + 1)); + let mut vertices = + Vec::with_capacity(mesh.cells().len() * (MeshT::Cell::expected_num_vertices() + 1)); for cell in mesh.cells().iter() { - vertices.push(vertices_per_cell as u32); + vertices.push(cell.num_vertices() as u32); cell.for_each_vertex(|v| vertices.push(v as u32)); } vertices }; - let cell_types = vec![::vtk_cell_type(); mesh.cells().len()]; + let mut cell_types = Vec::with_capacity(mesh.cells().len()); + cell_types.extend(mesh.cells().iter().map(|c| c.vtk_cell_type())); new_unstructured_grid_piece(points, vertices, cell_types) } - /// Creates a [`vtkio::model::UnstructuredGridPiece`](https://docs.rs/vtkio/0.6.*/vtkio/model/struct.UnstructuredGridPiece.html) representing this mesh - #[cfg_attr(doc_cfg, doc(cfg(feature = "vtk_extras")))] - impl From<&TriMesh3d> for UnstructuredGridPiece - where - R: Real, - { - fn from(mesh: &TriMesh3d) -> Self { - mesh_to_unstructured_grid(mesh) - } - } - - /// Creates a [`vtkio::model::UnstructuredGridPiece`](https://docs.rs/vtkio/0.6.*/vtkio/model/struct.UnstructuredGridPiece.html) representing this mesh - #[cfg_attr(doc_cfg, doc(cfg(feature = "vtk_extras")))] - impl<'a, R> From<&'a HexMesh3d> for UnstructuredGridPiece - where - R: Real, - { - fn from(mesh: &'a HexMesh3d) -> Self { - mesh_to_unstructured_grid(mesh) - } - } - - /// Creates a [`vtkio::model::UnstructuredGridPiece`](https://docs.rs/vtkio/0.6.*/vtkio/model/struct.UnstructuredGridPiece.html) representing this point cloud - #[cfg_attr(doc_cfg, doc(cfg(feature = "vtk_extras")))] - impl<'a, R> From<&'a PointCloud3d> for UnstructuredGridPiece - where - R: Real, - { - fn from(mesh: &'a PointCloud3d) -> Self { - mesh_to_unstructured_grid(mesh) - } - } - - /// Creates a [`vtkio::model::UnstructuredGridPiece`](https://docs.rs/vtkio/0.6.*/vtkio/model/struct.UnstructuredGridPiece.html) representing this mesh and wraps it into a [`vtkio::model::DataSet`](https://docs.rs/vtkio/0.6.*/vtkio/model/enum.DataSet.html) - #[cfg_attr(doc_cfg, doc(cfg(feature = "vtk_extras")))] - impl Into for &TriMesh3d { - fn into(self) -> DataSet { - DataSet::inline(UnstructuredGridPiece::from(self)) - } - } - - /// Creates a [`vtkio::model::UnstructuredGridPiece`](https://docs.rs/vtkio/0.6.*/vtkio/model/struct.UnstructuredGridPiece.html) representing this mesh and wraps it into a [`vtkio::model::DataSet`](https://docs.rs/vtkio/0.6.*/vtkio/model/enum.DataSet.html) - #[cfg_attr(doc_cfg, doc(cfg(feature = "vtk_extras")))] - impl Into for &HexMesh3d { - fn into(self) -> DataSet { - DataSet::inline(UnstructuredGridPiece::from(self)) - } - } - - /// Creates a [`vtkio::model::UnstructuredGridPiece`](https://docs.rs/vtkio/0.6.*/vtkio/model/struct.UnstructuredGridPiece.html) representing this point cloud and wraps it into a [`vtkio::model::DataSet`](https://docs.rs/vtkio/0.6.*/vtkio/model/enum.DataSet.html) - #[cfg_attr(doc_cfg, doc(cfg(feature = "vtk_extras")))] - impl Into for &PointCloud3d { - fn into(self) -> DataSet { - DataSet::inline(UnstructuredGridPiece::from(self)) - } - } - fn new_unstructured_grid_piece>( points: B, vertices: Vec, diff --git a/splashsurf_lib/src/traits.rs b/splashsurf_lib/src/traits.rs index 8bdb185..7c7a145 100644 --- a/splashsurf_lib/src/traits.rs +++ b/splashsurf_lib/src/traits.rs @@ -3,7 +3,7 @@ use std::hash::Hash; use std::ops::{AddAssign, MulAssign, SubAssign}; use bytemuck::Pod; -use nalgebra::{RealField, SVector}; +use nalgebra::{RealField, SMatrix}; use num_integer::Integer; use num_traits::{ Bounded, CheckedAdd, CheckedMul, CheckedSub, FromPrimitive, NumCast, SaturatingSub, ToPrimitive, @@ -116,23 +116,6 @@ RealField + Pod + ThreadSafe { - /// Tries to convert this value to another [`Real`] type `T` by converting first to `f64` followed by `T::from_f64`. If the value cannot be represented by the target type, `None` is returned. - fn try_convert(self) -> Option { - Some(T::from_f64(self.to_f64()?)?) - } - - /// Tries to convert the values of a statically sized `nalgebra::SVector` to another type, same behavior as [`Real::try_convert`] - fn try_convert_vec_from(vec: &SVector) -> Option> - where - R: Real, - { - let mut converted = SVector::::zeros(); - for i in 0..D { - converted[i] = vec[i].try_convert()? - } - Some(converted) - } - /// Converts this value to the specified [`Index`] type. If the value cannot be represented by the target type, `None` is returned. fn to_index(self) -> Option { I::from_f64(self.to_f64()?) @@ -193,3 +176,41 @@ impl< > Real for T { } + +/// Trait for converting values, matrices, etc. from one `Real` type to another. +pub trait RealConvert: Sized { + type Out + where + To: Real; + + /// Tries to convert this value to the target type, returns `None` if value cannot be represented by the target type + fn try_convert(self) -> Option>; + /// Converts this value to the target type, panics if value cannot be represented by the target type + fn convert(self) -> Self::Out { + self.try_convert().expect("failed to convert") + } +} + +impl RealConvert for &From { + type Out = To where To: Real; + + fn try_convert(self) -> Option { + ::from(*self) + } +} + +impl RealConvert for SMatrix { + type Out = SMatrix where To: Real; + + fn try_convert(self) -> Option> { + let mut m_out: SMatrix = SMatrix::zeros(); + m_out + .iter_mut() + .zip(self.iter()) + .try_for_each(|(x_out, x_in)| { + *x_out = ::from(*x_in)?; + Some(()) + })?; + Some(m_out) + } +} diff --git a/splashsurf_lib/src/utils.rs b/splashsurf_lib/src/utils.rs index 48d4cc3..4912f22 100644 --- a/splashsurf_lib/src/utils.rs +++ b/splashsurf_lib/src/utils.rs @@ -4,6 +4,14 @@ use log::info; use rayon::prelude::*; use std::cell::UnsafeCell; +/// "Convert" an empty vector to preserve allocated memory if size and alignment matches +/// See https://users.rust-lang.org/t/pattern-how-to-reuse-a-vec-str-across-loop-iterations/61657/5 +/// See https://github.com/rust-lang/rfcs/pull/2802 +pub(crate) fn recycle(mut v: Vec) -> Vec { + v.clear(); + v.into_iter().map(|_| unreachable!()).collect() +} + /// Macro version of Option::map that allows using e.g. using the ?-operator in the map expression /// /// For example: diff --git a/splashsurf_lib/tests/integration_tests/test_octree.rs b/splashsurf_lib/tests/integration_tests/test_octree.rs index 27ef429..3773964 100644 --- a/splashsurf_lib/tests/integration_tests/test_octree.rs +++ b/splashsurf_lib/tests/integration_tests/test_octree.rs @@ -25,7 +25,7 @@ fn octree_to_file, I: Index, R: Real>( path: P, ) { let mesh = octree.hexmesh(&grid, false); - io::vtk_format::write_vtk(mesh.to_unstructured_grid(), path.as_ref(), "octree").unwrap(); + io::vtk_format::write_vtk(mesh, path.as_ref(), "octree").unwrap(); } #[test] From 62c06e34e49e041565e0a011933c78c8ff5fbd1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20L=C3=B6schner?= Date: Tue, 18 Jul 2023 18:57:38 +0200 Subject: [PATCH 02/22] Support for mixed tri/quad mesh --- splashsurf_lib/src/mesh.rs | 100 ++++++++++++++++++++++++++++++++----- 1 file changed, 88 insertions(+), 12 deletions(-) diff --git a/splashsurf_lib/src/mesh.rs b/splashsurf_lib/src/mesh.rs index 6749f3b..6ff6267 100644 --- a/splashsurf_lib/src/mesh.rs +++ b/splashsurf_lib/src/mesh.rs @@ -1,16 +1,17 @@ //! Basic mesh types used by the library and implementation of VTK export //! -//! This modules provides three basic types of meshes embedded in three dimensional spaces used +//! This modules provides four basic types of meshes embedded in three dimensional spaces used //! by the library: -//! - [`TriMesh3d`] -//! - [`HexMesh3d`] -//! - [`PointCloud3d`] +//! - [`TriMesh3d`]: triangle surface mesh in 3D space +//! - [`MixedTriQuadMesh3d`]: surface mesh in 3D space with triangle and/or quadrilateral cells +//! - [`PointCloud3d`]: points without connectivity in 3D space +//! - [`HexMesh3d`]: mesh with volumetric hexahedral cells //! -//! Furthermore, it provides the [`MeshWithData`] type that is used when additional attributes are -//! attached to the vertices (e.g. normals) or cells (e.g. some identifiers) of the mesh. +//! Furthermore, it provides the [`MeshWithData`] type that can be used to attach additional +//! attributes to vertices (e.g. normals) or cells (e.g. areas/aspect ratios) of the mesh. //! -//! If the `vtk_extras` feature is enabled, this module also provides features for conversion of these -//! meshes to [`vtkio`] data structures. For example: +//! If the `vtk_extras` feature is enabled, this module also provides traits to convert these +//! meshes to [`vtkio`] data types for serialization to `VTK` files. For example: //! - [`IntoVtkUnstructuredGridPiece`] to convert basic meshes and meshes with attached attributes to the //! - [`IntoVtkDataSet`] for all meshes implementing [`IntoVtkUnstructuredGridPiece`] to directly save a mesh as a VTK file @@ -55,6 +56,42 @@ pub struct TriMesh3d { pub triangles: Vec<[usize; 3]>, } +/// Cell type for [`MixedTriQuadMesh3d`] +#[derive(Clone, Debug)] +pub enum TriangleOrQuadCell { + /// Vertex indices representing a triangle + Tri([usize; 3]), + /// Vertex indices representing a quadrilateral + Quad([usize; 4]), +} + +impl TriangleOrQuadCell { + /// Returns the number of actual number of vertices of this cell (3 if triangle, 4 if quad) + fn num_vertices(&self) -> usize { + match self { + TriangleOrQuadCell::Tri(_) => 3, + TriangleOrQuadCell::Quad(_) => 4, + } + } + + /// Returns the slice of vertex indices of this cell + fn vertices(&self) -> &[usize] { + match self { + TriangleOrQuadCell::Tri(v) => v, + TriangleOrQuadCell::Quad(v) => v, + } + } +} + +/// A surface mesh in 3D containing cells representing either triangles or quadrilaterals +#[derive(Clone, Debug, Default)] +pub struct MixedTriQuadMesh3d { + /// Coordinates of all vertices of the mesh + pub vertices: Vec>, + /// All triangle cells of the mesh + pub cells: Vec, +} + /// A hexahedral (volumetric) mesh in 3D #[derive(Clone, Debug, Default)] pub struct HexMesh3d { @@ -130,15 +167,15 @@ pub trait CellConnectivity { } } -/// Cell type for the [`TriMesh3d`] +/// Cell type for [`TriMesh3d`] #[derive(Copy, Clone, Pod, Zeroable)] #[repr(transparent)] pub struct TriangleCell(pub [usize; 3]); -/// Cell type for the [`HexMesh3d`] +/// Cell type for [`HexMesh3d`] #[derive(Copy, Clone, Pod, Zeroable)] #[repr(transparent)] pub struct HexCell(pub [usize; 8]); -/// Cell type for the [`PointCloud3d`] +/// Cell type for [`PointCloud3d`] #[derive(Copy, Clone, Pod, Zeroable)] #[repr(transparent)] pub struct PointCell(pub usize); @@ -153,6 +190,20 @@ impl CellConnectivity for TriangleCell { } } +impl CellConnectivity for TriangleOrQuadCell { + fn expected_num_vertices() -> usize { + 4 + } + + fn num_vertices(&self) -> usize { + return self.num_vertices(); + } + + fn try_for_each_vertex Result<(), E>>(&self, f: F) -> Result<(), E> { + self.vertices().iter().copied().try_for_each(f) + } +} + impl CellConnectivity for HexCell { fn expected_num_vertices() -> usize { 8 @@ -185,6 +236,18 @@ impl Mesh3d for TriMesh3d { } } +impl Mesh3d for MixedTriQuadMesh3d { + type Cell = TriangleOrQuadCell; + + fn vertices(&self) -> &[Vector3] { + self.vertices.as_slice() + } + + fn cells(&self) -> &[TriangleOrQuadCell] { + &self.cells + } +} + impl Mesh3d for HexMesh3d { type Cell = HexCell; @@ -850,6 +913,7 @@ macro_rules! impl_into_vtk { } impl_into_vtk!(TriMesh3d); +impl_into_vtk!(MixedTriQuadMesh3d); impl_into_vtk!(HexMesh3d); impl_into_vtk!(PointCloud3d); @@ -862,7 +926,9 @@ pub mod vtk_helper { }; use vtkio::IOBuffer; - use super::{CellConnectivity, HexCell, Mesh3d, PointCell, Real, TriangleCell}; + use super::{ + CellConnectivity, HexCell, Mesh3d, PointCell, Real, TriangleCell, TriangleOrQuadCell, + }; /// Trait that can be implemented by mesh cells to return the corresponding [`vtkio::model::CellType`] #[cfg_attr(doc_cfg, doc(cfg(feature = "vtk_extras")))] @@ -878,6 +944,16 @@ pub mod vtk_helper { } } + #[cfg_attr(doc_cfg, doc(cfg(feature = "vtk_extras")))] + impl HasVtkCellType for TriangleOrQuadCell { + fn vtk_cell_type(&self) -> CellType { + match self { + TriangleOrQuadCell::Tri(_) => CellType::Triangle, + TriangleOrQuadCell::Quad(_) => CellType::Quad, + } + } + } + #[cfg_attr(doc_cfg, doc(cfg(feature = "vtk_extras")))] impl HasVtkCellType for HexCell { fn vtk_cell_type(&self) -> CellType { From 81e5cdb4bf516b6d301d9df0071c4ed8aa21aaac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20L=C3=B6schner?= Date: Tue, 18 Jul 2023 18:57:11 +0200 Subject: [PATCH 03/22] Implement tris to quads postprocessing --- splashsurf/src/reconstruction.rs | 217 +++++++++++++++++------- splashsurf_lib/src/lib.rs | 5 +- splashsurf_lib/src/postprocessing.rs | 244 +++++++++++++++++++++++++++ 3 files changed, 408 insertions(+), 58 deletions(-) create mode 100644 splashsurf_lib/src/postprocessing.rs diff --git a/splashsurf/src/reconstruction.rs b/splashsurf/src/reconstruction.rs index 85e8e7f..6b8f0a2 100644 --- a/splashsurf/src/reconstruction.rs +++ b/splashsurf/src/reconstruction.rs @@ -1,17 +1,14 @@ use crate::{io, logging}; use anyhow::{anyhow, Context}; -use arguments::{ - ReconstructionRunnerArgs, ReconstructionRunnerPathCollection, ReconstructionRunnerPaths, -}; use clap::value_parser; use indicatif::{ProgressBar, ProgressStyle}; use log::info; use rayon::prelude::*; use splashsurf_lib::mesh::{AttributeData, Mesh3d, MeshAttribute, MeshWithData, PointCloud3d}; use splashsurf_lib::nalgebra::{Unit, Vector3}; -use splashsurf_lib::profile; use splashsurf_lib::sph_interpolation::SphInterpolator; -use splashsurf_lib::{density_map, Index, Real}; +use splashsurf_lib::{density_map, profile, Index, Real}; +use std::borrow::Cow; use std::convert::TryFrom; use std::path::PathBuf; @@ -198,7 +195,7 @@ pub struct ReconstructSubcommandArgs { #[arg( help_heading = ARGS_INTERP, long, - default_value = "on", + default_value = "off", value_name = "off|on", ignore_case = true, require_equals = true @@ -207,6 +204,35 @@ pub struct ReconstructSubcommandArgs { /// List of point attribute field names from the input file that should be interpolated to the reconstructed surface. Currently this is only supported for VTK and VTU input files. #[arg(help_heading = ARGS_INTERP, long)] pub interpolate_attributes: Vec, + /// Whether to write out the raw reconstructed mesh before applying any post-processing steps + #[arg( + help_heading = ARGS_INTERP, + long, + default_value = "off", + value_name = "off|on", + ignore_case = true, + require_equals = true + )] + pub output_raw_mesh: Switch, + /// Whether to try to convert triangles to quads if they meet quality criteria + #[arg( + help_heading = ARGS_INTERP, + long, + default_value = "off", + value_name = "off|on", + ignore_case = true, + require_equals = true + )] + pub generate_quads: Switch, + /// Maximum allowed ratio of quad edge lengths to its diagonals to merge two triangles to a quad (inverse is used for minimum) + #[arg(help_heading = ARGS_INTERP, long, default_value = "1.75")] + pub quad_max_edge_diag_ratio: f64, + /// Maximum allowed angle (in degrees) between triangle normals to merge them to a quad + #[arg(help_heading = ARGS_INTERP, long, default_value = "10")] + pub quad_max_normal_angle: f64, + /// Maximum allowed vertex interior angle (in degrees) inside of a quad to merge two triangles to a quad + #[arg(help_heading = ARGS_INTERP, long, default_value = "135")] + pub quad_max_interior_angle: f64, /// Lower corner of the bounding-box for the surface mesh, mesh outside gets cut away (requires mesh-max to be specified) #[arg( @@ -346,11 +372,17 @@ mod arguments { pub compute_normals: bool, pub sph_normals: bool, pub interpolate_attributes: Vec, + pub generate_quads: bool, + pub quad_max_edge_diag_ratio: f64, + pub quad_max_normal_angle: f64, + pub quad_max_interior_angle: f64, + pub output_raw_mesh: bool, pub mesh_aabb: Option>, } /// All arguments that can be supplied to the surface reconstruction tool converted to useful types pub struct ReconstructionRunnerArgs { + /// Parameters passed directly to the surface reconstruction pub params: splashsurf_lib::Parameters, pub use_double_precision: bool, pub io_params: io::FormatParameters, @@ -480,6 +512,11 @@ mod arguments { compute_normals: args.normals.into_bool(), sph_normals: args.sph_normals.into_bool(), interpolate_attributes: args.interpolate_attributes.clone(), + generate_quads: args.generate_quads.into_bool(), + quad_max_edge_diag_ratio: args.quad_max_edge_diag_ratio, + quad_max_normal_angle: args.quad_max_normal_angle, + quad_max_interior_angle: args.quad_max_interior_angle, + output_raw_mesh: args.output_raw_mesh.into_bool(), mesh_aabb, }; @@ -866,68 +903,94 @@ pub(crate) fn reconstruction_pipeline_generic( splashsurf_lib::reconstruct_surface::(particle_positions.as_slice(), ¶ms)?; let grid = reconstruction.grid(); - let mesh = reconstruction.mesh(); + let mut mesh_with_data = MeshWithData::new(Cow::Borrowed(reconstruction.mesh())); - let mesh = if let Some(aabb) = &postprocessing.mesh_aabb { - profile!("clamp mesh to aabb"); - info!("Post-processing: Clamping mesh to AABB..."); + if postprocessing.output_raw_mesh { + profile!("write surface mesh to file"); - let mut mesh = mesh.clone(); - mesh.clamp_with_aabb( - &aabb - .try_convert() - .ok_or_else(|| anyhow!("Failed to convert mesh AABB"))?, + let output_path = paths + .output_file + .parent() + // Add a trailing separator if the parent is non-empty + .map(|p| p.join("")) + .unwrap_or_else(PathBuf::new); + let output_filename = format!( + "raw_{}", + paths.output_file.file_name().unwrap().to_string_lossy() ); - mesh - } else { - mesh.clone() - }; - - // Add normals to mesh if requested - let mesh = if postprocessing.compute_normals || !attributes.is_empty() { - profile!("compute normals"); + let raw_output_file = output_path.join(output_filename); info!( - "Constructing global acceleration structure for SPH interpolation to {} vertices...", - mesh.vertices.len() + "Writing unprocessed surface mesh to \"{}\"...", + raw_output_file.display() ); - let particle_rest_density = params.rest_density; - let particle_rest_volume = R::from_f64((4.0 / 3.0) * std::f64::consts::PI).unwrap() - * params.particle_radius.powi(3); - let particle_rest_mass = particle_rest_volume * particle_rest_density; - - let particle_densities = reconstruction - .particle_densities() - .ok_or_else(|| anyhow::anyhow!("Particle densities were not returned by surface reconstruction but are required for SPH normal computation"))? - .as_slice(); - assert_eq!( - particle_positions.len(), - particle_densities.len(), - "There has to be one density value per particle" - ); - - let interpolator = SphInterpolator::new( - &particle_positions, - particle_densities, - particle_rest_mass, - params.compact_support_radius, - ); + io::write_mesh(&mesh_with_data, raw_output_file, &io_params.output).with_context(|| { + anyhow!( + "Failed to write raw output mesh to file \"{}\"", + paths.output_file.display() + ) + })?; + } - let mut mesh_with_data = MeshWithData::new(mesh); - let mesh = &mesh_with_data.mesh; + // Perform post-processing + { + profile!("postprocessing"); + + // Initialize SPH interpolator if required later + let interpolator_required = postprocessing.sph_normals || !attributes.is_empty(); + let interpolator = if interpolator_required { + profile!("initialize interpolator"); + info!("Post-processing: Initializing interpolator..."); + + info!( + "Constructing global acceleration structure for SPH interpolation to {} vertices...", + mesh_with_data.vertices().len() + ); + + let particle_rest_density = params.rest_density; + let particle_rest_volume = R::from_f64((4.0 / 3.0) * std::f64::consts::PI).unwrap() + * params.particle_radius.powi(3); + let particle_rest_mass = particle_rest_volume * particle_rest_density; + + let particle_densities = reconstruction + .particle_densities() + .ok_or_else(|| anyhow::anyhow!("Particle densities were not returned by surface reconstruction but are required for SPH normal computation"))? + .as_slice(); + assert_eq!( + particle_positions.len(), + particle_densities.len(), + "There has to be one density value per particle" + ); + + Some(SphInterpolator::new( + &particle_positions, + particle_densities, + particle_rest_mass, + params.compact_support_radius, + )) + } else { + None + }; - // Compute normals if requested + // Add normals to mesh if requested if postprocessing.compute_normals { + profile!("compute normals"); + info!("Post-processing: Computing surface normals..."); + + // Compute normals let normals = if postprocessing.sph_normals { info!("Using SPH interpolation to compute surface normals"); - let sph_normals = interpolator.interpolate_normals(mesh.vertices()); + let sph_normals = interpolator + .as_ref() + .expect("interpolator is required") + .interpolate_normals(mesh_with_data.vertices()); bytemuck::allocation::cast_vec::>, Vector3>(sph_normals) } else { info!("Using area weighted triangle normals for surface normals"); profile!("mesh.par_vertex_normals"); - let tri_normals = mesh.par_vertex_normals(); + let tri_normals = mesh_with_data.mesh.par_vertex_normals(); // Convert unit vectors to plain vectors bytemuck::allocation::cast_vec::>, Vector3>(tri_normals) @@ -941,6 +1004,10 @@ pub(crate) fn reconstruction_pipeline_generic( // Interpolate attributes if requested if !attributes.is_empty() { + profile!("interpolate attributes"); + info!("Post-processing: Interpolating attributes..."); + let interpolator = interpolator.as_ref().expect("interpolator is required"); + for attribute in attributes.into_iter() { info!("Interpolating attribute \"{}\"...", attribute.name); @@ -948,7 +1015,7 @@ pub(crate) fn reconstruction_pipeline_generic( AttributeData::ScalarReal(values) => { let interpolated_values = interpolator.interpolate_scalar_quantity( values.as_slice(), - mesh.vertices(), + mesh_with_data.vertices(), true, ); mesh_with_data.point_attributes.push(MeshAttribute::new( @@ -959,7 +1026,7 @@ pub(crate) fn reconstruction_pipeline_generic( AttributeData::Vector3Real(values) => { let interpolated_values = interpolator.interpolate_vector_quantity( values.as_slice(), - mesh.vertices(), + mesh_with_data.vertices(), true, ); mesh_with_data.point_attributes.push(MeshAttribute::new( @@ -971,10 +1038,27 @@ pub(crate) fn reconstruction_pipeline_generic( } } } + } + + // Convert triangles to quads + let (tri_mesh, tri_quad_mesh) = if postprocessing.generate_quads { + info!("Post-processing: Convert triangles to quads..."); + let non_squareness_limit = R::from_f64(postprocessing.quad_max_edge_diag_ratio).unwrap(); + let normal_angle_limit_rad = + R::from_f64(postprocessing.quad_max_normal_angle.to_radians()).unwrap(); + let max_interior_angle = + R::from_f64(postprocessing.quad_max_interior_angle.to_radians()).unwrap(); + + let tri_quad_mesh = splashsurf_lib::postprocessing::convert_tris_to_quads( + &mesh_with_data.mesh, + non_squareness_limit, + normal_angle_limit_rad, + max_interior_angle, + ); - mesh_with_data + (None, Some(mesh_with_data.with_mesh(tri_quad_mesh))) } else { - MeshWithData::new(mesh) + (Some(mesh_with_data), None) }; // Store the surface mesh @@ -985,7 +1069,17 @@ pub(crate) fn reconstruction_pipeline_generic( paths.output_file.display() ); - io::write_mesh(&mesh, paths.output_file.clone(), &io_params.output).with_context(|| { + match (&tri_mesh, &tri_quad_mesh) { + (Some(mesh), None) => { + io::write_mesh(mesh, paths.output_file.clone(), &io_params.output) + } + (None, Some(mesh)) => { + io::write_mesh(mesh, paths.output_file.clone(), &io_params.output) + } + + _ => unreachable!(), + } + .with_context(|| { anyhow!( "Failed to write output mesh to file \"{}\"", paths.output_file.display() @@ -1063,7 +1157,16 @@ pub(crate) fn reconstruction_pipeline_generic( } if postprocessing.check_mesh { - if let Err(err) = splashsurf_lib::marching_cubes::check_mesh_consistency(grid, &mesh.mesh) { + if let Err(err) = match (&tri_mesh, &tri_quad_mesh) { + (Some(mesh), None) => { + splashsurf_lib::marching_cubes::check_mesh_consistency(grid, &mesh.mesh) + } + (None, Some(_mesh)) => { + info!("Checking for mesh consistency not implemented for quad mesh at the moment."); + return Ok(()); + } + _ => unreachable!(), + } { return Err(anyhow!("{}", err)); } else { info!("Checked mesh for problems (holes, etc.), no problems were found."); diff --git a/splashsurf_lib/src/lib.rs b/splashsurf_lib/src/lib.rs index 0814039..770f4cd 100644 --- a/splashsurf_lib/src/lib.rs +++ b/splashsurf_lib/src/lib.rs @@ -65,6 +65,7 @@ pub mod marching_cubes; pub mod mesh; pub mod neighborhood_search; pub mod octree; +pub mod postprocessing; pub(crate) mod reconstruction; mod reconstruction_octree; pub mod sph_interpolation; @@ -88,8 +89,10 @@ pub(crate) mod workspace; pub(crate) type HashState = fxhash::FxBuildHasher; pub(crate) type MapType = std::collections::HashMap; +pub(crate) type SetType = std::collections::HashSet; pub(crate) fn new_map() -> MapType { - MapType::with_hasher(HashState::default()) + // TODO: Remove this function + Default::default() } /* diff --git a/splashsurf_lib/src/postprocessing.rs b/splashsurf_lib/src/postprocessing.rs new file mode 100644 index 0000000..d82dbde --- /dev/null +++ b/splashsurf_lib/src/postprocessing.rs @@ -0,0 +1,244 @@ +//! Functions for post-processing of surface meshes (decimation, smoothing, etc.) + +use crate::mesh::{Mesh3d, MixedTriQuadMesh3d, TriMesh3d, TriangleOrQuadCell}; +use crate::{SetType, profile, MapType, Real}; +use log::info; +use nalgebra::Vector3; +use rayon::prelude::*; + +/// Merges triangles sharing an edge to quads if they fulfill the given criteria +pub fn convert_tris_to_quads( + mesh: &TriMesh3d, + non_squareness_limit: R, + normal_angle_limit_rad: R, + max_interior_angle: R, +) -> MixedTriQuadMesh3d { + profile!("tri_to_quad"); + + let vert_tri_map = mesh.vertex_cell_connectivity(); + let tri_normals = mesh + .triangles + .par_iter() + .map(|tri| { + let v0 = &mesh.vertices[tri[0]]; + let v1 = &mesh.vertices[tri[1]]; + let v2 = &mesh.vertices[tri[2]]; + (v1 - v0).cross(&(v2 - v1)).normalize() + }) + .collect::>(); + + let min_dot = normal_angle_limit_rad.cos(); + let max_non_squareness = non_squareness_limit; + let sqrt_two = R::from_f64(2.0_f64.sqrt()).unwrap(); + + let tris_to_quad = |tri_i: &[usize; 3], tri_j: &[usize; 3]| -> [usize; 4] { + let mut quad = [0, 0, 0, 0]; + let missing_vertex: usize = tri_j.iter().copied().find(|v| !tri_i.contains(v)).unwrap(); + + quad[0] = tri_i[0]; + if tri_j.contains(&tri_i[0]) { + if tri_j.contains(&tri_i[1]) { + quad[1] = missing_vertex; + quad[2] = tri_i[1]; + quad[3] = tri_i[2]; + } else { + quad[1] = tri_i[1]; + quad[2] = tri_i[2]; + quad[3] = missing_vertex; + } + } else { + if tri_j.contains(&tri_i[1]) { + quad[1] = tri_i[1]; + quad[2] = missing_vertex; + quad[3] = tri_i[2]; + } else { + panic!("this should not happen"); + } + } + + quad + }; + + /// Computes the interior angle of a quad at vertex v_center with the quad given by [v_prev, v_center, v_next, v_opp] + fn quad_interior_angle( + v_center: &Vector3, + v_prev: &Vector3, + v_next: &Vector3, + v_opp: &Vector3, + ) -> R { + let d_prev = v_prev - v_center; + let d_middle = v_opp - v_center; + let d_next = v_next - v_center; + + let l_prev = d_prev.norm(); + let l_middle = d_middle.norm(); + let l_next = d_next.norm(); + + let angle_prev = (d_prev.dot(&d_middle)).unscale(l_prev * l_middle).acos(); + let angle_next = (d_middle.dot(&d_next)).unscale(l_middle * l_next).acos(); + + angle_prev + angle_next + } + + let quad_candidates = mesh + .triangles + .par_iter() + .enumerate() + .filter_map(|(i, tri_i)| { + for &vert in tri_i { + // Loop over all triangles of all vertices of the triangle from iterator chain + for &j in &vert_tri_map[vert] { + // Skip triangle from iterator chain and make sure that we process every triangle pair only once + if j <= i { + continue; + } else { + let tri_j = &mesh.triangles[j]; + let mut recurring_verts = 0; + let mut other_vert = 0; + for &v in tri_j { + if tri_i.contains(&v) { + recurring_verts += 1; + if v != vert { + other_vert = v; + } + } + } + + // Found triangle pair with shared edge + if recurring_verts == 2 { + let mut convert = false; + let mut quality = R::one(); + + // Check "squareness" of the two triangles + { + let quad = tris_to_quad(tri_i, tri_j); + + // Compute diagonal edge length + let diag = (mesh.vertices[vert] - mesh.vertices[other_vert]).norm(); + let max_length = (diag / sqrt_two) * max_non_squareness; + let min_length = (diag / sqrt_two) * max_non_squareness.recip(); + + let v0 = mesh.vertices[quad[0]]; + let v1 = mesh.vertices[quad[1]]; + let v2 = mesh.vertices[quad[2]]; + let v3 = mesh.vertices[quad[3]]; + + let d0 = v1 - v0; + let d1 = v2 - v1; + let d2 = v3 - v2; + let d3 = v0 - v3; + + let edge_ls = [d0.norm(), d1.norm(), d2.norm(), d3.norm()]; + + let angles = [ + quad_interior_angle(&v0, &v3, &v1, &v2), + quad_interior_angle(&v1, &v0, &v2, &v3), + quad_interior_angle(&v2, &v3, &v1, &v0), + quad_interior_angle(&v3, &v2, &v0, &v1), + ]; + + if edge_ls + .iter() + .all(|&edge_l| edge_l <= max_length && edge_l >= min_length) + && angles.iter().all(|&angle| angle <= max_interior_angle) + { + convert = true; + + let mut longest = edge_ls[0]; + let mut shortest = edge_ls[0]; + + for l in edge_ls { + longest = longest.max(l); + shortest = shortest.min(l); + } + + quality = shortest / longest; + } + } + + // Check normal deviation of triangles + if convert { + let dot_i_j = tri_normals[i].dot(&tri_normals[j]); + if dot_i_j >= min_dot { + convert = convert; + } else { + convert = false; + } + } + + if convert { + // Return the two triangle indices that should be merged + return Some(((i, j), quality)); + } + } + } + } + } + + return None; + }) + .collect::>(); + + info!( + "Number of quad conversion candidates: {}", + quad_candidates.len() + ); + + //let mut triangles_to_remove = new_map(); + let mut triangles_to_remove = SetType::default(); + let mut filtered_candidates = SetType::default(); + for ((i, j), _q) in quad_candidates { + // TODO: If triangle already exists in list, compare quality + + if !triangles_to_remove.contains(&i) && !triangles_to_remove.contains(&j) { + //triangles_to_remove.insert(i, ((i,j), q)); + //triangles_to_remove.insert(j, ((i,j), q)); + triangles_to_remove.insert(i); + triangles_to_remove.insert(j); + + filtered_candidates.insert((i, j)); + } + } + + let quads: Vec<[usize; 4]> = filtered_candidates + .par_iter() + .copied() + .map(|(i, j)| { + let tri_i = &mesh.triangles[i]; + let tri_j = &mesh.triangles[j]; + tris_to_quad(tri_i, tri_j) + }) + .collect::<_>(); + + let filtered_triangles = mesh + .triangles + .par_iter() + .copied() + .enumerate() + .filter_map(|(i, tri)| (!triangles_to_remove.contains(&i)).then_some(tri)) + .collect::>(); + + info!("Before conversion: {} triangles", mesh.triangles.len()); + info!( + "After conversion: {} triangles, {} quads, {} total cells ({:.2}% fewer)", + filtered_triangles.len(), + quads.len(), + filtered_triangles.len() + quads.len(), + (((mesh.triangles.len() - (filtered_triangles.len() + quads.len())) as f64) + / (mesh.triangles.len() as f64)) + * 100.0 + ); + + let mut cells = Vec::with_capacity(filtered_triangles.len() + quads.len()); + cells.extend( + filtered_triangles + .into_iter() + .map(|tri| TriangleOrQuadCell::Tri(tri)), + ); + cells.extend(quads.into_iter().map(|quad| TriangleOrQuadCell::Quad(quad))); + + MixedTriQuadMesh3d { + vertices: mesh.vertices.clone(), + cells, + } +} From 9c8d704eb7f1c9672e090f3c0de906fd82cf2ec0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20L=C3=B6schner?= Date: Tue, 18 Jul 2023 20:32:12 +0200 Subject: [PATCH 04/22] Implement basic half-edge triangle mesh, geometry helper functions --- data/icosphere.obj | 126 ++ data/icosphere_normals.obj | 206 +++ data/plane.obj | 1245 +++++++++++++++++ splashsurf_lib/src/halfedge_mesh.rs | 586 ++++++++ splashsurf_lib/src/lib.rs | 1 + splashsurf_lib/src/mesh.rs | 125 ++ splashsurf_lib/src/postprocessing.rs | 2 +- splashsurf_lib/tests/integration_tests/mod.rs | 2 + .../tests/integration_tests/test_mesh.rs | 66 + 9 files changed, 2358 insertions(+), 1 deletion(-) create mode 100644 data/icosphere.obj create mode 100644 data/icosphere_normals.obj create mode 100644 data/plane.obj create mode 100644 splashsurf_lib/src/halfedge_mesh.rs create mode 100644 splashsurf_lib/tests/integration_tests/test_mesh.rs diff --git a/data/icosphere.obj b/data/icosphere.obj new file mode 100644 index 0000000..b374c99 --- /dev/null +++ b/data/icosphere.obj @@ -0,0 +1,126 @@ +# Blender 3.5.0 +# www.blender.org +o Icosphere +v 0.000000 -1.000000 0.000000 +v 0.723607 -0.447220 0.525725 +v -0.276388 -0.447220 0.850649 +v -0.894426 -0.447216 0.000000 +v -0.276388 -0.447220 -0.850649 +v 0.723607 -0.447220 -0.525725 +v 0.276388 0.447220 0.850649 +v -0.723607 0.447220 0.525725 +v -0.723607 0.447220 -0.525725 +v 0.276388 0.447220 -0.850649 +v 0.894426 0.447216 0.000000 +v 0.000000 1.000000 0.000000 +v -0.162456 -0.850654 0.499995 +v 0.425323 -0.850654 0.309011 +v 0.262869 -0.525738 0.809012 +v 0.850648 -0.525736 0.000000 +v 0.425323 -0.850654 -0.309011 +v -0.525730 -0.850652 0.000000 +v -0.688189 -0.525736 0.499997 +v -0.162456 -0.850654 -0.499995 +v -0.688189 -0.525736 -0.499997 +v 0.262869 -0.525738 -0.809012 +v 0.951058 0.000000 0.309013 +v 0.951058 0.000000 -0.309013 +v 0.000000 0.000000 1.000000 +v 0.587786 0.000000 0.809017 +v -0.951058 0.000000 0.309013 +v -0.587786 0.000000 0.809017 +v -0.587786 0.000000 -0.809017 +v -0.951058 0.000000 -0.309013 +v 0.587786 0.000000 -0.809017 +v 0.000000 0.000000 -1.000000 +v 0.688189 0.525736 0.499997 +v -0.262869 0.525738 0.809012 +v -0.850648 0.525736 0.000000 +v -0.262869 0.525738 -0.809012 +v 0.688189 0.525736 -0.499997 +v 0.162456 0.850654 0.499995 +v 0.525730 0.850652 0.000000 +v -0.425323 0.850654 0.309011 +v -0.425323 0.850654 -0.309011 +v 0.162456 0.850654 -0.499995 +s 0 +f 1 14 13 +f 2 14 16 +f 1 13 18 +f 1 18 20 +f 1 20 17 +f 2 16 23 +f 3 15 25 +f 4 19 27 +f 5 21 29 +f 6 22 31 +f 2 23 26 +f 3 25 28 +f 4 27 30 +f 5 29 32 +f 6 31 24 +f 7 33 38 +f 8 34 40 +f 9 35 41 +f 10 36 42 +f 11 37 39 +f 39 42 12 +f 39 37 42 +f 37 10 42 +f 42 41 12 +f 42 36 41 +f 36 9 41 +f 41 40 12 +f 41 35 40 +f 35 8 40 +f 40 38 12 +f 40 34 38 +f 34 7 38 +f 38 39 12 +f 38 33 39 +f 33 11 39 +f 24 37 11 +f 24 31 37 +f 31 10 37 +f 32 36 10 +f 32 29 36 +f 29 9 36 +f 30 35 9 +f 30 27 35 +f 27 8 35 +f 28 34 8 +f 28 25 34 +f 25 7 34 +f 26 33 7 +f 26 23 33 +f 23 11 33 +f 31 32 10 +f 31 22 32 +f 22 5 32 +f 29 30 9 +f 29 21 30 +f 21 4 30 +f 27 28 8 +f 27 19 28 +f 19 3 28 +f 25 26 7 +f 25 15 26 +f 15 2 26 +f 23 24 11 +f 23 16 24 +f 16 6 24 +f 17 22 6 +f 17 20 22 +f 20 5 22 +f 20 21 5 +f 20 18 21 +f 18 4 21 +f 18 19 4 +f 18 13 19 +f 13 3 19 +f 16 17 6 +f 16 14 17 +f 14 1 17 +f 13 15 3 +f 13 14 15 +f 14 2 15 diff --git a/data/icosphere_normals.obj b/data/icosphere_normals.obj new file mode 100644 index 0000000..5fadb4f --- /dev/null +++ b/data/icosphere_normals.obj @@ -0,0 +1,206 @@ +# Blender 3.5.0 +# www.blender.org +o Icosphere +v 0.000000 -1.000000 0.000000 +v 0.723607 -0.447220 0.525725 +v -0.276388 -0.447220 0.850649 +v -0.894426 -0.447216 0.000000 +v -0.276388 -0.447220 -0.850649 +v 0.723607 -0.447220 -0.525725 +v 0.276388 0.447220 0.850649 +v -0.723607 0.447220 0.525725 +v -0.723607 0.447220 -0.525725 +v 0.276388 0.447220 -0.850649 +v 0.894426 0.447216 0.000000 +v 0.000000 1.000000 0.000000 +v -0.162456 -0.850654 0.499995 +v 0.425323 -0.850654 0.309011 +v 0.262869 -0.525738 0.809012 +v 0.850648 -0.525736 0.000000 +v 0.425323 -0.850654 -0.309011 +v -0.525730 -0.850652 0.000000 +v -0.688189 -0.525736 0.499997 +v -0.162456 -0.850654 -0.499995 +v -0.688189 -0.525736 -0.499997 +v 0.262869 -0.525738 -0.809012 +v 0.951058 0.000000 0.309013 +v 0.951058 0.000000 -0.309013 +v 0.000000 0.000000 1.000000 +v 0.587786 0.000000 0.809017 +v -0.951058 0.000000 0.309013 +v -0.587786 0.000000 0.809017 +v -0.587786 0.000000 -0.809017 +v -0.951058 0.000000 -0.309013 +v 0.587786 0.000000 -0.809017 +v 0.000000 0.000000 -1.000000 +v 0.688189 0.525736 0.499997 +v -0.262869 0.525738 0.809012 +v -0.850648 0.525736 0.000000 +v -0.262869 0.525738 -0.809012 +v 0.688189 0.525736 -0.499997 +v 0.162456 0.850654 0.499995 +v 0.525730 0.850652 0.000000 +v -0.425323 0.850654 0.309011 +v -0.425323 0.850654 -0.309011 +v 0.162456 0.850654 -0.499995 +vn 0.1024 -0.9435 0.3151 +vn 0.7002 -0.6617 0.2680 +vn -0.2680 -0.9435 0.1947 +vn -0.2680 -0.9435 -0.1947 +vn 0.1024 -0.9435 -0.3151 +vn 0.9050 -0.3304 0.2680 +vn 0.0247 -0.3304 0.9435 +vn -0.8897 -0.3304 0.3151 +vn -0.5746 -0.3304 -0.7488 +vn 0.5346 -0.3304 -0.7779 +vn 0.8026 -0.1256 0.5831 +vn -0.3066 -0.1256 0.9435 +vn -0.9921 -0.1256 -0.0000 +vn -0.3066 -0.1256 -0.9435 +vn 0.8026 -0.1256 -0.5831 +vn 0.4089 0.6617 0.6284 +vn -0.4713 0.6617 0.5831 +vn -0.7002 0.6617 -0.2680 +vn 0.0385 0.6617 -0.7488 +vn 0.7240 0.6617 -0.1947 +vn 0.2680 0.9435 -0.1947 +vn 0.4911 0.7947 -0.3568 +vn 0.4089 0.6617 -0.6284 +vn -0.1024 0.9435 -0.3151 +vn -0.1876 0.7947 -0.5773 +vn -0.4713 0.6617 -0.5831 +vn -0.3313 0.9435 -0.0000 +vn -0.6071 0.7947 -0.0000 +vn -0.7002 0.6617 0.2680 +vn -0.1024 0.9435 0.3151 +vn -0.1876 0.7947 0.5773 +vn 0.0385 0.6617 0.7488 +vn 0.2680 0.9435 0.1947 +vn 0.4911 0.7947 0.3568 +vn 0.7240 0.6617 0.1947 +vn 0.8897 0.3304 -0.3151 +vn 0.7947 0.1876 -0.5773 +vn 0.5746 0.3304 -0.7488 +vn -0.0247 0.3304 -0.9435 +vn -0.3035 0.1876 -0.9342 +vn -0.5346 0.3304 -0.7779 +vn -0.9050 0.3304 -0.2680 +vn -0.9822 0.1876 -0.0000 +vn -0.9050 0.3304 0.2680 +vn -0.5346 0.3304 0.7779 +vn -0.3035 0.1876 0.9342 +vn -0.0247 0.3304 0.9435 +vn 0.5746 0.3304 0.7488 +vn 0.7947 0.1876 0.5773 +vn 0.8897 0.3304 0.3151 +vn 0.3066 0.1256 -0.9435 +vn 0.3035 -0.1876 -0.9342 +vn 0.0247 -0.3304 -0.9435 +vn -0.8026 0.1256 -0.5831 +vn -0.7947 -0.1876 -0.5773 +vn -0.8897 -0.3304 -0.3151 +vn -0.8026 0.1256 0.5831 +vn -0.7947 -0.1876 0.5773 +vn -0.5746 -0.3304 0.7488 +vn 0.3066 0.1256 0.9435 +vn 0.3035 -0.1876 0.9342 +vn 0.5346 -0.3304 0.7779 +vn 0.9921 0.1256 -0.0000 +vn 0.9822 -0.1876 -0.0000 +vn 0.9050 -0.3304 -0.2680 +vn 0.4713 -0.6617 -0.5831 +vn 0.1876 -0.7947 -0.5773 +vn -0.0385 -0.6617 -0.7488 +vn -0.4089 -0.6617 -0.6284 +vn -0.4911 -0.7947 -0.3568 +vn -0.7240 -0.6617 -0.1947 +vn -0.7240 -0.6617 0.1947 +vn -0.4911 -0.7947 0.3568 +vn -0.4089 -0.6617 0.6284 +vn 0.7002 -0.6617 -0.2680 +vn 0.6071 -0.7947 -0.0000 +vn 0.3313 -0.9435 -0.0000 +vn -0.0385 -0.6617 0.7488 +vn 0.1876 -0.7947 0.5773 +vn 0.4713 -0.6617 0.5831 +s 0 +f 1//1 14//1 13//1 +f 2//2 14//2 16//2 +f 1//3 13//3 18//3 +f 1//4 18//4 20//4 +f 1//5 20//5 17//5 +f 2//6 16//6 23//6 +f 3//7 15//7 25//7 +f 4//8 19//8 27//8 +f 5//9 21//9 29//9 +f 6//10 22//10 31//10 +f 2//11 23//11 26//11 +f 3//12 25//12 28//12 +f 4//13 27//13 30//13 +f 5//14 29//14 32//14 +f 6//15 31//15 24//15 +f 7//16 33//16 38//16 +f 8//17 34//17 40//17 +f 9//18 35//18 41//18 +f 10//19 36//19 42//19 +f 11//20 37//20 39//20 +f 39//21 42//21 12//21 +f 39//22 37//22 42//22 +f 37//23 10//23 42//23 +f 42//24 41//24 12//24 +f 42//25 36//25 41//25 +f 36//26 9//26 41//26 +f 41//27 40//27 12//27 +f 41//28 35//28 40//28 +f 35//29 8//29 40//29 +f 40//30 38//30 12//30 +f 40//31 34//31 38//31 +f 34//32 7//32 38//32 +f 38//33 39//33 12//33 +f 38//34 33//34 39//34 +f 33//35 11//35 39//35 +f 24//36 37//36 11//36 +f 24//37 31//37 37//37 +f 31//38 10//38 37//38 +f 32//39 36//39 10//39 +f 32//40 29//40 36//40 +f 29//41 9//41 36//41 +f 30//42 35//42 9//42 +f 30//43 27//43 35//43 +f 27//44 8//44 35//44 +f 28//45 34//45 8//45 +f 28//46 25//46 34//46 +f 25//47 7//47 34//47 +f 26//48 33//48 7//48 +f 26//49 23//49 33//49 +f 23//50 11//50 33//50 +f 31//51 32//51 10//51 +f 31//52 22//52 32//52 +f 22//53 5//53 32//53 +f 29//54 30//54 9//54 +f 29//55 21//55 30//55 +f 21//56 4//56 30//56 +f 27//57 28//57 8//57 +f 27//58 19//58 28//58 +f 19//59 3//59 28//59 +f 25//60 26//60 7//60 +f 25//61 15//61 26//61 +f 15//62 2//62 26//62 +f 23//63 24//63 11//63 +f 23//64 16//64 24//64 +f 16//65 6//65 24//65 +f 17//66 22//66 6//66 +f 17//67 20//67 22//67 +f 20//68 5//68 22//68 +f 20//69 21//69 5//69 +f 20//70 18//70 21//70 +f 18//71 4//71 21//71 +f 18//72 19//72 4//72 +f 18//73 13//73 19//73 +f 13//74 3//74 19//74 +f 16//75 17//75 6//75 +f 16//76 14//76 17//76 +f 14//77 1//77 17//77 +f 13//78 15//78 3//78 +f 13//79 14//79 15//79 +f 14//80 2//80 15//80 diff --git a/data/plane.obj b/data/plane.obj new file mode 100644 index 0000000..98e138e --- /dev/null +++ b/data/plane.obj @@ -0,0 +1,1245 @@ +# Blender 3.5.0 +# www.blender.org +o Grid +v -1.000000 0.000000 1.000000 +v -0.900000 0.000000 1.000000 +v -0.800000 0.000000 1.000000 +v -0.700000 0.000000 1.000000 +v -0.600000 0.000000 1.000000 +v -0.500000 0.000000 1.000000 +v -0.400000 0.000000 1.000000 +v -0.300000 0.000000 1.000000 +v -0.200000 0.000000 1.000000 +v -0.100000 0.000000 1.000000 +v 0.000000 0.000000 1.000000 +v 0.100000 0.000000 1.000000 +v 0.200000 0.000000 1.000000 +v 0.300000 0.000000 1.000000 +v 0.400000 0.000000 1.000000 +v 0.500000 0.000000 1.000000 +v 0.600000 0.000000 1.000000 +v 0.700000 0.000000 1.000000 +v 0.800000 0.000000 1.000000 +v 0.900000 0.000000 1.000000 +v 1.000000 0.000000 1.000000 +v -1.000000 0.000000 0.900000 +v -0.900000 0.000000 0.900000 +v -0.800000 0.000000 0.900000 +v -0.700000 0.000000 0.900000 +v -0.600000 0.000000 0.900000 +v -0.500000 0.000000 0.900000 +v -0.400000 0.000000 0.900000 +v -0.300000 0.000000 0.900000 +v -0.200000 0.000000 0.900000 +v -0.100000 0.000000 0.900000 +v 0.000000 0.000000 0.900000 +v 0.100000 0.000000 0.900000 +v 0.200000 0.000000 0.900000 +v 0.300000 0.000000 0.900000 +v 0.400000 0.000000 0.900000 +v 0.500000 0.000000 0.900000 +v 0.600000 0.000000 0.900000 +v 0.700000 0.000000 0.900000 +v 0.800000 0.000000 0.900000 +v 0.900000 0.000000 0.900000 +v 1.000000 0.000000 0.900000 +v -1.000000 0.000000 0.800000 +v -0.900000 0.000000 0.800000 +v -0.800000 0.000000 0.800000 +v -0.700000 0.000000 0.800000 +v -0.600000 0.000000 0.800000 +v -0.500000 0.000000 0.800000 +v -0.400000 0.000000 0.800000 +v -0.300000 0.000000 0.800000 +v -0.200000 0.000000 0.800000 +v -0.100000 0.000000 0.800000 +v 0.000000 0.000000 0.800000 +v 0.100000 0.000000 0.800000 +v 0.200000 0.000000 0.800000 +v 0.300000 0.000000 0.800000 +v 0.400000 0.000000 0.800000 +v 0.500000 0.000000 0.800000 +v 0.600000 0.000000 0.800000 +v 0.700000 0.000000 0.800000 +v 0.800000 0.000000 0.800000 +v 0.900000 0.000000 0.800000 +v 1.000000 0.000000 0.800000 +v -1.000000 0.000000 0.700000 +v -0.900000 0.000000 0.700000 +v -0.800000 0.000000 0.700000 +v -0.700000 0.000000 0.700000 +v -0.600000 0.000000 0.700000 +v -0.500000 0.000000 0.700000 +v -0.400000 0.000000 0.700000 +v -0.300000 0.000000 0.700000 +v -0.200000 0.000000 0.700000 +v -0.100000 0.000000 0.700000 +v 0.000000 0.000000 0.700000 +v 0.100000 0.000000 0.700000 +v 0.200000 0.000000 0.700000 +v 0.300000 0.000000 0.700000 +v 0.400000 0.000000 0.700000 +v 0.500000 0.000000 0.700000 +v 0.600000 0.000000 0.700000 +v 0.700000 0.000000 0.700000 +v 0.800000 0.000000 0.700000 +v 0.900000 0.000000 0.700000 +v 1.000000 0.000000 0.700000 +v -1.000000 0.000000 0.600000 +v -0.900000 0.000000 0.600000 +v -0.800000 0.000000 0.600000 +v -0.700000 0.000000 0.600000 +v -0.600000 0.000000 0.600000 +v -0.500000 0.000000 0.600000 +v -0.400000 0.000000 0.600000 +v -0.300000 0.000000 0.600000 +v -0.200000 0.000000 0.600000 +v -0.100000 0.000000 0.600000 +v 0.000000 0.000000 0.600000 +v 0.100000 0.000000 0.600000 +v 0.200000 0.000000 0.600000 +v 0.300000 0.000000 0.600000 +v 0.400000 0.000000 0.600000 +v 0.500000 0.000000 0.600000 +v 0.600000 0.000000 0.600000 +v 0.700000 0.000000 0.600000 +v 0.800000 0.000000 0.600000 +v 0.900000 0.000000 0.600000 +v 1.000000 0.000000 0.600000 +v -1.000000 0.000000 0.500000 +v -0.900000 0.000000 0.500000 +v -0.800000 0.000000 0.500000 +v -0.700000 0.000000 0.500000 +v -0.600000 0.000000 0.500000 +v -0.500000 0.000000 0.500000 +v -0.400000 0.000000 0.500000 +v -0.300000 0.000000 0.500000 +v -0.200000 0.000000 0.500000 +v -0.100000 0.000000 0.500000 +v 0.000000 0.000000 0.500000 +v 0.100000 0.000000 0.500000 +v 0.200000 0.000000 0.500000 +v 0.300000 0.000000 0.500000 +v 0.400000 0.000000 0.500000 +v 0.500000 0.000000 0.500000 +v 0.600000 0.000000 0.500000 +v 0.700000 0.000000 0.500000 +v 0.800000 0.000000 0.500000 +v 0.900000 0.000000 0.500000 +v 1.000000 0.000000 0.500000 +v -1.000000 0.000000 0.400000 +v -0.900000 0.000000 0.400000 +v -0.800000 0.000000 0.400000 +v -0.700000 0.000000 0.400000 +v -0.600000 0.000000 0.400000 +v -0.500000 0.000000 0.400000 +v -0.400000 0.000000 0.400000 +v -0.300000 0.000000 0.400000 +v -0.200000 0.000000 0.400000 +v -0.100000 0.000000 0.400000 +v 0.000000 0.000000 0.400000 +v 0.100000 0.000000 0.400000 +v 0.200000 0.000000 0.400000 +v 0.300000 0.000000 0.400000 +v 0.400000 0.000000 0.400000 +v 0.500000 0.000000 0.400000 +v 0.600000 0.000000 0.400000 +v 0.700000 0.000000 0.400000 +v 0.800000 0.000000 0.400000 +v 0.900000 0.000000 0.400000 +v 1.000000 0.000000 0.400000 +v -1.000000 0.000000 0.300000 +v -0.900000 0.000000 0.300000 +v -0.800000 0.000000 0.300000 +v -0.700000 0.000000 0.300000 +v -0.600000 0.000000 0.300000 +v -0.500000 0.000000 0.300000 +v -0.400000 0.000000 0.300000 +v -0.300000 0.000000 0.300000 +v -0.200000 0.000000 0.300000 +v -0.100000 0.000000 0.300000 +v 0.000000 0.000000 0.300000 +v 0.100000 0.000000 0.300000 +v 0.200000 0.000000 0.300000 +v 0.300000 0.000000 0.300000 +v 0.400000 0.000000 0.300000 +v 0.500000 0.000000 0.300000 +v 0.600000 0.000000 0.300000 +v 0.700000 0.000000 0.300000 +v 0.800000 0.000000 0.300000 +v 0.900000 0.000000 0.300000 +v 1.000000 0.000000 0.300000 +v -1.000000 0.000000 0.200000 +v -0.900000 0.000000 0.200000 +v -0.800000 0.000000 0.200000 +v -0.700000 0.000000 0.200000 +v -0.600000 0.000000 0.200000 +v -0.500000 0.000000 0.200000 +v -0.400000 0.000000 0.200000 +v -0.300000 0.000000 0.200000 +v -0.200000 0.000000 0.200000 +v -0.100000 0.000000 0.200000 +v 0.000000 0.000000 0.200000 +v 0.100000 0.000000 0.200000 +v 0.200000 0.000000 0.200000 +v 0.300000 0.000000 0.200000 +v 0.400000 0.000000 0.200000 +v 0.500000 0.000000 0.200000 +v 0.600000 0.000000 0.200000 +v 0.700000 0.000000 0.200000 +v 0.800000 0.000000 0.200000 +v 0.900000 0.000000 0.200000 +v 1.000000 0.000000 0.200000 +v -1.000000 0.000000 0.100000 +v -0.900000 0.000000 0.100000 +v -0.800000 0.000000 0.100000 +v -0.700000 0.000000 0.100000 +v -0.600000 0.000000 0.100000 +v -0.500000 0.000000 0.100000 +v -0.400000 0.000000 0.100000 +v -0.300000 0.000000 0.100000 +v -0.200000 0.000000 0.100000 +v -0.100000 0.000000 0.100000 +v 0.000000 0.000000 0.100000 +v 0.100000 0.000000 0.100000 +v 0.200000 0.000000 0.100000 +v 0.300000 0.000000 0.100000 +v 0.400000 0.000000 0.100000 +v 0.500000 0.000000 0.100000 +v 0.600000 0.000000 0.100000 +v 0.700000 0.000000 0.100000 +v 0.800000 0.000000 0.100000 +v 0.900000 0.000000 0.100000 +v 1.000000 0.000000 0.100000 +v -1.000000 0.000000 0.000000 +v -0.900000 0.000000 0.000000 +v -0.800000 0.000000 0.000000 +v -0.700000 0.000000 0.000000 +v -0.600000 0.000000 0.000000 +v -0.500000 0.000000 0.000000 +v -0.400000 0.000000 0.000000 +v -0.300000 0.000000 0.000000 +v -0.200000 0.000000 0.000000 +v -0.100000 0.000000 0.000000 +v 0.000000 0.000000 0.000000 +v 0.100000 0.000000 0.000000 +v 0.200000 0.000000 0.000000 +v 0.300000 0.000000 0.000000 +v 0.400000 0.000000 0.000000 +v 0.500000 0.000000 0.000000 +v 0.600000 0.000000 0.000000 +v 0.700000 0.000000 0.000000 +v 0.800000 0.000000 0.000000 +v 0.900000 0.000000 0.000000 +v 1.000000 0.000000 0.000000 +v -1.000000 0.000000 -0.100000 +v -0.900000 0.000000 -0.100000 +v -0.800000 0.000000 -0.100000 +v -0.700000 0.000000 -0.100000 +v -0.600000 0.000000 -0.100000 +v -0.500000 0.000000 -0.100000 +v -0.400000 0.000000 -0.100000 +v -0.300000 0.000000 -0.100000 +v -0.200000 0.000000 -0.100000 +v -0.100000 0.000000 -0.100000 +v 0.000000 0.000000 -0.100000 +v 0.100000 0.000000 -0.100000 +v 0.200000 0.000000 -0.100000 +v 0.300000 0.000000 -0.100000 +v 0.400000 0.000000 -0.100000 +v 0.500000 0.000000 -0.100000 +v 0.600000 0.000000 -0.100000 +v 0.700000 0.000000 -0.100000 +v 0.800000 0.000000 -0.100000 +v 0.900000 0.000000 -0.100000 +v 1.000000 0.000000 -0.100000 +v -1.000000 0.000000 -0.200000 +v -0.900000 0.000000 -0.200000 +v -0.800000 0.000000 -0.200000 +v -0.700000 0.000000 -0.200000 +v -0.600000 0.000000 -0.200000 +v -0.500000 0.000000 -0.200000 +v -0.400000 0.000000 -0.200000 +v -0.300000 0.000000 -0.200000 +v -0.200000 0.000000 -0.200000 +v -0.100000 0.000000 -0.200000 +v 0.000000 0.000000 -0.200000 +v 0.100000 0.000000 -0.200000 +v 0.200000 0.000000 -0.200000 +v 0.300000 0.000000 -0.200000 +v 0.400000 0.000000 -0.200000 +v 0.500000 0.000000 -0.200000 +v 0.600000 0.000000 -0.200000 +v 0.700000 0.000000 -0.200000 +v 0.800000 0.000000 -0.200000 +v 0.900000 0.000000 -0.200000 +v 1.000000 0.000000 -0.200000 +v -1.000000 0.000000 -0.300000 +v -0.900000 0.000000 -0.300000 +v -0.800000 0.000000 -0.300000 +v -0.700000 0.000000 -0.300000 +v -0.600000 0.000000 -0.300000 +v -0.500000 0.000000 -0.300000 +v -0.400000 0.000000 -0.300000 +v -0.300000 0.000000 -0.300000 +v -0.200000 0.000000 -0.300000 +v -0.100000 0.000000 -0.300000 +v 0.000000 0.000000 -0.300000 +v 0.100000 0.000000 -0.300000 +v 0.200000 0.000000 -0.300000 +v 0.300000 0.000000 -0.300000 +v 0.400000 0.000000 -0.300000 +v 0.500000 0.000000 -0.300000 +v 0.600000 0.000000 -0.300000 +v 0.700000 0.000000 -0.300000 +v 0.800000 0.000000 -0.300000 +v 0.900000 0.000000 -0.300000 +v 1.000000 0.000000 -0.300000 +v -1.000000 0.000000 -0.400000 +v -0.900000 0.000000 -0.400000 +v -0.800000 0.000000 -0.400000 +v -0.700000 0.000000 -0.400000 +v -0.600000 0.000000 -0.400000 +v -0.500000 0.000000 -0.400000 +v -0.400000 0.000000 -0.400000 +v -0.300000 0.000000 -0.400000 +v -0.200000 0.000000 -0.400000 +v -0.100000 0.000000 -0.400000 +v 0.000000 0.000000 -0.400000 +v 0.100000 0.000000 -0.400000 +v 0.200000 0.000000 -0.400000 +v 0.300000 0.000000 -0.400000 +v 0.400000 0.000000 -0.400000 +v 0.500000 0.000000 -0.400000 +v 0.600000 0.000000 -0.400000 +v 0.700000 0.000000 -0.400000 +v 0.800000 0.000000 -0.400000 +v 0.900000 0.000000 -0.400000 +v 1.000000 0.000000 -0.400000 +v -1.000000 0.000000 -0.500000 +v -0.900000 0.000000 -0.500000 +v -0.800000 0.000000 -0.500000 +v -0.700000 0.000000 -0.500000 +v -0.600000 0.000000 -0.500000 +v -0.500000 0.000000 -0.500000 +v -0.400000 0.000000 -0.500000 +v -0.300000 0.000000 -0.500000 +v -0.200000 0.000000 -0.500000 +v -0.100000 0.000000 -0.500000 +v 0.000000 0.000000 -0.500000 +v 0.100000 0.000000 -0.500000 +v 0.200000 0.000000 -0.500000 +v 0.300000 0.000000 -0.500000 +v 0.400000 0.000000 -0.500000 +v 0.500000 0.000000 -0.500000 +v 0.600000 0.000000 -0.500000 +v 0.700000 0.000000 -0.500000 +v 0.800000 0.000000 -0.500000 +v 0.900000 0.000000 -0.500000 +v 1.000000 0.000000 -0.500000 +v -1.000000 0.000000 -0.600000 +v -0.900000 0.000000 -0.600000 +v -0.800000 0.000000 -0.600000 +v -0.700000 0.000000 -0.600000 +v -0.600000 0.000000 -0.600000 +v -0.500000 0.000000 -0.600000 +v -0.400000 0.000000 -0.600000 +v -0.300000 0.000000 -0.600000 +v -0.200000 0.000000 -0.600000 +v -0.100000 0.000000 -0.600000 +v 0.000000 0.000000 -0.600000 +v 0.100000 0.000000 -0.600000 +v 0.200000 0.000000 -0.600000 +v 0.300000 0.000000 -0.600000 +v 0.400000 0.000000 -0.600000 +v 0.500000 0.000000 -0.600000 +v 0.600000 0.000000 -0.600000 +v 0.700000 0.000000 -0.600000 +v 0.800000 0.000000 -0.600000 +v 0.900000 0.000000 -0.600000 +v 1.000000 0.000000 -0.600000 +v -1.000000 0.000000 -0.700000 +v -0.900000 0.000000 -0.700000 +v -0.800000 0.000000 -0.700000 +v -0.700000 0.000000 -0.700000 +v -0.600000 0.000000 -0.700000 +v -0.500000 0.000000 -0.700000 +v -0.400000 0.000000 -0.700000 +v -0.300000 0.000000 -0.700000 +v -0.200000 0.000000 -0.700000 +v -0.100000 0.000000 -0.700000 +v 0.000000 0.000000 -0.700000 +v 0.100000 0.000000 -0.700000 +v 0.200000 0.000000 -0.700000 +v 0.300000 0.000000 -0.700000 +v 0.400000 0.000000 -0.700000 +v 0.500000 0.000000 -0.700000 +v 0.600000 0.000000 -0.700000 +v 0.700000 0.000000 -0.700000 +v 0.800000 0.000000 -0.700000 +v 0.900000 0.000000 -0.700000 +v 1.000000 0.000000 -0.700000 +v -1.000000 0.000000 -0.800000 +v -0.900000 0.000000 -0.800000 +v -0.800000 0.000000 -0.800000 +v -0.700000 0.000000 -0.800000 +v -0.600000 0.000000 -0.800000 +v -0.500000 0.000000 -0.800000 +v -0.400000 0.000000 -0.800000 +v -0.300000 0.000000 -0.800000 +v -0.200000 0.000000 -0.800000 +v -0.100000 0.000000 -0.800000 +v 0.000000 0.000000 -0.800000 +v 0.100000 0.000000 -0.800000 +v 0.200000 0.000000 -0.800000 +v 0.300000 0.000000 -0.800000 +v 0.400000 0.000000 -0.800000 +v 0.500000 0.000000 -0.800000 +v 0.600000 0.000000 -0.800000 +v 0.700000 0.000000 -0.800000 +v 0.800000 0.000000 -0.800000 +v 0.900000 0.000000 -0.800000 +v 1.000000 0.000000 -0.800000 +v -1.000000 0.000000 -0.900000 +v -0.900000 0.000000 -0.900000 +v -0.800000 0.000000 -0.900000 +v -0.700000 0.000000 -0.900000 +v -0.600000 0.000000 -0.900000 +v -0.500000 0.000000 -0.900000 +v -0.400000 0.000000 -0.900000 +v -0.300000 0.000000 -0.900000 +v -0.200000 0.000000 -0.900000 +v -0.100000 0.000000 -0.900000 +v 0.000000 0.000000 -0.900000 +v 0.100000 0.000000 -0.900000 +v 0.200000 0.000000 -0.900000 +v 0.300000 0.000000 -0.900000 +v 0.400000 0.000000 -0.900000 +v 0.500000 0.000000 -0.900000 +v 0.600000 0.000000 -0.900000 +v 0.700000 0.000000 -0.900000 +v 0.800000 0.000000 -0.900000 +v 0.900000 0.000000 -0.900000 +v 1.000000 0.000000 -0.900000 +v -1.000000 0.000000 -1.000000 +v -0.900000 0.000000 -1.000000 +v -0.800000 0.000000 -1.000000 +v -0.700000 0.000000 -1.000000 +v -0.600000 0.000000 -1.000000 +v -0.500000 0.000000 -1.000000 +v -0.400000 0.000000 -1.000000 +v -0.300000 0.000000 -1.000000 +v -0.200000 0.000000 -1.000000 +v -0.100000 0.000000 -1.000000 +v 0.000000 0.000000 -1.000000 +v 0.100000 0.000000 -1.000000 +v 0.200000 0.000000 -1.000000 +v 0.300000 0.000000 -1.000000 +v 0.400000 0.000000 -1.000000 +v 0.500000 0.000000 -1.000000 +v 0.600000 0.000000 -1.000000 +v 0.700000 0.000000 -1.000000 +v 0.800000 0.000000 -1.000000 +v 0.900000 0.000000 -1.000000 +v 1.000000 0.000000 -1.000000 +s 0 +f 2 22 1 +f 3 23 2 +f 4 24 3 +f 5 25 4 +f 6 26 5 +f 7 27 6 +f 8 28 7 +f 9 29 8 +f 10 30 9 +f 11 31 10 +f 12 32 11 +f 13 33 12 +f 14 34 13 +f 15 35 14 +f 16 36 15 +f 17 37 16 +f 18 38 17 +f 19 39 18 +f 20 40 19 +f 21 41 20 +f 23 43 22 +f 24 44 23 +f 25 45 24 +f 26 46 25 +f 27 47 26 +f 28 48 27 +f 29 49 28 +f 30 50 29 +f 31 51 30 +f 32 52 31 +f 33 53 32 +f 34 54 33 +f 35 55 34 +f 36 56 35 +f 37 57 36 +f 38 58 37 +f 39 59 38 +f 40 60 39 +f 41 61 40 +f 42 62 41 +f 44 64 43 +f 45 65 44 +f 46 66 45 +f 47 67 46 +f 48 68 47 +f 49 69 48 +f 50 70 49 +f 51 71 50 +f 52 72 51 +f 53 73 52 +f 54 74 53 +f 55 75 54 +f 56 76 55 +f 57 77 56 +f 58 78 57 +f 59 79 58 +f 60 80 59 +f 61 81 60 +f 62 82 61 +f 63 83 62 +f 65 85 64 +f 66 86 65 +f 67 87 66 +f 68 88 67 +f 69 89 68 +f 70 90 69 +f 71 91 70 +f 72 92 71 +f 73 93 72 +f 74 94 73 +f 75 95 74 +f 76 96 75 +f 77 97 76 +f 78 98 77 +f 79 99 78 +f 80 100 79 +f 81 101 80 +f 82 102 81 +f 83 103 82 +f 84 104 83 +f 86 106 85 +f 87 107 86 +f 88 108 87 +f 89 109 88 +f 90 110 89 +f 91 111 90 +f 92 112 91 +f 93 113 92 +f 94 114 93 +f 95 115 94 +f 96 116 95 +f 97 117 96 +f 98 118 97 +f 99 119 98 +f 100 120 99 +f 101 121 100 +f 102 122 101 +f 103 123 102 +f 104 124 103 +f 105 125 104 +f 107 127 106 +f 108 128 107 +f 109 129 108 +f 110 130 109 +f 111 131 110 +f 112 132 111 +f 113 133 112 +f 114 134 113 +f 115 135 114 +f 116 136 115 +f 117 137 116 +f 118 138 117 +f 119 139 118 +f 120 140 119 +f 121 141 120 +f 122 142 121 +f 123 143 122 +f 124 144 123 +f 125 145 124 +f 126 146 125 +f 128 148 127 +f 129 149 128 +f 130 150 129 +f 131 151 130 +f 132 152 131 +f 133 153 132 +f 134 154 133 +f 135 155 134 +f 136 156 135 +f 137 157 136 +f 138 158 137 +f 139 159 138 +f 140 160 139 +f 141 161 140 +f 142 162 141 +f 143 163 142 +f 144 164 143 +f 145 165 144 +f 146 166 145 +f 147 167 146 +f 149 169 148 +f 150 170 149 +f 151 171 150 +f 152 172 151 +f 153 173 152 +f 154 174 153 +f 155 175 154 +f 156 176 155 +f 157 177 156 +f 158 178 157 +f 159 179 158 +f 160 180 159 +f 161 181 160 +f 162 182 161 +f 163 183 162 +f 164 184 163 +f 165 185 164 +f 166 186 165 +f 167 187 166 +f 168 188 167 +f 170 190 169 +f 171 191 170 +f 172 192 171 +f 173 193 172 +f 174 194 173 +f 175 195 174 +f 176 196 175 +f 177 197 176 +f 178 198 177 +f 179 199 178 +f 180 200 179 +f 181 201 180 +f 182 202 181 +f 183 203 182 +f 184 204 183 +f 185 205 184 +f 186 206 185 +f 187 207 186 +f 188 208 187 +f 189 209 188 +f 191 211 190 +f 192 212 191 +f 193 213 192 +f 194 214 193 +f 195 215 194 +f 196 216 195 +f 197 217 196 +f 198 218 197 +f 199 219 198 +f 200 220 199 +f 201 221 200 +f 202 222 201 +f 203 223 202 +f 204 224 203 +f 205 225 204 +f 206 226 205 +f 207 227 206 +f 208 228 207 +f 209 229 208 +f 210 230 209 +f 212 232 211 +f 213 233 212 +f 214 234 213 +f 215 235 214 +f 216 236 215 +f 217 237 216 +f 218 238 217 +f 219 239 218 +f 220 240 219 +f 221 241 220 +f 222 242 221 +f 223 243 222 +f 224 244 223 +f 225 245 224 +f 226 246 225 +f 227 247 226 +f 228 248 227 +f 229 249 228 +f 230 250 229 +f 231 251 230 +f 233 253 232 +f 234 254 233 +f 235 255 234 +f 236 256 235 +f 237 257 236 +f 238 258 237 +f 239 259 238 +f 240 260 239 +f 241 261 240 +f 242 262 241 +f 243 263 242 +f 244 264 243 +f 245 265 244 +f 246 266 245 +f 247 267 246 +f 248 268 247 +f 249 269 248 +f 250 270 249 +f 251 271 250 +f 252 272 251 +f 254 274 253 +f 255 275 254 +f 256 276 255 +f 257 277 256 +f 258 278 257 +f 259 279 258 +f 260 280 259 +f 261 281 260 +f 262 282 261 +f 263 283 262 +f 264 284 263 +f 265 285 264 +f 266 286 265 +f 267 287 266 +f 268 288 267 +f 269 289 268 +f 270 290 269 +f 271 291 270 +f 272 292 271 +f 273 293 272 +f 275 295 274 +f 276 296 275 +f 277 297 276 +f 278 298 277 +f 279 299 278 +f 280 300 279 +f 281 301 280 +f 282 302 281 +f 283 303 282 +f 284 304 283 +f 285 305 284 +f 286 306 285 +f 287 307 286 +f 288 308 287 +f 289 309 288 +f 290 310 289 +f 291 311 290 +f 292 312 291 +f 293 313 292 +f 294 314 293 +f 296 316 295 +f 297 317 296 +f 298 318 297 +f 299 319 298 +f 300 320 299 +f 301 321 300 +f 302 322 301 +f 303 323 302 +f 304 324 303 +f 305 325 304 +f 306 326 305 +f 307 327 306 +f 308 328 307 +f 309 329 308 +f 310 330 309 +f 311 331 310 +f 312 332 311 +f 313 333 312 +f 314 334 313 +f 315 335 314 +f 317 337 316 +f 318 338 317 +f 319 339 318 +f 320 340 319 +f 321 341 320 +f 322 342 321 +f 323 343 322 +f 324 344 323 +f 325 345 324 +f 326 346 325 +f 327 347 326 +f 328 348 327 +f 329 349 328 +f 330 350 329 +f 331 351 330 +f 332 352 331 +f 333 353 332 +f 334 354 333 +f 335 355 334 +f 336 356 335 +f 338 358 337 +f 339 359 338 +f 340 360 339 +f 341 361 340 +f 342 362 341 +f 343 363 342 +f 344 364 343 +f 345 365 344 +f 346 366 345 +f 347 367 346 +f 348 368 347 +f 349 369 348 +f 350 370 349 +f 351 371 350 +f 352 372 351 +f 353 373 352 +f 354 374 353 +f 355 375 354 +f 356 376 355 +f 357 377 356 +f 359 379 358 +f 360 380 359 +f 361 381 360 +f 362 382 361 +f 363 383 362 +f 364 384 363 +f 365 385 364 +f 366 386 365 +f 367 387 366 +f 368 388 367 +f 369 389 368 +f 370 390 369 +f 371 391 370 +f 372 392 371 +f 373 393 372 +f 374 394 373 +f 375 395 374 +f 376 396 375 +f 377 397 376 +f 378 398 377 +f 380 400 379 +f 381 401 380 +f 382 402 381 +f 383 403 382 +f 384 404 383 +f 385 405 384 +f 386 406 385 +f 387 407 386 +f 388 408 387 +f 389 409 388 +f 390 410 389 +f 391 411 390 +f 392 412 391 +f 393 413 392 +f 394 414 393 +f 395 415 394 +f 396 416 395 +f 397 417 396 +f 398 418 397 +f 399 419 398 +f 401 421 400 +f 402 422 401 +f 403 423 402 +f 404 424 403 +f 405 425 404 +f 406 426 405 +f 407 427 406 +f 408 428 407 +f 409 429 408 +f 410 430 409 +f 411 431 410 +f 412 432 411 +f 413 433 412 +f 414 434 413 +f 415 435 414 +f 416 436 415 +f 417 437 416 +f 418 438 417 +f 419 439 418 +f 420 440 419 +f 2 23 22 +f 3 24 23 +f 4 25 24 +f 5 26 25 +f 6 27 26 +f 7 28 27 +f 8 29 28 +f 9 30 29 +f 10 31 30 +f 11 32 31 +f 12 33 32 +f 13 34 33 +f 14 35 34 +f 15 36 35 +f 16 37 36 +f 17 38 37 +f 18 39 38 +f 19 40 39 +f 20 41 40 +f 21 42 41 +f 23 44 43 +f 24 45 44 +f 25 46 45 +f 26 47 46 +f 27 48 47 +f 28 49 48 +f 29 50 49 +f 30 51 50 +f 31 52 51 +f 32 53 52 +f 33 54 53 +f 34 55 54 +f 35 56 55 +f 36 57 56 +f 37 58 57 +f 38 59 58 +f 39 60 59 +f 40 61 60 +f 41 62 61 +f 42 63 62 +f 44 65 64 +f 45 66 65 +f 46 67 66 +f 47 68 67 +f 48 69 68 +f 49 70 69 +f 50 71 70 +f 51 72 71 +f 52 73 72 +f 53 74 73 +f 54 75 74 +f 55 76 75 +f 56 77 76 +f 57 78 77 +f 58 79 78 +f 59 80 79 +f 60 81 80 +f 61 82 81 +f 62 83 82 +f 63 84 83 +f 65 86 85 +f 66 87 86 +f 67 88 87 +f 68 89 88 +f 69 90 89 +f 70 91 90 +f 71 92 91 +f 72 93 92 +f 73 94 93 +f 74 95 94 +f 75 96 95 +f 76 97 96 +f 77 98 97 +f 78 99 98 +f 79 100 99 +f 80 101 100 +f 81 102 101 +f 82 103 102 +f 83 104 103 +f 84 105 104 +f 86 107 106 +f 87 108 107 +f 88 109 108 +f 89 110 109 +f 90 111 110 +f 91 112 111 +f 92 113 112 +f 93 114 113 +f 94 115 114 +f 95 116 115 +f 96 117 116 +f 97 118 117 +f 98 119 118 +f 99 120 119 +f 100 121 120 +f 101 122 121 +f 102 123 122 +f 103 124 123 +f 104 125 124 +f 105 126 125 +f 107 128 127 +f 108 129 128 +f 109 130 129 +f 110 131 130 +f 111 132 131 +f 112 133 132 +f 113 134 133 +f 114 135 134 +f 115 136 135 +f 116 137 136 +f 117 138 137 +f 118 139 138 +f 119 140 139 +f 120 141 140 +f 121 142 141 +f 122 143 142 +f 123 144 143 +f 124 145 144 +f 125 146 145 +f 126 147 146 +f 128 149 148 +f 129 150 149 +f 130 151 150 +f 131 152 151 +f 132 153 152 +f 133 154 153 +f 134 155 154 +f 135 156 155 +f 136 157 156 +f 137 158 157 +f 138 159 158 +f 139 160 159 +f 140 161 160 +f 141 162 161 +f 142 163 162 +f 143 164 163 +f 144 165 164 +f 145 166 165 +f 146 167 166 +f 147 168 167 +f 149 170 169 +f 150 171 170 +f 151 172 171 +f 152 173 172 +f 153 174 173 +f 154 175 174 +f 155 176 175 +f 156 177 176 +f 157 178 177 +f 158 179 178 +f 159 180 179 +f 160 181 180 +f 161 182 181 +f 162 183 182 +f 163 184 183 +f 164 185 184 +f 165 186 185 +f 166 187 186 +f 167 188 187 +f 168 189 188 +f 170 191 190 +f 171 192 191 +f 172 193 192 +f 173 194 193 +f 174 195 194 +f 175 196 195 +f 176 197 196 +f 177 198 197 +f 178 199 198 +f 179 200 199 +f 180 201 200 +f 181 202 201 +f 182 203 202 +f 183 204 203 +f 184 205 204 +f 185 206 205 +f 186 207 206 +f 187 208 207 +f 188 209 208 +f 189 210 209 +f 191 212 211 +f 192 213 212 +f 193 214 213 +f 194 215 214 +f 195 216 215 +f 196 217 216 +f 197 218 217 +f 198 219 218 +f 199 220 219 +f 200 221 220 +f 201 222 221 +f 202 223 222 +f 203 224 223 +f 204 225 224 +f 205 226 225 +f 206 227 226 +f 207 228 227 +f 208 229 228 +f 209 230 229 +f 210 231 230 +f 212 233 232 +f 213 234 233 +f 214 235 234 +f 215 236 235 +f 216 237 236 +f 217 238 237 +f 218 239 238 +f 219 240 239 +f 220 241 240 +f 221 242 241 +f 222 243 242 +f 223 244 243 +f 224 245 244 +f 225 246 245 +f 226 247 246 +f 227 248 247 +f 228 249 248 +f 229 250 249 +f 230 251 250 +f 231 252 251 +f 233 254 253 +f 234 255 254 +f 235 256 255 +f 236 257 256 +f 237 258 257 +f 238 259 258 +f 239 260 259 +f 240 261 260 +f 241 262 261 +f 242 263 262 +f 243 264 263 +f 244 265 264 +f 245 266 265 +f 246 267 266 +f 247 268 267 +f 248 269 268 +f 249 270 269 +f 250 271 270 +f 251 272 271 +f 252 273 272 +f 254 275 274 +f 255 276 275 +f 256 277 276 +f 257 278 277 +f 258 279 278 +f 259 280 279 +f 260 281 280 +f 261 282 281 +f 262 283 282 +f 263 284 283 +f 264 285 284 +f 265 286 285 +f 266 287 286 +f 267 288 287 +f 268 289 288 +f 269 290 289 +f 270 291 290 +f 271 292 291 +f 272 293 292 +f 273 294 293 +f 275 296 295 +f 276 297 296 +f 277 298 297 +f 278 299 298 +f 279 300 299 +f 280 301 300 +f 281 302 301 +f 282 303 302 +f 283 304 303 +f 284 305 304 +f 285 306 305 +f 286 307 306 +f 287 308 307 +f 288 309 308 +f 289 310 309 +f 290 311 310 +f 291 312 311 +f 292 313 312 +f 293 314 313 +f 294 315 314 +f 296 317 316 +f 297 318 317 +f 298 319 318 +f 299 320 319 +f 300 321 320 +f 301 322 321 +f 302 323 322 +f 303 324 323 +f 304 325 324 +f 305 326 325 +f 306 327 326 +f 307 328 327 +f 308 329 328 +f 309 330 329 +f 310 331 330 +f 311 332 331 +f 312 333 332 +f 313 334 333 +f 314 335 334 +f 315 336 335 +f 317 338 337 +f 318 339 338 +f 319 340 339 +f 320 341 340 +f 321 342 341 +f 322 343 342 +f 323 344 343 +f 324 345 344 +f 325 346 345 +f 326 347 346 +f 327 348 347 +f 328 349 348 +f 329 350 349 +f 330 351 350 +f 331 352 351 +f 332 353 352 +f 333 354 353 +f 334 355 354 +f 335 356 355 +f 336 357 356 +f 338 359 358 +f 339 360 359 +f 340 361 360 +f 341 362 361 +f 342 363 362 +f 343 364 363 +f 344 365 364 +f 345 366 365 +f 346 367 366 +f 347 368 367 +f 348 369 368 +f 349 370 369 +f 350 371 370 +f 351 372 371 +f 352 373 372 +f 353 374 373 +f 354 375 374 +f 355 376 375 +f 356 377 376 +f 357 378 377 +f 359 380 379 +f 360 381 380 +f 361 382 381 +f 362 383 382 +f 363 384 383 +f 364 385 384 +f 365 386 385 +f 366 387 386 +f 367 388 387 +f 368 389 388 +f 369 390 389 +f 370 391 390 +f 371 392 391 +f 372 393 392 +f 373 394 393 +f 374 395 394 +f 375 396 395 +f 376 397 396 +f 377 398 397 +f 378 399 398 +f 380 401 400 +f 381 402 401 +f 382 403 402 +f 383 404 403 +f 384 405 404 +f 385 406 405 +f 386 407 406 +f 387 408 407 +f 388 409 408 +f 389 410 409 +f 390 411 410 +f 391 412 411 +f 392 413 412 +f 393 414 413 +f 394 415 414 +f 395 416 415 +f 396 417 416 +f 397 418 417 +f 398 419 418 +f 399 420 419 +f 401 422 421 +f 402 423 422 +f 403 424 423 +f 404 425 424 +f 405 426 425 +f 406 427 426 +f 407 428 427 +f 408 429 428 +f 409 430 429 +f 410 431 430 +f 411 432 431 +f 412 433 432 +f 413 434 433 +f 414 435 434 +f 415 436 435 +f 416 437 436 +f 417 438 437 +f 418 439 438 +f 419 440 439 +f 420 441 440 diff --git a/splashsurf_lib/src/halfedge_mesh.rs b/splashsurf_lib/src/halfedge_mesh.rs new file mode 100644 index 0000000..74bcde4 --- /dev/null +++ b/splashsurf_lib/src/halfedge_mesh.rs @@ -0,0 +1,586 @@ +//! Basic implementation of a half-edge based triangle mesh +//! +//! See [`HalfEdgeTriMesh`] for more information. + +use crate::mesh::{Mesh3d, TriMesh3d, TriMesh3dExt}; +use crate::{profile, Real, SetType}; +use nalgebra::Vector3; +use rayon::prelude::*; +use thiserror::Error as ThisError; + +impl TriMesh3dExt for HalfEdgeTriMesh { + fn tri_vertices(&self) -> &[Vector3] { + &self.vertices + } +} + +/// A half-edge in a [`HalfEdgeTriMesh`] +#[derive(Copy, Clone, Debug, Default)] +pub struct HalfEdge { + /// Unique global index of this half-edge in the mesh + pub idx: usize, + /// Vertex this half-edge points to + pub to: usize, + /// Enclosed face of this half-edge loop (or `None` if boundary) + pub face: Option, + /// The next half-edge along the half-edge loop (or `None` if boundary) + pub next: Option, + /// Index of the half-edge going into the opposite direction + pub opposite: usize, +} + +impl HalfEdge { + /// Returns whether the given half-edge is a boundary edge + pub fn is_boundary(&self) -> bool { + self.face.is_none() + } +} + +/// A half-edge based triangle mesh data structure +/// +/// The main purpose of this data structure is to provide methods to perform consistent collapses of +/// half-edges for decimation procedures. +/// +/// As [`splashsurf_lib`](crate) is focused on closed meshes, handling of holes is not specifically tested. +/// In particular, it is not directly possible to walk along a mesh boundary using the half-edges of +/// this implementation. +/// +/// A [`HalfEdgeTriMesh`] can be easily constructed from a [`TriMesh3d`] using a [`From`](HalfEdgeTriMesh::from::) implementation. +/// +/// Note that affected vertex/face/half-edge indices become "invalid" after half-edge collapse is performed. +/// The corresponding data still exist (i.e. they can be retrieved from the mesh) but following these +/// indices amounts to following outdated connectivity. +/// Therefore, it should be checked if an index was marked as removed after a collapse using the +/// [`is_valid_vertex`](HalfEdgeTriMesh::is_valid_vertex)/[`is_valid_triangle`](HalfEdgeTriMesh::is_valid_triangle)/[`is_valid_half_edge`](HalfEdgeTriMesh::is_valid_half_edge) +/// methods. +#[derive(Clone, Debug, Default)] +pub struct HalfEdgeTriMesh { + /// All vertices in the mesh + pub vertices: Vec>, + /// All triangles in the mesh + pub triangles: Vec<[usize; 3]>, + /// All half-edges in the mesh + pub half_edges: Vec, + /// Connectivity map of all vertices to their connected neighbors + vertex_half_edge_map: Vec>, + /// Set of all vertices marked for removal + removed_vertices: SetType, + /// Set of all triangles marked for removal + removed_triangles: SetType, + /// Set of all half edges marked for removal + removed_half_edges: SetType, +} + +/// Error indicating why a specific half-edge collapse is illegal +#[derive(Copy, Clone, Debug, Eq, PartialEq, ThisError)] +pub enum IllegalHalfEdgeCollapse { + /// Trying to collapse an edge with boundary vertices at both ends + #[error("trying to collapse an edge with boundary vertices at both ends")] + BoundaryCollapse, + /// Trying to collapse an edge with vertices that share incident vertices other than the vertices directly opposite to the edge + #[error("trying to collapse an edge with vertices that share incident vertices other than the vertices directly opposite to the edge")] + IntersectionOfOneRing, + /// Trying to collapse an edge without faces + #[error("trying to collapse an edge without faces")] + FacelessEdge, +} + +impl HalfEdgeTriMesh { + /// Converts this mesh into a simple triangle mesh and a vertex-vertex connectivity map + pub fn into_parts(mut self, keep_vertices: bool) -> (TriMesh3d, Vec>) { + Self::compute_vertex_vertex_connectivity(&mut self.vertex_half_edge_map, &self.half_edges); + self.garbage_collection_for_trimesh(keep_vertices); + let mesh = TriMesh3d { + vertices: self.vertices, + triangles: self.triangles, + }; + (mesh, self.vertex_half_edge_map) + } + + /// Returns the valence of a vertex (size of its one-ring) + pub fn vertex_one_ring_len(&self, vertex: usize) -> usize { + self.vertex_half_edge_map[vertex].len() + } + + /// Returns the index of the `i`-th vertex from the one-ring of the given vertex + pub fn vertex_one_ring_ith(&self, vertex: usize, i: usize) -> usize { + self.half_edges[self.vertex_half_edge_map[vertex][i]].to + } + + /// Iterator over the one-ring vertex neighbors of the given vertex + pub fn vertex_one_ring<'a>(&'a self, vertex: usize) -> impl Iterator + 'a { + self.vertex_half_edge_map[vertex] + .iter() + .copied() + .map(|he_i| self.half_edges[he_i].to) + } + + /// Iterator over the outgoing half-edges of the given vertex + pub fn outgoing_half_edges<'a>(&'a self, vertex: usize) -> impl Iterator + 'a { + self.vertex_half_edge_map[vertex] + .iter() + .copied() + .map(|he_i| self.half_edges[he_i].clone()) + } + + /// Iterator over all incident faces of the given vertex + pub fn incident_faces<'a>(&'a self, vertex: usize) -> impl Iterator + 'a { + self.outgoing_half_edges(vertex).filter_map(|he| he.face) + } + + /// Returns the half-edge between the "from" and "to" vertex if it exists in the mesh + pub fn half_edge(&self, from: usize, to: usize) -> Option { + let from_edges = self + .vertex_half_edge_map + .get(from) + .expect("vertex must be part of the mesh"); + for &he_idx in from_edges { + let he = &self.half_edges[he_idx]; + if he.to == to { + return Some(he.clone()); + } + } + + None + } + + /// Returns whether the given half-edge or its opposite half-edge is a boundary edge + pub fn is_boundary_edge(&self, half_edge: HalfEdge) -> bool { + return half_edge.is_boundary() || self.opposite(half_edge).is_boundary(); + } + + /// Returns whether the given vertex is a boundary vertex + pub fn is_boundary_vertex(&self, vert_idx: usize) -> bool { + let hes = self + .vertex_half_edge_map + .get(vert_idx) + .expect("vertex must be part of the mesh"); + hes.iter() + .copied() + .any(|he_idx| self.half_edges[he_idx].is_boundary()) + } + + /// Returns whether the given triangle is valid (i.e. not marked as removed) + pub fn is_valid_triangle(&self, triangle_idx: usize) -> bool { + !self.removed_triangles.contains(&triangle_idx) + } + + /// Returns whether the given vertex is valid (i.e. not marked as removed) + pub fn is_valid_vertex(&self, vertex_idx: usize) -> bool { + !self.removed_vertices.contains(&vertex_idx) + } + + /// Returns whether the given vertex is valid (i.e. not marked as removed) + pub fn is_valid_half_edge(&self, half_edge_idx: usize) -> bool { + !self.removed_half_edges.contains(&half_edge_idx) + } + + /// Returns the next half-edge in the loop of the given half-edge, panics if there is none + pub fn next(&self, half_edge: HalfEdge) -> HalfEdge { + self.half_edges[half_edge + .next + .expect("half edge must have a next reference")] + } + + /// Returns the next half-edge in the loop of the given half-edge if it exists + pub fn try_next(&self, half_edge: HalfEdge) -> Option { + half_edge.next.map(|n| self.half_edges[n]) + } + + /// Returns the opposite half-edge of the given half-edge + pub fn opposite(&self, half_edge: HalfEdge) -> HalfEdge { + self.half_edges[half_edge.opposite] + } + + /// Returns a mutable reference to the opposite half-edge of the given half-edge + pub fn opposite_mut(&mut self, half_edge: usize) -> &mut HalfEdge { + let opp_idx = self.half_edges[half_edge].opposite; + &mut self.half_edges[opp_idx] + } + + /// Checks if the collapse of the given half-edge is topologically legal + pub fn is_collapse_ok(&self, half_edge: HalfEdge) -> Result<(), IllegalHalfEdgeCollapse> { + // Based on PMP library: + // https://github.com/pmp-library/pmp-library/blob/86099e4e274c310d23e8c46c4829f881242814d3/src/pmp/SurfaceMesh.cpp#L755 + + let v0v1 = half_edge; + let v1v0 = self.opposite(v0v1); + + let v0 = v1v0.to; // From vertex + let v1 = v0v1.to; // To vertex + + // Checks if edges to opposite vertex of half-edge are boundary edges and returns opposite vertex + let check_opposite_vertex = + |he: HalfEdge| -> Result, IllegalHalfEdgeCollapse> { + if !he.is_boundary() { + let h1 = self.next(he); + let h2 = self.next(h1); + + if self.opposite(h1).is_boundary() && self.opposite(h2).is_boundary() { + return Err(IllegalHalfEdgeCollapse::BoundaryCollapse); + } + + // Return the opposite vertex + Ok(Some(h1.to)) + } else { + Ok(None) + } + }; + + // Notation: + // v_pos -> vertex opposite to the half-edge to collapse (v0v1) + // v_neg -> vertex opposite to the opposite half-edge to collapse (v1v0) + let v_pos = check_opposite_vertex(v0v1)?; + let v_neg = check_opposite_vertex(v1v0)?; + + if v_pos.is_none() || v_neg.is_none() { + return Err(IllegalHalfEdgeCollapse::FacelessEdge); + } + + // Test intersection of the one-rings of v0 and v1 + for &he in &self.vertex_half_edge_map[v0] { + let he = &self.half_edges[he]; + let vv = he.to; + if vv != v1 && Some(vv) != v_pos && Some(vv) != v_neg { + if let Some(_) = self.half_edge(vv, v1) { + return Err(IllegalHalfEdgeCollapse::IntersectionOfOneRing); + } + } + } + + Ok(()) + } + + pub fn try_half_edge_collapse( + &mut self, + half_edge: HalfEdge, + ) -> Result<(), IllegalHalfEdgeCollapse> { + self.is_collapse_ok(half_edge)?; + + self.half_edge_collapse(half_edge); + Ok(()) + } + + pub fn half_edge_collapse(&mut self, half_edge: HalfEdge) { + let he = half_edge; + let he_o = self.opposite(he); + + let v_from = he_o.to; + let v_to = he.to; + + // TODO: Support collapse of boundary edges + + let he_n = self + .try_next(he) + .expect("encountered boundary (missing opposite vertex)"); + let he_nn = self.next(he_n); + + let he_on = self + .try_next(he_o) + .expect("encountered boundary (missing opposite vertex)"); + let he_onn = self.next(he_on); + + // Vertices opposite to the edge to collapse + let v_pos = he_n.to; + let v_neg = he_on.to; + + let conn_from = self.vertex_half_edge_map[v_from].clone(); + let mut conn_to = self.vertex_half_edge_map[v_to].clone(); + + // Mark faces and vertex for removal + { + he.face.map(|f| self.removed_triangles.insert(f)); + he_o.face.map(|f| self.removed_triangles.insert(f)); + self.removed_vertices.insert(v_from); + } + + // Collect half-edges to delete (inside collapsed triangles) + self.removed_half_edges.insert(he.idx); + self.removed_half_edges.insert(he_n.idx); + self.removed_half_edges.insert(he_nn.idx); + self.removed_half_edges.insert(he_o.idx); + self.removed_half_edges.insert(he_on.idx); + self.removed_half_edges.insert(he_onn.idx); + + // Handle case of two opposite but coincident faces + if v_pos == v_neg { + // Faces were already marked for removal above + // Half-edges were already marked for removal above + + // Mark other vertices of triangles for removal + self.removed_vertices.insert(v_to); + self.removed_vertices.insert(v_pos); + // Clear all connectivity of removed vertices + self.vertex_half_edge_map[v_from].clear(); + self.vertex_half_edge_map[v_to].clear(); + self.vertex_half_edge_map[v_pos].clear(); + + return; + } + + // Update the faces referencing the removed vertex + for &he_idx in &conn_from { + self.half_edges[he_idx].face.map(|f| { + self.triangles[f].iter_mut().for_each(|i| { + if *i == v_from { + *i = v_to; + } + }) + }); + } + + // Update the half-edges around the collapsed triangles (they become opposites) + { + let he_no = self.opposite(he_n); + let he_nno = self.opposite(he_nn); + + self.half_edges[he_no.idx].opposite = he_nno.idx; + self.half_edges[he_nno.idx].opposite = he_no.idx; + + let he_ono = self.opposite(he_on); + let he_onno = self.opposite(he_onn); + + self.half_edges[he_ono.idx].opposite = he_onno.idx; + self.half_edges[he_onno.idx].opposite = he_ono.idx; + } + + // Remove collapsed half-edges from connectivity of target vertex + conn_to.retain(|he_i| *he_i != he_n.idx && *he_i != he_o.idx); + // Transfer half-edge connectivity from collapsed to target vertex + for &he_i in &conn_from { + if he_i != he.idx && he_i != he_on.idx { + conn_to.push(he_i); + } + } + // Update the targets of half-edges pointing to collapsed vertex + for &he_i in &conn_to { + let opp = self.opposite_mut(he_i); + if opp.to == v_from { + opp.to = v_to; + } + } + self.vertex_half_edge_map[v_to] = conn_to; + // Clear all connectivity of the collapsed vertex + self.vertex_half_edge_map[v_from].clear(); + + // Remove collapsed half-edges from connectivity of vertices opposite to collapsed edge + self.vertex_half_edge_map[v_pos].retain(|he_i| *he_i != he_nn.idx); + self.vertex_half_edge_map[v_neg].retain(|he_i| *he_i != he_onn.idx); + } + + /// Computes the largest angle in radians by which a face normals rotates of triangles affect by the given half edge collapse, assumes that the given half edge is valid + pub fn half_edge_collapse_max_normal_change(&self, half_edge: HalfEdge) -> R { + let he = half_edge; + let he_o = self.opposite(he); + + let v_to = he.to; + let v_from = he_o.to; + + let mut max_normal_change_angle = R::zero(); + for face in self.incident_faces(v_from) { + let tri_old = self.triangles[face]; + let tri_new = tri_old.map(|i| if i == v_from { v_to } else { i }); + + // Skip faces that will be collapsed + if tri_new.iter().copied().filter(|i| *i == v_to).count() > 1 { + continue; + } + + let new_area = self.tri_area_ijk::(&tri_new); + if new_area > R::default_epsilon() { + let old_normal = self.tri_normal_ijk::(&tri_old); + let new_normal = self.tri_normal_ijk::(&tri_new); + + let alpha = old_normal.dot(&new_normal).acos(); + max_normal_change_angle = max_normal_change_angle.max(alpha); + } + } + + max_normal_change_angle + } + + /// Computes the largest ratio (`new/old`) of triangle aspect ratio of triangles affect by the given half edge collapse, assumes that the given half edge is valid + pub fn half_edge_collapse_max_aspect_ratio_change(&self, half_edge: HalfEdge) -> R { + let he = half_edge; + let he_o = self.opposite(he); + + let v_to = he.to; + let v_from = he_o.to; + + let mut max_aspect_ratio_change = R::one(); + for face in self.incident_faces(v_from) { + let tri_old = self.triangles[face]; + let tri_new = tri_old.map(|i| if i == v_from { v_to } else { i }); + + // Skip faces that will be collapsed + if tri_new.iter().copied().filter(|i| *i == v_to).count() > 1 { + continue; + } + + let old_aspect_ratio = self.tri_aspect_ratio::(&tri_old); + let new_aspect_ratio = self.tri_aspect_ratio::(&tri_new); + + max_aspect_ratio_change = + max_aspect_ratio_change.max(new_aspect_ratio / old_aspect_ratio) + } + + max_aspect_ratio_change + } + + fn compute_vertex_vertex_connectivity( + vertex_half_edge_map: &mut [Vec], + half_edges: &[HalfEdge], + ) { + vertex_half_edge_map.par_iter_mut().for_each(|hes| { + for he in hes { + *he = half_edges[*he].to; + } + }); + } + + /// Clean mesh of deleted elements (vertices, faces) for conversion into a triangle mesh (does not clean-up half-edges and their references) + fn garbage_collection_for_trimesh(&mut self, keep_vertices: bool) { + // Filter and update triangles + let filtered_triangles = self + .triangles + .par_iter() + .copied() + .enumerate() + .filter(|(i, _)| !self.removed_triangles.contains(i)) + .map(|(_, tri)| tri) + .collect(); + self.triangles = filtered_triangles; + + if !keep_vertices { + let mut new_vertex_indices = vec![0; self.vertices.len()]; + let mut filtered_vertices = + Vec::with_capacity(self.vertices.len() - self.removed_vertices.len()); + let mut filtered_vertex_map = + Vec::with_capacity(self.vertices.len() - self.removed_vertices.len()); + + // Filter vertices and assign new indices + let mut index_counter = 0; + for (i, new_index) in new_vertex_indices.iter_mut().enumerate() { + if !self.removed_vertices.contains(&i) { + *new_index = index_counter; + index_counter += 1; + filtered_vertices.push(self.vertices[i]); + filtered_vertex_map.push(std::mem::take(&mut self.vertex_half_edge_map[i])); + } + } + + // Update vertex maps + filtered_vertex_map.iter_mut().for_each(|m| { + for v in m { + *v = new_vertex_indices[*v]; + } + }); + + self.vertices = filtered_vertices; + self.vertex_half_edge_map = filtered_vertex_map; + + // Update triangles + self.triangles.par_iter_mut().for_each(|tri| { + tri[0] = new_vertex_indices[tri[0]]; + tri[1] = new_vertex_indices[tri[1]]; + tri[2] = new_vertex_indices[tri[2]]; + }); + + self.removed_vertices.clear(); + } + + self.removed_triangles.clear(); + } +} + +impl From> for HalfEdgeTriMesh { + fn from(mesh: TriMesh3d) -> Self { + profile!("construct_half_edge_mesh"); + + let mut he_mesh = HalfEdgeTriMesh::default(); + he_mesh.vertex_half_edge_map = vec![Vec::with_capacity(5); mesh.vertices().len()]; + he_mesh.vertices = mesh.vertices; + + for (tri_idx, tri) in mesh.triangles.iter().copied().enumerate() { + // Storage for inner half-edge indices + let mut tri_hes = [0, 0, 0]; + + // Loop over inner-half edges + for i in 0..3 { + let from = tri[i]; + let to = tri[(i + 1) % 3]; + + // Check if half-edge exists already + if let Some(he) = he_mesh.half_edge(from, to) { + // Store the current half-edge for later use + tri_hes[i] = he.idx; + // Update the face of the half-edge + he_mesh.half_edges[he.idx].face = Some(tri_idx); + } else { + let he_idx = he_mesh.half_edges.len(); + // Inner (counter-clockwise) edge + let he_ccw = HalfEdge { + idx: he_idx, + to, + face: Some(tri_idx), + next: None, + opposite: he_idx + 1, + }; + // Outer (counter-clockwise) edge + let he_cw = HalfEdge { + idx: he_idx + 1, + to: from, + face: None, + next: None, + opposite: he_idx, + }; + tri_hes[i] = he_idx; + + // Store half-edges + he_mesh.half_edges.push(he_ccw); + he_mesh.half_edges.push(he_cw); + // Update vertex connectivity + he_mesh.vertex_half_edge_map[from].push(he_idx); + he_mesh.vertex_half_edge_map[to].push(he_idx + 1); + } + } + + // Update inner half-edge next pointers + for i in 0..3 { + let j = (i + 1) % 3; + he_mesh.half_edges[tri_hes[i]].next = Some(tri_hes[j]); + } + } + + he_mesh.triangles = mesh.triangles; + he_mesh + } +} + +#[test] +fn test_half_edge_mesh() { + let tri_mesh = TriMesh3d:: { + vertices: vec![ + Vector3::new(0.0, 1.0, 0.0), + Vector3::new(1.0, 0.0, 0.0), + Vector3::new(1.0, 2.0, 0.0), + Vector3::new(2.0, 1.0, 0.0), + ], + triangles: vec![[0, 1, 2], [1, 3, 2]], + }; + + let he_mesh = crate::halfedge_mesh::HalfEdgeTriMesh::from(tri_mesh.clone()); + + let vert_map = tri_mesh.vertex_vertex_connectivity(); + let (_new_tri_mesh, new_vert_map) = he_mesh.into_parts(true); + + let mut a = vert_map; + let mut b = new_vert_map; + + a.par_iter_mut().for_each(|l| l.sort_unstable()); + b.par_iter_mut().for_each(|l| l.sort_unstable()); + + for (la, lb) in a.iter().zip(b.iter()) { + assert_eq!(la, lb); + } +} diff --git a/splashsurf_lib/src/lib.rs b/splashsurf_lib/src/lib.rs index 770f4cd..06a0fac 100644 --- a/splashsurf_lib/src/lib.rs +++ b/splashsurf_lib/src/lib.rs @@ -57,6 +57,7 @@ mod aabb; pub(crate) mod dense_subdomains; pub mod density_map; pub mod generic_tree; +pub mod halfedge_mesh; #[cfg(feature = "io")] #[cfg_attr(doc_cfg, doc(cfg(feature = "io")))] pub mod io; diff --git a/splashsurf_lib/src/mesh.rs b/splashsurf_lib/src/mesh.rs index 6ff6267..b3b637e 100644 --- a/splashsurf_lib/src/mesh.rs +++ b/splashsurf_lib/src/mesh.rs @@ -28,6 +28,131 @@ use vtkio::model::{Attribute, UnstructuredGridPiece}; #[cfg(feature = "vtk_extras")] pub use crate::mesh::vtk_helper::{IntoVtkDataSet, IntoVtkUnstructuredGridPiece}; +/// Computes the unsigned area of the given triangle +pub fn tri_area( + a: &Vector3, + b: &Vector3, + c: &Vector3, +) -> RComp { + let a = a.convert::(); + let b = b.convert::(); + let c = c.convert::(); + ((b - a).cross(&(c - a))) + .norm() + .unscale(RComp::one() + RComp::one()) +} + +/// Computes the face normal of the given triangle +pub fn tri_normal( + a: &Vector3, + b: &Vector3, + c: &Vector3, +) -> Vector3 { + let a = a.convert::(); + let b = b.convert::(); + let c = c.convert::(); + ((b - a).cross(&(c - a))).normalize() +} + +/// Computes the angle at vertex `b` of the given triangle +pub fn angle_between( + a: &Vector3, + b: &Vector3, + c: &Vector3, +) -> RComp { + let a = a.convert::(); + let b = b.convert::(); + let c = c.convert::(); + ((a - b).dot(&(c - b)) / ((a - b).norm() * (c - b).norm())).acos() +} + +/// Computes the minimum and maximum angle in the given triangle +pub fn tri_min_max_angles( + a: &Vector3, + b: &Vector3, + c: &Vector3, +) -> (RComp, RComp) { + let a = a.convert::(); + let b = b.convert::(); + let c = c.convert::(); + let alpha1: RComp = angle_between(&a, &b, &c); + let alpha2: RComp = angle_between(&b, &c, &a); + let alpha3 = RComp::pi() - alpha1 - alpha2; + + ( + alpha1.min(alpha2.min(alpha3)), + alpha1.max(alpha2.max(alpha3)), + ) +} + +/// Computes the aspect ratio of the given triangle +/// +/// The aspect ratio is computed as the inverse of the "stretch ratio" `S` given by +/// ```txt +/// S = sqrt(12) * (r_in / l_max) +/// ``` +/// where `r_in` is the radius of the in-circle and `l_max` is the longest edge of the triangle. +/// See e.g.: . +pub fn tri_aspect_ratio( + a: &Vector3, + b: &Vector3, + c: &Vector3, +) -> RComp { + let two = RComp::from_i32(2).unwrap(); + let sqrt_twelve = RComp::from_i32(12).unwrap().sqrt(); + + let a = a.convert::(); + let b = b.convert::(); + let c = c.convert::(); + + let l0 = (a - b).norm(); + let l1 = (b - c).norm(); + let l2 = (c - a).norm(); + let s = (l0 + l1 + l2) / two; + + let area: RComp = tri_area(&a, &b, &c); + let r_in = area / s; + let l_max = l0.max(l1.max(l2)); + + l_max / (sqrt_twelve * r_in) +} + +/// Utility functions for triangles meshes +pub trait TriMesh3dExt { + /// Returns the slice of all triangle vertices of the mesh + fn tri_vertices(&self) -> &[Vector3]; + + /// Computes the area of the triangle with the given vertices + fn tri_area_ijk(&self, ijk: &[usize; 3]) -> RComp { + let v = self.tri_vertices(); + tri_area(&v[ijk[0]], &v[ijk[1]], &v[ijk[2]]) + } + + /// Computes the face normal of the triangle with the given vertices + fn tri_normal_ijk(&self, ijk: &[usize; 3]) -> Vector3 { + let v = self.tri_vertices(); + tri_normal(&v[ijk[0]], &v[ijk[1]], &v[ijk[2]]) + } + + /// Computes the minimum and maximum angle in the triangle with the given vertices + fn tri_min_max_angles_ijk(&self, ijk: &[usize; 3]) -> (RComp, RComp) { + let v = self.tri_vertices(); + tri_min_max_angles(&v[ijk[0]], &v[ijk[1]], &v[ijk[2]]) + } + + /// Computes the aspect ratio of the triangle with the given vertices + fn tri_aspect_ratio(&self, ijk: &[usize; 3]) -> RComp { + let v = self.tri_vertices(); + tri_aspect_ratio(&v[ijk[0]], &v[ijk[1]], &v[ijk[2]]) + } +} + +impl TriMesh3dExt for TriMesh3d { + fn tri_vertices(&self) -> &[Vector3] { + &self.vertices + } +} + /// A named attribute with data that can be attached to the vertices or cells of a mesh #[derive(Clone, Debug)] pub struct MeshAttribute { diff --git a/splashsurf_lib/src/postprocessing.rs b/splashsurf_lib/src/postprocessing.rs index d82dbde..7b03f2c 100644 --- a/splashsurf_lib/src/postprocessing.rs +++ b/splashsurf_lib/src/postprocessing.rs @@ -1,7 +1,7 @@ //! Functions for post-processing of surface meshes (decimation, smoothing, etc.) use crate::mesh::{Mesh3d, MixedTriQuadMesh3d, TriMesh3d, TriangleOrQuadCell}; -use crate::{SetType, profile, MapType, Real}; +use crate::{profile, MapType, Real, SetType}; use log::info; use nalgebra::Vector3; use rayon::prelude::*; diff --git a/splashsurf_lib/tests/integration_tests/mod.rs b/splashsurf_lib/tests/integration_tests/mod.rs index 2319000..9b76c4c 100644 --- a/splashsurf_lib/tests/integration_tests/mod.rs +++ b/splashsurf_lib/tests/integration_tests/mod.rs @@ -1,5 +1,7 @@ #[cfg(feature = "io")] pub mod test_full; +#[cfg(feature = "io")] +pub mod test_mesh; pub mod test_neighborhood_search; #[cfg(feature = "io")] pub mod test_octree; diff --git a/splashsurf_lib/tests/integration_tests/test_mesh.rs b/splashsurf_lib/tests/integration_tests/test_mesh.rs new file mode 100644 index 0000000..e52dd73 --- /dev/null +++ b/splashsurf_lib/tests/integration_tests/test_mesh.rs @@ -0,0 +1,66 @@ +use splashsurf_lib::halfedge_mesh::HalfEdgeTriMesh; +use splashsurf_lib::io; +use splashsurf_lib::mesh::MeshWithData; + +#[test] +fn test_halfedge_ico() -> Result<(), anyhow::Error> { + let mesh = io::obj_format::surface_mesh_from_obj::("../data/icosphere.obj")?.mesh; + let mut he_mesh = HalfEdgeTriMesh::from(mesh); + + he_mesh.try_half_edge_collapse(he_mesh.half_edge(12, 0).unwrap())?; + he_mesh.try_half_edge_collapse(he_mesh.half_edge(18, 2).unwrap())?; + he_mesh.try_half_edge_collapse(he_mesh.half_edge(2, 17).unwrap())?; + he_mesh.try_half_edge_collapse(he_mesh.half_edge(17, 0).unwrap())?; + he_mesh.try_half_edge_collapse(he_mesh.half_edge(27, 7).unwrap())?; + he_mesh.try_half_edge_collapse(he_mesh.half_edge(34, 7).unwrap())?; + he_mesh.try_half_edge_collapse(he_mesh.half_edge(39, 7).unwrap())?; + he_mesh.try_half_edge_collapse(he_mesh.half_edge(26, 7).unwrap())?; + + he_mesh.try_half_edge_collapse(he_mesh.half_edge(0, 16).unwrap())?; + + let (tri_mesh, _vertex_map) = he_mesh.into_parts(true); + let _tri_vertex_map = tri_mesh.vertex_vertex_connectivity(); + + io::obj_format::mesh_to_obj(&MeshWithData::new(tri_mesh), "../icosphere_new.obj")?; + Ok(()) +} + +#[test] +fn test_halfedge_plane() -> Result<(), anyhow::Error> { + let mut mesh = io::obj_format::surface_mesh_from_obj::("../data/plane.obj")?.mesh; + + // Make mesh curved + for v in mesh.vertices.iter_mut() { + v.y = -0.1 * (v.x * v.x + v.z * v.z); + } + + io::obj_format::mesh_to_obj(&MeshWithData::new(mesh.clone()), "../plane_new_0.obj")?; + + let mut he_mesh = HalfEdgeTriMesh::from(mesh); + + //he_mesh.vertices[246].y = 0.01; + //he_mesh.vertices[267].y = 0.02; + + dbg!(he_mesh.half_edge_collapse_max_normal_change(he_mesh.half_edge(224, 223).unwrap())); + he_mesh.try_half_edge_collapse(he_mesh.half_edge(224, 223).unwrap())?; + + dbg!(he_mesh.half_edge_collapse_max_normal_change(he_mesh.half_edge(223, 225).unwrap())); + he_mesh.try_half_edge_collapse(he_mesh.half_edge(223, 225).unwrap())?; + + dbg!(he_mesh.half_edge_collapse_max_normal_change(he_mesh.half_edge(225, 246).unwrap())); + he_mesh.try_half_edge_collapse(he_mesh.half_edge(225, 246).unwrap())?; + + dbg!(he_mesh.half_edge_collapse_max_normal_change(he_mesh.half_edge(246, 267).unwrap())); + he_mesh.try_half_edge_collapse(he_mesh.half_edge(246, 267).unwrap())?; + + //dbg!(he_mesh.half_edge_collapse_max_normal_change(he_mesh.half_edge(223, 202).unwrap())); + //he_mesh.try_half_edge_collapse(he_mesh.half_edge(223, 202).unwrap())?; + //dbg!(he_mesh.half_edge_collapse_max_normal_change(he_mesh.half_edge(202, 181).unwrap())); + //he_mesh.try_half_edge_collapse(he_mesh.half_edge(202, 181).unwrap())?; + + let (tri_mesh, _vertex_map) = he_mesh.into_parts(true); + let _tri_vertex_map = tri_mesh.vertex_vertex_connectivity(); + + io::obj_format::mesh_to_obj(&MeshWithData::new(tri_mesh), "../plane_new_1.obj")?; + Ok(()) +} From 288404702511ec465220367e54847922be0913a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20L=C3=B6schner?= Date: Wed, 13 Sep 2023 16:40:30 +0200 Subject: [PATCH 05/22] Laplacian smoothing and "barnacle decimation" --- splashsurf/src/reconstruction.rs | 250 ++++++++++++- splashsurf_lib/src/postprocessing.rs | 532 ++++++++++++++++++++++++++- 2 files changed, 770 insertions(+), 12 deletions(-) diff --git a/splashsurf/src/reconstruction.rs b/splashsurf/src/reconstruction.rs index 6b8f0a2..237135d 100644 --- a/splashsurf/src/reconstruction.rs +++ b/splashsurf/src/reconstruction.rs @@ -7,7 +7,7 @@ use rayon::prelude::*; use splashsurf_lib::mesh::{AttributeData, Mesh3d, MeshAttribute, MeshWithData, PointCloud3d}; use splashsurf_lib::nalgebra::{Unit, Vector3}; use splashsurf_lib::sph_interpolation::SphInterpolator; -use splashsurf_lib::{density_map, profile, Index, Real}; +use splashsurf_lib::{density_map, profile, Aabb3d, Index, Real}; use std::borrow::Cow; use std::convert::TryFrom; use std::path::PathBuf; @@ -181,6 +181,26 @@ pub struct ReconstructSubcommandArgs { )] pub octree_sync_local_density: Switch, + /// Whether to enable decimation of some typical bad marching cubes triangle configurations (resulting in "barnacles" after Laplacian smoothing) + #[arg( + help_heading = ARGS_INTERP, + long, + default_value = "off", + value_name = "off|on", + ignore_case = true, + require_equals = true + )] + pub decimate_barnacles: Switch, + /// Whether to keep vertices without connectivity during decimation (faster and helps with debugging) + #[arg( + help_heading = ARGS_INTERP, + long, + default_value = "off", + value_name = "off|on", + ignore_case = true, + require_equals = true + )] + pub keep_vertices: Switch, /// Whether to compute surface normals at the mesh vertices and write them to the output file #[arg( help_heading = ARGS_INTERP, @@ -201,9 +221,48 @@ pub struct ReconstructSubcommandArgs { require_equals = true )] pub sph_normals: Switch, + /// Number of smoothing iterations to run on the normal field if normal interpolation is enabled (disabled by default) + #[arg(help_heading = ARGS_INTERP, long)] + pub normals_smoothing_iters: Option, + /// Whether to write raw normals without smoothing to the output mesh if normal smoothing is enabled + #[arg( + help_heading = ARGS_INTERP, + long, + default_value = "off", + value_name = "off|on", + ignore_case = true, + require_equals = true + )] + pub output_raw_normals: Switch, /// List of point attribute field names from the input file that should be interpolated to the reconstructed surface. Currently this is only supported for VTK and VTU input files. #[arg(help_heading = ARGS_INTERP, long)] pub interpolate_attributes: Vec, + /// Number of smoothing iterations to run on the reconstructed mesh + #[arg(help_heading = ARGS_INTERP, long)] + pub mesh_smoothing_iters: Option, + /// Whether to enable feature weights for mesh smoothing if mesh smoothing enabled. Preserves isolated particles even under strong smoothing. + #[arg( + help_heading = ARGS_INTERP, + long, + default_value = "off", + value_name = "off|on", + ignore_case = true, + require_equals = true + )] + pub mesh_smoothing_weights: Switch, + /// Normalization value from weighted number of neighbors to mesh smoothing weights + #[arg(help_heading = ARGS_INTERP, long, default_value = "13.0")] + pub mesh_smoothing_weights_normalization: f64, + /// Whether to write the smoothing weights to the output mesh file + #[arg( + help_heading = ARGS_INTERP, + long, + default_value = "off", + value_name = "off|on", + ignore_case = true, + require_equals = true + )] + pub output_smoothing_weights: Switch, /// Whether to write out the raw reconstructed mesh before applying any post-processing steps #[arg( help_heading = ARGS_INTERP, @@ -369,13 +428,21 @@ mod arguments { pub struct ReconstructionRunnerPostprocessingArgs { pub check_mesh: bool, + pub decimate_barnacles: bool, + pub keep_vertices: bool, pub compute_normals: bool, pub sph_normals: bool, + pub normals_smoothing_iters: Option, pub interpolate_attributes: Vec, + pub mesh_smoothing_iters: Option, + pub mesh_smoothing_weights: bool, + pub mesh_smoothing_weights_normalization: f64, pub generate_quads: bool, pub quad_max_edge_diag_ratio: f64, pub quad_max_normal_angle: f64, pub quad_max_interior_angle: f64, + pub output_mesh_smoothing_weights: bool, + pub output_raw_normals: bool, pub output_raw_mesh: bool, pub mesh_aabb: Option>, } @@ -509,13 +576,21 @@ mod arguments { let postprocessing = ReconstructionRunnerPostprocessingArgs { check_mesh: args.check_mesh.into_bool(), + decimate_barnacles: args.decimate_barnacles.into_bool(), + keep_vertices: args.keep_vertices.into_bool(), compute_normals: args.normals.into_bool(), sph_normals: args.sph_normals.into_bool(), + normals_smoothing_iters: args.normals_smoothing_iters, interpolate_attributes: args.interpolate_attributes.clone(), + mesh_smoothing_iters: args.mesh_smoothing_iters, + mesh_smoothing_weights: args.mesh_smoothing_weights.into_bool(), + mesh_smoothing_weights_normalization: args.mesh_smoothing_weights_normalization, generate_quads: args.generate_quads.into_bool(), quad_max_edge_diag_ratio: args.quad_max_edge_diag_ratio, quad_max_normal_angle: args.quad_max_normal_angle, quad_max_interior_angle: args.quad_max_interior_angle, + output_mesh_smoothing_weights: args.output_smoothing_weights.into_bool(), + output_raw_normals: args.output_raw_normals.into_bool(), output_raw_mesh: args.output_raw_mesh.into_bool(), mesh_aabb, }; @@ -937,8 +1012,21 @@ pub(crate) fn reconstruction_pipeline_generic( { profile!("postprocessing"); + let mut vertex_connectivity = None; + + // Decimate mesh if requested + if postprocessing.decimate_barnacles { + info!("Post-processing: Performing decimation"); + vertex_connectivity = Some(splashsurf_lib::postprocessing::decimation( + mesh_with_data.mesh.to_mut(), + postprocessing.keep_vertices, + )); + } + // Initialize SPH interpolator if required later - let interpolator_required = postprocessing.sph_normals || !attributes.is_empty(); + let interpolator_required = postprocessing.mesh_smoothing_weights + || postprocessing.sph_normals + || !attributes.is_empty(); let interpolator = if interpolator_required { profile!("initialize interpolator"); info!("Post-processing: Initializing interpolator..."); @@ -973,6 +1061,131 @@ pub(crate) fn reconstruction_pipeline_generic( None }; + // Compute mesh vertex-vertex connectivity map if required later + let vertex_connectivity_required = postprocessing.normals_smoothing_iters.is_some() + || postprocessing.mesh_smoothing_iters.is_some(); + if vertex_connectivity.is_none() && vertex_connectivity_required { + vertex_connectivity = Some(mesh_with_data.mesh.vertex_vertex_connectivity()); + } + + // Compute smoothing weights if requested + let smoothing_weights = if postprocessing.mesh_smoothing_weights { + profile!("compute smoothing weights"); + info!("Post-processing: Computing smoothing weights..."); + + // TODO: Switch between parallel/single threaded + // TODO: Re-use data from reconstruction? + + // Global neighborhood search + let nl = { + let search_radius = params.compact_support_radius; + + let mut domain = Aabb3d::from_points(particle_positions.as_slice()); + domain.grow_uniformly(search_radius); + + let mut nl = Vec::new(); + splashsurf_lib::neighborhood_search::neighborhood_search_spatial_hashing_parallel::< + I, + R, + >( + &domain, + particle_positions.as_slice(), + search_radius, + &mut nl, + ); + assert_eq!(nl.len(), particle_positions.len()); + nl + }; + + // Compute weighted neighbor count + let squared_r = params.compact_support_radius * params.compact_support_radius; + let weighted_ncounts = nl + .par_iter() + .enumerate() + .map(|(i, nl)| { + nl.iter() + .copied() + .map(|j| { + let dist = + (particle_positions[i] - particle_positions[j]).norm_squared(); + let weight = R::one() - (dist / squared_r).clamp(R::zero(), R::one()); + return weight; + }) + .fold(R::zero(), R::add) + }) + .collect::>(); + + let vertex_weighted_num_neighbors = { + profile!("interpolate weighted neighbor counts"); + interpolator + .as_ref() + .expect("interpolator is required") + .interpolate_scalar_quantity( + weighted_ncounts.as_slice(), + &mesh_with_data.vertices(), + true, + ) + }; + + let smoothing_weights = { + let offset = R::zero(); + let normalization = + R::from_f64(postprocessing.mesh_smoothing_weights_normalization).expect( + "smoothing weight normalization value cannot be represented as Real type", + ) - offset; + + // Normalize number of neighbors + let smoothing_weights = vertex_weighted_num_neighbors + .par_iter() + .copied() + .map(|n| (n - offset).max(R::zero())) + .map(|n| (n / normalization).min(R::one())) + // Smooth-Step function + .map(|x| x.powi(5).times(6) - x.powi(4).times(15) + x.powi(3).times(10)) + .collect::>(); + + if postprocessing.output_mesh_smoothing_weights { + // Raw distance-weighted number of neighbors value per vertex (can be used to determine normalization value) + mesh_with_data.point_attributes.push(MeshAttribute::new( + "wnn".to_string(), + AttributeData::ScalarReal(vertex_weighted_num_neighbors), + )); + // Final smoothing weights per vertex + mesh_with_data.point_attributes.push(MeshAttribute::new( + "sw".to_string(), + AttributeData::ScalarReal(smoothing_weights.clone()), + )); + } + + smoothing_weights + }; + + Some(smoothing_weights) + } else { + None + }; + + // Perform smoothing if requested + if let Some(mesh_smoothing_iters) = postprocessing.mesh_smoothing_iters { + profile!("mesh smoothing"); + info!("Post-processing: Smoothing mesh..."); + + // TODO: Switch between parallel/single threaded + + let smoothing_weights = smoothing_weights + .unwrap_or_else(|| vec![R::one(); mesh_with_data.vertices().len()]); + + splashsurf_lib::postprocessing::par_laplacian_smoothing_inplace( + mesh_with_data.mesh.to_mut(), + vertex_connectivity + .as_ref() + .expect("vertex connectivity is required"), + mesh_smoothing_iters, + R::one(), + &smoothing_weights, + ); + } + // Add normals to mesh if requested if postprocessing.compute_normals { profile!("compute normals"); @@ -996,10 +1209,35 @@ pub(crate) fn reconstruction_pipeline_generic( bytemuck::allocation::cast_vec::>, Vector3>(tri_normals) }; - mesh_with_data.point_attributes.push(MeshAttribute::new( - "normals".to_string(), - AttributeData::Vector3Real(normals), - )); + // Smooth normals + if let Some(smoothing_iters) = postprocessing.normals_smoothing_iters { + info!("Post-processing: Smoothing normals..."); + + let mut smoothed_normals = normals.clone(); + splashsurf_lib::postprocessing::par_laplacian_smoothing_normals_inplace( + &mut smoothed_normals, + vertex_connectivity + .as_ref() + .expect("vertex connectivity is required"), + smoothing_iters, + ); + + mesh_with_data.point_attributes.push(MeshAttribute::new( + "normals".to_string(), + AttributeData::Vector3Real(smoothed_normals), + )); + if postprocessing.output_raw_normals { + mesh_with_data.point_attributes.push(MeshAttribute::new( + "raw_normals".to_string(), + AttributeData::Vector3Real(normals), + )); + } + } else { + mesh_with_data.point_attributes.push(MeshAttribute::new( + "normals".to_string(), + AttributeData::Vector3Real(normals), + )); + } } // Interpolate attributes if requested diff --git a/splashsurf_lib/src/postprocessing.rs b/splashsurf_lib/src/postprocessing.rs index 7b03f2c..0645049 100644 --- a/splashsurf_lib/src/postprocessing.rs +++ b/splashsurf_lib/src/postprocessing.rs @@ -1,11 +1,534 @@ //! Functions for post-processing of surface meshes (decimation, smoothing, etc.) -use crate::mesh::{Mesh3d, MixedTriQuadMesh3d, TriMesh3d, TriangleOrQuadCell}; -use crate::{profile, MapType, Real, SetType}; -use log::info; +use crate::halfedge_mesh::{HalfEdgeTriMesh, IllegalHalfEdgeCollapse}; +use crate::mesh::{Mesh3d, MixedTriQuadMesh3d, TriMesh3d, TriMesh3dExt, TriangleOrQuadCell}; +use crate::topology::{Axis, DirectedAxis, Direction}; +use crate::uniform_grid::UniformCartesianCubeGrid3d; +use crate::{profile, Index, MapType, Real, SetType}; +use log::{info, warn}; use nalgebra::Vector3; use rayon::prelude::*; +/// Laplacian Smoothing with feature weights +/// +/// Move each vertex towards the mean position of its neighbors. +/// Factor beta in \[0;1] proportional to amount of smoothing (for beta=1 each vertex is placed at the mean position). +/// Additionally, feature weights can be specified to apply a varying amount of smoothing over the mesh. +pub fn par_laplacian_smoothing_inplace( + mesh: &mut TriMesh3d, + vertex_connectivity: &[Vec], + iterations: usize, + beta: R, + weights: &[R], +) { + profile!("laplacian_smoothing"); + + let mut vertex_buffer = mesh.vertices.clone(); + + for _ in 0..iterations { + profile!("laplacian_smoothing iter"); + + std::mem::swap(&mut vertex_buffer, &mut mesh.vertices); + + mesh.vertices + .par_iter_mut() + .enumerate() + .for_each(|(i, vertex_i)| { + let beta_eff = beta * weights[i]; + + // Compute mean position of neighboring vertices + let mut vertex_sum = Vector3::zeros(); + for j in vertex_connectivity[i].iter() { + vertex_sum += vertex_buffer[j.clone()]; + } + if vertex_connectivity[i].len() > 0 { + let n = R::from_usize(vertex_connectivity[i].len()).unwrap(); + vertex_sum /= n; + } + + *vertex_i = vertex_i.scale(R::one() - beta_eff) + vertex_sum.scale(beta_eff); + }); + } +} + +/// Laplacian smoothing of a normal field +pub fn par_laplacian_smoothing_normals_inplace( + normals: &mut Vec>, + vertex_connectivity: &[Vec], + iterations: usize, +) { + profile!("par_laplacian_smoothing_normals_inplace"); + + let mut normal_buffer = normals.clone(); + + for _ in 0..iterations { + profile!("smoothing iteration"); + + std::mem::swap(&mut normal_buffer, normals); + + normals + .par_iter_mut() + .enumerate() + .for_each(|(i, normal_i)| { + *normal_i = Vector3::zeros(); + for j in vertex_connectivity[i].iter().copied() { + let normal_j = normal_buffer[j]; + *normal_i += normal_j; + } + normal_i.normalize_mut(); + }); + } +} + +pub fn decimation(mesh: &mut TriMesh3d, keep_vertices: bool) -> Vec> { + profile!("decimation"); + + let mut half_edge_mesh = HalfEdgeTriMesh::from(std::mem::take(mesh)); + merge_barnacle_configurations(&mut half_edge_mesh); + + { + profile!("convert mesh back"); + let (new_mesh, vertex_map) = half_edge_mesh.into_parts(keep_vertices); + *mesh = new_mesh; + return vertex_map; + } +} + +pub fn merge_barnacle_configurations(mesh: &mut HalfEdgeTriMesh) { + profile!("merge_barnacle_configurations"); + + merge_single_barnacle_configurations_he(mesh); + merge_double_barnacle_configurations_he(mesh); +} + +#[allow(unused)] +fn find_small_triangles(mesh: &HalfEdgeTriMesh, area_limit: R) -> Vec { + profile!("find_small_triangles"); + let zero_sized_triangles = mesh + .triangles + .par_iter() + .enumerate() + .filter(|(i, _)| mesh.is_valid_triangle(*i)) + .filter(|(_, tri)| mesh.tri_area_ijk::(tri) <= area_limit) + .map(|(i, _)| i) + .collect::>(); + info!( + "Found {} small sized triangles (area <= {})", + zero_sized_triangles.len(), + area_limit + ); + zero_sized_triangles +} + +#[allow(unused)] +fn find_bad_triangles(mesh: &HalfEdgeTriMesh, max_aspect_ratio: R) -> Vec { + profile!("find_bad_triangles"); + let mut ars = mesh + .triangles + .par_iter() + .enumerate() + .filter(|(i, _)| mesh.is_valid_triangle(*i)) + .filter(|(_, tri)| mesh.tri_area_ijk::(tri) > R::default_epsilon()) + .map(|(i, tri)| { + let aspect_ratio = mesh.tri_aspect_ratio::(tri); + (i, aspect_ratio) + }) + .filter(|(_, ar)| *ar >= max_aspect_ratio) + .collect::>(); + ars.par_sort_unstable_by(|a, b| { + let a: R = a.1; + let b: R = b.1; + a.to_f32() + .unwrap() + .total_cmp(&b.to_f32().unwrap()) + .reverse() + }); + dbg!(&ars[0..10.min(ars.len())]); + println!( + "Found {} triangles with bad aspect ratio (>= {})", + ars.len(), + max_aspect_ratio + ); + ars.into_iter().map(|(i, _)| i).collect() +} + +#[allow(unused)] +fn process_triangle_collapse_queue( + mesh: &mut HalfEdgeTriMesh, + triangles: impl Iterator, +) -> (Vec, usize) { + let mut processed = 0; + let remaining = triangles + .flat_map(|tri_idx| { + if !mesh.is_valid_triangle(tri_idx) { + return None; + } + + let tri = mesh.triangles[tri_idx]; + + let mut last_res = None; + let mut from = 0; + let mut to = 0; + // Try to find an edge of the triangle that can be collapsed + for i in 0..3 { + from = tri[i]; + to = tri[(i + 1) % 3]; + + if let Some(he) = mesh.half_edge(from, to) { + last_res = Some(mesh.try_half_edge_collapse(he)); + match last_res { + Some(Ok(_)) => { + processed += 1; + return None; + } + _ => {} + } + } else { + warn!( + "Invalid collapse: Half-edge missing (from {} to {})", + from, to + ); + return None; + } + } + + match last_res { + Some(Err(IllegalHalfEdgeCollapse::IntersectionOfOneRing)) => Some(tri_idx), + Some(Err(e)) => { + warn!("Invalid collapse: {:?} (from {} to {})", e, from, to); + None + } + _ => return None, + } + }) + .collect(); + + (remaining, processed) +} + +#[allow(unused)] +fn process_triangle_collapse_queue_iterative( + mesh: &mut HalfEdgeTriMesh, + triangles: impl Iterator, +) -> usize { + profile!("process_triangle_collapse_queue_iterative"); + let (mut remaining, mut processed) = process_triangle_collapse_queue(mesh, triangles); + let mut iter = 1; + info!( + "{} collapse operations remaining after pass {}", + remaining.len(), + iter + ); + while !remaining.is_empty() && iter < 5 { + iter += 1; + let (remaining_new, processed_new) = + process_triangle_collapse_queue(mesh, remaining.into_iter()); + remaining = remaining_new; + processed += processed_new; + info!( + "{} collapse operations remaining after pass {}", + remaining.len(), + iter + ); + } + + processed +} + +fn process_collapse_queue( + mesh: &mut HalfEdgeTriMesh, + collapses: impl Iterator, +) -> SetType<(usize, usize)> { + collapses + .flat_map(|(from, to)| { + if let Some(he) = mesh.half_edge(from, to) { + match mesh.try_half_edge_collapse(he) { + Ok(_) => None, + Err(IllegalHalfEdgeCollapse::IntersectionOfOneRing) => Some((from, to)), + Err(e) => { + warn!("Invalid collapse: {:?} (from {} to {})", e, from, to); + None + } + } + } else { + warn!( + "Invalid collapse: Half-edge missing (from {} to {})", + from, to + ); + None + } + }) + .collect() +} + +fn process_collapse_queue_iterative( + mesh: &mut HalfEdgeTriMesh, + collapses: impl Iterator, +) { + profile!("process_collapse_queue_iterative"); + let mut remaining = process_collapse_queue(mesh, collapses); + let mut iter = 1; + info!( + "{} collapse operations remaining after pass {}", + remaining.len(), + iter + ); + while !remaining.is_empty() && iter < 5 { + iter += 1; + remaining = process_collapse_queue(mesh, remaining.into_iter()); + info!( + "{} collapse operations remaining after pass {}", + remaining.len(), + iter + ); + } +} + +pub fn merge_single_barnacle_configurations_he(mesh: &mut HalfEdgeTriMesh) { + profile!("merge_single_barnacle_configurations"); + + //let vertex_map = &mesh.vertex_map; + + let half_edge_collapses = { + profile!("find candidates"); + + let mut candidates = mesh + .vertices + .par_iter() + .enumerate() + .filter_map(|(i, _v_i)| { + (mesh.vertex_one_ring_len(i) == 4 + && mesh + .vertex_one_ring(i) + .map(|j| mesh.vertex_one_ring_len(j)) + .all(|len| len >= 4 && len <= 6) + && mesh + .vertex_one_ring(i) + .map(|j| mesh.vertex_one_ring_len(j)) + .sum::() + == 20) + .then_some(i) + }) + .collect::>(); + + info!("Found {} single barnacle candidates", candidates.len()); + + let invalid_candidates = candidates + .par_iter() + .copied() + .filter_map(|c| { + mesh.vertex_one_ring(c) + .any(|i| candidates.contains(&i)) + .then_some(c) + }) + .collect::>(); + info!( + "Filtered out {} adjacent candidates", + invalid_candidates.len() + ); + invalid_candidates.into_iter().for_each(|c| { + candidates.remove(&c); + }); + + /* + let mut max_angles = candidates + .iter() + .copied() + .map(|c| { + let mut max_angle = R::zero(); + for j in mesh.vertex_one_ring(c) { + if let Some(he) = mesh.half_edge(j, c) { + if let Ok(_) = mesh.is_collapse_ok(he) { + max_angle = + max_angle.max(mesh.half_edge_collapse_max_normal_change(he)); + println!("max_angle: {}", max_angle); + } + } + } + println!( + "max angle change for config {}: {}\n\n", + c, + max_angle.to_f32().unwrap().to_degrees() + ); + (c, max_angle.to_f32().unwrap().to_degrees()) + }) + .collect::>(); + max_angles.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap()); + dbg!(max_angles); + */ + + let half_edge_collapses = candidates + .iter() + .copied() + .flat_map(|c| mesh.vertex_one_ring(c).map(move |i| (i, c))) + .collect::>(); + + info!("Enqueued {} collapse operations", half_edge_collapses.len()); + + half_edge_collapses + }; + + process_collapse_queue_iterative(mesh, half_edge_collapses.iter().map(|(i, j)| (*i, *j))); +} + +pub fn merge_double_barnacle_configurations_he(mesh: &mut HalfEdgeTriMesh) { + profile!("merge_double_barnacle_configurations"); + + let is_center_candidate = |i: usize| -> bool { + if mesh.vertex_one_ring_len(i) == 5 { + let mut neighbor_count = + std::array::from_fn(|j| mesh.vertex_one_ring_len(mesh.vertex_one_ring_ith(i, j))); + neighbor_count.sort_unstable(); + neighbor_count == [5, 5, 5, 6, 6] + } else { + false + } + }; + + let half_edge_collapses = { + profile!("find candidates"); + + let sorted_pair = |i: usize, j: usize| -> (usize, usize) { + let a = i.min(j); + let b = j.max(i); + (a, b) + }; + + let mut candidate_pairs = mesh + .vertices + .par_iter() + .enumerate() + .filter_map(|(i, _v_i)| { + if is_center_candidate(i) { + let mut count = 0; + let mut other = 0; + + // Identify the second center vertex of the double config + for j in mesh.vertex_one_ring(i) { + if is_center_candidate(j) { + count += 1; + other = j; + } + } + + // Sort the candidate pair to avoid duplicates + (count == 1).then_some(sorted_pair(i, other)) + } else { + None + } + }) + .collect::>(); + + info!("Found {} double barnacle candidates", candidate_pairs.len(),); + + // First filter out any candidate pair with one vertex of the pair being part of another pair + // This allows to make a unique vertex -> pair mapping in the next step + { + let is_double_candidate_overlapping = |i: usize, j: usize| { + let pair = sorted_pair(i, j); + mesh.vertex_one_ring(i).any(|k| { + let other_pair = sorted_pair(i, k); + // Only filter out one side of the adjacency + let is_smaller = other_pair < pair; + k != j && is_smaller && candidate_pairs.contains(&other_pair) + }) + }; + + let invalid_candidates = candidate_pairs + .par_iter() + .copied() + .filter_map(|(i, j)| { + (is_double_candidate_overlapping(i, j) || is_double_candidate_overlapping(j, i)) + .then_some((i, j)) + }) + .collect::>(); + info!( + "Filtered out {} overlapping candidates", + invalid_candidates.len() + ); + + invalid_candidates.into_iter().for_each(|c| { + candidate_pairs.remove(&c); + }); + } + + // Filter out any candidate pairs whose neighbors overlap with another candidate pair + { + // Collect unique mapping of all center vertices to their candidate pair + let mut candidate_to_pair_map = MapType::default(); + for (i, j) in candidate_pairs.iter().copied() { + candidate_to_pair_map.insert(i, sorted_pair(i, j)); + candidate_to_pair_map.insert(j, sorted_pair(i, j)); + } + + // Checks if any neighbor of `i` has a neighbor that belongs to a different candidate pair than `(i,j)` + let is_double_candidate_adjacent = |i: usize, j: usize| -> bool { + let pair = sorted_pair(i, j); + mesh.vertex_one_ring(i) + .filter(|k| *k != j) + .flat_map(|k| mesh.vertex_one_ring(k).filter(move |&l| l != i && l != j)) + .any(|l| { + if let Some(other_pair) = candidate_to_pair_map.get(&l) { + return *other_pair < pair; + } + return false; + }) + }; + + let invalid_candidates = candidate_pairs + .par_iter() + .copied() + .filter_map(|(i, j)| { + (is_double_candidate_adjacent(i, j) || is_double_candidate_adjacent(j, i)) + .then_some((i, j)) + }) + .collect::>(); + info!( + "Filtered out {} adjacent neighbor candidates", + invalid_candidates.len() + ); + + invalid_candidates.into_iter().for_each(|c| { + candidate_pairs.remove(&c); + }); + } + + // Collect the actual half-edge collapses to perform + let mut half_edge_collapses = MapType::default(); + for (i, j) in candidate_pairs { + let mut insert_replacement = |i: usize, j: usize, k: usize| { + if k != j { + if mesh.vertex_one_ring(k).all(|l| l != j) { + half_edge_collapses.insert(k, i); + } else { + if (mesh.vertices[k] - mesh.vertices[i]).norm() + <= (mesh.vertices[k] - mesh.vertices[j]).norm() + { + half_edge_collapses.insert(k, i); + } else { + half_edge_collapses.insert(k, j); + } + } + } + }; + + for k in mesh.vertex_one_ring(i) { + insert_replacement(i, j, k); + } + + for k in mesh.vertex_one_ring(j) { + insert_replacement(j, i, k); + } + } + + info!("Enqueued {} collapse operations", half_edge_collapses.len()); + + half_edge_collapses + }; + + process_collapse_queue_iterative(mesh, half_edge_collapses.iter().map(|(i, j)| (*i, *j))); +} + /// Merges triangles sharing an edge to quads if they fulfill the given criteria pub fn convert_tris_to_quads( mesh: &TriMesh3d, @@ -184,15 +707,12 @@ pub fn convert_tris_to_quads( quad_candidates.len() ); - //let mut triangles_to_remove = new_map(); let mut triangles_to_remove = SetType::default(); let mut filtered_candidates = SetType::default(); for ((i, j), _q) in quad_candidates { // TODO: If triangle already exists in list, compare quality if !triangles_to_remove.contains(&i) && !triangles_to_remove.contains(&j) { - //triangles_to_remove.insert(i, ((i,j), q)); - //triangles_to_remove.insert(j, ((i,j), q)); triangles_to_remove.insert(i); triangles_to_remove.insert(j); From 75d78f0b248d56715917885dc94745ee698a6b1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20L=C3=B6schner?= Date: Tue, 25 Jul 2023 15:36:35 +0200 Subject: [PATCH 06/22] Add particle densities to reconstruction output for subdomain grid --- splashsurf_lib/src/reconstruction.rs | 83 ++++++++++++++-------------- 1 file changed, 40 insertions(+), 43 deletions(-) diff --git a/splashsurf_lib/src/reconstruction.rs b/splashsurf_lib/src/reconstruction.rs index a3b6d2e..f3be76d 100644 --- a/splashsurf_lib/src/reconstruction.rs +++ b/splashsurf_lib/src/reconstruction.rs @@ -13,51 +13,48 @@ pub(crate) fn reconstruct_surface_subdomain_grid<'a, I: Index, R: Real>( parameters: &Parameters, output_surface: &'a mut SurfaceReconstruction, ) -> Result<(), anyhow::Error> { - let mesh = { - profile!("surface reconstruction subdomain-grid"); - - let parameters = initialize_parameters(parameters, &particle_positions, output_surface)?; - - // Filter "narrow band" - /* - let narrow_band_particles = extract_narrow_band(¶meters, &particles); - let particles = narrow_band_particles; - */ - - let subdomains = - decomposition::>(¶meters, &particle_positions)?; - - /* - { - use super::dense_subdomains::debug::*; - subdomain_stats(¶meters, &particle_positions, &subdomains); - info!( - "Number of subdomains with only ghost particles: {}", - count_no_owned_particles_subdomains(¶meters, &particle_positions, &subdomains) - ); - } - */ - - let particle_densities = - compute_global_density_vector(¶meters, &particle_positions, &subdomains); - - let surface_patches = reconstruction( - ¶meters, - &particle_positions, - &particle_densities, - &subdomains, - ); + profile!("surface reconstruction subdomain-grid"); - let global_mesh = stitching(surface_patches); - info!( - "Global mesh has {} vertices and {} triangles.", - global_mesh.vertices.len(), - global_mesh.triangles.len() - ); + let parameters = initialize_parameters(parameters, &particle_positions, output_surface)?; - global_mesh - }; + // Filter "narrow band" + /* + let narrow_band_particles = extract_narrow_band(¶meters, &particles); + let particles = narrow_band_particles; + */ - let _ = std::mem::replace(&mut output_surface.mesh, mesh); + let subdomains = + decomposition::>(¶meters, &particle_positions)?; + + /* + { + use super::dense_subdomains::debug::*; + subdomain_stats(¶meters, &particle_positions, &subdomains); + info!( + "Number of subdomains with only ghost particles: {}", + count_no_owned_particles_subdomains(¶meters, &particle_positions, &subdomains) + ); + } + */ + + let particle_densities = + compute_global_density_vector(¶meters, &particle_positions, &subdomains); + + let surface_patches = reconstruction( + ¶meters, + &particle_positions, + &particle_densities, + &subdomains, + ); + + let global_mesh = stitching(surface_patches); + info!( + "Global mesh has {} vertices and {} triangles.", + global_mesh.vertices.len(), + global_mesh.triangles.len() + ); + + output_surface.mesh = global_mesh; + output_surface.particle_densities = Some(particle_densities); Ok(()) } From 471600621ca5e0e48728579907cf2c3264b0c6f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20L=C3=B6schner?= Date: Fri, 28 Jul 2023 16:33:39 +0200 Subject: [PATCH 07/22] Collect global neighborhood list from subdomains --- splashsurf/src/reconstruction.rs | 41 ++++++++++--------- splashsurf_lib/benches/benches/bench_full.rs | 3 ++ splashsurf_lib/benches/benches/bench_mesh.rs | 1 + .../benches/benches/bench_subdomain_grid.rs | 1 + splashsurf_lib/src/dense_subdomains.rs | 36 ++++++++++++++-- splashsurf_lib/src/lib.rs | 13 ++++++ splashsurf_lib/src/neighborhood_search.rs | 6 +++ splashsurf_lib/src/reconstruction.rs | 21 ++++++---- .../tests/integration_tests/test_full.rs | 1 + 9 files changed, 94 insertions(+), 29 deletions(-) diff --git a/splashsurf/src/reconstruction.rs b/splashsurf/src/reconstruction.rs index 237135d..407c16f 100644 --- a/splashsurf/src/reconstruction.rs +++ b/splashsurf/src/reconstruction.rs @@ -567,6 +567,7 @@ mod arguments { particle_aabb, enable_multi_threading: args.parallelize_over_particles.into_bool(), spatial_decomposition, + global_neighborhood_list: args.mesh_smoothing_weights.into_bool(), }; // Optionally initialize thread pool @@ -975,7 +976,7 @@ pub(crate) fn reconstruction_pipeline_generic( // Perform the surface reconstruction let reconstruction = - splashsurf_lib::reconstruct_surface::(particle_positions.as_slice(), ¶ms)?; + splashsurf_lib::reconstruct_surface::(particle_positions.as_slice(), params)?; let grid = reconstruction.grid(); let mut mesh_with_data = MeshWithData::new(Cow::Borrowed(reconstruction.mesh())); @@ -1077,25 +1078,27 @@ pub(crate) fn reconstruction_pipeline_generic( // TODO: Re-use data from reconstruction? // Global neighborhood search - let nl = { - let search_radius = params.compact_support_radius; - - let mut domain = Aabb3d::from_points(particle_positions.as_slice()); - domain.grow_uniformly(search_radius); - - let mut nl = Vec::new(); - splashsurf_lib::neighborhood_search::neighborhood_search_spatial_hashing_parallel::< - I, - R, - >( - &domain, - particle_positions.as_slice(), - search_radius, - &mut nl, + let nl = reconstruction + .particle_neighbors() + .map(|nl| Cow::Borrowed(nl)) + .unwrap_or_else(|| + { + let search_radius = params.compact_support_radius; + + let mut domain = Aabb3d::from_points(particle_positions.as_slice()); + domain.grow_uniformly(search_radius); + + let mut nl = Vec::new(); + splashsurf_lib::neighborhood_search::neighborhood_search_spatial_hashing_parallel::( + &domain, + particle_positions.as_slice(), + search_radius, + &mut nl, + ); + assert_eq!(nl.len(), particle_positions.len()); + Cow::Owned(nl) + } ); - assert_eq!(nl.len(), particle_positions.len()); - nl - }; // Compute weighted neighbor count let squared_r = params.compact_support_radius * params.compact_support_radius; diff --git a/splashsurf_lib/benches/benches/bench_full.rs b/splashsurf_lib/benches/benches/bench_full.rs index 1550c21..8b8cf2d 100644 --- a/splashsurf_lib/benches/benches/bench_full.rs +++ b/splashsurf_lib/benches/benches/bench_full.rs @@ -104,6 +104,7 @@ pub fn surface_reconstruction_dam_break(c: &mut Criterion) { particle_aabb: None, enable_multi_threading: true, spatial_decomposition: None, + global_neighborhood_list: false, }; let mut group = c.benchmark_group("full surface reconstruction"); @@ -202,6 +203,7 @@ pub fn surface_reconstruction_double_dam_break(c: &mut Criterion) { particle_aabb: None, enable_multi_threading: true, spatial_decomposition: None, + global_neighborhood_list: false, }; let mut group = c.benchmark_group("full surface reconstruction"); @@ -300,6 +302,7 @@ pub fn surface_reconstruction_double_dam_break_inplace(c: &mut Criterion) { particle_aabb: None, enable_multi_threading: true, spatial_decomposition: None, + global_neighborhood_list: false, }; let mut group = c.benchmark_group("full surface reconstruction"); diff --git a/splashsurf_lib/benches/benches/bench_mesh.rs b/splashsurf_lib/benches/benches/bench_mesh.rs index 36951e2..6df2448 100644 --- a/splashsurf_lib/benches/benches/bench_mesh.rs +++ b/splashsurf_lib/benches/benches/bench_mesh.rs @@ -33,6 +33,7 @@ fn reconstruct_particles>(particle_file: P) -> SurfaceReconstruct ParticleDensityComputationStrategy::SynchronizeSubdomains, }, )), + global_neighborhood_list: false, }; reconstruct_surface::(particle_positions.as_slice(), ¶meters).unwrap() diff --git a/splashsurf_lib/benches/benches/bench_subdomain_grid.rs b/splashsurf_lib/benches/benches/bench_subdomain_grid.rs index 24dec38..c853d69 100644 --- a/splashsurf_lib/benches/benches/bench_subdomain_grid.rs +++ b/splashsurf_lib/benches/benches/bench_subdomain_grid.rs @@ -25,6 +25,7 @@ fn parameters_canyon() -> Parameters { subdomain_num_cubes_per_dim: 32, }, )), + global_neighborhood_list: false, }; parameters diff --git a/splashsurf_lib/src/dense_subdomains.rs b/splashsurf_lib/src/dense_subdomains.rs index e04b0b5..410ba26 100644 --- a/splashsurf_lib/src/dense_subdomains.rs +++ b/splashsurf_lib/src/dense_subdomains.rs @@ -72,6 +72,8 @@ pub(crate) struct ParametersSubdomainGrid { subdomain_grid: UniformCartesianCubeGrid3d, /// Chunk size for chunked parallel processing chunk_size: usize, + /// Whether to return the global particle neighborhood list instead of only using per-domain lists internally + global_neighborhood_list: bool, } /// Result of the subdomain decomposition procedure @@ -238,6 +240,7 @@ pub(crate) fn initialize_parameters<'a, I: Index, R: Real>( global_marching_cubes_grid: global_mc_grid, subdomain_grid, chunk_size, + global_neighborhood_list: parameters.global_neighborhood_list, }) } @@ -493,15 +496,16 @@ pub(crate) fn decomposition< }) } -pub(crate) fn compute_global_density_vector( +pub(crate) fn compute_global_densities_and_neighbors( parameters: &ParametersSubdomainGrid, global_particles: &[Vector3], subdomains: &Subdomains, -) -> Vec { +) -> (Vec, Vec>) { profile!(parent, "compute_global_density_vector"); info!("Starting computation of global density vector."); let global_particle_densities = Mutex::new(vec![R::zero(); global_particles.len()]); + let global_neighbors = Mutex::new(vec![Vec::new(); global_particles.len()]); #[derive(Default)] struct SubdomainWorkspace { @@ -610,9 +614,35 @@ pub(crate) fn compute_global_density_vector( global_particle_densities[particle_idx] = density; }); } + + // Write particle neighbor lists into global storage + if parameters.global_neighborhood_list { + profile!("update global neighbor list"); + // Lock global vector while this subdomain writes into it + let mut global_neighbors = global_neighbors.lock(); + is_inside + .iter() + .copied() + .zip( + subdomain_particle_indices + .iter() + .copied() + .zip(neighborhood_lists.iter()), + ) + // Update density values only for particles inside of the subdomain (ghost particles have wrong values) + .filter(|(is_inside, _)| *is_inside) + .for_each(|(_, (particle_idx, neighbors))| { + global_neighbors[particle_idx] = neighbors + .iter() + .copied() + .map(|local| subdomain_particle_indices[local]) + .collect(); + }); + } }); let global_particle_densities = global_particle_densities.into_inner(); + let global_neighbors = global_neighbors.into_inner(); /* { @@ -625,7 +655,7 @@ pub(crate) fn compute_global_density_vector( } */ - global_particle_densities + (global_particle_densities, global_neighbors) } pub(crate) struct SurfacePatch { diff --git a/splashsurf_lib/src/lib.rs b/splashsurf_lib/src/lib.rs index 06a0fac..785bd3e 100644 --- a/splashsurf_lib/src/lib.rs +++ b/splashsurf_lib/src/lib.rs @@ -269,6 +269,10 @@ pub struct Parameters { /// Parameters for the spatial decomposition of the surface reconstruction /// If not provided, no spatial decomposition is performed and a global approach is used instead. pub spatial_decomposition: Option>, + /// Whether to return the global particle neighborhood list from the reconstruction. + /// Depending on the settings of the reconstruction, neighborhood lists are only computed locally + /// in subdomains. Enabling this flag joins this data over all particles which can add a small overhead. + pub global_neighborhood_list: bool, } impl Parameters { @@ -283,6 +287,7 @@ impl Parameters { particle_aabb: map_option!(&self.particle_aabb, aabb => aabb.try_convert()?), enable_multi_threading: self.enable_multi_threading, spatial_decomposition: map_option!(&self.spatial_decomposition, sd => sd.try_convert()?), + global_neighborhood_list: self.global_neighborhood_list, }) } } @@ -300,6 +305,8 @@ pub struct SurfaceReconstruction { particle_densities: Option>, /// If an AABB was specified to restrict the reconstruction, this stores per input particle whether they were inside particle_inside_aabb: Option>, + /// Per particles neighbor lists + particle_neighbors: Option>>, /// Surface mesh that is the result of the surface reconstruction mesh: TriMesh3d, /// Workspace with allocated memory for subsequent surface reconstructions @@ -314,6 +321,7 @@ impl Default for SurfaceReconstruction { octree: None, density_map: None, particle_densities: None, + particle_neighbors: None, particle_inside_aabb: None, mesh: TriMesh3d::default(), workspace: ReconstructionWorkspace::default(), @@ -342,6 +350,11 @@ impl SurfaceReconstruction { self.particle_densities.as_ref() } + /// Returns a reference to the global particle density vector if it was computed during the reconstruction (always `None` when using octree based domain decomposition) + pub fn particle_neighbors(&self) -> Option<&Vec>> { + self.particle_neighbors.as_ref() + } + /// Returns a reference to the virtual background grid that was used as a basis for discretization of the density map for marching cubes, can be used to convert the density map to a hex mesh (using [`density_map::sparse_density_map_to_hex_mesh`]) pub fn grid(&self) -> &UniformGrid { &self.grid diff --git a/splashsurf_lib/src/neighborhood_search.rs b/splashsurf_lib/src/neighborhood_search.rs index f26a717..c064b56 100644 --- a/splashsurf_lib/src/neighborhood_search.rs +++ b/splashsurf_lib/src/neighborhood_search.rs @@ -252,6 +252,12 @@ impl FlatNeighborhoodList { self.neighbor_ptr.len() - 1 } + pub fn iter<'a>(&'a self) -> impl Iterator + 'a { + (0..self.neighbor_ptr.len()) + .into_iter() + .flat_map(|i| self.get_neighbors(i)) + } + /// Returns a slice containing the neighborhood list of the given particle pub fn get_neighbors(&self, particle_i: usize) -> Option<&[usize]> { let range = self diff --git a/splashsurf_lib/src/reconstruction.rs b/splashsurf_lib/src/reconstruction.rs index f3be76d..3b323af 100644 --- a/splashsurf_lib/src/reconstruction.rs +++ b/splashsurf_lib/src/reconstruction.rs @@ -2,8 +2,8 @@ use log::info; use nalgebra::Vector3; use crate::dense_subdomains::{ - compute_global_density_vector, decomposition, initialize_parameters, reconstruction, stitching, - subdomain_classification::GhostMarginClassifier, + compute_global_densities_and_neighbors, decomposition, initialize_parameters, reconstruction, + stitching, subdomain_classification::GhostMarginClassifier, }; use crate::{profile, Index, Parameters, Real, SurfaceReconstruction}; @@ -15,7 +15,8 @@ pub(crate) fn reconstruct_surface_subdomain_grid<'a, I: Index, R: Real>( ) -> Result<(), anyhow::Error> { profile!("surface reconstruction subdomain-grid"); - let parameters = initialize_parameters(parameters, &particle_positions, output_surface)?; + let internal_parameters = + initialize_parameters(parameters, &particle_positions, output_surface)?; // Filter "narrow band" /* @@ -24,7 +25,7 @@ pub(crate) fn reconstruct_surface_subdomain_grid<'a, I: Index, R: Real>( */ let subdomains = - decomposition::>(¶meters, &particle_positions)?; + decomposition::>(&internal_parameters, &particle_positions)?; /* { @@ -37,11 +38,14 @@ pub(crate) fn reconstruct_surface_subdomain_grid<'a, I: Index, R: Real>( } */ - let particle_densities = - compute_global_density_vector(¶meters, &particle_positions, &subdomains); + let (particle_densities, particle_neighbors) = compute_global_densities_and_neighbors( + &internal_parameters, + &particle_positions, + &subdomains, + ); let surface_patches = reconstruction( - ¶meters, + &internal_parameters, &particle_positions, &particle_densities, &subdomains, @@ -56,5 +60,8 @@ pub(crate) fn reconstruct_surface_subdomain_grid<'a, I: Index, R: Real>( output_surface.mesh = global_mesh; output_surface.particle_densities = Some(particle_densities); + if parameters.global_neighborhood_list { + output_surface.particle_neighbors = Some(particle_neighbors); + } Ok(()) } diff --git a/splashsurf_lib/tests/integration_tests/test_full.rs b/splashsurf_lib/tests/integration_tests/test_full.rs index 4133f67..c96f3a8 100644 --- a/splashsurf_lib/tests/integration_tests/test_full.rs +++ b/splashsurf_lib/tests/integration_tests/test_full.rs @@ -39,6 +39,7 @@ fn params_with_aabb( particle_aabb: domain_aabb, enable_multi_threading: false, spatial_decomposition: None, + global_neighborhood_list: false, }; match strategy { From 74b123ef465007abf582d3ec0659c5dc7080417a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20L=C3=B6schner?= Date: Tue, 12 Sep 2023 10:48:13 +0200 Subject: [PATCH 08/22] Implement MC cleanup postprocessing step Update log message --- splashsurf/src/reconstruction.rs | 27 ++++++ splashsurf_lib/src/dense_subdomains.rs | 19 +++- splashsurf_lib/src/postprocessing.rs | 122 ++++++++++++++++++++++++- splashsurf_lib/src/reconstruction.rs | 4 + 4 files changed, 170 insertions(+), 2 deletions(-) diff --git a/splashsurf/src/reconstruction.rs b/splashsurf/src/reconstruction.rs index 407c16f..38b2b87 100644 --- a/splashsurf/src/reconstruction.rs +++ b/splashsurf/src/reconstruction.rs @@ -181,6 +181,16 @@ pub struct ReconstructSubcommandArgs { )] pub octree_sync_local_density: Switch, + /// Whether to enable MC specific mesh decimation/simplification which removes bad quality triangles typically generated by MC + #[arg( + help_heading = ARGS_INTERP, + long, + default_value = "on", + value_name = "off|on", + ignore_case = true, + require_equals = true + )] + pub mesh_cleanup: Switch, /// Whether to enable decimation of some typical bad marching cubes triangle configurations (resulting in "barnacles" after Laplacian smoothing) #[arg( help_heading = ARGS_INTERP, @@ -428,6 +438,7 @@ mod arguments { pub struct ReconstructionRunnerPostprocessingArgs { pub check_mesh: bool, + pub mesh_cleanup: bool, pub decimate_barnacles: bool, pub keep_vertices: bool, pub compute_normals: bool, @@ -577,6 +588,7 @@ mod arguments { let postprocessing = ReconstructionRunnerPostprocessingArgs { check_mesh: args.check_mesh.into_bool(), + mesh_cleanup: args.mesh_cleanup.into_bool(), decimate_barnacles: args.decimate_barnacles.into_bool(), keep_vertices: args.keep_vertices.into_bool(), compute_normals: args.normals.into_bool(), @@ -1015,6 +1027,21 @@ pub(crate) fn reconstruction_pipeline_generic( let mut vertex_connectivity = None; + if postprocessing.mesh_cleanup { + info!("Post-processing: Performing mesh cleanup"); + let tris_before = mesh_with_data.mesh.triangles.len(); + let verts_before = mesh_with_data.mesh.vertices.len(); + vertex_connectivity = Some(splashsurf_lib::postprocessing::marching_cubes_cleanup( + mesh_with_data.mesh.to_mut(), + reconstruction.grid(), + 5, + postprocessing.keep_vertices, + )); + let tris_after = mesh_with_data.mesh.triangles.len(); + let verts_after = mesh_with_data.mesh.vertices.len(); + info!("Post-processing: Cleanup reduced number of vertices to {:.2}% and number of triangles to {:.2}% of original mesh.", (verts_after as f64 / verts_before as f64) * 100.0, (tris_after as f64 / tris_before as f64) * 100.0) + } + // Decimate mesh if requested if postprocessing.decimate_barnacles { info!("Post-processing: Performing decimation"); diff --git a/splashsurf_lib/src/dense_subdomains.rs b/splashsurf_lib/src/dense_subdomains.rs index 410ba26..b82c40c 100644 --- a/splashsurf_lib/src/dense_subdomains.rs +++ b/splashsurf_lib/src/dense_subdomains.rs @@ -19,7 +19,7 @@ use crate::neighborhood_search::{ neighborhood_search_spatial_hashing_flat_filtered, neighborhood_search_spatial_hashing_parallel, FlatNeighborhoodList, }; -use crate::uniform_grid::{EdgeIndex, UniformCartesianCubeGrid3d}; +use crate::uniform_grid::{EdgeIndex, GridConstructionError, UniformCartesianCubeGrid3d}; use crate::{ new_map, new_parallel_map, profile, Aabb3d, MapType, Parameters, SpatialDecomposition, SurfaceReconstruction, @@ -76,6 +76,23 @@ pub(crate) struct ParametersSubdomainGrid { global_neighborhood_list: bool, } +impl ParametersSubdomainGrid { + pub(crate) fn global_marching_cubes_grid( + &self, + ) -> Result, GridConstructionError> { + let n_cells = self.global_marching_cubes_grid.cells_per_dim(); + UniformCartesianCubeGrid3d::new( + self.global_marching_cubes_grid.aabb().min(), + &[ + I::from(n_cells[0]).ok_or(GridConstructionError::IndexTypeTooSmallCellsPerDim)?, + I::from(n_cells[1]).ok_or(GridConstructionError::IndexTypeTooSmallCellsPerDim)?, + I::from(n_cells[2]).ok_or(GridConstructionError::IndexTypeTooSmallCellsPerDim)?, + ], + self.global_marching_cubes_grid.cell_size(), + ) + } +} + /// Result of the subdomain decomposition procedure pub(crate) struct Subdomains { // Flat subdomain coordinate indices (same order as the particle list) diff --git a/splashsurf_lib/src/postprocessing.rs b/splashsurf_lib/src/postprocessing.rs index 0645049..66d9709 100644 --- a/splashsurf_lib/src/postprocessing.rs +++ b/splashsurf_lib/src/postprocessing.rs @@ -4,7 +4,7 @@ use crate::halfedge_mesh::{HalfEdgeTriMesh, IllegalHalfEdgeCollapse}; use crate::mesh::{Mesh3d, MixedTriQuadMesh3d, TriMesh3d, TriMesh3dExt, TriangleOrQuadCell}; use crate::topology::{Axis, DirectedAxis, Direction}; use crate::uniform_grid::UniformCartesianCubeGrid3d; -use crate::{profile, Index, MapType, Real, SetType}; +use crate::{new_map, profile, Index, MapType, Real, SetType}; use log::{info, warn}; use nalgebra::Vector3; use rayon::prelude::*; @@ -80,6 +80,126 @@ pub fn par_laplacian_smoothing_normals_inplace( } } +/// Mesh simplification designed for marching cubes surfaces meshes inspired by the "Compact Contouring"/"Mesh displacement" approach by Doug Moore and Joe Warren +/// +/// See ["Mesh Displacement: An Improved Contouring Method for Trivariate Data"](https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.49.5214&rep=rep1&type=pdf). +pub fn marching_cubes_cleanup( + mesh: &mut TriMesh3d, + grid: &UniformCartesianCubeGrid3d, + max_iter: usize, + keep_vertices: bool, +) -> Vec> { + profile!("marching_cubes_cleanup"); + + let half_dx = grid.cell_size() / (R::one() + R::one()); + + let nearest_grid_point = { + profile!("determine nearest grid points"); + mesh.vertices + .par_iter() + .enumerate() + .map(|(_, v)| { + // TODO: Move this to uniform grid + let cell_ijk = grid.enclosing_cell(v); + let min_point = grid.get_point(cell_ijk).unwrap(); + let min_coord = grid.point_coordinates(&min_point); + + let mut nearest_point = min_point; + if (v.x - min_coord.x) > half_dx { + nearest_point = grid + .get_point_neighbor( + &nearest_point, + DirectedAxis::new(Axis::X, Direction::Positive), + ) + .unwrap() + } + if (v.y - min_coord.y) > half_dx { + nearest_point = grid + .get_point_neighbor( + &nearest_point, + DirectedAxis::new(Axis::Y, Direction::Positive), + ) + .unwrap() + } + if (v.z - min_coord.z) > half_dx { + nearest_point = grid + .get_point_neighbor( + &nearest_point, + DirectedAxis::new(Axis::Z, Direction::Positive), + ) + .unwrap() + } + + grid.flatten_point_index(&nearest_point) + }) + .collect::>() + }; + + let (tri_mesh, vertex_map) = { + profile!("mesh displacement"); + let mut mesh = HalfEdgeTriMesh::from(std::mem::take(mesh)); + + // Tracks per vertex how many collapsed vertices contributed to its position + let mut vertex_sum_count = vec![1_usize; mesh.vertices.len()]; + // Buffer for vertices that should get collapsed + let mut vertex_buffer = Vec::new(); + + for _ in 0..max_iter { + profile!("mesh displacement iteration"); + + let mut collapse_count = 0; + for v0 in 0..mesh.vertices.len() { + if !mesh.is_valid_vertex(v0) { + continue; + } + + for he in mesh.outgoing_half_edges(v0) { + let v1 = he.to; + if nearest_grid_point[v0] == nearest_grid_point[v1] { + vertex_buffer.push(v1); + } + } + + for &v1 in vertex_buffer.iter() { + if mesh.is_valid_vertex(v1) { + if let Some(he) = mesh.half_edge(v1, v0) { + if let Ok(_) = mesh.try_half_edge_collapse(he) { + collapse_count += 1; + + // Move to averaged position + let pos_v0 = mesh.vertices[v0]; + let pos_v1 = mesh.vertices[v1]; + + let n0 = vertex_sum_count[v0]; + let n1 = vertex_sum_count[v1]; + + let n_new = n0 + n1; + let pos_new = (pos_v0.scale(R::from_usize(n0).unwrap()) + + pos_v1.scale(R::from_usize(n1).unwrap())) + .unscale(R::from_usize(n_new).unwrap()); + + vertex_sum_count[v0] = n_new; + mesh.vertices[v0] = pos_new; + } + } + } + } + + vertex_buffer.clear(); + } + + if collapse_count == 0 { + break; + } + } + + mesh.into_parts(keep_vertices) + }; + + *mesh = tri_mesh; + vertex_map +} + pub fn decimation(mesh: &mut TriMesh3d, keep_vertices: bool) -> Vec> { profile!("decimation"); diff --git a/splashsurf_lib/src/reconstruction.rs b/splashsurf_lib/src/reconstruction.rs index 3b323af..56b5764 100644 --- a/splashsurf_lib/src/reconstruction.rs +++ b/splashsurf_lib/src/reconstruction.rs @@ -1,3 +1,4 @@ +use anyhow::Context; use log::info; use nalgebra::Vector3; @@ -17,6 +18,9 @@ pub(crate) fn reconstruct_surface_subdomain_grid<'a, I: Index, R: Real>( let internal_parameters = initialize_parameters(parameters, &particle_positions, output_surface)?; + output_surface.grid = internal_parameters + .global_marching_cubes_grid() + .context("failed to convert global marching cubes grid")?; // Filter "narrow band" /* From 6314e9ba13e5b956d7e778c65cc2583a9c49fc98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20L=C3=B6schner?= Date: Tue, 12 Sep 2023 13:41:25 +0200 Subject: [PATCH 09/22] Simplify CellConnectivity --- splashsurf_lib/src/io/obj_format.rs | 10 ++- splashsurf_lib/src/io/ply_format.rs | 2 +- splashsurf_lib/src/mesh.rs | 79 +++++++++---------- .../tests/integration_tests/test_mesh.rs | 2 +- 4 files changed, 47 insertions(+), 46 deletions(-) diff --git a/splashsurf_lib/src/io/obj_format.rs b/splashsurf_lib/src/io/obj_format.rs index af7e1c1..98420f6 100644 --- a/splashsurf_lib/src/io/obj_format.rs +++ b/splashsurf_lib/src/io/obj_format.rs @@ -52,13 +52,19 @@ pub fn mesh_to_obj, P: AsRef>( if normals.is_some() { for f in mesh_vertices.cells() { write!(writer, "f")?; - f.try_for_each_vertex(|v| write!(writer, " {}//{}", v + 1, v + 1))?; + f.vertices() + .iter() + .copied() + .try_for_each(|v| write!(writer, " {}//{}", v + 1, v + 1))?; write!(writer, "\n")?; } } else { for f in mesh_vertices.cells() { write!(writer, "f")?; - f.try_for_each_vertex(|v| write!(writer, " {}", v + 1))?; + f.vertices() + .iter() + .copied() + .try_for_each(|v| write!(writer, " {}", v + 1))?; write!(writer, "\n")?; } } diff --git a/splashsurf_lib/src/io/ply_format.rs b/splashsurf_lib/src/io/ply_format.rs index bd6c095..fffe4ae 100644 --- a/splashsurf_lib/src/io/ply_format.rs +++ b/splashsurf_lib/src/io/ply_format.rs @@ -259,7 +259,7 @@ pub fn mesh_to_ply, P: AsRef>( for c in mesh.cells() { let num_verts = c.num_vertices().to_u8().expect("failed to convert cell vertex count to u8"); writer.write_all(&num_verts.to_le_bytes())?; - c.try_for_each_vertex(|v| { + c.vertices().iter().copied().try_for_each(|v| { let idx = v.to_u32().expect("failed to convert vertex index to u32"); writer.write_all(&idx.to_le_bytes()) })?; diff --git a/splashsurf_lib/src/mesh.rs b/splashsurf_lib/src/mesh.rs index b3b637e..a75dedb 100644 --- a/splashsurf_lib/src/mesh.rs +++ b/splashsurf_lib/src/mesh.rs @@ -256,16 +256,35 @@ pub trait Mesh3d { /// Returns a slice of all cells of the mesh fn cells(&self) -> &[Self::Cell]; + /// Returns a mapping of all mesh vertices to the set of their connected neighbor vertices + fn vertex_vertex_connectivity(&self) -> Vec> { + profile!("vertex_vertex_connectivity"); + + let mut connectivity_map: Vec> = + vec![Vec::with_capacity(4); self.vertices().len()]; + for cell in self.cells() { + for &i in cell.vertices() { + for &j in cell.vertices() { + if i != j && !connectivity_map[i].contains(&j) { + connectivity_map[i].push(j); + } + } + } + } + + connectivity_map + } + /// Returns a mapping of all mesh vertices to the set of the cells they belong to fn vertex_cell_connectivity(&self) -> Vec> { profile!("vertex_cell_connectivity"); let mut connectivity_map: Vec> = vec![Vec::new(); self.vertices().len()]; for (cell_idx, cell) in self.cells().iter().enumerate() { - cell.for_each_vertex(|v_i| { + for &v_i in cell.vertices() { if !connectivity_map[v_i].contains(&cell_idx) { connectivity_map[v_i].push(cell_idx); } - }) + } } connectivity_map @@ -274,22 +293,14 @@ pub trait Mesh3d { /// Basic interface for mesh cells consisting of a collection of vertex indices pub trait CellConnectivity { - /// Returns the number of vertices per cell + /// Returns the number of vertices per cell (may vary between cells) fn num_vertices(&self) -> usize { Self::expected_num_vertices() } - /// Returns the expected number of vertices per cell (helpful for connectivities with a constant number of vertices to reserve storage) + /// Returns the expected number of vertices per cell (helpful for connectivities with a constant or upper bound on the number of vertices to reserve storage) fn expected_num_vertices() -> usize; - /// Calls the given closure with each vertex index that is part of this cell, stopping at the first error and returning that error - fn try_for_each_vertex Result<(), E>>(&self, f: F) -> Result<(), E>; - /// Calls the given closure with each vertex index that is part of this cell - fn for_each_vertex(&self, mut f: F) { - self.try_for_each_vertex::<(), _>(move |i| { - f(i); - Ok(()) - }) - .unwrap(); - } + /// Returns a reference to the vertex indices connected by this cell + fn vertices(&self) -> &[usize]; } /// Cell type for [`TriMesh3d`] @@ -310,8 +321,8 @@ impl CellConnectivity for TriangleCell { 3 } - fn try_for_each_vertex Result<(), E>>(&self, f: F) -> Result<(), E> { - self.0.iter().copied().try_for_each(f) + fn vertices(&self) -> &[usize] { + &self.0[..] } } @@ -324,8 +335,8 @@ impl CellConnectivity for TriangleOrQuadCell { return self.num_vertices(); } - fn try_for_each_vertex Result<(), E>>(&self, f: F) -> Result<(), E> { - self.vertices().iter().copied().try_for_each(f) + fn vertices(&self) -> &[usize] { + self.vertices() } } @@ -334,8 +345,8 @@ impl CellConnectivity for HexCell { 8 } - fn try_for_each_vertex Result<(), E>>(&self, f: F) -> Result<(), E> { - self.0.iter().copied().try_for_each(f) + fn vertices(&self) -> &[usize] { + &self.0[..] } } @@ -344,8 +355,8 @@ impl CellConnectivity for PointCell { 1 } - fn try_for_each_vertex Result<(), E>>(&self, mut f: F) -> Result<(), E> { - f(self.0) + fn vertices(&self) -> &[usize] { + std::slice::from_ref(&self.0) } } @@ -544,25 +555,6 @@ impl TriMesh3d { }) } - /// Returns a mapping of all mesh vertices to the set of their connected neighbor vertices - pub fn vertex_vertex_connectivity(&self) -> Vec> { - profile!("vertex_vertex_connectivity"); - - let mut connectivity_map: Vec> = - vec![Vec::with_capacity(4); self.vertices().len()]; - for tri in &self.triangles { - for &i in tri { - for &j in tri { - if i != j && !connectivity_map[i].contains(&j) { - connectivity_map[i].push(j); - } - } - } - } - - connectivity_map - } - /// Same as [`Self::vertex_normal_directions_inplace`] but assumes that the output is already zeroed fn vertex_normal_directions_inplace_assume_zeroed(&self, normal_directions: &mut [Vector3]) { assert_eq!(normal_directions.len(), self.vertices.len()); @@ -1142,7 +1134,10 @@ pub mod vtk_helper { Vec::with_capacity(mesh.cells().len() * (MeshT::Cell::expected_num_vertices() + 1)); for cell in mesh.cells().iter() { vertices.push(cell.num_vertices() as u32); - cell.for_each_vertex(|v| vertices.push(v as u32)); + cell.vertices() + .iter() + .copied() + .for_each(|v| vertices.push(v as u32)); } vertices }; diff --git a/splashsurf_lib/tests/integration_tests/test_mesh.rs b/splashsurf_lib/tests/integration_tests/test_mesh.rs index e52dd73..fcfba89 100644 --- a/splashsurf_lib/tests/integration_tests/test_mesh.rs +++ b/splashsurf_lib/tests/integration_tests/test_mesh.rs @@ -1,6 +1,6 @@ use splashsurf_lib::halfedge_mesh::HalfEdgeTriMesh; use splashsurf_lib::io; -use splashsurf_lib::mesh::MeshWithData; +use splashsurf_lib::mesh::{Mesh3d, MeshWithData}; #[test] fn test_halfedge_ico() -> Result<(), anyhow::Error> { From aaef0f019f7cb4ecf1f2b62b6a29de2bc0ed74fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20L=C3=B6schner?= Date: Tue, 12 Sep 2023 15:57:24 +0200 Subject: [PATCH 10/22] Move keep_cells and clamp from TriMesh to all meshes --- splashsurf_lib/src/mesh.rs | 328 ++++++++++++++++++++++++------------- 1 file changed, 215 insertions(+), 113 deletions(-) diff --git a/splashsurf_lib/src/mesh.rs b/splashsurf_lib/src/mesh.rs index a75dedb..7d5b132 100644 --- a/splashsurf_lib/src/mesh.rs +++ b/splashsurf_lib/src/mesh.rs @@ -199,13 +199,21 @@ impl TriangleOrQuadCell { } } - /// Returns the slice of vertex indices of this cell + /// Returns a reference to the vertex indices of this cell fn vertices(&self) -> &[usize] { match self { TriangleOrQuadCell::Tri(v) => v, TriangleOrQuadCell::Quad(v) => v, } } + + /// Returns a mutable reference to the vertex indices of this cell + fn vertices_mut(&mut self) -> &mut [usize] { + match self { + TriangleOrQuadCell::Tri(v) => v, + TriangleOrQuadCell::Quad(v) => v, + } + } } /// A surface mesh in 3D containing cells representing either triangles or quadrilaterals @@ -247,15 +255,26 @@ impl PointCloud3d { /// /// Meshes consist of vertices and cells. Cells identify their associated vertices using indices /// into the mesh's slice of vertices. -pub trait Mesh3d { +pub trait Mesh3d +where + Self: Sized, +{ /// The cell connectivity type of the mesh - type Cell: CellConnectivity; + type Cell: CellConnectivity + Clone; /// Returns a slice of all vertices of the mesh fn vertices(&self) -> &[Vector3]; + /// Returns a mutable slice of all vertices of the mesh + fn vertices_mut(&mut self) -> &mut [Vector3]; /// Returns a slice of all cells of the mesh fn cells(&self) -> &[Self::Cell]; + /// Constructs a mesh from the given vertices and connectivity (does not check inputs for validity) + fn from_vertices_and_connectivity( + vertices: Vec>, + connectivity: Vec, + ) -> Self; + /// Returns a mapping of all mesh vertices to the set of their connected neighbor vertices fn vertex_vertex_connectivity(&self) -> Vec> { profile!("vertex_vertex_connectivity"); @@ -289,6 +308,91 @@ pub trait Mesh3d { connectivity_map } + + /// Returns a new mesh containing only the specified cells and removes all unreferenced vertices + fn keep_cells(&self, cell_indices: &[usize]) -> Self { + let vertices = self.vertices(); + let cells = self.cells(); + + // Each entry is true if this vertex should be kept, false otherwise + let vertex_keep_table = { + let mut table = vec![false; vertices.len()]; + for cell in cell_indices.iter().copied().map(|c_i| &cells[c_i]) { + for &vertex_index in cell.vertices() { + table[vertex_index] = true; + } + } + table + }; + + let old_to_new_label_map = { + let mut label_map = MapType::default(); + let mut next_label = 0; + for (i, keep) in vertex_keep_table.iter().enumerate() { + if *keep { + label_map.insert(i, next_label); + next_label += 1; + } + } + label_map + }; + + let relabeled_cells: Vec<_> = cell_indices + .iter() + .map(|&i| &cells[i]) + .cloned() + .map(|mut cell| { + for index in cell.vertices_mut() { + *index = *old_to_new_label_map + .get(index) + .expect("Index must be in map"); + } + cell + }) + .collect(); + + let relabeled_vertices: Vec<_> = vertex_keep_table + .iter() + .enumerate() + .filter_map(|(i, should_keep)| if *should_keep { Some(i) } else { None }) + .map(|index| vertices[index].clone()) + .collect(); + + Self::from_vertices_and_connectivity(relabeled_vertices, relabeled_cells) + } + + /// Removes all cells from the mesh that are completely outside of the given AABB and clamps the remaining cells to the boundary + fn par_clamp_with_aabb(&self, aabb: &Aabb3d) -> Self + where + Self::Cell: Sync, + { + // Find all triangles with at least one vertex inside of AABB + let vertices = self.vertices(); + let cells_to_keep = self + .cells() + .par_iter() + .enumerate() + .filter(|(_, cell)| { + cell.vertices() + .iter() + .copied() + .any(|v| aabb.contains_point(&vertices[v])) + }) + .map(|(i, _)| i) + .collect::>(); + // Remove all other cells from mesh + let mut new_mesh = self.keep_cells(&cells_to_keep); + // Clamp remaining vertices to AABB + new_mesh.vertices_mut().par_iter_mut().for_each(|v| { + let min = aabb.min(); + let max = aabb.max(); + v.x = v.x.clamp(min.x, max.x); + v.y = v.y.clamp(min.y, max.y); + v.z = v.z.clamp(min.z, max.z); + }); + + new_mesh + } } /// Basic interface for mesh cells consisting of a collection of vertex indices @@ -301,6 +405,8 @@ pub trait CellConnectivity { fn expected_num_vertices() -> usize; /// Returns a reference to the vertex indices connected by this cell fn vertices(&self) -> &[usize]; + /// Returns a reference to the vertex indices connected by this cell + fn vertices_mut(&mut self) -> &mut [usize]; } /// Cell type for [`TriMesh3d`] @@ -324,29 +430,41 @@ impl CellConnectivity for TriangleCell { fn vertices(&self) -> &[usize] { &self.0[..] } + + fn vertices_mut(&mut self) -> &mut [usize] { + &mut self.0[..] + } } -impl CellConnectivity for TriangleOrQuadCell { +impl CellConnectivity for HexCell { fn expected_num_vertices() -> usize { - 4 + 8 } - fn num_vertices(&self) -> usize { - return self.num_vertices(); + fn vertices(&self) -> &[usize] { + &self.0[..] } - fn vertices(&self) -> &[usize] { - self.vertices() + fn vertices_mut(&mut self) -> &mut [usize] { + &mut self.0[..] } } -impl CellConnectivity for HexCell { +impl CellConnectivity for TriangleOrQuadCell { fn expected_num_vertices() -> usize { - 8 + 4 + } + + fn num_vertices(&self) -> usize { + return self.num_vertices(); } fn vertices(&self) -> &[usize] { - &self.0[..] + TriangleOrQuadCell::vertices(self) + } + + fn vertices_mut(&mut self) -> &mut [usize] { + TriangleOrQuadCell::vertices_mut(self) } } @@ -358,6 +476,10 @@ impl CellConnectivity for PointCell { fn vertices(&self) -> &[usize] { std::slice::from_ref(&self.0) } + + fn vertices_mut(&mut self) -> &mut [usize] { + std::slice::from_mut(&mut self.0) + } } impl Mesh3d for TriMesh3d { @@ -367,32 +489,68 @@ impl Mesh3d for TriMesh3d { self.vertices.as_slice() } + fn vertices_mut(&mut self) -> &mut [Vector3] { + self.vertices.as_mut_slice() + } + fn cells(&self) -> &[TriangleCell] { self.triangle_cells() } + + fn from_vertices_and_connectivity( + vertices: Vec>, + triangles: Vec, + ) -> Self { + Self { + vertices, + triangles: bytemuck::cast_vec::(triangles), + } + } } -impl Mesh3d for MixedTriQuadMesh3d { - type Cell = TriangleOrQuadCell; +impl Mesh3d for HexMesh3d { + type Cell = HexCell; fn vertices(&self) -> &[Vector3] { self.vertices.as_slice() } - fn cells(&self) -> &[TriangleOrQuadCell] { - &self.cells + fn vertices_mut(&mut self) -> &mut [Vector3] { + self.vertices.as_mut_slice() + } + + fn cells(&self) -> &[HexCell] { + bytemuck::cast_slice::<[usize; 8], HexCell>(self.cells.as_slice()) + } + + fn from_vertices_and_connectivity(vertices: Vec>, cells: Vec) -> Self { + Self { + vertices, + cells: bytemuck::cast_vec::(cells), + } } } -impl Mesh3d for HexMesh3d { - type Cell = HexCell; +impl Mesh3d for MixedTriQuadMesh3d { + type Cell = TriangleOrQuadCell; fn vertices(&self) -> &[Vector3] { self.vertices.as_slice() } - fn cells(&self) -> &[HexCell] { - bytemuck::cast_slice::<[usize; 8], HexCell>(self.cells.as_slice()) + fn vertices_mut(&mut self) -> &mut [Vector3] { + self.vertices.as_mut_slice() + } + + fn cells(&self) -> &[TriangleOrQuadCell] { + &self.cells + } + + fn from_vertices_and_connectivity( + vertices: Vec>, + cells: Vec, + ) -> Self { + Self { vertices, cells } } } @@ -403,29 +561,40 @@ impl Mesh3d for PointCloud3d { self.points.as_slice() } + fn vertices_mut(&mut self) -> &mut [Vector3] { + self.points.as_mut_slice() + } + fn cells(&self) -> &[PointCell] { bytemuck::cast_slice::(self.indices.as_slice()) } -} -impl> Mesh3d for &MeshT { - type Cell = MeshT::Cell; - fn vertices(&self) -> &[Vector3] { - (*self).vertices() - } - fn cells(&self) -> &[MeshT::Cell] { - (*self).cells() + fn from_vertices_and_connectivity(points: Vec>, cells: Vec) -> Self { + Self { + points, + indices: bytemuck::cast_vec::(cells), + } } } -impl<'a, R: Real, MeshT: Mesh3d + ToOwned> Mesh3d for std::borrow::Cow<'a, MeshT> { +impl<'a, R: Real, MeshT: Mesh3d + Clone> Mesh3d for std::borrow::Cow<'a, MeshT> { type Cell = MeshT::Cell; + fn vertices(&self) -> &[Vector3] { (*self.as_ref()).vertices() } + + fn vertices_mut(&mut self) -> &mut [Vector3] { + (self.to_mut()).vertices_mut() + } + fn cells(&self) -> &[MeshT::Cell] { (*self.as_ref()).cells() } + + fn from_vertices_and_connectivity(vertices: Vec>, cells: Vec) -> Self { + std::borrow::Cow::Owned(MeshT::from_vertices_and_connectivity(vertices, cells).to_owned()) + } } impl TriangleCell { @@ -472,89 +641,6 @@ impl TriMesh3d { } } - /// Returns a new triangle mesh containing only the specified triangles and removes all unreferenced vertices - pub fn keep_tris(&self, cell_indices: &[usize]) -> Self { - // Each entry is true if this vertex should be kept, false otherwise - let vertex_keep_table = { - let mut table = vec![false; self.vertices.len()]; - for &cell_index in cell_indices { - let cell_connectivity = &self.triangles[cell_index]; - - for &vertex_index in cell_connectivity { - table[vertex_index] = true; - } - } - table - }; - - let old_to_new_label_map = { - let mut label_map = MapType::default(); - let mut next_label = 0; - for (i, keep) in vertex_keep_table.iter().enumerate() { - if *keep { - label_map.insert(i, next_label); - next_label += 1; - } - } - label_map - }; - - let relabeled_cells: Vec<_> = cell_indices - .iter() - .map(|&i| self.triangles[i].clone()) - .map(|mut cell| { - for index in &mut cell { - *index = *old_to_new_label_map - .get(index) - .expect("Index must be in map"); - } - cell - }) - .collect(); - - let relabeled_vertices: Vec<_> = vertex_keep_table - .iter() - .enumerate() - .filter_map(|(i, should_keep)| if *should_keep { Some(i) } else { None }) - .map(|index| self.vertices[index].clone()) - .collect(); - - Self { - vertices: relabeled_vertices, - triangles: relabeled_cells, - } - } - - /// Removes all triangles from the mesh that are completely outside of the given AABB and clamps the remaining triangles to the boundary - pub fn clamp_with_aabb(&mut self, aabb: &Aabb3d) { - // Find all triangles with at least one vertex inside of AABB - let triangles_to_keep = self - .triangles - .par_iter() - .enumerate() - .filter(|(_, tri)| tri.iter().any(|&v| aabb.contains_point(&self.vertices[v]))) - .map(|(i, _)| i) - .collect::>(); - // Remove all other triangles from mesh - let new_mesh = self.keep_tris(&triangles_to_keep); - // Replace current mesh - let TriMesh3d { - vertices, - triangles, - } = new_mesh; - self.vertices = vertices; - self.triangles = triangles; - - // Clamp remaining vertices to AABB - self.vertices.par_iter_mut().for_each(|v| { - let min = aabb.min(); - let max = aabb.max(); - v.x = v.x.clamp(min.x, max.x); - v.y = v.y.clamp(min.y, max.y); - v.z = v.z.clamp(min.z, max.z); - }) - } - /// Same as [`Self::vertex_normal_directions_inplace`] but assumes that the output is already zeroed fn vertex_normal_directions_inplace_assume_zeroed(&self, normal_directions: &mut [Vector3]) { assert_eq!(normal_directions.len(), self.vertices.len()); @@ -819,12 +905,28 @@ pub struct MeshWithData> { impl> Mesh3d for MeshWithData { type Cell = MeshT::Cell; + fn vertices(&self) -> &[Vector3] { self.mesh.vertices() } + + fn vertices_mut(&mut self) -> &mut [Vector3] { + self.mesh.vertices_mut() + } + fn cells(&self) -> &[MeshT::Cell] { self.mesh.cells() } + + fn from_vertices_and_connectivity( + vertices: Vec>, + connectivity: Vec, + ) -> Self { + MeshWithData::new(MeshT::from_vertices_and_connectivity( + vertices, + connectivity, + )) + } } /// Returns an mesh data wrapper with a default mesh and without attached attributes @@ -997,7 +1099,7 @@ macro_rules! impl_into_vtk { #[cfg_attr(doc_cfg, doc(cfg(feature = "vtk_extras")))] impl<'a, R: Real> IntoVtkUnstructuredGridPiece for &std::borrow::Cow<'a, $name> { fn into_unstructured_grid(self) -> UnstructuredGridPiece { - vtk_helper::mesh_to_unstructured_grid(&self) + vtk_helper::mesh_to_unstructured_grid(self) } } From ad93f37d3d9d90d4563702b9a3bbc421cec14b97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20L=C3=B6schner?= Date: Wed, 13 Sep 2023 12:36:23 +0200 Subject: [PATCH 11/22] Implement mesh clamping for MeshWithData, implement keep_vertices --- splashsurf/src/reconstruction.rs | 37 +++++- splashsurf_lib/src/mesh.rs | 192 +++++++++++++++++++++++-------- 2 files changed, 178 insertions(+), 51 deletions(-) diff --git a/splashsurf/src/reconstruction.rs b/splashsurf/src/reconstruction.rs index 38b2b87..5ca3e26 100644 --- a/splashsurf/src/reconstruction.rs +++ b/splashsurf/src/reconstruction.rs @@ -210,7 +210,7 @@ pub struct ReconstructSubcommandArgs { ignore_case = true, require_equals = true )] - pub keep_vertices: Switch, + pub keep_verts: Switch, /// Whether to compute surface normals at the mesh vertices and write them to the output file #[arg( help_heading = ARGS_INTERP, @@ -283,6 +283,7 @@ pub struct ReconstructSubcommandArgs { require_equals = true )] pub output_raw_mesh: Switch, + /// Whether to try to convert triangles to quads if they meet quality criteria #[arg( help_heading = ARGS_INTERP, @@ -303,7 +304,7 @@ pub struct ReconstructSubcommandArgs { #[arg(help_heading = ARGS_INTERP, long, default_value = "135")] pub quad_max_interior_angle: f64, - /// Lower corner of the bounding-box for the surface mesh, mesh outside gets cut away (requires mesh-max to be specified) + /// Lower corner of the bounding-box for the surface mesh, triangles completely outside are removed (requires mesh-aabb-max to be specified) #[arg( help_heading = ARGS_POSTPROC, long, @@ -313,7 +314,7 @@ pub struct ReconstructSubcommandArgs { requires = "mesh_aabb_max", )] pub mesh_aabb_min: Option>, - /// Upper corner of the bounding-box for the surface mesh, mesh outside gets cut away (requires mesh-min to be specified) + /// Upper corner of the bounding-box for the surface mesh, triangles completely outside are removed (requires mesh-aabb-min to be specified) #[arg( help_heading = ARGS_POSTPROC, long, @@ -323,6 +324,16 @@ pub struct ReconstructSubcommandArgs { requires = "mesh_aabb_min", )] pub mesh_aabb_max: Option>, + /// Whether to clamp vertices outside of the specified mesh AABB to the AABB (only has an effect if mesh-aabb-min/max are specified) + #[arg( + help_heading = ARGS_POSTPROC, + long, + default_value = "off", + value_name = "off|on", + ignore_case = true, + require_equals = true + )] + pub mesh_aabb_clamp_verts: Switch, /// Optional filename for writing the point cloud representation of the intermediate density map to disk #[arg(help_heading = ARGS_DEBUG, long, value_parser = value_parser!(PathBuf))] @@ -456,6 +467,7 @@ mod arguments { pub output_raw_normals: bool, pub output_raw_mesh: bool, pub mesh_aabb: Option>, + pub mesh_aabb_clamp_vertices: bool, } /// All arguments that can be supplied to the surface reconstruction tool converted to useful types @@ -590,7 +602,7 @@ mod arguments { check_mesh: args.check_mesh.into_bool(), mesh_cleanup: args.mesh_cleanup.into_bool(), decimate_barnacles: args.decimate_barnacles.into_bool(), - keep_vertices: args.keep_vertices.into_bool(), + keep_vertices: args.keep_verts.into_bool(), compute_normals: args.normals.into_bool(), sph_normals: args.sph_normals.into_bool(), normals_smoothing_iters: args.normals_smoothing_iters, @@ -606,6 +618,7 @@ mod arguments { output_raw_normals: args.output_raw_normals.into_bool(), output_raw_mesh: args.output_raw_mesh.into_bool(), mesh_aabb, + mesh_aabb_clamp_vertices: args.mesh_aabb_clamp_verts.into_bool(), }; Ok(ReconstructionRunnerArgs { @@ -1308,6 +1321,22 @@ pub(crate) fn reconstruction_pipeline_generic( } } + // Remove and clamp cells outside of AABB + let mesh_with_data = if let Some(mesh_aabb) = &postprocessing.mesh_aabb { + profile!("clamp mesh to aabb"); + info!("Post-processing: Clamping mesh to AABB..."); + + mesh_with_data.par_clamp_with_aabb( + &mesh_aabb + .try_convert() + .ok_or_else(|| anyhow!("Failed to convert mesh AABB"))?, + postprocessing.mesh_aabb_clamp_vertices, + postprocessing.keep_vertices, + ) + } else { + mesh_with_data + }; + // Convert triangles to quads let (tri_mesh, tri_quad_mesh) = if postprocessing.generate_quads { info!("Post-processing: Convert triangles to quads..."); diff --git a/splashsurf_lib/src/mesh.rs b/splashsurf_lib/src/mesh.rs index 7d5b132..9de804d 100644 --- a/splashsurf_lib/src/mesh.rs +++ b/splashsurf_lib/src/mesh.rs @@ -310,20 +310,91 @@ where } /// Returns a new mesh containing only the specified cells and removes all unreferenced vertices - fn keep_cells(&self, cell_indices: &[usize]) -> Self { + fn keep_cells(&self, cell_indices: &[usize], keep_vertices: bool) -> Self { + if keep_vertices { + keep_cells_impl(self, cell_indices, &[]) + } else { + let vertex_keep_table = vertex_keep_table(self, cell_indices); + keep_cells_impl(self, cell_indices, &vertex_keep_table) + } + } + + /// Removes all cells from the mesh that are completely outside of the given AABB and clamps the remaining cells to the boundary + fn par_clamp_with_aabb( + &self, + aabb: &Aabb3d, + clamp_vertices: bool, + keep_vertices: bool, + ) -> Self + where + Self::Cell: Sync, + { + // Find all triangles with at least one vertex inside of AABB let vertices = self.vertices(); - let cells = self.cells(); - - // Each entry is true if this vertex should be kept, false otherwise - let vertex_keep_table = { - let mut table = vec![false; vertices.len()]; - for cell in cell_indices.iter().copied().map(|c_i| &cells[c_i]) { - for &vertex_index in cell.vertices() { - table[vertex_index] = true; - } + let cells_to_keep = self + .cells() + .par_iter() + .enumerate() + .filter(|(_, cell)| { + cell.vertices() + .iter() + .copied() + .any(|v| aabb.contains_point(&vertices[v])) + }) + .map(|(i, _)| i) + .collect::>(); + // Remove all other cells from mesh + let mut new_mesh = self.keep_cells(&cells_to_keep, keep_vertices); + // Clamp remaining vertices to AABB + if clamp_vertices { + new_mesh.vertices_mut().par_iter_mut().for_each(|v| { + let min = aabb.min(); + let max = aabb.max(); + v.x = v.x.clamp(min.x, max.x); + v.y = v.y.clamp(min.y, max.y); + v.z = v.z.clamp(min.z, max.z); + }); + } + + new_mesh + } +} + +/// Returns the list of vertices that should remain in the given mesh after keeping only the given cells +fn vertex_keep_table>(mesh: &MeshT, cell_indices: &[usize]) -> Vec { + let vertices = mesh.vertices(); + let cells = mesh.cells(); + + // Each entry is true if this vertex should be kept, false otherwise + let vertex_keep_table = { + let mut table = vec![false; vertices.len()]; + for cell in cell_indices.iter().copied().map(|c_i| &cells[c_i]) { + for &vertex_index in cell.vertices() { + table[vertex_index] = true; } - table - }; + } + table + }; + + vertex_keep_table +} + +/// Returns a new mesh keeping only the given cells and vertices in the mesh +fn keep_cells_impl>( + mesh: &MeshT, + cell_indices: &[usize], + vertex_keep_table: &[bool], +) -> MeshT { + let vertices = mesh.vertices(); + let cells = mesh.cells(); + + if vertex_keep_table.is_empty() { + MeshT::from_vertices_and_connectivity( + mesh.vertices().to_vec(), + cell_indices.iter().map(|&i| &cells[i]).cloned().collect(), + ) + } else { + assert_eq!(mesh.vertices().len(), vertex_keep_table.len()); let old_to_new_label_map = { let mut label_map = MapType::default(); @@ -353,45 +424,13 @@ where let relabeled_vertices: Vec<_> = vertex_keep_table .iter() + .copied() .enumerate() - .filter_map(|(i, should_keep)| if *should_keep { Some(i) } else { None }) + .filter_map(|(i, should_keep)| if should_keep { Some(i) } else { None }) .map(|index| vertices[index].clone()) .collect(); - Self::from_vertices_and_connectivity(relabeled_vertices, relabeled_cells) - } - - /// Removes all cells from the mesh that are completely outside of the given AABB and clamps the remaining cells to the boundary - fn par_clamp_with_aabb(&self, aabb: &Aabb3d) -> Self - where - Self::Cell: Sync, - { - // Find all triangles with at least one vertex inside of AABB - let vertices = self.vertices(); - let cells_to_keep = self - .cells() - .par_iter() - .enumerate() - .filter(|(_, cell)| { - cell.vertices() - .iter() - .copied() - .any(|v| aabb.contains_point(&vertices[v])) - }) - .map(|(i, _)| i) - .collect::>(); - // Remove all other cells from mesh - let mut new_mesh = self.keep_cells(&cells_to_keep); - // Clamp remaining vertices to AABB - new_mesh.vertices_mut().par_iter_mut().for_each(|v| { - let min = aabb.min(); - let max = aabb.max(); - v.x = v.x.clamp(min.x, max.x); - v.y = v.y.clamp(min.y, max.y); - v.z = v.z.clamp(min.z, max.z); - }); - - new_mesh + MeshT::from_vertices_and_connectivity(relabeled_vertices, relabeled_cells) } } @@ -927,6 +966,45 @@ impl> Mesh3d for MeshWithData { connectivity, )) } + + /// Returns a new mesh containing only the specified cells and removes all unreferenced vertices and attributes + fn keep_cells(&self, cell_indices: &[usize], keep_all_vertices: bool) -> Self { + // Filter internal mesh + let mut new_mesh = if keep_all_vertices { + let mut new_mesh = keep_cells_impl(self, cell_indices, &[]); + new_mesh.point_attributes = self.point_attributes.clone(); + new_mesh + } else { + let vertex_keep_table = vertex_keep_table(self, cell_indices); + let mut new_mesh = keep_cells_impl(self, cell_indices, &vertex_keep_table); + + let vertex_indices = vertex_keep_table + .iter() + .copied() + .enumerate() + .filter_map(|(i, should_keep)| if should_keep { Some(i) } else { None }) + .collect::>(); + + // Filter the point attributes + new_mesh.point_attributes = self + .point_attributes + .iter() + .map(|attr| attr.keep_indices(&vertex_indices)) + .collect(); + + new_mesh + }; + + // Filter the cell attributes + let cell_attributes = self + .cell_attributes + .iter() + .map(|attr| attr.keep_indices(&cell_indices)) + .collect(); + new_mesh.cell_attributes = cell_attributes; + + new_mesh + } } /// Returns an mesh data wrapper with a default mesh and without attached attributes @@ -1026,6 +1104,26 @@ impl MeshAttribute { .with_data(vec3r_vec.iter().flatten().copied().collect::>()), } } + + /// Returns a new attribute keeping only the entries with the given index + fn keep_indices(&self, indices: &[usize]) -> Self { + let data = match &self.data { + AttributeData::ScalarU64(d) => { + AttributeData::ScalarU64(indices.iter().copied().map(|i| d[i].clone()).collect()) + } + AttributeData::ScalarReal(d) => { + AttributeData::ScalarReal(indices.iter().copied().map(|i| d[i].clone()).collect()) + } + AttributeData::Vector3Real(d) => { + AttributeData::Vector3Real(indices.iter().copied().map(|i| d[i].clone()).collect()) + } + }; + + Self { + name: self.name.clone(), + data, + } + } } impl AttributeData { From 2f5cfc574183c5a1023c1016f845400aa1998c66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20L=C3=B6schner?= Date: Wed, 13 Sep 2023 13:27:04 +0200 Subject: [PATCH 12/22] Update command line args --- splashsurf/src/reconstruction.rs | 112 ++++++++++++++++--------------- 1 file changed, 57 insertions(+), 55 deletions(-) diff --git a/splashsurf/src/reconstruction.rs b/splashsurf/src/reconstruction.rs index 5ca3e26..1a80ea5 100644 --- a/splashsurf/src/reconstruction.rs +++ b/splashsurf/src/reconstruction.rs @@ -21,7 +21,7 @@ static ARGS_BASIC: &str = "Numerical reconstruction parameters"; static ARGS_ADV: &str = "Advanced parameters"; static ARGS_OCTREE: &str = "Domain decomposition (octree or grid) parameters"; static ARGS_DEBUG: &str = "Debug options"; -static ARGS_INTERP: &str = "Interpolation"; +static ARGS_INTERP: &str = "Interpolation & normals"; static ARGS_POSTPROC: &str = "Postprocessing"; static ARGS_OTHER: &str = "Remaining options"; @@ -62,7 +62,7 @@ pub struct ReconstructSubcommandArgs { #[arg(help_heading = ARGS_BASIC, short = 't', long, default_value = "0.6")] pub surface_threshold: f64, - /// Whether to enable the use of double precision for all computations + /// Enable the use of double precision for all computations #[arg( help_heading = ARGS_ADV, short = 'd', @@ -94,7 +94,7 @@ pub struct ReconstructSubcommandArgs { )] pub particle_aabb_max: Option>, - /// Flag to enable multi-threading to process multiple input files in parallel + /// Enable multi-threading to process multiple input files in parallel #[arg( help_heading = ARGS_ADV, long = "mt-files", @@ -104,7 +104,7 @@ pub struct ReconstructSubcommandArgs { require_equals = true )] pub parallelize_over_files: Switch, - /// Flag to enable multi-threading for a single input file by processing chunks of particles in parallel + /// Enable multi-threading for a single input file by processing chunks of particles in parallel #[arg( help_heading = ARGS_ADV, long = "mt-particles", @@ -118,7 +118,7 @@ pub struct ReconstructSubcommandArgs { #[arg(help_heading = ARGS_ADV, long, short = 'n')] pub num_threads: Option, - /// Whether to enable spatial decomposition using a regular grid-based approach + /// Enable spatial decomposition using a regular grid-based approach #[arg( help_heading = ARGS_OCTREE, long, @@ -132,7 +132,7 @@ pub struct ReconstructSubcommandArgs { #[arg(help_heading = ARGS_OCTREE, long, default_value="64")] pub subdomain_cubes: u32, - /// Whether to enable spatial decomposition using an octree (faster) instead of a global approach + /// Enable spatial decomposition using an octree (faster) instead of a global approach #[arg( help_heading = ARGS_OCTREE, long, @@ -142,7 +142,7 @@ pub struct ReconstructSubcommandArgs { require_equals = true )] pub octree_decomposition: Switch, - /// Whether to enable stitching of the disconnected local meshes resulting from the reconstruction when spatial decomposition is enabled (slower, but without stitching meshes will not be closed) + /// Enable stitching of the disconnected local meshes resulting from the reconstruction when spatial decomposition is enabled (slower, but without stitching meshes will not be closed) #[arg( help_heading = ARGS_OCTREE, long, @@ -158,7 +158,7 @@ pub struct ReconstructSubcommandArgs { /// Safety factor applied to the kernel compact support radius when it's used as a margin to collect ghost particles in the leaf nodes when performing the spatial decomposition #[arg(help_heading = ARGS_OCTREE, long)] pub octree_ghost_margin_factor: Option, - /// Whether to compute particle densities in a global step before domain decomposition (slower) + /// Enable computing particle densities in a global step before domain decomposition (slower) #[arg( help_heading = ARGS_OCTREE, long, @@ -168,7 +168,7 @@ pub struct ReconstructSubcommandArgs { require_equals = true )] pub octree_global_density: Switch, - /// Whether to compute particle densities per subdomain but synchronize densities for ghost-particles (faster, recommended). + /// Enable computing particle densities per subdomain but synchronize densities for ghost-particles (faster, recommended). /// Note: if both this and global particle density computation is disabled the ghost particle margin has to be increased to at least 2.0 /// to compute correct density values for ghost particles. #[arg( @@ -181,17 +181,17 @@ pub struct ReconstructSubcommandArgs { )] pub octree_sync_local_density: Switch, - /// Whether to enable MC specific mesh decimation/simplification which removes bad quality triangles typically generated by MC + /// Enable omputing surface normals at the mesh vertices and write them to the output file #[arg( help_heading = ARGS_INTERP, long, - default_value = "on", + default_value = "off", value_name = "off|on", ignore_case = true, require_equals = true )] - pub mesh_cleanup: Switch, - /// Whether to enable decimation of some typical bad marching cubes triangle configurations (resulting in "barnacles" after Laplacian smoothing) + pub normals: Switch, + /// Enable computing the normals using SPH interpolation instead of using the area weighted triangle normals #[arg( help_heading = ARGS_INTERP, long, @@ -200,8 +200,11 @@ pub struct ReconstructSubcommandArgs { ignore_case = true, require_equals = true )] - pub decimate_barnacles: Switch, - /// Whether to keep vertices without connectivity during decimation (faster and helps with debugging) + pub sph_normals: Switch, + /// Number of smoothing iterations to run on the normal field if normal interpolation is enabled (disabled by default) + #[arg(help_heading = ARGS_INTERP, long)] + pub normals_smoothing_iters: Option, + /// Enable writing raw normals without smoothing to the output mesh if normal smoothing is enabled #[arg( help_heading = ARGS_INTERP, long, @@ -210,49 +213,47 @@ pub struct ReconstructSubcommandArgs { ignore_case = true, require_equals = true )] - pub keep_verts: Switch, - /// Whether to compute surface normals at the mesh vertices and write them to the output file + pub output_raw_normals: Switch, + /// List of point attribute field names from the input file that should be interpolated to the reconstructed surface. Currently this is only supported for VTK and VTU input files. + #[arg(help_heading = ARGS_INTERP, long)] + pub interpolate_attributes: Vec, + + /// Enable MC specific mesh decimation/simplification which removes bad quality triangles typically generated by MC #[arg( - help_heading = ARGS_INTERP, + help_heading = ARGS_POSTPROC, long, - default_value = "off", + default_value = "on", value_name = "off|on", ignore_case = true, require_equals = true )] - pub normals: Switch, - /// Whether to compute the normals using SPH interpolation (smoother and more true to actual fluid surface, but slower) instead of just using area weighted triangle normals + pub mesh_cleanup: Switch, + /// Enable decimation of some typical bad marching cubes triangle configurations (resulting in "barnacles" after Laplacian smoothing) #[arg( - help_heading = ARGS_INTERP, + help_heading = ARGS_POSTPROC, long, default_value = "off", value_name = "off|on", ignore_case = true, require_equals = true )] - pub sph_normals: Switch, - /// Number of smoothing iterations to run on the normal field if normal interpolation is enabled (disabled by default) - #[arg(help_heading = ARGS_INTERP, long)] - pub normals_smoothing_iters: Option, - /// Whether to write raw normals without smoothing to the output mesh if normal smoothing is enabled + pub decimate_barnacles: Switch, + /// Enable keeping vertices without connectivity during decimation instead of filtering them out (faster and helps with debugging) #[arg( - help_heading = ARGS_INTERP, + help_heading = ARGS_POSTPROC, long, default_value = "off", value_name = "off|on", ignore_case = true, require_equals = true )] - pub output_raw_normals: Switch, - /// List of point attribute field names from the input file that should be interpolated to the reconstructed surface. Currently this is only supported for VTK and VTU input files. - #[arg(help_heading = ARGS_INTERP, long)] - pub interpolate_attributes: Vec, + pub keep_verts: Switch, /// Number of smoothing iterations to run on the reconstructed mesh - #[arg(help_heading = ARGS_INTERP, long)] + #[arg(help_heading = ARGS_POSTPROC, long)] pub mesh_smoothing_iters: Option, - /// Whether to enable feature weights for mesh smoothing if mesh smoothing enabled. Preserves isolated particles even under strong smoothing. + /// Enable feature weights for mesh smoothing if mesh smoothing enabled. Preserves isolated particles even under strong smoothing. #[arg( - help_heading = ARGS_INTERP, + help_heading = ARGS_POSTPROC, long, default_value = "off", value_name = "off|on", @@ -261,11 +262,11 @@ pub struct ReconstructSubcommandArgs { )] pub mesh_smoothing_weights: Switch, /// Normalization value from weighted number of neighbors to mesh smoothing weights - #[arg(help_heading = ARGS_INTERP, long, default_value = "13.0")] + #[arg(help_heading = ARGS_POSTPROC, long, default_value = "13.0")] pub mesh_smoothing_weights_normalization: f64, - /// Whether to write the smoothing weights to the output mesh file + /// Enable writing the smoothing weights as a vertex attribute to the output mesh file #[arg( - help_heading = ARGS_INTERP, + help_heading = ARGS_POSTPROC, long, default_value = "off", value_name = "off|on", @@ -273,20 +274,10 @@ pub struct ReconstructSubcommandArgs { require_equals = true )] pub output_smoothing_weights: Switch, - /// Whether to write out the raw reconstructed mesh before applying any post-processing steps - #[arg( - help_heading = ARGS_INTERP, - long, - default_value = "off", - value_name = "off|on", - ignore_case = true, - require_equals = true - )] - pub output_raw_mesh: Switch, - /// Whether to try to convert triangles to quads if they meet quality criteria + /// Enable trying to convert triangles to quads if they meet quality criteria #[arg( - help_heading = ARGS_INTERP, + help_heading = ARGS_POSTPROC, long, default_value = "off", value_name = "off|on", @@ -295,13 +286,13 @@ pub struct ReconstructSubcommandArgs { )] pub generate_quads: Switch, /// Maximum allowed ratio of quad edge lengths to its diagonals to merge two triangles to a quad (inverse is used for minimum) - #[arg(help_heading = ARGS_INTERP, long, default_value = "1.75")] + #[arg(help_heading = ARGS_POSTPROC, long, default_value = "1.75")] pub quad_max_edge_diag_ratio: f64, /// Maximum allowed angle (in degrees) between triangle normals to merge them to a quad - #[arg(help_heading = ARGS_INTERP, long, default_value = "10")] + #[arg(help_heading = ARGS_POSTPROC, long, default_value = "10")] pub quad_max_normal_angle: f64, /// Maximum allowed vertex interior angle (in degrees) inside of a quad to merge two triangles to a quad - #[arg(help_heading = ARGS_INTERP, long, default_value = "135")] + #[arg(help_heading = ARGS_POSTPROC, long, default_value = "135")] pub quad_max_interior_angle: f64, /// Lower corner of the bounding-box for the surface mesh, triangles completely outside are removed (requires mesh-aabb-max to be specified) @@ -324,7 +315,7 @@ pub struct ReconstructSubcommandArgs { requires = "mesh_aabb_min", )] pub mesh_aabb_max: Option>, - /// Whether to clamp vertices outside of the specified mesh AABB to the AABB (only has an effect if mesh-aabb-min/max are specified) + /// Enable clamping of vertices outside of the specified mesh AABB to the AABB (only has an effect if mesh-aabb-min/max are specified) #[arg( help_heading = ARGS_POSTPROC, long, @@ -335,6 +326,17 @@ pub struct ReconstructSubcommandArgs { )] pub mesh_aabb_clamp_verts: Switch, + /// Enable writing the raw reconstructed mesh before applying any post-processing steps + #[arg( + help_heading = ARGS_POSTPROC, + long, + default_value = "off", + value_name = "off|on", + ignore_case = true, + require_equals = true + )] + pub output_raw_mesh: Switch, + /// Optional filename for writing the point cloud representation of the intermediate density map to disk #[arg(help_heading = ARGS_DEBUG, long, value_parser = value_parser!(PathBuf))] pub output_dm_points: Option, @@ -344,7 +346,7 @@ pub struct ReconstructSubcommandArgs { /// Optional filename for writing the octree used to partition the particles to disk #[arg(help_heading = ARGS_DEBUG, long, value_parser = value_parser!(PathBuf))] pub output_octree: Option, - /// Whether to check the final mesh for topological problems such as holes (note that when stitching is disabled this will lead to a lot of reported problems) + /// Enable checking the final mesh for topological problems such as holes (note that when stitching is disabled this will lead to a lot of reported problems) #[arg( help_heading = ARGS_DEBUG, long, From d31341b069990ec4ebb1d234ff870449d2758b06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20L=C3=B6schner?= Date: Wed, 13 Sep 2023 13:39:32 +0200 Subject: [PATCH 13/22] Implement functions to find non-manifold edges and vertices --- splashsurf_lib/src/mesh.rs | 378 ++++++++++++++++++++++++++++++++++--- 1 file changed, 349 insertions(+), 29 deletions(-) diff --git a/splashsurf_lib/src/mesh.rs b/splashsurf_lib/src/mesh.rs index 9de804d..c12e6ea 100644 --- a/splashsurf_lib/src/mesh.rs +++ b/splashsurf_lib/src/mesh.rs @@ -20,6 +20,7 @@ use bytemuck_derive::{Pod, Zeroable}; use nalgebra::{Unit, Vector3}; use rayon::prelude::*; use std::cell::RefCell; +use std::collections::BTreeSet; use std::fmt::Debug; use thread_local::ThreadLocal; #[cfg(feature = "vtk_extras")] @@ -643,6 +644,95 @@ impl TriangleCell { } } +pub struct MeshEdgeInformation { + /// Map from sorted edge to `(edge_idx, edge_count)` + edge_counts: MapType<[usize; 2], (usize, usize)>, + /// For each edge_idx: (edge, face_idx, local_edge_idx) + edge_info: Vec<([usize; 2], usize, usize)>, +} + +pub struct EdgeInformation { + /// The vertices of the edge + pub edge: [usize; 2], + /// The vertices of the edge in ascending order + pub edge_sorted: [usize; 2], + /// Total number of incident faces to the edge + pub incident_faces: usize, + /// One representative face that contains the edge with the given index + pub face: usize, + /// Local index of this edge in the representative face + pub local_edge_index: usize, +} + +impl MeshEdgeInformation { + /// Iterator over all edge information stored in this struct + pub fn iter<'a>(&'a self) -> impl Iterator + 'a { + self.edge_counts.iter().map(|(e, (edge_idx, count))| { + let info = &self.edge_info[*edge_idx]; + EdgeInformation { + edge: info.0, + edge_sorted: *e, + incident_faces: *count, + face: info.1, + local_edge_index: info.2, + } + }) + } + + /// Returns the number of boundary edges (i.e. edges with 1 incident face) + pub fn count_boundary_edges(&self) -> usize { + self.edge_counts + .values() + .filter(|(_, count)| *count == 1) + .count() + } + + /// Returns the number of non-manifold edges (i.e. edges with more than 2 incident face) + pub fn count_non_manifold_edges(&self) -> usize { + self.edge_counts + .values() + .filter(|(_, count)| *count > 2) + .count() + } + + /// Iterator over all boundary edges + pub fn boundary_edges<'a>(&'a self) -> impl Iterator + 'a { + self.edge_counts + .values() + .filter(|(_, count)| *count == 1) + .map(move |(edge_idx, _)| self.edge_info[*edge_idx].0) + } + + /// Iterator over all non-manifold edges + pub fn non_manifold_edges<'a>(&'a self) -> impl Iterator + 'a { + self.edge_counts + .values() + .filter(|(_, count)| *count > 2) + .map(move |(edge_idx, _)| self.edge_info[*edge_idx].0) + } +} + +pub struct MeshManifoldInformation { + /// List of all edges with only one incident face + pub boundary_edges: Vec<[usize; 2]>, + /// List of all non-manifold edges (edges with more than two incident faces) + pub non_manifold_edges: Vec<[usize; 2]>, + /// List of all non-manifold vertices (vertices with more than one fan of faces) + pub non_manifold_vertices: Vec, +} + +impl MeshManifoldInformation { + /// Returns whether the associated mesh is closed (has no boundary edges) + pub fn is_closed(&self) -> bool { + self.boundary_edges.is_empty() + } + + /// Returns whether the associated mesh is a 2-manifold (no non-manifold edges and no non-manifold vertices) + pub fn is_manifold(&self) -> bool { + self.non_manifold_edges.is_empty() && self.non_manifold_vertices.is_empty() + } +} + impl TriMesh3d { /// Returns a slice of all triangles of the mesh as `TriangleCell`s pub fn triangle_cells(&self) -> &[TriangleCell] { @@ -852,13 +942,8 @@ impl TriMesh3d { normals } - /// Returns all boundary edges of the mesh - /// - /// Returns edges which are only connected to exactly one triangle, along with the connected triangle - /// index and the local index of the edge within that triangle. - /// - /// Note that the output order is not necessarily deterministic due to the internal use of hashmaps. - pub fn find_boundary_edges(&self) -> Vec<([usize; 2], usize, usize)> { + /// Computes a helper struct with information about all edges in the mesh (i.e. number of incident triangles etc.) + pub fn compute_edge_information(&self) -> MeshEdgeInformation { let mut sorted_edges = Vec::new(); let mut edge_info = Vec::new(); @@ -896,6 +981,24 @@ impl TriMesh3d { .or_insert((edge_idx, 1)); } + MeshEdgeInformation { + edge_counts, + edge_info, + } + } + + /// Returns all boundary edges of the mesh + /// + /// Returns edges which are only connected to exactly one triangle, along with the connected triangle + /// index and the local index of the edge within that triangle. + /// + /// Note that the output order is not necessarily deterministic due to the internal use of hashmaps. + pub fn find_boundary_edges(&self) -> Vec<([usize; 2], usize, usize)> { + let MeshEdgeInformation { + edge_counts, + edge_info, + } = self.compute_edge_information(); + // Take only the faces which have a count of 1, which correspond to boundary faces edge_counts .into_iter() @@ -904,31 +1007,248 @@ impl TriMesh3d { .map(move |(edge_idx, _)| edge_info[edge_idx].clone()) .collect() } + + /// Returns all non-manifold vertices of this mesh + /// + /// A non-manifold vertex is generated by pinching two surface sheets together at that vertex + /// such that the vertex is incident to more than one fan of triangles. + /// + /// Note: This function assumes that all edges in the mesh are manifold edges! If there are non- + /// manifold edges, it is possible to connect two triangle fans using a third fan which is not + /// detected by this function. + pub fn find_non_manifold_vertices(&self) -> Vec { + let mut non_manifold_verts = Vec::new(); + let mut tri_fan = Vec::new(); + + let sort_edge = |edge: (usize, usize)| -> (usize, usize) { + if edge.0 > edge.1 { + (edge.1, edge.0) + } else { + edge + } + }; + + let is_fan_triangle = + |vert: usize, next_tri: &TriangleCell, current_fan: &[TriangleCell]| -> bool { + let mut is_fan_tri = false; + + // Check each edge of the tri against all triangles in the fan + 'edge_loop: for edge in next_tri.edges().map(sort_edge) { + // Only edges connected to the current vertex are relevant + if edge.0 == vert || edge.1 == vert { + // Check against all triangles of the current fan + for fan_tri in current_fan { + for fan_edge in fan_tri.edges().map(sort_edge) { + if edge == fan_edge { + // Triangle is part of the current fan + is_fan_tri = true; + break 'edge_loop; + } + } + } + } + } + + is_fan_tri + }; + + let tris = self.triangle_cells(); + let vert_face_connectivity = self.vertex_cell_connectivity(); + for vert in 0..self.vertices.len() { + let mut remaining_faces = vert_face_connectivity[vert] + .iter() + .cloned() + .collect::>(); + + if remaining_faces.len() > 1 { + // Pick an arbitrary first face of the fan + tri_fan.push(tris[remaining_faces.pop_first().unwrap()]); + // Try to match all other faces + while !remaining_faces.is_empty() { + let mut found_next = None; + + // Check all remaining faces against the current fan + for &next_candidate in &remaining_faces { + if is_fan_triangle(vert, &tris[next_candidate], &tri_fan) { + found_next = Some(next_candidate); + break; + } + } + + if let Some(next) = found_next { + // New fan triangle found + tri_fan.push(tris[next]); + remaining_faces.remove(&next); + } else { + // None of the remaining faces are part of the fan + break; + } + } + + if !remaining_faces.is_empty() { + // At least one triangle is not part of the current fan + // -> Non-manifold vertex was found + non_manifold_verts.push(vert); + } + + tri_fan.clear(); + } + } + + non_manifold_verts + } + + /// Returns a struct with lists of all boundary edges, non-manifold edges and non-manifold vertices + /// + /// Note that the output order is not necessarily deterministic due to the internal use of hashmaps. + pub fn compute_manifold_information(&self) -> MeshManifoldInformation { + let edges = self.compute_edge_information(); + let boundary_edges = edges.boundary_edges().collect(); + let non_manifold_edges = edges.non_manifold_edges().collect(); + + let non_manifold_vertices = self.find_non_manifold_vertices(); + + MeshManifoldInformation { + boundary_edges, + non_manifold_edges, + non_manifold_vertices, + } + } } -#[test] -fn test_find_boundary() { - // TODO: Needs a test with a real mesh - let mesh = TriMesh3d:: { - vertices: vec![ - Vector3::new_random(), - Vector3::new_random(), - Vector3::new_random(), - ], - triangles: vec![[0, 1, 2]], - }; +#[cfg(test)] +mod tri_mesh_tests { + use super::*; + + fn mesh_one_tri() -> TriMesh3d { + TriMesh3d:: { + vertices: vec![ + Vector3::new_random(), + Vector3::new_random(), + Vector3::new_random(), + ], + triangles: vec![[0, 1, 2]], + } + } - let mut boundary = mesh.find_boundary_edges(); - boundary.sort_unstable(); - - assert_eq!( - boundary, - vec![ - ([0usize, 1usize], 0, 0), - ([1usize, 2usize], 0, 1), - ([2usize, 0usize], 0, 2), - ] - ); + fn mesh_non_manifold_edge() -> TriMesh3d { + TriMesh3d:: { + vertices: vec![ + Vector3::new(0.0, 0.0, 0.0), + Vector3::new(1.0, 0.0, 0.0), + Vector3::new(0.0, 1.0, 0.0), + Vector3::new(1.0, 1.0, 0.0), + Vector3::new(0.0, 0.0, 1.0), + ], + triangles: vec![[0, 1, 2], [1, 3, 2], [1, 2, 4]], + } + } + + fn mesh_non_manifold_edge_double() -> TriMesh3d { + TriMesh3d:: { + vertices: vec![ + Vector3::new(0.0, 0.0, 0.0), + Vector3::new(1.0, 0.0, 0.0), + Vector3::new(0.0, 1.0, 0.0), + Vector3::new(1.0, 1.0, 0.0), + Vector3::new(0.0, 0.0, 1.0), + ], + triangles: vec![[0, 1, 2], [1, 3, 2], [1, 2, 4], [4, 2, 1]], + } + } + + fn mesh_non_manifold_vertex() -> TriMesh3d { + TriMesh3d:: { + vertices: vec![ + Vector3::new(1.0, 0.0, 0.0), + Vector3::new(0.0, 1.0, 0.0), + Vector3::new(1.0, 1.0, 0.0), + Vector3::new(2.0, 1.0, 1.0), + Vector3::new(1.0, 2.0, 1.0), + ], + triangles: vec![[0, 2, 1], [2, 3, 4]], + } + } + + #[test] + fn test_tri_mesh_find_boundary() { + let mesh = mesh_one_tri(); + + let mut boundary = mesh.find_boundary_edges(); + boundary.sort_unstable(); + + assert_eq!( + boundary, + vec![ + ([0usize, 1usize], 0, 0), + ([1usize, 2usize], 0, 1), + ([2usize, 0usize], 0, 2), + ] + ); + } + + #[test] + fn test_tri_mesh_edge_info() { + let mesh = mesh_non_manifold_edge(); + let edges = mesh.compute_edge_information(); + + for ei in edges.iter() { + if ei.edge_sorted == [1, 2] { + assert_eq!(ei.incident_faces, 3); + } else { + assert_eq!(ei.incident_faces, 1); + } + } + + assert_eq!(edges.count_boundary_edges(), 6); + assert_eq!(edges.count_non_manifold_edges(), 1); + } + + #[test] + fn test_tri_mesh_non_manifold_vertex_info() { + let mesh = mesh_non_manifold_vertex(); + let non_manifold_vertes = mesh.find_non_manifold_vertices(); + assert_eq!(non_manifold_vertes, [2]); + } + + #[test] + fn test_tri_mesh_manifold_info() { + { + let mesh = mesh_one_tri(); + let info = mesh.compute_manifold_information(); + assert!(!info.is_closed()); + assert!(info.is_manifold()); + assert_eq!(info.non_manifold_edges.len(), 0); + assert_eq!(info.non_manifold_vertices.len(), 0); + } + + { + let mesh = mesh_non_manifold_edge(); + let info = mesh.compute_manifold_information(); + assert!(!info.is_closed()); + assert!(!info.is_manifold()); + assert_eq!(info.non_manifold_edges.len(), 1); + assert_eq!(info.non_manifold_vertices.len(), 0); + } + + { + let mesh = mesh_non_manifold_edge_double(); + let info = mesh.compute_manifold_information(); + assert!(!info.is_closed()); + assert!(!info.is_manifold()); + assert_eq!(info.non_manifold_edges.len(), 1); + assert_eq!(info.non_manifold_vertices.len(), 0); + } + + { + let mesh = mesh_non_manifold_vertex(); + let info = mesh.compute_manifold_information(); + assert!(!info.is_closed()); + assert!(!info.is_manifold()); + assert_eq!(info.non_manifold_edges.len(), 0); + assert_eq!(info.non_manifold_vertices.len(), 1); + } + } } /// Wrapper type for meshes with attached point or cell data From 9c580a352e6eb5c8589f436a1ea167ec3ceacc20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20L=C3=B6schner?= Date: Wed, 13 Sep 2023 15:24:55 +0200 Subject: [PATCH 14/22] Update check mesh function with non-manifold checks --- splashsurf/src/reconstruction.rs | 59 ++++++++++++-- splashsurf_lib/src/marching_cubes.rs | 78 ++++++++++++++++--- splashsurf_lib/src/mesh.rs | 38 --------- .../tests/integration_tests/test_full.rs | 9 ++- 4 files changed, 122 insertions(+), 62 deletions(-) diff --git a/splashsurf/src/reconstruction.rs b/splashsurf/src/reconstruction.rs index 1a80ea5..4673503 100644 --- a/splashsurf/src/reconstruction.rs +++ b/splashsurf/src/reconstruction.rs @@ -346,7 +346,7 @@ pub struct ReconstructSubcommandArgs { /// Optional filename for writing the octree used to partition the particles to disk #[arg(help_heading = ARGS_DEBUG, long, value_parser = value_parser!(PathBuf))] pub output_octree: Option, - /// Enable checking the final mesh for topological problems such as holes (note that when stitching is disabled this will lead to a lot of reported problems) + /// Enable checking the final mesh for holes and non-manifold edges and vertices #[arg( help_heading = ARGS_DEBUG, long, @@ -356,6 +356,36 @@ pub struct ReconstructSubcommandArgs { require_equals = true )] pub check_mesh: Switch, + /// Enable checking the final mesh for holes + #[arg( + help_heading = ARGS_DEBUG, + long, + default_value = "off", + value_name = "off|on", + ignore_case = true, + require_equals = true + )] + pub check_mesh_closed: Switch, + /// Enable checking the final mesh for non-manifold edges and vertices + #[arg( + help_heading = ARGS_DEBUG, + long, + default_value = "off", + value_name = "off|on", + ignore_case = true, + require_equals = true + )] + pub check_mesh_manifold: Switch, + /// Enable debug output for the check-mesh operations (has no effect if no other check-mesh option is enabled) + #[arg( + help_heading = ARGS_DEBUG, + long, + default_value = "off", + value_name = "off|on", + ignore_case = true, + require_equals = true + )] + pub check_mesh_debug: Switch, } #[derive(Copy, Clone, Debug, PartialEq, Eq, clap::ValueEnum)] @@ -450,7 +480,9 @@ mod arguments { use walkdir::WalkDir; pub struct ReconstructionRunnerPostprocessingArgs { - pub check_mesh: bool, + pub check_mesh_closed: bool, + pub check_mesh_manifold: bool, + pub check_mesh_debug: bool, pub mesh_cleanup: bool, pub decimate_barnacles: bool, pub keep_vertices: bool, @@ -601,7 +633,11 @@ mod arguments { } let postprocessing = ReconstructionRunnerPostprocessingArgs { - check_mesh: args.check_mesh.into_bool(), + check_mesh_closed: args.check_mesh.into_bool() + || args.check_mesh_closed.into_bool(), + check_mesh_manifold: args.check_mesh.into_bool() + || args.check_mesh_manifold.into_bool(), + check_mesh_debug: args.check_mesh_debug.into_bool(), mesh_cleanup: args.mesh_cleanup.into_bool(), decimate_barnacles: args.decimate_barnacles.into_bool(), keep_vertices: args.keep_verts.into_bool(), @@ -1455,11 +1491,18 @@ pub(crate) fn reconstruction_pipeline_generic( info!("Done."); } - if postprocessing.check_mesh { + if postprocessing.check_mesh_closed + || postprocessing.check_mesh_manifold + || postprocessing.check_mesh_debug + { if let Err(err) = match (&tri_mesh, &tri_quad_mesh) { - (Some(mesh), None) => { - splashsurf_lib::marching_cubes::check_mesh_consistency(grid, &mesh.mesh) - } + (Some(mesh), None) => splashsurf_lib::marching_cubes::check_mesh_consistency( + grid, + &mesh.mesh, + postprocessing.check_mesh_closed, + postprocessing.check_mesh_manifold, + postprocessing.check_mesh_debug, + ), (None, Some(_mesh)) => { info!("Checking for mesh consistency not implemented for quad mesh at the moment."); return Ok(()); @@ -1468,7 +1511,7 @@ pub(crate) fn reconstruction_pipeline_generic( } { return Err(anyhow!("{}", err)); } else { - info!("Checked mesh for problems (holes, etc.), no problems were found."); + info!("Checked mesh for problems (holes: {}, non-manifold edges/vertices: {}), no problems were found.", postprocessing.check_mesh_closed, postprocessing.check_mesh_manifold); } } diff --git a/splashsurf_lib/src/marching_cubes.rs b/splashsurf_lib/src/marching_cubes.rs index 0c4c8d2..88a4c02 100644 --- a/splashsurf_lib/src/marching_cubes.rs +++ b/splashsurf_lib/src/marching_cubes.rs @@ -195,21 +195,39 @@ pub(crate) fn triangulate_density_map_to_surface_patch( }) } -/// Checks the consistency of the mesh (currently only checks for holes) and returns a string with debug information in case of problems +/// Checks the consistency of the mesh (currently checks for holes, non-manifold edges and vertices) and returns a string with debug information in case of problems pub fn check_mesh_consistency( grid: &UniformGrid, mesh: &TriMesh3d, + check_closed: bool, + check_manifold: bool, + debug: bool, ) -> Result<(), String> { profile!("check_mesh_consistency"); - let boundary_edges = mesh.find_boundary_edges(); - - if boundary_edges.is_empty() { + let edge_info = mesh.compute_edge_information(); + + let boundary_edges = edge_info + .iter() + .filter(|ei| ei.incident_faces == 1) + .map(|ei| (ei.edge, ei.face, ei.local_edge_index)) + .collect::>(); + let non_manifold_edges = edge_info + .iter() + .filter(|ei| ei.incident_faces > 2) + .map(|ei| (ei.edge, ei.face, ei.local_edge_index)) + .collect::>(); + + let non_manifold_vertices = mesh.find_non_manifold_vertices(); + + if (!check_closed || boundary_edges.is_empty()) + && (!check_manifold || (non_manifold_edges.is_empty() && non_manifold_vertices.is_empty())) + { return Ok(()); } - let mut error_string = String::new(); - error_string += &format!("Mesh is not closed. It has {} boundary edges (edges that are connected to only one triangle):", boundary_edges.len()); - for (edge, tri_idx, _) in boundary_edges { + let add_edge_errors = |error_string: &mut String, edge: ([usize; 2], usize, usize)| { + let (edge, tri_idx, _) = edge; + let v0 = mesh.vertices[edge[0]]; let v1 = mesh.vertices[edge[1]]; let center = (v0 + v1) / (R::one() + R::one()); @@ -221,13 +239,43 @@ pub fn check_mesh_consistency( let cell_center = grid.point_coordinates(&point_index) + &Vector3::repeat(grid.cell_size().times_f64(0.5)); - error_string += &format!("\n\tTriangle {}, boundary edge {:?} is located in cell with {:?} with center coordinates {:?} and edge length {}.", tri_idx, edge, cell_index, cell_center, grid.cell_size()); + *error_string += &format!("\n\tTriangle {}, boundary edge {:?} is located in cell with {:?} with center coordinates {:?} and edge length {}.", tri_idx, edge, cell_index, cell_center, grid.cell_size()); } else { - error_string += &format!( - "\n\tCannot get cell index for boundary edge {:?} of triangle {}", + *error_string += &format!( + "\n\tCannot get cell index for edge {:?} of triangle {}", edge, tri_idx ); } + }; + + let mut error_string = String::new(); + + if check_closed && !boundary_edges.is_empty() { + error_string += &format!("Mesh is not closed. It has {} boundary edges (edges that are connected to only one triangle).", boundary_edges.len()); + if debug { + for e in boundary_edges { + add_edge_errors(&mut error_string, e); + } + } + error_string += &format!("\n"); + } + + if check_manifold && !non_manifold_edges.is_empty() { + error_string += &format!("Mesh is not manifold. It has {} non-manifold edges (edges that are connected to more than twi triangles).", non_manifold_edges.len()); + if debug { + for e in non_manifold_edges { + add_edge_errors(&mut error_string, e); + } + } + error_string += &format!("\n"); + } + + if check_manifold && !non_manifold_vertices.is_empty() { + error_string += &format!("Mesh is not manifold. It has {} non-manifold vertices (vertices with more than one triangle fan).", non_manifold_vertices.len()); + if debug { + error_string += &format!("\n\t{:?}", non_manifold_vertices); + } + error_string += &format!("\n"); } Err(error_string) @@ -240,7 +288,13 @@ fn check_mesh_with_cell_data( marching_cubes_data: &MarchingCubesInput, mesh: &TriMesh3d, ) -> Result<(), String> { - let boundary_edges = mesh.find_boundary_edges(); + let edge_info = mesh.compute_edge_information(); + + let boundary_edges = edge_info + .iter() + .filter(|ei| ei.incident_faces == 1) + .map(|ei| (ei.edge, ei.face, ei.local_edge_index)) + .collect::>(); if boundary_edges.is_empty() { return Ok(()); @@ -263,7 +317,7 @@ fn check_mesh_with_cell_data( let cell_data = marching_cubes_data .cell_data .get(&grid.flatten_cell_index(&cell_index)) - .expect("Unabel to get cell data of cell"); + .expect("Unable to get cell data of cell"); error_string += &format!("\n\tTriangle {}, boundary edge {:?} is located in cell with {:?} with center coordinates {:?} and edge length {}. {:?}", tri_idx, edge, cell_index, cell_center, grid.cell_size(), cell_data); } else { diff --git a/splashsurf_lib/src/mesh.rs b/splashsurf_lib/src/mesh.rs index c12e6ea..28fb19e 100644 --- a/splashsurf_lib/src/mesh.rs +++ b/splashsurf_lib/src/mesh.rs @@ -987,27 +987,6 @@ impl TriMesh3d { } } - /// Returns all boundary edges of the mesh - /// - /// Returns edges which are only connected to exactly one triangle, along with the connected triangle - /// index and the local index of the edge within that triangle. - /// - /// Note that the output order is not necessarily deterministic due to the internal use of hashmaps. - pub fn find_boundary_edges(&self) -> Vec<([usize; 2], usize, usize)> { - let MeshEdgeInformation { - edge_counts, - edge_info, - } = self.compute_edge_information(); - - // Take only the faces which have a count of 1, which correspond to boundary faces - edge_counts - .into_iter() - .map(|(_edge, value)| value) - .filter(|&(_, count)| count == 1) - .map(move |(edge_idx, _)| edge_info[edge_idx].clone()) - .collect() - } - /// Returns all non-manifold vertices of this mesh /// /// A non-manifold vertex is generated by pinching two surface sheets together at that vertex @@ -1170,23 +1149,6 @@ mod tri_mesh_tests { } } - #[test] - fn test_tri_mesh_find_boundary() { - let mesh = mesh_one_tri(); - - let mut boundary = mesh.find_boundary_edges(); - boundary.sort_unstable(); - - assert_eq!( - boundary, - vec![ - ([0usize, 1usize], 0, 0), - ([1usize, 2usize], 0, 1), - ([2usize, 0usize], 0, 2), - ] - ); - } - #[test] fn test_tri_mesh_edge_info() { let mesh = mesh_non_manifold_edge(); diff --git a/splashsurf_lib/tests/integration_tests/test_full.rs b/splashsurf_lib/tests/integration_tests/test_full.rs index c96f3a8..c46d9b1 100644 --- a/splashsurf_lib/tests/integration_tests/test_full.rs +++ b/splashsurf_lib/tests/integration_tests/test_full.rs @@ -133,10 +133,11 @@ macro_rules! generate_test { let reconstruction = reconstruct_surface::(particle_positions.as_slice(), ¶meters).unwrap(); - write_vtk(reconstruction.mesh(), output_file, "mesh").unwrap(); + write_vtk(reconstruction.mesh(), &output_file, "mesh").unwrap(); println!( - "Reconstructed mesh from particle file '{}' with {} particles has {} triangles.", + "Reconstructed mesh '{}' from particle file '{}' with {} particles has {} triangles.", + output_file.display(), $input_file, particle_positions.len(), reconstruction.mesh().triangles.len() @@ -158,10 +159,10 @@ macro_rules! generate_test { if test_for_boundary(¶meters) { // Ensure that the mesh does not have a boundary - if let Err(e) = check_mesh_consistency(reconstruction.grid(), reconstruction.mesh()) + if let Err(e) = check_mesh_consistency(reconstruction.grid(), reconstruction.mesh(), true, true, true) { eprintln!("{}", e); - panic!("Mesh contains boundary edges"); + panic!("Mesh contains topological/manifold errors"); } } } From d7f2064fce4054828085914bca68272637d27572 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20L=C3=B6schner?= Date: Wed, 13 Sep 2023 16:28:32 +0200 Subject: [PATCH 15/22] Update changelog --- CHANGELOG.md | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e4fdf73..899e262 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,18 @@ The following changes are present in the `main` branch of the repository and are not yet part of a release: - - CLI: Make new spatial decomposition available in CLI with `--subdomain-grid=on` - Lib: Implement new spatial decomposition based on a regular grid of subdomains, subdomains are dense marching cubes grids - - Lib: Support for reading and writing PLY meshes + - CLI: Make new spatial decomposition available in CLI with `--subdomain-grid=on` + - Lib: Implement weighted Laplacian smoothing to remove bumps from surfaces according to paper "Weighted Laplacian Smoothing for Surface Reconstruction of Particle-based Fluids" (Löschner, Böttcher, Jeske, Bender 2023) + - CLI: Add arguments to enable and control weighted Laplacian smoothing `--mesh-smoothing-iters=...`, `--mesh-smoothing-weights=on` etc. + - Lib: Implement `marching_cubes_cleanup` function: a marching cubes "mesh cleanup" decimation inspired by "Mesh Displacement: An Improved Contouring Method for Trivariate Data" (Moore, Warren 1991) + - CLI: Add argument to enable mesh cleanup: `--mesh-cleanup=on` + - Lib: Add functions to `TriMesh3d` to find non-manifold edges and vertices + - CLI: Add arguments to check if output meshes are manifold (no non-manifold edges and vertices): `--mesh-check-manifold=on`, `--mesh-check-closed=on` + - Lib: Support for mixed triangle and quad meshes + - Lib: Implement `convert_tris_to_quads` function: greedily merge triangles to quads if they fulfill certain criteria (maximum angle in quad, "squareness" of the quad, angle between triangle normals) + - CLI: Add arguments to enable and control triangle to quad conversion with `--generate-quads=on` etc. + - Lib: Support for reading and writing PLY meshes (`MixedTriQuadMesh3d`) - CLI: Support for filtering input particles using an AABB with `--particle-aabb-min`/`--particle-aabb-max` - CLI: Support for clamping the triangle mesh using an AABB with `--mesh-aabb-min`/`--mesh-aabb-max` @@ -69,9 +78,9 @@ The following changes are present in the `main` branch of the repository and are This release fixes a couple of bugs that may lead to inconsistent surface reconstructions when using domain decomposition (i.e. reconstructions with artificial bumps exactly at the subdomain boundaries, especially on flat surfaces). Currently there are no other known bugs and the domain decomposed approach appears to be really fast and robust. -In addition the CLI now reports more detailed timing statistics for multi-threaded reconstructions. +In addition, the CLI now reports more detailed timing statistics for multi-threaded reconstructions. -Otherwise this release contains just some small changes to command line parameters. +Otherwise, this release contains just some small changes to command line parameters. - Lib: Add a `ParticleDensityComputationStrategy` enum to the `SpatialDecompositionParameters` struct. In order for domain decomposition to work consistently, the per particle densities have to be evaluated to a consistent value between domains. This is especially important for the ghost particles. Previously, this resulted inconsistent density values on boundaries if the ghost particle margin was not at least 2x the compact support radius (as this ensures that the inner ghost particles actually have the correct density). This option is now still available as the `IndependentSubdomains` strategy. The preferred way, that avoids the 2x ghost particle margin is the `SynchronizeSubdomains` where the density values of the particles in the subdomains are first collected into a global storage. This can be faster as the previous method as this avoids having to collect a double-width ghost particle layer. In addition there is the "playing it safe" option, the `Global` strategy, where the particle densities are computed in a completely global step before any domain decomposition. This approach however is *really* slow for large quantities of particles. For more information, read the documentation on the `ParticleDensityComputationStrategy` enum. - Lib: Fix bug where the workspace storage was not cleared correctly leading to inconsistent results depending on the sequence of processed subdomains @@ -91,7 +100,7 @@ Otherwise this release contains just some small changes to command line paramete The biggest new feature is a domain decomposed approach for the surface reconstruction by performing a spatial decomposition of the particle set with an octree. The resulting local patches can then be processed in parallel (leaving a single layer of boundary cells per patch untriangulated to avoid incompatible boundaries). -Afterwards, a stitching procedure walks the octree back upwards and merges the octree leaves by averaging density values on the boundaries. +Afterward, a stitching procedure walks the octree back upwards and merges the octree leaves by averaging density values on the boundaries. As the library uses task based parallelism, a task for stitching can be enqueued as soon as all children of an octree node are processed. Depending on the number of available threads and the particle data, this approach results in a speedup of 4-10x in comparison to the global parallel approach in selected benchmarks. At the moment, this domain decomposition approach is only available when allowing to parallelize over particles using the `--mt-particles` flag. From f06dbaf4aba9f36c29500a7673ccd59ffb5ef000 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20L=C3=B6schner?= Date: Wed, 13 Sep 2023 16:33:31 +0200 Subject: [PATCH 16/22] Update docs --- splashsurf_lib/src/density_map.rs | 2 +- splashsurf_lib/src/lib.rs | 2 +- splashsurf_lib/src/marching_cubes.rs | 2 +- splashsurf_lib/src/mesh.rs | 4 ++-- splashsurf_lib/src/octree.rs | 2 +- splashsurf_lib/src/postprocessing.rs | 3 ++- 6 files changed, 8 insertions(+), 7 deletions(-) diff --git a/splashsurf_lib/src/density_map.rs b/splashsurf_lib/src/density_map.rs index b136b07..9aebac8 100644 --- a/splashsurf_lib/src/density_map.rs +++ b/splashsurf_lib/src/density_map.rs @@ -13,7 +13,7 @@ //! "flat point indices". These are computed from the background grid point coordinates `(i,j,k)` //! analogous to multidimensional array index flattening. That means for a grid with dimensions //! `[n_x, n_y, n_z]`, the flat point index is given by the expression `i*n_x + j*n_y + k*n_z`. -//! For these point index operations, the [`UniformGrid`](crate::UniformGrid) is used. +//! For these point index operations, the [`UniformGrid`] is used. //! //! Note that all density mapping functions always use the global background grid for flat point //! indices, even if the density map is only generated for a smaller subdomain. diff --git a/splashsurf_lib/src/lib.rs b/splashsurf_lib/src/lib.rs index 785bd3e..393294b 100644 --- a/splashsurf_lib/src/lib.rs +++ b/splashsurf_lib/src/lib.rs @@ -9,7 +9,7 @@ //! The following features are all non-default features to reduce the amount of additional dependencies. //! //! - **`vtk_extras`**: Enables helper functions and trait implementations to export meshes using [`vtkio`](https://github.com/elrnv/vtkio). -//! In particular it adds `From` impls for the [mesh](crate::mesh) types used by this crate to convert them to +//! In particular it adds `From` impls for the [mesh] types used by this crate to convert them to //! [`vtkio::model::UnstructuredGridPiece`](https://docs.rs/vtkio/0.6.*/vtkio/model/struct.UnstructuredGridPiece.html) and [`vtkio::model::DataSet`](https://docs.rs/vtkio/0.6.*/vtkio/model/enum.DataSet.html) //! types. If the feature is enabled, The crate exposes its `vtkio` dependency as `splashsurflib::vtkio`. //! - **`io`**: Enables the [`io`] module, containing functions to load and store particle and mesh files diff --git a/splashsurf_lib/src/marching_cubes.rs b/splashsurf_lib/src/marching_cubes.rs index 88a4c02..81d0007 100644 --- a/splashsurf_lib/src/marching_cubes.rs +++ b/splashsurf_lib/src/marching_cubes.rs @@ -1,4 +1,4 @@ -//! Triangulation of [`DensityMap`](crate::density_map::DensityMap)s using marching cubes +//! Triangulation of [`DensityMap`]s using marching cubes use crate::marching_cubes::narrow_band_extraction::{ construct_mc_input, construct_mc_input_with_stitching_data, diff --git a/splashsurf_lib/src/mesh.rs b/splashsurf_lib/src/mesh.rs index 28fb19e..97e0207 100644 --- a/splashsurf_lib/src/mesh.rs +++ b/splashsurf_lib/src/mesh.rs @@ -1355,7 +1355,7 @@ impl MeshAttribute { } } - /// Creates a new named mesh attribute with scalar values implementing the [`Real`](crate::Real) trait + /// Creates a new named mesh attribute with scalar values implementing the [`Real`] trait pub fn new_real_scalar>(name: S, data: impl Into>) -> Self { Self { name: name.into(), @@ -1363,7 +1363,7 @@ impl MeshAttribute { } } - /// Creates a new named mesh attribute with scalar values implementing the [`Real`](crate::Real) trait + /// Creates a new named mesh attribute with scalar values implementing the [`Real`] trait pub fn new_real_vector3>(name: S, data: impl Into>>) -> Self { Self { name: name.into(), diff --git a/splashsurf_lib/src/octree.rs b/splashsurf_lib/src/octree.rs index 1b81748..b8679cf 100644 --- a/splashsurf_lib/src/octree.rs +++ b/splashsurf_lib/src/octree.rs @@ -397,7 +397,7 @@ impl OctreeNode { &self.aabb } - /// Constructs a [`UniformGrid`](crate::UniformGrid) that represents the domain of this octree node + /// Constructs a [`UniformGrid`] that represents the domain of this octree node pub fn grid( &self, min: &Vector3, diff --git a/splashsurf_lib/src/postprocessing.rs b/splashsurf_lib/src/postprocessing.rs index 66d9709..65cf1ef 100644 --- a/splashsurf_lib/src/postprocessing.rs +++ b/splashsurf_lib/src/postprocessing.rs @@ -82,7 +82,8 @@ pub fn par_laplacian_smoothing_normals_inplace( /// Mesh simplification designed for marching cubes surfaces meshes inspired by the "Compact Contouring"/"Mesh displacement" approach by Doug Moore and Joe Warren /// -/// See ["Mesh Displacement: An Improved Contouring Method for Trivariate Data"](https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.49.5214&rep=rep1&type=pdf). +/// See Moore and Warren: ["Mesh Displacement: An Improved Contouring Method for Trivariate Data"](https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.49.5214&rep=rep1&type=pdf) (1991) +/// or Moore and Warren: "Compact Isocontours from Sampled Data" in "Graphics Gems III" (1992). pub fn marching_cubes_cleanup( mesh: &mut TriMesh3d, grid: &UniformCartesianCubeGrid3d, From 1dcecdd54eca399a51c61315e9336239ab87aca8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20L=C3=B6schner?= Date: Thu, 21 Sep 2023 17:07:23 +0200 Subject: [PATCH 17/22] cargo update --- Cargo.lock | 110 +++++++++++++++++++++-------------------------------- 1 file changed, 44 insertions(+), 66 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cd452f9..b983c51 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,9 +10,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aho-corasick" -version = "1.0.5" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c378d78423fdad8089616f827526ee33c19f2fddbd5de1629152c9593ba4783" +checksum = "ea5d730647d4fadd988536d06fecce94b7b4f2a7efdae548f1cf4b63205518ab" dependencies = [ "memchr 2.6.3", ] @@ -148,9 +148,9 @@ checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" [[package]] name = "bumpalo" -version = "3.13.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] name = "bytecount" @@ -172,7 +172,7 @@ checksum = "965ab7eb5f8f97d2a083c799f3a1b994fc397b2fe2da5d1da1626ce15a39f2b1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.37", ] [[package]] @@ -235,9 +235,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.30" +version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "defd4e7873dbddba6c7c91e199c7fcb946abc4a6a4ac3195400bcfb01b5de877" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" dependencies = [ "android-tzdata", "iana-time-zone", @@ -276,9 +276,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.2" +version = "4.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a13b88d2c62ff462f88e4a121f17a82c1af05693a2f192b5c38d14de73c19f6" +checksum = "b1d7b8d5ec32af0fadc644bf1fd509a688c2103b185644bb1e29d164e0703136" dependencies = [ "clap_builder", "clap_derive", @@ -286,9 +286,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.2" +version = "4.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bb9faaa7c2ef94b2743a21f5a29e6f0010dff4caa69ac8e9d6cf8b6fa74da08" +checksum = "5179bb514e4d7c2051749d8fcefa2ed6d06a9f4e6d69faf3805f5d80b8cf8d56" dependencies = [ "anstream", "anstyle", @@ -305,7 +305,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.37", ] [[package]] @@ -390,16 +390,6 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7059fff8937831a9ae6f0fe4d658ffabf58f2ca96aa9dec1c889f936f705f216" -[[package]] -name = "crossbeam-channel" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" -dependencies = [ - "cfg-if", - "crossbeam-utils", -] - [[package]] name = "crossbeam-deque" version = "0.8.3" @@ -581,9 +571,9 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" [[package]] name = "iana-time-zone" @@ -691,9 +681,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.147" +version = "0.2.148" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" [[package]] name = "libm" @@ -748,9 +738,9 @@ dependencies = [ [[package]] name = "matrixmultiply" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "090126dc04f95dc0d1c1c91f61bdd474b3930ca064c1edc8a849da2c6cbe1e77" +checksum = "7574c1cf36da4798ab73da5b215bbf444f50718207754cb522201d78d1cd0ff2" dependencies = [ "autocfg", "rawpointer", @@ -895,16 +885,6 @@ dependencies = [ "libm", ] -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi", - "libc", -] - [[package]] name = "number_prefix" version = "0.4.0" @@ -1049,9 +1029,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.66" +version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" dependencies = [ "unicode-ident", ] @@ -1134,9 +1114,9 @@ checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" [[package]] name = "rayon" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" +checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" dependencies = [ "either", "rayon-core", @@ -1144,14 +1124,12 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" +checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" dependencies = [ - "crossbeam-channel", "crossbeam-deque", "crossbeam-utils", - "num_cpus", ] [[package]] @@ -1214,9 +1192,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.13" +version = "0.38.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7db8590df6dfcd144d22afd1b83b36c21a18d7cbc1dc4bb5295a8712e9eb662" +checksum = "747c788e9ce8e92b12cd485c49ddf90723550b654b32508f979b71a7b1ecda4f" dependencies = [ "bitflags 2.4.0", "errno", @@ -1289,14 +1267,14 @@ checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.37", ] [[package]] name = "serde_json" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cc66a619ed80bf7a0f6b17dd063a84b88f6dea1813737cf469aef1d081142c2" +checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" dependencies = [ "itoa", "ryu", @@ -1333,9 +1311,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" +checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" [[package]] name = "spin" @@ -1425,9 +1403,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.32" +version = "2.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "239814284fd6f1a4ffe4ca893952cdd93c224b6a1571c9a9eadd670295c0c9e2" +checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" dependencies = [ "proc-macro2", "quote", @@ -1464,7 +1442,7 @@ checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.37", ] [[package]] @@ -1489,9 +1467,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "ultraviolet" @@ -1513,15 +1491,15 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-width" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" [[package]] name = "utf8parse" @@ -1591,7 +1569,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.37", "wasm-bindgen-shared", ] @@ -1613,7 +1591,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.37", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -1662,9 +1640,9 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" dependencies = [ "winapi", ] From 99c7f2935cafa1b029119fab9b56a17fbf761e27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20L=C3=B6schner?= Date: Thu, 21 Sep 2023 17:07:32 +0200 Subject: [PATCH 18/22] Small fixes --- splashsurf_lib/src/lib.rs | 2 +- splashsurf_lib/src/postprocessing.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/splashsurf_lib/src/lib.rs b/splashsurf_lib/src/lib.rs index 393294b..20ac58e 100644 --- a/splashsurf_lib/src/lib.rs +++ b/splashsurf_lib/src/lib.rs @@ -276,7 +276,7 @@ pub struct Parameters { } impl Parameters { - /// Tries to convert the parameters from one [Real] type to another [Real] type, returns None if conversion fails + /// Tries to convert the parameters from one [Real] type to another [Real] type, returns `None` if conversion fails pub fn try_convert(&self) -> Option> { Some(Parameters { particle_radius: self.particle_radius.try_convert()?, diff --git a/splashsurf_lib/src/postprocessing.rs b/splashsurf_lib/src/postprocessing.rs index 65cf1ef..2665d75 100644 --- a/splashsurf_lib/src/postprocessing.rs +++ b/splashsurf_lib/src/postprocessing.rs @@ -4,7 +4,7 @@ use crate::halfedge_mesh::{HalfEdgeTriMesh, IllegalHalfEdgeCollapse}; use crate::mesh::{Mesh3d, MixedTriQuadMesh3d, TriMesh3d, TriMesh3dExt, TriangleOrQuadCell}; use crate::topology::{Axis, DirectedAxis, Direction}; use crate::uniform_grid::UniformCartesianCubeGrid3d; -use crate::{new_map, profile, Index, MapType, Real, SetType}; +use crate::{profile, Index, MapType, Real, SetType}; use log::{info, warn}; use nalgebra::Vector3; use rayon::prelude::*; From f426a047daaeeecf8603e03e345b53c958353478 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20L=C3=B6schner?= Date: Mon, 25 Sep 2023 16:18:02 +0200 Subject: [PATCH 19/22] Disable mesh cleanup by default --- splashsurf/src/reconstruction.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/splashsurf/src/reconstruction.rs b/splashsurf/src/reconstruction.rs index 4673503..6b0ca4c 100644 --- a/splashsurf/src/reconstruction.rs +++ b/splashsurf/src/reconstruction.rs @@ -94,7 +94,7 @@ pub struct ReconstructSubcommandArgs { )] pub particle_aabb_max: Option>, - /// Enable multi-threading to process multiple input files in parallel + /// Enable multi-threading to process multiple input files in parallel (NOTE: Currently, the subdomain-grid domain decomposition approach and some post-processing functions including interpolation do not have sequential versions and therefore do not work well with this option enabled) #[arg( help_heading = ARGS_ADV, long = "mt-files", @@ -222,7 +222,7 @@ pub struct ReconstructSubcommandArgs { #[arg( help_heading = ARGS_POSTPROC, long, - default_value = "on", + default_value = "off", value_name = "off|on", ignore_case = true, require_equals = true From de0d1b48dc42896e28cbc352f0b536bee2877783 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20L=C3=B6schner?= Date: Mon, 25 Sep 2023 21:27:06 +0200 Subject: [PATCH 20/22] Update readme and changelog --- CHANGELOG.md | 8 +- CITATION.cff | 8 +- README.md | 173 ++++++++++++++++++++++++++++++++-------- example_smoothed.jpg | Bin 0 -> 162786 bytes example_unsmoothed.jpg | Bin 0 -> 188334 bytes splashsurf/README.md | 175 +++++++++++++++++++++++++++++++++-------- 6 files changed, 296 insertions(+), 68 deletions(-) create mode 100644 example_smoothed.jpg create mode 100644 example_unsmoothed.jpg diff --git a/CHANGELOG.md b/CHANGELOG.md index 899e262..1ff87ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,13 @@ The following changes are present in the `main` branch of the repository and are not yet part of a release: - - Lib: Implement new spatial decomposition based on a regular grid of subdomains, subdomains are dense marching cubes grids + - N/A + +## Version 0.10.0 + +This release implements ["Weighted Laplacian Smoothing for Surface Reconstruction of Particle-based Fluids" (Löschner, Böttcher, Jeske, Bender; 2023)](https://animation.rwth-aachen.de/publication/0583/), mesh cleanup based on ["Mesh Displacement: An Improved Contouring Method for Trivariate Data" (Moore, Warren; 1991)](https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.49.5214&rep=rep1&type=pdf) and a new, more efficient domain decomposition (see README.md for more details). + + - Lib: Implement new spatial decomposition based on a regular grid of subdomains (subdomains are dense marching cubes grids) - CLI: Make new spatial decomposition available in CLI with `--subdomain-grid=on` - Lib: Implement weighted Laplacian smoothing to remove bumps from surfaces according to paper "Weighted Laplacian Smoothing for Surface Reconstruction of Particle-based Fluids" (Löschner, Böttcher, Jeske, Bender 2023) - CLI: Add arguments to enable and control weighted Laplacian smoothing `--mesh-smoothing-iters=...`, `--mesh-smoothing-weights=on` etc. diff --git a/CITATION.cff b/CITATION.cff index 2cca56a..af22017 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -2,7 +2,7 @@ # Visit https://bit.ly/cffinit to generate yours today! cff-version: 1.2.0 -title: splashsurf +title: '"splashsurf" Surface Reconstruction Software' message: >- If you use this software in your work, please consider citing it using these metadata. @@ -12,10 +12,10 @@ authors: given-names: Fabian affiliation: RWTH Aachen University orcid: 'https://orcid.org/0000-0001-6818-2953' -url: 'https://www.floeschner.de/splashsurf' +url: 'https://splashsurf.physics-simulation.org' abstract: >- Splashsurf is a surface reconstruction tool and framework for reconstructing surfaces from particle data. license: MIT -version: 0.9.1 -date-released: '2023-04-19' +version: 0.10.0 +date-released: '2023-09-25' diff --git a/README.md b/README.md index 2abe987..8aea2dc 100644 --- a/README.md +++ b/README.md @@ -25,19 +25,27 @@ reconstructed from this particle data. The next image shows a reconstructed surf with a "smoothing length" of `2.2` times the particles radius and a cell size of `1.1` times the particle radius. The third image shows a finer reconstruction with a cell size of `0.45` times the particle radius. These surface meshes can then be fed into 3D rendering software such as [Blender](https://www.blender.org/) to generate beautiful water animations. -The result might look something like this (please excuse the lack of 3D rendering skills): +The result might look something like this:

Rendered water animation

+Note: This animation does not show the recently added smoothing features of the tool, for more recent rendering see [this video](https://youtu.be/2bYvaUXlBQs). + +--- + **Contents** - [The `splashsurf` CLI](#the-splashsurf-cli) - [Introduction](#introduction) + - [Domain decomposition](#domain-decomposition) + - [Octree-based decomposition](#octree-based-decomposition) + - [Subdomain grid-based decomposition](#subdomain-grid-based-decomposition) - [Notes](#notes) - [Installation](#installation) - [Usage](#usage) - [Recommended settings](#recommended-settings) + - [Weighted surface smoothing](#weighted-surface-smoothing) - [Benchmark example](#benchmark-example) - [Sequences of files](#sequences-of-files) - [Input file formats](#input-file-formats) @@ -53,34 +61,56 @@ The result might look something like this (please excuse the lack of 3D renderin - [The `convert` subcommand](#the-convert-subcommand) - [License](#license) + # The `splashsurf` CLI The following sections mainly focus on the CLI of `splashsurf`. For more information on the library, see the [corresponding readme](https://github.com/InteractiveComputerGraphics/splashsurf/blob/main/splashsurf_lib) in the `splashsurf_lib` subfolder or the [`splashsurf_lib` crate](https://crates.io/crates/splashsurf_lib) on crates.io. ## Introduction -This is a basic but high-performance implementation of a marching cubes based surface reconstruction for SPH fluid simulations (e.g performed with [SPlisHSPlasH](https://github.com/InteractiveComputerGraphics/SPlisHSPlasH)). +This is CLI to run a fast marching cubes based surface reconstruction for SPH fluid simulations (e.g. performed with [SPlisHSPlasH](https://github.com/InteractiveComputerGraphics/SPlisHSPlasH)). The output of this tool is the reconstructed triangle surface mesh of the fluid. At the moment it supports computing normals on the surface using SPH gradients and interpolating scalar and vector particle attributes to the surface. -No additional smoothing or decimation operations are currently implemented. -As input, it supports reading particle positions from `.vtk`, `.bgeo`, `.ply`, `.json` and binary `.xyz` files (i.e. files containing a binary dump of a particle position array). -In addition, required parameters are the kernel radius and particle radius (to compute the volume of particles) used for the original SPH simulation as well as the surface threshold. - -By default, a domain decomposition of the particle set is performed using octree-based subdivision. -The implementation first computes the density of each particle using the typical SPH approach with a cubic kernel. -This density is then evaluated or mapped onto a sparse grid using spatial hashing in the support radius of each particle. -This implies that memory is only allocated in areas where the fluid density is non-zero. This is in contrast to a naive approach where the marching cubes background grid is allocated for the whole domain. -The marching cubes reconstruction is performed only in the narrowband of grid cells where the density values cross the surface threshold. Cells completely in the interior of the fluid are skipped. For more details, please refer to the [readme of the library]((https://github.com/InteractiveComputerGraphics/splashsurf/blob/main/splashsurf_lib/README.md)). +To get rid of the typical bumps from SPH simulations, it supports a weighted Laplacian smoothing approach [detailed below](#weighted-surface-smoothing). +As input, it supports reading particle positions from `.vtk`/`.vtu`, `.bgeo`, `.ply`, `.json` and binary `.xyz` (i.e. files containing a binary dump of a particle position array) files. +Required parameters to perform a reconstruction are the kernel radius and particle radius (to compute the volume of particles) used for the original SPH simulation as well as the marching cubes resolution (a default iso-surface threshold is pre-configured). + +## Domain decomposition + +A naive dense marching cubes reconstruction allocating a full 3D array over the entire fulid domain quickly becomes infeasible for larger simulations. +Instead, one could use a global hashmap where only cubes that contain non-zero fluid density values are allocated. +This approach is used in `splashsurf` if domain decomposition is disabled completely. +However, the global hashmap approach does not lead to good cache locality and is not well suited for parallelization (even specialized parallel map implementations like [`dashmap`](https://github.com/xacrimon/dashmap) have their performance limitations). +To improve on this situation `splashsurf` currently implements two domain decomposition approaches. + +### Octree-based decomposition +The octree-based decomposition is currently the default approach if no other option is specified but will probably be replaced by the grid-based approach described below. +For the octree-based decomposition an octree is built over all particles with an automatically determined target number of particles per leaf node. +For each leaf node, a hashmap is used like outlined above. +As each hashmap is smaller, cache locality is improved and due to the decomposition, each thread can work on its own local hashmap. Finally, all surface patches are stitched together by walking the octree back up, resulting in a closed surface. +Downsides of this approach are that the octree construction starting from the root and stitching back towards the root limit the amount of paralleism during some stages. + +### Subdomain grid-based decomposition + +Since version 0.10.0, `splashsurf` implements a new domain decomposition approach called the "subdomain grid" approach, toggeled with the `--subdomain-grid=on` flag. +Here, the goal is to divide the fluid domain into subdomains with a fixed number of marching cubes cells, by default `64x64x64` cubes. +For each subdomain a dense 3D array is allocated for the marching cubes cells. +Of course, only subdomains that contain fluid particles are actually allocated. +For subdomains that contain only a very small number of fluid particles (less th 5% of the largest subdomain) a hashmap is used instead to not waste too much storage. +As most domains are dense however, the marching cubes triangulation per subdomain is very fast as it can make full use of cache locality and the entire procedure is trivially parallelizable. +For the stitching we ensure that we perform floating point operations in the same order at the subdomain boundaries (this can be ensured without synchronization). +If the field values on the subdomain boundaries are identical from both sides, the marching cubes triangulations will be topologically compatible and can be merged in a post-processing step that is also parallelizable. +Overall, this approach should almost always be faster than the previous octree-based aproach. + ## Notes -For small numbers of fluid particles (i.e. in the low thousands or less) the multithreaded implementation may have worse performance due to the task based parallelism and the additional overhead of domain decomposition and stitching. +For small numbers of fluid particles (i.e. in the low thousands or less) the domain decomposition implementation may have worse performance due to the task based parallelism and the additional overhead of domain decomposition and stitching. In this case, you can try to disable the domain decomposition. The reconstruction will then use a global approach that is parallelized using thread-local hashmaps. For larger quantities of particles the decomposition approach is expected to be always faster. Due to the use of hash maps and multi-threading (if enabled), the output of this implementation is not deterministic. -In the future, flags may be added to switch the internal data structures to use binary trees for debugging purposes. As shown below, the tool can handle the output of large simulations. However, it was not tested with a wide range of parameters and may not be totally robust against corner-cases or extreme parameters. @@ -103,6 +133,29 @@ Good settings for the surface reconstruction depend on the original simulation a - `surface-threshold`: a good value depends on the selected `particle-radius` and `smoothing-length` and can be used to counteract a fluid volume increase e.g. due to a larger particle radius. In combination with the other recommended values a threshold of `0.6` seemed to work well. - `cube-size` usually should not be chosen larger than `1.0` to avoid artifacts (e.g. single particles decaying into rhomboids), start with a value in the range of `0.75` to `0.5` and decrease/increase it if the result is too coarse or the reconstruction takes too long. +### Weighted surface smoothing +The CLI implements the paper ["Weighted Laplacian Smoothing for Surface Reconstruction of Particle-based Fluids" (Löschner, Böttcher, Jeske, Bender; 2023)](https://animation.rwth-aachen.de/publication/0583/) which proposes a fast smoothing approach to avoid typical bumpy surfaces while preventing loss of volume that typically occurs with simple smoothing methods. +The following images show a rendering of a typical surface reconstruction (on the right) with visible bumps due to the particles compared to the same surface reconstruction with weighted smoothing applied (on the left): + +

+Image of the original surface reconstruction without smoothing (bumpy & rough) Image of the surface reconstruction with weighted smoothing applied (nice & smooth) +

+ +You can see this rendering in motion in [this video](https://youtu.be/2bYvaUXlBQs). +To apply this smoothing, we recommend the following settings: + - `--mesh-smoothing-weights=on`: This enables the use of special weights during the smoothing process that preserve fluid details. For more information we refer to the [paper](https://animation.rwth-aachen.de/publication/0583/). + - `--mesh-smoothing-iters=25`: This enables smoothing of the output mesh. The individual iterations are relatively fast and 25 iterations appeared to strike a good balance between an initially bumpy surface and potential over-smoothing. + - `--mesh-cleanup=on`/`--decimate-barnacles=on`: On of the options should be used when applying smoothing, otherwise artifacts can appear on the surface (for more details see the paper). The `mesh-cleanup` flag enables a general purpose marching cubes mesh cleanup procedure that removes small sliver triangles everywhere on the mesh. The `decimate-barnacles` enables a more targeted decimation that only removes specific triangle configurations that are problematic for the smoothing. The former approach results in a "nicer" mesh overall but can be slower than the latter. + - `--normals-smoothing-iters=10`: If normals are being exported (with `--normals=on`), this results in an even smoother appearance during rendering. + +For the reconstruction parameters in conjunction with the weighted smoothing we recommend parameters close to the simulation parameters. +That means selecting the same particle radius as in the simulation, a corresponding smoothing length (e.g. for SPlisHSPlasH a value of `2.0`), a surface-threshold between `0.6` and `0.7` and a cube size usually between `0.5` and `1.0`. + +A full invocation of the tool might look like this: +``` +splashsurf reconstruct particles.vtk -r=0.025 -l=2.0 -c=0.5 -t=0.6 --subdomain-grid=on --mesh-cleanup=on --mesh-smoothing-weights=on --mesh-smoothing-iters=25 --normals=on --normals-smoothing-iters=10 +``` + ### Benchmark example For example: ``` @@ -179,9 +232,19 @@ Note that the tool collects all existing filenames as soon as the command is inv The first and last file of a sequences that should be processed can be specified with the `-s`/`--start-index` and/or `-e`/`--end-index` arguments. By specifying the flag `--mt-files=on`, several files can be processed in parallel. -If this is enabled, you should ideally also set `--mt-particles=off` as enabling both will probably degrade performance. +If this is enabled, you should also set `--mt-particles=off` as enabling both will probably degrade performance. The combination of `--mt-files=on` and `--mt-particles=off` can be faster if many files with only few particles have to be processed. +The number of threads can be influenced using the `--num-threads`/`-n` argument or the `RAYON_NUM_THREADS` environment variable + +**NOTE:** Currently, some functions do not have a sequential implementation and always parallelize over the particles or the mesh/domain. +This includes: + - the new "subdomain-grid" domain decomposition approach, as an alternative to the previous octree-based approach + - some post-processing functionality (interpolation of smoothing weights, interpolation of normals & other fluid attributes) + +Using the `--mt-particles=off` argument does not have an effect on these parts of the surface reconstruction. +For now, it is therefore recommended to not parallelize over multiple files if this functionality is used. + ## Input file formats ### VTK @@ -242,16 +305,18 @@ The file format is inferred from the extension of output filename. ### The `reconstruct` command ``` -splashsurf-reconstruct (v0.9.3) - Reconstruct a surface from particle data +splashsurf-reconstruct (v0.10.0) - Reconstruct a surface from particle data Usage: splashsurf reconstruct [OPTIONS] --particle-radius --smoothing-length --cube-size Options: + -q, --quiet Enable quiet mode (no output except for severe panic messages), overrides verbosity level + -v... Print more verbose output, use multiple "v"s for even more verbose output (-v, -vv) -h, --help Print help -V, --version Print version Input/output: - -o, --output-file Filename for writing the reconstructed surface to disk (default: "{original_filename}_surface.vtk") + -o, --output-file Filename for writing the reconstructed surface to disk (supported formats: VTK, PLY, OBJ, default: "{original_filename}_surface.vtk") --output-dir Optional base directory for all output files (default: current working directory) -s, --start-index Index of the first input file to process when processing a sequence of files (default: lowest index of the sequence) -e, --end-index Index of the last input file to process when processing a sequence of files (default: highest index of the sequence) @@ -268,39 +333,79 @@ Numerical reconstruction parameters: The cube edge length used for marching cubes in multiplies of the particle radius, corresponds to the cell size of the implicit background grid -t, --surface-threshold The iso-surface threshold for the density, i.e. the normalized value of the reconstructed density level that indicates the fluid surface (in multiplies of the rest density) [default: 0.6] - --domain-min + --particle-aabb-min Lower corner of the domain where surface reconstruction should be performed (requires domain-max to be specified) - --domain-max + --particle-aabb-max Upper corner of the domain where surface reconstruction should be performed (requires domain-min to be specified) Advanced parameters: - -d, --double-precision= Whether to enable the use of double precision for all computations [default: off] [possible values: off, on] - --mt-files= Flag to enable multi-threading to process multiple input files in parallel [default: off] [possible values: off, on] - --mt-particles= Flag to enable multi-threading for a single input file by processing chunks of particles in parallel [default: on] [possible values: off, on] + -d, --double-precision= Enable the use of double precision for all computations [default: off] [possible values: off, on] + --mt-files= Enable multi-threading to process multiple input files in parallel (NOTE: Currently, the subdomain-grid domain decomposition approach and some post-processing functions including interpolation do not have sequential versions and therefore do not work well with this option enabled) [default: off] [possible values: off, on] + --mt-particles= Enable multi-threading for a single input file by processing chunks of particles in parallel [default: on] [possible values: off, on] -n, --num-threads Set the number of threads for the worker thread pool -Octree (domain decomposition) parameters: +Domain decomposition (octree or grid) parameters: + --subdomain-grid= + Enable spatial decomposition using a regular grid-based approach [default: off] [possible values: off, on] + --subdomain-cubes + Each subdomain will be a cube consisting of this number of MC cube cells along each coordinate axis [default: 64] --octree-decomposition= - Whether to enable spatial decomposition using an octree (faster) instead of a global approach [default: on] [possible values: off, on] + Enable spatial decomposition using an octree (faster) instead of a global approach [default: on] [possible values: off, on] --octree-stitch-subdomains= - Whether to enable stitching of the disconnected local meshes resulting from the reconstruction when spatial decomposition is enabled (slower, but without stitching meshes will not be closed) [default: on] [possible values: off, on] + Enable stitching of the disconnected local meshes resulting from the reconstruction when spatial decomposition is enabled (slower, but without stitching meshes will not be closed) [default: on] [possible values: off, on] --octree-max-particles The maximum number of particles for leaf nodes of the octree, default is to compute it based on the number of threads and particles --octree-ghost-margin-factor Safety factor applied to the kernel compact support radius when it's used as a margin to collect ghost particles in the leaf nodes when performing the spatial decomposition --octree-global-density= - Whether to compute particle densities in a global step before domain decomposition (slower) [default: off] [possible values: off, on] + Enable computing particle densities in a global step before domain decomposition (slower) [default: off] [possible values: off, on] --octree-sync-local-density= - Whether to compute particle densities per subdomain but synchronize densities for ghost-particles (faster, recommended). Note: if both this and global particle density computation is disabled the ghost particle margin has to be increased to at least 2.0 to compute correct density values for ghost particles [default: on] [possible values: off, on] + Enable computing particle densities per subdomain but synchronize densities for ghost-particles (faster, recommended). Note: if both this and global particle density computation is disabled the ghost particle margin has to be increased to at least 2.0 to compute correct density values for ghost particles [default: on] [possible values: off, on] -Interpolation: +Interpolation & normals: --normals= - Whether to compute surface normals at the mesh vertices and write them to the output file [default: off] [possible values: off, on] + Enable omputing surface normals at the mesh vertices and write them to the output file [default: off] [possible values: off, on] --sph-normals= - Whether to compute the normals using SPH interpolation (smoother and more true to actual fluid surface, but slower) instead of just using area weighted triangle normals [default: on] [possible values: off, on] + Enable computing the normals using SPH interpolation instead of using the area weighted triangle normals [default: off] [possible values: off, on] + --normals-smoothing-iters + Number of smoothing iterations to run on the normal field if normal interpolation is enabled (disabled by default) + --output-raw-normals= + Enable writing raw normals without smoothing to the output mesh if normal smoothing is enabled [default: off] [possible values: off, on] --interpolate-attributes List of point attribute field names from the input file that should be interpolated to the reconstructed surface. Currently this is only supported for VTK and VTU input files +Postprocessing: + --mesh-cleanup= + Enable MC specific mesh decimation/simplification which removes bad quality triangles typically generated by MC [default: off] [possible values: off, on] + --decimate-barnacles= + Enable decimation of some typical bad marching cubes triangle configurations (resulting in "barnacles" after Laplacian smoothing) [default: off] [possible values: off, on] + --keep-verts= + Enable keeping vertices without connectivity during decimation instead of filtering them out (faster and helps with debugging) [default: off] [possible values: off, on] + --mesh-smoothing-iters + Number of smoothing iterations to run on the reconstructed mesh + --mesh-smoothing-weights= + Enable feature weights for mesh smoothing if mesh smoothing enabled. Preserves isolated particles even under strong smoothing [default: off] [possible values: off, on] + --mesh-smoothing-weights-normalization + Normalization value from weighted number of neighbors to mesh smoothing weights [default: 13.0] + --output-smoothing-weights= + Enable writing the smoothing weights as a vertex attribute to the output mesh file [default: off] [possible values: off, on] + --generate-quads= + Enable trying to convert triangles to quads if they meet quality criteria [default: off] [possible values: off, on] + --quad-max-edge-diag-ratio + Maximum allowed ratio of quad edge lengths to its diagonals to merge two triangles to a quad (inverse is used for minimum) [default: 1.75] + --quad-max-normal-angle + Maximum allowed angle (in degrees) between triangle normals to merge them to a quad [default: 10] + --quad-max-interior-angle + Maximum allowed vertex interior angle (in degrees) inside of a quad to merge two triangles to a quad [default: 135] + --mesh-aabb-min + Lower corner of the bounding-box for the surface mesh, triangles completely outside are removed (requires mesh-aabb-max to be specified) + --mesh-aabb-max + Upper corner of the bounding-box for the surface mesh, triangles completely outside are removed (requires mesh-aabb-min to be specified) + --mesh-aabb-clamp-verts= + Enable clamping of vertices outside of the specified mesh AABB to the AABB (only has an effect if mesh-aabb-min/max are specified) [default: off] [possible values: off, on] + --output-raw-mesh= + Enable writing the raw reconstructed mesh before applying any post-processing steps [default: off] [possible values: off, on] + Debug options: --output-dm-points Optional filename for writing the point cloud representation of the intermediate density map to disk @@ -309,7 +414,13 @@ Debug options: --output-octree Optional filename for writing the octree used to partition the particles to disk --check-mesh= - Whether to check the final mesh for topological problems such as holes (note that when stitching is disabled this will lead to a lot of reported problems) [default: off] [possible values: off, on] + Enable checking the final mesh for holes and non-manifold edges and vertices [default: off] [possible values: off, on] + --check-mesh-closed= + Enable checking the final mesh for holes [default: off] [possible values: off, on] + --check-mesh-manifold= + Enable checking the final mesh for non-manifold edges and vertices [default: off] [possible values: off, on] + --check-mesh-debug= + Enable debug output for the check-mesh operations (has no effect if no other check-mesh option is enabled) [default: off] [possible values: off, on] ``` ### The `convert` subcommand @@ -343,6 +454,6 @@ Options: # License -For license information of this project, see the LICENSE file. +For license information of this project, see the [LICENSE](LICENSE) file. The splashsurf logo is based on two graphics ([1](https://www.svgrepo.com/svg/295647/wave), [2](https://www.svgrepo.com/svg/295652/surfboard-surfboard)) published on SVG Repo under a CC0 ("No Rights Reserved") license. The dragon model shown in the images on this page are part of the ["Stanford 3D Scanning Repository"](https://graphics.stanford.edu/data/3Dscanrep/). diff --git a/example_smoothed.jpg b/example_smoothed.jpg new file mode 100644 index 0000000000000000000000000000000000000000..aa8411e20576f0af43f94e40a3c5fab09a86d97c GIT binary patch literal 162786 zcmeFYcU+Ur_AeSb(gZ020wPF}UJ{VdyY$|RgdRF1bWn;E>Am+7dXrv6qzaK5dJ&Zl z0xF<_h4aAs?sxBVKfiPC=l*^7@Cnb#%vx*Kd}qx(vt}lji~2F#Xs_p)OK^MUyUcmeo)gjfI_ zUbgm7T?M7TH2~ivS^hTF*VmWNSD4S;%Yk1|OiYYlK!{&Rh!^1CMfkfRVSc=B2-bgT zP_RSTc)>l8aCbN6D~&K~cW@n_Co*CqvD1@!rW}^uJiz!`QU&OTQE$-*7~6} zuZR!?kQcHM<+T=s+4I7L1jS&2!q&nMA^*s0dBFiUg1P*sJy&{c0XUuO?Hy$eQWd}pLd+EEoyGXM9GuZwr(*S(h7KVf=z>s!;*gs=W|6gN| zTR;dZ0N4ez6{_KG3%B?Ge`CEGMb};T2#x@%_5XWB=-GMxO}W6Cul*4Uv$^taNfrdm z$Ih1J?@SvS;TMI#~ zg}^}9{UwiZw@3QIyzFEh06zi{0KDjWx?$$LM)$$L_)FuYf~{%4;5V=k#J5QG2$ATz)r zW8>k41ZXRO7V$-TT+vwoO$992Yyr9!pn1Fi0|9#RD&P7q`qLHda7_boA3%HQ>&gRd zqXKAVhyS3h|AV%1^l}AwfN&6W-`3R)(2s5W7j1h*2VK#wF5ZA`*Xb%kC9!ie&;y?L zfe!;n8KeTz0BM7mLDnE|5FF$JLV|dKryG!i0OT~{X!7v<}(neZSdcD)|ntsdlU#nyW|1$f?d~h6>nqP1L6AD z3J{3Y1O%d-0)dFk{=y9?yDA5jE`UJ#09NYbAW(J=2*mCH=r;d9wEHUN{|C4KE6?Bf zU9N!SL3lX0xVSiYz&|`ZJbVHYLINP&BqqK=LUHrfEsC2I6qGa!w<)RUsVOLE@6pmT zGTyy=m-05teHNzs3`}>Ku9RQ_q6GK^WQ2rdOjHz9O#h$LWj~055Ssy~4+o0^giV2k zLxFWUc(tp6je`ZGD=4lL4zLe_4OmGCl-|JtT^Ihl6ez>N1xhcMK_oa>AZ&6Ra=?{; za>|5a;}YH#Lu>~4^|m%h(P85~BczEYXEv6J4ut_lG~_G*eZ@pWag_^D#0rD}rEM2u zv%WS`f(v|+xf3l}eZ@o0tbNT(&dL1OQ^L5j7zQg^^bW@gp|ujC`76;A6BCo-MMOl^ zGh9iBbi8^v`y0_bek_LO0qST0WbvLU5CfGnEBsX@Az+3~<8>80S5$OdsO;MS-;Uob z3D*V`{cp81Ys+ev>TeU}RDc)IC+kVofb~orJpYlvG&I*n5Y$~;5Gs2@AZ1jV`st3> zha?{6YYV1f|68L8qyKI`A^DXBp)d+;d`=^>Kid{S3xL{*u38c*bB}XyXcBj@^p+!; z9FP9TQeF}pEu1k<57LA$56PKbB(049Nsd?cTpM!J6eyDkl?7paeXKnC-6Pjds2>1; z5UV^V2ce;7s)Wj#v{1R~nk(Rq6_gZ|Oaa)qQWa(5qC>I!S#SxdCOq2%Q>PZ+UtxzW zx6wKZmlQHmPwg1m%8f;|KDAW6~pX!)$$~?{@im5M5l{}%nYaSqw~EG(t&LF6qF=;A3w#eQCMQ6arzax%J*p9bqdgU%&N?f?E+^ zE}Z8LX|aGSRvF!<#3jhesYoWIFTkVm?=*FV+!bhmkxVTffF;=~voYNVIF zM~IG8OtwZm!%O`@@6oDb=*|H~mz!iuYEWQxc6!!fnTBR4+=#jPR_UEIS!w5)L1C5N zYTAscVhNz^L0IYLuP1Gkq1ZUEJ?#F!(ts=sNJWi}fWeNERzQyot>6ZT=`GwIwdPj& z)m+%HE6JeT$(l0Uv#&N|rk;mkywU|2cI&ZPW(|*8Y5viZd~_%_LH^;!sf2|OPPvB6 z>bxnBK}fwfzt@IbIjzecBU|mkWZgr0hWw)2Oi}7)Dm}+V$q_9?D3TYleSkY+y$I1* zF-vs}O#;np-gM-ou}p5-qt_hR`eiu;RK|Eyfrf_sy0`xt2zU{ZVf_QL2lzyZtQho; z&yLw65z~OMfU?EY1QPg3UYFDWrVrIrq?yDgs;zG_Ro|a0M_vO1Tx~U|Hkzi*(*-~ z)8k;tl1<#jo1S=PI{j2}J-3thd4OVtW8;ru#zND*_j9EhgMIT(2<_hNIi+XysVuSl z`L$>F4k<(h-a$I+*j2ifOi^L;c?s!|jH8>F`iW2;T>CgotxUjKz*pzZA}Nbm@^{Hv@Z zqs|_U6Y#jIh4{D=R>JDq{n2KWL}+~dd=>)DWLi13qQ=`*1~I89$Xh+r1T(-Lg`xEJ zN7<*N0Uz2>0Y1(213smXsHk}KZ@Kd@(%CmJ#kENV0Fy~ksO-eelo-Hcpv6nTtbLtG z!)4yD;D;1r<3-?N5e!M*=|-2A-vZt*t-x=BIH%h*Zd=Dp7eZkM3;Z~oEl8K{Uh zT*YT3t?+sg#U1}{sM-4pBRaatk@C$4ZRT;%@AZ}iooV?J=Et)W7osU+t7=ny2}!3? zIIUSPSuBI&T0FIA5$c0A6Fc3e_R12U+P3W4r3-zg2A~UtM&@0n_Hzy{jgI;ximf*x ztPV!GrNxoe2L+&MTSZ|iJCiSKaqV1{R8%)y-)u_)v&LrBm?AK>lXZAs4cET+yKDHb zi>83t7sw=}qGD2B_#E`a?5m9m;!N#z$4d^!#@QTAf{EtcgLf7{i^RQTP}-z9qCN22#9ayct`+`Eb}NwJCSu%wcgMDbCO){?DB$;Wp{ z&$Ey>d7%c9b)7%+8OHaL$(8O((lKFkBn#e-7v>8PFNlT5F1d(w^{hmmD4PT_R;553 zm?9DLBeK|GctAHW%g9K`q~H!$HZm3+gcp5YBLKYy?7o`R{(Afn-uvZ#eOw!q1ylrtaCLVLE_cUojN>v|d#hz7)@yjPwTG`jlF?i|1ve&(QU z1=pvX6S;%F=Nk-?xlAjai#nEtI1Y7t-0!bR&*&+zEUj3@FcmC{>h?0iPfvDo>+5g& zQ!@Uf|7w`sh@Hb_Q>mxE36G7mQP_uy?Xs^-bZ0wmo84uK{Jbl%7sb67Zi4J90*w6Y z_5K&cMZiKN4DkK`Kz--C-=d?T-yAS+%RbA}oQ4sUnMzpCjZKy>jN~K}t*uf7+&_Hy}QL)NzP*K?DRhA*UWb-6fH@%eiNr(>NRMLd@HOLE{m{;YtU(UaT2VE zoGgt}(L+Cir*cF~Nz8OAIV${#vXN-xS{eVZnoJ z@|V0@lFfO_<*h;?33z{B(D{1pr<3y*k1pZu4b1XNE>%=MKfS?-5u@%pm8yv8z1s^5 zcv(aY+lv=+1Jy~ZNv2FCGcg}GUWt+=ly5EU?mT_s54KWuQ_EvyK33;QR6S&i7|GiZ)o)tl#(;f#W&J5MyE%NX%cVt ze7~QgJbA%drBi9wVTc@F5>FS-Kj{|B95FoqZf4$fVKWjxoL^Cs&r0f)gbJ%E%m~YY zC_9(tx*F@2mwW>e2QWW}d?>Up1uKtR714*QOIA zDV@7_o^W&*vGFsI)>+DD@$~${(|?&5+7}AC{oKZx=oTYGFxivkznljT=m08m9%d_M zm%oMt@K%<(x#xe^JPRReGoVu|ibK*!xRff3rTw`}>Jnshn-b0k-8En8ja6A@>*uMs z7qiAasnJS_Q4^MHI(p!wGMqy*IKP-Z9@i{KF%V;1-ZsC_aaWCYaYfHHYay%IY#~eC zmr<)-sxKyzRUZ&gp41$^Q-@M%F3spIjhg=8du|#Z(38Jr`J}OvEUP_J;mXjWJ`yl4 zA@}uK85j-ye~soh##f7TV5JJM_)T1bXpeq+7NmPYg<6n^nVF*S@J3*retxV_HJ#}G z1adAeaZ=T5(QA=1PpD=FL+6`iHuen{cutd?*)(Jw>E>#>TRFe^eut1|K}$SP{A5{v zv}lHnzN};d`YIwJiD_URXRBO4gS#f^dzonZvl* z6U>}kSsy^*5`uc*QIn2zAW+yK@43Z51MJ#OI00|h{yUNY;)q7Tc|ZqT^cGL#y4VqQ zq-vH=O-{3QtWf^%dPF^fL_Wb@h;%NGq%HFmA9`-@RDX-!nEp^fGzB&tum_LMvee!h zM_I)aB((N&4oASdCylVQywG~C#y?HqEzC1-DU+tzj<~+eU^oYiUV2V+x^Z{L#}uSG zIoAno8WfwEk9(aKJS`tS*rEyOJ^A{w=Sk`K8O{C&y+_Fv4PPbOl;7@d@130Xh00=s zWbVmg;~CoL6W$?-48tWNA|}R)hy*$U7{JUVL_nk@69OG0# zQpr~J?8)s0e)-7;){3m|_fnSihM{SxE@6zkf?IYSEXxZ#8Y0BihTK)!L~2t#mi7ge zyoyDArG)trts3@@UR%6j$qFva%ck~jqy=R;(4gOlEzf|G9+M5y&0w+s5r>uY#~Paf zBp2@xwq;@Wc8+ z{aDyx{nu{?a)jhf%Iq@4h0rI@Q*p@7h49e_I~juU{he}i-e$bIdn@RmmzIyMCIwMM zhPkb{xF7rSu+5io>vnjHNKlb9G?lbQZ{ z)ZR&u<)6DR3%pByNNcJG+&B)J_`RNG^Zu#cnS>OP)Hii+Pj6`F+U|+=dfwszzG=qS zh5X+8XmvOvLyO_oua;%x^ow1-faf)nHaoSr(2hIXhbS7SFrY*3;NV3>;5{S43dNBH z=2u|04Z{z=S5!pq^6*LK^Wk--LTUL%S>Af*<#)cJ28IQo@t2PkJD2K zOJD0(+y`H!5+_uHgHOFur@yLg1XBb(En4qBk$jx--SSMzCh;2+zuCs0_0~wzpS|)I z9^Ca#=BJH|tja+eRPzDbC=Eu2-%a4@kNf4drvrT8`-`h%ah3{bga)ztp#+$2y4P(4 zv0$Zr6`lf<@73%p^G1eNn?ZmUJL>kU!7o4Se7tUwPS#tHz~8p+*_5fh^+h0DJGAiN zUSGVlElmfUimT%UK7!mO@8mRj9wG;wawjH5%LpC~X`W!F5l3%IY}I{nD(}y4hkL$V z0UwE_)jXoo(c89ZzGHhg{hQ}xJMm$V}(9oeS>yNt#z_CU`kis4K7M$!|!PBqPowX78!va$-_#jvk`-4aDY_ey!@V@(XU(h)h_k~8WRx&b! zO-hFH4W1F=U;&c~6T$jRao%w7N%HI`KCsdU z^knhm^j_z8#hR+2?SxJ$GIrrvO{KT9W^!bju_~$DYur7vr79_KQ@IZ2v`I~=;G33# zG&v`K2<8)Gr$s9^ombBLgC6h3pBx+nJ-zYld%k~z**bx3=eMW%d6Uae%+B)e*6i$_ zo;&}10#&{EaWL6ImLw9FyX2h7H6yMC>!v`?Vd*J9LD)5=uFeVdiuneWx}ke? zL?SbH(XQ+-N4Pc$)zT)q6MUBH@}(iW=YitCev>GL6A_a}%I_4)pKzq5IX4XodR7$| z`!~5~AbKDe|Fd#`>M2qL9Bohtg#?c^Rg{m{W2(yLtBuL4ABC~EvaqLgEGViXJAEW? zTH*8Xh|DRAbs-trrC*%vh4tBaV-7z z+aMP%At9Eiq^TlfTLC&EP#Zbzj1BfJcw3Fo?5#Q0j=pca9P<;i9s3RyMx+-e&eJ1a z8dE`+pq29@d$W^oBb{xy6Q4yIL`qWP;gf-+LCS`b;zL1gJ9CB+W8u+<+KBo`yj;55sS-K^$<9XXXf;Z|sUzJv&S^>(WKKJ4JQg)L+@S0#`*#II+;7v|?n4LI3WU z7^f@5L1vunq%{aFx4BPa_6mBtJL5u(JtrLs$MRhez9lb*X4~CmFk}p4IoQgHe z?C$uH`ElKRjX2;UDz~G6f5utT7fSA;9uU`S!0#Sgdhq4+I1IG%T7Lt+Yhn;X3@Njl zFbUlapvF%wUIqi((SrgcAyzHJQ$rzKT;-0l;nZwgnz;gd5y?uPEreTTqz`fMOmfGc zoy-l>bY&e-DmJ5D%BLq8Ox+~?u{`j-kV7(WC9m14d^t2{GEqr*AzfLkH0wrs2|inA z{;pwFw$t7nI@sq5Ev5IJuH!3|`irrUZU2jN%#-=X52u^&W5I8mhrG*9mif%!I~`?P zKi)+-yKxGfcddITme#*?XYmLa{Q7d^{7+`(mtRT24k4P4)hbR41>J-S(n-^``_q@2 zGQiK9sVl@4OZvmIfjxL2YS8C$!#?C}@aAXDF(vBa^T*4Sq_IvHO19N3_;^9SNM0ZK zfZqddt(Df|RTAo!AYL}b$l!XH`ijEy$h(uC#daP?f5XcMYD3R7q zpt49Jw2cXxTM%gRYYT3Es<@DQ5quzz8?fJJ20W)Mu_$( zVY3l#EQ`I8@C)75=Fub!urlY-r$TGu5eY@fi0pxPG!?&o0Sh^o)vz!7i5-1W@NCG^21NZ=UUVLJus2fXRZ0KBRQU<%wLz*kFI;xY;I(1- zX_B^S+~@c%U)wIlp0GK0O^3)LuZ{68dmH|Mn;kI+vKjJvptPcwRMiwBVO*-|jW)_d zo1lWt#`_V7h2;ASrKwR$CF_{nh)l!O<5-5^<5PtA+V(fkX&R4D@p{6#71AafZr^(3 z7nl;D#+}QW{16k&M5Pn>hhX}$M}Q)(Eukdwqee8d!h4xF-?*vC`iY`sGYD-AfazHP z2VSl2xGWPWV{offuB~gysC77KUE#B6_`GFi=2PLhMBTeKq|b@BC3O}%Jhh}9cW=;5 zo42xemgk>&4`p&Ypx81}$GU^QPwc*aduB4iVPcud&xmd_jWM@jgmwKuBF(v@44p(p z9+XB7>kzwn)x7i=z%xl?idS4+Re#=2wfp^*l;l%7VJ)?Z&kTN;lC6+^)vZI?kwDEe zw0zSI6uCaElBOCaN#sUxyKHm`7X6!37F?1<9&&cw>W-EaQ))(R zV-jr`FXTSg%w9;@dQnyLW)i>P`d4T87N`Os(+l^!0(s?N1|G(z4q<^EMg> z$$Srcep|d0TvpatGBH}%*lWYyi5YRi!c1;|KJbldT>kjVNV?Dz%ocR4<`kWcU4Mhh z|0O;HM)fcxXK0%!89#DTTJ~hYs^2BFW%O8U%rH#t)OhB5PWALUYcwV>7p)(gSskK# z%Z0E>on@b<9MvG{!%3jUD#Rm{X9tmubTlz&Ii=UavMy?qHT9vOl0-K)o#3bX&{>y4 z9__2~dj<`uY4FpOIsSI|MA8`?^pa~p51!FmIslO6LPvaJs{xbua!tZI^I*n+9< z9}&zAM+^_zL$p^iMobQ#zhsxyKsdd4Ga%0yl%N^2-Kz1teEZ^-m!s9_=;AiZ3SOlD zOLSL0%A_ZpaM?3iqP3{yy>_j?db}yKv8e~=gDhv3xUixD8Jvo;)5_;Zj#X25#jO!K z<;y!mG0s*ZH}d%s=&MYmtT7GsgpTDrJ2uXhUv`vMYeju)@<%v7O!cfAXDXx_yHTQp zNF*40Gq?YQHgHdah#xU^*0rgjTz22yeGDyF$e3TMzt;?t&E$5D$vxRoUq;N;=9IVU z*N4kM!*zEbNVGTi6w#!XkIl@R3ex2-sZeIzOQ)l8TJqbf<$_u};bWG4xalY*=2Mqz zoWgEFjWbr$sL;f!L}YlPz>XXKbf>XvpnQ=^yhJ2+Un!TpQvXsYq2H~zJGpxsQl{L! zys!!m+l2E@l7|OQqbRGSnRE75?Y00qsd4)o4U$pv3*-db7fc%)e`@l1p%(eVXRnk$ z!i@62yp)o@t0iGSkzF<4`MEcBz^pqd6sCzl1uon=NK`tpeqdLH*A|TfW8fS0TpvDV z8D}jc&#&IxKd;M`w1}u+qu%gC>pG{qLNk5y7_duKJ`ono(KyRZ*;P}K#!PE?xv5b7 zG^B~#DXLiMeiiiY{q>@1Ej`8E070le9 zKK}adBPjvqa-@2tM26U%edpxQtu_H{trcYDV59JG)_%FqAydD7#scOwl-YC7P#<`nWh;fn8@}J(nQmtpf=9ZLs^>@8Un&Q8u|AxlSVBF_PF^=a`-Jj6dPc zks+N;7xV$Me8|m6OK8>hg(waHgs&~5B>^t_vF{&*5R$$UpsPu_gg!)mp$vGbs#nU$1l zs&yn^#C~imCYy$xNb@Mb-l@4(Z>MXWn?136q2DRArP(%zvr=7cTp?F3iYL7t zENfa0UrC>|7@xM@^de3c6SrEU#2HPUm#Z=FLoSkLuar~F@xU`9dgv_gMuD41n;X0< zvO%i3V6a{|SS*P;^b{qwT)wMhR;M&}vigp#uFYYhR%;xwBR6~g3F-*ZnTQv6gLXNX zxJd=S&Gw!w-1%0HZda4^`z19}uy+QT4jT==y(=ww>cDZkK}x|b)fY2yAEQ6!Ie|LU zwMCwPFE_b0e)ExRr(WZIBV`dKJ@b|a;-`JONnhK1XGZ3{KcDPJqAAEf=@Zv(E}CSc z*dugjlZ&RyzHzDZcrs6q#Y>X$Ry{~!=2pHbpVgp2#GO-TVAgT=eZtm1;>E0QS|N1V zl`ZYevz;q_JvlGCT!xXhG`od#Qedj{g?o%?KgLDu$(O-3o2hP59te3In5NV`4+>4N z=$Y3C?^0Lkh(TuV80E#lu=0QWk{a_rO-oc+d%xkLw2+0iH9)%aeOvVU7G@q1cb}mZ zV
?QkDzuCaE?oGmwO^O=?=6=jhTpj|WyC2Aw(cknzi6fX!Xyhj3THL~w2lCalc( z1di+jGUHVAvTSaT z=W3vk>~TFEiAs7a~WV>s(;i)x|)i@N+GK0k^BQCFUCqhmYi)U!}|xD`ZN zTn@?im*Q7+p6ZQaPjTP=4plr(NHk=W>R}tWW&Vz!ou;0vMW(F?{!m=jK+bC}I_0!r z8U}L=)!4%e6*=$X(?3}AN5UMzRDvL7s~H8=D3~m)%8=@{o^b`$k^Rk`pKc;K7?F3b z=Ju8jvgt4i`FI6F@KmH&DXYyJt{ko$$3vOuF+U=E$~W3Bz!CNQa^qAshIcPIW`ldj z6qQ|>s^JjqvL^|%;HZfgjNDK+RW-UP@q@Zudic<&M&ld8A+fUOrIzpcTKx^*W)Z#{ z^$rLddb`R{)n>2}d%V|Ng*BzJLCb4@3ng<|xt4GTmDDnalt10iE3-$XH5WThCmB>*|JqfsA z&?zv+IYzDF*%g%QM(mg>&u28saziv}40!gPPT3uMoijobD5|$}QTL=27T^`jjm&X( zNJ1jXZ~2qpFAFPpDcR97*)E#te*QZ8_|}Hi(o6K?aFxdQmXVdc(e1Ch+w@2V|5BZk(?GM)1s(`UgI}A= zIA_P9L0+Bxd1Va=_E;w~XPfyek2%fqVOzpjgXgsoJLM5Arupym-$qi<$`ConJ$iGd ztD4%`@;MKSt3S|bz<}4#xMNE*@65QfyFiQI*XexRgQvx&P%}z_N|@d(A0pS3JQ0Rz z7uqb_yj?EavX}v{<9fc0f|M)d6gA-&#lvan6$oR2-Oo1+AI|%8dNA=~{;p}~=T2P> zMLzyV`fQ@E(4EPh-e+a-k;$hr{K_a-F-jAf?Rm^YTCMTnsM~?&F+A~31r|lHHkLVO zU$WWn)5S6^do}UgjDt8-O2ewD`tw=aWlohMvxE^bw05Xt;a2P^Jg1>WGedgP79(O3 zU#=mJk~tZ@ZGXwmgx*U~^A@zI33x>}KT0)#Bp-ph(QZC-#5|2Gb?k2~Dl}k~^X3>G zsxfLv@veS;L}=M+t|C62`gZkZ4W1USX{yKFp5McH*~-}k2@3mT!Jahmj9kZlv>eH} z7yCK<^x~X;QVvZlGOJ#-rs{ffI+;4z5&qz;9!g3g#AxDGrl||uWuiy2r8o;Js5^x$ zHL!}I<%ym8m+Ac(@pD#EJDZB>bpxG-T&A?O6=YZRX|S;&I57CKXDuFXQMP-Dt7g@$ zqVRSoAr)?JSktlx4-a#Z-Gq~Cl0I-<%!5ZC$Brm$sfD=)z0=MFdD8r8KG;I&i@&$$ z`30@=RF}Vau}Wka^V@~&+-=lwb|c>s<)cl}&KD@{*}2w6N?AM;S@skd)*CnZm(2sJ zt5qljw;C_H-aOA2-k@gEl1}{$*x9hMuVntl?Zgp+GA5;=w~rtQDLb9RWh_az^@*)| zW6MdTfo}p&kD|b2+sK(o-{NqTeew{PH#e5S>z_c*8&C*w z$UHNxPR(TrHZ6#b5saj7+|r1DTV+r`@7_$jHN7;ma97bd(;QRwU}h^@-w88SC2mWd zz@p*F=TG_y<@(NbtBSl%OVWs+*AlU3z9T;7Gry0VCkSaPA{UvKC#2-$;Sr!?AST7f z9$*a%Re#f%kCHXsC7d7DaVxcME0PJ14v!9{!^Ye8Iz{bld{V!3#aQhx!n*t>F+HR7F)8( z!tf%i>ckqG%$QBVv5FDGJfW10;m?6B9T3m3(?nW6piI8q_J8Gb}HO`fNK=WzpO13|v;89)|qnKez zsa*%WKHVOj0tuZW5u!o^hiSl=25xj=v&v%UfM!laM-nk?pM%|X3Q^~l-zU!Q9eFy7 zA3d~oR_Ti=@nG>_mU&(PE^3_dp(o^|v#JpX96UOd0Jw(afy5zc#mjIsc4Se>n8nBS zuidJ!rl4F+0uD{jO%?;9d|sRMx|vDV^X;WH6|uK$9@3P>o0t5ZiTVyyw_|8%`6E==qE#Z zl92Fxa4Bg$S3=Lb@}5b%p3l^#>qeVCJ+7jM=M_6EHR;@Ck(ruKcz9&}EMfSlv1+^u z;06F5U8r@*GH{qX=jIenrmRW?acpO`qPM8UPxYXm$Qn;^tZW*TnE}q&aRyE;nB_Bx zZVI||C`&4WsZFRz^+$NkyR~KBJJdUwbnIh!zL0m@oR8jNZHz|QUR^hg=4y#s#Hld( zxWJWphY=HdaFA$SzA2FY0$&NtrOSgS7qmv=oIYzt`Q>HEg-#;(Jl9Cgk)PYwJ@H>V zR{J(6`sXijO0kkRh3E+B^-a2oXiVEmz%}w=MUC+?&oSECCRO9A=~QE+v0Sy_*gM&J zotD$sWHa%DwLt%bI-dM&?W(X zeWs`^XeRc;oG9YO?knlqpQ0iobwLTZN@4wBQL=P$z%eprYLsvJO;xr1OuSO1PHk&O z5mAObNFU9ELvLp#``lgq6FArS!11q}!_3AXuZ(nLB!7^^X+FM@ar7)%RWZ`}9yT6< z(urraB3byo5Cgr1OdPUM4f>7dSJIC);Tb=e`ubbuI`KRAuw4C}YFbX?G>jMVu{r~D zUn_X1##SjM$)s$~@7apd$hy`51CTM&{II0d8ZBEC&4uKN5PmdO)jQ@$zld%!qZ7&ZoQW4t0HaLvs4#e~vKsDWx& zSUr{Dh%FLFtjljTHA8HyXYK`Ny)Wtww1y37bo7!hUKP=iKa`^ z>zY+!c=YpIWz)V+rM_{i@Ft8?&F(4n_j;3%5y>F{mc%Q;#KUwHlS~@HI%1>_hIC$^ zbgN$s=DK9sF8isPG9{}PCFf%olz)H3Wk_yK6l(+Z2F^=?_ZcU%v0WO~m@O4y0$)vT zk;2#f2;iPOM^b3oXX0ucRrc-9)!w!DMk1J{b!Ac)fLo?$z}#}ecIAm-Hd~b%4^&O` zEd*Q{k^T|9E5=1sklJP?FpKEQ7aMrg_3`{k3aX%@ed1u=ujdVI6@8adxRpOmQOYN3 z#+j`&bUtb&hhl3AF?L|j0qxj`s&*{s2`i&3b6gH`T&UBAiSU8<1}1%-A;L@52|(b& znjm)W^47>%U`D2B>OFAzV}MzYxhjbn(W2=f*ZCFoj1Wy4lGsj~N??;+G=OtA^(?`MvbyJ^o3*Hi zCHfBBA_>M)fs@?6uX2kq6aBb?Tm#Aq>ZHBNs78{aDz7zmDVOypUzu6Kt#$g}^f~NG zYr;1yqd3a*lAVW_wd0w918cP1J(Nkq{kC0wL6edO<_B%ZBKvoHV%fMYd09dWA{<*q zZ|@7Hu;3N>XzMW-$%Ky2L0IUOd0MvLkJBqyQPVNGxsOly!aw_WE%p4&MR--%H}DTI~#U?R6 zvd!&8VPQnsGWi8g>w;z%cF*1`HPQ5#PP-k(>o*isIaJ!G#dcX}CRh$_&0uavyED(g zV%Kg#sAeqk%4jfsL%Oq2Ydj>2N1-_ET&yj%;w?J3#VOZm2&4UrSKegC+v1_NUAo@5a$~*zZk2s@AZiY*?ki6XeZeqm*ywXW!<^0KtGUZ;eB0Wy+2L(?n zq`-UX+(sT^#p5b9vdBpaYx<2c*Sb)}@qDqN?;=~L?zhxFPV@#qJ^A;j$9Isbd;G@} zs}V6KLeZz4Y{(n4^m%>#OtF#lnTo%-qoc=5xgv8OXeS+n2-c-UEyP1{@{RVwWlVqX(P80B&p z=5+|LBV58l%n}N051J&99LVStxcx~*__lZ$gebE*yC{y)L0Zn#az4a!x}vHF(49t zGjMyc7DZB$f;PESq1T_fuFz2 z+?NH*X|vG)zl0$Kn6U7$FD^l5AAJR%s@m0wT!KzX`WR~)>Sn=;#D5m__Wfzk3t!r4 zy>l<@F_HF%|9tyB%JS%qmuvokgQ)Z;Tm4MT+!r$f=_0$p`n;zXbYs7B3OkK{n&n=n zeB0xyV4Rab#|M@L6Vj@-7KRR&Aepz^s&-GsO-zT!2aSK5Rn$J6WrWx~LA|E91nF-O zu2&CASGz`?NvU6gE|`ii-+LP!jv1Yp8s;mGe%n7ep4&N=G<&tXpF8%6f z`+U1zaPOq}jpebYp@(%G{u}|m703y9Q5TDY-y*J}Uu~Jth6mQckDuFH@_HS0Vw^Lc zpa$YE!g))*En97!yS7r7z3*@w-k)6hQndSHIzWlDu+4@r3tUUP&ta=M`0b=gT)0_W z?cx17y?DJgl2^YjL5k8_m!QD21l9$k-2vEube~-IYYzH~mCUtay_5#=8sIOMv_`z2 z+s*W23+k-#62$F(ZtjA7$u8kwaQn=%P`&+(iN~#d)+O<&MOPq6cL~yT%Yr36Hl&91 z;enV{?eXac$p!T&@p7KuE2XdJp$ZK($O_0(yzNg9Cr8a2;@%T;h$i)KXYz548)oAE zVZ!!2?O+4md$kvWuO|O|=qd_4bFY~ahPo?HNb>VOvdd-4u(v7xZZ&a|z3M;O8~&rD ze!UJp=Ch#Lqk(VQlX;hE9r3FcEBp%?>TG{Wy)Bq#T|@3qYe>pOdD3^tnga`2s%wOR z!2SaZoN0~7r*HCj;D@?_>VATrgNMu!!KT$YCgIO*CwHkknsKUZAarYT*|8T9&r0j8 zOt{{lmR`N*=J|68QsVfd{F7jCbl><)D=_7?Yxn(fr{+PB*8IbAxMU=l){z7DUOicQd%_>_GF5UYY12#`g`bh3p93pKGs!w}rRKk7vJ?jK}KBXMn_}yCSTmbq5MNVDhs9#rqHS zzB8!&OegoID(skiD!Eti<^5auam5j_G@?v)&M~TL#%Qna5+r>gN3{A{C11IwI`7wv zMkm{Y`hm6A0}4ddjlq&ZA{=IetwK#S>AE2DFV8Syb7J+m+D}-!zq&42Kw=J#R=+i* zpVpCAp?nmmc|NlD<<0os;?v*>?Dvg31|LbRrADB5_2ShHdpRoY80<8QjfjQ0IU*IP z3~&C}=50N0XzDO<43xoQmgK%sSMNBjii%56=6O0}$%e7@$LJPAW{^Ay&vEc!ltCF;u;}r2Vqf^)Z{-jn z&sR$3=xl$9Z^1zULM|boj>fv&7{gHNXzXtPm!cfxeG@3lp>o%ndtPIelXXR{mI$W8 zZvM{EQBosrh2b|!q;6a>UV_U{J{U=?ekypF?XO|ivn<(abf;Rakd@gey2{jWUgIQ- zQzIeW8LB5^LqfQkbaBh<3oH$pd;$ZSsR+=VGgZy$eE{PH6t^Ji5uQQG*R@zn>(8WdaB`EY1xPf%|i<$B^({^m+v?n)( zv*N|ufuZmIN<9p8bI?$9KHF@sxGTZzK+5bPrmJ%!SpU5HKKypT*~#9LrrvF_C>`{m zC-(?-xT$)KbMaTc@{ASd7hRtq-G&--b6-X~rtZM)ae~9W^K%{}NGdwpdY*6}K5Mqf zY~@b$o>1j6)Db*(+UYpX&AeyN#liiPD~e{tdRmd$wpu;!?52I78WWl}aK+9-_}-Qz z?bwBewNZqwmRo8RLO+0hfc@aKQrto8M;m1$Ts!PP>yTe z;hDBOA?=QFJFKb~Pv(6wSV1-8Iibi#m$F%1kMk(jd#>!&!>n_W9_kfX4QhENAzPt> zn9%`mQ45-O3DRzkBkGn?<;H%*lW6Pk6Bgy6_a^<&g!fO!`i@7`6!x@J2uM1yQg?|q ze%fo$EjzENNyv~IwuiX{mDGUM)s{K51O=)noa>%W*GO01P_={j&2MT)*0{bRU*@~} z*tBvT^HkkFP~zu2VPNh@OsMJ4v@eh2)m&Y%;uF_t3@$+|W6RkVA2L;KI)#cV`E6d2 z>d;%Ruqb+CABe3ehKPG?c&gd#PM8wSWxed(R;K^hiJ4xDpwL~7*;f<(#ZW`f4q2e@ zGE@Bwe^g1eVEk+*)@=T>=9Z%yUu9iT%|!uG+hb+&vo9`<1ibu+*xooxaWl>1U#z#{ ze7I_BxSC~kFF`07!7okAM$hAtK+c*DFAMS#7XfOc@ub1VOX>v`o_Q^V#&v5=)iR< zj`mNG9P(+?OT~2kJcA#{or6m$-gfgQ4+ckj^`<|5m^R-rqD<>{i4?FLCi}BQHCC%S z4s0&EJfmp%agS62EyXhPqMgm<5_H!i=aZ{hFLEjM+X~x|^}9nz{(SW~O*vB>vbEdm z614dx6coq)RQp9m^*QVBWs2Q``II29mZ+AGkgdWrf4JK@*Y9pQN~brc=5)X=K_-g^YpDrT ztogD#)3u7>lT-Wk`NFs2++KVd+&0&3pvQOP5)^K3e zje6!pdlIiCFwM7${R7*uH!!$qa)@sz=joxhgMO9Ch%MJ%4f`kPV1R0yWSyn%)8SZe z6OH9#84!(ySFe16-&sY_>xPOBv8NBhXJ7v>0H{D$zy620p_dN6Gyp`hpz#xc)c*jf zZ9|xQxKU&8*QwKLbxbU*Z0d9AR-|MiCK`)tsq!(8yGL5rgmk-A3j4W$30={?;)e@Y zXeB6>c3dE1SZFIs*~#T!HWnV$-dGYA7tp%p5BF~s;H<59FL8ul&O;TBSBS{-t=z0b z7bX)MkOp}%Q0OZ1Z8@QSS8`;n8$!M=`hb%gY+0PP$ZA32YdpQBoRb-lE;AO$taj@m zGE{=Gf-Gw*wb2z8)hsJr51^pmq5LN<=kaUnCJ*GHzx@ny3d8j4aE}$#5xsJ#Oo~5G z(N%dFcM9vKikTL^#5Yp$xUHh3wVD1ZiDDXUW(8IyYLd~~roP>TkGfTPZ;3}d>=1m5 z40RS+$wNn1v1-FapBU;m)EagyBByA|uUh{AUobbH8c)J8Q2dD4gUAc9Ulf*kn8j8S z#Hzw{71*;G^8&)U##)gbEVCVKRT(w4sfAsET9I8f)wZE{-9`F&YI9!CpRAd<52lCu zM1Lviv<#Z|whzS2zQk;Nf^763UM0sDI_pkl_~YSm``2x5tvi-s*Ve?j5#xSP5e`ggwTUgjiVU`2}?sn>BUVAR3m9WYoSN?Gj0fug1#$ zRIe5;ArjP45SjRlG@^?wl+nUAt#=Et{SvR5C)W*Ds%vITG(ts<w!^jq3~+fNFwaWju5 z*UwHtPTO}(Yk4v&ycHrdCcs(5AYIEzRi)xLFvS*RTB2=)-DLS5iDhOx1|fGp3;mev zo#^O+Rd1#^+=Jn1eXA`M)U#7j?X6}_miF=8)iYIY%;{Lz$(wx(+&F5P7?k^k$i|v! zllBu8o$=!4A-C#d^zzgOZ_|HH{Y)R|GVUVr^mW`{T9s_Jq44x-#_guQKH}={zo~!3 z$3jxnR9kKag`%pBil(~hO682;V{qPQDrP-A#|A1jT;;UW7O_t8u2EYzmUejCoL%DM ziogJkSGQ5E2%#DUg+TN+ISsjJLpcEyH!B!Qt8P__bZQ20Fi`w^Z&^E+ToN`*PFC7e z8O-+92*|)=ww6EA>;&HamK%e_2hb0o&3_i){z7LV0o=cRuZ?3Fi{pBo1+@WeW5(;K zJ?021q$zfAtB+Z#iX0hxsWs$QtZWXb4;At@i4B~fZbFmAo#HW`SOG|3vk3VW0r`P8 zNMWFaPF3uh%o~-us;PGd!bqU-$HTFOwVLXu02CN>g4KqtYg~HjxbDXancYRTYx&1o z%Bxl)vPa3OC%khRxyo3h6)bTw10~FT%e84Q%&8$%R0VPs(#{zi)|zsA^i#_c>N86| z>3G&uwc)*XOOG0{2WunH<_`6vaYnrhDEfrl-_l_I0w5p6Vm{_+$Vp&pTLJl{r9ybf zy4!22BP)yUN@HH+YsI!pr*$MTyGw$7LaoMa{`_EXVC4@qx7xv&zxo+BGaG zrGpJ>X_)qk#i@)aiWQVj`_DYMF^g8%))Eo&U+-6Mxm{>aWEFZ@;tnx+Ia-#brq1eE zYghwZG*&y;%I8sD&YAw%;w9u-H`*z8=#-au@Wzm3V-J=AT$Fe*pT79-H*vuHc$_ z>0558O*Iy&YpE=Hprg)RK)wL;)Rl?kwM%dAHfplIBY?TQZcIy&aQco)y*2GIYkMX+ z+PV2sq5>J8xdxOSnNBihh%zGrVRJ*Q~Fr6C~BRGik*3z(5 ze8Qm3YB1d8b#klH$PDHD`k#nk^fOQibCFHQy7cl(I9=|$ZMyBV9*lg@?zhxVNdlX@ zRoRhkUCk}jTR=z0r1vtH;px-A&zDyiF6PH7=DMpayAO{g#lE^%+*WFQuv(mO zFR^Q~Vl!j4b^&d-a%!>$ulu)S-W5Xw87=3RWrF&dxWfkhb|7UjjC`b7R!vVCRi_sa zU1~~lYP0xA^xWUksi;ic25w!MTALXb;^?cjvg%J7F8=@-%V(~jj5AIBy6f&XMQhu^ zQjpO>uB$9;+?Dv+N@F!n_A;O%lIy`~!5%31n%M)rJj8Gv*+CQ+tX8=;J$TM%wZ}$I z+)^4Ga8)rWN!&$@;gKz_w6inM-g(E0av-(ZRKAOiq<|0yDufy zJPSWchEzpaK*Rz`9fZzMiCV}>@MQEi?A0&Bg+x3Sut zt?bWY!Lu>a!(dKLWh|2$kzILhMFM!9AR=r-#&R3(M(w;cuG@9Q$r?vj42aUzx|xla zRkeZ-&{;(8*Bc_Gp6-@{)Pa!wC+S~|b*qT2tO|vYGF0rT$#KJ%w%km6GEPiabgso~ z6gL*Coy>f7s$~ljJGoN9YUBdY7$un;asF2-rIgG}5oNp+fyBp&jf|4Ttd7Qwb&B9S zgMy;AjkzsQTf2jn<-_A+0&!ULe1#+%FDJI9!gtSPj6iK zKOw7Bqd`CJP&UXsY6+MB0G1_n{AxYBSIE!*0LWrn5r@a03qQ;xRs zi+c!fV=TzxA30^2?nSmU9BiCmnHB?fMW%$KHuPLxs*EeXr<;enV@fjfk%6 z%C)yA&3(zg4CWK4bR&WfBdo%=RfIk#iVkAaat-70RI9TsVp@u~b0Jl%#COXuO z@-y-+tgZalP#*`58dWTYHFh8QoS33z@0Kd3f{?kS8F0H8nLg>cJiiYIlEe zM`fIVDh-*bZe`P*mPV%NG1;eLj(4pw(H_ycQvw^tWBU zr1!QjPhn)Unx%OkHNUvwM6S-=nRj!@MD96EaZa^ziz{DqWhu_b2~pNH1SO$+VzqPG zvE>B?dQx$+t!2q;*{sONuBwf;s#1b#GXxEI4su#@O+Rak{g<%&gl-g2c!}my@l$6h zhnrnC<+d%Z`YH#&vT6BKJ679HIVQVD3U790J7lN>j!`WZ&Ej{8G^K;}op;7}eC$+;`QLXBq6WN%ppgxXCL`23f%b)h2etbXjFu zk35(;2GMD!o9=UMt(=;=&tYmswBxU9unKK4tF5vvRHwH^ zBDs~u%PAeXQ}Cto1u~3)Wp-n;_z>!3j9_TGw3%h>7^KKGYuY-`1y6h$BwumwJYae8bPS#6Io`7y|}khZjAh(E7S-C(0~cAp}a(9EDqb_JWf-S&$+boW8L(i1f!E;dAb1`*ZNsMmZ}t5|+`v;#60u|3H~x2Rd$tG#jY#^cK8 zv$*O?u05P@Ls)Bqi9)l^(us0%YH3(F!+llXy2x6wg1P~1XzKP-NNYf&FQ{`&JdWM@ zccrze+RTbLulC|trZtkkl*=FTa~HWzCe0Z>20Fx{m^}qX<(9f4<-Xm4U|=x;V~bF! za_D;>F!ObODcxz_V*@ir@O+3J+CAk$KfzsCa&3WR(@lRirbAejEv?wZwe1FFcR1CM zQkJ{7c5+PRnC)+>25WI(Oy_=%56d2unKZ4 zDAsa4J=Zq8h4@#F;=13lEnk(jad0XYXEFT`3XFQyTYJp>P$7HU)+B;_C%mS&5h$3T)0^-_) zC8<12OHEB)O>Pj@^2*l5%IUin2dq_aClmfES7z51{XOYvd`gXy{3g0xU$(QVlV!D| zGUHY%SIGAgjq+k6(WyZqwzrG>jV{+%p)PxIW7k5rkc(Fs>AKV*8DA2*P{U4B7qEiH zrp()WMUBVZ)lto6%DGS=w50uxt897}gi~V5yjR@ST{|GXuV=Tq{w$Z`ikqj>Qm!m7 z$}ilc_Qe(|OzgH2+iZAPwCPE2cJ9@2GAiWxBWeb+X|6p8Y;1J097^Nkf~(=Vg|3xs zt1LWHmE{_^0@WXDDZ7kJz1LgV@~VU63Zv{q4a)k(!kPko48TQg{eOwwP4W=8h2^g^ z<-N3BM)M7HnD}(GvjmA^*saZ$cT~dYTFx;R&`sGeJGcjJUCJ3M0naMU9Q$OAX717Xf=&ZRJ zTjW=Ot2ehV$+FqrP}N!xKO5f8Uj5x}UAR=Y<5i3XWtQ>B3eS{%&Qe~TN?TrT1U%MVZ3@&6{{TCMYS^4n=&_o00a?ZB zIQ_s4y|*i76;5*E4@n-IQoj@QHP^3lB*P&Ul zEclS%%MRfz7mncEUBT#+a2_DMK zHfeNuaBYI7yibakK{r<(O0f$qg6fr-#9DxIgu!0D4A7esVr^$7o*+b?C&p|~8|KrK zPDTP|%;aY%du=|<-BVS|aIu*F*<4?i1}5MbX48$0GWP%*bPzkNz6vq5a2C zKIbQ_(i(KSo7~>T7b7yd@o$+1y*TeYk#ugd9QMV?9Ebb!Y?m6qPOJ4hh$>&Q)rC(01!3e4@!%FY6C z6NsEd;wKfHR&Ws4I8Fv|X{Ut3PS=b&-^Or?r_(|eFp1b%+cDBvjYGkOi#?SP;lYV& zMxogT8?xg9t&>4LM$7n8^>c=fn^* zI<42*_u7ssuH#)7=i1cc*LGCL!>*8F^lu>7KM{Q_tzK{ykjtld3G~>7<2+__ILhf} zDTKlz(OhOEPY_4Y814XT*svf#*nI^@om`N^dXIiu?-r!%FA^{nB3WB63 zet}dDWX<{m15KC%7^ypAX(!@u)bfM7%Tr#yCyC?g)BV=W-de*~H_p2n>a9PTQLM}D z2h7PqTh;|c~&^x?s~~E9fJTAU{W$=hS$c^y-<`Q((UPm0rfPzim-^jnO)*MCyoVy`aI7 zhQuyAPRh>BqH7>dSBP!03)**-%PIyTtXJ4d?*`6q#>`8dg!_Dgr#rGNHgo2Qjcoq_ z(%#;yN9SRBp4-=g+;;VIJOZ1tEV5N|E8)OF=n&cyz56LXoI+H#px*O0ekTaKw?tS#M310>|k>imZ*1Ov}bIF7#1Ygv^V zt?_PFJy7)wVJp8H=xVIYR7z_19umN8vgt*fi>TFOZ=o?tLEi*hE2kn?6ItTUQV>03vII{i)40{ zV`Nsd-K<5u;YSgHgv#7(w~OuIFUY(^&#|mlB?q@ai5WBz>u3njuuEG)hwdh!$4< zI-5}oZe%xZ<#l1mO@r!mp|bJTd+bTy#wR}dcVASs2+P6A7IeIOCPfWJ!CE265pK+i z`EsnQk}auv-Y%ltiWcQcXa^?jd>ZgeWDA|I$+`7oBD^WuI+}jcsVi-K(Q7J|SzS`Q z?R!tcEskt!c%^ww1q)*wCXY>udPN1Tf9@>fInu>+rR-uXeh}n5#O08BrHw*XnpOZs zZP|+ZcDlM(HqJ=dIJ|7iT`i}Nx=p=>nH`Ks%kdv<^5rzzo&@O+u$8(zlMf@V_+0B;Tdnn+*3~+e(;fKa#4mnNp8Fc$$lH|JU&_F~3aS1}1PZP6{ zorAGfp;Qx!#qhhHXPK$5rFX5g*fZ@;6&Mn^#U~V=Cg$eg9+u_f)Y`!RB!S z9x!p56NbtUL*b_x$%O8~WCvrC!08&=)^~$Kxepr(j^JvLwctzog%X#pi<8M)snnJ?P_5Ktr(-7HHe+}EpqJq*;?fUIWnpNBx>mtrPDG`V}KDOHV&i^+QnJH$rM;Bm5Rz7P!j zPN9x2rCn$hirU&*hPT;eYP>2$�irqN^&&H&nK9u<<4e*q+$rl~}C!t!vkXK%4NN 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
@Y1m2ENAe+IuDcaXCnXK~ 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
  • uk{^fhbjU@D_! zWP05?A>GDE0s$Q?gGHl-PE{K)AU=xLR+R%2(OsbRUT9HSx|x*@M~$Sch&_G{INOM; zJ{I^XzjaQFBrF$}Jyn)WJ<8|9p2{0D>baWf-jz%IrrU+%Hb-T52M^Q~VdF8~3zT?b z+N)zqs1b-dDa3PIJtYS)Vd7KhqS$ahEW&;n!|_<}$WZ&+Xi#H=+CMa)?;bizwl&u^ zrs5V8u*12tgwg>cB->52(4nPwIzp-Kmu2d#(Vc?vBK;AtX!@wK7LKZ7$237EChE~D zfI%Q=scsOqauZ+xNl+aHy;E@u8e>>Q;$$^X#5X_L3JH|J1OOp7lFA0Vs4l3Uibt8c zp=ozS=w=w|+bcYcl{Pc1DhU9CkU^G_^+vs=UDiR9-d89^K`{tH&1NUEg=bw^W=5z9Ii1)(|B{Z&BXgSb*0ahbBK50w5_FPVn! z;HsMDpXUq9z4t_bTD&_Jpw_Xi{#7`v4lY&T;%ku#ad87r@}TT~ZHs0UVAD@PWee1Y zCBmr0hsrk&Du{q=NKjrq^9a_aWd>9LIwr&Bn*C}`&j zxCdR6v0sN#&!XjqtGUj+sev5=j3(2Eq7}d@x(_k5qU{hEU=zG4v2YtMeRD>QY>0c$ zy72JLL#mxrIL^qEc~PRj4AyFMS#jlZn(x#oB~w+Gn8(Efg~Sf)U-G$36$P;f;fym^ zL<6$!s-~xIq46tJT*Yf|b<2$^*YsbC=(g~TiB$u13k!OMG)PzWS;>o_Tvsb9y14gI zD^3x{>f9z&sV!S|Q!=uZ1VYHErBek^1&)duGSN&dl}}?@jc#j(kc?A%qQKcC$Ww^w zUDC+I$w0FdqvPSUx2CFa+y|oOeP8ikCCqna%pv>UhYiaa!YtKFGfb zuBY}x#ceIh%MhAtY6l>4*%!K;_fdPD5RO9KQ^aL;cIEX=poY@jcSM_~HJPe;>)Bk> zZ&U`?REV(AP|#`yQ6tNm0#0@a0P`8xs!nb`${65D(FW60TuzJTHaf2iVe`8u9D8-yaOy-H zH+riz!UCz7l5>UPVTxWAKmd1z%yU*Jpk8Y#LzPm4iMF!2LM^jZ`F&K>(!2FjE0n5( z309t}>{%--LguW%HeL{rKXu|v30%e;KN5`gS$9S42;w==bVayv z86S+~brT+{mf?c_pXEfgjp(T`M&%4sh$C>R#*rC7P8bvyL_s$y z&&7XrOPJz2rr`Npv*UKILqEdJK=>@xAsIY+s+9v>7G725bAM$^oI}UeQeB!PNh_HE z3`*T}SGAC(L`a<1DOsVmmQYk^pp|l*cTji?h1Kq?v*CLM04uD|8><=NqR-h~hu4Nn zE*q_uYjx#W1!2R?v*CJ5m|V2`DZ3oG+lu84Vf730ua}A+(Nb{a`zeRC^Z)Hz=z#=P+{-nsoOh!RSda^Kl` z<+^h!YRyc}+$1;~=kds4(y1$9HmPbJJhMLmCPGfhypoiqj ze7ICy=_}2ey1Q0?23#`Zb)v8s!R6~eGr3gj>P zRX^$%D^-w^qs$eE0bG4ixakT@zKP5ZvTTqWIjr)|5q=}1W#=6gQ%ZE1x~0yJ$h_fQ zMK}2L_%5a=gsi?9S}UjlU3py5#|_83l2! zX_L$&CGzspRW2svWuvZMIb6VRkObNtQT_w_YDG zR|GI_r!-0XJ89^tZV`umW%`bME*49hCs|_u0OVB!{Jp6|69W*i{^(=9w3p$794T|I za#^Y1T5~HU1kYtja+3K#g5_nJuvrzKvgY(&Xebl)Lzc=;C!kXEZKVeR3V@4;C{*SK zstuK#yv7#cM*!{_aI%GC!u%yzO1^9SuQ$k;}zKfiq2#D2EV�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 JPUojT|Jf|fvkw3O literal 0 HcmV?d00001 diff --git a/splashsurf/README.md b/splashsurf/README.md index 737e241..641ffb0 100644 --- a/splashsurf/README.md +++ b/splashsurf/README.md @@ -9,7 +9,7 @@ CLI for surface reconstruction of particle data from SPH simulations, written in Rust. For a the library used by the CLI see the [`splashsurf_lib`](https://crates.io/crates/splashsurf_lib) crate.

    -Image of the original particle data Image of a coarse reconstructed surface mesh Image of a fine reconstructed surface mesh +Image of the original particle data Image of a coarse reconstructed surface mesh Image of a fine reconstructed surface mesh

    `splashsurf` is a tool to reconstruct surfaces meshes from SPH particle data. @@ -19,19 +19,27 @@ reconstructed from this particle data. The next image shows a reconstructed surf with a "smoothing length" of `2.2` times the particles radius and a cell size of `1.1` times the particle radius. The third image shows a finer reconstruction with a cell size of `0.45` times the particle radius. These surface meshes can then be fed into 3D rendering software such as [Blender](https://www.blender.org/) to generate beautiful water animations. -The result might look something like this (please excuse the lack of 3D rendering skills): +The result might look something like this:

    Rendered water animation

    +Note: This animation does not show the recently added smoothing features of the tool, for more recent rendering see [this video](https://youtu.be/2bYvaUXlBQs). + +--- + **Contents** - [The `splashsurf` CLI](#the-splashsurf-cli) - [Introduction](#introduction) + - [Domain decomposition](#domain-decomposition) + - [Octree-based decomposition](#octree-based-decomposition) + - [Subdomain grid-based decomposition](#subdomain-grid-based-decomposition) - [Notes](#notes) - [Installation](#installation) - [Usage](#usage) - [Recommended settings](#recommended-settings) + - [Weighted surface smoothing](#weighted-surface-smoothing) - [Benchmark example](#benchmark-example) - [Sequences of files](#sequences-of-files) - [Input file formats](#input-file-formats) @@ -47,34 +55,56 @@ The result might look something like this (please excuse the lack of 3D renderin - [The `convert` subcommand](#the-convert-subcommand) - [License](#license) + # The `splashsurf` CLI The following sections mainly focus on the CLI of `splashsurf`. For more information on the library, see the [corresponding readme](https://github.com/InteractiveComputerGraphics/splashsurf/blob/main/splashsurf_lib) in the `splashsurf_lib` subfolder or the [`splashsurf_lib` crate](https://crates.io/crates/splashsurf_lib) on crates.io. ## Introduction -This is a basic but high-performance implementation of a marching cubes based surface reconstruction for SPH fluid simulations (e.g performed with [SPlisHSPlasH](https://github.com/InteractiveComputerGraphics/SPlisHSPlasH)). +This is CLI to run a fast marching cubes based surface reconstruction for SPH fluid simulations (e.g. performed with [SPlisHSPlasH](https://github.com/InteractiveComputerGraphics/SPlisHSPlasH)). The output of this tool is the reconstructed triangle surface mesh of the fluid. At the moment it supports computing normals on the surface using SPH gradients and interpolating scalar and vector particle attributes to the surface. -No additional smoothing or decimation operations are currently implemented. -As input, it supports reading particle positions from `.vtk`, `.bgeo`, `.ply`, `.json` and binary `.xyz` files (i.e. files containing a binary dump of a particle position array). -In addition, required parameters are the kernel radius and particle radius (to compute the volume of particles) used for the original SPH simulation as well as the surface threshold. - -By default, a domain decomposition of the particle set is performed using octree-based subdivision. -The implementation first computes the density of each particle using the typical SPH approach with a cubic kernel. -This density is then evaluated or mapped onto a sparse grid using spatial hashing in the support radius of each particle. -This implies that memory is only allocated in areas where the fluid density is non-zero. This is in contrast to a naive approach where the marching cubes background grid is allocated for the whole domain. -The marching cubes reconstruction is performed only in the narrowband of grid cells where the density values cross the surface threshold. Cells completely in the interior of the fluid are skipped. For more details, please refer to the [readme of the library]((https://github.com/InteractiveComputerGraphics/splashsurf/blob/main/splashsurf_lib/README.md)). +To get rid of the typical bumps from SPH simulations, it supports a weighted Laplacian smoothing approach [detailed below](#weighted-surface-smoothing). +As input, it supports reading particle positions from `.vtk`/`.vtu`, `.bgeo`, `.ply`, `.json` and binary `.xyz` (i.e. files containing a binary dump of a particle position array) files. +Required parameters to perform a reconstruction are the kernel radius and particle radius (to compute the volume of particles) used for the original SPH simulation as well as the marching cubes resolution (a default iso-surface threshold is pre-configured). + +## Domain decomposition + +A naive dense marching cubes reconstruction allocating a full 3D array over the entire fulid domain quickly becomes infeasible for larger simulations. +Instead, one could use a global hashmap where only cubes that contain non-zero fluid density values are allocated. +This approach is used in `splashsurf` if domain decomposition is disabled completely. +However, the global hashmap approach does not lead to good cache locality and is not well suited for parallelization (even specialized parallel map implementations like [`dashmap`](https://github.com/xacrimon/dashmap) have their performance limitations). +To improve on this situation `splashsurf` currently implements two domain decomposition approaches. + +### Octree-based decomposition +The octree-based decomposition is currently the default approach if no other option is specified but will probably be replaced by the grid-based approach described below. +For the octree-based decomposition an octree is built over all particles with an automatically determined target number of particles per leaf node. +For each leaf node, a hashmap is used like outlined above. +As each hashmap is smaller, cache locality is improved and due to the decomposition, each thread can work on its own local hashmap. Finally, all surface patches are stitched together by walking the octree back up, resulting in a closed surface. +Downsides of this approach are that the octree construction starting from the root and stitching back towards the root limit the amount of paralleism during some stages. + +### Subdomain grid-based decomposition + +Since version 0.10.0, `splashsurf` implements a new domain decomposition approach called the "subdomain grid" approach, toggeled with the `--subdomain-grid=on` flag. +Here, the goal is to divide the fluid domain into subdomains with a fixed number of marching cubes cells, by default `64x64x64` cubes. +For each subdomain a dense 3D array is allocated for the marching cubes cells. +Of course, only subdomains that contain fluid particles are actually allocated. +For subdomains that contain only a very small number of fluid particles (less th 5% of the largest subdomain) a hashmap is used instead to not waste too much storage. +As most domains are dense however, the marching cubes triangulation per subdomain is very fast as it can make full use of cache locality and the entire procedure is trivially parallelizable. +For the stitching we ensure that we perform floating point operations in the same order at the subdomain boundaries (this can be ensured without synchronization). +If the field values on the subdomain boundaries are identical from both sides, the marching cubes triangulations will be topologically compatible and can be merged in a post-processing step that is also parallelizable. +Overall, this approach should almost always be faster than the previous octree-based aproach. + ## Notes -For small numbers of fluid particles (i.e. in the low thousands or less) the multithreaded implementation may have worse performance due to the task based parallelism and the additional overhead of domain decomposition and stitching. +For small numbers of fluid particles (i.e. in the low thousands or less) the domain decomposition implementation may have worse performance due to the task based parallelism and the additional overhead of domain decomposition and stitching. In this case, you can try to disable the domain decomposition. The reconstruction will then use a global approach that is parallelized using thread-local hashmaps. For larger quantities of particles the decomposition approach is expected to be always faster. Due to the use of hash maps and multi-threading (if enabled), the output of this implementation is not deterministic. -In the future, flags may be added to switch the internal data structures to use binary trees for debugging purposes. As shown below, the tool can handle the output of large simulations. However, it was not tested with a wide range of parameters and may not be totally robust against corner-cases or extreme parameters. @@ -97,6 +127,29 @@ Good settings for the surface reconstruction depend on the original simulation a - `surface-threshold`: a good value depends on the selected `particle-radius` and `smoothing-length` and can be used to counteract a fluid volume increase e.g. due to a larger particle radius. In combination with the other recommended values a threshold of `0.6` seemed to work well. - `cube-size` usually should not be chosen larger than `1.0` to avoid artifacts (e.g. single particles decaying into rhomboids), start with a value in the range of `0.75` to `0.5` and decrease/increase it if the result is too coarse or the reconstruction takes too long. +### Weighted surface smoothing +The CLI implements the paper ["Weighted Laplacian Smoothing for Surface Reconstruction of Particle-based Fluids" (Löschner, Böttcher, Jeske, Bender; 2023)](https://animation.rwth-aachen.de/publication/0583/) which proposes a fast smoothing approach to avoid typical bumpy surfaces while preventing loss of volume that typically occurs with simple smoothing methods. +The following images show a rendering of a typical surface reconstruction (on the right) with visible bumps due to the particles compared to the same surface reconstruction with weighted smoothing applied (on the left): + +

    +Image of the original surface reconstruction without smoothing (bumpy & rough) Image of the surface reconstruction with weighted smoothing applied (nice & smooth) +

    + +You can see this rendering in motion in [this video](https://youtu.be/2bYvaUXlBQs). +To apply this smoothing, we recommend the following settings: + - `--mesh-smoothing-weights=on`: This enables the use of special weights during the smoothing process that preserve fluid details. For more information we refer to the [paper](https://animation.rwth-aachen.de/publication/0583/). + - `--mesh-smoothing-iters=25`: This enables smoothing of the output mesh. The individual iterations are relatively fast and 25 iterations appeared to strike a good balance between an initially bumpy surface and potential over-smoothing. + - `--mesh-cleanup=on`/`--decimate-barnacles=on`: On of the options should be used when applying smoothing, otherwise artifacts can appear on the surface (for more details see the paper). The `mesh-cleanup` flag enables a general purpose marching cubes mesh cleanup procedure that removes small sliver triangles everywhere on the mesh. The `decimate-barnacles` enables a more targeted decimation that only removes specific triangle configurations that are problematic for the smoothing. The former approach results in a "nicer" mesh overall but can be slower than the latter. + - `--normals-smoothing-iters=10`: If normals are being exported (with `--normals=on`), this results in an even smoother appearance during rendering. + +For the reconstruction parameters in conjunction with the weighted smoothing we recommend parameters close to the simulation parameters. +That means selecting the same particle radius as in the simulation, a corresponding smoothing length (e.g. for SPlisHSPlasH a value of `2.0`), a surface-threshold between `0.6` and `0.7` and a cube size usually between `0.5` and `1.0`. + +A full invocation of the tool might look like this: +``` +splashsurf reconstruct particles.vtk -r=0.025 -l=2.0 -c=0.5 -t=0.6 --subdomain-grid=on --mesh-cleanup=on --mesh-smoothing-weights=on --mesh-smoothing-iters=25 --normals=on --normals-smoothing-iters=10 +``` + ### Benchmark example For example: ``` @@ -173,9 +226,19 @@ Note that the tool collects all existing filenames as soon as the command is inv The first and last file of a sequences that should be processed can be specified with the `-s`/`--start-index` and/or `-e`/`--end-index` arguments. By specifying the flag `--mt-files=on`, several files can be processed in parallel. -If this is enabled, you should ideally also set `--mt-particles=off` as enabling both will probably degrade performance. +If this is enabled, you should also set `--mt-particles=off` as enabling both will probably degrade performance. The combination of `--mt-files=on` and `--mt-particles=off` can be faster if many files with only few particles have to be processed. +The number of threads can be influenced using the `--num-threads`/`-n` argument or the `RAYON_NUM_THREADS` environment variable + +**NOTE:** Currently, some functions do not have a sequential implementation and always parallelize over the particles or the mesh/domain. +This includes: + - the new "subdomain-grid" domain decomposition approach, as an alternative to the previous octree-based approach + - some post-processing functionality (interpolation of smoothing weights, interpolation of normals & other fluid attributes) + +Using the `--mt-particles=off` argument does not have an effect on these parts of the surface reconstruction. +For now, it is therefore recommended to not parallelize over multiple files if this functionality is used. + ## Input file formats ### VTK @@ -236,16 +299,18 @@ The file format is inferred from the extension of output filename. ### The `reconstruct` command ``` -splashsurf-reconstruct (v0.9.3) - Reconstruct a surface from particle data +splashsurf-reconstruct (v0.10.0) - Reconstruct a surface from particle data Usage: splashsurf reconstruct [OPTIONS] --particle-radius --smoothing-length --cube-size Options: + -q, --quiet Enable quiet mode (no output except for severe panic messages), overrides verbosity level + -v... Print more verbose output, use multiple "v"s for even more verbose output (-v, -vv) -h, --help Print help -V, --version Print version Input/output: - -o, --output-file Filename for writing the reconstructed surface to disk (default: "{original_filename}_surface.vtk") + -o, --output-file Filename for writing the reconstructed surface to disk (supported formats: VTK, PLY, OBJ, default: "{original_filename}_surface.vtk") --output-dir Optional base directory for all output files (default: current working directory) -s, --start-index Index of the first input file to process when processing a sequence of files (default: lowest index of the sequence) -e, --end-index Index of the last input file to process when processing a sequence of files (default: highest index of the sequence) @@ -262,39 +327,79 @@ Numerical reconstruction parameters: The cube edge length used for marching cubes in multiplies of the particle radius, corresponds to the cell size of the implicit background grid -t, --surface-threshold The iso-surface threshold for the density, i.e. the normalized value of the reconstructed density level that indicates the fluid surface (in multiplies of the rest density) [default: 0.6] - --domain-min + --particle-aabb-min Lower corner of the domain where surface reconstruction should be performed (requires domain-max to be specified) - --domain-max + --particle-aabb-max Upper corner of the domain where surface reconstruction should be performed (requires domain-min to be specified) Advanced parameters: - -d, --double-precision= Whether to enable the use of double precision for all computations [default: off] [possible values: off, on] - --mt-files= Flag to enable multi-threading to process multiple input files in parallel [default: off] [possible values: off, on] - --mt-particles= Flag to enable multi-threading for a single input file by processing chunks of particles in parallel [default: on] [possible values: off, on] + -d, --double-precision= Enable the use of double precision for all computations [default: off] [possible values: off, on] + --mt-files= Enable multi-threading to process multiple input files in parallel (NOTE: Currently, the subdomain-grid domain decomposition approach and some post-processing functions including interpolation do not have sequential versions and therefore do not work well with this option enabled) [default: off] [possible values: off, on] + --mt-particles= Enable multi-threading for a single input file by processing chunks of particles in parallel [default: on] [possible values: off, on] -n, --num-threads Set the number of threads for the worker thread pool -Octree (domain decomposition) parameters: +Domain decomposition (octree or grid) parameters: + --subdomain-grid= + Enable spatial decomposition using a regular grid-based approach [default: off] [possible values: off, on] + --subdomain-cubes + Each subdomain will be a cube consisting of this number of MC cube cells along each coordinate axis [default: 64] --octree-decomposition= - Whether to enable spatial decomposition using an octree (faster) instead of a global approach [default: on] [possible values: off, on] + Enable spatial decomposition using an octree (faster) instead of a global approach [default: on] [possible values: off, on] --octree-stitch-subdomains= - Whether to enable stitching of the disconnected local meshes resulting from the reconstruction when spatial decomposition is enabled (slower, but without stitching meshes will not be closed) [default: on] [possible values: off, on] + Enable stitching of the disconnected local meshes resulting from the reconstruction when spatial decomposition is enabled (slower, but without stitching meshes will not be closed) [default: on] [possible values: off, on] --octree-max-particles The maximum number of particles for leaf nodes of the octree, default is to compute it based on the number of threads and particles --octree-ghost-margin-factor Safety factor applied to the kernel compact support radius when it's used as a margin to collect ghost particles in the leaf nodes when performing the spatial decomposition --octree-global-density= - Whether to compute particle densities in a global step before domain decomposition (slower) [default: off] [possible values: off, on] + Enable computing particle densities in a global step before domain decomposition (slower) [default: off] [possible values: off, on] --octree-sync-local-density= - Whether to compute particle densities per subdomain but synchronize densities for ghost-particles (faster, recommended). Note: if both this and global particle density computation is disabled the ghost particle margin has to be increased to at least 2.0 to compute correct density values for ghost particles [default: on] [possible values: off, on] + Enable computing particle densities per subdomain but synchronize densities for ghost-particles (faster, recommended). Note: if both this and global particle density computation is disabled the ghost particle margin has to be increased to at least 2.0 to compute correct density values for ghost particles [default: on] [possible values: off, on] -Interpolation: +Interpolation & normals: --normals= - Whether to compute surface normals at the mesh vertices and write them to the output file [default: off] [possible values: off, on] + Enable omputing surface normals at the mesh vertices and write them to the output file [default: off] [possible values: off, on] --sph-normals= - Whether to compute the normals using SPH interpolation (smoother and more true to actual fluid surface, but slower) instead of just using area weighted triangle normals [default: on] [possible values: off, on] + Enable computing the normals using SPH interpolation instead of using the area weighted triangle normals [default: off] [possible values: off, on] + --normals-smoothing-iters + Number of smoothing iterations to run on the normal field if normal interpolation is enabled (disabled by default) + --output-raw-normals= + Enable writing raw normals without smoothing to the output mesh if normal smoothing is enabled [default: off] [possible values: off, on] --interpolate-attributes List of point attribute field names from the input file that should be interpolated to the reconstructed surface. Currently this is only supported for VTK and VTU input files +Postprocessing: + --mesh-cleanup= + Enable MC specific mesh decimation/simplification which removes bad quality triangles typically generated by MC [default: off] [possible values: off, on] + --decimate-barnacles= + Enable decimation of some typical bad marching cubes triangle configurations (resulting in "barnacles" after Laplacian smoothing) [default: off] [possible values: off, on] + --keep-verts= + Enable keeping vertices without connectivity during decimation instead of filtering them out (faster and helps with debugging) [default: off] [possible values: off, on] + --mesh-smoothing-iters + Number of smoothing iterations to run on the reconstructed mesh + --mesh-smoothing-weights= + Enable feature weights for mesh smoothing if mesh smoothing enabled. Preserves isolated particles even under strong smoothing [default: off] [possible values: off, on] + --mesh-smoothing-weights-normalization + Normalization value from weighted number of neighbors to mesh smoothing weights [default: 13.0] + --output-smoothing-weights= + Enable writing the smoothing weights as a vertex attribute to the output mesh file [default: off] [possible values: off, on] + --generate-quads= + Enable trying to convert triangles to quads if they meet quality criteria [default: off] [possible values: off, on] + --quad-max-edge-diag-ratio + Maximum allowed ratio of quad edge lengths to its diagonals to merge two triangles to a quad (inverse is used for minimum) [default: 1.75] + --quad-max-normal-angle + Maximum allowed angle (in degrees) between triangle normals to merge them to a quad [default: 10] + --quad-max-interior-angle + Maximum allowed vertex interior angle (in degrees) inside of a quad to merge two triangles to a quad [default: 135] + --mesh-aabb-min + Lower corner of the bounding-box for the surface mesh, triangles completely outside are removed (requires mesh-aabb-max to be specified) + --mesh-aabb-max + Upper corner of the bounding-box for the surface mesh, triangles completely outside are removed (requires mesh-aabb-min to be specified) + --mesh-aabb-clamp-verts= + Enable clamping of vertices outside of the specified mesh AABB to the AABB (only has an effect if mesh-aabb-min/max are specified) [default: off] [possible values: off, on] + --output-raw-mesh= + Enable writing the raw reconstructed mesh before applying any post-processing steps [default: off] [possible values: off, on] + Debug options: --output-dm-points Optional filename for writing the point cloud representation of the intermediate density map to disk @@ -303,7 +408,13 @@ Debug options: --output-octree Optional filename for writing the octree used to partition the particles to disk --check-mesh= - Whether to check the final mesh for topological problems such as holes (note that when stitching is disabled this will lead to a lot of reported problems) [default: off] [possible values: off, on] + Enable checking the final mesh for holes and non-manifold edges and vertices [default: off] [possible values: off, on] + --check-mesh-closed= + Enable checking the final mesh for holes [default: off] [possible values: off, on] + --check-mesh-manifold= + Enable checking the final mesh for non-manifold edges and vertices [default: off] [possible values: off, on] + --check-mesh-debug= + Enable debug output for the check-mesh operations (has no effect if no other check-mesh option is enabled) [default: off] [possible values: off, on] ``` ### The `convert` subcommand @@ -337,6 +448,6 @@ Options: # License -For license information of this project, see the LICENSE file. +For license information of this project, see the [LICENSE](LICENSE) file. The splashsurf logo is based on two graphics ([1](https://www.svgrepo.com/svg/295647/wave), [2](https://www.svgrepo.com/svg/295652/surfboard-surfboard)) published on SVG Repo under a CC0 ("No Rights Reserved") license. The dragon model shown in the images on this page are part of the ["Stanford 3D Scanning Repository"](https://graphics.stanford.edu/data/3Dscanrep/). From e14124790dccd0ffe9f5e9b7f76ddbd81a8e2ae6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20L=C3=B6schner?= Date: Mon, 25 Sep 2023 21:29:45 +0200 Subject: [PATCH 21/22] cargo update --- Cargo.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b983c51..3183845 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -480,9 +480,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" [[package]] name = "fern" @@ -600,9 +600,9 @@ dependencies = [ [[package]] name = "indicatif" -version = "0.17.6" +version = "0.17.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b297dc40733f23a0e52728a58fa9489a5b7638a324932de16b41adc3ef80730" +checksum = "fb28741c9db9a713d93deb3bb9515c20788cef5815265bee4980e87bde7e0f25" dependencies = [ "console", "instant", @@ -1243,9 +1243,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" +checksum = "ad977052201c6de01a8ef2aa3378c4bd23217a056337d1d6da40468d267a4fb0" dependencies = [ "serde", ] From fa38bbd6e036381de7187b02c9477259a753b146 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20L=C3=B6schner?= Date: Mon, 25 Sep 2023 21:45:01 +0200 Subject: [PATCH 22/22] Fix compile error/warning with --no-default-features --- splashsurf_lib/Cargo.toml | 2 +- splashsurf_lib/src/utils.rs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/splashsurf_lib/Cargo.toml b/splashsurf_lib/Cargo.toml index 5a6ba83..8e2b7ae 100644 --- a/splashsurf_lib/Cargo.toml +++ b/splashsurf_lib/Cargo.toml @@ -51,7 +51,7 @@ fxhash = "0.2" bitflags = "2.4" smallvec = { version = "1.11", features = ["union"] } arrayvec = "0.7" -bytemuck = "1.9" +bytemuck = { version = "1.9", features = ["extern_crate_alloc"] } bytemuck_derive = "1.3" numeric_literals = "0.2" rstar = "0.11" diff --git a/splashsurf_lib/src/utils.rs b/splashsurf_lib/src/utils.rs index 4912f22..fdcd96a 100644 --- a/splashsurf_lib/src/utils.rs +++ b/splashsurf_lib/src/utils.rs @@ -7,6 +7,7 @@ use std::cell::UnsafeCell; /// "Convert" an empty vector to preserve allocated memory if size and alignment matches /// See https://users.rust-lang.org/t/pattern-how-to-reuse-a-vec-str-across-loop-iterations/61657/5 /// See https://github.com/rust-lang/rfcs/pull/2802 +#[allow(unused)] pub(crate) fn recycle(mut v: Vec
    ) -> Vec { v.clear(); v.into_iter().map(|_| unreachable!()).collect()