Skip to content

Commit dba1393

Browse files
committed
Py: Add plain marching cubes functionality
1 parent 13b4aea commit dba1393

File tree

6 files changed

+128
-5
lines changed

6 files changed

+128
-5
lines changed

pysplashsurf/pysplashsurf/pysplashsurf.pyi

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,11 @@ def laplacian_smoothing_parallel(mesh:typing.Union[TriMesh3d, MeshWithData], ver
353353
The smoothing is performed inplace and modifies the vertices of the given mesh.
354354
"""
355355

356+
def marching_cubes(values:numpy.typing.NDArray[typing.Any], *, cube_size:builtins.float, iso_surface_threshold:builtins.float, translation:typing.Optional[typing.Sequence[builtins.float]]=None) -> tuple[TriMesh3d, UniformGrid]:
357+
r"""
358+
Performs a standard marching cubes triangulation of a 3D array of values
359+
"""
360+
356361
def marching_cubes_cleanup(mesh:typing.Union[TriMesh3d, MeshWithData], grid:UniformGrid, *, max_rel_snap_dist:typing.Optional[builtins.float]=None, max_iter:builtins.int=5, keep_vertices:builtins.bool=False) -> typing.Union[TriMesh3d, MeshWithData]:
357362
r"""
358363
Performs simplification on the given mesh inspired by the "Compact Contouring"/"Mesh displacement" approach by Doug Moore and Joe Warren

pysplashsurf/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ fn pysplashsurf(m: &Bound<'_, PyModule>) -> PyResult<()> {
4444

4545
m.add_function(wrap!(reconstruction::reconstruct_surface, m)?)?;
4646
m.add_function(wrap!(marching_cubes::check_mesh_consistency, m)?)?;
47+
m.add_function(wrap!(marching_cubes::marching_cubes, m)?)?;
4748
m.add_function(wrap!(postprocessing::marching_cubes_cleanup, m)?)?;
4849
m.add_function(wrap!(postprocessing::convert_tris_to_quads, m)?)?;
4950
m.add_function(wrap!(postprocessing::barnacle_decimation, m)?)?;

pysplashsurf/src/marching_cubes.rs

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1-
use numpy::PyUntypedArray;
1+
use numpy::prelude::*;
2+
use numpy::{Element, PyArray3, PyUntypedArray};
23
use pyo3::prelude::*;
34
use pyo3_stub_gen::derive::*;
5+
use splashsurf_lib::nalgebra::Vector3;
6+
use splashsurf_lib::{DensityMap, Real, UniformGrid};
47

58
use crate::mesh::{PyTriMesh3d, get_triangle_mesh_generic};
69
use crate::uniform_grid::PyUniformGrid;
7-
use crate::utils::*;
10+
use crate::utils;
11+
use crate::utils::IndexT;
812

913
/// Checks the consistency of a reconstructed surface mesh (watertightness, manifoldness), optionally returns a string with details if problems are found
1014
#[gen_stub_pyfunction]
@@ -22,7 +26,7 @@ pub fn check_mesh_consistency<'py>(
2226
let py = mesh.py();
2327

2428
// Try to extract the triangle mesh;
25-
let mesh = get_triangle_mesh_generic(&mesh).ok_or_else(pyerr_only_triangle_mesh)?;
29+
let mesh = get_triangle_mesh_generic(&mesh).ok_or_else(utils::pyerr_only_triangle_mesh)?;
2630
let mesh = mesh.borrow(py);
2731

2832
if let (Some(grid), Some(mesh)) = (grid.as_f32(), mesh.as_f32()) {
@@ -44,6 +48,66 @@ pub fn check_mesh_consistency<'py>(
4448
)
4549
.err())
4650
} else {
47-
Err(pyerr_scalar_type_mismatch())
51+
Err(utils::pyerr_scalar_type_mismatch())
52+
}
53+
}
54+
55+
/// Performs a standard marching cubes triangulation of a 3D array of values
56+
#[gen_stub_pyfunction]
57+
#[pyfunction]
58+
#[pyo3(name = "marching_cubes")]
59+
#[pyo3(signature = (values, *, cube_size, iso_surface_threshold, translation = None))]
60+
pub fn marching_cubes<'py>(
61+
values: &Bound<'py, PyUntypedArray>,
62+
cube_size: f64,
63+
iso_surface_threshold: f64,
64+
translation: Option<[f64; 3]>,
65+
) -> PyResult<(PyTriMesh3d, PyUniformGrid)> {
66+
assert_eq!(values.shape().len(), 3, "values must be a 3D array");
67+
68+
fn triangulate_density_map_generic<'py, R: Real + Element>(
69+
values: &Bound<'py, PyArray3<R>>,
70+
cube_size: R,
71+
iso_surface_threshold: R,
72+
translation: Option<[R; 3]>,
73+
) -> PyResult<(PyTriMesh3d, PyUniformGrid)> {
74+
let shape = values.shape();
75+
let translation = Vector3::from(translation.unwrap_or([R::zero(); 3]));
76+
let n_cells_per_dim = [
77+
shape[0] as IndexT - 1,
78+
shape[1] as IndexT - 1,
79+
shape[2] as IndexT - 1,
80+
];
81+
82+
let grid = UniformGrid::new(&translation, &n_cells_per_dim, cube_size)
83+
.map_err(anyhow::Error::from)?;
84+
85+
// TODO: Replace with borrow
86+
let values = values.try_readonly()?.as_slice()?.to_vec();
87+
let density_map = DensityMap::from(values);
88+
89+
let mesh = splashsurf_lib::marching_cubes::triangulate_density_map(
90+
&grid,
91+
&density_map,
92+
iso_surface_threshold,
93+
)
94+
.map_err(anyhow::Error::from)?;
95+
Ok((
96+
PyTriMesh3d::try_from_generic(mesh)?,
97+
PyUniformGrid::try_from_generic(grid)?,
98+
))
99+
}
100+
101+
if let Ok(values) = values.downcast::<PyArray3<f32>>() {
102+
triangulate_density_map_generic(
103+
&values,
104+
cube_size as f32,
105+
iso_surface_threshold as f32,
106+
translation.map(|t| t.map(|t| t as f32)),
107+
)
108+
} else if let Ok(values) = values.downcast::<PyArray3<f64>>() {
109+
triangulate_density_map_generic(&values, cube_size, iso_surface_threshold, translation)
110+
} else {
111+
Err(utils::pyerr_unsupported_scalar())
48112
}
49113
}

