From 395f16bb409485458fff82c1b8b0f631bab3ebe9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Crozet?= Date: Mon, 25 Nov 2024 17:13:44 +0100 Subject: [PATCH 1/3] =?UTF-8?q?feat:=E2=80=AFallow=20calculating=20meshes?= =?UTF-8?q?=20connected=20components=20when=20there=20is=20no=20topology?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/parry2d-f64/Cargo.toml | 2 + crates/parry2d/Cargo.toml | 2 + crates/parry3d-f64/Cargo.toml | 2 + crates/parry3d/Cargo.toml | 2 + src/shape/trimesh.rs | 166 ++++++++++++++++++++++++++++------ 5 files changed, 144 insertions(+), 30 deletions(-) diff --git a/crates/parry2d-f64/Cargo.toml b/crates/parry2d-f64/Cargo.toml index 965b9553..3020f023 100644 --- a/crates/parry2d-f64/Cargo.toml +++ b/crates/parry2d-f64/Cargo.toml @@ -30,6 +30,7 @@ std = [ "arrayvec/std", "spade", "thiserror", + "ena" ] dim2 = [] f64 = [] @@ -84,6 +85,7 @@ bytemuck = { version = "1", features = ["derive"], optional = true } log = "0.4" ordered-float = { version = "4", default-features = false } thiserror = { version = "1", optional = true } +ena = { version = "0.14.3", optional = true } [dev-dependencies] simba = { version = "0.9", default-features = false } diff --git a/crates/parry2d/Cargo.toml b/crates/parry2d/Cargo.toml index e98dd6b1..5328910a 100644 --- a/crates/parry2d/Cargo.toml +++ b/crates/parry2d/Cargo.toml @@ -30,6 +30,7 @@ std = [ "arrayvec/std", "spade", "thiserror", + "ena" ] dim2 = [] f32 = [] @@ -84,6 +85,7 @@ bytemuck = { version = "1", features = ["derive"], optional = true } ordered-float = { version = "4", default-features = false } log = "0.4" thiserror = { version = "1", optional = true } +ena = { version = "0.14.3", optional = true } [dev-dependencies] simba = { version = "0.9", default-features = false } diff --git a/crates/parry3d-f64/Cargo.toml b/crates/parry3d-f64/Cargo.toml index 4ad6c3a3..f7fada37 100644 --- a/crates/parry3d-f64/Cargo.toml +++ b/crates/parry3d-f64/Cargo.toml @@ -30,6 +30,7 @@ std = [ "arrayvec/std", "spade", "thiserror", + "ena" ] dim3 = [] f64 = [] @@ -80,6 +81,7 @@ rayon = { version = "1", optional = true } bytemuck = { version = "1", features = ["derive"], optional = true } rstar = "0.12.0" obj = { version = "0.10.2", optional = true } +ena = { version = "0.14.3", optional = true } log = "0.4" ordered-float = { version = "4", default-features = false } diff --git a/crates/parry3d/Cargo.toml b/crates/parry3d/Cargo.toml index 9ad8d9c0..030fdee1 100644 --- a/crates/parry3d/Cargo.toml +++ b/crates/parry3d/Cargo.toml @@ -30,6 +30,7 @@ std = [ "arrayvec/std", "spade", "thiserror", + "ena" ] dim3 = [] f32 = [] @@ -84,6 +85,7 @@ ordered-float = { version = "4", default-features = false } thiserror = { version = "1", optional = true } rstar = "0.12.0" obj = { version = "0.10.2", optional = true } +ena = { version = "0.14.3", optional = true } [dev-dependencies] oorandom = "11" diff --git a/src/shape/trimesh.rs b/src/shape/trimesh.rs index 32569a28..a5028b39 100644 --- a/src/shape/trimesh.rs +++ b/src/shape/trimesh.rs @@ -2,9 +2,8 @@ use crate::bounding_volume::Aabb; use crate::math::{Isometry, Point, Real, Vector}; use crate::partitioning::Qbvh; use crate::shape::{FeatureId, Shape, Triangle, TrianglePseudoNormals, TypedSimdCompositeShape}; -use std::fmt; - use crate::utils::HashablePartialEq; +use std::fmt; #[cfg(feature = "dim3")] use {crate::shape::Cuboid, crate::utils::SortedPair, na::Unit}; @@ -183,11 +182,7 @@ bitflags::bitflags! { impl TriMeshFlags: u16 { /// If set, the half-edge topology of the trimesh will be computed if possible. const HALF_EDGE_TOPOLOGY = 1; - /// If set, the half-edge topology and connected components of the trimesh will be computed if possible. - /// - /// Because of the way it is currently implemented, connected components can only be computed on - /// a mesh where the half-edge topology computation succeeds. It will no longer be the case in the - /// future once we decouple the computations. + /// If set, the connected components of the trimesh will be computed. const CONNECTED_COMPONENTS = 1 << 1; /// If set, any triangle that results in a failing half-hedge topology computation will be deleted. const DELETE_BAD_TOPOLOGY_TRIANGLES = 1 << 2; @@ -321,14 +316,14 @@ impl TriMesh { } if difference.intersects( - TriMeshFlags::HALF_EDGE_TOPOLOGY - | TriMeshFlags::CONNECTED_COMPONENTS - | TriMeshFlags::DELETE_BAD_TOPOLOGY_TRIANGLES, + TriMeshFlags::HALF_EDGE_TOPOLOGY | TriMeshFlags::DELETE_BAD_TOPOLOGY_TRIANGLES, ) { - result = self.compute_topology( - flags.contains(TriMeshFlags::CONNECTED_COMPONENTS), - flags.contains(TriMeshFlags::DELETE_BAD_TOPOLOGY_TRIANGLES), - ); + result = + self.compute_topology(flags.contains(TriMeshFlags::DELETE_BAD_TOPOLOGY_TRIANGLES)); + } + + if difference.intersects(TriMeshFlags::CONNECTED_COMPONENTS) { + self.compute_connected_components(); } #[cfg(feature = "dim3")] @@ -472,7 +467,7 @@ impl TriMesh { if self.flags.contains(TriMeshFlags::HALF_EDGE_TOPOLOGY) { // TODO: this could be done more efficiently. - let _ = self.compute_topology(false, false); + let _ = self.compute_topology(false); } } @@ -559,7 +554,7 @@ impl TriMesh { // Vertices and indices changed: the topology no longer valid. #[cfg(feature = "dim3")] if self.topology.is_some() { - let _ = self.compute_topology(self.connected_components.is_some(), false); + let _ = self.compute_topology(false); } } @@ -681,11 +676,7 @@ impl TriMesh { /// # Return /// Returns `true` if the computation succeeded. Returns `false` if this mesh can’t have an half-edge representation /// because at least three faces share the same edge. - fn compute_topology( - &mut self, - compute_connected_components: bool, - delete_bad_triangles: bool, - ) -> Result<(), TopologyError> { + fn compute_topology(&mut self, delete_bad_triangles: bool) -> Result<(), TopologyError> { if delete_bad_triangles { self.delete_bad_topology_triangles(); } @@ -754,18 +745,110 @@ impl TriMesh { self.topology = Some(topology); - if compute_connected_components { - self.compute_connected_components(); - } - Ok(()) } - // NOTE: we can only compute connected components if the topology - // has been computed too. So instead of making this method - // public, the `.compute_topology` method has a boolean to - // compute the connected components too. + // NOTE: this is private because that calculation is controlled by TriMeshFlags::CONNECTED_COMPONENTS + // TODO: we should remove the CONNECTED_COMPONENTS flags and just have this be a free function. fn compute_connected_components(&mut self) { + if self.topology.is_some() { + self.compute_connected_components_from_topology() + } else { + self.compute_connected_components_union_find() + } + } + + fn compute_connected_components_union_find(&mut self) { + use ena::unify::{InPlaceUnificationTable, UnifyKey}; + + #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] + struct IntKey(u32); + + impl UnifyKey for IntKey { + type Value = (); + fn index(&self) -> u32 { + self.0 + } + fn from_index(u: u32) -> IntKey { + IntKey(u) + } + fn tag() -> &'static str { + "IntKey" + } + } + + let mut ufind: InPlaceUnificationTable = InPlaceUnificationTable::new(); + let mut face_colors = vec![u32::MAX; self.indices.len()]; + let mut ranges = vec![0]; + let mut vertex_to_range = vec![u32::MAX; self.vertices.len()]; + let mut grouped_faces = vec![u32::MAX; self.indices.len()]; + let mut vertex_to_key = vec![IntKey(u32::MAX); self.vertices.len()]; + + let mut vertex_key = |id: u32, ufind: &mut InPlaceUnificationTable| { + if vertex_to_key[id as usize].0 == u32::MAX { + let new_key = ufind.new_key(()); + vertex_to_key[id as usize] = new_key; + new_key + } else { + vertex_to_key[id as usize] + } + }; + + for idx in self.indices() { + let keys = idx.map(|i| vertex_key(i, &mut ufind)); + ufind.union(keys[0], keys[1]); + ufind.union(keys[1], keys[2]); + ufind.union(keys[2], keys[0]); + } + + for (idx, face_color) in self.indices().iter().zip(face_colors.iter_mut()) { + debug_assert_eq!( + ufind.find(vertex_to_key[idx[0] as usize]), + ufind.find(vertex_to_key[idx[1] as usize]) + ); + debug_assert_eq!( + ufind.find(vertex_to_key[idx[0] as usize]), + ufind.find(vertex_to_key[idx[2] as usize]) + ); + + let group_index = ufind.find(vertex_to_key[idx[0] as usize]).0 as usize; + + if vertex_to_range[group_index] == u32::MAX { + // Additional range + ranges.push(0); + vertex_to_range[group_index] = ranges.len() as u32 - 1; + } + + let range_id = vertex_to_range[group_index]; + ranges[range_id as usize] += 1; + // NOTE: the range_id points to the range upper bound. The face color is the range lower bound. + *face_color = range_id - 1; + } + + // Cumulated sum on range indices, to find the first index faces need to be inserted into + // for each range. + for i in 1..ranges.len() { + ranges[i] += ranges[i - 1]; + } + + debug_assert_eq!(*ranges.last().unwrap(), self.indices().len()); + + // Group faces. + let mut insertion_in_range_index = ranges.clone(); + for (face_id, face_color) in face_colors.iter().enumerate() { + let insertion_index = &mut insertion_in_range_index[*face_color as usize]; + grouped_faces[*insertion_index] = face_id as u32; + *insertion_index += 1; + } + + self.connected_components = Some(TriMeshConnectedComponents { + face_colors, + grouped_faces, + ranges, + }) + } + + fn compute_connected_components_from_topology(&mut self) { let topo = self.topology.as_ref().unwrap(); let mut face_colors = vec![u32::MAX; topo.faces.len()]; let mut grouped_faces = Vec::new(); @@ -1042,7 +1125,8 @@ impl TypedSimdCompositeShape for TriMesh { #[cfg(test)] mod test { - use crate::shape::{TriMesh, TriMeshFlags}; + use crate::math::{Real, Vector}; + use crate::shape::{Cuboid, TriMesh, TriMeshFlags}; #[test] fn trimesh_error_empty_indices() { @@ -1051,4 +1135,26 @@ mod test { "A triangle mesh with no triangles is invalid." ); } + + #[test] + fn connected_components() { + let (vtx, idx) = Cuboid::new(Vector::repeat(0.5)).to_trimesh(); + + // Push 10 copy of the mesh, each time pushed with an offset. + let mut mesh = TriMesh::new(vtx.clone(), idx.clone()).unwrap(); + + for i in 1..10 { + let cc_vtx = vtx + .iter() + .map(|pt| pt + Vector::repeat(2.0 * i as Real)) + .collect(); + + let to_append = TriMesh::new(cc_vtx, idx.clone()).unwrap(); + mesh.append(&to_append); + } + + mesh.set_flags(TriMeshFlags::CONNECTED_COMPONENTS).unwrap(); + let connected_components = mesh.connected_components().unwrap(); + assert_eq!(connected_components.num_connected_components(), 10); + } } From 64ffee4f296beb1b3dd41e688e2ec7a0b61fd6f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Crozet?= Date: Mon, 25 Nov 2024 17:51:44 +0100 Subject: [PATCH 2/3] feat: add helper functions to get trimesh connected-components as actual meshes --- src/shape/trimesh.rs | 74 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/src/shape/trimesh.rs b/src/shape/trimesh.rs index a5028b39..e4fb3ce6 100644 --- a/src/shape/trimesh.rs +++ b/src/shape/trimesh.rs @@ -96,6 +96,61 @@ impl TriMeshConnectedComponents { pub fn num_connected_components(&self) -> usize { self.ranges.len() - 1 } + + /// Convert the connected-component description into actual meshes (returned as raw index and + /// vertex buffers). + /// + /// The `mesh` must be the one used to generate `self`, otherwise it might panic or produce an + /// unexpected result. + pub fn to_mesh_buffers(&self, mesh: &TriMesh) -> Vec<(Vec>, Vec<[u32; 3]>)> { + let mut result = vec![]; + let mut new_vtx_index: Vec<_> = vec![u32::MAX; mesh.vertices.len()]; + + for ranges in self.ranges.windows(2) { + let num_faces = ranges[1] - ranges[0]; + + if num_faces == 0 { + continue; + } + + let mut vertices = Vec::with_capacity(num_faces); + let mut indices = Vec::with_capacity(num_faces); + + for fid in ranges[0]..ranges[1] { + let vids = mesh.indices[self.grouped_faces[fid] as usize]; + let new_vids = vids.map(|id| { + if new_vtx_index[id as usize] == u32::MAX { + vertices.push(mesh.vertices[id as usize]); + new_vtx_index[id as usize] = vertices.len() as u32 - 1; + } + + new_vtx_index[id as usize] + }); + indices.push(new_vids); + } + + result.push((vertices, indices)); + } + + result + } + + /// Convert the connected-component description into actual meshes. + /// + /// The `mesh` must be the one used to generate `self`, otherwise it might panic or produce an + /// unexpected result. + /// + /// All the meshes are constructed with the given `flags`. + pub fn to_meshes( + &self, + mesh: &TriMesh, + flags: TriMeshFlags, + ) -> Vec> { + self.to_mesh_buffers(mesh) + .into_iter() + .map(|(vtx, idx)| TriMesh::with_flags(vtx, idx, flags)) + .collect() + } } /// A vertex of a triangle-mesh’s half-edge topology. @@ -1040,6 +1095,17 @@ impl TriMesh { self.connected_components.as_ref() } + /// Returns the connected-component of this mesh. + /// + /// The connected-components are returned as a set of `TriMesh` build with the given `flags`. + pub fn connected_component_meshes( + &self, + flags: TriMeshFlags, + ) -> Option>> { + self.connected_components() + .map(|cc| cc.to_meshes(self, flags)) + } + /// The pseudo-normals of this triangle mesh, if they have been computed. #[cfg(feature = "dim3")] pub fn pseudo_normals(&self) -> Option<&TriMeshPseudoNormals> { @@ -1156,5 +1222,13 @@ mod test { mesh.set_flags(TriMeshFlags::CONNECTED_COMPONENTS).unwrap(); let connected_components = mesh.connected_components().unwrap(); assert_eq!(connected_components.num_connected_components(), 10); + + let cc_meshes = connected_components.to_meshes(&mesh, TriMeshFlags::empty()); + + for cc in cc_meshes { + let cc = cc.unwrap(); + assert_eq!(cc.vertices.len(), vtx.len()); + assert_eq!(cc.indices.len(), idx.len()); + } } } From 3d2ddde3ee23736c743894a1e9b43042e957b1e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Crozet?= Date: Tue, 26 Nov 2024 11:31:19 +0100 Subject: [PATCH 3/3] chore: update changelog --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 666a7dd7..407a2693 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,19 @@ - Removed `IntersectionCompositeShapeShapeBestFirstVisitor` (which had been deprecated for a while): use `IntersectionCompositeShapeShapeVisitor` instead. +## v0.17.4 + +### Added + +- Add `TriMeshConnectedComponents::to_meshes` and `::to_mesh_buffers` to easily extract individual meshes from the set + of connected components. +- Add `TriMesh::connected_component_meshes` to get the connected components as meshes directly. + +### Modified + +- Connected-components extraction will never fail now, and no longer require the successful calculation of the mesh’s + half-edge topology. + ## v0.17.3 ### Fix