Skip to content

Commit 919d3dc

Browse files
committed
Fix bug with edges going from above threshold to outside support
See #215
1 parent a8343ff commit 919d3dc

File tree

5 files changed

+159
-21
lines changed

5 files changed

+159
-21
lines changed

splashsurf_lib/src/density_map.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -528,7 +528,7 @@ pub(crate) fn compute_kernel_evaluation_radius<I: Index, R: Real>(
528528
// The number of points corresponding to the number of supported cells
529529
let supported_points: I = I::one() + supported_cells;
530530

531-
// Evaluate kernel in a smaller domain, points outside of this radius have to be assumed to be outside of the iso-surface
531+
// Evaluate kernel in a smaller domain, points outside of this radius have to be assumed to be outside the iso-surface
532532
let kernel_evaluation_radius =
533533
cube_size * half_supported_cells_real * (R::one() + R::default_epsilon().sqrt());
534534

@@ -599,7 +599,7 @@ impl<I: Index, R: Real> SparseDensityMapGenerator<I, R> {
599599
particle: &Vector3<R>,
600600
particle_density: R,
601601
) {
602-
// Skip particles outside of allowed domain
602+
// Skip particles outside allowed domain
603603
if !self.allowed_domain.contains_point(particle) {
604604
return;
605605
}

splashsurf_lib/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,7 @@ pub fn grid_for_reconstruction<I: Index, R: Real>(
412412
} else {
413413
Aabb3d::from_points(particle_positions)
414414
};
415-
// TODO: Is this really necessary?
415+
// TODO: Is this really necessary? This seems unnecessary purely for the density map...
416416
aabb.grow_uniformly(particle_radius);
417417
aabb
418418
};

splashsurf_lib/src/marching_cubes/narrow_band_extraction.rs

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ pub(crate) fn construct_mc_input<I: Index, R: Real>(
4545
/// Note: The threshold flags in the resulting cell data are not complete and still have to be updated after
4646
/// this procedure using the [update_cell_data_threshold_flags] function.
4747
///
48-
/// Note: This functions assumes that the default value for missing point data is below the iso-surface threshold.
48+
/// Note: This functions assumes that the default value for missing point data is zero.
4949
#[inline(never)]
5050
fn interpolate_points_to_cell_data_generic<I: Index, R: Real>(
5151
grid: &UniformGrid<I, R>,
@@ -66,15 +66,13 @@ fn interpolate_points_to_cell_data_generic<I: Index, R: Real>(
6666
density_map.for_each(|flat_point_index, point_value| {
6767
let point = grid.try_unflatten_point_index(flat_point_index).unwrap();
6868

69-
// We want to find edges that cross the iso-surface,
70-
// therefore we can choose to either skip all points above or below the threshold.
71-
//
72-
// In most scenes, the sparse density map should contain more entries above than
73-
// below the threshold, as it contains the whole fluid interior, whereas areas completely
74-
// devoid of fluid are not part of the density map.
75-
//
76-
// Skip points with densities above the threshold to improve efficiency
77-
if point_value > iso_surface_threshold {
69+
// We want to find edges that cross the iso-surface, i.e. edges where one point is above
70+
// and the other below the threshold. To not process an edge twice, we skip all points
71+
// that are already below the iso-surface threshold. Note that we cannot do it the other
72+
// way around, as some points below the threshold might not be part of the density map
73+
// at all (e.g. points outside the kernel evaluation radius). This could lead to missing
74+
// edges that go directly from above the threshold to e.g. zero.
75+
if point_value < iso_surface_threshold {
7876
return;
7977
}
8078

@@ -89,15 +87,13 @@ fn interpolate_points_to_cell_data_generic<I: Index, R: Real>(
8987
let neighbor_value = if let Some(v) = density_map.get(flat_neighbor_index) {
9088
v
9189
} else {
92-
// Neighbors that are not in the point-value map were outside of the kernel evaluation radius.
93-
// This should only happen for cells that are completely outside of the compact support of a particle.
94-
// The point-value map has to be consistent such that for each cell, where at least one point-value
95-
// is missing like this, the cell has to be completely below the iso-surface threshold.
96-
continue;
90+
// Neighbors that are not in the point-value map were outside the kernel evaluation radius.
91+
// Assume zero density for these points.
92+
R::zero()
9793
};
9894

9995
// Skip edges that don't cross the iso-surface
100-
if !(neighbor_value > iso_surface_threshold) {
96+
if !(neighbor_value < iso_surface_threshold) {
10197
continue;
10298
}
10399

@@ -132,8 +128,8 @@ fn interpolate_points_to_cell_data_generic<I: Index, R: Real>(
132128
);
133129
cell_data_entry.iso_surface_vertices[local_edge_index] = Some(vertex_index);
134130

135-
// Mark the neighbor as above the iso-surface threshold
136-
let local_vertex_index = cell.local_point_index_of(neighbor.index()).unwrap();
131+
// Mark the corner of the current point as above the iso-surface threshold
132+
let local_vertex_index = cell.local_point_index_of(point.index()).unwrap();
137133
cell_data_entry.corner_above_threshold[local_vertex_index] =
138134
RelativeToThreshold::Above;
139135
}

splashsurf_lib/tests/integration_tests/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ pub mod test_full;
33
#[cfg(feature = "io")]
44
pub mod test_mesh;
55
pub mod test_neighborhood_search;
6+
pub mod test_simple;
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
use nalgebra::Vector3;
2+
use splashsurf_lib::marching_cubes::check_mesh_consistency;
3+
use splashsurf_lib::{
4+
reconstruct_surface, Aabb3d, GridDecompositionParameters, Parameters, Real,
5+
SpatialDecomposition,
6+
};
7+
8+
enum Strategy {
9+
Global,
10+
SubdomainGrid,
11+
}
12+
13+
fn params_with_aabb<R: Real>(
14+
particle_radius: R,
15+
compact_support_radius: R,
16+
cube_size: R,
17+
iso_surface_threshold: R,
18+
domain_aabb: Option<Aabb3d<R>>,
19+
strategy: Strategy,
20+
) -> Parameters<R> {
21+
let compact_support_radius = particle_radius * compact_support_radius;
22+
let cube_size = particle_radius * cube_size;
23+
24+
let mut parameters = Parameters {
25+
particle_radius,
26+
rest_density: R::from_f64(1000.0).unwrap(),
27+
compact_support_radius,
28+
cube_size,
29+
iso_surface_threshold,
30+
particle_aabb: domain_aabb,
31+
enable_multi_threading: false,
32+
spatial_decomposition: None,
33+
global_neighborhood_list: false,
34+
};
35+
36+
match strategy {
37+
Strategy::Global => {}
38+
Strategy::SubdomainGrid => {
39+
parameters.spatial_decomposition = Some(SpatialDecomposition::UniformGrid(
40+
GridDecompositionParameters {
41+
subdomain_num_cubes_per_dim: 64,
42+
},
43+
))
44+
}
45+
}
46+
47+
parameters
48+
}
49+
50+
fn params<R: Real>(
51+
particle_radius: R,
52+
compact_support_radius: R,
53+
cube_size: R,
54+
iso_surface_threshold: R,
55+
strategy: Strategy,
56+
) -> Parameters<R> {
57+
params_with_aabb(
58+
particle_radius,
59+
compact_support_radius,
60+
cube_size,
61+
iso_surface_threshold,
62+
None,
63+
strategy,
64+
)
65+
}
66+
67+
/// This tests ensures that a surface is reconstructed properly if a single edge is above the threshold
68+
/// on one side but outside the compact support on the other side.
69+
#[test]
70+
fn test_edge_above_threshold_to_outside_of_compact_support_global() {
71+
let particle_positions: Vec<Vector3<f32>> = vec![Vector3::new(0.01, 0.0, 0.0)];
72+
let parameters = params(1.0, 1.0, 1.0, 0.1, Strategy::Global);
73+
let reconstruction =
74+
reconstruct_surface::<i64, _>(particle_positions.as_slice(), &parameters).unwrap();
75+
76+
println!(
77+
"Reconstructed mesh from {} particles has {} triangles.",
78+
particle_positions.len(),
79+
reconstruction.mesh().triangles.len()
80+
);
81+
82+
assert_eq!(
83+
reconstruction.mesh().vertices.len(),
84+
6,
85+
"Number of vertices"
86+
);
87+
assert_eq!(
88+
reconstruction.mesh().triangles.len(),
89+
8,
90+
"Number of triangles"
91+
);
92+
93+
// Ensure that the mesh does not have a boundary
94+
if let Err(e) = check_mesh_consistency(
95+
reconstruction.grid(),
96+
reconstruction.mesh(),
97+
true,
98+
true,
99+
true,
100+
) {
101+
eprintln!("{}", e);
102+
panic!("Mesh contains topological/manifold errors");
103+
}
104+
}
105+
106+
#[test]
107+
fn test_edge_above_threshold_to_outside_of_compact_support_subdomains() {
108+
let particle_positions: Vec<Vector3<f32>> = vec![Vector3::new(0.01, 0.0, 0.0)];
109+
let parameters = params(1.0, 1.0, 1.0, 0.1, Strategy::SubdomainGrid);
110+
let reconstruction =
111+
reconstruct_surface::<i64, _>(particle_positions.as_slice(), &parameters).unwrap();
112+
113+
println!(
114+
"Reconstructed mesh from {} particles has {} triangles.",
115+
particle_positions.len(),
116+
reconstruction.mesh().triangles.len()
117+
);
118+
119+
assert_eq!(
120+
reconstruction.mesh().vertices.len(),
121+
6,
122+
"Number of vertices"
123+
);
124+
assert_eq!(
125+
reconstruction.mesh().triangles.len(),
126+
8,
127+
"Number of triangles"
128+
);
129+
130+
// Ensure that the mesh does not have a boundary
131+
if let Err(e) = check_mesh_consistency(
132+
reconstruction.grid(),
133+
reconstruction.mesh(),
134+
true,
135+
true,
136+
true,
137+
) {
138+
eprintln!("{}", e);
139+
panic!("Mesh contains topological/manifold errors");
140+
}
141+
}

0 commit comments

Comments
 (0)