graphic design tool
git clone https://git.lucas.co/cce-designer.git
src/volume.rs (19.7K)
1 //! Signed distance fields, and the way back to a surface.
2 //!
3 //! The representation the manufacturing work stands on. Shelling a shape,
4 //! offsetting it, cutting one shape out of another — none of those are natural
5 //! on a triangle mesh, where they mean finding every self-intersection the
6 //! operation creates and stitching the result back into something closed. On a
7 //! distance field they are arithmetic: offsetting is subtraction, union is a
8 //! minimum, difference is a maximum against a negation. The mesh comes back at
9 //! the end, closed by construction.
10 //!
11 //! ## Dense, not sparse
12 //!
13 //! A dense grid over the shape's bounding box, not a sparse tree. At the sizes
14 //! this tool works on — a thing you can hold, meshed finely enough to print — a
15 //! 128³ grid is eight megabytes and answers every query by indexing. A sparse
16 //! structure buys memory back on volumes mostly made of empty space, and costs
17 //! a tree walk on every one of the millions of lookups the surface extraction
18 //! makes. The day a model needs 512³ is the day to write one.
19 //!
20 //! ## Two passes, because sign and distance are different questions
21 //!
22 //! Building the field from a mesh is done twice over:
23 //!
24 //! 1. **Distance** comes from the triangle grid — the closest point on the
25 //! surface, which is exact and needs no assumptions about the mesh.
26 //! 2. **Sign** applies only to a CLOSED surface — an open one has no inside,
27 //! and is left unsigned so that offsetting thickens it into a slab. Where
28 //! there is an inside, it comes from two tests, each used where it is the
29 //! accurate one: a
30 //! flood outward from the grid boundary decides everything far from the
31 //! surface, and the nearest face's normal decides the thin band either side
32 //! of it.
33 //!
34 //! A sign that is wrong anywhere is a bubble or a hole in the result, where in
35 //! the Distance node the same mistake was only a slightly wrong number — which
36 //! is why this is worth two passes and not one ray cast.
37
38 use crate::detail::Detail;
39 use crate::spatial::TriGrid;
40 use glam::Vec3;
41
42 /// A signed distance field on a regular grid. Negative is inside.
43 #[derive(Clone, Debug)]
44 pub struct Volume {
45 origin: Vec3,
46 voxel: f32,
47 /// Samples per axis, so the last sample sits at `origin + (dims-1) * voxel`.
48 dims: [usize; 3],
49 data: Vec<f32>,
50 }
51
52 /// How far from the surface distances are measured, in voxels.
53 ///
54 /// Beyond this the field records only "further than this", which is all the
55 /// extraction and the flood fill need. It bounds the work per sample, and it
56 /// bounds what an offset can do: moving the surface more than this far has
57 /// nothing to move into, so a node offsetting further must ask for a bigger
58 /// voxel or accept the clamp.
59 pub const REACH_VOXELS: f32 = 3.0;
60
61 /// How many samples an axis needs to span `extent` at `voxel`, clamped.
62 fn axis_dims(extent: f32, voxel: f32) -> usize {
63 ((extent / voxel).ceil() as usize + 3).clamp(2, 256)
64 }
65
66 impl Volume {
67 pub fn dims(&self) -> [usize; 3] {
68 self.dims
69 }
70
71 pub fn voxel(&self) -> f32 {
72 self.voxel
73 }
74
75 fn index(&self, i: usize, j: usize, k: usize) -> usize {
76 (k * self.dims[1] + j) * self.dims[0] + i
77 }
78
79 pub fn at(&self, i: usize, j: usize, k: usize) -> f32 {
80 self.data[self.index(i, j, k)]
81 }
82
83 /// The world position of a sample. Public so a caller can check a field
84 /// against the shape it was built from.
85 pub fn sample_position(&self, i: usize, j: usize, k: usize) -> Vec3 {
86 self.position(i, j, k)
87 }
88
89 fn position(&self, i: usize, j: usize, k: usize) -> Vec3 {
90 self.origin + Vec3::new(i as f32, j as f32, k as f32) * self.voxel
91 }
92
93 /// The bounds a mesh needs, with room for the offset a caller will apply.
94 ///
95 /// Padding matters: a field built tight to the surface has no room to
96 /// dilate into, and the offset surface would be clipped flat at the edge of
97 /// the grid rather than rounded.
98 pub fn bounds_for(d: &Detail, padding: f32) -> Option<(Vec3, Vec3)> {
99 let (lo, hi) = d.bounds()?;
100 Some((lo - Vec3::splat(padding), hi + Vec3::splat(padding)))
101 }
102
103 /// Sample a mesh into a field over the given bounds.
104 ///
105 /// Two meshes sampled over the SAME bounds at the same voxel size share a
106 /// grid, which is what lets the booleans be elementwise.
107 pub fn from_mesh_in(d: &Detail, lo: Vec3, hi: Vec3, voxel: f32) -> Volume {
108 Self::build(d, lo, hi, voxel, voxel.max(1e-5) * REACH_VOXELS)
109 }
110
111 /// [`Volume::from_mesh_in`] measuring distances out to `reach`.
112 ///
113 /// Beyond `reach` the field says only "further than this", so it bounds
114 /// both the cost and how far an offset can move the surface. A caller that
115 /// means to offset by more must say so here.
116 pub fn build(d: &Detail, lo: Vec3, hi: Vec3, voxel: f32, reach: f32) -> Volume {
117 let voxel = voxel.max(1e-5);
118 let reach = reach.max(voxel * 2.0);
119 let extent = hi - lo;
120 let dims = [
121 axis_dims(extent.x, voxel),
122 axis_dims(extent.y, voxel),
123 axis_dims(extent.z, voxel),
124 ];
125 // Centred, so the padding is even on both sides rather than piling up
126 // wherever the rounding landed.
127 let span = Vec3::new(
128 (dims[0] - 1) as f32,
129 (dims[1] - 1) as f32,
130 (dims[2] - 1) as f32,
131 ) * voxel;
132 let origin = (lo + hi) * 0.5 - span * 0.5;
133
134 let n = dims[0] * dims[1] * dims[2];
135 let mut vol = Volume { origin, voxel, dims, data: vec![f32::MAX; n] };
136 let grid = TriGrid::build(d);
137 if grid.is_empty() {
138 // No surface: everything is outside, at a distance nothing will
139 // mistake for a crossing.
140 vol.data.fill(span.max_element().max(1.0));
141 return vol;
142 }
143
144 // Pass one: unsigned distance, exact from the triangle grid — but only
145 // out to REACH, beyond which the field records "further than this".
146 //
147 // A volume does not need an exact distance far from the surface: the
148 // extraction only looks at the zero crossing and the flood fill only
149 // asks "further than the band". Measuring it anyway is what made this
150 // slow, because an unbounded nearest-surface query from a corner of the
151 // grid gathers every triangle in the mesh. The consequence to know is
152 // that an OFFSET larger than the reach is not represented — which is
153 // why the reach is taken from the padding, the room the caller asked
154 // for in order to offset into.
155 for k in 0..dims[2] {
156 for j in 0..dims[1] {
157 for i in 0..dims[0] {
158 let p = vol.position(i, j, k);
159 let d = grid
160 .closest_within(p, reach)
161 .map(|h| h.distance)
162 .unwrap_or(reach);
163 let idx = vol.index(i, j, k);
164 vol.data[idx] = d;
165 }
166 }
167 }
168
169 // Pass two: sign — but only if there is an inside to speak of.
170 //
171 // "What is inside this?" only has an answer for a CLOSED surface. A
172 // flat disc, a single polygon or a torn mesh has none, and signing one
173 // anyway gives whichever side its normals happen to face: the field
174 // then reads as a half-space and a boolean against it carves something
175 // nobody asked for. Left unsigned, the same disc is the set of points
176 // near it — so an offset thickens it into a slab, which is the useful
177 // answer and the honest one.
178 if !d.is_closed() {
179 return vol;
180 }
181
182 // Which way the mesh is wound, by the divergence theorem: the signed
183 // volume of a closed surface is positive when its faces look outward.
184 //
185 // The band test below asks the nearest face which side a sample is on,
186 // which trusts the winding — and a mesh wound inside out then produces
187 // a field whose band signs alternate against the flood fill's, giving
188 // a surface riddled with holes. Rather than trust it, measure it once
189 // and flip. An imported mesh is not obliged to agree with this app's
190 // convention.
191 let signed_volume: f32 = grid
192 .triangles()
193 .iter()
194 .map(|t| t[0].dot(t[1].cross(t[2])))
195 .sum::<f32>()
196 / 6.0;
197 let facing = if signed_volume < 0.0 { -1.0 } else { 1.0 };
198
199 // Two tests, each used where it is the accurate one.
200 //
201 // Ray parity was the obvious choice and is wrong here: a ray along a
202 // grid row is axis-aligned, the meshes tessellate on the axes, and a
203 // ray through a shared edge hits two triangles at the same point. The
204 // parity flips and STAYS flipped for the rest of the row — which shows
205 // up as contiguous runs of outside samples reading as inside. The
206 // collision resolver already carried a comment about exactly this.
207 //
208 // Instead:
209 //
210 // 1. FAR FROM THE SURFACE, flood outward from the grid's boundary,
211 // which is outside by construction. Anything the flood cannot reach
212 // without crossing the surface band is enclosed, however convoluted
213 // the shape. No rays, no epsilons, no degenerate cases.
214 // 2. IN THE BAND either side of the surface, ask the nearest face which
215 // way it points. That test is unreliable far away in a concave
216 // shape — the nearest face can be one across the gap — and reliable
217 // within half a voxel of the surface, where the nearest face is the
218 // one the sample is sitting on.
219 // A FULL voxel, not a fraction of one. Two samples one voxel apart
220 // cannot both be more than a voxel from the surface AND have the
221 // surface between them — so flooding only between such samples can
222 // never cross it. At 0.75 of a voxel two adjacent samples straddling a
223 // thin wall could both qualify, and the flood walked straight through
224 // into the interior: a slab came back hollow and a boolean against it
225 // cut almost nothing.
226 let band = voxel * 1.01;
227 let idx_of = |i: usize, j: usize, k: usize| (k * dims[1] + j) * dims[0] + i;
228
229 let mut outside = vec![false; n];
230 let mut queue: Vec<(usize, usize, usize)> = Vec::new();
231 for k in 0..dims[2] {
232 for j in 0..dims[1] {
233 for i in 0..dims[0] {
234 let on_boundary = i == 0
235 || j == 0
236 || k == 0
237 || i == dims[0] - 1
238 || j == dims[1] - 1
239 || k == dims[2] - 1;
240 if on_boundary && vol.data[idx_of(i, j, k)] > band {
241 outside[idx_of(i, j, k)] = true;
242 queue.push((i, j, k));
243 }
244 }
245 }
246 }
247 while let Some((i, j, k)) = queue.pop() {
248 let mut visit = |i: usize, j: usize, k: usize, queue: &mut Vec<(usize, usize, usize)>| {
249 let idx = idx_of(i, j, k);
250 if !outside[idx] && vol.data[idx] > band {
251 outside[idx] = true;
252 queue.push((i, j, k));
253 }
254 };
255 if i > 0 { visit(i - 1, j, k, &mut queue); }
256 if j > 0 { visit(i, j - 1, k, &mut queue); }
257 if k > 0 { visit(i, j, k - 1, &mut queue); }
258 if i + 1 < dims[0] { visit(i + 1, j, k, &mut queue); }
259 if j + 1 < dims[1] { visit(i, j + 1, k, &mut queue); }
260 if k + 1 < dims[2] { visit(i, j, k + 1, &mut queue); }
261 }
262
263 for k in 0..dims[2] {
264 for j in 0..dims[1] {
265 for i in 0..dims[0] {
266 let idx = idx_of(i, j, k);
267 let inside = if vol.data[idx] > band {
268 !outside[idx]
269 } else {
270 // In the band: which side of the nearest face.
271 let p = vol.position(i, j, k);
272 match grid.closest_within(p, band * 4.0) {
273 Some(h) => (p - h.point).dot(h.normal) * facing < 0.0,
274 None => false,
275 }
276 };
277 if inside {
278 vol.data[idx] = -vol.data[idx];
279 }
280 }
281 }
282 }
283 vol
284 }
285
286 /// Sample a mesh into a field sized to it, with room to grow.
287 ///
288 /// The padding IS the room to offset into, so it is also the distance the
289 /// field measures out to — asking for space and then not measuring it
290 /// would give an offset nothing to find.
291 pub fn from_mesh(d: &Detail, voxel: f32, padding: f32) -> Volume {
292 let (lo, hi) = Self::bounds_for(d, padding).unwrap_or((Vec3::ZERO, Vec3::ZERO));
293 Self::build(d, lo, hi, voxel, padding)
294 }
295
296 /// Move the surface out (positive) or in (negative).
297 ///
298 /// The whole reason for the representation: an offset on a mesh means
299 /// resolving every self-intersection the move creates, and here it is a
300 /// subtraction.
301 pub fn offset(&mut self, by: f32) {
302 for v in self.data.iter_mut() {
303 *v -= by;
304 }
305 }
306
307 /// Whether two fields share a grid, which the booleans require.
308 pub fn aligned_with(&self, other: &Volume) -> bool {
309 self.dims == other.dims
310 && (self.voxel - other.voxel).abs() < 1e-6
311 && (self.origin - other.origin).length() < 1e-4
312 }
313
314 /// Keep whatever is inside EITHER — the minimum of the two distances.
315 pub fn union(&mut self, other: &Volume) {
316 self.combine(other, |a, b| a.min(b));
317 }
318
319 /// Keep only what is inside BOTH.
320 pub fn intersect(&mut self, other: &Volume) {
321 self.combine(other, |a, b| a.max(b));
322 }
323
324 /// Cut `other` out of this one.
325 pub fn subtract(&mut self, other: &Volume) {
326 self.combine(other, |a, b| a.max(-b));
327 }
328
329 fn combine(&mut self, other: &Volume, f: impl Fn(f32, f32) -> f32) {
330 if !self.aligned_with(other) {
331 return;
332 }
333 for (a, b) in self.data.iter_mut().zip(other.data.iter()) {
334 *a = f(*a, *b);
335 }
336 }
337
338 /// Extract the zero surface as a quad mesh, by naive surface nets.
339 ///
340 /// Surface nets rather than marching cubes: one vertex per cell that the
341 /// surface passes through, placed at the average of that cell's edge
342 /// crossings, and a quad around each grid edge the surface crosses. It is a
343 /// tenth of the code of a correct marching-cubes table, produces quads
344 /// rather than slivers, and cannot be got subtly wrong in one of 256 cases
345 /// nobody exercises. The triangulation it gives is not beautiful, which
346 /// does not matter here: this output goes into the remesher.
347 pub fn to_mesh(&self) -> Detail {
348 let [nx, ny, nz] = self.dims;
349 let mut d = Detail::new();
350 if nx < 2 || ny < 2 || nz < 2 {
351 return d;
352 }
353
354 // One vertex per crossed cell, indexed by cell.
355 let cells = (nx - 1) * (ny - 1) * (nz - 1);
356 let mut vertex = vec![u32::MAX; cells];
357 let cell_index = |i: usize, j: usize, k: usize| (k * (ny - 1) + j) * (nx - 1) + i;
358
359 const CORNERS: [[usize; 3]; 8] = [
360 [0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0],
361 [0, 0, 1], [1, 0, 1], [1, 1, 1], [0, 1, 1],
362 ];
363 const EDGES: [[usize; 2]; 12] = [
364 [0, 1], [1, 2], [2, 3], [3, 0],
365 [4, 5], [5, 6], [6, 7], [7, 4],
366 [0, 4], [1, 5], [2, 6], [3, 7],
367 ];
368
369 for k in 0..nz - 1 {
370 for j in 0..ny - 1 {
371 for i in 0..nx - 1 {
372 let s: Vec<f32> = CORNERS
373 .iter()
374 .map(|c| self.at(i + c[0], j + c[1], k + c[2]))
375 .collect();
376 if s.iter().all(|v| *v < 0.0) || s.iter().all(|v| *v >= 0.0) {
377 continue;
378 }
379 let mut sum = Vec3::ZERO;
380 let mut hits = 0.0f32;
381 for e in EDGES {
382 let (a, b) = (s[e[0]], s[e[1]]);
383 if (a < 0.0) == (b < 0.0) {
384 continue;
385 }
386 // Where along the edge the field is zero. Linear, which
387 // is exactly right for a field that is a distance.
388 let t = a / (a - b);
389 let pa = self.position(
390 i + CORNERS[e[0]][0],
391 j + CORNERS[e[0]][1],
392 k + CORNERS[e[0]][2],
393 );
394 let pb = self.position(
395 i + CORNERS[e[1]][0],
396 j + CORNERS[e[1]][1],
397 k + CORNERS[e[1]][2],
398 );
399 sum += pa + (pb - pa) * t;
400 hits += 1.0;
401 }
402 if hits > 0.0 {
403 vertex[cell_index(i, j, k)] = d.add_point(sum / hits);
404 }
405 }
406 }
407 }
408
409 // One quad per grid edge the surface crosses, joining the four cells
410 // around that edge. Winding follows the sign: the face must look from
411 // inside to outside, so that the plain cross points away from the
412 // solid like every other generator in the app.
413 let quad = |a: u32, b: u32, c: u32, e: u32, flip: bool, d: &mut Detail| {
414 if [a, b, c, e].iter().any(|&v| v == u32::MAX) {
415 return;
416 }
417 if flip {
418 d.add_prim(&[a, b, c, e]);
419 } else {
420 d.add_prim(&[e, c, b, a]);
421 }
422 };
423 for k in 0..nz - 1 {
424 for j in 0..ny - 1 {
425 for i in 0..nx - 1 {
426 // +X edge: the four cells sharing it differ in j and k.
427 if j > 0 && k > 0 && (self.at(i, j, k) < 0.0) != (self.at(i + 1, j, k) < 0.0) {
428 let inside = self.at(i, j, k) < 0.0;
429 quad(
430 vertex[cell_index(i, j - 1, k - 1)],
431 vertex[cell_index(i, j, k - 1)],
432 vertex[cell_index(i, j, k)],
433 vertex[cell_index(i, j - 1, k)],
434 inside,
435 &mut d,
436 );
437 }
438 if i > 0 && k > 0 && (self.at(i, j, k) < 0.0) != (self.at(i, j + 1, k) < 0.0) {
439 let inside = self.at(i, j, k) < 0.0;
440 quad(
441 vertex[cell_index(i - 1, j, k - 1)],
442 vertex[cell_index(i, j, k - 1)],
443 vertex[cell_index(i, j, k)],
444 vertex[cell_index(i - 1, j, k)],
445 !inside,
446 &mut d,
447 );
448 }
449 if i > 0 && j > 0 && (self.at(i, j, k) < 0.0) != (self.at(i, j, k + 1) < 0.0) {
450 let inside = self.at(i, j, k) < 0.0;
451 quad(
452 vertex[cell_index(i - 1, j - 1, k)],
453 vertex[cell_index(i, j - 1, k)],
454 vertex[cell_index(i, j, k)],
455 vertex[cell_index(i - 1, j, k)],
456 inside,
457 &mut d,
458 );
459 }
460 }
461 }
462 }
463 d
464 }
465 }