Skip to main content

polyscope_core/
marching_cubes.rs

1//! Marching Cubes isosurface extraction algorithm.
2//!
3//! Ported from the C++ `MarchingCubeCpp` library (public domain) used by C++ Polyscope.
4//! Extracts a triangle mesh representing the isosurface of a 3D scalar field.
5
6#![allow(
7    clippy::unreadable_literal,
8    clippy::too_many_lines,
9    clippy::too_many_arguments,
10    clippy::cast_sign_loss,
11    clippy::cast_possible_truncation,
12    clippy::cast_precision_loss
13)]
14
15use glam::Vec3;
16
17/// Output mesh from the marching cubes algorithm.
18#[derive(Debug, Clone, Default)]
19pub struct McmMesh {
20    /// Interpolated vertex positions in grid-index space.
21    pub vertices: Vec<Vec3>,
22    /// Per-vertex normals (accumulated from adjacent face normals, then normalized).
23    pub normals: Vec<Vec3>,
24    /// Triangle indices (every 3 consecutive indices form a triangle).
25    pub indices: Vec<u32>,
26}
27
28impl McmMesh {
29    /// Returns the number of triangles in the mesh.
30    #[must_use]
31    pub fn num_triangles(&self) -> usize {
32        self.indices.len() / 3
33    }
34
35    /// Returns true if the mesh has no triangles.
36    #[must_use]
37    pub fn is_empty(&self) -> bool {
38        self.indices.is_empty()
39    }
40}
41
42/// Extracts the isosurface from a 3D scalar field using marching cubes.
43///
44/// # Arguments
45/// * `field` - Scalar field values in C-contiguous order: the value for grid point
46///   (ix, iy, iz) is stored at index `(ix * ny + iy) * nz + iz`.
47/// * `isoval` - The isovalue defining the surface (surface is where `field == isoval`).
48/// * `nx`, `ny`, `nz` - Grid dimensions (number of nodes in each direction).
49///
50/// # Returns
51/// A mesh with vertices in grid-index space (integer coordinates interpolated along edges).
52/// The caller must transform vertices to world space using grid spacing and origin.
53///
54/// # Panics
55/// Panics if `field.len() != nx * ny * nz` or if any dimension is less than 2.
56#[must_use]
57pub fn marching_cubes(field: &[f32], isoval: f32, nx: u32, ny: u32, nz: u32) -> McmMesh {
58    assert!(
59        field.len() == (nx as usize) * (ny as usize) * (nz as usize),
60        "Field size {} does not match dimensions {}x{}x{} = {}",
61        field.len(),
62        nx,
63        ny,
64        nz,
65        (nx as usize) * (ny as usize) * (nz as usize)
66    );
67    assert!(nx >= 2 && ny >= 2 && nz >= 2, "All dimensions must be >= 2");
68
69    let mut mesh = McmMesh {
70        vertices: Vec::with_capacity(100_000),
71        normals: Vec::with_capacity(100_000),
72        indices: Vec::with_capacity(400_000),
73    };
74
75    let size = [nx, ny, nz];
76    // Slab indices: stores vertex index for each of 3 edge axes at each (x, y) position
77    // Uses modular z indexing (z % 2) to reuse memory between slabs
78    let slab_len = (nx as usize) * (ny as usize) * 2;
79    let mut slab_inds: Vec<[u32; 3]> = vec![[0; 3]; slab_len];
80
81    let mut vs = [0.0_f32; 8];
82    let mut edge_indices = [0_u32; 12];
83
84    for z in 0..nz - 1 {
85        for y in 0..ny - 1 {
86            for x in 0..nx - 1 {
87                // Evaluate 8 corner values, shifted by isoval
88                vs[0] = -isoval + field[to_index_1d(x, y, z, &size)];
89                vs[1] = -isoval + field[to_index_1d(x + 1, y, z, &size)];
90                vs[2] = -isoval + field[to_index_1d(x, y + 1, z, &size)];
91                vs[3] = -isoval + field[to_index_1d(x + 1, y + 1, z, &size)];
92                vs[4] = -isoval + field[to_index_1d(x, y, z + 1, &size)];
93                vs[5] = -isoval + field[to_index_1d(x + 1, y, z + 1, &size)];
94                vs[6] = -isoval + field[to_index_1d(x, y + 1, z + 1, &size)];
95                vs[7] = -isoval + field[to_index_1d(x + 1, y + 1, z + 1, &size)];
96
97                // Build 8-bit configuration index from corner signs
98                #[allow(clippy::cast_possible_truncation)]
99                let config_n = (i32::from(vs[0] < 0.0))
100                    | (i32::from(vs[1] < 0.0) << 1)
101                    | (i32::from(vs[2] < 0.0) << 2)
102                    | (i32::from(vs[3] < 0.0) << 3)
103                    | (i32::from(vs[4] < 0.0) << 4)
104                    | (i32::from(vs[5] < 0.0) << 5)
105                    | (i32::from(vs[6] < 0.0) << 6)
106                    | (i32::from(vs[7] < 0.0) << 7);
107
108                // Skip fully inside or fully outside
109                if config_n == 0 || config_n == 255 {
110                    continue;
111                }
112
113                // Compute edge vertices (only for boundary edges not yet computed)
114                // X-axis edges (axis=0)
115                if y == 0 && z == 0 {
116                    compute_edge(&mut slab_inds, &mut mesh, vs[0], vs[1], 0, x, y, z, &size);
117                }
118                if z == 0 {
119                    compute_edge(
120                        &mut slab_inds,
121                        &mut mesh,
122                        vs[2],
123                        vs[3],
124                        0,
125                        x,
126                        y + 1,
127                        z,
128                        &size,
129                    );
130                }
131                if y == 0 {
132                    compute_edge(
133                        &mut slab_inds,
134                        &mut mesh,
135                        vs[4],
136                        vs[5],
137                        0,
138                        x,
139                        y,
140                        z + 1,
141                        &size,
142                    );
143                }
144                compute_edge(
145                    &mut slab_inds,
146                    &mut mesh,
147                    vs[6],
148                    vs[7],
149                    0,
150                    x,
151                    y + 1,
152                    z + 1,
153                    &size,
154                );
155
156                // Y-axis edges (axis=1)
157                if x == 0 && z == 0 {
158                    compute_edge(&mut slab_inds, &mut mesh, vs[0], vs[2], 1, x, y, z, &size);
159                }
160                if z == 0 {
161                    compute_edge(
162                        &mut slab_inds,
163                        &mut mesh,
164                        vs[1],
165                        vs[3],
166                        1,
167                        x + 1,
168                        y,
169                        z,
170                        &size,
171                    );
172                }
173                if x == 0 {
174                    compute_edge(
175                        &mut slab_inds,
176                        &mut mesh,
177                        vs[4],
178                        vs[6],
179                        1,
180                        x,
181                        y,
182                        z + 1,
183                        &size,
184                    );
185                }
186                compute_edge(
187                    &mut slab_inds,
188                    &mut mesh,
189                    vs[5],
190                    vs[7],
191                    1,
192                    x + 1,
193                    y,
194                    z + 1,
195                    &size,
196                );
197
198                // Z-axis edges (axis=2)
199                if x == 0 && y == 0 {
200                    compute_edge(&mut slab_inds, &mut mesh, vs[0], vs[4], 2, x, y, z, &size);
201                }
202                if y == 0 {
203                    compute_edge(
204                        &mut slab_inds,
205                        &mut mesh,
206                        vs[1],
207                        vs[5],
208                        2,
209                        x + 1,
210                        y,
211                        z,
212                        &size,
213                    );
214                }
215                if x == 0 {
216                    compute_edge(
217                        &mut slab_inds,
218                        &mut mesh,
219                        vs[2],
220                        vs[6],
221                        2,
222                        x,
223                        y + 1,
224                        z,
225                        &size,
226                    );
227                }
228                compute_edge(
229                    &mut slab_inds,
230                    &mut mesh,
231                    vs[3],
232                    vs[7],
233                    2,
234                    x + 1,
235                    y + 1,
236                    z,
237                    &size,
238                );
239
240                // Gather edge indices for this cell
241                edge_indices[0] = slab_inds[to_index_1d_slab(x, y, z, &size)][0];
242                edge_indices[1] = slab_inds[to_index_1d_slab(x, y + 1, z, &size)][0];
243                edge_indices[2] = slab_inds[to_index_1d_slab(x, y, z + 1, &size)][0];
244                edge_indices[3] = slab_inds[to_index_1d_slab(x, y + 1, z + 1, &size)][0];
245                edge_indices[4] = slab_inds[to_index_1d_slab(x, y, z, &size)][1];
246                edge_indices[5] = slab_inds[to_index_1d_slab(x + 1, y, z, &size)][1];
247                edge_indices[6] = slab_inds[to_index_1d_slab(x, y, z + 1, &size)][1];
248                edge_indices[7] = slab_inds[to_index_1d_slab(x + 1, y, z + 1, &size)][1];
249                edge_indices[8] = slab_inds[to_index_1d_slab(x, y, z, &size)][2];
250                edge_indices[9] = slab_inds[to_index_1d_slab(x + 1, y, z, &size)][2];
251                edge_indices[10] = slab_inds[to_index_1d_slab(x, y + 1, z, &size)][2];
252                edge_indices[11] = slab_inds[to_index_1d_slab(x + 1, y + 1, z, &size)][2];
253
254                // Look up triangle configuration
255                let config = MC_TRIS[config_n as usize];
256                let n_triangles = (config & 0xF) as usize;
257                let n_indices = n_triangles * 3;
258                let index_base = mesh.indices.len();
259
260                // Emit triangle indices
261                let mut offset = 4;
262                for _ in 0..n_indices {
263                    let edge = ((config >> offset) & 0xF) as usize;
264                    mesh.indices.push(edge_indices[edge]);
265                    offset += 4;
266                }
267
268                // Accumulate face normals
269                for i in 0..n_triangles {
270                    let ia = mesh.indices[index_base + i * 3];
271                    let ib = mesh.indices[index_base + i * 3 + 1];
272                    let ic = mesh.indices[index_base + i * 3 + 2];
273                    accumulate_normal(&mut mesh, ia, ib, ic);
274                }
275            }
276        }
277    }
278
279    // Normalize all accumulated normals
280    for normal in &mut mesh.normals {
281        let len = normal.length();
282        if len > 1e-10 {
283            *normal /= len;
284        }
285    }
286
287    mesh
288}
289
290/// Converts 3D grid coordinates to a 1D array index.
291/// Layout: `(i * ny + j) * nz + k`
292#[inline]
293fn to_index_1d(i: u32, j: u32, k: u32, size: &[u32; 3]) -> usize {
294    ((i as usize) * (size[1] as usize) + (j as usize)) * (size[2] as usize) + (k as usize)
295}
296
297/// Converts 3D coordinates to a slab index (modular z for memory reuse).
298/// Layout: `nx * ny * (k % 2) + j * nx + i`
299#[inline]
300fn to_index_1d_slab(i: u32, j: u32, k: u32, size: &[u32; 3]) -> usize {
301    (size[0] as usize) * (size[1] as usize) * ((k as usize) % 2)
302        + (j as usize) * (size[0] as usize)
303        + (i as usize)
304}
305
306/// Computes an edge vertex where the isosurface crosses, if the two endpoint values
307/// have opposite signs. Stores the vertex index in the slab array.
308#[inline]
309fn compute_edge(
310    slab_inds: &mut [[u32; 3]],
311    mesh: &mut McmMesh,
312    va: f32,
313    vb: f32,
314    axis: usize,
315    x: u32,
316    y: u32,
317    z: u32,
318    size: &[u32; 3],
319) {
320    // Only create vertex if sign differs (surface crosses this edge)
321    if (va < 0.0) == (vb < 0.0) {
322        return;
323    }
324    let mut v = Vec3::new(x as f32, y as f32, z as f32);
325    v[axis] += va / (va - vb);
326    let idx = mesh.vertices.len() as u32;
327    slab_inds[to_index_1d_slab(x, y, z, size)][axis] = idx;
328    mesh.vertices.push(v);
329    mesh.normals.push(Vec3::ZERO);
330}
331
332/// Accumulates the geometric normal of triangle (a, b, c) to all three vertices.
333#[inline]
334fn accumulate_normal(mesh: &mut McmMesh, a: u32, b: u32, c: u32) {
335    let va = mesh.vertices[a as usize];
336    let vb = mesh.vertices[b as usize];
337    let vc = mesh.vertices[c as usize];
338    let ab = va - vb;
339    let cb = vc - vb;
340    let n = cb.cross(ab);
341    mesh.normals[a as usize] += n;
342    mesh.normals[b as usize] += n;
343    mesh.normals[c as usize] += n;
344}
345
346/// Look-up table for triangle configurations (256 entries, one per cube configuration).
347///
348/// Each entry is a `u64` encoding:
349/// - Bits `[3:0]`: Number of triangles (0-5)
350/// - Bits `[7:4]`, `[11:8]`, ...: Edge indices (0-11) for each triangle vertex, 4 bits each
351///
352/// Ported from `MarchingCubeCpp` (public domain).
353#[rustfmt::skip]
354static MC_TRIS: [u64; 256] = [
355    0, 33793, 36945, 159668546,
356    18961, 144771090, 5851666, 595283255635,
357    20913, 67640146, 193993474, 655980856339,
358    88782242, 736732689667, 797430812739, 194554754,
359    26657, 104867330, 136709522, 298069416227,
360    109224258, 8877909667, 318136408323, 1567994331701604,
361    189884450, 350847647843, 559958167731, 3256298596865604,
362    447393122899, 651646838401572, 2538311371089956, 737032694307,
363    29329, 43484162, 91358498, 374810899075,
364    158485010, 178117478419, 88675058979, 433581536604804,
365    158486962, 649105605635, 4866906995, 3220959471609924,
366    649165714851, 3184943915608436, 570691368417972, 595804498035,
367    124295042, 431498018963, 508238522371, 91518530,
368    318240155763, 291789778348404, 1830001131721892, 375363605923,
369    777781811075, 1136111028516116, 3097834205243396, 508001629971,
370    2663607373704004, 680242583802939237, 333380770766129845, 179746658,
371    42545, 138437538, 93365810, 713842853011,
372    73602098, 69575510115, 23964357683, 868078761575828,
373    28681778, 713778574611, 250912709379, 2323825233181284,
374    302080811955, 3184439127991172, 1694042660682596, 796909779811,
375    176306722, 150327278147, 619854856867, 1005252473234484,
376    211025400963, 36712706, 360743481544788, 150627258963,
377    117482600995, 1024968212107700, 2535169275963444, 4734473194086550421,
378    628107696687956, 9399128243, 5198438490361643573, 194220594,
379    104474994, 566996932387, 427920028243, 2014821863433780,
380    492093858627, 147361150235284, 2005882975110676, 9671606099636618005,
381    777701008947, 3185463219618820, 482784926917540, 2900953068249785909,
382    1754182023747364, 4274848857537943333, 13198752741767688709, 2015093490989156,
383    591272318771, 2659758091419812, 1531044293118596, 298306479155,
384    408509245114388, 210504348563, 9248164405801223541, 91321106,
385    2660352816454484, 680170263324308757, 8333659837799955077, 482966828984116,
386    4274926723105633605, 3184439197724820, 192104450, 15217,
387    45937, 129205250, 129208402, 529245952323,
388    169097138, 770695537027, 382310500883, 2838550742137652,
389    122763026, 277045793139, 81608128403, 1991870397907988,
390    362778151475, 2059003085103236, 2132572377842852, 655681091891,
391    58419234, 239280858627, 529092143139, 1568257451898804,
392    447235128115, 679678845236084, 2167161349491220, 1554184567314086709,
393    165479003923, 1428768988226596, 977710670185060, 10550024711307499077,
394    1305410032576132, 11779770265620358997, 333446212255967269, 978168444447012,
395    162736434, 35596216627, 138295313843, 891861543990356,
396    692616541075, 3151866750863876, 100103641866564, 6572336607016932133,
397    215036012883, 726936420696196, 52433666, 82160664963,
398    2588613720361524, 5802089162353039525, 214799000387, 144876322,
399    668013605731, 110616894681956, 1601657732871812, 430945547955,
400    3156382366321172, 7644494644932993285, 3928124806469601813, 3155990846772900,
401    339991010498708, 10743689387941597493, 5103845475, 105070898,
402    3928064910068824213, 156265010, 1305138421793636, 27185,
403    195459938, 567044449971, 382447549283, 2175279159592324,
404    443529919251, 195059004769796, 2165424908404116, 1554158691063110021,
405    504228368803, 1436350466655236, 27584723588724, 1900945754488837749,
406    122971970, 443829749251, 302601798803, 108558722,
407    724700725875, 43570095105972, 2295263717447940, 2860446751369014181,
408    2165106202149444, 69275726195, 2860543885641537797, 2165106320445780,
409    2280890014640004, 11820349930268368933, 8721082628082003989, 127050770,
410    503707084675, 122834978, 2538193642857604, 10129,
411    801441490467, 2923200302876740, 1443359556281892, 2901063790822564949,
412    2728339631923524, 7103874718248233397, 12775311047932294245, 95520290,
413    2623783208098404, 1900908618382410757, 137742672547, 2323440239468964,
414    362478212387, 727199575803140, 73425410, 34337,
415    163101314, 668566030659, 801204361987, 73030562,
416    591509145619, 162574594, 100608342969108, 5553,
417    724147968595, 1436604830452292, 176259090, 42001,
418    143955266, 2385, 18433, 0,
419];
420
421#[cfg(test)]
422mod tests {
423    use super::*;
424
425    #[test]
426    fn test_empty_field() {
427        // All values above isoval → no surface
428        let field = vec![1.0; 3 * 3 * 3];
429        let mesh = marching_cubes(&field, 0.0, 3, 3, 3);
430        assert!(mesh.is_empty());
431    }
432
433    #[test]
434    fn test_constant_below() {
435        // All values below isoval → no surface
436        let field = vec![-1.0; 3 * 3 * 3];
437        let mesh = marching_cubes(&field, 0.0, 3, 3, 3);
438        assert!(mesh.is_empty());
439    }
440
441    #[test]
442    fn test_sphere_sdf() {
443        // Sphere SDF: distance from center minus radius
444        let n = 20_u32;
445        let center = Vec3::splat(n as f32 / 2.0);
446        let radius = n as f32 / 4.0;
447        let mut field = vec![0.0_f32; (n * n * n) as usize];
448        for i in 0..n {
449            for j in 0..n {
450                for k in 0..n {
451                    let p = Vec3::new(i as f32, j as f32, k as f32);
452                    let dist = (p - center).length() - radius;
453                    field[to_index_1d(i, j, k, &[n, n, n])] = dist;
454                }
455            }
456        }
457
458        let mesh = marching_cubes(&field, 0.0, n, n, n);
459
460        // Should produce a reasonable number of triangles for a sphere
461        assert!(
462            mesh.num_triangles() > 100,
463            "Expected >100 triangles, got {}",
464            mesh.num_triangles()
465        );
466        assert_eq!(mesh.vertices.len(), mesh.normals.len());
467        assert_eq!(mesh.indices.len() % 3, 0);
468
469        // All indices should be valid
470        for &idx in &mesh.indices {
471            assert!(
472                (idx as usize) < mesh.vertices.len(),
473                "Index {} out of range ({})",
474                idx,
475                mesh.vertices.len()
476            );
477        }
478
479        // Normals should be unit length
480        for normal in &mesh.normals {
481            let len = normal.length();
482            assert!((len - 1.0).abs() < 0.01, "Normal length {len} is not unit",);
483        }
484
485        // Vertices should be near the sphere surface (within grid spacing)
486        for v in &mesh.vertices {
487            let dist = (Vec3::new(v.z, v.y, v.x) - center).length(); // swizzle back
488            assert!(
489                (dist - radius).abs() < 2.0,
490                "Vertex {v:?} is {dist} from sphere (radius {radius})",
491            );
492        }
493    }
494
495    #[test]
496    fn test_single_crossing() {
497        // 2x2x2 grid with one corner inside, rest outside
498        let mut field = vec![1.0_f32; 8];
499        field[0] = -1.0; // corner (0,0,0) is inside
500        let mesh = marching_cubes(&field, 0.0, 2, 2, 2);
501
502        // Should produce exactly 1 triangle (config = 1)
503        assert_eq!(mesh.num_triangles(), 1);
504        assert_eq!(mesh.indices.len(), 3);
505    }
506
507    #[test]
508    #[should_panic(expected = "Field size")]
509    fn test_wrong_field_size() {
510        let field = vec![0.0; 10]; // wrong size for 3x3x3
511        let _ = marching_cubes(&field, 0.0, 3, 3, 3);
512    }
513
514    #[test]
515    #[should_panic(expected = "dimensions must be >= 2")]
516    fn test_dimension_too_small() {
517        let field = vec![0.0; 1];
518        let _ = marching_cubes(&field, 0.0, 1, 1, 1);
519    }
520}