pub mod ambiguity;
pub mod interior;
pub mod table;
pub mod trilinear;
#[cfg(test)]
mod reference;
#[cfg(test)]
mod tests;
use alloc::vec::Vec;
use crate::cube::corner_offset;
use crate::vec3;
use crate::{MeshSink, Real, Sdf, Shape3};
pub use ambiguity::FaceAmbiguity;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum InteriorAmbiguity {
#[default]
Ignore,
Trilinear,
}
use ambiguity::joined_mask;
use table::{
AMBIGUOUS_FACES, CASES, EDGE_AXIS, EDGE_CORNERS, NO_EDGE, is_inside, segment_links, triangulate,
};
#[derive(Debug)]
pub struct MarchingCubes<R: Real> {
values: Vec<R>,
edge_vertices: Vec<u32>,
face_ambiguity: FaceAmbiguity,
interior_ambiguity: InteriorAmbiguity,
crossing_refinement: u32,
}
impl<R: Real> MarchingCubes<R> {
#[must_use]
pub const fn new() -> Self {
Self {
values: Vec::new(),
edge_vertices: Vec::new(),
face_ambiguity: FaceAmbiguity::Separate,
interior_ambiguity: InteriorAmbiguity::Ignore,
crossing_refinement: 0,
}
}
pub fn set_face_ambiguity(&mut self, face_ambiguity: FaceAmbiguity) {
self.face_ambiguity = face_ambiguity;
}
pub fn set_interior_ambiguity(&mut self, interior_ambiguity: InteriorAmbiguity) {
self.interior_ambiguity = interior_ambiguity;
}
pub fn set_crossing_refinement(&mut self, steps: u32) {
self.crossing_refinement = steps;
}
pub fn extract<S, M>(
&mut self,
sdf: &S,
shape: &impl Shape3,
origin: [R; 3],
cell_size: R,
out: &mut M,
) -> crate::Result<()>
where
S: Sdf<Scalar = R>,
M: MeshSink<Scalar = R>,
{
let size = shape.size();
if size[0] < 2 || size[1] < 2 || size[2] < 2 {
return Err(crate::Error::GridTooSmall { size });
}
let sample_count = shape.element_count();
let cells = u64::from(size[0] - 1) * u64::from(size[1] - 1) * u64::from(size[2] - 1);
let per_cell = if table::MAX_CENTROIDS > trilinear::MAX_INTERIOR_VERTICES {
table::MAX_CENTROIDS
} else {
trilinear::MAX_INTERIOR_VERTICES
};
let bound = 3u64 * sample_count as u64 + per_cell as u64 * cells;
if bound > u64::from(u32::MAX) {
return Err(crate::Error::IndexSpaceExhausted { needed: bound });
}
self.values.clear();
self.values.reserve(sample_count);
for z in 0..size[2] {
for y in 0..size[1] {
for x in 0..size[0] {
let p = [
origin[0] + cell_size * R::from_f64(f64::from(x)),
origin[1] + cell_size * R::from_f64(f64::from(y)),
origin[2] + cell_size * R::from_f64(f64::from(z)),
];
self.values.push(sdf.sample(p));
}
}
}
debug_assert_eq!(self.values.len(), sample_count);
self.edge_vertices.clear();
self.edge_vertices.resize(sample_count * 3, u32::MAX);
for z in 0..size[2] - 1 {
for y in 0..size[1] - 1 {
for x in 0..size[0] - 1 {
let base = [x, y, z];
let mut case = 0u8;
let mut corner_value = [R::ZERO; 8];
for (c, slot) in corner_value.iter_mut().enumerate() {
let s = corner_sample(shape, base, c as u8);
let v = self.values[s as usize];
*slot = v;
if is_inside(v) {
case |= 1 << c;
}
}
let ambiguous = match self.face_ambiguity {
FaceAmbiguity::Separate => 0,
FaceAmbiguity::AsymptoticDecider => AMBIGUOUS_FACES[case as usize],
};
let mask = if ambiguous == 0 {
0
} else {
joined_mask(&corner_value, ambiguous)
};
if ambiguous != 0 && self.interior_ambiguity == InteriorAmbiguity::Trilinear {
self.emit_trilinear(
sdf,
shape,
base,
case,
mask,
&corner_value,
origin,
cell_size,
out,
)?;
continue;
}
let entry = if ambiguous == 0 {
CASES[case as usize]
} else {
triangulate(segment_links(case, mask))
};
if entry.count == 0 {
continue;
}
let mut centroid = [0u32; table::MAX_CENTROIDS];
for (c, slot) in centroid
.iter_mut()
.enumerate()
.take(entry.centroids as usize)
{
let code = table::CENTROID_BASE + c as u8;
let mut sum = [R::ZERO; 3];
let mut n = 0u32;
for tri in &entry.triangles[..entry.count as usize] {
if tri[0] != code {
continue;
}
let position = edge_position(
sdf,
base,
tri[1],
&corner_value,
origin,
cell_size,
self.crossing_refinement,
);
sum = [
sum[0] + position[0],
sum[1] + position[1],
sum[2] + position[2],
];
n += 1;
}
debug_assert!(n >= 4, "a centroid stands for a cycle of four or more");
let scale = R::from_f64(f64::from(n)).recip();
let position = [sum[0] * scale, sum[1] * scale, sum[2] * scale];
*slot = out.vertex(position, unit_gradient(sdf, position));
}
for tri in &entry.triangles[..entry.count as usize] {
let mut idx = [0u32; 3];
for (k, &code) in tri.iter().enumerate() {
debug_assert!(code != NO_EDGE);
idx[k] = if table::is_centroid(code) {
centroid[(code - table::CENTROID_BASE) as usize]
} else {
self.vertex_on_edge(
sdf,
shape,
base,
code,
&corner_value,
origin,
cell_size,
out,
)
};
}
out.triangle(idx[0], idx[1], idx[2]);
}
}
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn emit_trilinear<S, M>(
&mut self,
sdf: &S,
shape: &impl Shape3,
base: [u32; 3],
case: u8,
mask: u8,
corner_value: &[R; 8],
origin: [R; 3],
cell_size: R,
out: &mut M,
) -> crate::Result<()>
where
S: Sdf<Scalar = R>,
M: MeshSink<Scalar = R>,
{
use trilinear::{BodySaddles, Contours, INTERIOR, MAX_INTERIOR_VERTICES, Topology};
let contours = Contours::of(case, mask);
if contours.count() == 0 {
return Ok(());
}
let saddles = BodySaddles::of(corner_value);
if contours.topology(&saddles) == Topology::SeparateDisks {
return Err(crate::Error::UnresolvedSixSaddle {
case,
mask,
longest: contours.longest(),
});
}
let to_world = |p: [R; 3]| {
[
origin[0] + cell_size * (R::from_f64(f64::from(base[0])) + p[0]),
origin[1] + cell_size * (R::from_f64(f64::from(base[1])) + p[1]),
origin[2] + cell_size * (R::from_f64(f64::from(base[2])) + p[2]),
]
};
let mut interior = [u32::MAX; MAX_INTERIOR_VERTICES];
let hexagon = saddles.inner_hexagon();
if let Some(ring) = hexagon {
for (slot, local) in interior.iter_mut().zip(ring) {
let position = to_world(local);
*slot = out.vertex(position, unit_gradient(sdf, position));
}
} else if let Some(local) = saddles.interior_vertex() {
debug_assert!(
local.iter().all(|&c| c >= R::ZERO && c <= R::ONE),
"interior vertex outside the cell: {local:?} mask {:#08b}",
saddles.inside_mask()
);
let position = to_world(local);
interior[0] = out.vertex(position, unit_gradient(sdf, position));
}
let mut triangles = [[0u8; 3]; trilinear::MAX_PATCH_TRIANGLES];
let mut count = 0usize;
if hexagon.is_some() {
let unresolved = contours.fan_tunnel(&saddles, corner_value, |t| {
triangles[count] = t;
count += 1;
});
if unresolved != 0 {
return Err(crate::Error::UnresolvedTunnel {
case,
mask,
edges: unresolved,
});
}
} else {
let fanned = saddles.interior_vertex().is_some();
contours.fan(fanned, |t| {
triangles[count] = t;
count += 1;
});
}
for tri in &triangles[..count] {
let mut idx = [0u32; 3];
for (k, &code) in tri.iter().enumerate() {
idx[k] = if code >= INTERIOR {
let slot = interior[(code - INTERIOR) as usize];
debug_assert!(
slot != u32::MAX,
"a triangle named an interior vertex that was never created"
);
slot
} else {
self.vertex_on_edge(
sdf,
shape,
base,
code,
corner_value,
origin,
cell_size,
out,
)
};
}
out.triangle(idx[0], idx[1], idx[2]);
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn vertex_on_edge<S, M>(
&mut self,
sdf: &S,
shape: &impl Shape3,
base: [u32; 3],
edge: u8,
corner_value: &[R; 8],
origin: [R; 3],
cell_size: R,
out: &mut M,
) -> u32
where
S: Sdf<Scalar = R>,
M: MeshSink<Scalar = R>,
{
let axis = EDGE_AXIS[edge as usize] as usize;
let lo_sample = corner_sample(shape, base, EDGE_CORNERS[edge as usize][0]);
let key = lo_sample as usize * 3 + axis;
let cached = self.edge_vertices[key];
if cached != u32::MAX {
return cached;
}
let position = edge_position(
sdf,
base,
edge,
corner_value,
origin,
cell_size,
self.crossing_refinement,
);
let index = out.vertex(position, unit_gradient(sdf, position));
self.edge_vertices[key] = index;
index
}
}
fn edge_position<S, R: Real>(
sdf: &S,
base: [u32; 3],
edge: u8,
corner_value: &[R; 8],
origin: [R; 3],
cell_size: R,
refinement: u32,
) -> [R; 3]
where
S: Sdf<Scalar = R>,
{
let [lo_corner, hi_corner] = EDGE_CORNERS[edge as usize];
let a = corner_value[lo_corner as usize];
let b = corner_value[hi_corner as usize];
debug_assert!(is_inside(a) != is_inside(b));
let t = a / (a - b);
let lo_pos = corner_position(base, lo_corner, origin, cell_size);
let hi_pos = corner_position(base, hi_corner, origin, cell_size);
let t = refine_crossing(sdf, lo_pos, hi_pos, a, t, refinement);
[
lo_pos[0] + (hi_pos[0] - lo_pos[0]) * t,
lo_pos[1] + (hi_pos[1] - lo_pos[1]) * t,
lo_pos[2] + (hi_pos[2] - lo_pos[2]) * t,
]
}
fn refine_crossing<S, R>(sdf: &S, lo: [R; 3], hi: [R; 3], a: R, t0: R, steps: u32) -> R
where
S: Sdf<Scalar = R>,
R: Real,
{
if steps == 0 {
return t0;
}
let (mut low, mut high) = (R::ZERO, R::ONE);
let a_inside = is_inside(a);
let mut t = t0;
for _ in 0..steps {
let p = [
lo[0] + (hi[0] - lo[0]) * t,
lo[1] + (hi[1] - lo[1]) * t,
lo[2] + (hi[2] - lo[2]) * t,
];
if is_inside(sdf.sample(p)) == a_inside {
low = t;
} else {
high = t;
}
t = (low + high) * R::HALF;
}
t
}
#[inline]
fn unit_gradient<R: Real, S: Sdf<Scalar = R>>(sdf: &S, position: [R; 3]) -> [R; 3] {
let g = sdf.gradient(position);
let len = vec3::length(g);
debug_assert!(len > R::ZERO, "zero gradient at a surface vertex");
vec3::scale(g, len.recip())
}
impl<R: Real> Default for MarchingCubes<R> {
fn default() -> Self {
Self::new()
}
}
#[inline]
fn corner_sample(shape: &impl Shape3, base: [u32; 3], corner: u8) -> u32 {
let o = corner_offset(corner);
shape.linearize([base[0] + o[0], base[1] + o[1], base[2] + o[2]])
}
#[inline]
fn corner_position<R: Real>(base: [u32; 3], corner: u8, origin: [R; 3], cell_size: R) -> [R; 3] {
let o = corner_offset(corner);
[
origin[0] + cell_size * R::from_f64(f64::from(base[0] + o[0])),
origin[1] + cell_size * R::from_f64(f64::from(base[1] + o[1])),
origin[2] + cell_size * R::from_f64(f64::from(base[2] + o[2])),
]
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct TableReport {
pub triangles_on_uncut_edges: u64,
pub cut_edge_mismatch: u64,
pub degenerate_triangles: u64,
pub bad_segment_degree: u64,
pub bad_centroid_reference: u64,
pub face_disagreements: u64,
pub max_triangles: u64,
}
impl TableReport {
#[must_use]
pub fn is_sound(&self) -> bool {
self.triangles_on_uncut_edges == 0
&& self.cut_edge_mismatch == 0
&& self.degenerate_triangles == 0
&& self.bad_segment_degree == 0
&& self.bad_centroid_reference == 0
&& self.face_disagreements == 0
}
}
type FaceMemo = [[[Option<[u8; table::EDGE_COUNT]>; 2]; 16]; 6];
#[must_use]
pub fn validate_table() -> TableReport {
let mut report = TableReport::default();
let mut face_seen: FaceMemo = [[[None; 2]; 16]; 6];
for case in 0..=255u8 {
check_case(case, 0, &CASES[case as usize], &mut report, &mut face_seen);
}
report
}
#[must_use]
pub fn validate_decider_table() -> TableReport {
let mut report = TableReport::default();
let mut face_seen: FaceMemo = [[[None; 2]; 16]; 6];
for case in 0..=255u8 {
for mask in 0..(1u8 << table::FACE_COUNT) {
let entry = triangulate(segment_links(case, mask));
check_case(case, mask, &entry, &mut report, &mut face_seen);
}
}
report
}
const fn pattern_is_ambiguous(pattern: usize) -> bool {
pattern == 0b0101 || pattern == 0b1010
}
fn check_case(
case: u8,
mask: u8,
entry: &table::McCase,
report: &mut TableReport,
face_seen: &mut FaceMemo,
) {
use table::{EDGE_COUNT, corner_inside, edge_index, face_bit, face_corners};
report.max_triangles = report.max_triangles.max(u64::from(entry.count));
let mut cut = [false; EDGE_COUNT];
for (e, slot) in cut.iter_mut().enumerate() {
let [a, b] = EDGE_CORNERS[e];
*slot = corner_inside(case, a) != corner_inside(case, b);
}
let mut used = [false; EDGE_COUNT];
let mut centroid_uses = [0u32; table::MAX_CENTROIDS];
for tri in &entry.triangles[..entry.count as usize] {
if tri[0] == tri[1] || tri[1] == tri[2] || tri[0] == tri[2] {
report.degenerate_triangles += 1;
}
for &code in tri {
if table::is_centroid(code) {
let c = (code - table::CENTROID_BASE) as usize;
if c >= entry.centroids as usize {
report.bad_centroid_reference += 1;
} else {
centroid_uses[c] += 1;
}
continue;
}
if !cut[code as usize] {
report.triangles_on_uncut_edges += 1;
}
used[code as usize] = true;
}
}
if used != cut {
report.cut_edge_mismatch += 1;
}
for &uses in centroid_uses.iter().take(entry.centroids as usize) {
if uses < 4 {
report.bad_centroid_reference += 1;
}
}
let links = segment_links(case, mask);
let mut incoming = [0u8; EDGE_COUNT];
for e in 0..EDGE_COUNT {
if links[e] != NO_EDGE {
incoming[links[e] as usize] += 1;
}
}
for e in 0..EDGE_COUNT {
let out_degree = u8::from(links[e] != NO_EDGE);
if cut[e] != (out_degree == 1) || incoming[e] != out_degree {
report.bad_segment_degree += 1;
}
}
for axis in 0..3usize {
for side in 0..2u8 {
let f = axis * 2 + side as usize;
let c = face_corners(axis, side);
let mut pattern = 0usize;
for (k, &corner) in c.iter().enumerate() {
if corner_inside(case, corner) {
pattern |= 1 << k;
}
}
let bit =
usize::from(mask & face_bit(axis, side) != 0 && pattern_is_ambiguous(pattern));
let mut segments = [NO_EDGE; EDGE_COUNT];
if let Some(start) = (0..4).find(|&k| !corner_inside(case, c[k])) {
let mut entries = [NO_EDGE; 2];
let mut exits = [NO_EDGE; 2];
let mut pairs = 0usize;
for j in 0..4 {
let p = c[(start + j) % 4];
let q = c[(start + j + 1) % 4];
match (corner_inside(case, p), corner_inside(case, q)) {
(false, true) => entries[pairs] = edge_index(p, q),
(true, false) => {
exits[pairs] = edge_index(p, q);
pairs += 1;
}
_ => {}
}
}
if pairs == 2 && bit == 1 {
segments[entries[0] as usize] = exits[1];
segments[entries[1] as usize] = exits[0];
} else {
for n in 0..pairs {
segments[entries[n] as usize] = exits[n];
}
}
}
match face_seen[f][pattern][bit] {
None => face_seen[f][pattern][bit] = Some(segments),
Some(previous) => {
if previous != segments {
report.face_disagreements += 1;
}
}
}
}
}
}