1use oxiphysics_core::math::Vec3;
20
21use crate::convex_decomposition::{ConvexPart, ConvexParts};
22use crate::quickhull::ConvexHull3DVec;
23use crate::voxel_grid::VoxelGrid;
24
25#[derive(Debug, Clone)]
31pub struct VHacdConfig {
32 pub resolution: usize,
37 pub concavity_threshold: f64,
43 pub max_depth: u32,
45 pub min_voxels_per_cluster: usize,
48 pub max_parts: usize,
50}
51
52impl Default for VHacdConfig {
53 fn default() -> Self {
54 Self {
55 resolution: 64,
56 concavity_threshold: 0.05,
57 max_depth: 8,
58 min_voxels_per_cluster: 32,
59 max_parts: 64,
60 }
61 }
62}
63
64#[derive(Debug, thiserror::Error)]
66pub enum VHacdError {
67 #[error("input triangle soup is empty")]
69 EmptyInput,
70}
71
72pub struct VHacdVoxel {
82 pub config: VHacdConfig,
84}
85
86impl VHacdVoxel {
87 pub fn new(config: VHacdConfig) -> Self {
89 Self { config }
90 }
91
92 pub fn decompose(&self, tris: &[[f64; 9]]) -> Result<ConvexParts, VHacdError> {
97 if tris.is_empty() {
98 return Err(VHacdError::EmptyInput);
99 }
100
101 let (bmin, bmax) = bounding_box_of_tris(tris);
105 let extents = [bmax[0] - bmin[0], bmax[1] - bmin[1], bmax[2] - bmin[2]];
106 let max_extent = extents.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
107
108 let step = if max_extent > 0.0 {
109 max_extent / self.config.resolution as f64
110 } else {
111 1.0
112 };
113
114 let dims = [
115 ((extents[0] / step).ceil() as usize + 1).max(1),
116 ((extents[1] / step).ceil() as usize + 1).max(1),
117 ((extents[2] / step).ceil() as usize + 1).max(1),
118 ];
119
120 let mut grid = VoxelGrid::new(bmin, step, dims);
124 for tri in tris {
125 rasterize_triangle_into_grid(tri, &mut grid);
126 }
127 flood_fill_interior(&mut grid);
128
129 let components = grid.connected_components();
133
134 let mut parts: Vec<ConvexPart> = Vec::new();
138 for comp in components {
139 self.split_cluster(&comp, &grid, 0, &mut parts);
140 if parts.len() >= self.config.max_parts {
141 break;
142 }
143 }
144
145 Ok(ConvexParts { parts })
146 }
147
148 fn split_cluster(
153 &self,
154 cluster: &[usize],
155 grid: &VoxelGrid,
156 depth: u32,
157 out: &mut Vec<ConvexPart>,
158 ) {
159 if out.len() >= self.config.max_parts {
160 return;
161 }
162
163 let mut bmin = [f64::INFINITY; 3];
168 let mut bmax = [f64::NEG_INFINITY; 3];
169 for &idx in cluster {
170 let (ix, iy, iz) = grid_coords(idx, grid);
171 let c = grid.voxel_center(ix, iy, iz);
172 for ax in 0..3 {
173 if c[ax] < bmin[ax] {
174 bmin[ax] = c[ax];
175 }
176 if c[ax] > bmax[ax] {
177 bmax[ax] = c[ax];
178 }
179 }
180 }
181
182 let corner_pts: Vec<Vec3> = aabb_corners_as_vec3(&bmin, &bmax);
196
197 let hull_opt = ConvexHull3DVec::build(&corner_pts);
198
199 let Some(hull) = hull_opt else {
200 let centroid = [
202 (bmin[0] + bmax[0]) * 0.5,
203 (bmin[1] + bmax[1]) * 0.5,
204 (bmin[2] + bmax[2]) * 0.5,
205 ];
206 out.push(ConvexPart {
207 vertices: corner_pts.iter().map(|v| [v.x, v.y, v.z]).collect(),
208 concavity: 0.0,
209 indices: Vec::new(),
210 volume: 0.0,
211 centroid,
212 });
213 return;
214 };
215
216 let concavity = {
222 let diag = ((bmax[0] - bmin[0]).powi(2)
223 + (bmax[1] - bmin[1]).powi(2)
224 + (bmax[2] - bmin[2]).powi(2))
225 .sqrt();
226 if diag < 1e-12 {
227 0.0
228 } else {
229 let voxel_vol = cluster.len() as f64 * grid.voxel_volume();
235 let hull_vol = hull.volume().max(1e-30);
236 let fill_ratio = (voxel_vol / hull_vol).min(1.0);
237 1.0 - fill_ratio
238 }
239 };
240
241 let should_split = concavity > self.config.concavity_threshold
242 && depth < self.config.max_depth
243 && cluster.len() >= 2 * self.config.min_voxels_per_cluster;
244
245 if !should_split {
246 out.push(hull_to_convex_part(&hull));
247 return;
248 }
249
250 let extents = [bmax[0] - bmin[0], bmax[1] - bmin[1], bmax[2] - bmin[2]];
255 let split_axis = if extents[0] >= extents[1] && extents[0] >= extents[2] {
256 0
257 } else if extents[1] >= extents[2] {
258 1
259 } else {
260 2
261 };
262 let split_val = (bmin[split_axis] + bmax[split_axis]) * 0.5;
263
264 let mut left: Vec<usize> = Vec::new();
265 let mut right: Vec<usize> = Vec::new();
266 for &idx in cluster {
267 let (ix, iy, iz) = grid_coords(idx, grid);
268 if grid.voxel_center(ix, iy, iz)[split_axis] < split_val {
269 left.push(idx);
270 } else {
271 right.push(idx);
272 }
273 }
274
275 emit_or_recurse(self, &left, grid, depth, out);
277 emit_or_recurse(self, &right, grid, depth, out);
278 }
279}
280
281fn emit_or_recurse(
283 decomp: &VHacdVoxel,
284 cluster: &[usize],
285 grid: &VoxelGrid,
286 depth: u32,
287 out: &mut Vec<ConvexPart>,
288) {
289 if cluster.len() >= decomp.config.min_voxels_per_cluster {
290 decomp.split_cluster(cluster, grid, depth + 1, out);
291 } else if !cluster.is_empty() {
292 let mut bmin = [f64::INFINITY; 3];
294 let mut bmax = [f64::NEG_INFINITY; 3];
295 for &idx in cluster {
296 let (ix, iy, iz) = grid_coords(idx, grid);
297 let c = grid.voxel_center(ix, iy, iz);
298 for ax in 0..3 {
299 if c[ax] < bmin[ax] {
300 bmin[ax] = c[ax];
301 }
302 if c[ax] > bmax[ax] {
303 bmax[ax] = c[ax];
304 }
305 }
306 }
307 let centroid = [
308 (bmin[0] + bmax[0]) * 0.5,
309 (bmin[1] + bmax[1]) * 0.5,
310 (bmin[2] + bmax[2]) * 0.5,
311 ];
312 let corner_verts = aabb_corners_as_vec3(&bmin, &bmax);
313 out.push(ConvexPart {
314 vertices: corner_verts.iter().map(|v| [v.x, v.y, v.z]).collect(),
315 concavity: 0.0,
316 indices: Vec::new(),
317 volume: 0.0,
318 centroid,
319 });
320 }
321}
322
323fn aabb_corners_as_vec3(bmin: &[f64; 3], bmax: &[f64; 3]) -> Vec<Vec3> {
329 let mut pts = Vec::with_capacity(8);
330 for &x in &[bmin[0], bmax[0]] {
331 for &y in &[bmin[1], bmax[1]] {
332 for &z in &[bmin[2], bmax[2]] {
333 pts.push(Vec3::new(x, y, z));
334 }
335 }
336 }
337 pts
338}
339
340#[inline]
342fn grid_coords(idx: usize, grid: &VoxelGrid) -> (usize, usize, usize) {
343 let iz = idx / (grid.dims[0] * grid.dims[1]);
344 let rem = idx % (grid.dims[0] * grid.dims[1]);
345 let iy = rem / grid.dims[0];
346 let ix = rem % grid.dims[0];
347 (ix, iy, iz)
348}
349
350fn bounding_box_of_tris(tris: &[[f64; 9]]) -> ([f64; 3], [f64; 3]) {
352 let mut bmin = [f64::INFINITY; 3];
353 let mut bmax = [f64::NEG_INFINITY; 3];
354 for tri in tris {
355 for v in 0..3 {
356 for ax in 0..3 {
357 let c = tri[v * 3 + ax];
358 if c < bmin[ax] {
359 bmin[ax] = c;
360 }
361 if c > bmax[ax] {
362 bmax[ax] = c;
363 }
364 }
365 }
366 }
367 let pad = 1e-6;
368 for ax in 0..3 {
369 bmin[ax] -= pad;
370 bmax[ax] += pad;
371 }
372 (bmin, bmax)
373}
374
375fn rasterize_triangle_into_grid(tri: &[f64; 9], grid: &mut VoxelGrid) {
378 let v0 = [tri[0], tri[1], tri[2]];
379 let v1 = [tri[3], tri[4], tri[5]];
380 let v2 = [tri[6], tri[7], tri[8]];
381
382 let tri_min = [
384 v0[0].min(v1[0]).min(v2[0]),
385 v0[1].min(v1[1]).min(v2[1]),
386 v0[2].min(v1[2]).min(v2[2]),
387 ];
388 let tri_max = [
389 v0[0].max(v1[0]).max(v2[0]),
390 v0[1].max(v1[1]).max(v2[1]),
391 v0[2].max(v1[2]).max(v2[2]),
392 ];
393
394 let ix_min = ((tri_min[0] - grid.origin[0]) / grid.step).floor().max(0.0) as usize;
395 let iy_min = ((tri_min[1] - grid.origin[1]) / grid.step).floor().max(0.0) as usize;
396 let iz_min = ((tri_min[2] - grid.origin[2]) / grid.step).floor().max(0.0) as usize;
397 let ix_max = (((tri_max[0] - grid.origin[0]) / grid.step).ceil() as usize)
398 .min(grid.dims[0].saturating_sub(1));
399 let iy_max = (((tri_max[1] - grid.origin[1]) / grid.step).ceil() as usize)
400 .min(grid.dims[1].saturating_sub(1));
401 let iz_max = (((tri_max[2] - grid.origin[2]) / grid.step).ceil() as usize)
402 .min(grid.dims[2].saturating_sub(1));
403
404 for iz in iz_min..=iz_max {
405 for iy in iy_min..=iy_max {
406 for ix in ix_min..=ix_max {
407 let vox_min = [
408 grid.origin[0] + ix as f64 * grid.step,
409 grid.origin[1] + iy as f64 * grid.step,
410 grid.origin[2] + iz as f64 * grid.step,
411 ];
412 let vox_max = [
413 vox_min[0] + grid.step,
414 vox_min[1] + grid.step,
415 vox_min[2] + grid.step,
416 ];
417 if aabb_triangle_intersect(&vox_min, &vox_max, &v0, &v1, &v2) {
418 grid.mark(ix, iy, iz);
419 }
420 }
421 }
422 }
423}
424
425fn flood_fill_interior(grid: &mut VoxelGrid) {
433 use bitvec::prelude::*;
434 use std::collections::VecDeque;
435
436 let [nx, ny, nz] = grid.dims;
437 let n = nx * ny * nz;
438 let mut exterior: BitVec = bitvec![0; n];
440 let mut queue: VecDeque<usize> = VecDeque::new();
441
442 let seed_if_empty =
444 |idx: usize, occupied: &BitVec, exterior: &mut BitVec, queue: &mut VecDeque<usize>| {
445 if !occupied[idx] && !exterior[idx] {
446 exterior.set(idx, true);
447 queue.push_back(idx);
448 }
449 };
450
451 for iz in [0usize, nz.saturating_sub(1)] {
453 for iy in 0..ny {
454 for ix in 0..nx {
455 let idx = grid.index(ix, iy, iz);
456 seed_if_empty(idx, &grid.occupied, &mut exterior, &mut queue);
457 }
458 }
459 }
460 for iy in [0usize, ny.saturating_sub(1)] {
462 for iz in 0..nz {
463 for ix in 0..nx {
464 let idx = grid.index(ix, iy, iz);
465 seed_if_empty(idx, &grid.occupied, &mut exterior, &mut queue);
466 }
467 }
468 }
469 for ix in [0usize, nx.saturating_sub(1)] {
471 for iz in 0..nz {
472 for iy in 0..ny {
473 let idx = grid.index(ix, iy, iz);
474 seed_if_empty(idx, &grid.occupied, &mut exterior, &mut queue);
475 }
476 }
477 }
478
479 let neighbors: [(i64, i64, i64); 6] = [
481 (1, 0, 0),
482 (-1, 0, 0),
483 (0, 1, 0),
484 (0, -1, 0),
485 (0, 0, 1),
486 (0, 0, -1),
487 ];
488 while let Some(idx) = queue.pop_front() {
489 let iz = idx / (nx * ny);
490 let rem = idx % (nx * ny);
491 let iy = rem / nx;
492 let ix = rem % nx;
493 for (dx, dy, dz) in neighbors {
494 let nxi = ix as i64 + dx;
495 let nyi = iy as i64 + dy;
496 let nzi = iz as i64 + dz;
497 if nxi < 0
498 || nyi < 0
499 || nzi < 0
500 || nxi >= nx as i64
501 || nyi >= ny as i64
502 || nzi >= nz as i64
503 {
504 continue;
505 }
506 let nidx = grid.index(nxi as usize, nyi as usize, nzi as usize);
507 if !grid.occupied[nidx] && !exterior[nidx] {
508 exterior.set(nidx, true);
509 queue.push_back(nidx);
510 }
511 }
512 }
513
514 for i in 0..n {
516 if !grid.occupied[i] && !exterior[i] {
517 grid.occupied.set(i, true);
518 }
519 }
520}
521
522fn aabb_triangle_intersect(
525 aabb_min: &[f64; 3],
526 aabb_max: &[f64; 3],
527 v0: &[f64; 3],
528 v1: &[f64; 3],
529 v2: &[f64; 3],
530) -> bool {
531 let center = [
532 (aabb_min[0] + aabb_max[0]) * 0.5,
533 (aabb_min[1] + aabb_max[1]) * 0.5,
534 (aabb_min[2] + aabb_max[2]) * 0.5,
535 ];
536 let half = [
537 (aabb_max[0] - aabb_min[0]) * 0.5,
538 (aabb_max[1] - aabb_min[1]) * 0.5,
539 (aabb_max[2] - aabb_min[2]) * 0.5,
540 ];
541
542 let tv0 = [v0[0] - center[0], v0[1] - center[1], v0[2] - center[2]];
544 let tv1 = [v1[0] - center[0], v1[1] - center[1], v1[2] - center[2]];
545 let tv2 = [v2[0] - center[0], v2[1] - center[1], v2[2] - center[2]];
546
547 for ax in 0..3 {
549 let p0 = tv0[ax];
550 let p1 = tv1[ax];
551 let p2 = tv2[ax];
552 let mn = p0.min(p1).min(p2);
553 let mx = p0.max(p1).max(p2);
554 if mn > half[ax] || mx < -half[ax] {
555 return false;
556 }
557 }
558
559 let e0 = [tv1[0] - tv0[0], tv1[1] - tv0[1], tv1[2] - tv0[2]];
561 let e1 = [tv2[0] - tv0[0], tv2[1] - tv0[1], tv2[2] - tv0[2]];
562 let e2 = [tv2[0] - tv1[0], tv2[1] - tv1[1], tv2[2] - tv1[2]];
563
564 let normal = cross3(&e0, &e1);
566 if !test_axis_sep(&normal, &half, &tv0, &tv1, &tv2) {
567 return false;
568 }
569
570 let edges = [e0, e1, e2];
572 let aabb_axes = [[1.0_f64, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
573 for e in &edges {
574 for a in &aabb_axes {
575 let ax = cross3(e, a);
576 let len_sq = ax[0] * ax[0] + ax[1] * ax[1] + ax[2] * ax[2];
577 if len_sq < 1e-30 {
578 continue; }
580 if !test_axis_sep(&ax, &half, &tv0, &tv1, &tv2) {
581 return false;
582 }
583 }
584 }
585
586 true
587}
588
589#[inline]
590fn cross3(a: &[f64; 3], b: &[f64; 3]) -> [f64; 3] {
591 [
592 a[1] * b[2] - a[2] * b[1],
593 a[2] * b[0] - a[0] * b[2],
594 a[0] * b[1] - a[1] * b[0],
595 ]
596}
597
598#[inline]
599fn dot3(a: &[f64; 3], b: &[f64; 3]) -> f64 {
600 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
601}
602
603#[inline]
606fn test_axis_sep(
607 axis: &[f64; 3],
608 half: &[f64; 3],
609 tv0: &[f64; 3],
610 tv1: &[f64; 3],
611 tv2: &[f64; 3],
612) -> bool {
613 let r = half[0] * axis[0].abs() + half[1] * axis[1].abs() + half[2] * axis[2].abs();
614 let p0 = dot3(axis, tv0);
615 let p1 = dot3(axis, tv1);
616 let p2 = dot3(axis, tv2);
617 let mn = p0.min(p1).min(p2);
618 let mx = p0.max(p1).max(p2);
619 !(mn > r || mx < -r)
620}
621
622fn hull_to_convex_part(hull: &ConvexHull3DVec) -> ConvexPart {
628 let vertices: Vec<[f64; 3]> = hull.vertices.iter().map(|v| [v.x, v.y, v.z]).collect();
629 let indices: Vec<[u32; 3]> = hull
630 .faces
631 .iter()
632 .map(|f| [f[0] as u32, f[1] as u32, f[2] as u32])
633 .collect();
634 let volume = hull.volume();
635 let centroid = {
637 let n = hull.vertices.len() as f64;
638 if n > 0.0 {
639 let sx: f64 = hull.vertices.iter().map(|v| v.x).sum();
640 let sy: f64 = hull.vertices.iter().map(|v| v.y).sum();
641 let sz: f64 = hull.vertices.iter().map(|v| v.z).sum();
642 [sx / n, sy / n, sz / n]
643 } else {
644 [0.0; 3]
645 }
646 };
647 ConvexPart {
648 vertices,
649 concavity: 0.0,
650 indices,
651 volume,
652 centroid,
653 }
654}
655
656#[cfg(test)]
661mod tests {
662 use super::*;
663
664 fn box_tris(min: [f64; 3], max: [f64; 3]) -> Vec<[f64; 9]> {
665 let [x0, y0, z0] = min;
666 let [x1, y1, z1] = max;
667 vec![
668 [x0, y0, z0, x0, y1, z0, x0, y1, z1],
670 [x0, y0, z0, x0, y1, z1, x0, y0, z1],
671 [x1, y0, z0, x1, y1, z1, x1, y1, z0],
673 [x1, y0, z0, x1, y0, z1, x1, y1, z1],
674 [x0, y0, z0, x1, y0, z1, x1, y0, z0],
676 [x0, y0, z0, x0, y0, z1, x1, y0, z1],
677 [x0, y1, z0, x1, y1, z0, x1, y1, z1],
679 [x0, y1, z0, x1, y1, z1, x0, y1, z1],
680 [x0, y0, z0, x1, y0, z0, x1, y1, z0],
682 [x0, y0, z0, x1, y1, z0, x0, y1, z0],
683 [x0, y0, z1, x1, y1, z1, x1, y0, z1],
685 [x0, y0, z1, x0, y1, z1, x1, y1, z1],
686 ]
687 }
688
689 #[test]
690 fn test_unit_box_one_part() {
691 let tris = box_tris([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
692 let cfg = VHacdConfig {
693 resolution: 32,
694 ..Default::default()
695 };
696 let decomposer = VHacdVoxel::new(cfg);
697 let result = decomposer
698 .decompose(&tris)
699 .expect("decompose should succeed");
700 assert_eq!(result.parts.len(), 1, "unit box should give 1 part");
701 let vol = result.parts[0].volume;
702 assert!(
703 (vol - 1.0).abs() / 1.0 <= 0.10,
704 "volume within 10% of 1.0, got {vol}"
705 );
706 }
707
708 #[test]
709 fn test_empty_input_error() {
710 let decomposer = VHacdVoxel::new(VHacdConfig::default());
711 assert!(
712 decomposer.decompose(&[]).is_err(),
713 "empty input should return error"
714 );
715 }
716}