parry3d/transformation/convex_hull3/
convex_hull.rs

1use super::InitialMesh;
2use super::{ConvexHullError, TriangleFacet};
3use crate::math::Vector3;
4use crate::transformation::convex_hull_utils::indexed_support_point_nth;
5use crate::transformation::convex_hull_utils::{indexed_support_point_id, normalize};
6use crate::utils;
7use alloc::{vec, vec::Vec};
8
9/// Computes the convex hull of a set of 3D points.
10///
11/// The convex hull is the smallest convex shape that contains all input points.
12/// Imagine wrapping a rubber band (2D) or shrink-wrap (3D) tightly around the points.
13///
14/// # Algorithm
15///
16/// Uses the **quickhull algorithm**, which is efficient for most inputs:
17/// - Average case: O(n log n)
18/// - Worst case: O(n²) for specially constructed inputs
19///
20/// # Returns
21///
22/// A tuple `(vertices, indices)`:
23/// - **vertices**: The hull vertices (subset of input points, duplicates removed)
24/// - **indices**: Triangle faces as triplets of vertex indices (CCW winding)
25///
26/// # Panics
27///
28/// Panics if the input is degenerate or the algorithm fails. For error handling,
29/// use [`try_convex_hull`] instead.
30///
31/// # Example
32///
33/// ```rust
34/// # #[cfg(all(feature = "dim3", feature = "f32"))] {
35/// use parry3d::transformation::convex_hull;
36/// use parry3d::math::Vector;
37///
38/// // Vectors forming a tetrahedron
39/// let points = vec![
40///     Vector::ZERO,
41///     Vector::new(1.0, 0.0, 0.0),
42///     Vector::new(0.0, 1.0, 0.0),
43///     Vector::new(0.0, 0.0, 1.0),
44/// ];
45///
46/// let (vertices, indices) = convex_hull(&points);
47///
48/// // Hull has 4 vertices (all input points are on the hull)
49/// assert_eq!(vertices.len(), 4);
50///
51/// // Hull has 4 triangular faces
52/// assert_eq!(indices.len(), 4);
53/// # }
54/// ```
55///
56/// # See Also
57///
58/// - [`try_convex_hull`] - Returns `Result` instead of panicking
59pub fn convex_hull(points: &[Vector3]) -> (Vec<Vector3>, Vec<[u32; 3]>) {
60    try_convex_hull(points).unwrap()
61}
62
63/// Computes the convex hull of a set of 3D points, with error handling.
64///
65/// This is the safe version of [`convex_hull`] that returns a `Result` instead
66/// of panicking on degenerate inputs.
67///
68/// # Arguments
69///
70/// * `points` - The input points (must have at least 3 points)
71///
72/// # Returns
73///
74/// * `Ok((vertices, indices))` - Successfully computed convex hull
75/// * `Err(ConvexHullError::IncompleteInput)` - Less than 3 input points
76/// * `Err(ConvexHullError::...)` - Other errors (degenerate geometry, numerical issues)
77///
78/// # Example
79///
80/// ```rust
81/// # #[cfg(all(feature = "dim3", feature = "f32"))] {
82/// use parry3d::transformation::try_convex_hull;
83/// use parry3d::math::Vector;
84///
85/// // Valid input
86/// let points = vec![
87///     Vector::ZERO,
88///     Vector::new(1.0, 0.0, 0.0),
89///     Vector::new(0.0, 1.0, 0.0),
90///     Vector::new(0.0, 0.0, 1.0),
91/// ];
92///
93/// match try_convex_hull(&points) {
94///     Ok((vertices, indices)) => {
95///         println!("Hull: {} vertices, {} faces", vertices.len(), indices.len());
96///     }
97///     Err(e) => {
98///         println!("Failed: {:?}", e);
99///     }
100/// }
101///
102/// // Degenerate input (too few points)
103/// let bad_points = vec![Vector::ZERO, Vector::new(1.0, 0.0, 0.0)];
104/// assert!(try_convex_hull(&bad_points).is_err());
105/// # }
106/// ```
107pub fn try_convex_hull(
108    points: &[Vector3],
109) -> Result<(Vec<Vector3>, Vec<[u32; 3]>), ConvexHullError> {
110    if points.len() < 3 {
111        return Err(ConvexHullError::IncompleteInput);
112    }
113
114    // print_buildable_vec("input", points);
115
116    let mut normalized_points = points.to_vec();
117    let _ = normalize(&mut normalized_points[..]);
118
119    let mut undecidable_points = Vec::new();
120    let mut silhouette_loop_facets_and_idx = Vec::new();
121    let mut removed_facets = Vec::new();
122
123    let mut triangles;
124
125    match super::try_get_initial_mesh(points, &mut normalized_points[..], &mut undecidable_points)?
126    {
127        InitialMesh::Facets(facets) => {
128            triangles = facets;
129        }
130        InitialMesh::ResultMesh(vertices, indices) => {
131            return Ok((vertices, indices));
132        }
133    }
134
135    let mut i = 0;
136    while i != triangles.len() {
137        silhouette_loop_facets_and_idx.clear();
138
139        if !triangles[i].valid || triangles[i].affinely_dependent {
140            i += 1;
141            continue;
142        }
143
144        // TODO: use triangles[i].furthest_point instead.
145        let pt_id = indexed_support_point_id(
146            triangles[i].normal,
147            &normalized_points[..],
148            triangles[i].visible_points[..].iter().copied(),
149        );
150
151        if let Some(point) = pt_id {
152            triangles[i].valid = false;
153
154            removed_facets.clear();
155            removed_facets.push(i);
156
157            for j in 0usize..3 {
158                // println!(">> loop;");
159                compute_silhouette(
160                    triangles[i].adj[j],
161                    triangles[i].indirect_adj_id[j],
162                    point,
163                    &mut silhouette_loop_facets_and_idx,
164                    &normalized_points[..],
165                    &mut removed_facets,
166                    &mut triangles[..],
167                );
168            }
169
170            // In some degenerate cases (because of float rounding problems), the silhouette may:
171            // 1. Contain self-intersections (i.e. a single vertex is used by more than two edges).
172            // 2. Contain multiple disjoint (but nested) loops.
173            fix_silhouette_topology(
174                &normalized_points,
175                &mut silhouette_loop_facets_and_idx,
176                &mut removed_facets,
177                &mut triangles[..],
178            )?;
179
180            // Check that the silhouette is valid.
181            // TODO: remove this debug code.
182            // {
183            //     for (facet, id) in &silhouette_loop_facets_and_idx {
184            //         assert!(triangles[*facet].valid);
185            //         assert!(!triangles[triangles[*facet].adj[*id]].valid);
186            //     }
187            // }
188
189            if silhouette_loop_facets_and_idx.is_empty() {
190                // Due to inaccuracies, the silhouette could not be computed
191                // (the point seems to be visible from… every triangle).
192                let mut any_valid = false;
193                for triangle in &triangles[i + 1..] {
194                    if triangle.valid && !triangle.affinely_dependent {
195                        any_valid = true;
196                    }
197                }
198
199                if any_valid {
200                    return Err(ConvexHullError::InternalError(
201                        "Internal error: exiting an unfinished work.",
202                    ));
203                }
204
205                // TODO: this is very harsh.
206                triangles[i].valid = true;
207                break;
208            }
209
210            attach_and_push_facets(
211                &silhouette_loop_facets_and_idx[..],
212                point,
213                &normalized_points[..],
214                &mut triangles,
215                &removed_facets[..],
216                &mut undecidable_points,
217            );
218
219            // println!("Verifying facets at iteration: {}, k: {}", i, k);
220            // for i in 0..triangles.len() {
221            //     if triangles[i].valid {
222            //         super::check_facet_links(i, &triangles[..]);
223            //     }
224            // }
225        }
226
227        i += 1;
228    }
229
230    let mut idx = Vec::new();
231
232    for facet in triangles.iter() {
233        if facet.valid {
234            idx.push([
235                facet.pts[0] as u32,
236                facet.pts[1] as u32,
237                facet.pts[2] as u32,
238            ]);
239        }
240    }
241
242    let mut points = points.to_vec();
243    utils::remove_unused_points(&mut points, &mut idx[..]);
244    // super::check_convex_hull(&points, &idx);
245
246    Ok((points, idx))
247}
248
249fn compute_silhouette(
250    facet: usize,
251    indirect_id: usize,
252    point: usize,
253    out_facets_and_idx: &mut Vec<(usize, usize)>,
254    points: &[Vector3],
255    removed_facets: &mut Vec<usize>,
256    triangles: &mut [TriangleFacet],
257) {
258    if triangles[facet].valid {
259        if !triangles[facet].order_independent_can_be_seen_by_point(point, points) {
260            // println!("triangles: {}, valid: true, keep: true", facet);
261            // println!(
262            //     "Taking edge: [{}, {}]",
263            //     triangles[facet].second_point_from_edge(indirect_id),
264            //     triangles[facet].first_point_from_edge(indirect_id)
265            // );
266            out_facets_and_idx.push((facet, indirect_id));
267        } else {
268            triangles[facet].valid = false; // The facet must be removed from the convex hull.
269            removed_facets.push(facet);
270            // println!("triangles: {}, valid: true, keep: false", facet);
271
272            compute_silhouette(
273                triangles[facet].adj[(indirect_id + 1) % 3],
274                triangles[facet].indirect_adj_id[(indirect_id + 1) % 3],
275                point,
276                out_facets_and_idx,
277                points,
278                removed_facets,
279                triangles,
280            );
281
282            compute_silhouette(
283                triangles[facet].adj[(indirect_id + 2) % 3],
284                triangles[facet].indirect_adj_id[(indirect_id + 2) % 3],
285                point,
286                out_facets_and_idx,
287                points,
288                removed_facets,
289                triangles,
290            );
291        }
292    } else {
293        // println!("triangles: {}, valid: false, keep: false", facet);
294    }
295}
296
297fn fix_silhouette_topology(
298    points: &[Vector3],
299    out_facets_and_idx: &mut Vec<(usize, usize)>,
300    removed_facets: &mut Vec<usize>,
301    triangles: &mut [TriangleFacet],
302) -> Result<(), ConvexHullError> {
303    // TODO: don't allocate this everytime.
304    let mut workspace = vec![0; points.len()];
305    let mut needs_fixing = false;
306
307    // NOTE: we wore with the second_point_from_edge instead
308    // of the first one, because when we traverse the silhouette
309    // we see the second edge point before the first.
310    for (facet, adj_id) in &*out_facets_and_idx {
311        let p = triangles[*facet].second_point_from_edge(*adj_id);
312        workspace[p] += 1;
313
314        if workspace[p] > 1 {
315            needs_fixing = true;
316        }
317    }
318
319    // We detected a topological problem, i.e., we have
320    // multiple loops.
321    if needs_fixing {
322        // First, we need to know which loop is the one we
323        // need to keep.
324        let mut loop_start = 0;
325        for (facet, adj_id) in &*out_facets_and_idx {
326            let p1 = points[triangles[*facet].second_point_from_edge(*adj_id)];
327            let p2 = points[triangles[*facet].first_point_from_edge(*adj_id)];
328            let supp = indexed_support_point_nth(
329                p2 - p1,
330                points,
331                out_facets_and_idx
332                    .iter()
333                    .map(|(f, ai)| triangles[*f].second_point_from_edge(*ai)),
334            )
335            .ok_or(ConvexHullError::MissingSupportPoint)?;
336            let selected = &out_facets_and_idx[supp];
337            if workspace[triangles[selected.0].second_point_from_edge(selected.1)] == 1 {
338                // This is a valid point to start with.
339                loop_start = supp;
340                break;
341            }
342        }
343
344        let mut removing = None;
345        let old_facets_and_idx = core::mem::take(out_facets_and_idx);
346
347        for i in 0..old_facets_and_idx.len() {
348            let facet_id = (loop_start + i) % old_facets_and_idx.len();
349            let (facet, adj_id) = old_facets_and_idx[facet_id];
350
351            match removing {
352                Some(p) => {
353                    let p1 = triangles[facet].second_point_from_edge(adj_id);
354                    if p == p1 {
355                        removing = None;
356                    }
357                }
358                _ => {
359                    let p1 = triangles[facet].second_point_from_edge(adj_id);
360                    if workspace[p1] > 1 {
361                        removing = Some(p1);
362                    }
363                }
364            }
365
366            if removing.is_some() {
367                if triangles[facet].valid {
368                    triangles[facet].valid = false;
369                    removed_facets.push(facet);
370                }
371            } else {
372                out_facets_and_idx.push((facet, adj_id));
373            }
374
375            // // Debug
376            // {
377            //     let p1 = triangles[facet].second_point_from_edge(adj_id);
378            //     let p2 = triangles[facet].first_point_from_edge(adj_id);
379            //     if removing.is_some() {
380            //         print!("/{}, {}\\ ", p1, p2);
381            //     } else {
382            //         print!("[{}, {}] ", p1, p2);
383            //     }
384            // }
385        }
386
387        // println!();
388    }
389
390    Ok(())
391}
392
393fn attach_and_push_facets(
394    silhouette_loop_facets_and_idx: &[(usize, usize)],
395    point: usize,
396    points: &[Vector3],
397    triangles: &mut Vec<TriangleFacet>,
398    removed_facets: &[usize],
399    undecidable: &mut Vec<usize>,
400) {
401    // The silhouette is built to be in CCW order.
402    let mut new_facets = Vec::with_capacity(silhouette_loop_facets_and_idx.len());
403
404    // Create new facets.
405    let mut adj_facet: usize;
406    let mut indirect_id: usize;
407
408    for silhouette_loop_facets_and_id in silhouette_loop_facets_and_idx {
409        adj_facet = silhouette_loop_facets_and_id.0;
410        indirect_id = silhouette_loop_facets_and_id.1;
411
412        // print!(
413        //     "[{}, {}] ",
414        //     triangles[adj_facet].second_point_from_edge(indirect_id),
415        //     triangles[adj_facet].first_point_from_edge(indirect_id)
416        // );
417
418        let facet = TriangleFacet::new(
419            point,
420            triangles[adj_facet].second_point_from_edge(indirect_id),
421            triangles[adj_facet].first_point_from_edge(indirect_id),
422            points,
423        );
424        new_facets.push(facet);
425    }
426    // println!();
427
428    // Link the facets together.
429    for i in 0..silhouette_loop_facets_and_idx.len() {
430        let prev_facet = if i == 0 {
431            triangles.len() + silhouette_loop_facets_and_idx.len() - 1
432        } else {
433            triangles.len() + i - 1
434        };
435
436        let (middle_facet, middle_id) = silhouette_loop_facets_and_idx[i];
437        let next_facet = triangles.len() + (i + 1) % silhouette_loop_facets_and_idx.len();
438
439        new_facets[i].set_facets_adjascency(prev_facet, middle_facet, next_facet, 2, middle_id, 0);
440        assert!(!triangles[triangles[middle_facet].adj[middle_id]].valid); // Check that we are not overwriting a valid link.
441        triangles[middle_facet].adj[middle_id] = triangles.len() + i; // The future id of curr_facet.
442        triangles[middle_facet].indirect_adj_id[middle_id] = 1;
443    }
444
445    // Assign to each facets some of the points which can see it.
446    // TODO: refactor this with the others.
447    for curr_facet in removed_facets.iter() {
448        for visible_point in triangles[*curr_facet].visible_points.iter() {
449            if points[*visible_point] == points[point] {
450                continue;
451            }
452
453            let mut furthest = usize::MAX;
454            let mut furthest_dist = 0.0;
455
456            for (i, curr_facet) in new_facets.iter_mut().enumerate() {
457                if !curr_facet.affinely_dependent {
458                    let distance = curr_facet.distance_to_point(*visible_point, points);
459
460                    if distance > furthest_dist {
461                        furthest = i;
462                        furthest_dist = distance;
463                    }
464                }
465            }
466
467            if furthest != usize::MAX && new_facets[furthest].can_see_point(*visible_point, points)
468            {
469                new_facets[furthest].add_visible_point(*visible_point, points);
470            }
471
472            // If none of the facet can be seen from the point, it is implicitly
473            // deleted because it won't be referenced by any facet.
474        }
475    }
476
477    // Try to assign collinear points to one of the new facets.
478    let mut i = 0;
479
480    while i != undecidable.len() {
481        let mut furthest = usize::MAX;
482        let mut furthest_dist = 0.0;
483        let undecidable_point = undecidable[i];
484
485        for (j, curr_facet) in new_facets.iter_mut().enumerate() {
486            if curr_facet.can_see_point(undecidable_point, points) {
487                let distance = curr_facet.distance_to_point(undecidable_point, points);
488
489                if distance > furthest_dist {
490                    furthest = j;
491                    furthest_dist = distance;
492                }
493            }
494        }
495
496        if furthest != usize::MAX {
497            new_facets[furthest].add_visible_point(undecidable_point, points);
498            let _ = undecidable.swap_remove(i);
499        } else {
500            i += 1;
501        }
502    }
503
504    // Push facets.
505    // TODO: can we avoid the tmp vector `new_facets` ?
506    triangles.append(&mut new_facets);
507}
508
509#[cfg(test)]
510mod test {
511    #[cfg(feature = "dim2")]
512    use crate::math::Vector2;
513    use crate::transformation;
514
515    #[cfg(feature = "dim2")]
516    #[test]
517    fn test_simple_convex_hull() {
518        let points = [
519            Vector::new(4.723881f32, 3.597233),
520            Vector::new(3.333363, 3.429991),
521            Vector::new(3.137215, 2.812263),
522        ];
523
524        let chull = transformation::convex_hull(points.as_slice());
525
526        assert!(chull.len() == 3);
527    }
528
529    #[cfg(feature = "dim3")]
530    #[test]
531    fn test_ball_convex_hull() {
532        use crate::shape::Ball;
533
534        // This triggered a failure to an affinely dependent facet.
535        let (points, _) = Ball::new(0.4).to_trimesh(20, 20);
536        let (vertices, _) = transformation::convex_hull(points.as_slice());
537
538        // dummy test, we are just checking that the construction did not fail.
539        assert!(vertices.len() == vertices.len());
540    }
541}