pysplashsurf/tests/test_basic.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,8 @@ def test_check_consistency():
205205
pysplashsurf.check_mesh_consistency(mesh_with_data, reconstruction.grid) is None
206206
)
207207

208+
# TODO: Delete some triangles and check for failure
209+
208210

209211
def test_tris_to_quads():
210212
particles = np.array(meshio.read(VTK_PATH).points, dtype=np.float32)

pysplashsurf/tests/test_sdf.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import pysplashsurf
2+
import numpy as np
3+
4+
5+
def test_sphere_sdf_mc():
6+
radius = 1.0
7+
num_verts = 100
8+
9+
grid_size = radius * 2.2
10+
dx = grid_size / (num_verts - 1)
11+
12+
translation = -0.5 * grid_size
13+
14+
def make_sdf():
15+
coords = np.arange(num_verts, dtype=np.float32) * dx + translation
16+
x, y, z = np.meshgrid(coords, coords, coords, indexing="ij")
17+
sdf = np.sqrt(x**2 + y**2 + z**2) - radius
18+
return sdf
19+
20+
sdf = make_sdf()
21+
22+
# Note: Currently this reconstruction assumes that inside the surface values get bigger (like a density function)
23+
mesh, grid = pysplashsurf.marching_cubes(
24+
sdf, cube_size=dx, iso_surface_threshold=0.0, translation=[translation] * 3
25+
)
26+
27+
assert len(mesh.vertices) > 0
28+
29+
norms = np.linalg.norm(mesh.vertices, axis=1)
30+
assert norms.min() > radius - 1e-4
31+
assert norms.max() < radius + 1e-4
32+
33+
assert pysplashsurf.check_mesh_consistency(mesh, grid) is None

splashsurf_lib/src/density_map.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,11 +217,12 @@ pub fn parallel_compute_particle_densities<I: Index, R: Real>(
217217
/// A sparse density map
218218
///
219219
/// The density map contains values for all points of the background grid where the density is not
220-
/// trivially zero (which is the case when a point is outside of the compact support of any particles).
220+
/// trivially zero (which is the case when a point is outside the compact support of any particles).
221221
#[derive(Clone, Debug)]
222222
pub enum DensityMap<I: Index, R: Real> {
223223
Standard(MapType<I, R>),
224224
DashMap(ReadDashMap<I, R, HashState>),
225+
Dense(Vec<R>),
225226
}
226227

227228
impl<I: Index, R: Real> Default for DensityMap<I, R> {
@@ -242,12 +243,24 @@ impl<I: Index, R: Real> From<ParallelMapType<I, R>> for DensityMap<I, R> {
242243
}
243244
}
244245

246+
impl<I: Index, R: Real> From<Vec<R>> for DensityMap<I, R> {
247+
fn from(values: Vec<R>) -> Self {
248+
Self::Dense(values)
249+
}
250+
}
251+
245252
impl<I: Index, R: Real> DensityMap<I, R> {
246253
/// Converts the contained map into a vector of tuples of (flat_point_index, density)
247254
pub fn to_vec(&self) -> Vec<(I, R)> {
248255
match self {
249256
DensityMap::Standard(map) => map.iter().map(|(&i, &r)| (i, r)).collect(),
250257
DensityMap::DashMap(map) => map.iter().map(|(&i, &r)| (i, r)).collect(),
258+
DensityMap::Dense(values) => values
259+
.iter()
260+
.copied()
261+
.enumerate()
262+
.map(|(i, r)| (I::from_usize(i).unwrap(), r))
263+
.collect(),
251264
}
252265
}
253266

@@ -256,6 +269,7 @@ impl<I: Index, R: Real> DensityMap<I, R> {
256269
match self {
257270
DensityMap::Standard(map) => map.len(),
258271
DensityMap::DashMap(map) => map.len(),
272+
DensityMap::Dense(values) => values.len(),
259273
}
260274
}
261275

@@ -264,6 +278,7 @@ impl<I: Index, R: Real> DensityMap<I, R> {
264278
match self {
265279
DensityMap::Standard(map) => map.get(&flat_point_index).copied(),
266280
DensityMap::DashMap(map) => map.get(&flat_point_index).copied(),
281+
DensityMap::Dense(values) => values.get(flat_point_index.to_usize()?).copied(),
267282
}
268283
}
269284

@@ -273,6 +288,9 @@ impl<I: Index, R: Real> DensityMap<I, R> {
273288
match self {
274289
DensityMap::Standard(map) => map.iter().for_each(|(&i, &r)| f(i, r)),
275290
DensityMap::DashMap(map) => map.iter().for_each(|(&i, &r)| f(i, r)),
291+
DensityMap::Dense(values) => values.iter().copied().enumerate().for_each(|(i, r)| {
292+
f(I::from_usize(i).unwrap(), r);
293+
}),
276294
}
277295
}
278296
}

0 commit comments

Comments
 (0)