parry3d/transformation/voxelization/voxel_set.rs
1// Rust port, with modifications, of https://github.com/kmammou/v-hacd/blob/master/src/VHACD_Lib/src/vhacdVolume.cpp
2// By Khaled Mamou
3//
4// # License of the original C++ code:
5// > Copyright (c) 2011 Khaled Mamou (kmamou at gmail dot com)
6// > All rights reserved.
7// >
8// >
9// > Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
10// >
11// > 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
12// >
13// > 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
14// >
15// > 3. The names of the contributors may not be used to endorse or promote products derived from this software without specific prior written permission.
16// >
17// > THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
18
19use super::{FillMode, VoxelizedVolume};
20use crate::bounding_volume::Aabb;
21use crate::math::{Matrix, Point, Real, Vector, DIM};
22use crate::transformation::vhacd::CutPlane;
23use alloc::sync::Arc;
24use alloc::{vec, vec::Vec};
25
26#[cfg(feature = "dim2")]
27type ConvexHull = Vec<Point<Real>>;
28#[cfg(feature = "dim3")]
29type ConvexHull = (Vec<Point<Real>>, Vec<[u32; DIM]>);
30
31/// A single voxel in a voxel grid.
32///
33/// A voxel represents a cubic (or square in 2D) cell in a discrete grid. Each voxel is identified
34/// by its integer grid coordinates and contains metadata about its position relative to the
35/// voxelized shape.
36///
37/// # Fields
38///
39/// - `coords`: The grid position `(i, j)` in 2D or `(i, j, k)` in 3D. These are **integer**
40/// coordinates in the voxel grid, not world-space coordinates.
41///
42/// - `is_on_surface`: Whether this voxel intersects the surface boundary of the shape. If `false`,
43/// the voxel is completely inside the shape (only possible when using [`FillMode::FloodFill`]).
44///
45/// # Example
46///
47/// ```
48/// # #[cfg(all(feature = "dim3", feature = "f32"))] {
49/// use parry3d::transformation::voxelization::{FillMode, VoxelSet};
50/// use parry3d::shape::Ball;
51/// ///
52/// let ball = Ball::new(1.0);
53/// let (vertices, indices) = ball.to_trimesh(10, 10);
54///
55/// let voxels = VoxelSet::voxelize(
56/// &vertices,
57/// &indices,
58/// 8,
59/// FillMode::FloodFill { detect_cavities: false },
60/// false,
61/// );
62///
63/// // Examine individual voxels
64/// for voxel in voxels.voxels() {
65/// // Grid coordinates (integer position in the voxel grid)
66/// println!("Grid position: {:?}", voxel.coords);
67///
68/// // World-space position (actual 3D coordinates)
69/// let world_pos = voxels.get_voxel_point(voxel);
70/// println!("World position: {:?}", world_pos);
71///
72/// // Surface vs interior
73/// if voxel.is_on_surface {
74/// println!("This voxel is on the surface boundary");
75/// } else {
76/// println!("This voxel is inside the shape");
77/// }
78/// }
79/// # }
80/// ```
81#[derive(Copy, Clone, Debug)]
82pub struct Voxel {
83 /// The integer coordinates of the voxel as part of the voxel grid.
84 pub coords: Point<u32>,
85 /// Is this voxel on the surface of the volume (i.e. not inside of it)?
86 pub is_on_surface: bool,
87 /// Range of indices (to be looked up into the `VoxelSet` primitive map)
88 /// of the primitives intersected by this voxel.
89 pub(crate) intersections_range: (usize, usize),
90}
91
92impl Default for Voxel {
93 fn default() -> Self {
94 Self {
95 coords: Point::origin(),
96 is_on_surface: false,
97 intersections_range: (0, 0),
98 }
99 }
100}
101
102/// A sparse set of filled voxels resulting from voxelization.
103///
104/// `VoxelSet` is a memory-efficient storage format that only contains voxels marked as "filled"
105/// during the voxelization process. This is much more efficient than storing a dense 3D array
106/// for shapes that are mostly empty or have a hollow interior.
107///
108/// # Structure
109///
110/// Each `VoxelSet` contains:
111/// - A list of filled voxels with their grid coordinates
112/// - The origin point and scale factor for converting grid coordinates to world space
113/// - Optional metadata tracking which primitives (triangles/segments) intersect each voxel
114///
115/// # Grid Coordinates vs World Coordinates
116///
117/// Voxels are stored with integer grid coordinates `(i, j, k)`. To convert to world-space
118/// coordinates, use:
119///
120/// ```text
121/// world_position = origin + (i, j, k) * scale
122/// ```
123///
124/// The [`get_voxel_point()`](VoxelSet::get_voxel_point) method does this conversion for you.
125///
126/// # Memory Layout
127///
128/// Unlike [`VoxelizedVolume`] which stores a dense 3D array, `VoxelSet` uses sparse storage:
129/// - Only filled voxels are stored (typically much fewer than total grid cells)
130/// - Memory usage is `O(filled_voxels)` instead of `O(resolution^3)`
131/// - Ideal for shapes with low surface-to-volume ratio
132///
133/// # Example: Basic Usage
134///
135/// ```
136/// # #[cfg(all(feature = "dim3", feature = "f32"))] {
137/// use parry3d::transformation::voxelization::{FillMode, VoxelSet};
138/// use parry3d::shape::Cuboid;
139/// use nalgebra::Vector3;
140///
141/// // Create and voxelize a shape
142/// let cuboid = Cuboid::new(Vector3::new(1.0, 1.0, 1.0));
143/// let (vertices, indices) = cuboid.to_trimesh();
144///
145/// let voxels = VoxelSet::voxelize(
146/// &vertices,
147/// &indices,
148/// 10, // resolution
149/// FillMode::SurfaceOnly, // hollow surface only
150/// false, // no primitive mapping
151/// );
152///
153/// // Query the voxel set
154/// println!("Number of voxels: {}", voxels.len());
155/// println!("Voxel size: {}", voxels.scale);
156/// println!("Total volume: {}", voxels.compute_volume());
157///
158/// // Access voxels
159/// for voxel in voxels.voxels() {
160/// let world_pos = voxels.get_voxel_point(voxel);
161/// println!("Voxel at {:?}", world_pos);
162/// }
163/// # }
164/// ```
165///
166/// # Example: Volume Computation
167///
168/// ```
169/// # #[cfg(all(feature = "dim3", feature = "f32"))] {
170/// use parry3d::transformation::voxelization::{FillMode, VoxelSet};
171/// use parry3d::shape::Ball;
172/// ///
173/// let ball = Ball::new(1.0);
174/// let (vertices, indices) = ball.to_trimesh(20, 20);
175///
176/// // Voxelize with interior filling
177/// let voxels = VoxelSet::voxelize(
178/// &vertices,
179/// &indices,
180/// 20,
181/// FillMode::FloodFill { detect_cavities: false },
182/// false,
183/// );
184///
185/// // Compute approximate volume
186/// let voxel_volume = voxels.compute_volume();
187/// let expected_volume = 4.0 / 3.0 * std::f32::consts::PI;
188/// println!("Voxel volume: {:.3}, Expected: {:.3}", voxel_volume, expected_volume);
189/// # }
190/// ```
191pub struct VoxelSet {
192 /// The 3D origin of this voxel-set.
193 pub origin: Point<Real>,
194 /// The scale factor between the voxel integer coordinates and their
195 /// actual float world-space coordinates.
196 pub scale: Real,
197 pub(crate) min_bb_voxels: Point<u32>,
198 pub(crate) max_bb_voxels: Point<u32>,
199 pub(crate) voxels: Vec<Voxel>,
200 pub(crate) intersections: Arc<Vec<u32>>,
201 pub(crate) primitive_classes: Arc<Vec<u32>>,
202}
203
204impl Default for VoxelSet {
205 fn default() -> Self {
206 Self::new()
207 }
208}
209
210impl VoxelSet {
211 /// Creates a new empty set of voxels.
212 pub fn new() -> Self {
213 Self {
214 origin: Point::origin(),
215 min_bb_voxels: Point::origin(),
216 max_bb_voxels: Vector::repeat(1).into(),
217 scale: 1.0,
218 voxels: Vec::new(),
219 intersections: Arc::new(Vec::new()),
220 primitive_classes: Arc::new(Vec::new()),
221 }
222 }
223
224 /// The volume of a single voxel of this voxel set.
225 #[cfg(feature = "dim2")]
226 pub fn voxel_volume(&self) -> Real {
227 self.scale * self.scale
228 }
229
230 /// The volume of a single voxel of this voxel set.
231 #[cfg(feature = "dim3")]
232 pub fn voxel_volume(&self) -> Real {
233 self.scale * self.scale * self.scale
234 }
235
236 /// Voxelizes a shape by specifying the physical size of each voxel.
237 ///
238 /// This creates a voxelized representation of a shape defined by its boundary:
239 /// - In 3D: A triangle mesh (vertices and triangle indices)
240 /// - In 2D: A polyline (vertices and segment indices)
241 ///
242 /// The resolution is automatically determined based on the shape's bounding box
243 /// and the requested voxel size.
244 ///
245 /// # Parameters
246 ///
247 /// * `points` - Vertex buffer defining the boundary of the shape. These are the
248 /// vertices of the triangle mesh (3D) or polyline (2D).
249 ///
250 /// * `indices` - Index buffer defining the boundary primitives:
251 /// - 3D: Each element is `[v0, v1, v2]` defining a triangle
252 /// - 2D: Each element is `[v0, v1]` defining a line segment
253 ///
254 /// * `voxel_size` - The physical size (edge length) of each cubic voxel. Smaller
255 /// values give higher resolution but use more memory.
256 ///
257 /// * `fill_mode` - Controls which voxels are marked as filled:
258 /// - [`FillMode::SurfaceOnly`]: Only voxels intersecting the boundary
259 /// - [`FillMode::FloodFill`]: Surface voxels plus interior voxels
260 ///
261 /// * `keep_voxel_to_primitives_map` - If `true`, stores which primitives (triangles
262 /// or segments) intersect each voxel. Required for [`compute_exact_convex_hull()`]
263 /// and [`compute_primitive_intersections()`], but uses additional memory.
264 ///
265 /// # Returns
266 ///
267 /// A sparse `VoxelSet` containing only the filled voxels.
268 ///
269 /// # Example
270 ///
271 /// ```
272 /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
273 /// use parry3d::transformation::voxelization::{FillMode, VoxelSet};
274 /// use parry3d::shape::Ball;
275 ///
276 /// let ball = Ball::new(2.0); // radius = 2.0
277 /// let (vertices, indices) = ball.to_trimesh(20, 20);
278 ///
279 /// // Create voxels with 0.1 unit size
280 /// let voxels = VoxelSet::with_voxel_size(
281 /// &vertices,
282 /// &indices,
283 /// 0.1, // each voxel is 0.1 x 0.1 x 0.1
284 /// FillMode::SurfaceOnly,
285 /// false,
286 /// );
287 ///
288 /// println!("Created {} voxels", voxels.len());
289 /// println!("Each voxel has volume {}", voxels.voxel_volume());
290 /// # }
291 /// ```
292 ///
293 /// [`compute_exact_convex_hull()`]: VoxelSet::compute_exact_convex_hull
294 /// [`compute_primitive_intersections()`]: VoxelSet::compute_primitive_intersections
295 pub fn with_voxel_size(
296 points: &[Point<Real>],
297 indices: &[[u32; DIM]],
298 voxel_size: Real,
299 fill_mode: FillMode,
300 keep_voxel_to_primitives_map: bool,
301 ) -> Self {
302 VoxelizedVolume::with_voxel_size(
303 points,
304 indices,
305 voxel_size,
306 fill_mode,
307 keep_voxel_to_primitives_map,
308 )
309 .into()
310 }
311
312 /// Voxelizes a shape by specifying the grid resolution along the longest axis.
313 ///
314 /// This creates a voxelized representation of a shape defined by its boundary:
315 /// - In 3D: A triangle mesh (vertices and triangle indices)
316 /// - In 2D: A polyline (vertices and segment indices)
317 ///
318 /// The voxel size is automatically computed to fit the specified number of subdivisions
319 /// along the shape's longest axis, while maintaining cubic (or square in 2D) voxels.
320 /// Other axes will have proportionally determined resolutions.
321 ///
322 /// # Parameters
323 ///
324 /// * `points` - Vertex buffer defining the boundary of the shape. These are the
325 /// vertices of the triangle mesh (3D) or polyline (2D).
326 ///
327 /// * `indices` - Index buffer defining the boundary primitives:
328 /// - 3D: Each element is `[v0, v1, v2]` defining a triangle
329 /// - 2D: Each element is `[v0, v1]` defining a line segment
330 ///
331 /// * `resolution` - Number of voxel subdivisions along the longest axis of the shape's
332 /// bounding box. Higher values give more detail but use more memory.
333 /// For example, `resolution = 10` creates approximately 10 voxels along the longest dimension.
334 ///
335 /// * `fill_mode` - Controls which voxels are marked as filled:
336 /// - [`FillMode::SurfaceOnly`]: Only voxels intersecting the boundary
337 /// - [`FillMode::FloodFill`]: Surface voxels plus interior voxels
338 ///
339 /// * `keep_voxel_to_primitives_map` - If `true`, stores which primitives (triangles
340 /// or segments) intersect each voxel. Required for [`compute_exact_convex_hull()`]
341 /// and [`compute_primitive_intersections()`], but uses additional memory.
342 ///
343 /// # Returns
344 ///
345 /// A sparse `VoxelSet` containing only the filled voxels.
346 ///
347 /// # Example
348 ///
349 /// ```
350 /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
351 /// use parry3d::transformation::voxelization::{FillMode, VoxelSet};
352 /// use parry3d::shape::Cuboid;
353 /// use nalgebra::Vector3;
354 ///
355 /// // Create a cuboid: 2 units wide (x), 1 unit tall (y), 0.5 units deep (z)
356 /// let cuboid = Cuboid::new(Vector3::new(1.0, 0.5, 0.25));
357 /// let (vertices, indices) = cuboid.to_trimesh();
358 ///
359 /// // Voxelize with 20 subdivisions along the longest axis (x = 2.0)
360 /// // Other axes will be proportionally subdivided to maintain cubic voxels
361 /// let voxels = VoxelSet::voxelize(
362 /// &vertices,
363 /// &indices,
364 /// 20, // 20 voxels along x-axis
365 /// FillMode::FloodFill {
366 /// detect_cavities: false,
367 /// },
368 /// false,
369 /// );
370 ///
371 /// println!("Created {} voxels", voxels.len());
372 /// println!("Voxel scale: {}", voxels.scale); // automatically computed
373 /// println!("Total volume: {}", voxels.compute_volume());
374 /// # }
375 /// ```
376 ///
377 /// # Choosing Resolution
378 ///
379 /// - **Low (5-10)**: Fast, coarse approximation, good for rough collision proxies
380 /// - **Medium (10-30)**: Balanced detail and performance, suitable for most use cases
381 /// - **High (50+)**: Fine detail, high memory usage, used for precise volume computation
382 ///
383 /// [`compute_exact_convex_hull()`]: VoxelSet::compute_exact_convex_hull
384 /// [`compute_primitive_intersections()`]: VoxelSet::compute_primitive_intersections
385 pub fn voxelize(
386 points: &[Point<Real>],
387 indices: &[[u32; DIM]],
388 resolution: u32,
389 fill_mode: FillMode,
390 keep_voxel_to_primitives_map: bool,
391 ) -> Self {
392 VoxelizedVolume::voxelize(
393 points,
394 indices,
395 resolution,
396 fill_mode,
397 keep_voxel_to_primitives_map,
398 )
399 .into()
400 }
401
402 /// The minimal coordinates of the integer bounding-box of the voxels in this set.
403 pub fn min_bb_voxels(&self) -> Point<u32> {
404 self.min_bb_voxels
405 }
406
407 /// The maximal coordinates of the integer bounding-box of the voxels in this set.
408 pub fn max_bb_voxels(&self) -> Point<u32> {
409 self.max_bb_voxels
410 }
411
412 /// Computes the total volume occupied by all voxels in this set.
413 ///
414 /// This calculates the approximate volume by multiplying the number of filled voxels
415 /// by the volume of each individual voxel. The result is an approximation of the
416 /// volume of the original shape.
417 ///
418 /// # Accuracy
419 ///
420 /// The accuracy depends on the voxelization resolution:
421 /// - Higher resolution (smaller voxels) → more accurate volume
422 /// - Lower resolution (larger voxels) → faster but less accurate
423 ///
424 /// # Example
425 ///
426 /// ```
427 /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
428 /// use parry3d::transformation::voxelization::{FillMode, VoxelSet};
429 /// use parry3d::shape::Ball;
430 ///
431 /// let ball = Ball::new(1.0);
432 /// let (vertices, indices) = ball.to_trimesh(20, 20);
433 ///
434 /// let voxels = VoxelSet::voxelize(
435 /// &vertices,
436 /// &indices,
437 /// 30, // Higher resolution for better accuracy
438 /// FillMode::FloodFill { detect_cavities: false },
439 /// false,
440 /// );
441 ///
442 /// let voxel_volume = voxels.compute_volume();
443 /// let expected_volume = 4.0 / 3.0 * std::f32::consts::PI * 1.0_f32.powi(3);
444 ///
445 /// println!("Voxel volume: {:.3}", voxel_volume);
446 /// println!("Expected volume: {:.3}", expected_volume);
447 /// println!("Error: {:.1}%", ((voxel_volume - expected_volume).abs() / expected_volume * 100.0));
448 /// # }
449 /// ```
450 pub fn compute_volume(&self) -> Real {
451 self.voxel_volume() * self.voxels.len() as Real
452 }
453
454 /// Converts voxel grid coordinates to world-space coordinates.
455 ///
456 /// Given a voxel, this computes the world-space position of the voxel's center.
457 /// The conversion formula is:
458 ///
459 /// ```text
460 /// world_position = origin + (voxel.coords + 0.5) * scale
461 /// ```
462 ///
463 /// Note that we add 0.5 to get the center of the voxel rather than its corner.
464 ///
465 /// # Example
466 ///
467 /// ```
468 /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
469 /// use parry3d::transformation::voxelization::{FillMode, VoxelSet};
470 /// use parry3d::shape::Cuboid;
471 /// use nalgebra::Vector3;
472 ///
473 /// let cuboid = Cuboid::new(Vector3::new(1.0, 1.0, 1.0));
474 /// let (vertices, indices) = cuboid.to_trimesh();
475 ///
476 /// let voxels = VoxelSet::voxelize(
477 /// &vertices,
478 /// &indices,
479 /// 10,
480 /// FillMode::SurfaceOnly,
481 /// false,
482 /// );
483 ///
484 /// // Convert grid coordinates to world coordinates
485 /// for voxel in voxels.voxels() {
486 /// let grid_coords = voxel.coords;
487 /// let world_coords = voxels.get_voxel_point(voxel);
488 ///
489 /// println!("Grid: {:?} -> World: {:?}", grid_coords, world_coords);
490 /// }
491 /// # }
492 /// ```
493 pub fn get_voxel_point(&self, voxel: &Voxel) -> Point<Real> {
494 self.get_point(na::convert(voxel.coords))
495 }
496
497 pub(crate) fn get_point(&self, voxel: Point<Real>) -> Point<Real> {
498 self.origin + voxel.coords * self.scale
499 }
500
501 /// Does this voxel not contain any element?
502 pub fn is_empty(&self) -> bool {
503 self.len() == 0
504 }
505
506 /// The number of voxels in this set.
507 pub fn len(&self) -> usize {
508 self.voxels.len()
509 }
510
511 /// The set of voxels.
512 pub fn voxels(&self) -> &[Voxel] {
513 &self.voxels
514 }
515
516 /// Update the bounding box of this voxel set.
517 pub fn compute_bb(&mut self) {
518 let num_voxels = self.voxels.len();
519
520 if num_voxels == 0 {
521 return;
522 }
523
524 self.min_bb_voxels = self.voxels[0].coords;
525 self.max_bb_voxels = self.voxels[0].coords;
526
527 for p in 0..num_voxels {
528 self.min_bb_voxels = self.min_bb_voxels.inf(&self.voxels[p].coords);
529 self.max_bb_voxels = self.max_bb_voxels.sup(&self.voxels[p].coords);
530 }
531 }
532
533 /// Computes a precise convex hull by clipping primitives against voxel boundaries.
534 ///
535 /// This method produces a more accurate convex hull than [`compute_convex_hull()`] by:
536 /// 1. Finding which primitives (triangles/segments) intersect each voxel
537 /// 2. Clipping those primitives to the voxel boundaries
538 /// 3. Computing the convex hull from the clipped geometry
539 ///
540 /// This approach gives much tighter convex hulls, especially at lower resolutions, because
541 /// it uses the actual intersection geometry rather than just voxel centers.
542 ///
543 /// # Requirements
544 ///
545 /// This method requires that the voxel set was created with `keep_voxel_to_primitives_map = true`.
546 /// Otherwise, this method will panic.
547 ///
548 /// # Parameters
549 ///
550 /// * `points` - The same vertex buffer used during voxelization
551 /// * `indices` - The same index buffer used during voxelization
552 ///
553 /// # Returns
554 ///
555 /// In 2D: A vector of points forming the convex hull polygon
556 /// In 3D: A tuple of `(vertices, triangle_indices)` forming the convex hull mesh
557 ///
558 /// # Panics
559 ///
560 /// Panics if this `VoxelSet` was created with `keep_voxel_to_primitives_map = false`.
561 ///
562 /// # Example
563 ///
564 /// ```
565 /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
566 /// use parry3d::transformation::voxelization::{FillMode, VoxelSet};
567 /// use parry3d::shape::Cuboid;
568 /// use nalgebra::Vector3;
569 ///
570 /// let cuboid = Cuboid::new(Vector3::new(1.0, 1.0, 1.0));
571 /// let (vertices, indices) = cuboid.to_trimesh();
572 ///
573 /// // IMPORTANT: Set keep_voxel_to_primitives_map = true
574 /// let voxels = VoxelSet::voxelize(
575 /// &vertices,
576 /// &indices,
577 /// 10,
578 /// FillMode::SurfaceOnly,
579 /// true, // Enable primitive mapping
580 /// );
581 ///
582 /// // Compute exact convex hull using triangle clipping
583 /// let (hull_vertices, hull_indices) = voxels.compute_exact_convex_hull(&vertices, &indices);
584 ///
585 /// println!("Exact convex hull: {} vertices, {} triangles",
586 /// hull_vertices.len(), hull_indices.len());
587 /// # }
588 /// ```
589 ///
590 /// # Comparison with `compute_convex_hull()`
591 ///
592 /// - `compute_exact_convex_hull()`: More accurate, requires primitive mapping, slower
593 /// - `compute_convex_hull()`: Approximate, uses voxel centers, faster
594 ///
595 /// [`compute_convex_hull()`]: VoxelSet::compute_convex_hull
596 #[cfg(feature = "dim2")]
597 pub fn compute_exact_convex_hull(
598 &self,
599 points: &[Point<Real>],
600 indices: &[[u32; DIM]],
601 ) -> Vec<Point<Real>> {
602 self.do_compute_exact_convex_hull(points, indices)
603 }
604
605 /// Computes a precise convex hull by clipping primitives against voxel boundaries.
606 ///
607 /// This method produces a more accurate convex hull than [`compute_convex_hull()`] by:
608 /// 1. Finding which primitives (triangles/segments) intersect each voxel
609 /// 2. Clipping those primitives to the voxel boundaries
610 /// 3. Computing the convex hull from the clipped geometry
611 ///
612 /// This approach gives much tighter convex hulls, especially at lower resolutions, because
613 /// it uses the actual intersection geometry rather than just voxel centers.
614 ///
615 /// # Requirements
616 ///
617 /// This method requires that the voxel set was created with `keep_voxel_to_primitives_map = true`.
618 /// Otherwise, this method will panic.
619 ///
620 /// # Parameters
621 ///
622 /// * `points` - The same vertex buffer used during voxelization
623 /// * `indices` - The same index buffer used during voxelization
624 ///
625 /// # Returns
626 ///
627 /// In 2D: A vector of points forming the convex hull polygon
628 /// In 3D: A tuple of `(vertices, triangle_indices)` forming the convex hull mesh
629 ///
630 /// # Panics
631 ///
632 /// Panics if this `VoxelSet` was created with `keep_voxel_to_primitives_map = false`.
633 ///
634 /// # Example
635 ///
636 /// ```
637 /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
638 /// use parry3d::transformation::voxelization::{FillMode, VoxelSet};
639 /// use parry3d::shape::Cuboid;
640 /// use nalgebra::Vector3;
641 ///
642 /// let cuboid = Cuboid::new(Vector3::new(1.0, 1.0, 1.0));
643 /// let (vertices, indices) = cuboid.to_trimesh();
644 ///
645 /// // IMPORTANT: Set keep_voxel_to_primitives_map = true
646 /// let voxels = VoxelSet::voxelize(
647 /// &vertices,
648 /// &indices,
649 /// 10,
650 /// FillMode::SurfaceOnly,
651 /// true, // Enable primitive mapping
652 /// );
653 ///
654 /// // Compute exact convex hull using triangle clipping
655 /// let (hull_vertices, hull_indices) = voxels.compute_exact_convex_hull(&vertices, &indices);
656 ///
657 /// println!("Exact convex hull: {} vertices, {} triangles",
658 /// hull_vertices.len(), hull_indices.len());
659 /// # }
660 /// ```
661 ///
662 /// # Comparison with `compute_convex_hull()`
663 ///
664 /// - `compute_exact_convex_hull()`: More accurate, requires primitive mapping, slower
665 /// - `compute_convex_hull()`: Approximate, uses voxel centers, faster
666 ///
667 /// [`compute_convex_hull()`]: VoxelSet::compute_convex_hull
668 #[cfg(feature = "dim3")]
669 pub fn compute_exact_convex_hull(
670 &self,
671 points: &[Point<Real>],
672 indices: &[[u32; DIM]],
673 ) -> (Vec<Point<Real>>, Vec<[u32; DIM]>) {
674 self.do_compute_exact_convex_hull(points, indices)
675 }
676
677 fn do_compute_exact_convex_hull(
678 &self,
679 points: &[Point<Real>],
680 indices: &[[u32; DIM]],
681 ) -> ConvexHull {
682 assert!(!self.intersections.is_empty(),
683 "Cannot compute exact convex hull without voxel-to-primitives-map. Consider passing voxel_to_primitives_map = true to the voxelizer.");
684 let mut surface_points = Vec::new();
685 #[cfg(feature = "dim3")]
686 let (mut polygon, mut workspace) = (Vec::new(), Vec::new());
687 let mut pushed_points = vec![false; points.len()];
688
689 // Grab all the points.
690 for voxel in self.voxels.iter().filter(|v| v.is_on_surface) {
691 let intersections =
692 &self.intersections[voxel.intersections_range.0..voxel.intersections_range.1];
693 for prim_id in intersections {
694 let ia = indices[*prim_id as usize][0] as usize;
695 let ib = indices[*prim_id as usize][1] as usize;
696 #[cfg(feature = "dim3")]
697 let ic = indices[*prim_id as usize][2] as usize;
698
699 // If the primitives have been classified by VHACD, we know that:
700 // - A class equal to Some(u32::MAX) means that the primitives intersects multiple
701 // convex parts, so we need to split it.
702 // - A class equal to None means that we did not compute any classes (so we
703 // must assume that each triangle have to be split since it may intersect
704 // multiple parts.
705 // - A class different from `None` and `Some(u32::MAX)` means that the triangle is
706 // included in only one convex part. So instead of cutting it, just push the whole
707 // triangle once.
708 let prim_class = self.primitive_classes.get(*prim_id as usize).copied();
709 if prim_class == Some(u32::MAX) || prim_class.is_none() {
710 let aabb_center =
711 self.origin + voxel.coords.coords.map(|k| k as Real) * self.scale;
712 let aabb =
713 Aabb::from_half_extents(aabb_center, Vector::repeat(self.scale / 2.0));
714
715 #[cfg(feature = "dim2")]
716 if let Some(seg) = aabb.clip_segment(&points[ia], &points[ib]) {
717 surface_points.push(seg.a);
718 surface_points.push(seg.b);
719 }
720
721 #[cfg(feature = "dim3")]
722 {
723 polygon.clear();
724 polygon.extend_from_slice(&[points[ia], points[ib], points[ic]]);
725 aabb.clip_polygon_with_workspace(&mut polygon, &mut workspace);
726 surface_points.append(&mut polygon);
727 }
728 } else {
729 // We know this triangle is only contained by
730 // one voxel set, i.e., `self`. So we don't
731 // need to cut it.
732 //
733 // Because one triangle may intersect multiple voxels contained by
734 // the same convex part, we only push vertices we have not pushed
735 // so far in order to avoid some useless duplicate points (duplicate
736 // points are OK as far as convex hull computation is concerned, but
737 // they imply some redundant computations).
738 let mut push_pt = |i: usize| {
739 if !pushed_points[i] {
740 surface_points.push(points[i]);
741 pushed_points[i] = true;
742 }
743 };
744
745 push_pt(ia);
746 push_pt(ib);
747 #[cfg(feature = "dim3")]
748 push_pt(ic);
749 }
750 }
751
752 if intersections.is_empty() {
753 self.map_voxel_points(voxel, |p| surface_points.push(p));
754 }
755 }
756
757 // Compute the convex-hull.
758 convex_hull(&surface_points)
759 }
760
761 /// Computes the intersections between all the voxels of this voxel set,
762 /// and all the primitives (triangle or segments) it intersected (as per
763 /// the voxel-to-primitives-map computed during voxelization).
764 ///
765 /// Panics if the voxelization was performed without setting the parameter
766 /// `voxel_to_primitives_map = true`.
767 pub fn compute_primitive_intersections(
768 &self,
769 points: &[Point<Real>],
770 indices: &[[u32; DIM]],
771 ) -> Vec<Point<Real>> {
772 assert!(!self.intersections.is_empty(),
773 "Cannot compute primitive intersections voxel-to-primitives-map. Consider passing voxel_to_primitives_map = true to the voxelizer.");
774 let mut surface_points = Vec::new();
775 #[cfg(feature = "dim3")]
776 let (mut polygon, mut workspace) = (Vec::new(), Vec::new());
777
778 // Grab all the points.
779 for voxel in self.voxels.iter().filter(|v| v.is_on_surface) {
780 let intersections =
781 &self.intersections[voxel.intersections_range.0..voxel.intersections_range.1];
782 for prim_id in intersections {
783 let aabb_center = self.origin + voxel.coords.coords.map(|k| k as Real) * self.scale;
784 let aabb = Aabb::from_half_extents(aabb_center, Vector::repeat(self.scale / 2.0));
785
786 let pa = points[indices[*prim_id as usize][0] as usize];
787 let pb = points[indices[*prim_id as usize][1] as usize];
788 #[cfg(feature = "dim3")]
789 let pc = points[indices[*prim_id as usize][2] as usize];
790
791 #[cfg(feature = "dim2")]
792 if let Some(seg) = aabb.clip_segment(&pa, &pb) {
793 surface_points.push(seg.a);
794 surface_points.push(seg.b);
795 }
796
797 #[cfg(feature = "dim3")]
798 {
799 workspace.clear();
800 polygon.clear();
801 polygon.extend_from_slice(&[pa, pb, pc]);
802 aabb.clip_polygon_with_workspace(&mut polygon, &mut workspace);
803
804 if polygon.len() > 2 {
805 for i in 1..polygon.len() - 1 {
806 surface_points.push(polygon[0]);
807 surface_points.push(polygon[i]);
808 surface_points.push(polygon[i + 1]);
809 }
810 }
811 }
812 }
813 }
814
815 surface_points
816 }
817
818 /// Compute the convex-hull of the voxels in this set.
819 ///
820 /// # Parameters
821 /// * `sampling` - The convex-hull computation will ignore `sampling` voxels at
822 /// regular intervals. Useful to save some computation times if an exact result isn't need.
823 /// Use `0` to make sure no voxel is being ignored.
824 #[cfg(feature = "dim2")]
825 pub fn compute_convex_hull(&self, sampling: u32) -> Vec<Point<Real>> {
826 let mut points = Vec::new();
827
828 // Grab all the points.
829 for voxel in self
830 .voxels
831 .iter()
832 .filter(|v| v.is_on_surface)
833 .step_by(sampling as usize)
834 {
835 self.map_voxel_points(voxel, |p| points.push(p));
836 }
837
838 // Compute the convex-hull.
839 convex_hull(&points)
840 }
841
842 /// Compute the convex-hull of the voxels in this set.
843 ///
844 /// # Parameters
845 /// * `sampling` - The convex-hull computation will ignore `sampling` voxels at
846 /// regular intervals. Useful to save some computation times if an exact result isn't need.
847 /// Use `0` to make sure no voxel is being ignored.
848 #[cfg(feature = "dim3")]
849 pub fn compute_convex_hull(&self, sampling: u32) -> (Vec<Point<Real>>, Vec<[u32; DIM]>) {
850 let mut points = Vec::new();
851
852 // Grab all the points.
853 for voxel in self
854 .voxels
855 .iter()
856 .filter(|v| v.is_on_surface)
857 .step_by(sampling as usize)
858 {
859 self.map_voxel_points(voxel, |p| points.push(p));
860 }
861
862 // Compute the convex-hull.
863 convex_hull(&points)
864 }
865
866 /// Gets the vertices of the given voxel.
867 fn map_voxel_points(&self, voxel: &Voxel, mut f: impl FnMut(Point<Real>)) {
868 let ijk = voxel.coords.coords.map(|e| e as Real);
869
870 #[cfg(feature = "dim2")]
871 let shifts = [
872 Vector::new(-0.5, -0.5),
873 Vector::new(0.5, -0.5),
874 Vector::new(0.5, 0.5),
875 Vector::new(-0.5, 0.5),
876 ];
877
878 #[cfg(feature = "dim3")]
879 let shifts = [
880 Vector::new(-0.5, -0.5, -0.5),
881 Vector::new(0.5, -0.5, -0.5),
882 Vector::new(0.5, 0.5, -0.5),
883 Vector::new(-0.5, 0.5, -0.5),
884 Vector::new(-0.5, -0.5, 0.5),
885 Vector::new(0.5, -0.5, 0.5),
886 Vector::new(0.5, 0.5, 0.5),
887 Vector::new(-0.5, 0.5, 0.5),
888 ];
889
890 for shift in &shifts {
891 f(self.origin + (ijk + *shift) * self.scale)
892 }
893 }
894
895 pub(crate) fn intersect(
896 &self,
897 plane: &CutPlane,
898 positive_pts: &mut Vec<Point<Real>>,
899 negative_pts: &mut Vec<Point<Real>>,
900 sampling: u32,
901 ) {
902 let num_voxels = self.voxels.len();
903
904 if num_voxels == 0 {
905 return;
906 }
907
908 let d0 = self.scale;
909 let mut sp = 0;
910 let mut sn = 0;
911
912 for v in 0..num_voxels {
913 let voxel = self.voxels[v];
914 let pt = self.get_voxel_point(&voxel);
915 let d = plane.abc.dot(&pt.coords) + plane.d;
916
917 // if (d >= 0.0 && d <= d0) positive_pts.push(pt);
918 // else if (d < 0.0 && -d <= d0) negative_pts.push(pt);
919
920 if d >= 0.0 {
921 if d <= d0 {
922 self.map_voxel_points(&voxel, |p| positive_pts.push(p));
923 } else {
924 sp += 1;
925
926 if sp == sampling {
927 self.map_voxel_points(&voxel, |p| positive_pts.push(p));
928 sp = 0;
929 }
930 }
931 } else if -d <= d0 {
932 self.map_voxel_points(&voxel, |p| negative_pts.push(p));
933 } else {
934 sn += 1;
935 if sn == sampling {
936 self.map_voxel_points(&voxel, |p| negative_pts.push(p));
937 sn = 0;
938 }
939 }
940 }
941 }
942
943 // Returns (negative_volume, positive_volume)
944 pub(crate) fn compute_clipped_volumes(&self, plane: &CutPlane) -> (Real, Real) {
945 if self.voxels.is_empty() {
946 return (0.0, 0.0);
947 }
948
949 let mut num_positive_voxels = 0;
950
951 for voxel in &self.voxels {
952 let pt = self.get_voxel_point(voxel);
953 let d = plane.abc.dot(&pt.coords) + plane.d;
954 num_positive_voxels += (d >= 0.0) as usize;
955 }
956
957 let num_negative_voxels = self.voxels.len() - num_positive_voxels;
958 let positive_volume = self.voxel_volume() * (num_positive_voxels as Real);
959 let negative_volume = self.voxel_volume() * (num_negative_voxels as Real);
960
961 (negative_volume, positive_volume)
962 }
963
964 // Set `on_surf` such that it contains only the voxel on surface contained by `self`.
965 pub(crate) fn select_on_surface(&self, on_surf: &mut VoxelSet) {
966 on_surf.origin = self.origin;
967 on_surf.voxels.clear();
968 on_surf.scale = self.scale;
969
970 for voxel in &self.voxels {
971 if voxel.is_on_surface {
972 on_surf.voxels.push(*voxel);
973 }
974 }
975 }
976
977 /// Splits this voxel set into two parts, depending on where the voxel center lies wrt. the given plane.
978 pub(crate) fn clip(
979 &self,
980 plane: &CutPlane,
981 positive_part: &mut VoxelSet,
982 negative_part: &mut VoxelSet,
983 ) {
984 let num_voxels = self.voxels.len();
985
986 if num_voxels == 0 {
987 return;
988 }
989
990 negative_part.origin = self.origin;
991 negative_part.voxels.clear();
992 negative_part.voxels.reserve(num_voxels);
993 negative_part.scale = self.scale;
994
995 positive_part.origin = self.origin;
996 positive_part.voxels.clear();
997 positive_part.voxels.reserve(num_voxels);
998 positive_part.scale = self.scale;
999
1000 let d0 = self.scale;
1001
1002 for v in 0..num_voxels {
1003 let mut voxel = self.voxels[v];
1004 let pt = self.get_voxel_point(&voxel);
1005 let d = plane.abc.dot(&pt.coords) + plane.d;
1006
1007 if d >= 0.0 {
1008 if voxel.is_on_surface || d <= d0 {
1009 voxel.is_on_surface = true;
1010 positive_part.voxels.push(voxel);
1011 } else {
1012 positive_part.voxels.push(voxel);
1013 }
1014 } else if voxel.is_on_surface || -d <= d0 {
1015 voxel.is_on_surface = true;
1016 negative_part.voxels.push(voxel);
1017 } else {
1018 negative_part.voxels.push(voxel);
1019 }
1020 }
1021 }
1022
1023 /// Convert `self` into a mesh, including only the voxels on the surface or only the voxel
1024 /// inside of the volume.
1025 #[cfg(feature = "dim3")]
1026 pub fn to_trimesh(
1027 &self,
1028 base_index: u32,
1029 is_on_surface: bool,
1030 ) -> (Vec<Point<Real>>, Vec<[u32; DIM]>) {
1031 let mut vertices = Vec::new();
1032 let mut indices = Vec::new();
1033
1034 for voxel in &self.voxels {
1035 if voxel.is_on_surface == is_on_surface {
1036 self.map_voxel_points(voxel, |p| vertices.push(p));
1037
1038 indices.push([base_index, base_index + 2, base_index + 1]);
1039 indices.push([base_index, base_index + 3, base_index + 2]);
1040 indices.push([base_index + 4, base_index + 5, base_index + 6]);
1041 indices.push([base_index + 4, base_index + 6, base_index + 7]);
1042 indices.push([base_index + 7, base_index + 6, base_index + 2]);
1043 indices.push([base_index + 7, base_index + 2, base_index + 3]);
1044 indices.push([base_index + 4, base_index + 1, base_index + 5]);
1045 indices.push([base_index + 4, base_index, base_index + 1]);
1046 indices.push([base_index + 6, base_index + 5, base_index + 1]);
1047 indices.push([base_index + 6, base_index + 1, base_index + 2]);
1048 indices.push([base_index + 7, base_index, base_index + 4]);
1049 indices.push([base_index + 7, base_index + 3, base_index]);
1050 }
1051 }
1052
1053 (vertices, indices)
1054 }
1055
1056 pub(crate) fn compute_principal_axes(&self) -> Vector<Real> {
1057 let num_voxels = self.voxels.len();
1058 if num_voxels == 0 {
1059 return Vector::zeros();
1060 }
1061
1062 // TODO: find a way to reuse crate::utils::cov?
1063 // The difficulty being that we need to iterate through the set of
1064 // points twice. So passing an iterator to crate::utils::cov
1065 // isn't really possible.
1066 let mut center = Point::origin();
1067 let denom = 1.0 / (num_voxels as Real);
1068
1069 for voxel in &self.voxels {
1070 center += voxel.coords.map(|e| e as Real).coords * denom;
1071 }
1072
1073 let mut cov_mat = Matrix::zeros();
1074 for voxel in &self.voxels {
1075 let xyz = voxel.coords.map(|e| e as Real) - center;
1076 cov_mat.syger(denom, &xyz, &xyz, 1.0);
1077 }
1078
1079 cov_mat.symmetric_eigenvalues()
1080 }
1081
1082 pub(crate) fn compute_axes_aligned_clipping_planes(
1083 &self,
1084 downsampling: u32,
1085 planes: &mut Vec<CutPlane>,
1086 ) {
1087 let min_v = self.min_bb_voxels();
1088 let max_v = self.max_bb_voxels();
1089
1090 for dim in 0..DIM {
1091 let i0 = min_v[dim];
1092 let i1 = max_v[dim];
1093
1094 for i in (i0..=i1).step_by(downsampling as usize) {
1095 let plane = CutPlane {
1096 abc: Vector::ith(dim, 1.0),
1097 axis: dim as u8,
1098 d: -(self.origin[dim] + (i as Real + 0.5) * self.scale),
1099 index: i,
1100 };
1101
1102 planes.push(plane);
1103 }
1104 }
1105 }
1106}
1107
1108#[cfg(feature = "dim2")]
1109fn convex_hull(vertices: &[Point<Real>]) -> Vec<Point<Real>> {
1110 if vertices.len() > 1 {
1111 crate::transformation::convex_hull(vertices)
1112 } else {
1113 Vec::new()
1114 }
1115}
1116
1117#[cfg(feature = "dim3")]
1118fn convex_hull(vertices: &[Point<Real>]) -> (Vec<Point<Real>>, Vec<[u32; DIM]>) {
1119 if vertices.len() > 2 {
1120 crate::transformation::convex_hull(vertices)
1121 } else {
1122 (Vec::new(), Vec::new())
1123 }
1124}