Skip to main content

proof_engine/geometry/
implicit.rs

1//! Implicit surface extraction — marching cubes/tetrahedra for isosurfaces from scalar fields.
2
3use glam::{Vec3, Vec2};
4use super::{GeoMesh, Triangle};
5
6/// A scalar field f(x,y,z) → f32. Isosurface at f=0.
7pub trait ScalarField: Send + Sync {
8    fn evaluate(&self, p: Vec3) -> f32;
9
10    /// Gradient (finite differences).
11    fn gradient(&self, p: Vec3) -> Vec3 {
12        let eps = 1e-4;
13        Vec3::new(
14            self.evaluate(p + Vec3::X * eps) - self.evaluate(p - Vec3::X * eps),
15            self.evaluate(p + Vec3::Y * eps) - self.evaluate(p - Vec3::Y * eps),
16            self.evaluate(p + Vec3::Z * eps) - self.evaluate(p - Vec3::Z * eps),
17        ) / (2.0 * eps)
18    }
19}
20
21/// Vertex produced by marching cubes.
22#[derive(Debug, Clone, Copy)]
23pub struct IsoVertex {
24    pub position: Vec3,
25    pub normal: Vec3,
26}
27
28// ── Built-in scalar fields ──────────────────────────────────────────────────
29
30pub struct SdfSphere { pub center: Vec3, pub radius: f32 }
31impl ScalarField for SdfSphere {
32    fn evaluate(&self, p: Vec3) -> f32 {
33        (p - self.center).length() - self.radius
34    }
35}
36
37pub struct SdfBox { pub center: Vec3, pub half_extents: Vec3 }
38impl ScalarField for SdfBox {
39    fn evaluate(&self, p: Vec3) -> f32 {
40        let d = (p - self.center).abs() - self.half_extents;
41        let outside = Vec3::new(d.x.max(0.0), d.y.max(0.0), d.z.max(0.0)).length();
42        let inside = d.x.max(d.y.max(d.z)).min(0.0);
43        outside + inside
44    }
45}
46
47pub struct SdfTorus { pub center: Vec3, pub major: f32, pub minor: f32 }
48impl ScalarField for SdfTorus {
49    fn evaluate(&self, p: Vec3) -> f32 {
50        let q = p - self.center;
51        let xz = Vec2::new(q.x, q.z).length() - self.major;
52        Vec2::new(xz, q.y).length() - self.minor
53    }
54}
55
56pub struct SdfGyroid { pub scale: f32, pub thickness: f32 }
57impl ScalarField for SdfGyroid {
58    fn evaluate(&self, p: Vec3) -> f32 {
59        let s = self.scale;
60        let val = (p.x * s).sin() * (p.y * s).cos()
61                + (p.y * s).sin() * (p.z * s).cos()
62                + (p.z * s).sin() * (p.x * s).cos();
63        val.abs() - self.thickness
64    }
65}
66
67/// Custom scalar field from a closure.
68pub struct CustomField<F: Fn(Vec3) -> f32 + Send + Sync> { pub func: F }
69impl<F: Fn(Vec3) -> f32 + Send + Sync> ScalarField for CustomField<F> {
70    fn evaluate(&self, p: Vec3) -> f32 { (self.func)(p) }
71}
72
73// ── Marching Cubes ──────────────────────────────────────────────────────────
74
75/// Marching cubes isosurface extraction.
76pub struct MarchingCubes {
77    pub resolution: u32,
78    pub bounds_min: Vec3,
79    pub bounds_max: Vec3,
80    pub iso_level: f32,
81}
82
83impl MarchingCubes {
84    pub fn new(resolution: u32, bounds_min: Vec3, bounds_max: Vec3) -> Self {
85        Self { resolution, bounds_min, bounds_max, iso_level: 0.0 }
86    }
87
88    /// Extract isosurface from a scalar field. Returns a triangle mesh.
89    pub fn extract(&self, field: &dyn ScalarField) -> GeoMesh {
90        let mut mesh = GeoMesh::new();
91        let res = self.resolution;
92        let step = (self.bounds_max - self.bounds_min) / res as f32;
93
94        // Sample the field on a grid
95        let grid_size = (res + 1) as usize;
96        let mut values = vec![0.0f32; grid_size * grid_size * grid_size];
97
98        for z in 0..=res {
99            for y in 0..=res {
100                for x in 0..=res {
101                    let p = self.bounds_min + Vec3::new(x as f32, y as f32, z as f32) * step;
102                    let idx = x as usize + y as usize * grid_size + z as usize * grid_size * grid_size;
103                    values[idx] = field.evaluate(p);
104                }
105            }
106        }
107
108        // March through cubes
109        for z in 0..res {
110            for y in 0..res {
111                for x in 0..res {
112                    self.process_cube(&mut mesh, field, &values, x, y, z, grid_size, step);
113                }
114            }
115        }
116
117        mesh.recompute_normals();
118        mesh
119    }
120
121    fn process_cube(
122        &self,
123        mesh: &mut GeoMesh,
124        field: &dyn ScalarField,
125        values: &[f32],
126        x: u32, y: u32, z: u32,
127        grid_size: usize,
128        step: Vec3,
129    ) {
130        let idx = |xi: u32, yi: u32, zi: u32| -> usize {
131            xi as usize + yi as usize * grid_size + zi as usize * grid_size * grid_size
132        };
133
134        // 8 corner values
135        let corners = [
136            values[idx(x, y, z)],
137            values[idx(x + 1, y, z)],
138            values[idx(x + 1, y + 1, z)],
139            values[idx(x, y + 1, z)],
140            values[idx(x, y, z + 1)],
141            values[idx(x + 1, y, z + 1)],
142            values[idx(x + 1, y + 1, z + 1)],
143            values[idx(x, y + 1, z + 1)],
144        ];
145
146        // Compute cube index (bitmask of which corners are inside)
147        let mut cube_index = 0u8;
148        for i in 0..8 {
149            if corners[i] < self.iso_level {
150                cube_index |= 1 << i;
151            }
152        }
153
154        // Skip if entirely inside or outside
155        if cube_index == 0 || cube_index == 0xFF { return; }
156
157        let corner_positions = [
158            self.bounds_min + Vec3::new(x as f32, y as f32, z as f32) * step,
159            self.bounds_min + Vec3::new(x as f32 + 1.0, y as f32, z as f32) * step,
160            self.bounds_min + Vec3::new(x as f32 + 1.0, y as f32 + 1.0, z as f32) * step,
161            self.bounds_min + Vec3::new(x as f32, y as f32 + 1.0, z as f32) * step,
162            self.bounds_min + Vec3::new(x as f32, y as f32, z as f32 + 1.0) * step,
163            self.bounds_min + Vec3::new(x as f32 + 1.0, y as f32, z as f32 + 1.0) * step,
164            self.bounds_min + Vec3::new(x as f32 + 1.0, y as f32 + 1.0, z as f32 + 1.0) * step,
165            self.bounds_min + Vec3::new(x as f32, y as f32 + 1.0, z as f32 + 1.0) * step,
166        ];
167
168        // Edge table: which edges are intersected
169        let edge_mask = EDGE_TABLE[cube_index as usize];
170        if edge_mask == 0 { return; }
171
172        // Interpolate edge vertices
173        let mut edge_verts = [Vec3::ZERO; 12];
174        let edges: [(usize, usize); 12] = [
175            (0,1),(1,2),(2,3),(3,0),(4,5),(5,6),(6,7),(7,4),(0,4),(1,5),(2,6),(3,7)
176        ];
177        for i in 0..12 {
178            if (edge_mask & (1 << i)) != 0 {
179                let (a, b) = edges[i];
180                edge_verts[i] = interpolate_vertex(
181                    corner_positions[a], corner_positions[b],
182                    corners[a], corners[b], self.iso_level
183                );
184            }
185        }
186
187        // Generate triangles from the triangle table
188        let row = &TRI_TABLE[cube_index as usize];
189        let mut i = 0;
190        while i < row.len() && row[i] != -1 {
191            let v0 = mesh.add_vertex(edge_verts[row[i] as usize], Vec3::Y, Vec2::ZERO);
192            let v1 = mesh.add_vertex(edge_verts[row[i + 1] as usize], Vec3::Y, Vec2::ZERO);
193            let v2 = mesh.add_vertex(edge_verts[row[i + 2] as usize], Vec3::Y, Vec2::ZERO);
194            mesh.add_triangle(v0, v1, v2);
195            i += 3;
196        }
197    }
198}
199
200fn interpolate_vertex(p1: Vec3, p2: Vec3, v1: f32, v2: f32, iso: f32) -> Vec3 {
201    let denom = v2 - v1;
202    if denom.abs() < 1e-8 { return (p1 + p2) * 0.5; }
203    let t = (iso - v1) / denom;
204    p1 + (p2 - p1) * t.clamp(0.0, 1.0)
205}
206
207// ── Marching cubes tables (abbreviated — full 256 entries) ──────────────────
208
209// Edge table: for each of 256 cube configurations, which edges are intersected.
210static EDGE_TABLE: [u16; 256] = {
211    let mut table = [0u16; 256];
212    // Populate common configurations. In production this is a full 256-entry LUT.
213    // Here we generate them programmatically from vertex corner states.
214    let edges: [(usize, usize); 12] = [
215        (0,1),(1,2),(2,3),(3,0),(4,5),(5,6),(6,7),(7,4),(0,4),(1,5),(2,6),(3,7)
216    ];
217    let mut ci = 0u16;
218    while ci < 256 {
219        let mut mask = 0u16;
220        let mut e = 0;
221        while e < 12 {
222            let (a, b) = edges[e];
223            let a_in = (ci >> a) & 1;
224            let b_in = (ci >> b) & 1;
225            if a_in != b_in {
226                mask |= 1 << e;
227            }
228            e += 1;
229        }
230        table[ci as usize] = mask;
231        ci += 1;
232    }
233    table
234};
235
236// Triangle table: for each of 256 cube configurations, the triangles to generate.
237// -1 terminates each row. This is a simplified version; full tables have up to 5 triangles.
238// We generate a basic version from the edge crossings.
239static TRI_TABLE: [[i8; 16]; 256] = {
240    let empty = [-1i8; 16];
241    let mut table = [empty; 256];
242    // Configuration 1 (only corner 0 inside): edges 0,3,8
243    table[1] = [0, 8, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1];
244    table[2] = [0, 1, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1];
245    table[3] = [1, 8, 3, 9, 8, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1];
246    table[4] = [1, 2, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1];
247    table[5] = [0, 8, 3, 1, 2, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1];
248    table[6] = [9, 2, 10, 0, 2, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1];
249    // ... remaining 249 entries follow the standard Lorensen & Cline table
250    // For brevity, unlisted configs produce no triangles (empty row = all -1)
251    table
252};
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257
258    #[test]
259    fn sphere_sdf_at_surface_is_zero() {
260        let s = SdfSphere { center: Vec3::ZERO, radius: 1.0 };
261        let val = s.evaluate(Vec3::new(1.0, 0.0, 0.0));
262        assert!(val.abs() < 0.01);
263    }
264
265    #[test]
266    fn sphere_sdf_inside_is_negative() {
267        let s = SdfSphere { center: Vec3::ZERO, radius: 1.0 };
268        assert!(s.evaluate(Vec3::ZERO) < 0.0);
269    }
270
271    #[test]
272    fn marching_cubes_sphere_produces_mesh() {
273        let field = SdfSphere { center: Vec3::ZERO, radius: 1.0 };
274        let mc = MarchingCubes::new(8, Vec3::splat(-2.0), Vec3::splat(2.0));
275        let mesh = mc.extract(&field);
276        assert!(mesh.vertex_count() > 0, "MC should produce vertices for a sphere");
277    }
278
279    #[test]
280    fn box_sdf_inside_is_negative() {
281        let b = SdfBox { center: Vec3::ZERO, half_extents: Vec3::ONE };
282        assert!(b.evaluate(Vec3::ZERO) < 0.0);
283    }
284
285    #[test]
286    fn gyroid_sdf_evaluates() {
287        let g = SdfGyroid { scale: 5.0, thickness: 0.3 };
288        let _v = g.evaluate(Vec3::new(0.5, 0.5, 0.5));
289        // Just verify it doesn't panic
290    }
291}