Skip to main content

axiolid_refine/
remesh.rs

1//! Isotropic remeshing: rewrite a triangle mesh so its edges approach one
2//! target length and its vertices approach valence six, while the surface
3//! stays on the input.
4//!
5//! # Method
6//!
7//! The incremental scheme of Botsch and Kobbelt ("A remeshing approach to
8//! multiresolution modeling", SGP 2004), as CGAL's
9//! `Polygon_mesh_processing::isotropic_remeshing` implements it. With `L`
10//! the target edge length, each iteration runs four passes over a
11//! [`HalfedgeMesh`]:
12//!
13//! 1. split every edge longer than `4/3 L` at its midpoint;
14//! 2. collapse every edge shorter than `4/5 L` whose collapse creates no
15//!    edge longer than `4/3 L`;
16//! 3. flip edges where that brings the four corners' valences closer to
17//!    optimal (6 inside, 4 on a boundary);
18//! 4. move every free vertex towards the area-weighted centroid of its
19//!    triangles, within its tangent plane, and project it back onto the
20//!    input surface.
21//!
22//! # What is kept
23//!
24//! *Protected* edges are the input's boundary edges and, unless
25//! [`RemeshOptions::feature_angle`] is `None`, every edge whose two faces'
26//! normals differ by more than that angle. A protected edge is never
27//! flipped, may be split (its midpoint stays on the edge) and may be
28//! collapsed only along itself, removing a vertex its feature line runs
29//! straight through. So every protected polyline of the input is kept
30//! exactly: its corners stay, bit-identical, and every vertex added on it
31//! lies on one of its segments. Vertices on protected edges are not
32//! relaxed. The boundary loops, the Euler characteristic and the
33//! orientation of the input are therefore preserved.
34//!
35//! The protected edges cut the input into *patches*. A free vertex is
36//! projected onto the closest point of its own patch only, so it never
37//! crosses a sharp edge.
38//!
39//! # What is refused and what is skipped
40//!
41//! Input the halfedge structure cannot represent -- non-manifold edges or
42//! vertices, inconsistent winding, out-of-range or repeated corners -- is
43//! refused by name, as are non-finite positions, degenerate (zero-area)
44//! input triangles, a non-positive target, a feature angle outside
45//! `[0, pi]` and a request whose output would exceed the triangle budget.
46//!
47//! Individual edits are *skipped*, not forced: a collapse, flip or move that
48//! would fold a triangle (turn its normal by a right angle or more) or leave
49//! it flatter than a height-to-longest-edge ratio of `1e-3`, a collapse or
50//! flip that would lower the smallest angle of the triangles it rewrites
51//! below 15 degrees (when it was above), or an edit that breaks the link
52//! condition, does not happen. The result is therefore always a
53//! valid, consistently oriented mesh, and [`RemeshReport`] states how close
54//! it came: the fraction of edges within `[4/5 L, 4/3 L]`, the valence
55//! deviation before and after, the smallest angle, and the largest distance
56//! of any output vertex from the input surface.
57//!
58//! # Deviation from the input
59//!
60//! Protected vertices lie on the input exactly (up to the rounding of a
61//! midpoint). Free vertices are projected onto the input, so their distance
62//! from it is zero up to rounding unless a projection was skipped to avoid a
63//! fold; [`RemeshReport::max_vertex_deviation`] measures it over every
64//! output vertex. Between vertices the output is a chord of the input: on a
65//! surface of curvature radius `R` its distance grows like `L^2 / (8 R)`,
66//! the sagitta of an edge of length `L`. That part is the cost of the
67//! requested resolution, not of the algorithm.
68//!
69//! # Determinism
70//!
71//! Every pass visits elements in id order and every decision is a function
72//! of the current mesh; no hash map is iterated. The same input and options
73//! give a bit-identical output on every run.
74
75mod edits;
76mod reference;
77
78use axiolid_core::{Point3, Scalar};
79use axiolid_mesh::{
80    AttributeFate, EdgeId, HalfedgeBuildError, HalfedgeEditError, HalfedgeMesh, TriMesh,
81};
82
83use edits::Remesher;
84use reference::Reference;
85
86/// Why a remeshing could not be performed.
87#[derive(Debug, Clone, PartialEq, thiserror::Error)]
88#[non_exhaustive]
89pub enum RemeshError {
90    /// The target edge length must be a positive, finite length.
91    #[error("target edge length {0} is not a positive finite length")]
92    InvalidTarget(Scalar),
93    /// The feature angle must lie in `[0, pi]` radians.
94    #[error("feature angle {0} is not within [0, pi] radians")]
95    InvalidFeatureAngle(Scalar),
96    /// The input is not an oriented 2-manifold triangle mesh: the halfedge
97    /// structure names the configuration it cannot represent.
98    #[error("input is not an oriented manifold triangle mesh: {0}")]
99    InvalidMesh(#[from] HalfedgeBuildError),
100    /// A vertex position is NaN or infinite.
101    #[error("vertex {vertex} has a non-finite position")]
102    NonFinitePosition {
103        /// The vertex.
104        vertex: usize,
105    },
106    /// An input triangle has no area, so it has no normal to keep.
107    #[error("triangle {triangle} is degenerate")]
108    DegenerateTriangle {
109        /// The triangle.
110        triangle: usize,
111    },
112    /// The output would exceed the triangle budget.
113    #[error("remeshing would produce about {produced} triangles, over the {limit} budget")]
114    BudgetExceeded {
115        /// Triangles the request would produce.
116        produced: usize,
117        /// The cap.
118        limit: usize,
119    },
120    /// A local edit refused a configuration the remesher had checked; this
121    /// indicates a defect, not a property of the input.
122    #[error("internal halfedge edit refused: {0}")]
123    Internal(HalfedgeEditError),
124}
125
126/// What to remesh towards.
127#[derive(Debug, Clone, Copy, PartialEq)]
128#[non_exhaustive]
129pub struct RemeshOptions {
130    /// Target edge length `L`, in model units.
131    pub target_edge_length: Scalar,
132    /// Split-collapse-flip-relax iterations.
133    pub iterations: u32,
134    /// Dihedral angle, in radians, above which an interior edge is a sharp
135    /// feature and protected like a boundary edge. `None` protects only the
136    /// boundary.
137    pub feature_angle: Option<Scalar>,
138}
139
140impl RemeshOptions {
141    /// Iterations run by default, as in Botsch and Kobbelt's paper.
142    pub const DEFAULT_ITERATIONS: u32 = 5;
143
144    /// Default feature angle: 60 degrees between face normals.
145    pub const DEFAULT_FEATURE_ANGLE: Scalar = core::f64::consts::FRAC_PI_3;
146
147    /// Remesh towards `target_edge_length` with the default iterations and
148    /// feature angle.
149    #[must_use]
150    pub fn new(target_edge_length: Scalar) -> Self {
151        Self {
152            target_edge_length,
153            iterations: Self::DEFAULT_ITERATIONS,
154            feature_angle: Some(Self::DEFAULT_FEATURE_ANGLE),
155        }
156    }
157
158    /// The same options with another iteration count.
159    #[must_use]
160    pub fn with_iterations(mut self, iterations: u32) -> Self {
161        self.iterations = iterations;
162        self
163    }
164
165    /// The same options with another feature angle; `None` protects only
166    /// the boundary.
167    #[must_use]
168    pub fn with_feature_angle(mut self, feature_angle: Option<Scalar>) -> Self {
169        self.feature_angle = feature_angle;
170        self
171    }
172}
173
174/// What a remeshing did and how close it came to the target.
175#[derive(Debug, Clone, PartialEq)]
176#[non_exhaustive]
177pub struct RemeshReport {
178    /// Triangles before.
179    pub input_triangles: usize,
180    /// Triangles after.
181    pub output_triangles: usize,
182    /// Edge splits performed.
183    pub splits: usize,
184    /// Edge collapses performed.
185    pub collapses: usize,
186    /// Edge flips performed.
187    pub flips: usize,
188    /// Vertex relocations (relaxation and projection) performed.
189    pub moves: usize,
190    /// Protected (boundary or sharp feature) edges of the output.
191    pub protected_edges: usize,
192    /// Fraction of output edges with length in `[4/5 L, 4/3 L]`.
193    pub edges_in_band: Scalar,
194    /// Mean absolute distance of the input's vertex valences from optimal
195    /// (6 inside, 4 on a boundary).
196    pub valence_deviation_before: Scalar,
197    /// The same for the output.
198    pub valence_deviation_after: Scalar,
199    /// Smallest interior angle of any output triangle, in radians.
200    pub min_angle: Scalar,
201    /// Largest distance of an output vertex from the input surface, in
202    /// model units.
203    pub max_vertex_deviation: Scalar,
204    /// What happened to each named attribute channel: every one is dropped,
205    /// since remeshing creates vertices with no preimage to read from.
206    pub attribute_fates: Vec<(String, AttributeFate)>,
207}
208
209/// Remesh a triangle mesh towards a uniform edge length.
210///
211/// See the [module documentation](self) for the method, what is preserved
212/// and what the report measures. Normals and attribute channels are not
213/// carried; the report lists each channel as dropped.
214///
215/// # Errors
216///
217/// [`RemeshError::InvalidTarget`], [`RemeshError::InvalidFeatureAngle`],
218/// [`RemeshError::InvalidMesh`] for non-manifold, inconsistently wound,
219/// ragged or out-of-range input, [`RemeshError::NonFinitePosition`],
220/// [`RemeshError::DegenerateTriangle`] and [`RemeshError::BudgetExceeded`].
221pub fn remesh(
222    mesh: &TriMesh,
223    options: RemeshOptions,
224) -> Result<(TriMesh, RemeshReport), RemeshError> {
225    let target = options.target_edge_length;
226    if !target.is_finite() || target <= 0.0 {
227        return Err(RemeshError::InvalidTarget(target));
228    }
229    if let Some(angle) = options.feature_angle {
230        if !(0.0..=core::f64::consts::PI).contains(&angle) {
231            return Err(RemeshError::InvalidFeatureAngle(angle));
232        }
233    }
234    let input = HalfedgeMesh::from_tri_mesh(mesh)?;
235    if let Some(vertex) = mesh.positions.iter().position(|p| !p.is_finite()) {
236        return Err(RemeshError::NonFinitePosition { vertex });
237    }
238    let triangles: Vec<[Point3; 3]> = mesh
239        .triangles()
240        .map(|t| t.map(|i| mesh.positions[i as usize]))
241        .collect();
242    let mut area = 0.0;
243    for (index, t) in triangles.iter().enumerate() {
244        let n = edits::normal(*t);
245        let scale = (t[1] - t[0]).length() * (t[2] - t[0]).length();
246        if n.length() <= DEGENERATE_SINE * scale {
247            return Err(RemeshError::DegenerateTriangle { triangle: index });
248        }
249        area += n.length() / 2.0;
250    }
251    // An equilateral triangle of side L has area sqrt(3)/4 L^2.
252    let expected = area / (3.0_f64.sqrt() / 4.0 * target * target);
253    if !expected.is_finite() || expected > crate::MAX_TRIANGLES as Scalar {
254        return Err(RemeshError::BudgetExceeded {
255            produced: if expected.is_finite() {
256                expected as usize
257            } else {
258                usize::MAX
259            },
260            limit: crate::MAX_TRIANGLES,
261        });
262    }
263
264    let protected = protected_edges(&input, &triangles, options.feature_angle);
265    let patch = patches(&input, &protected);
266    let reference = Reference::new(triangles, &patch);
267    let valence_deviation_before = valence_deviation(&input);
268    let input_triangles = input.face_count();
269
270    let mut remesher = Remesher::new(
271        input,
272        protected,
273        patch,
274        &reference,
275        target,
276        crate::MAX_TRIANGLES,
277    );
278    for _ in 0..options.iterations {
279        remesher.split_long_edges()?;
280        remesher.collapse_short_edges();
281        remesher.equalize_valences();
282        remesher.relax_and_project();
283    }
284
285    let result = &remesher.mesh;
286    let (low, high) = (target * 4.0 / 5.0, target * 4.0 / 3.0);
287    let mut in_band = 0usize;
288    let mut protected_count = 0usize;
289    for e in result.edges() {
290        let [a, b] = result.edge_vertices(e);
291        let length = (result.position(a) - result.position(b)).length();
292        if (low..=high).contains(&length) {
293            in_band += 1;
294        }
295        if remesher.is_protected(e) {
296            protected_count += 1;
297        }
298    }
299    let edges_in_band = if result.edge_count() == 0 {
300        1.0
301    } else {
302        in_band as Scalar / result.edge_count() as Scalar
303    };
304    let max_vertex_deviation = result
305        .vertices()
306        .filter(|&v| !result.is_isolated(v))
307        .map(|v| reference.distance(result.position(v)))
308        .fold(0.0, Scalar::max);
309    let min_angle = result
310        .faces()
311        .map(|f| {
312            let mut corners = result.face_vertices(f).map(|v| result.position(v));
313            let t = [(); 3].map(|()| corners.next().unwrap_or(Point3::ZERO));
314            edits::min_angle(t)
315        })
316        .fold(core::f64::consts::PI, Scalar::min);
317    let valence_deviation_after = valence_deviation(result);
318    let out = result.to_tri_mesh().map_err(RemeshError::Internal)?;
319    let counts = remesher.counts;
320
321    let attribute_fates = crate::carry_attributes(mesh);
322    let report = RemeshReport {
323        input_triangles,
324        output_triangles: out.triangle_count(),
325        splits: counts.splits,
326        collapses: counts.collapses,
327        flips: counts.flips,
328        moves: counts.moves,
329        protected_edges: protected_count,
330        edges_in_band,
331        valence_deviation_before,
332        valence_deviation_after,
333        min_angle,
334        max_vertex_deviation,
335        attribute_fates,
336    };
337    Ok((out, report))
338}
339
340/// An input triangle whose corner angle at its first vertex has a sine at
341/// most this is refused as degenerate.
342const DEGENERATE_SINE: Scalar = 1e-12;
343
344/// Boundary edges, and interior edges whose faces' normals differ by more
345/// than the feature angle. Indexed by edge id.
346fn protected_edges(
347    mesh: &HalfedgeMesh,
348    triangles: &[[Point3; 3]],
349    feature_angle: Option<Scalar>,
350) -> Vec<bool> {
351    let mut protected = vec![false; mesh.edges().last().map_or(0, |e| e.index() + 1)];
352    for e in mesh.edges() {
353        protected[e.index()] = if mesh.is_boundary_edge(e) {
354            true
355        } else if let Some(angle) = feature_angle {
356            let normal = |side| {
357                let f = mesh.face(mesh.edge_halfedge(e, side)).map(|f| f.index());
358                f.and_then(|f| edits::normal(triangles[f]).try_normalize())
359            };
360            match (normal(0), normal(1)) {
361                (Some(n0), Some(n1)) => n0.dot(n1).clamp(-1.0, 1.0).acos() > angle,
362                _ => true,
363            }
364        } else {
365            false
366        };
367    }
368    protected
369}
370
371/// Connected components of faces across unprotected edges, numbered in
372/// order of their lowest face id. Indexed by face id.
373fn patches(mesh: &HalfedgeMesh, protected: &[bool]) -> Vec<u32> {
374    let count = mesh.faces().last().map_or(0, |f| f.index() + 1);
375    let mut patch = vec![u32::MAX; count];
376    let mut next = 0u32;
377    let mut stack = Vec::new();
378    for seed in mesh.faces() {
379        if patch[seed.index()] != u32::MAX {
380            continue;
381        }
382        patch[seed.index()] = next;
383        stack.push(seed);
384        while let Some(f) = stack.pop() {
385            for h in mesh.face_halfedges(f) {
386                let e: EdgeId = mesh.edge(h);
387                if protected[e.index()] {
388                    continue;
389                }
390                if let Some(g) = mesh.face(mesh.opposite(h)) {
391                    if patch[g.index()] == u32::MAX {
392                        patch[g.index()] = next;
393                        stack.push(g);
394                    }
395                }
396            }
397        }
398        next += 1;
399    }
400    patch
401}
402
403/// Mean absolute distance of vertex valences from optimal, over vertices
404/// with at least one edge.
405fn valence_deviation(mesh: &HalfedgeMesh) -> Scalar {
406    let mut total = 0i64;
407    let mut count = 0usize;
408    for v in mesh.vertices().filter(|&v| !mesh.is_isolated(v)) {
409        let optimal = if mesh.is_boundary_vertex(v) { 4 } else { 6 };
410        total += (mesh.degree(v) as i64 - optimal).abs();
411        count += 1;
412    }
413    if count == 0 {
414        0.0
415    } else {
416        total as Scalar / count as Scalar
417    }
418}
419
420#[cfg(test)]
421mod tests {
422    use super::*;
423
424    #[test]
425    fn protected_edges_cut_a_box_into_its_six_faces() {
426        let p = |x: Scalar, y: Scalar, z: Scalar| Point3::new(x, y, z);
427        let positions = vec![
428            p(0.0, 0.0, 0.0),
429            p(1.0, 0.0, 0.0),
430            p(1.0, 1.0, 0.0),
431            p(0.0, 1.0, 0.0),
432            p(0.0, 0.0, 1.0),
433            p(1.0, 0.0, 1.0),
434            p(1.0, 1.0, 1.0),
435            p(0.0, 1.0, 1.0),
436        ];
437        let indices = vec![
438            0, 2, 1, 0, 3, 2, 4, 5, 6, 4, 6, 7, 0, 1, 5, 0, 5, 4, 1, 2, 6, 1, 6, 5, 2, 3, 7, 2, 7,
439            6, 3, 0, 4, 3, 4, 7,
440        ];
441        let mesh = TriMesh::new(positions, indices);
442        let he = HalfedgeMesh::from_tri_mesh(&mesh).expect("closed box");
443        let triangles: Vec<[Point3; 3]> = mesh
444            .triangles()
445            .map(|t| t.map(|i| mesh.positions[i as usize]))
446            .collect();
447        let protected =
448            protected_edges(&he, &triangles, Some(RemeshOptions::DEFAULT_FEATURE_ANGLE));
449        assert_eq!(protected.iter().filter(|&&p| p).count(), 12);
450        let patch = patches(&he, &protected);
451        assert_eq!(patch, vec![0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5]);
452        // Without features, one patch.
453        let protected = protected_edges(&he, &triangles, None);
454        assert_eq!(patches(&he, &protected), vec![0; 12]);
455    }
456}