Skip to main content

sci_form/gpu/
marching_cubes.rs

1//! Marching cubes isosurface extraction from 3D scalar fields.
2//!
3//! Converts a scalar field (orbital ψ or density ρ) into a triangle mesh
4//! at a specified isovalue. Reference: Lorensen & Cline, SIGGRAPH 1987.
5//!
6//! GPU path: dispatches a WGSL compute shader that processes each voxel
7//! independently, then reads back the generated triangle vertices.
8//! CPU path: triple-nested loop (always available as fallback).
9
10use super::backend_report::OrbitalGridReport;
11use super::context::{
12    bytes_to_f32_vec, ceil_div_u32, f32_slice_to_bytes, pack_uniform_values,
13    ComputeBindingDescriptor, ComputeBindingKind, ComputeDispatchDescriptor, GpuContext,
14    UniformValue,
15};
16use super::orbital_grid::GridParams;
17
18/// Triangle mesh output from marching cubes.
19#[derive(Debug, Clone)]
20pub struct McOutput {
21    /// Triangle vertices (flat: [x0,y0,z0, x1,y1,z1, ...]).
22    pub vertices: Vec<f32>,
23    /// Triangle normals (flat: [nx0,ny0,nz0, ...]).
24    pub normals: Vec<f32>,
25    /// Triangle indices.
26    pub indices: Vec<u32>,
27    /// Number of triangles.
28    pub n_triangles: usize,
29}
30
31/// CPU marching cubes on a 3D scalar field.
32pub fn marching_cubes_cpu(values: &[f64], params: &GridParams, isovalue: f64) -> McOutput {
33    let [nx, ny, nz] = params.dimensions;
34    let mut vertices = Vec::new();
35    let mut normals = Vec::new();
36    let mut indices = Vec::new();
37    let mut vertex_count = 0u32;
38
39    for ix in 0..nx.saturating_sub(1) {
40        for iy in 0..ny.saturating_sub(1) {
41            for iz in 0..nz.saturating_sub(1) {
42                let corners = [
43                    values[params.flat_index(ix, iy, iz)],
44                    values[params.flat_index(ix + 1, iy, iz)],
45                    values[params.flat_index(ix + 1, iy + 1, iz)],
46                    values[params.flat_index(ix, iy + 1, iz)],
47                    values[params.flat_index(ix, iy, iz + 1)],
48                    values[params.flat_index(ix + 1, iy, iz + 1)],
49                    values[params.flat_index(ix + 1, iy + 1, iz + 1)],
50                    values[params.flat_index(ix, iy + 1, iz + 1)],
51                ];
52
53                let mut cube_index = 0u8;
54                for (i, &val) in corners.iter().enumerate() {
55                    if val > isovalue {
56                        cube_index |= 1 << i;
57                    }
58                }
59
60                let edge_bits = EDGE_TABLE[cube_index as usize];
61                if edge_bits == 0 {
62                    continue;
63                }
64
65                let corner_positions = [
66                    params.point(ix, iy, iz),
67                    params.point(ix + 1, iy, iz),
68                    params.point(ix + 1, iy + 1, iz),
69                    params.point(ix, iy + 1, iz),
70                    params.point(ix, iy, iz + 1),
71                    params.point(ix + 1, iy, iz + 1),
72                    params.point(ix + 1, iy + 1, iz + 1),
73                    params.point(ix, iy + 1, iz + 1),
74                ];
75
76                let mut edge_vertices = [[0.0f64; 3]; 12];
77                for edge in 0..12 {
78                    if edge_bits & (1 << edge) != 0 {
79                        let (v0, v1) = EDGE_VERTICES[edge];
80                        let p0 = corner_positions[v0];
81                        let p1 = corner_positions[v1];
82                        let val0 = corners[v0];
83                        let val1 = corners[v1];
84
85                        let t = if (val1 - val0).abs() > 1e-10 {
86                            ((isovalue - val0) / (val1 - val0)).clamp(0.0, 1.0)
87                        } else {
88                            0.5
89                        };
90
91                        edge_vertices[edge] = [
92                            p0[0] + t * (p1[0] - p0[0]),
93                            p0[1] + t * (p1[1] - p0[1]),
94                            p0[2] + t * (p1[2] - p0[2]),
95                        ];
96                    }
97                }
98
99                let tri_entry = &TRI_TABLE[cube_index as usize];
100                let mut i = 0;
101                while i < tri_entry.len() && tri_entry[i] != -1 {
102                    let e0 = tri_entry[i] as usize;
103                    let e1 = tri_entry[i + 1] as usize;
104                    let e2 = tri_entry[i + 2] as usize;
105
106                    let v0 = edge_vertices[e0];
107                    let v1 = edge_vertices[e1];
108                    let v2 = edge_vertices[e2];
109
110                    // Face normal via cross product
111                    let u = [v1[0] - v0[0], v1[1] - v0[1], v1[2] - v0[2]];
112                    let v = [v2[0] - v0[0], v2[1] - v0[1], v2[2] - v0[2]];
113                    let mut n = [
114                        u[1] * v[2] - u[2] * v[1],
115                        u[2] * v[0] - u[0] * v[2],
116                        u[0] * v[1] - u[1] * v[0],
117                    ];
118                    let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
119                    if len > 1e-10 {
120                        n[0] /= len;
121                        n[1] /= len;
122                        n[2] /= len;
123                    }
124
125                    for vert in &[v0, v1, v2] {
126                        vertices.push(vert[0] as f32);
127                        vertices.push(vert[1] as f32);
128                        vertices.push(vert[2] as f32);
129                        normals.push(n[0] as f32);
130                        normals.push(n[1] as f32);
131                        normals.push(n[2] as f32);
132                        indices.push(vertex_count);
133                        vertex_count += 1;
134                    }
135
136                    i += 3;
137                }
138            }
139        }
140    }
141
142    McOutput {
143        n_triangles: indices.len() / 3,
144        vertices,
145        normals,
146        indices,
147    }
148}
149
150/// Smooth vertex normals by averaging at shared positions.
151pub fn smooth_mesh_normals(mesh: &mut McOutput) {
152    if mesh.vertices.is_empty() {
153        return;
154    }
155
156    let n_verts = mesh.vertices.len() / 3;
157    let tolerance = 1e-4f32;
158    let mut smoothed = vec![[0.0f32; 3]; n_verts];
159
160    for i in 0..n_verts {
161        let pi = [
162            mesh.vertices[i * 3],
163            mesh.vertices[i * 3 + 1],
164            mesh.vertices[i * 3 + 2],
165        ];
166        let mut nx = 0.0f32;
167        let mut ny = 0.0f32;
168        let mut nz = 0.0f32;
169        let mut count = 0u32;
170
171        for j in 0..n_verts {
172            let pj = [
173                mesh.vertices[j * 3],
174                mesh.vertices[j * 3 + 1],
175                mesh.vertices[j * 3 + 2],
176            ];
177            let dx = pi[0] - pj[0];
178            let dy = pi[1] - pj[1];
179            let dz = pi[2] - pj[2];
180            if dx * dx + dy * dy + dz * dz < tolerance * tolerance {
181                nx += mesh.normals[j * 3];
182                ny += mesh.normals[j * 3 + 1];
183                nz += mesh.normals[j * 3 + 2];
184                count += 1;
185            }
186        }
187
188        if count > 0 {
189            let len = (nx * nx + ny * ny + nz * nz).sqrt();
190            if len > 1e-10 {
191                smoothed[i] = [nx / len, ny / len, nz / len];
192            } else {
193                smoothed[i] = [
194                    mesh.normals[i * 3],
195                    mesh.normals[i * 3 + 1],
196                    mesh.normals[i * 3 + 2],
197                ];
198            }
199        }
200    }
201
202    for i in 0..n_verts {
203        mesh.normals[i * 3] = smoothed[i][0];
204        mesh.normals[i * 3 + 1] = smoothed[i][1];
205        mesh.normals[i * 3 + 2] = smoothed[i][2];
206    }
207}
208
209/// Marching cubes with automatic GPU/CPU backend selection.
210///
211/// Uses GPU when available, falls back to CPU otherwise.
212pub fn marching_cubes_with_report(
213    values: &[f64],
214    params: &GridParams,
215    isovalue: f64,
216) -> (McOutput, OrbitalGridReport) {
217    let ctx = GpuContext::best_available();
218    if ctx.is_gpu_available() {
219        if let Ok(mesh) = marching_cubes_gpu(&ctx, values, params, isovalue) {
220            return (
221                mesh,
222                OrbitalGridReport {
223                    backend: ctx.capabilities.backend.clone(),
224                    used_gpu: true,
225                    attempted_gpu: true,
226                    n_points: params.n_points(),
227                    note: format!("GPU marching cubes on {}", ctx.capabilities.backend),
228                },
229            );
230        }
231    }
232    let mesh = marching_cubes_cpu(values, params, isovalue);
233    (
234        mesh,
235        OrbitalGridReport {
236            backend: "CPU".to_string(),
237            used_gpu: false,
238            attempted_gpu: ctx.is_gpu_available(),
239            n_points: params.n_points(),
240            note: "CPU marching cubes".to_string(),
241        },
242    )
243}
244
245/// GPU dispatch for marching cubes isosurface extraction.
246///
247/// Each workgroup processes one voxel. The shader classifies corners,
248/// looks up the edge/triangle tables, interpolates edge vertices, and
249/// writes triangle data into an append-style output buffer.
250pub fn marching_cubes_gpu(
251    ctx: &GpuContext,
252    values: &[f64],
253    params: &GridParams,
254    isovalue: f64,
255) -> Result<McOutput, String> {
256    let [nx, ny, nz] = params.dimensions;
257    let vx = nx.saturating_sub(1).max(1);
258    let vy = ny.saturating_sub(1).max(1);
259    let vz = nz.saturating_sub(1).max(1);
260    let n_voxels = vx * vy * vz;
261
262    // Each voxel can produce at most 5 triangles (15 vertices × 6 floats each)
263    let max_floats_per_voxel = 5 * 3 * 6; // 5 triangles × 3 verts × (pos3 + norm3)
264    let max_output = n_voxels * max_floats_per_voxel;
265
266    let values_f32: Vec<f32> = values.iter().map(|&v| v as f32).collect();
267
268    let params_bytes = pack_uniform_values(&[
269        UniformValue::F32(params.origin[0] as f32),
270        UniformValue::F32(params.origin[1] as f32),
271        UniformValue::F32(params.origin[2] as f32),
272        UniformValue::F32(params.spacing as f32),
273        UniformValue::U32(nx as u32),
274        UniformValue::U32(ny as u32),
275        UniformValue::U32(nz as u32),
276        UniformValue::F32(isovalue as f32),
277    ]);
278
279    // Pack edge table as u32 array
280    let edge_table_u32: Vec<u32> = EDGE_TABLE.iter().map(|&v| v as u32).collect();
281    let edge_table_bytes: Vec<u8> = edge_table_u32
282        .iter()
283        .flat_map(|v| v.to_le_bytes())
284        .collect();
285
286    // Pack tri table as i32 array (flattened 256 × 16)
287    let mut tri_table_i32: Vec<i32> = Vec::with_capacity(256 * 16);
288    for row in &TRI_TABLE {
289        for &val in row.iter() {
290            tri_table_i32.push(val as i32);
291        }
292    }
293    let tri_table_bytes: Vec<u8> = tri_table_i32.iter().flat_map(|v| v.to_le_bytes()).collect();
294
295    // Atomic counter buffer (1 u32 for triangle count)
296    let counter_bytes: Vec<u8> = vec![0u8; 4];
297
298    // Output buffer for vertex+normal data
299    let output_bytes = f32_slice_to_bytes(&vec![0.0f32; max_output]);
300
301    let descriptor = ComputeDispatchDescriptor {
302        label: "marching cubes".to_string(),
303        shader_source: MARCHING_CUBES_SHADER.to_string(),
304        entry_point: "main".to_string(),
305        workgroup_count: [
306            ceil_div_u32(vx, 4),
307            ceil_div_u32(vy, 4),
308            ceil_div_u32(vz, 4),
309        ],
310        bindings: vec![
311            ComputeBindingDescriptor {
312                label: "scalar_field".to_string(),
313                kind: ComputeBindingKind::StorageReadOnly,
314                bytes: f32_slice_to_bytes(&values_f32),
315            },
316            ComputeBindingDescriptor {
317                label: "edge_table".to_string(),
318                kind: ComputeBindingKind::StorageReadOnly,
319                bytes: edge_table_bytes,
320            },
321            ComputeBindingDescriptor {
322                label: "tri_table".to_string(),
323                kind: ComputeBindingKind::StorageReadOnly,
324                bytes: tri_table_bytes,
325            },
326            ComputeBindingDescriptor {
327                label: "params".to_string(),
328                kind: ComputeBindingKind::Uniform,
329                bytes: params_bytes,
330            },
331            ComputeBindingDescriptor {
332                label: "counter".to_string(),
333                kind: ComputeBindingKind::StorageReadWrite,
334                bytes: counter_bytes,
335            },
336            ComputeBindingDescriptor {
337                label: "output".to_string(),
338                kind: ComputeBindingKind::StorageReadWrite,
339                bytes: output_bytes,
340            },
341        ],
342    };
343
344    let result = ctx.run_compute(&descriptor)?;
345    if result.outputs.len() < 2 {
346        return Err("Missing outputs from marching cubes kernel".to_string());
347    }
348
349    // First ReadWrite output is the counter, second is the vertex data
350    let counter_out = &result.outputs[0];
351    let tri_count = if counter_out.len() >= 4 {
352        u32::from_le_bytes([
353            counter_out[0],
354            counter_out[1],
355            counter_out[2],
356            counter_out[3],
357        ])
358    } else {
359        0
360    };
361
362    let vertex_data = bytes_to_f32_vec(&result.outputs[1]);
363    let n_triangles = tri_count as usize;
364    let n_verts = n_triangles * 3;
365    let n_floats = n_verts * 6; // 3 pos + 3 normal per vertex
366
367    let actual_floats = n_floats.min(vertex_data.len());
368    let actual_verts = actual_floats / 6;
369    let actual_tris = actual_verts / 3;
370
371    let mut vertices = Vec::with_capacity(actual_verts * 3);
372    let mut normals = Vec::with_capacity(actual_verts * 3);
373    let mut indices = Vec::with_capacity(actual_verts);
374
375    for v in 0..actual_verts {
376        let base = v * 6;
377        if base + 5 < vertex_data.len() {
378            vertices.push(vertex_data[base]);
379            vertices.push(vertex_data[base + 1]);
380            vertices.push(vertex_data[base + 2]);
381            normals.push(vertex_data[base + 3]);
382            normals.push(vertex_data[base + 4]);
383            normals.push(vertex_data[base + 5]);
384            indices.push(v as u32);
385        }
386    }
387
388    Ok(McOutput {
389        n_triangles: actual_tris,
390        vertices,
391        normals,
392        indices,
393    })
394}
395
396/// WGSL compute shader for marching cubes isosurface extraction.
397///
398/// Each invocation processes one voxel, classifying corners against the isovalue,
399/// looking up the edge and triangle tables, and emitting interpolated triangle
400/// vertices with face normals into an append-style output buffer.
401const MARCHING_CUBES_SHADER: &str = r#"
402struct McParams {
403    origin_x: f32, origin_y: f32, origin_z: f32,
404    spacing: f32,
405    dims_x: u32, dims_y: u32, dims_z: u32,
406    isovalue: f32,
407};
408
409@group(0) @binding(0) var<storage, read> scalar_field: array<f32>;
410@group(0) @binding(1) var<storage, read> edge_table: array<u32>;
411@group(0) @binding(2) var<storage, read> tri_table: array<i32>;
412@group(0) @binding(3) var<uniform> params: McParams;
413@group(0) @binding(4) var<storage, read_write> tri_counter: array<atomic<u32>>;
414@group(0) @binding(5) var<storage, read_write> output: array<f32>;
415
416fn field_index(ix: u32, iy: u32, iz: u32) -> u32 {
417    return ix * params.dims_y * params.dims_z + iy * params.dims_z + iz;
418}
419
420fn grid_point(ix: u32, iy: u32, iz: u32) -> vec3<f32> {
421    return vec3<f32>(
422        params.origin_x + f32(ix) * params.spacing,
423        params.origin_y + f32(iy) * params.spacing,
424        params.origin_z + f32(iz) * params.spacing,
425    );
426}
427
428fn interp_vertex(p0: vec3<f32>, p1: vec3<f32>, v0: f32, v1: f32, iso: f32) -> vec3<f32> {
429    let dv = v1 - v0;
430    var t: f32 = 0.5;
431    if (abs(dv) > 1e-10) {
432        t = clamp((iso - v0) / dv, 0.0, 1.0);
433    }
434    return mix(p0, p1, vec3<f32>(t));
435}
436
437@compute @workgroup_size(4, 4, 4)
438fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
439    let ix = gid.x;
440    let iy = gid.y;
441    let iz = gid.z;
442
443    let vx = params.dims_x - 1u;
444    let vy = params.dims_y - 1u;
445    let vz = params.dims_z - 1u;
446
447    if (ix >= vx || iy >= vy || iz >= vz) {
448        return;
449    }
450
451    // Sample 8 corners
452    let c0 = scalar_field[field_index(ix,     iy,     iz)];
453    let c1 = scalar_field[field_index(ix + 1, iy,     iz)];
454    let c2 = scalar_field[field_index(ix + 1, iy + 1, iz)];
455    let c3 = scalar_field[field_index(ix,     iy + 1, iz)];
456    let c4 = scalar_field[field_index(ix,     iy,     iz + 1)];
457    let c5 = scalar_field[field_index(ix + 1, iy,     iz + 1)];
458    let c6 = scalar_field[field_index(ix + 1, iy + 1, iz + 1)];
459    let c7 = scalar_field[field_index(ix,     iy + 1, iz + 1)];
460
461    let iso = params.isovalue;
462
463    var cube_index: u32 = 0u;
464    if (c0 > iso) { cube_index |= 1u; }
465    if (c1 > iso) { cube_index |= 2u; }
466    if (c2 > iso) { cube_index |= 4u; }
467    if (c3 > iso) { cube_index |= 8u; }
468    if (c4 > iso) { cube_index |= 16u; }
469    if (c5 > iso) { cube_index |= 32u; }
470    if (c6 > iso) { cube_index |= 64u; }
471    if (c7 > iso) { cube_index |= 128u; }
472
473    let edge_bits = edge_table[cube_index];
474    if (edge_bits == 0u) {
475        return;
476    }
477
478    // Corner positions
479    let p0 = grid_point(ix,     iy,     iz);
480    let p1 = grid_point(ix + 1, iy,     iz);
481    let p2 = grid_point(ix + 1, iy + 1, iz);
482    let p3 = grid_point(ix,     iy + 1, iz);
483    let p4 = grid_point(ix,     iy,     iz + 1);
484    let p5 = grid_point(ix + 1, iy,     iz + 1);
485    let p6 = grid_point(ix + 1, iy + 1, iz + 1);
486    let p7 = grid_point(ix,     iy + 1, iz + 1);
487
488    // Interpolate edge vertices
489    var ev: array<vec3<f32>, 12>;
490    if ((edge_bits &    1u) != 0u) { ev[0]  = interp_vertex(p0, p1, c0, c1, iso); }
491    if ((edge_bits &    2u) != 0u) { ev[1]  = interp_vertex(p1, p2, c1, c2, iso); }
492    if ((edge_bits &    4u) != 0u) { ev[2]  = interp_vertex(p2, p3, c2, c3, iso); }
493    if ((edge_bits &    8u) != 0u) { ev[3]  = interp_vertex(p3, p0, c3, c0, iso); }
494    if ((edge_bits &   16u) != 0u) { ev[4]  = interp_vertex(p4, p5, c4, c5, iso); }
495    if ((edge_bits &   32u) != 0u) { ev[5]  = interp_vertex(p5, p6, c5, c6, iso); }
496    if ((edge_bits &   64u) != 0u) { ev[6]  = interp_vertex(p6, p7, c6, c7, iso); }
497    if ((edge_bits &  128u) != 0u) { ev[7]  = interp_vertex(p7, p4, c7, c4, iso); }
498    if ((edge_bits &  256u) != 0u) { ev[8]  = interp_vertex(p0, p4, c0, c4, iso); }
499    if ((edge_bits &  512u) != 0u) { ev[9]  = interp_vertex(p1, p5, c1, c5, iso); }
500    if ((edge_bits & 1024u) != 0u) { ev[10] = interp_vertex(p2, p6, c2, c6, iso); }
501    if ((edge_bits & 2048u) != 0u) { ev[11] = interp_vertex(p3, p7, c3, c7, iso); }
502
503    // Emit triangles from tri_table
504    let tri_base = cube_index * 16u;
505    var i: u32 = 0u;
506    loop {
507        if (i >= 16u) { break; }
508        let e0_i = tri_table[tri_base + i];
509        if (e0_i < 0) { break; }
510        let e1_i = tri_table[tri_base + i + 1u];
511        let e2_i = tri_table[tri_base + i + 2u];
512
513        let v0 = ev[u32(e0_i)];
514        let v1 = ev[u32(e1_i)];
515        let v2 = ev[u32(e2_i)];
516
517        // Face normal
518        let u_vec = v1 - v0;
519        let v_vec = v2 - v0;
520        var n = cross(u_vec, v_vec);
521        let len = length(n);
522        if (len > 1e-10) {
523            n = n / len;
524        }
525
526        // Atomically allocate 1 triangle slot
527        let tri_idx = atomicAdd(&tri_counter[0], 1u);
528        let out_base = tri_idx * 18u; // 3 verts × 6 floats (pos3 + normal3)
529
530        // Vertex 0
531        output[out_base +  0u] = v0.x;
532        output[out_base +  1u] = v0.y;
533        output[out_base +  2u] = v0.z;
534        output[out_base +  3u] = n.x;
535        output[out_base +  4u] = n.y;
536        output[out_base +  5u] = n.z;
537        // Vertex 1
538        output[out_base +  6u] = v1.x;
539        output[out_base +  7u] = v1.y;
540        output[out_base +  8u] = v1.z;
541        output[out_base +  9u] = n.x;
542        output[out_base + 10u] = n.y;
543        output[out_base + 11u] = n.z;
544        // Vertex 2
545        output[out_base + 12u] = v2.x;
546        output[out_base + 13u] = v2.y;
547        output[out_base + 14u] = v2.z;
548        output[out_base + 15u] = n.x;
549        output[out_base + 16u] = n.y;
550        output[out_base + 17u] = n.z;
551
552        i = i + 3u;
553    }
554}
555"#;
556
557// ─── Lookup tables ───────────────────────────────────────────────────────────
558
559const EDGE_VERTICES: [(usize, usize); 12] = [
560    (0, 1),
561    (1, 2),
562    (2, 3),
563    (3, 0),
564    (4, 5),
565    (5, 6),
566    (6, 7),
567    (7, 4),
568    (0, 4),
569    (1, 5),
570    (2, 6),
571    (3, 7),
572];
573
574#[rustfmt::skip]
575const EDGE_TABLE: [u16; 256] = [
576    0x000, 0x109, 0x203, 0x30a, 0x406, 0x50f, 0x605, 0x70c,
577    0x80c, 0x905, 0xa0f, 0xb06, 0xc0a, 0xd03, 0xe09, 0xf00,
578    0x190, 0x099, 0x393, 0x29a, 0x596, 0x49f, 0x795, 0x69c,
579    0x99c, 0x895, 0xb9f, 0xa96, 0xd9a, 0xc93, 0xf99, 0xe90,
580    0x230, 0x339, 0x033, 0x13a, 0x636, 0x73f, 0x435, 0x53c,
581    0xa3c, 0xb35, 0x83f, 0x936, 0xe3a, 0xf33, 0xc39, 0xd30,
582    0x3a0, 0x2a9, 0x1a3, 0x0aa, 0x7a6, 0x6af, 0x5a5, 0x4ac,
583    0xbac, 0xaa5, 0x9af, 0x8a6, 0xfaa, 0xea3, 0xda9, 0xca0,
584    0x460, 0x569, 0x663, 0x76a, 0x066, 0x16f, 0x265, 0x36c,
585    0xc6c, 0xd65, 0xe6f, 0xf66, 0x86a, 0x963, 0xa69, 0xb60,
586    0x5f0, 0x4f9, 0x7f3, 0x6fa, 0x1f6, 0x0ff, 0x3f5, 0x2fc,
587    0xdfc, 0xcf5, 0xfff, 0xef6, 0x9fa, 0x8f3, 0xbf9, 0xaf0,
588    0x650, 0x759, 0x453, 0x55a, 0x256, 0x35f, 0x055, 0x15c,
589    0xe5c, 0xf55, 0xc5f, 0xd56, 0xa5a, 0xb53, 0x859, 0x950,
590    0x7c0, 0x6c9, 0x5c3, 0x4ca, 0x3c6, 0x2cf, 0x1c5, 0x0cc,
591    0xfcc, 0xec5, 0xdcf, 0xcc6, 0xbca, 0xac3, 0x9c9, 0x8c0,
592    0x8c0, 0x9c9, 0xac3, 0xbca, 0xcc6, 0xdcf, 0xec5, 0xfcc,
593    0x0cc, 0x1c5, 0x2cf, 0x3c6, 0x4ca, 0x5c3, 0x6c9, 0x7c0,
594    0x950, 0x859, 0xb53, 0xa5a, 0xd56, 0xc5f, 0xf55, 0xe5c,
595    0x15c, 0x055, 0x35f, 0x256, 0x55a, 0x453, 0x759, 0x650,
596    0xaf0, 0xbf9, 0x8f3, 0x9fa, 0xef6, 0xfff, 0xcf5, 0xdfc,
597    0x2fc, 0x3f5, 0x0ff, 0x1f6, 0x6fa, 0x7f3, 0x4f9, 0x5f0,
598    0xb60, 0xa69, 0x963, 0x86a, 0xf66, 0xe6f, 0xd65, 0xc6c,
599    0x36c, 0x265, 0x16f, 0x066, 0x76a, 0x663, 0x569, 0x460,
600    0xca0, 0xda9, 0xea3, 0xfaa, 0x8a6, 0x9af, 0xaa5, 0xbac,
601    0x4ac, 0x5a5, 0x6af, 0x7a6, 0x0aa, 0x1a3, 0x2a9, 0x3a0,
602    0xd30, 0xc39, 0xf33, 0xe3a, 0x936, 0x83f, 0xb35, 0xa3c,
603    0x53c, 0x435, 0x73f, 0x636, 0x13a, 0x033, 0x339, 0x230,
604    0xe90, 0xf99, 0xc93, 0xd9a, 0xa96, 0xb9f, 0x895, 0x99c,
605    0x69c, 0x795, 0x49f, 0x596, 0x29a, 0x393, 0x099, 0x190,
606    0xf00, 0xe09, 0xd03, 0xc0a, 0xb06, 0xa0f, 0x905, 0x80c,
607    0x70c, 0x605, 0x50f, 0x406, 0x30a, 0x203, 0x109, 0x000,
608];
609
610/// Triangle table: edge triples for each cube configuration, terminated by -1.
611const TRI_TABLE: [[i8; 16]; 256] = {
612    let mut t = [[-1i8; 16]; 256];
613    t[1] = [0, 8, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1];
614    t[2] = [0, 1, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1];
615    t[3] = [1, 8, 3, 9, 8, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1];
616    t[4] = [1, 2, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1];
617    t[5] = [0, 8, 3, 1, 2, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1];
618    t[6] = [9, 2, 10, 0, 2, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1];
619    t[7] = [2, 8, 3, 2, 10, 8, 10, 9, 8, -1, -1, -1, -1, -1, -1, -1];
620    t[8] = [3, 11, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1];
621    t[9] = [0, 11, 2, 8, 11, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1];
622    t[10] = [1, 9, 0, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1];
623    t[11] = [1, 11, 2, 1, 9, 11, 9, 8, 11, -1, -1, -1, -1, -1, -1, -1];
624    t[12] = [3, 10, 1, 11, 10, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1];
625    t[13] = [0, 10, 1, 0, 8, 10, 8, 11, 10, -1, -1, -1, -1, -1, -1, -1];
626    t[14] = [3, 9, 0, 3, 11, 9, 11, 10, 9, -1, -1, -1, -1, -1, -1, -1];
627    t[15] = [9, 8, 10, 10, 8, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1];
628    t
629};
630
631#[cfg(test)]
632mod tests {
633    use super::*;
634
635    #[test]
636    fn test_marching_cubes_sphere() {
637        let params = GridParams {
638            origin: [-2.0, -2.0, -2.0],
639            spacing: 0.5,
640            dimensions: [9, 9, 9],
641        };
642
643        let mut values = vec![0.0; params.n_points()];
644        let [nx, ny, nz] = params.dimensions;
645
646        for ix in 0..nx {
647            for iy in 0..ny {
648                for iz in 0..nz {
649                    let r = params.point(ix, iy, iz);
650                    let r2 = r[0] * r[0] + r[1] * r[1] + r[2] * r[2];
651                    values[params.flat_index(ix, iy, iz)] = 1.0 - r2;
652                }
653            }
654        }
655
656        let mesh = marching_cubes_cpu(&values, &params, 0.0);
657        assert!(mesh.n_triangles > 0);
658        assert_eq!(mesh.vertices.len(), mesh.n_triangles * 9);
659        assert_eq!(mesh.normals.len(), mesh.n_triangles * 9);
660    }
661
662    #[test]
663    fn test_marching_cubes_empty() {
664        let params = GridParams {
665            origin: [0.0, 0.0, 0.0],
666            spacing: 1.0,
667            dimensions: [3, 3, 3],
668        };
669        let values = vec![-1.0; params.n_points()];
670        let mesh = marching_cubes_cpu(&values, &params, 0.0);
671        assert_eq!(mesh.n_triangles, 0);
672    }
673
674    #[test]
675    fn test_marching_cubes_all_inside() {
676        let params = GridParams {
677            origin: [0.0, 0.0, 0.0],
678            spacing: 1.0,
679            dimensions: [3, 3, 3],
680        };
681        let values = vec![1.0; params.n_points()];
682        let mesh = marching_cubes_cpu(&values, &params, 0.0);
683        assert_eq!(mesh.n_triangles, 0);
684    }
685
686    #[test]
687    fn test_smooth_normals_empty() {
688        let mut mesh = McOutput {
689            vertices: vec![],
690            normals: vec![],
691            indices: vec![],
692            n_triangles: 0,
693        };
694        smooth_mesh_normals(&mut mesh); // Should not panic
695    }
696}