1use glam::{Vec3, Vec4};
13
14use super::entity_field::MetaballEntity;
15
16pub const EDGE_TABLE: [u16; 256] = [0u16; 256];
18pub 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
49const 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 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 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 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 pub fn extract_entity(entity: &MetaballEntity) -> ExtractedMesh {
159 let (min, max) = entity.bounds();
160 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
171fn 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
181fn 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 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 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 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 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}