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}