Skip to main content

proof_engine/metaball/
marching_cubes.rs

1//! Isosurface extraction for metaball fields.
2//!
3//! Marching tetrahedra rather than marching cubes: each cell of the grid is
4//! split into six tetrahedra and each tetrahedron is cut by the isosurface
5//! in one of three ways (no crossing, one triangle, two triangles). It needs
6//! no 256-entry case table, has no ambiguous cases, and produces a closed
7//! mesh for any field. The cost is a few more triangles than cubes would
8//! give for the same grid, which a particle renderer does not care about.
9//!
10//! The name is kept from the stub this replaces, so callers need not change.
11
12use glam::{Vec3, Vec4};
13
14use super::entity_field::MetaballEntity;
15
16/// Kept for compatibility; the tetrahedral extractor does not use case tables.
17pub const EDGE_TABLE: [u16; 256] = [0u16; 256];
18/// Kept for compatibility; the tetrahedral extractor does not use case tables.
19pub const TRI_TABLE: [[i8; 16]; 256] = [[-1i8; 16]; 256];
20
21#[derive(Debug, Clone, Default)]
22pub struct MCVertex {
23    pub position: Vec3,
24    pub normal: Vec3,
25    pub color: Vec4,
26    pub emission: f32,
27}
28
29#[derive(Debug, Clone, Default)]
30pub struct ExtractedMesh {
31    pub vertices: Vec<MCVertex>,
32    pub indices: Vec<u32>,
33}
34
35impl ExtractedMesh {
36    pub fn is_empty(&self) -> bool {
37        self.indices.is_empty()
38    }
39
40    pub fn vertex_count(&self) -> usize {
41        self.vertices.len()
42    }
43
44    pub fn triangle_count(&self) -> usize {
45        self.indices.len() / 3
46    }
47}
48
49/// The six tetrahedra of a cube, as indices into its eight corners.
50///
51/// Corner `i` is at `(i & 1, (i >> 1) & 1, (i >> 2) & 1)`. Every tetrahedron
52/// shares the main diagonal from corner 0 to corner 7, which is what makes
53/// the six of them tile the cube without gaps.
54const TETRA: [[usize; 4]; 6] = [
55    [0, 1, 3, 7],
56    [0, 1, 5, 7],
57    [0, 2, 3, 7],
58    [0, 2, 6, 7],
59    [0, 4, 5, 7],
60    [0, 4, 6, 7],
61];
62
63pub struct MarchingCubesExtractor;
64
65impl MarchingCubesExtractor {
66    /// Extract the `threshold` isosurface of `field` over the box, sampling
67    /// `resolution` cells along each axis. Normals are the field's gradient,
68    /// so they are exact for a smooth field rather than averaged from faces.
69    pub fn extract(
70        field: &dyn Fn(Vec3) -> f32,
71        bounds_min: Vec3,
72        bounds_max: Vec3,
73        resolution: u32,
74        threshold: f32,
75    ) -> ExtractedMesh {
76        Self::extract_with(field, &|_| (Vec4::ONE, 0.0), bounds_min, bounds_max, resolution, threshold)
77    }
78
79    /// As [`extract`](Self::extract), with a second function giving each
80    /// vertex a colour and an emission.
81    pub fn extract_with(
82        field: &dyn Fn(Vec3) -> f32,
83        shade: &dyn Fn(Vec3) -> (Vec4, f32),
84        bounds_min: Vec3,
85        bounds_max: Vec3,
86        resolution: u32,
87        threshold: f32,
88    ) -> ExtractedMesh {
89        let mut mesh = ExtractedMesh::default();
90        let res = resolution.clamp(1, 512) as usize;
91        let size = bounds_max - bounds_min;
92        if !(size.x > 0.0 && size.y > 0.0 && size.z > 0.0) {
93            return mesh;
94        }
95        let cell = size / res as f32;
96
97        // Sample the whole lattice once: (res + 1)^3 values.
98        let n = res + 1;
99        let idx = |x: usize, y: usize, z: usize| (z * n + y) * n + x;
100        let mut samples = vec![0.0f32; n * n * n];
101        for z in 0..n {
102            for y in 0..n {
103                for x in 0..n {
104                    let p = bounds_min + Vec3::new(x as f32, y as f32, z as f32) * cell;
105                    samples[idx(x, y, z)] = field(p);
106                }
107            }
108        }
109
110        let eps = cell.min_element() * 0.5;
111        let gradient = |p: Vec3| -> Vec3 {
112            let dx = field(p + Vec3::X * eps) - field(p - Vec3::X * eps);
113            let dy = field(p + Vec3::Y * eps) - field(p - Vec3::Y * eps);
114            let dz = field(p + Vec3::Z * eps) - field(p - Vec3::Z * eps);
115            let g = Vec3::new(dx, dy, dz);
116            if g.length_squared() > 1e-12 { -g.normalize() } else { Vec3::Y }
117        };
118
119        let mut emit_vertex = |p: Vec3, mesh: &mut ExtractedMesh| -> u32 {
120            let (color, emission) = shade(p);
121            mesh.vertices.push(MCVertex { position: p, normal: gradient(p), color, emission });
122            (mesh.vertices.len() - 1) as u32
123        };
124
125        for z in 0..res {
126            for y in 0..res {
127                for x in 0..res {
128                    let mut corner_pos = [Vec3::ZERO; 8];
129                    let mut corner_val = [0.0f32; 8];
130                    for (i, (cp, cv)) in corner_pos.iter_mut().zip(corner_val.iter_mut()).enumerate() {
131                        let cx = x + (i & 1);
132                        let cy = y + ((i >> 1) & 1);
133                        let cz = z + ((i >> 2) & 1);
134                        *cp = bounds_min + Vec3::new(cx as f32, cy as f32, cz as f32) * cell;
135                        *cv = samples[idx(cx, cy, cz)];
136                    }
137                    for tet in &TETRA {
138                        polygonise_tetra(
139                            [corner_pos[tet[0]], corner_pos[tet[1]], corner_pos[tet[2]], corner_pos[tet[3]]],
140                            [corner_val[tet[0]], corner_val[tet[1]], corner_val[tet[2]], corner_val[tet[3]]],
141                            threshold,
142                            &mut |a, b, c| {
143                                let ia = emit_vertex(a, &mut mesh);
144                                let ib = emit_vertex(b, &mut mesh);
145                                let ic = emit_vertex(c, &mut mesh);
146                                mesh.indices.extend_from_slice(&[ia, ib, ic]);
147                            },
148                        );
149                    }
150                }
151            }
152        }
153        mesh
154    }
155
156    /// Extract a metaball entity's surface at its own threshold and
157    /// resolution, coloured by its sources.
158    pub fn extract_entity(entity: &MetaballEntity) -> ExtractedMesh {
159        let (min, max) = entity.bounds();
160        // A little margin, so a surface at the edge of the bounds closes.
161        let pad = (max - min) * 0.1 + Vec3::splat(0.05);
162        let field = |p: Vec3| entity.evaluate(p);
163        let shade = |p: Vec3| {
164            let (_, color, emission) = entity.evaluate_full(p);
165            (color, emission)
166        };
167        Self::extract_with(&field, &shade, min - pad, max + pad, entity.grid_resolution.max(4), entity.threshold)
168    }
169}
170
171/// Where the isosurface crosses the edge between two samples.
172fn cross(pa: Vec3, va: f32, pb: Vec3, vb: f32, iso: f32) -> Vec3 {
173    let d = vb - va;
174    if d.abs() < 1e-9 {
175        return (pa + pb) * 0.5;
176    }
177    let t = ((iso - va) / d).clamp(0.0, 1.0);
178    pa + (pb - pa) * t
179}
180
181/// Cut one tetrahedron by the isosurface and hand back its triangles.
182///
183/// Winding is chosen so triangles face outward from the side where the
184/// field is above the threshold, which for a metaball is the inside of the
185/// body; the gradient normals agree with it.
186fn polygonise_tetra(p: [Vec3; 4], v: [f32; 4], iso: f32, tri: &mut dyn FnMut(Vec3, Vec3, Vec3)) {
187    let inside: [bool; 4] = [v[0] >= iso, v[1] >= iso, v[2] >= iso, v[3] >= iso];
188    let count = inside.iter().filter(|b| **b).count();
189    if count == 0 || count == 4 {
190        return;
191    }
192    let e = |a: usize, b: usize| cross(p[a], v[a], p[b], v[b], iso);
193
194    if count == 1 || count == 3 {
195        // One vertex on its own side: a single triangle cutting off that corner.
196        let lone = if count == 1 { inside.iter().position(|b| *b) } else { inside.iter().position(|b| !*b) }
197            .unwrap();
198        let others: Vec<usize> = (0..4).filter(|i| *i != lone).collect();
199        let a = e(lone, others[0]);
200        let b = e(lone, others[1]);
201        let c = e(lone, others[2]);
202        if count == 1 {
203            tri(a, b, c);
204        } else {
205            tri(a, c, b);
206        }
207    } else {
208        // Two and two: a quad, as two triangles.
209        let ins: Vec<usize> = (0..4).filter(|i| inside[*i]).collect();
210        let outs: Vec<usize> = (0..4).filter(|i| !inside[*i]).collect();
211        let a = e(ins[0], outs[0]);
212        let b = e(ins[0], outs[1]);
213        let c = e(ins[1], outs[1]);
214        let d = e(ins[1], outs[0]);
215        tri(a, b, c);
216        tri(a, c, d);
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223
224    #[test]
225    fn a_sphere_field_gives_a_closed_mesh_of_about_the_right_size() {
226        let field = |p: Vec3| 1.0 - p.length();
227        let mesh = MarchingCubesExtractor::extract(&field, Vec3::splat(-1.5), Vec3::splat(1.5), 24, 0.0);
228        assert!(!mesh.is_empty());
229        assert_eq!(mesh.indices.len() % 3, 0);
230        // Every vertex sits on the unit sphere, within a cell.
231        for v in &mesh.vertices {
232            assert!((v.position.length() - 1.0).abs() < 0.15, "{:?}", v.position);
233            assert!((v.normal.length() - 1.0).abs() < 1e-3);
234        }
235        // Normals point outward for a field that is positive inside.
236        let outward = mesh.vertices.iter().filter(|v| v.normal.dot(v.position) > 0.0).count();
237        assert!(outward > mesh.vertices.len() * 9 / 10);
238    }
239
240    #[test]
241    fn an_empty_field_gives_nothing() {
242        let field = |_p: Vec3| -1.0;
243        let mesh = MarchingCubesExtractor::extract(&field, Vec3::ZERO, Vec3::ONE, 8, 0.0);
244        assert!(mesh.is_empty());
245    }
246}