git.lucas.co / cce-designer
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 }