#[cfg(feature = "profiling")]
pub use coarse_prof;
pub use nalgebra;
#[cfg(feature = "vtk_extras")]
pub use vtkio;
#[cfg(feature = "profiling")]
macro_rules! profile {
($body:expr) => {
coarse_prof::profile!($body);
};
}
#[cfg(not(feature = "profiling"))]
macro_rules! profile {
($body:expr) => {
$body
};
}
mod aabb;
pub mod density_map;
pub mod kernel;
pub mod marching_cubes;
mod marching_cubes_lut;
pub mod mesh;
pub mod neighborhood_search;
mod numeric_types;
mod uniform_grid;
mod utils;
pub use aabb::{AxisAlignedBoundingBox, AxisAlignedBoundingBox2d, AxisAlignedBoundingBox3d};
pub use density_map::DensityMap;
pub use numeric_types::{Index, Real, ThreadSafe};
pub use uniform_grid::{GridConstructionError, UniformGrid};
use log::info;
use mesh::TriMesh3d;
use nalgebra::Vector3;
use thiserror::Error as ThisError;
pub(crate) type HashState = fxhash::FxBuildHasher;
pub(crate) type MapType<K, V> = std::collections::HashMap<K, V, HashState>;
pub(crate) fn new_map<K, V>() -> MapType<K, V> {
MapType::with_hasher(HashState::default())
}
pub(crate) type ParallelMapType<K, V> = dashmap::DashMap<K, V, HashState>;
#[derive(Clone, Debug)]
pub struct Parameters<R: Real> {
pub particle_radius: R,
pub rest_density: R,
pub kernel_radius: R,
pub splash_detection_radius: Option<R>,
pub cube_size: R,
pub iso_surface_threshold: R,
pub domain_aabb: Option<AxisAlignedBoundingBox3d<R>>,
pub enable_multi_threading: bool,
}
macro_rules! map_option {
($some_optional:expr, $value_identifier:ident => $value_transformation:expr) => {
match $some_optional {
Some($value_identifier) => Some($value_transformation),
None => None,
}
};
}
impl<R: Real> Parameters<R> {
pub fn try_convert<T: Real>(&self) -> Option<Parameters<T>> {
Some(Parameters {
particle_radius: self.particle_radius.try_convert()?,
rest_density: self.rest_density.try_convert()?,
kernel_radius: self.kernel_radius.try_convert()?,
splash_detection_radius: map_option!(
&self.splash_detection_radius,
r => r.try_convert()?
),
cube_size: self.cube_size.try_convert()?,
iso_surface_threshold: self.iso_surface_threshold.try_convert()?,
domain_aabb: map_option!(&self.domain_aabb, aabb => aabb.try_convert()?),
enable_multi_threading: self.enable_multi_threading,
})
}
}
#[derive(Clone, Debug)]
pub struct SurfaceReconstruction<I: Index, R: Real> {
grid: UniformGrid<I, R>,
density_map: Option<DensityMap<I, R>>,
mesh: TriMesh3d<R>,
}
impl<I: Index, R: Real> Default for SurfaceReconstruction<I, R> {
fn default() -> Self {
Self {
grid: UniformGrid::new_zero(),
density_map: None,
mesh: TriMesh3d::default(),
}
}
}
impl<I: Index, R: Real> SurfaceReconstruction<I, R> {
pub fn mesh(&self) -> &TriMesh3d<R> {
&self.mesh
}
pub fn density_map(&self) -> Option<&DensityMap<I, R>> {
self.density_map.as_ref()
}
pub fn grid(&self) -> &UniformGrid<I, R> {
&self.grid
}
}
impl<I: Index, R: Real> From<SurfaceReconstruction<I, R>> for TriMesh3d<R> {
fn from(result: SurfaceReconstruction<I, R>) -> Self {
result.mesh
}
}
#[non_exhaustive]
#[derive(Debug, ThisError)]
pub enum ReconstructionError<I: Index, R: Real> {
#[error("grid construction: {0}")]
GridConstructionError(GridConstructionError<I, R>),
#[error("unknown error")]
Unknown(anyhow::Error),
}
impl<I: Index, R: Real> From<GridConstructionError<I, R>> for ReconstructionError<I, R> {
fn from(error: GridConstructionError<I, R>) -> Self {
ReconstructionError::GridConstructionError(error)
}
}
impl<I: Index, R: Real> From<anyhow::Error> for ReconstructionError<I, R> {
fn from(error: anyhow::Error) -> Self {
ReconstructionError::Unknown(error)
}
}
#[inline(never)]
pub fn reconstruct_surface<I: Index, R: Real>(
particle_positions: &[Vector3<R>],
parameters: &Parameters<R>,
) -> Result<SurfaceReconstruction<I, R>, ReconstructionError<I, R>> {
profile!("reconstruct_surface");
let mut surface = SurfaceReconstruction::default();
reconstruct_surface_inplace(particle_positions, parameters, &mut surface)?;
Ok(surface)
}
pub fn reconstruct_surface_inplace<'a, I: Index, R: Real>(
particle_positions: &[Vector3<R>],
parameters: &Parameters<R>,
surface: &'a mut SurfaceReconstruction<I, R>,
) -> Result<(), ReconstructionError<I, R>> {
profile!("reconstruct_surface_inplace");
let Parameters {
particle_radius,
rest_density,
kernel_radius,
splash_detection_radius,
cube_size,
iso_surface_threshold,
domain_aabb,
enable_multi_threading,
} = parameters.clone();
surface.grid = grid_for_reconstruction(
particle_positions,
particle_radius,
cube_size,
domain_aabb.as_ref(),
)?;
let grid = &surface.grid;
info!(
"Using a grid with {:?}x{:?}x{:?} points and {:?}x{:?}x{:?} cells of edge length {}.",
grid.points_per_dim()[0],
grid.points_per_dim()[1],
grid.points_per_dim()[2],
grid.cells_per_dim()[0],
grid.cells_per_dim()[1],
grid.cells_per_dim()[2],
grid.cell_size()
);
info!("The resulting domain size is: {:?}", grid.aabb());
let particle_rest_density = rest_density;
let particle_rest_volume =
R::from_f64((4.0 / 3.0) * std::f64::consts::PI).unwrap() * particle_radius.powi(3);
let particle_rest_mass = particle_rest_volume * particle_rest_density;
let particle_densities = {
info!("Starting neighborhood search...");
let particle_neighbor_lists = neighborhood_search::search::<I, R>(
&grid.aabb(),
particle_positions,
kernel_radius,
enable_multi_threading,
);
info!("Computing particle densities...");
density_map::compute_particle_densities::<I, R>(
particle_positions,
particle_neighbor_lists.as_slice(),
kernel_radius,
particle_rest_mass,
enable_multi_threading,
)
};
let particle_indices = splash_detection_radius.map(|splash_detection_radius| {
let neighborhood_list = neighborhood_search::search::<I, R>(
&grid.aabb(),
particle_positions,
splash_detection_radius,
enable_multi_threading,
);
let mut active_particles = Vec::new();
for (particle_i, neighbors) in neighborhood_list.iter().enumerate() {
if !neighbors.is_empty() {
active_particles.push(particle_i);
}
}
active_particles
});
let density_map = density_map::generate_sparse_density_map::<I, R>(
&grid,
particle_positions,
particle_densities.as_slice(),
particle_indices.as_ref().map(|is| is.as_slice()),
particle_rest_mass,
kernel_radius,
cube_size,
enable_multi_threading,
);
marching_cubes::triangulate_density_map::<I, R>(
&grid,
&density_map,
iso_surface_threshold,
&mut surface.mesh,
);
surface.density_map = Some(density_map);
Ok(())
}
pub fn grid_for_reconstruction<I: Index, R: Real>(
particle_positions: &[Vector3<R>],
particle_radius: R,
cube_size: R,
domain_aabb: Option<&AxisAlignedBoundingBox3d<R>>,
) -> Result<UniformGrid<I, R>, ReconstructionError<I, R>> {
let domain_aabb = if let Some(domain_aabb) = domain_aabb {
domain_aabb.clone()
} else {
profile!("compute minimum enclosing aabb");
let mut domain_aabb = {
let mut aabb = AxisAlignedBoundingBox3d::from_points(particle_positions);
aabb.grow_uniformly(particle_radius);
aabb
};
info!(
"Minimal enclosing bounding box of particles was computed as: {:?}",
domain_aabb
);
domain_aabb.scale_uniformly(R::one().times(2));
domain_aabb
};
Ok(UniformGrid::from_aabb(&domain_aabb, cube_size)?)
}