From 787141611d8b95e1d9bd040001e1bae11a6763c0 Mon Sep 17 00:00:00 2001 From: 0xbeefd1ed Date: Sun, 6 Sep 2026 15:14:39 -0700 Subject: [PATCH 1/9] Add generic voxel traits --- crates/parry3d/tests/geometry/mod.rs | 1 + .../geometry/voxel_query_custom_storage.rs | 369 ++++++++++++++++++ src/mass_properties/mass_properties_voxels.rs | 9 +- .../contact_manifolds_voxels_ball.rs | 11 +- ...ontact_manifolds_voxels_composite_shape.rs | 11 +- .../contact_manifolds_voxels_shape.rs | 20 +- .../contact_manifolds_voxels_voxels.rs | 28 +- src/query/contact_manifolds/mod.rs | 4 +- .../intersection_test_voxels_shape.rs | 14 +- .../nonlinear_shape_cast_voxels_shape.rs | 18 +- src/query/point/mod.rs | 2 + src/query/point/point_voxels.rs | 89 ++++- src/query/ray/mod.rs | 2 + src/query/ray/ray_voxels.rs | 116 +++++- .../shape_cast/shape_cast_voxels_shape.rs | 18 +- src/shape/mod.rs | 5 +- src/shape/voxels/mod.rs | 2 + src/shape/voxels/voxel_query.rs | 262 +++++++++++++ src/shape/voxels/voxels.rs | 45 ++- src/shape/voxels/voxels_chunk.rs | 6 +- 20 files changed, 975 insertions(+), 57 deletions(-) create mode 100644 crates/parry3d/tests/geometry/voxel_query_custom_storage.rs create mode 100644 src/shape/voxels/voxel_query.rs diff --git a/crates/parry3d/tests/geometry/mod.rs b/crates/parry3d/tests/geometry/mod.rs index 25a4e755..1c1d91db 100644 --- a/crates/parry3d/tests/geometry/mod.rs +++ b/crates/parry3d/tests/geometry/mod.rs @@ -10,3 +10,4 @@ mod time_of_impact3; mod trimesh_connected_components; mod trimesh_intersection; mod trimesh_trimesh_toi; +mod voxel_query_custom_storage; diff --git a/crates/parry3d/tests/geometry/voxel_query_custom_storage.rs b/crates/parry3d/tests/geometry/voxel_query_custom_storage.rs new file mode 100644 index 00000000..ece4e50f --- /dev/null +++ b/crates/parry3d/tests/geometry/voxel_query_custom_storage.rs @@ -0,0 +1,369 @@ +//! Checks that parry's voxel collision-detection algorithms, which are generic over the +//! [`VoxelQuery`] trait, produce the same results when running on a custom voxel storage +//! as when running on the built-in [`Voxels`] shape. + +use parry3d::mass_properties::MassProperties; +use parry3d::math::{IVector, Pose, Real, Vector}; +use parry3d::query::details; +use parry3d::query::{ + ContactManifold, DefaultQueryDispatcher, PointQuery, Ray, RayCast, ShapeCastOptions, +}; +use parry3d::shape::{Ball, Cuboid, Shape, VoxelData, VoxelQuery, VoxelState, Voxels}; +use std::collections::BTreeMap; + +/// A custom sparse voxel storage backed by a `BTreeMap`. +/// +/// It mirrors the content of a [`Voxels`] shape (including its linear ids) so that query +/// results are directly comparable, but shares none of its implementation. +struct BTreeVoxels { + voxel_size: Vector, + domain: [IVector; 2], + voxels: BTreeMap<[i32; 3], (VoxelState, u32)>, +} + +impl BTreeVoxels { + fn mirroring(voxels: &Voxels) -> Self { + let mut map = BTreeMap::new(); + for vox in voxels.voxels() { + if !vox.state.is_empty() { + map.insert( + [vox.grid_coords.x, vox.grid_coords.y, vox.grid_coords.z], + (vox.state, vox.linear_id), + ); + } + } + + Self { + voxel_size: voxels.voxel_size(), + domain: VoxelQuery::domain(voxels), + voxels: map, + } + } +} + +impl VoxelQuery for BTreeVoxels { + fn voxel_size(&self) -> Vector { + self.voxel_size + } + + fn domain(&self) -> [IVector; 2] { + self.domain + } + + fn voxel_state(&self, key: IVector) -> Option { + Some( + self.voxels + .get(&[key.x, key.y, key.z]) + .map(|(state, _)| *state) + .unwrap_or(VoxelState::EMPTY), + ) + } + + fn linear_id(&self, key: IVector) -> Option { + self.voxels.get(&[key.x, key.y, key.z]).map(|(_, id)| *id) + } + + fn voxels_in_range(&self, mins: IVector, maxs: IVector) -> impl Iterator { + self.voxels.iter().filter_map(move |(k, (state, id))| { + let key = IVector::new(k[0], k[1], k[2]); + (key.cmpge(mins).all() && key.cmplt(maxs).all()).then(|| VoxelData { + linear_id: *id, + grid_coords: key, + center: self.voxel_center(key), + state: *state, + }) + }) + } +} + +/// An 8×8 ground plate, a wall along one of its edges, and a disconnected lone voxel, +/// with non-uniform voxel sizes. +fn reference_shape() -> Voxels { + let mut keys = vec![]; + + for x in 0..8 { + for z in 0..8 { + keys.push(IVector::new(x, 0, z)); + } + } + + for y in 1..4 { + for z in 0..8 { + keys.push(IVector::new(0, y, z)); + } + } + + keys.push(IVector::new(10, 2, 3)); + + Voxels::new(Vector::new(1.0, 0.5, 0.75), &keys) +} + +fn fixtures() -> (Voxels, BTreeVoxels) { + let voxels = reference_shape(); + let custom = BTreeVoxels::mirroring(&voxels); + (voxels, custom) +} + +#[test] +fn custom_storage_matches_voxels_states() { + let (voxels, custom) = fixtures(); + let [mins, maxs] = VoxelQuery::domain(&voxels); + let margin = IVector::splat(2); + + let mut checked_non_empty = 0; + for x in mins.x - margin.x..maxs.x + margin.x { + for y in mins.y - margin.y..maxs.y + margin.y { + for z in mins.z - margin.z..maxs.z + margin.z { + let key = IVector::new(x, y, z); + let state1 = voxels.voxel_state(key).unwrap_or(VoxelState::EMPTY); + let state2 = VoxelQuery::voxel_state(&custom, key).unwrap_or(VoxelState::EMPTY); + assert_eq!(state1, state2, "state mismatch at {:?}", key); + + if !state1.is_empty() { + assert_eq!( + VoxelQuery::linear_id(&voxels, key), + VoxelQuery::linear_id(&custom, key), + "linear_id mismatch at {:?}", + key + ); + checked_non_empty += 1; + } + } + } + } + + // 8×8 plate + 3×8 wall + 1 lone voxel. + assert_eq!(checked_non_empty, 64 + 24 + 1); + assert_eq!(custom.voxels().count(), 64 + 24 + 1); +} + +#[test] +fn custom_storage_matches_voxels_mass_properties() { + let (voxels, custom) = fixtures(); + let density = 2.0; + let props1 = MassProperties::from_voxels(density, &voxels); + let props2 = MassProperties::from_voxels(density, &custom); + + // Absolute anchor: 89 voxels of volume 1.0 × 0.5 × 0.75. + let expected_mass = 89.0 * (1.0 * 0.5 * 0.75) * density; + assert_relative_eq!(props1.mass(), expected_mass, epsilon = 1.0e-4); + + assert_relative_eq!(props1.mass(), props2.mass(), epsilon = 1.0e-6); + assert_relative_eq!(props1.local_com, props2.local_com, epsilon = 1.0e-5); +} + +#[test] +fn custom_storage_matches_voxels_raycast() { + let (voxels, custom) = fixtures(); + + let mut origins = vec![]; + for i in 0..8 { + for j in 0..8 { + // Jittered origins above the shape (jitter avoids exact ties on voxel edges). + origins.push(Vector::new( + i as Real * 1.043 + 0.117, + 4.31, + j as Real * 0.921 + 0.083, + )); + } + } + + let dirs = [ + Vector::new(0.0231, -1.0, 0.0173), + Vector::new(-0.4173, -0.8317, 0.1531), + Vector::new(0.723, -0.317, -0.5911), + Vector::new(0.0731, 1.0, 0.0413), // Away from the shape: must miss. + ]; + + let mut num_hits = 0; + for origin in &origins { + for dir in &dirs { + let ray = Ray::new(*origin, *dir); + let hit1 = voxels.cast_local_ray_and_get_normal(&ray, 100.0, true); + let hit2 = details::cast_local_ray_on_voxels(&custom, &ray, 100.0, true); + + assert_eq!(hit1.is_some(), hit2.is_some(), "hit mismatch for {:?}", ray); + + if let (Some(hit1), Some(hit2)) = (hit1, hit2) { + num_hits += 1; + assert_relative_eq!(hit1.time_of_impact, hit2.time_of_impact, epsilon = 1.0e-5); + assert_relative_eq!(hit1.normal, hit2.normal, epsilon = 1.0e-5); + assert_eq!(hit1.feature, hit2.feature, "feature mismatch for {:?}", ray); + } + } + } + + // Sanity check: the straight-down rays from above the plate must all hit. + assert!(num_hits >= 64); + + // Absolute anchor: a ray straight above the plate hits its top at y = 0.5. + let ray = Ray::new(Vector::new(4.13, 4.0, 3.77), Vector::new(0.0, -1.0, 0.0)); + let hit = details::cast_local_ray_on_voxels(&custom, &ray, 100.0, true).unwrap(); + assert_relative_eq!(hit.time_of_impact, 4.0 - 0.5, epsilon = 1.0e-5); + assert_relative_eq!(hit.normal, Vector::new(0.0, 1.0, 0.0), epsilon = 1.0e-5); +} + +#[test] +fn custom_storage_matches_voxels_point_projection() { + let (voxels, custom) = fixtures(); + + let mut points = vec![]; + for i in -2..12 { + for j in -2..6 { + for k in -2..10 { + points.push(Vector::new( + i as Real * 1.117 + 0.031, + j as Real * 0.617 + 0.043, + k as Real * 0.917 + 0.021, + )); + } + } + } + + for solid in [true, false] { + for pt in &points { + let proj1 = voxels.project_local_point(*pt, solid); + let proj2 = details::project_local_point_on_voxels(&custom, *pt, solid) + .expect("the shape is not empty") + .0; + + assert_eq!( + proj1.is_inside, proj2.is_inside, + "is_inside mismatch at {:?} (solid: {})", + pt, solid + ); + assert_relative_eq!(proj1.point, proj2.point, epsilon = 1.0e-4); + } + } +} + +type TestManifold = ContactManifold<(), ()>; + +fn compare_manifolds(manifolds1: &mut [TestManifold], manifolds2: &mut [TestManifold]) { + assert_eq!(manifolds1.len(), manifolds2.len()); + + // The two backends iterate voxels in a different order, so match manifolds by + // their subshape ids. + let sort_key = |m: &TestManifold| (m.subshape1, m.subshape2); + manifolds1.sort_by_key(sort_key); + manifolds2.sort_by_key(sort_key); + + for (m1, m2) in manifolds1.iter().zip(manifolds2.iter()) { + assert_eq!(m1.subshape1, m2.subshape1); + assert_eq!(m1.subshape2, m2.subshape2); + assert_eq!(m1.points.len(), m2.points.len()); + + if !m1.points.is_empty() { + assert_relative_eq!(m1.local_n1, m2.local_n1, epsilon = 1.0e-5); + } + + for (pt1, pt2) in m1.points.iter().zip(m2.points.iter()) { + assert_relative_eq!(pt1.dist, pt2.dist, epsilon = 1.0e-5); + assert_relative_eq!(pt1.local_p1, pt2.local_p1, epsilon = 1.0e-4); + assert_relative_eq!(pt1.local_p2, pt2.local_p2, epsilon = 1.0e-4); + } + } +} + +#[test] +fn custom_storage_matches_voxels_contact_manifolds() { + let (voxels, custom) = fixtures(); + let dispatcher = DefaultQueryDispatcher; + let cuboid = Cuboid::new(Vector::new(0.4, 0.6, 0.5)); + let prediction = 0.05; + + let poses = [ + // Resting on the plate, slightly penetrating. + Pose::translation(3.13, 0.5 + 0.6 - 0.02, 4.21), + // Touching both the plate and the wall. + Pose::translation(1.0 + 0.4 - 0.01, 0.5 + 0.6 - 0.01, 3.87), + // Hovering within prediction distance. + Pose::translation(5.11, 0.5 + 0.6 + 0.03, 2.93), + // Overlapping the lone voxel. + Pose::translation(10.5, 1.3, 2.71), + // Far away: no contacts at all. + Pose::translation(20.0, 10.0, 20.0), + ]; + + let mut total_points = 0; + for pos12 in &poses { + let mut manifolds1 = Vec::::new(); + let mut manifolds2 = Vec::::new(); + let mut workspace1 = None; + let mut workspace2 = None; + + details::contact_manifolds_voxels_shape( + &dispatcher, + pos12, + &voxels, + &cuboid as &dyn Shape, + prediction, + &mut manifolds1, + &mut workspace1, + false, + ); + details::contact_manifolds_voxels_shape( + &dispatcher, + pos12, + &custom, + &cuboid as &dyn Shape, + prediction, + &mut manifolds2, + &mut workspace2, + false, + ); + + compare_manifolds(&mut manifolds1, &mut manifolds2); + total_points += manifolds1.iter().map(|m| m.points.len()).sum::(); + } + + // Sanity check: at least the resting/touching poses must have produced actual contacts. + assert!(total_points > 0); +} + +#[test] +fn custom_storage_matches_voxels_shape_cast() { + let (voxels, custom) = fixtures(); + let dispatcher = DefaultQueryDispatcher; + let ball = Ball::new(0.3); + let pos12 = Pose::translation(4.05, 3.0, 4.1); + let vel12 = Vector::new(0.0, -1.0, 0.0); + let options = ShapeCastOptions::default(); + + let hit1 = + details::cast_shapes_voxels_shape(&dispatcher, &pos12, vel12, &voxels, &ball, options); + let hit2 = + details::cast_shapes_voxels_shape(&dispatcher, &pos12, vel12, &custom, &ball, options); + + let hit1 = hit1.expect("the ball must hit the plate"); + let hit2 = hit2.expect("the ball must hit the plate"); + + // Absolute anchor: the ball surface reaches the plate's top (y = 0.5) after + // travelling 3.0 - 0.5 - 0.3 units. + assert_relative_eq!(hit1.time_of_impact, 3.0 - 0.5 - 0.3, epsilon = 1.0e-4); + + assert_relative_eq!(hit1.time_of_impact, hit2.time_of_impact, epsilon = 1.0e-5); + assert_relative_eq!(hit1.normal1, hit2.normal1, epsilon = 1.0e-4); + assert_relative_eq!(hit1.witness1, hit2.witness1, epsilon = 1.0e-4); +} + +#[test] +fn custom_storage_matches_voxels_intersection_test() { + let (voxels, custom) = fixtures(); + let dispatcher = DefaultQueryDispatcher; + let cuboid = Cuboid::new(Vector::new(0.4, 0.6, 0.5)); + + let poses = [ + (Pose::translation(3.13, 0.7, 4.21), true), // Overlapping the plate. + (Pose::translation(10.5, 1.3, 2.71), true), // Overlapping the lone voxel. + (Pose::translation(4.0, 5.0, 4.0), false), // Above everything. + (Pose::translation(20.0, 0.0, 20.0), false), // Far away. + ]; + + for (pos12, expected) in &poses { + let hit1 = details::intersection_test_voxels_shape(&dispatcher, pos12, &voxels, &cuboid); + let hit2 = details::intersection_test_voxels_shape(&dispatcher, pos12, &custom, &cuboid); + assert_eq!(hit1, *expected); + assert_eq!(hit2, *expected); + } +} diff --git a/src/mass_properties/mass_properties_voxels.rs b/src/mass_properties/mass_properties_voxels.rs index e08b907f..fcf01b55 100644 --- a/src/mass_properties/mass_properties_voxels.rs +++ b/src/mass_properties/mass_properties_voxels.rs @@ -2,7 +2,7 @@ use crate::mass_properties::MassProperties; #[cfg(feature = "dim3")] use crate::math::Matrix; use crate::math::{Real, Vector}; -use crate::shape::Voxels; +use crate::shape::VoxelQuery; impl MassProperties { /// Computes the mass properties of a voxel grid. @@ -17,10 +17,11 @@ impl MassProperties { /// * `density` - The material density /// - In 3D: kg/m³ (mass per unit volume) /// - In 2D: kg/m² (mass per unit area) - /// * `voxels` - A `Voxels` structure containing the voxel grid + /// * `voxels` - Any voxel storage implementing [`VoxelQuery`], e.g. the + /// [`Voxels`](crate::shape::Voxels) shape /// - Each voxel is a small cube/square of uniform size /// - Voxels can be empty or filled - /// - Since v0.25.0, uses sparse storage internally for efficiency + /// - Since v0.25.0, `Voxels` uses sparse storage internally for efficiency /// /// # Returns /// @@ -171,7 +172,7 @@ impl MassProperties { /// - `Voxels::set_voxel()`: Add or remove voxels /// - `from_trimesh()`: Alternative for precise shapes /// - `from_compound()`: Combine multiple shapes efficiently - pub fn from_voxels(density: Real, voxels: &Voxels) -> Self { + pub fn from_voxels(density: Real, voxels: &V) -> Self { let mut com = Vector::ZERO; let mut num_not_empty = 0; #[cfg(feature = "dim2")] diff --git a/src/query/contact_manifolds/contact_manifolds_voxels_ball.rs b/src/query/contact_manifolds/contact_manifolds_voxels_ball.rs index fc16604c..99b7ca06 100644 --- a/src/query/contact_manifolds/contact_manifolds_voxels_ball.rs +++ b/src/query/contact_manifolds/contact_manifolds_voxels_ball.rs @@ -2,7 +2,7 @@ use crate::bounding_volume::BoundingVolume; use crate::math::{Pose, Real, Vector, VectorExt}; use crate::query::{ContactManifold, PointQuery, TrackedContact}; use crate::shape::{ - Ball, Cuboid, OctantPattern, PackedFeatureId, Shape, VoxelState, VoxelType, Voxels, + Ball, Cuboid, OctantPattern, PackedFeatureId, Shape, VoxelQuery, VoxelState, VoxelType, }; use alloc::vec::Vec; @@ -31,10 +31,12 @@ pub fn contact_manifolds_voxels_ball_shapes( } } -/// Computes the contact manifold between a convex shape and a ball. -pub fn contact_manifolds_voxels_ball<'a, ManifoldData, ContactData>( +/// Computes the contact manifold between a voxels shape and a ball. +/// +/// The voxels shape can be any voxel storage implementing [`VoxelQuery`]. +pub fn contact_manifolds_voxels_ball<'a, ManifoldData, ContactData, V>( pos12: &Pose, - voxels1: &'a Voxels, + voxels1: &'a V, ball2: &'a Ball, prediction: Real, manifolds: &mut Vec>, @@ -42,6 +44,7 @@ pub fn contact_manifolds_voxels_ball<'a, ManifoldData, ContactData>( ) where ManifoldData: Default, ContactData: Default + Copy, + V: ?Sized + VoxelQuery, { // TODO: don’t generate one manifold per voxel. manifolds.clear(); diff --git a/src/query/contact_manifolds/contact_manifolds_voxels_composite_shape.rs b/src/query/contact_manifolds/contact_manifolds_voxels_composite_shape.rs index af948a09..14d832fe 100644 --- a/src/query/contact_manifolds/contact_manifolds_voxels_composite_shape.rs +++ b/src/query/contact_manifolds/contact_manifolds_voxels_composite_shape.rs @@ -7,7 +7,7 @@ use crate::query::{ ContactManifold, ContactManifoldsWorkspace, PersistentQueryDispatcher, PointQuery, TypedWorkspaceData, WorkspaceData, }; -use crate::shape::{CompositeShape, Cuboid, Shape, SupportMap, VoxelType, Voxels}; +use crate::shape::{CompositeShape, Cuboid, Shape, SupportMap, VoxelQuery, VoxelType}; use crate::utils::hashmap::Entry; use crate::utils::PoseOpt; use alloc::{boxed::Box, vec::Vec}; @@ -55,10 +55,12 @@ pub fn contact_manifolds_voxels_composite_shape_shapes( +/// +/// The voxels shape can be any voxel storage implementing [`VoxelQuery`]. +pub fn contact_manifolds_voxels_composite_shape( dispatcher: &dyn PersistentQueryDispatcher, pos12: &Pose, - voxels1: &Voxels, + voxels1: &V, shape2: &dyn CompositeShape, prediction: Real, manifolds: &mut Vec>, @@ -67,6 +69,7 @@ pub fn contact_manifolds_voxels_composite_shape( ) where ManifoldData: Default + Clone, ContactData: Default + Copy, + V: ?Sized + VoxelQuery, { VoxelsShapeContactManifoldsWorkspace::<3>::ensure_exists(workspace); let workspace: &mut VoxelsShapeContactManifoldsWorkspace<3> = @@ -135,7 +138,7 @@ pub fn contact_manifolds_voxels_composite_shape( timestamp: new_timestamp, }; - let vox_id = vox1.linear_id.flat_id() as u32; + let vox_id = vox1.linear_id; let (id1, id2) = if flipped { (leaf2, vox_id) } else { diff --git a/src/query/contact_manifolds/contact_manifolds_voxels_shape.rs b/src/query/contact_manifolds/contact_manifolds_voxels_shape.rs index f0b50e6d..f1131681 100644 --- a/src/query/contact_manifolds/contact_manifolds_voxels_shape.rs +++ b/src/query/contact_manifolds/contact_manifolds_voxels_shape.rs @@ -4,7 +4,7 @@ use crate::query::{ ContactManifold, ContactManifoldsWorkspace, PersistentQueryDispatcher, PointQuery, TypedWorkspaceData, WorkspaceData, }; -use crate::shape::{AxisMask, Cuboid, Shape, SupportMap, VoxelData, VoxelType, Voxels}; +use crate::shape::{AxisMask, Cuboid, Shape, SupportMap, VoxelData, VoxelQuery, VoxelType}; use crate::utils::hashmap::{Entry, HashMap}; use crate::utils::PoseOpt; use alloc::{boxed::Box, vec::Vec}; @@ -113,10 +113,12 @@ pub fn contact_manifolds_voxels_shape_shapes( } /// Computes the contact manifold between a convex shape and a voxels shape. -pub fn contact_manifolds_voxels_shape( +/// +/// The voxels shape can be any voxel storage implementing [`VoxelQuery`]. +pub fn contact_manifolds_voxels_shape( dispatcher: &dyn PersistentQueryDispatcher, pos12: &Pose, - voxels1: &Voxels, + voxels1: &V, shape2: &dyn Shape, prediction: Real, manifolds: &mut Vec>, @@ -125,6 +127,7 @@ pub fn contact_manifolds_voxels_shape( ) where ManifoldData: Default + Clone, ContactData: Default + Copy, + V: ?Sized + VoxelQuery, { VoxelsShapeContactManifoldsWorkspace::<2>::ensure_exists(workspace); let workspace: &mut VoxelsShapeContactManifoldsWorkspace<2> = @@ -185,7 +188,7 @@ pub fn contact_manifolds_voxels_shape( timestamp: new_timestamp, }; - let vid = vox1.linear_id.flat_id() as u32; + let vid = vox1.linear_id; let (id1, id2) = if flipped { (0, vid) } else { (vid, 0) }; manifolds.push(ContactManifold::with_data( id1, @@ -334,7 +337,7 @@ pub(crate) struct CanonicalVoxelShape { } impl CanonicalVoxelShape { - pub fn from_voxel(voxels: &Voxels, vox: &VoxelData) -> Self { + pub fn from_voxel(voxels: &V, vox: &VoxelData) -> Self { let mut key_low = vox.grid_coords; let mut key_high = key_low; @@ -381,7 +384,12 @@ impl CanonicalVoxelShape { } } - pub fn cuboid(&self, voxels: &Voxels, vox: &VoxelData, domain2_1: Aabb) -> (Vector, Cuboid) { + pub fn cuboid( + &self, + voxels: &V, + vox: &VoxelData, + domain2_1: Aabb, + ) -> (Vector, Cuboid) { let radius = voxels.voxel_size() / 2.0; let mut canonical_mins = voxels.voxel_center(self.range[0]); let mut canonical_maxs = voxels.voxel_center(self.range[1]); diff --git a/src/query/contact_manifolds/contact_manifolds_voxels_voxels.rs b/src/query/contact_manifolds/contact_manifolds_voxels_voxels.rs index 630ab3a6..e3c6b5b0 100644 --- a/src/query/contact_manifolds/contact_manifolds_voxels_voxels.rs +++ b/src/query/contact_manifolds/contact_manifolds_voxels_voxels.rs @@ -6,7 +6,7 @@ use crate::query::{ ContactManifold, ContactManifoldsWorkspace, PersistentQueryDispatcher, PointQuery, TypedWorkspaceData, WorkspaceData, }; -use crate::shape::{Cuboid, Shape, SupportMap, VoxelData, VoxelType, Voxels}; +use crate::shape::{Cuboid, Shape, SupportMap, VoxelData, VoxelQuery, VoxelType}; use crate::utils::hashmap::Entry; use crate::utils::PoseOpt; use alloc::{boxed::Box, vec::Vec}; @@ -41,18 +41,22 @@ pub fn contact_manifolds_voxels_voxels_shapes( } } -/// Computes the contact manifold between a convex shape and a ball. -pub fn contact_manifolds_voxels_voxels<'a, ManifoldData, ContactData>( +/// Computes the contact manifold between two voxels shapes. +/// +/// The voxels shapes can be any voxel storages implementing [`VoxelQuery`]. +pub fn contact_manifolds_voxels_voxels<'a, ManifoldData, ContactData, V1, V2>( dispatcher: &dyn PersistentQueryDispatcher, pos12: &Pose, - voxels1: &'a Voxels, - voxels2: &'a Voxels, + voxels1: &'a V1, + voxels2: &'a V2, prediction: Real, manifolds: &mut Vec>, workspace: &mut Option, ) where ManifoldData: Default + Clone, ContactData: Default + Copy, + V1: ?Sized + VoxelQuery, + V2: ?Sized + VoxelQuery, { VoxelsShapeContactManifoldsWorkspace::<4>::ensure_exists(workspace); let workspace: &mut VoxelsShapeContactManifoldsWorkspace<4> = @@ -77,11 +81,17 @@ pub fn contact_manifolds_voxels_voxels<'a, ManifoldData, ContactData>( aabb1.aligned_intersections(pos12, &aabb2) { let domain_margin = (radius1 + radius2) * 10.0; - let full_domain2_1 = voxels2.compute_aabb(pos12).add_half_extents(domain_margin); + let full_domain2_1 = voxels2 + .local_aabb() + .transform_by(pos12) + .add_half_extents(domain_margin); let domain2_1 = full_domain2_1 .intersection(&aabb1.add_half_extents(domain_margin)) .unwrap_or(full_domain2_1); - let full_domain1_2 = voxels1.compute_aabb(&pos21).add_half_extents(domain_margin); + let full_domain1_2 = voxels1 + .local_aabb() + .transform_by(&pos21) + .add_half_extents(domain_margin); let domain1_2 = full_domain1_2 .intersection(&aabb2.add_half_extents(domain_margin)) .unwrap_or(full_domain1_2); @@ -129,8 +139,8 @@ pub fn contact_manifolds_voxels_voxels<'a, ManifoldData, ContactData>( }; manifolds.push(ContactManifold::with_data( - vox1.linear_id.flat_id() as u32, - vox2.linear_id.flat_id() as u32, + vox1.linear_id, + vox2.linear_id, ManifoldData::default(), )); diff --git a/src/query/contact_manifolds/mod.rs b/src/query/contact_manifolds/mod.rs index 7fe44fb2..89885dc4 100644 --- a/src/query/contact_manifolds/mod.rs +++ b/src/query/contact_manifolds/mod.rs @@ -163,7 +163,9 @@ pub use self::contact_manifolds_pfm_pfm::{ pub use self::contact_manifolds_trimesh_shape::{ contact_manifolds_trimesh_shape, contact_manifolds_trimesh_shape_shapes, }; -pub use self::contact_manifolds_voxels_ball::contact_manifolds_voxels_ball_shapes; +pub use self::contact_manifolds_voxels_ball::{ + contact_manifolds_voxels_ball, contact_manifolds_voxels_ball_shapes, +}; pub use self::contact_manifolds_voxels_composite_shape::{ contact_manifolds_voxels_composite_shape, contact_manifolds_voxels_composite_shape_shapes, }; diff --git a/src/query/intersection_test/intersection_test_voxels_shape.rs b/src/query/intersection_test/intersection_test_voxels_shape.rs index 5e36affc..362850d1 100644 --- a/src/query/intersection_test/intersection_test_voxels_shape.rs +++ b/src/query/intersection_test/intersection_test_voxels_shape.rs @@ -1,6 +1,6 @@ use crate::math::Pose; use crate::query::PersistentQueryDispatcher; -use crate::shape::{Cuboid, Shape, VoxelType, Voxels}; +use crate::shape::{Cuboid, Shape, VoxelQuery, VoxelType}; /// Checks for any intersection between voxels and an arbitrary shape, both represented as a `Shape` trait-object. pub fn intersection_test_voxels_shape_shapes( @@ -19,10 +19,12 @@ pub fn intersection_test_voxels_shape_shapes( } /// Checks for any intersection between voxels and an arbitrary shape. -pub fn intersection_test_voxels_shape( +/// +/// The voxels shape can be any voxel storage implementing [`VoxelQuery`]. +pub fn intersection_test_voxels_shape( dispatcher: &dyn PersistentQueryDispatcher, pos12: &Pose, - voxels1: &Voxels, + voxels1: &V, shape2: &dyn Shape, ) -> bool { let radius1 = voxels1.voxel_size() / 2.0; @@ -54,11 +56,13 @@ pub fn intersection_test_voxels_shape( } /// Checks for any intersection between voxels and an arbitrary shape. -pub fn intersection_test_shape_voxels( +/// +/// The voxels shape can be any voxel storage implementing [`VoxelQuery`]. +pub fn intersection_test_shape_voxels( dispatcher: &dyn PersistentQueryDispatcher, pos12: &Pose, shape1: &dyn Shape, - voxels2: &Voxels, + voxels2: &V, ) -> bool { intersection_test_voxels_shape(dispatcher, &pos12.inverse(), voxels2, shape1) } diff --git a/src/query/nonlinear_shape_cast/nonlinear_shape_cast_voxels_shape.rs b/src/query/nonlinear_shape_cast/nonlinear_shape_cast_voxels_shape.rs index 123a8e32..565cd491 100644 --- a/src/query/nonlinear_shape_cast/nonlinear_shape_cast_voxels_shape.rs +++ b/src/query/nonlinear_shape_cast/nonlinear_shape_cast_voxels_shape.rs @@ -1,13 +1,15 @@ use crate::bounding_volume::BoundingVolume; use crate::math::{IVector, IVectorExt, Real, Vector, VectorExt}; use crate::query::{NonlinearRigidMotion, QueryDispatcher, ShapeCastHit}; -use crate::shape::{Cuboid, Shape, Voxels}; +use crate::shape::{Cuboid, Shape, VoxelQuery}; /// Time Of Impact of a voxels shape with any other shape, under a rigid motion (translation + rotation). -pub fn cast_shapes_nonlinear_voxels_shape( +/// +/// The voxels shape can be any voxel storage implementing [`VoxelQuery`]. +pub fn cast_shapes_nonlinear_voxels_shape( dispatcher: &D, motion1: &NonlinearRigidMotion, - g1: &Voxels, + g1: &V, motion2: &NonlinearRigidMotion, g2: &dyn Shape, start_time: Real, @@ -16,6 +18,7 @@ pub fn cast_shapes_nonlinear_voxels_shape( ) -> Option where D: ?Sized + QueryDispatcher, + V: ?Sized + VoxelQuery, { use num_traits::Bounded; @@ -170,19 +173,22 @@ where hit } -/// Time Of Impact of any shape with a composite shape, under a rigid motion (translation + rotation). -pub fn cast_shapes_nonlinear_shape_voxels( +/// Time Of Impact of any shape with a voxels shape, under a rigid motion (translation + rotation). +/// +/// The voxels shape can be any voxel storage implementing [`VoxelQuery`]. +pub fn cast_shapes_nonlinear_shape_voxels( dispatcher: &D, motion1: &NonlinearRigidMotion, g1: &dyn Shape, motion2: &NonlinearRigidMotion, - g2: &Voxels, + g2: &V, start_time: Real, end_time: Real, stop_at_penetration: bool, ) -> Option where D: ?Sized + QueryDispatcher, + V: ?Sized + VoxelQuery, { cast_shapes_nonlinear_voxels_shape( dispatcher, diff --git a/src/query/point/mod.rs b/src/query/point/mod.rs index 3ecf3250..d16ef499 100644 --- a/src/query/point/mod.rs +++ b/src/query/point/mod.rs @@ -4,6 +4,8 @@ pub use self::point_query::{PointProjection, PointQuery, PointQueryWithLocation}; #[cfg(feature = "alloc")] pub use self::point_support_map::local_point_projection_on_support_map; +#[cfg(feature = "alloc")] +pub use self::point_voxels::project_local_point_on_voxels; mod point_aabb; mod point_ball; diff --git a/src/query/point/point_voxels.rs b/src/query/point/point_voxels.rs index 87a5b147..61de069c 100644 --- a/src/query/point/point_voxels.rs +++ b/src/query/point/point_voxels.rs @@ -1,6 +1,89 @@ +use crate::bounding_volume::{Aabb, BoundingVolume}; use crate::math::{Real, Vector}; use crate::query::{PointProjection, PointQuery}; -use crate::shape::{Cuboid, FeatureId, Voxels, VoxelsChunkRef}; +use crate::shape::{Cuboid, FeatureId, VoxelQuery, Voxels, VoxelsChunkRef}; + +/// Projects a point on a voxel shape represented by any storage implementing [`VoxelQuery`]. +/// +/// Returns the projection and the [`linear_id`](VoxelQuery::linear_id) of the voxel the +/// projected point lies on, or `None` if the shape contains no voxel. If `solid` is `true` and +/// the point lies inside a non-empty voxel, the point itself is returned as the projection. +/// +/// This searches voxels in growing regions centered on the point, without relying on any +/// acceleration structure. The concrete [`Voxels`] shape implements [`PointQuery`] with a +/// faster search based on its internal BVH; this function is mostly useful for implementing +/// point queries on custom voxel storages. +/// +/// Like the [`PointQuery`] implementation of [`Voxels`], the non-solid projection of a point +/// lying inside of the shape is approximated: the point is projected on the boundary of the +/// closest voxel, which isn't necessarily on the boundary of the union of all the voxels. +pub fn project_local_point_on_voxels( + voxels: &V, + pt: Vector, + solid: bool, +) -> Option<(PointProjection, u32)> { + let base_cuboid = Cuboid::new(voxels.voxel_size() / 2.0); + + // Fast path: the point lies inside a non-empty voxel. + let key_at_pt = voxels.voxel_at_point(pt); + if solid + && voxels + .voxel_state(key_at_pt) + .is_some_and(|state| !state.is_empty()) + { + return Some(( + PointProjection::new(true, pt), + voxels.linear_id(key_at_pt).unwrap_or(0), + )); + } + + // The distance from `pt` to the domain’s AABB is a lower-bound of the distance from `pt` + // to its projection on the shape. Use it to initialize the search radius. + let domain_aabb = voxels.local_aabb(); + let mut search_radius = + domain_aabb.distance_to_local_point(pt, true) + voxels.voxel_size().max_element(); + + loop { + let search_aabb = Aabb::from_half_extents(pt, Vector::splat(search_radius)); + let mut best: Option<(PointProjection, u32)> = None; + let mut best_dist = Real::MAX; + + for vox in voxels.voxels_intersecting_local_aabb(&search_aabb) { + if vox.state.is_empty() { + continue; + } + + let mut candidate = base_cuboid.project_local_point(pt - vox.center, solid); + candidate.point += vox.center; + + let candidate_dist = (candidate.point - pt).length(); + if candidate_dist < best_dist { + best = Some((candidate, vox.linear_id)); + best_dist = candidate_dist; + } + } + + if let Some(best) = best { + if best_dist <= search_radius { + // Any voxel closer to `pt` than `best_dist` would have intersected the + // search region, so this projection is optimal. + return Some(best); + } + + // The projection found lies outside of the search region: another voxel closer + // to its boundary could still be a better candidate. Re-run with a search region + // that covers every possibly-better voxel. + search_radius = best_dist; + } else { + if search_aabb.contains(&domain_aabb) { + // The whole shape was searched and no voxel was found. + return None; + } + + search_radius *= 2.0; + } + } +} impl PointQuery for Voxels { #[inline] @@ -55,11 +138,11 @@ impl<'a> VoxelsChunkRef<'a> { let candidate_dist = (candidate.point - pt).length(); if candidate_dist < smallest_dist { result = candidate; - result_vox_id = vox.linear_id.flat_id(); + result_vox_id = vox.linear_id; smallest_dist = candidate_dist; } } - (smallest_dist < Real::MAX).then_some((result, result_vox_id as u32)) + (smallest_dist < Real::MAX).then_some((result, result_vox_id)) } } diff --git a/src/query/ray/mod.rs b/src/query/ray/mod.rs index 543dcae7..5ed735e6 100644 --- a/src/query/ray/mod.rs +++ b/src/query/ray/mod.rs @@ -9,6 +9,8 @@ pub use self::ray_support_map::local_ray_intersection_with_support_map_with_para pub use self::ray_triangle::local_ray_intersection_with_triangle; #[cfg(all(feature = "dim3", feature = "alloc"))] pub use self::ray_trimesh::RayCullingMode; +#[cfg(feature = "alloc")] +pub use self::ray_voxels::cast_local_ray_on_voxels; pub use self::simd_ray::SimdRay; #[doc(hidden)] diff --git a/src/query/ray/ray_voxels.rs b/src/query/ray/ray_voxels.rs index 4da55478..a1b1ded8 100644 --- a/src/query/ray/ray_voxels.rs +++ b/src/query/ray/ray_voxels.rs @@ -1,7 +1,119 @@ -use crate::math::{IVectorExt, Real, Vector, VectorExt}; +use crate::math::{IVector, IVectorExt, Real, Vector, VectorExt}; use crate::partitioning::BvhNode; use crate::query::{Ray, RayCast, RayIntersection}; -use crate::shape::{FeatureId, Voxels, VoxelsChunkRef}; +use crate::shape::{FeatureId, VoxelQuery, Voxels, VoxelsChunkRef}; + +/// Casts a ray on a voxel shape represented by any storage implementing [`VoxelQuery`]. +/// +/// This performs a DDA traversal of the voxels along the ray, across the shape's whole +/// [`domain`](VoxelQuery::domain), without relying on any acceleration structure. The concrete +/// [`Voxels`] shape implements [`RayCast`] with a faster traversal that skips empty space using +/// its internal BVH; this function is mostly useful for implementing ray-casting on custom +/// voxel storages. +/// +/// On a hit, the intersection's [`FeatureId`] is a face containing the hit voxel's +/// [`linear_id`](VoxelQuery::linear_id). +pub fn cast_local_ray_on_voxels( + voxels: &V, + ray: &Ray, + max_time_of_impact: Real, + solid: bool, +) -> Option { + use num_traits::Bounded; + + let aabb = voxels.local_aabb(); + let (min_t, mut max_t) = aabb.clip_ray_parameters(ray)?; + + #[cfg(feature = "dim2")] + let ii = [0, 1]; + #[cfg(feature = "dim3")] + let ii = [0, 1, 2]; + + if min_t > max_time_of_impact { + return None; + } + + let [domain_mins, domain_maxs] = voxels.domain(); + if domain_maxs.cmple(domain_mins).any() { + // Empty or degenerate domain. + return None; + } + + max_t = max_t.min(max_time_of_impact); + let clip_ray_a = ray.point_at(min_t); + let mut voxel_key = voxels + .voxel_at_point(clip_ray_a) + .clamp(domain_mins, domain_maxs - IVector::splat(1)); + + loop { + let aabb = voxels.voxel_aabb(voxel_key); + + if let Some(voxel) = voxels.voxel_state(voxel_key) { + if !voxel.is_empty() { + // We hit a voxel! + let hit = aabb.cast_local_ray_and_get_normal(ray, max_t, solid); + + if let Some(mut hit) = hit { + hit.feature = voxels + .linear_id(voxel_key) + .map_or(FeatureId::Unknown, FeatureId::Face); + return Some(hit); + } + } + } + + /* + * Find the next voxel to cast the ray on. + */ + let toi = ii.map(|i| { + if ray.dir.vget(i) > 0.0 { + let t = (aabb.maxs.vget(i) - ray.origin.vget(i)) / ray.dir.vget(i); + if t < 0.0 { + (Real::max_value(), true) + } else { + (t, true) + } + } else if ray.dir.vget(i) < 0.0 { + let t = (aabb.mins.vget(i) - ray.origin.vget(i)) / ray.dir.vget(i); + if t < 0.0 { + (Real::max_value(), false) + } else { + (t, false) + } + } else { + (Real::max_value(), false) + } + }); + + #[cfg(feature = "dim2")] + if toi[0].0 > max_t && toi[1].0 > max_t { + break; + } + + #[cfg(feature = "dim3")] + if toi[0].0 > max_t && toi[1].0 > max_t && toi[2].0 > max_t { + break; + } + + let imin = Vector::from(toi.map(|t| t.0)).min_position(); + + if toi[imin].1 { + if voxel_key.ivget(imin) < domain_maxs.ivget(imin) - 1 { + voxel_key.ivset(imin, voxel_key.ivget(imin) + 1); + } else { + // Leaving the shape's bounds. + break; + } + } else if voxel_key.ivget(imin) > domain_mins.ivget(imin) { + voxel_key.ivset(imin, voxel_key.ivget(imin) - 1); + } else { + // Leaving the shape’s bounds. + break; + } + } + + None +} impl RayCast for Voxels { #[inline] diff --git a/src/query/shape_cast/shape_cast_voxels_shape.rs b/src/query/shape_cast/shape_cast_voxels_shape.rs index baa8ada1..80aa53d8 100644 --- a/src/query/shape_cast/shape_cast_voxels_shape.rs +++ b/src/query/shape_cast/shape_cast_voxels_shape.rs @@ -1,18 +1,21 @@ use crate::math::{IVector, IVectorExt, Pose, Real, Vector, VectorExt}; use crate::query::{QueryDispatcher, ShapeCastHit, ShapeCastOptions}; -use crate::shape::{Cuboid, Shape, Voxels}; +use crate::shape::{Cuboid, Shape, VoxelQuery}; /// Time Of Impact of a voxels shape with any other shape, under a translational movement. -pub fn cast_shapes_voxels_shape( +/// +/// The voxels shape can be any voxel storage implementing [`VoxelQuery`]. +pub fn cast_shapes_voxels_shape( dispatcher: &D, pos12: &Pose, vel12: Vector, - g1: &Voxels, + g1: &V, g2: &dyn Shape, options: ShapeCastOptions, ) -> Option where D: ?Sized + QueryDispatcher, + V: ?Sized + VoxelQuery, { use num_traits::Bounded; @@ -133,17 +136,20 @@ where hit } -/// Time Of Impact of any shape with a composite shape, under a rigid motion (translation + rotation). -pub fn cast_shapes_shape_voxels( +/// Time Of Impact of any shape with a voxels shape, under a translational movement. +/// +/// The voxels shape can be any voxel storage implementing [`VoxelQuery`]. +pub fn cast_shapes_shape_voxels( dispatcher: &D, pos12: &Pose, vel12: Vector, g1: &dyn Shape, - g2: &Voxels, + g2: &V, options: ShapeCastOptions, ) -> Option where D: ?Sized + QueryDispatcher, + V: ?Sized + VoxelQuery, { cast_shapes_voxels_shape( dispatcher, diff --git a/src/shape/mod.rs b/src/shape/mod.rs index e6b95f01..4b33c877 100644 --- a/src/shape/mod.rs +++ b/src/shape/mod.rs @@ -22,7 +22,10 @@ pub use self::{ compound::Compound, polyline::Polyline, shared_shape::SharedShape, - voxels::{AxisMask, OctantPattern, VoxelData, VoxelState, VoxelType, Voxels, VoxelsChunkRef}, + voxels::{ + AxisMask, OctantPattern, VoxelData, VoxelQuery, VoxelState, VoxelType, Voxels, + VoxelsChunkRef, + }, }; // `PolylineFlags` is a 2D-only feature. diff --git a/src/shape/voxels/mod.rs b/src/shape/voxels/mod.rs index ea8b7c0a..24acf7f1 100644 --- a/src/shape/voxels/mod.rs +++ b/src/shape/voxels/mod.rs @@ -1,8 +1,10 @@ +pub use voxel_query::*; pub use voxels::*; pub use voxels_chunk::*; use voxels_consts::*; +mod voxel_query; mod voxels; mod voxels_chunk; mod voxels_consts; diff --git a/src/shape/voxels/voxel_query.rs b/src/shape/voxels/voxel_query.rs new file mode 100644 index 00000000..a0d33aed --- /dev/null +++ b/src/shape/voxels/voxel_query.rs @@ -0,0 +1,262 @@ +use crate::math::{ivect_to_vect, vect_to_ivect, IVector, Vector}; + +use crate::bounding_volume::Aabb; +use crate::shape::{VoxelData, VoxelState, Voxels}; + +/// Abstraction over the storage of a shape made of axis-aligned, uniformly sized voxels. +/// +/// Parry's voxel collision-detection algorithms (contact manifolds, intersection tests, +/// linear and nonlinear shape-casting, ray-casting, point projection, mass properties) are +/// written against this trait rather than against the concrete [`Voxels`] shape. Implementing +/// it for a custom sparse data-structure (chunked grid, octree, VDB-like tree, etc.) lets these +/// algorithms run directly on that structure without copying it into a [`Voxels`] shape, +/// typically by calling the generic query functions from a custom +/// [`QueryDispatcher`](crate::query::QueryDispatcher). +/// +/// # Grid conventions +/// +/// Voxels are identified by their integer grid coordinates `key`. The voxel with coordinates +/// `key` covers the world-space (well, shape-local-space) range +/// `[key * voxel_size, (key + 1) * voxel_size]`, so its center is at +/// `(key + 0.5) * voxel_size`. Grid ranges are always given as semi-open intervals +/// `[mins, maxs)`: `mins` is included, `maxs` is excluded. +/// +/// # Neighborhood states +/// +/// Each non-empty voxel must know which of its immediate axis-aligned neighbors are also +/// non-empty, exposed as a [`VoxelState`]. This is what allows collision-detection to avoid +/// hitting the "internal edges" between adjacent voxels. Implementors can either store this +/// information (like [`Voxels`] does, one byte per voxel), or derive it on the fly from +/// occupancy data using [`VoxelState::with_filled_neighbors`]. +/// +/// # Note for implementors +/// +/// This trait is not dyn-compatible (`voxels_in_range` returns `impl Iterator`). The generic +/// query functions are monomorphized for each storage type. To plug a custom storage into a +/// physics pipeline, wrap it in a type implementing [`Shape`](crate::shape::Shape) (typically +/// with [`ShapeType::Custom`](crate::shape::ShapeType::Custom)) and dispatch to the generic +/// voxel query functions from a custom `QueryDispatcher`. +/// +/// # Example +/// +/// Implementing `VoxelQuery` for a dense boolean grid, then running one of parry's generic +/// algorithms on it: +/// +/// ``` +/// # #[cfg(all(feature = "dim3", feature = "f32"))] { +/// use parry3d::mass_properties::MassProperties; +/// use parry3d::math::{IVector, Vector}; +/// use parry3d::shape::{AxisMask, VoxelData, VoxelQuery, VoxelState}; +/// +/// const N: i32 = 4; +/// +/// /// A dense 4×4×4 grid of voxels, each of size 1×1×1. +/// struct DenseGrid { +/// cells: [[[bool; 4]; 4]; 4], +/// } +/// +/// impl DenseGrid { +/// fn filled(&self, key: IVector) -> bool { +/// key.cmpge(IVector::ZERO).all() +/// && key.cmplt(IVector::splat(N)).all() +/// && self.cells[key.x as usize][key.y as usize][key.z as usize] +/// } +/// } +/// +/// impl VoxelQuery for DenseGrid { +/// fn voxel_size(&self) -> Vector { +/// Vector::splat(1.0) +/// } +/// +/// fn domain(&self) -> [IVector; 2] { +/// [IVector::ZERO, IVector::splat(N)] +/// } +/// +/// fn voxel_state(&self, key: IVector) -> Option { +/// if !self.filled(key) { +/// return Some(VoxelState::EMPTY); +/// } +/// +/// let mut mask = AxisMask::empty(); +/// if self.filled(key + IVector::new(1, 0, 0)) { mask |= AxisMask::X_POS; } +/// if self.filled(key - IVector::new(1, 0, 0)) { mask |= AxisMask::X_NEG; } +/// if self.filled(key + IVector::new(0, 1, 0)) { mask |= AxisMask::Y_POS; } +/// if self.filled(key - IVector::new(0, 1, 0)) { mask |= AxisMask::Y_NEG; } +/// if self.filled(key + IVector::new(0, 0, 1)) { mask |= AxisMask::Z_POS; } +/// if self.filled(key - IVector::new(0, 0, 1)) { mask |= AxisMask::Z_NEG; } +/// Some(VoxelState::with_filled_neighbors(mask)) +/// } +/// +/// fn linear_id(&self, key: IVector) -> Option { +/// self.filled(key) +/// .then(|| (key.x * N * N + key.y * N + key.z) as u32) +/// } +/// +/// fn voxels_in_range( +/// &self, +/// mins: IVector, +/// maxs: IVector, +/// ) -> impl Iterator { +/// let mins = mins.max(IVector::ZERO); +/// let maxs = maxs.min(IVector::splat(N)); +/// (mins.x..maxs.x).flat_map(move |x| { +/// (mins.y..maxs.y).flat_map(move |y| { +/// (mins.z..maxs.z).filter_map(move |z| { +/// let key = IVector::new(x, y, z); +/// let state = self.voxel_state(key)?; +/// (!state.is_empty()).then(|| VoxelData { +/// linear_id: self.linear_id(key).unwrap(), +/// grid_coords: key, +/// center: self.voxel_center(key), +/// state, +/// }) +/// }) +/// }) +/// }) +/// } +/// } +/// +/// let mut grid = DenseGrid { cells: [[[true; 4]; 4]; 4] }; +/// // Any of parry's generic voxel algorithms now runs on `DenseGrid` directly: +/// let props = MassProperties::from_voxels(1.0, &grid); +/// assert_eq!(props.mass(), 64.0); +/// # } +/// ``` +pub trait VoxelQuery { + /// The size of each voxel along each local coordinate axis. + fn voxel_size(&self) -> Vector; + + /// The semi-open range `[mins, maxs)` of grid coordinates covered by this shape. + /// + /// This must be a conservative bound: every non-empty voxel must lie within the returned + /// range, but the range may also cover empty voxels. + fn domain(&self) -> [IVector; 2]; + + /// The state of the voxel at the given grid coordinates. + /// + /// Both `None` and `Some(VoxelState::EMPTY)` designate an empty voxel; by convention, + /// `None` is returned when `key` falls outside of the storage's tracked domain. + fn voxel_state(&self, key: IVector) -> Option; + + /// A stable identifier of the voxel at the given grid coordinates. + /// + /// The identifier must be unique among the currently stored voxels and must match the + /// value of [`VoxelData::linear_id`] yielded by [`Self::voxels_in_range`] for the same + /// voxel. It is used to build [`FeatureId`](crate::shape::FeatureId)s in query results + /// and to match contact points across frames, so it should remain stable as long as the + /// shape isn't modified. Returns `None` if no identifier is associated to this + /// coordinate (e.g. empty voxel in unallocated storage). + fn linear_id(&self, key: IVector) -> Option; + + /// Iterates through the voxels within the given semi-open grid coordinate range. + /// + /// Implementations must yield every non-empty voxel with grid coordinates in + /// `[mins, maxs)` exactly once. They may additionally yield empty voxels within that + /// range (callers filter on [`VoxelData::state`]), but must never yield a voxel outside + /// of the range. + fn voxels_in_range(&self, mins: IVector, maxs: IVector) -> impl Iterator; + + /// Iterates through every voxel of this shape. + /// + /// This is equivalent to [`Self::voxels_in_range`] applied to the whole [`Self::domain`]. + fn voxels(&self) -> impl Iterator { + let [mins, maxs] = self.domain(); + self.voxels_in_range(mins, maxs) + } + + /// Iterates through every voxel intersecting the given local-space AABB. + fn voxels_intersecting_local_aabb(&self, aabb: &Aabb) -> impl Iterator { + let [mins, maxs] = self.voxel_range_intersecting_local_aabb(aabb); + self.voxels_in_range(mins, maxs) + } + + /// The grid coordinates of the voxel containing the given local-space point. + /// + /// The returned coordinates are valid regardless of whether the corresponding voxel + /// is filled, empty, or outside of [`Self::domain`]. + fn voxel_at_point(&self, point: Vector) -> IVector { + vect_to_ivect((point / self.voxel_size()).floor()) + } + + /// The local-space center of the voxel with the given grid coordinates. + fn voxel_center(&self, key: IVector) -> Vector { + (ivect_to_vect(key) + Vector::splat(0.5)) * self.voxel_size() + } + + /// The local-space AABB of the voxel with the given grid coordinates. + fn voxel_aabb(&self, key: IVector) -> Aabb { + let center = self.voxel_center(key); + Aabb::from_half_extents(center, self.voxel_size() / 2.0) + } + + /// The semi-open range of grid coordinates of the voxels intersecting the given AABB. + /// + /// The returned range covers both empty and non-empty voxels, and is not limited to the + /// bounds defined by [`Self::domain`]. + fn voxel_range_intersecting_local_aabb(&self, aabb: &Aabb) -> [IVector; 2] { + let mins = vect_to_ivect((aabb.mins / self.voxel_size()).floor()); + let maxs = vect_to_ivect((aabb.maxs / self.voxel_size()).ceil()); + [mins, maxs] + } + + /// The local-space AABB of the given semi-open range of voxel grid coordinates. + fn voxel_range_aabb(&self, mins: IVector, maxs: IVector) -> Aabb { + Aabb { + mins: ivect_to_vect(mins) * self.voxel_size(), + maxs: ivect_to_vect(maxs) * self.voxel_size(), + } + } + + /// Aligns the given AABB with the voxelized grid. + /// + /// The returned AABB has corners lying at the grid intersections (i.e. matches voxel + /// corners) and fully contains the input `aabb`. + fn align_aabb_to_grid(&self, aabb: &Aabb) -> Aabb { + let mins = (aabb.mins / self.voxel_size()).floor() * self.voxel_size(); + let maxs = (aabb.maxs / self.voxel_size()).ceil() * self.voxel_size(); + Aabb { mins, maxs } + } + + /// The local-space AABB of this voxels shape. + fn local_aabb(&self) -> Aabb { + let [mins, maxs] = self.domain(); + self.voxel_range_aabb(mins, maxs) + } +} + +impl VoxelQuery for Voxels { + #[inline] + fn voxel_size(&self) -> Vector { + self.voxel_size() + } + + #[inline] + fn domain(&self) -> [IVector; 2] { + self.domain() + } + + #[inline] + fn voxel_state(&self, key: IVector) -> Option { + self.voxel_state(key) + } + + #[inline] + fn linear_id(&self, key: IVector) -> Option { + self.linear_index(key).map(|id| id.flat_id() as u32) + } + + #[inline] + fn voxels_in_range(&self, mins: IVector, maxs: IVector) -> impl Iterator { + self.voxels_in_range(mins, maxs) + } + + #[inline] + fn voxels(&self) -> impl Iterator { + self.voxels() + } + + #[inline] + fn local_aabb(&self) -> Aabb { + self.local_aabb() + } +} diff --git a/src/shape/voxels/voxels.rs b/src/shape/voxels/voxels.rs index 892b9f50..43658efd 100644 --- a/src/shape/voxels/voxels.rs +++ b/src/shape/voxels/voxels.rs @@ -228,6 +228,41 @@ impl VoxelState { Self(state) } + /// The state of a **non-empty** voxel given the set of its non-empty axis-aligned neighbors. + /// + /// This is mostly useful for implementing [`VoxelQuery`](crate::shape::VoxelQuery) on a + /// custom voxel storage that only tracks per-voxel occupancy: the [`VoxelState`] of a filled + /// voxel is fully determined by which of its (up to 6 in 3D, 4 in 2D) immediate neighbors + /// along the coordinate axes are filled. + /// + /// Passing a mask with every axis direction set yields [`VoxelState::INTERIOR`]. Note that + /// the state of an *empty* voxel is always [`VoxelState::EMPTY`], regardless of its + /// neighborhood. + /// + /// # Example + /// + /// ``` + /// # #[cfg(all(feature = "dim3", feature = "f32"))] { + /// use parry3d::shape::{AxisMask, VoxelState, VoxelType}; + /// + /// // A voxel with filled neighbors in every direction is an interior voxel. + /// let state = VoxelState::with_filled_neighbors(AxisMask::all()); + /// assert_eq!(state, VoxelState::INTERIOR); + /// + /// // A voxel from a flat ground layer: neighbors on ±x and ±z, nothing above or below. + /// let state = VoxelState::with_filled_neighbors( + /// AxisMask::X_POS | AxisMask::X_NEG | AxisMask::Z_POS | AxisMask::Z_NEG, + /// ); + /// assert_eq!(state.voxel_type(), VoxelType::Face); + /// assert_eq!(state.free_faces(), AxisMask::Y_POS | AxisMask::Y_NEG); + /// # } + /// ``` + pub const fn with_filled_neighbors(filled_neighbors: AxisMask) -> Self { + // The `AxisMask` bits match the internal neighborhood bit layout: + // bit `2 * axis` is the positive direction, bit `2 * axis + 1` the negative one. + Self(filled_neighbors.bits()) + } + /// Is this voxel empty? pub const fn is_empty(self) -> bool { self.0 == EMPTY_FACE_MASK @@ -293,11 +328,13 @@ impl VoxelState { /// ``` #[derive(Copy, Clone, Debug, PartialEq)] pub struct VoxelData { - /// The temporary index in the internal voxels' storage. + /// A stable, storage-defined identifier of this voxel. /// - /// This index can be invalidated after a call to [`Voxels::set_voxel`], or - /// [`Voxels::crop`]. - pub linear_id: VoxelIndex, + /// For the [`Voxels`] shape this is the flattened form of [`Voxels::linear_index`]. + /// This identifier can be invalidated after the voxels shape is modified (e.g. by a call + /// to [`Voxels::set_voxel`], or [`Voxels::crop`]). + /// For stable references to voxels, always use `grid_coords`. + pub linear_id: u32, /// The voxel's integer grid coordinates. pub grid_coords: IVector, /// The voxel's center position in the local-space of the [`Voxels`] shape it is part of. diff --git a/src/shape/voxels/voxels_chunk.rs b/src/shape/voxels/voxels_chunk.rs index 47603f09..1266379a 100644 --- a/src/shape/voxels/voxels_chunk.rs +++ b/src/shape/voxels/voxels_chunk.rs @@ -259,7 +259,8 @@ impl<'a> VoxelsChunkRef<'a> { linear_id: VoxelIndex { chunk_id: self.my_id, id_in_chunk, - }, + } + .flat_id() as u32, grid_coords, center, state, @@ -303,7 +304,8 @@ impl<'a> VoxelsChunkRef<'a> { linear_id: VoxelIndex { chunk_id: self.my_id, id_in_chunk, - }, + } + .flat_id() as u32, grid_coords, center, state, From ead29fb74e5ffa9f9400f1899464e5dce8a30bfe Mon Sep 17 00:00:00 2001 From: 0xbeefd1ed Date: Sun, 6 Sep 2026 15:15:23 -0700 Subject: [PATCH 2/9] Add default impl for VoxelType --- src/shape/voxels/voxels.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/shape/voxels/voxels.rs b/src/shape/voxels/voxels.rs index 43658efd..36bee7c4 100644 --- a/src/shape/voxels/voxels.rs +++ b/src/shape/voxels/voxels.rs @@ -55,9 +55,10 @@ use alloc::{vec, vec::Vec}; /// println!("Voxel type: {:?}", voxel_type); /// # } /// ``` -#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)] pub enum VoxelType { /// The voxel is empty. + #[default] Empty, /// The voxel is a vertex if all three coordinate axis directions have at /// least one empty neighbor. From fff0af74a23a05dfd3080fec158020be355835b7 Mon Sep 17 00:00:00 2001 From: 0xbeefd1ed Date: Sun, 6 Sep 2026 15:18:47 -0700 Subject: [PATCH 3/9] Update VoxelQuery trait signature to return associated type --- .../geometry/voxel_query_custom_storage.rs | 369 ------------------ src/mass_properties/mass_properties_voxels.rs | 14 +- .../contact_manifolds_voxels_ball.rs | 9 +- ...ontact_manifolds_voxels_composite_shape.rs | 15 +- .../contact_manifolds_voxels_shape.rs | 29 +- .../contact_manifolds_voxels_voxels.rs | 30 +- .../intersection_test_voxels_shape.rs | 6 +- .../nonlinear_shape_cast_voxels_shape.rs | 6 +- src/query/point/point_voxels.rs | 20 +- src/query/ray/ray_voxels.rs | 25 +- .../shape_cast/shape_cast_voxels_shape.rs | 6 +- src/shape/mod.rs | 4 +- src/shape/voxels/voxel_query.rs | 215 +++++----- src/shape/voxels/voxels.rs | 41 +- src/shape/voxels/voxels_chunk.rs | 4 +- src/shape/voxels/voxels_edition.rs | 8 +- .../to_outline/voxels_to_outline.rs | 9 +- .../to_polyline/voxels_to_polyline.rs | 9 +- .../to_trimesh/voxels_to_trimesh.rs | 5 +- 19 files changed, 233 insertions(+), 591 deletions(-) delete mode 100644 crates/parry3d/tests/geometry/voxel_query_custom_storage.rs diff --git a/crates/parry3d/tests/geometry/voxel_query_custom_storage.rs b/crates/parry3d/tests/geometry/voxel_query_custom_storage.rs deleted file mode 100644 index ece4e50f..00000000 --- a/crates/parry3d/tests/geometry/voxel_query_custom_storage.rs +++ /dev/null @@ -1,369 +0,0 @@ -//! Checks that parry's voxel collision-detection algorithms, which are generic over the -//! [`VoxelQuery`] trait, produce the same results when running on a custom voxel storage -//! as when running on the built-in [`Voxels`] shape. - -use parry3d::mass_properties::MassProperties; -use parry3d::math::{IVector, Pose, Real, Vector}; -use parry3d::query::details; -use parry3d::query::{ - ContactManifold, DefaultQueryDispatcher, PointQuery, Ray, RayCast, ShapeCastOptions, -}; -use parry3d::shape::{Ball, Cuboid, Shape, VoxelData, VoxelQuery, VoxelState, Voxels}; -use std::collections::BTreeMap; - -/// A custom sparse voxel storage backed by a `BTreeMap`. -/// -/// It mirrors the content of a [`Voxels`] shape (including its linear ids) so that query -/// results are directly comparable, but shares none of its implementation. -struct BTreeVoxels { - voxel_size: Vector, - domain: [IVector; 2], - voxels: BTreeMap<[i32; 3], (VoxelState, u32)>, -} - -impl BTreeVoxels { - fn mirroring(voxels: &Voxels) -> Self { - let mut map = BTreeMap::new(); - for vox in voxels.voxels() { - if !vox.state.is_empty() { - map.insert( - [vox.grid_coords.x, vox.grid_coords.y, vox.grid_coords.z], - (vox.state, vox.linear_id), - ); - } - } - - Self { - voxel_size: voxels.voxel_size(), - domain: VoxelQuery::domain(voxels), - voxels: map, - } - } -} - -impl VoxelQuery for BTreeVoxels { - fn voxel_size(&self) -> Vector { - self.voxel_size - } - - fn domain(&self) -> [IVector; 2] { - self.domain - } - - fn voxel_state(&self, key: IVector) -> Option { - Some( - self.voxels - .get(&[key.x, key.y, key.z]) - .map(|(state, _)| *state) - .unwrap_or(VoxelState::EMPTY), - ) - } - - fn linear_id(&self, key: IVector) -> Option { - self.voxels.get(&[key.x, key.y, key.z]).map(|(_, id)| *id) - } - - fn voxels_in_range(&self, mins: IVector, maxs: IVector) -> impl Iterator { - self.voxels.iter().filter_map(move |(k, (state, id))| { - let key = IVector::new(k[0], k[1], k[2]); - (key.cmpge(mins).all() && key.cmplt(maxs).all()).then(|| VoxelData { - linear_id: *id, - grid_coords: key, - center: self.voxel_center(key), - state: *state, - }) - }) - } -} - -/// An 8×8 ground plate, a wall along one of its edges, and a disconnected lone voxel, -/// with non-uniform voxel sizes. -fn reference_shape() -> Voxels { - let mut keys = vec![]; - - for x in 0..8 { - for z in 0..8 { - keys.push(IVector::new(x, 0, z)); - } - } - - for y in 1..4 { - for z in 0..8 { - keys.push(IVector::new(0, y, z)); - } - } - - keys.push(IVector::new(10, 2, 3)); - - Voxels::new(Vector::new(1.0, 0.5, 0.75), &keys) -} - -fn fixtures() -> (Voxels, BTreeVoxels) { - let voxels = reference_shape(); - let custom = BTreeVoxels::mirroring(&voxels); - (voxels, custom) -} - -#[test] -fn custom_storage_matches_voxels_states() { - let (voxels, custom) = fixtures(); - let [mins, maxs] = VoxelQuery::domain(&voxels); - let margin = IVector::splat(2); - - let mut checked_non_empty = 0; - for x in mins.x - margin.x..maxs.x + margin.x { - for y in mins.y - margin.y..maxs.y + margin.y { - for z in mins.z - margin.z..maxs.z + margin.z { - let key = IVector::new(x, y, z); - let state1 = voxels.voxel_state(key).unwrap_or(VoxelState::EMPTY); - let state2 = VoxelQuery::voxel_state(&custom, key).unwrap_or(VoxelState::EMPTY); - assert_eq!(state1, state2, "state mismatch at {:?}", key); - - if !state1.is_empty() { - assert_eq!( - VoxelQuery::linear_id(&voxels, key), - VoxelQuery::linear_id(&custom, key), - "linear_id mismatch at {:?}", - key - ); - checked_non_empty += 1; - } - } - } - } - - // 8×8 plate + 3×8 wall + 1 lone voxel. - assert_eq!(checked_non_empty, 64 + 24 + 1); - assert_eq!(custom.voxels().count(), 64 + 24 + 1); -} - -#[test] -fn custom_storage_matches_voxels_mass_properties() { - let (voxels, custom) = fixtures(); - let density = 2.0; - let props1 = MassProperties::from_voxels(density, &voxels); - let props2 = MassProperties::from_voxels(density, &custom); - - // Absolute anchor: 89 voxels of volume 1.0 × 0.5 × 0.75. - let expected_mass = 89.0 * (1.0 * 0.5 * 0.75) * density; - assert_relative_eq!(props1.mass(), expected_mass, epsilon = 1.0e-4); - - assert_relative_eq!(props1.mass(), props2.mass(), epsilon = 1.0e-6); - assert_relative_eq!(props1.local_com, props2.local_com, epsilon = 1.0e-5); -} - -#[test] -fn custom_storage_matches_voxels_raycast() { - let (voxels, custom) = fixtures(); - - let mut origins = vec![]; - for i in 0..8 { - for j in 0..8 { - // Jittered origins above the shape (jitter avoids exact ties on voxel edges). - origins.push(Vector::new( - i as Real * 1.043 + 0.117, - 4.31, - j as Real * 0.921 + 0.083, - )); - } - } - - let dirs = [ - Vector::new(0.0231, -1.0, 0.0173), - Vector::new(-0.4173, -0.8317, 0.1531), - Vector::new(0.723, -0.317, -0.5911), - Vector::new(0.0731, 1.0, 0.0413), // Away from the shape: must miss. - ]; - - let mut num_hits = 0; - for origin in &origins { - for dir in &dirs { - let ray = Ray::new(*origin, *dir); - let hit1 = voxels.cast_local_ray_and_get_normal(&ray, 100.0, true); - let hit2 = details::cast_local_ray_on_voxels(&custom, &ray, 100.0, true); - - assert_eq!(hit1.is_some(), hit2.is_some(), "hit mismatch for {:?}", ray); - - if let (Some(hit1), Some(hit2)) = (hit1, hit2) { - num_hits += 1; - assert_relative_eq!(hit1.time_of_impact, hit2.time_of_impact, epsilon = 1.0e-5); - assert_relative_eq!(hit1.normal, hit2.normal, epsilon = 1.0e-5); - assert_eq!(hit1.feature, hit2.feature, "feature mismatch for {:?}", ray); - } - } - } - - // Sanity check: the straight-down rays from above the plate must all hit. - assert!(num_hits >= 64); - - // Absolute anchor: a ray straight above the plate hits its top at y = 0.5. - let ray = Ray::new(Vector::new(4.13, 4.0, 3.77), Vector::new(0.0, -1.0, 0.0)); - let hit = details::cast_local_ray_on_voxels(&custom, &ray, 100.0, true).unwrap(); - assert_relative_eq!(hit.time_of_impact, 4.0 - 0.5, epsilon = 1.0e-5); - assert_relative_eq!(hit.normal, Vector::new(0.0, 1.0, 0.0), epsilon = 1.0e-5); -} - -#[test] -fn custom_storage_matches_voxels_point_projection() { - let (voxels, custom) = fixtures(); - - let mut points = vec![]; - for i in -2..12 { - for j in -2..6 { - for k in -2..10 { - points.push(Vector::new( - i as Real * 1.117 + 0.031, - j as Real * 0.617 + 0.043, - k as Real * 0.917 + 0.021, - )); - } - } - } - - for solid in [true, false] { - for pt in &points { - let proj1 = voxels.project_local_point(*pt, solid); - let proj2 = details::project_local_point_on_voxels(&custom, *pt, solid) - .expect("the shape is not empty") - .0; - - assert_eq!( - proj1.is_inside, proj2.is_inside, - "is_inside mismatch at {:?} (solid: {})", - pt, solid - ); - assert_relative_eq!(proj1.point, proj2.point, epsilon = 1.0e-4); - } - } -} - -type TestManifold = ContactManifold<(), ()>; - -fn compare_manifolds(manifolds1: &mut [TestManifold], manifolds2: &mut [TestManifold]) { - assert_eq!(manifolds1.len(), manifolds2.len()); - - // The two backends iterate voxels in a different order, so match manifolds by - // their subshape ids. - let sort_key = |m: &TestManifold| (m.subshape1, m.subshape2); - manifolds1.sort_by_key(sort_key); - manifolds2.sort_by_key(sort_key); - - for (m1, m2) in manifolds1.iter().zip(manifolds2.iter()) { - assert_eq!(m1.subshape1, m2.subshape1); - assert_eq!(m1.subshape2, m2.subshape2); - assert_eq!(m1.points.len(), m2.points.len()); - - if !m1.points.is_empty() { - assert_relative_eq!(m1.local_n1, m2.local_n1, epsilon = 1.0e-5); - } - - for (pt1, pt2) in m1.points.iter().zip(m2.points.iter()) { - assert_relative_eq!(pt1.dist, pt2.dist, epsilon = 1.0e-5); - assert_relative_eq!(pt1.local_p1, pt2.local_p1, epsilon = 1.0e-4); - assert_relative_eq!(pt1.local_p2, pt2.local_p2, epsilon = 1.0e-4); - } - } -} - -#[test] -fn custom_storage_matches_voxels_contact_manifolds() { - let (voxels, custom) = fixtures(); - let dispatcher = DefaultQueryDispatcher; - let cuboid = Cuboid::new(Vector::new(0.4, 0.6, 0.5)); - let prediction = 0.05; - - let poses = [ - // Resting on the plate, slightly penetrating. - Pose::translation(3.13, 0.5 + 0.6 - 0.02, 4.21), - // Touching both the plate and the wall. - Pose::translation(1.0 + 0.4 - 0.01, 0.5 + 0.6 - 0.01, 3.87), - // Hovering within prediction distance. - Pose::translation(5.11, 0.5 + 0.6 + 0.03, 2.93), - // Overlapping the lone voxel. - Pose::translation(10.5, 1.3, 2.71), - // Far away: no contacts at all. - Pose::translation(20.0, 10.0, 20.0), - ]; - - let mut total_points = 0; - for pos12 in &poses { - let mut manifolds1 = Vec::::new(); - let mut manifolds2 = Vec::::new(); - let mut workspace1 = None; - let mut workspace2 = None; - - details::contact_manifolds_voxels_shape( - &dispatcher, - pos12, - &voxels, - &cuboid as &dyn Shape, - prediction, - &mut manifolds1, - &mut workspace1, - false, - ); - details::contact_manifolds_voxels_shape( - &dispatcher, - pos12, - &custom, - &cuboid as &dyn Shape, - prediction, - &mut manifolds2, - &mut workspace2, - false, - ); - - compare_manifolds(&mut manifolds1, &mut manifolds2); - total_points += manifolds1.iter().map(|m| m.points.len()).sum::(); - } - - // Sanity check: at least the resting/touching poses must have produced actual contacts. - assert!(total_points > 0); -} - -#[test] -fn custom_storage_matches_voxels_shape_cast() { - let (voxels, custom) = fixtures(); - let dispatcher = DefaultQueryDispatcher; - let ball = Ball::new(0.3); - let pos12 = Pose::translation(4.05, 3.0, 4.1); - let vel12 = Vector::new(0.0, -1.0, 0.0); - let options = ShapeCastOptions::default(); - - let hit1 = - details::cast_shapes_voxels_shape(&dispatcher, &pos12, vel12, &voxels, &ball, options); - let hit2 = - details::cast_shapes_voxels_shape(&dispatcher, &pos12, vel12, &custom, &ball, options); - - let hit1 = hit1.expect("the ball must hit the plate"); - let hit2 = hit2.expect("the ball must hit the plate"); - - // Absolute anchor: the ball surface reaches the plate's top (y = 0.5) after - // travelling 3.0 - 0.5 - 0.3 units. - assert_relative_eq!(hit1.time_of_impact, 3.0 - 0.5 - 0.3, epsilon = 1.0e-4); - - assert_relative_eq!(hit1.time_of_impact, hit2.time_of_impact, epsilon = 1.0e-5); - assert_relative_eq!(hit1.normal1, hit2.normal1, epsilon = 1.0e-4); - assert_relative_eq!(hit1.witness1, hit2.witness1, epsilon = 1.0e-4); -} - -#[test] -fn custom_storage_matches_voxels_intersection_test() { - let (voxels, custom) = fixtures(); - let dispatcher = DefaultQueryDispatcher; - let cuboid = Cuboid::new(Vector::new(0.4, 0.6, 0.5)); - - let poses = [ - (Pose::translation(3.13, 0.7, 4.21), true), // Overlapping the plate. - (Pose::translation(10.5, 1.3, 2.71), true), // Overlapping the lone voxel. - (Pose::translation(4.0, 5.0, 4.0), false), // Above everything. - (Pose::translation(20.0, 0.0, 20.0), false), // Far away. - ]; - - for (pos12, expected) in &poses { - let hit1 = details::intersection_test_voxels_shape(&dispatcher, pos12, &voxels, &cuboid); - let hit2 = details::intersection_test_voxels_shape(&dispatcher, pos12, &custom, &cuboid); - assert_eq!(hit1, *expected); - assert_eq!(hit2, *expected); - } -} diff --git a/src/mass_properties/mass_properties_voxels.rs b/src/mass_properties/mass_properties_voxels.rs index fcf01b55..d4ac8bd7 100644 --- a/src/mass_properties/mass_properties_voxels.rs +++ b/src/mass_properties/mass_properties_voxels.rs @@ -2,7 +2,7 @@ use crate::mass_properties::MassProperties; #[cfg(feature = "dim3")] use crate::math::Matrix; use crate::math::{Real, Vector}; -use crate::shape::VoxelQuery; +use crate::shape::{QueriedVoxel, VoxelQuery, VoxelType}; impl MassProperties { /// Computes the mass properties of a voxel grid. @@ -164,7 +164,9 @@ impl MassProperties { /// /// - Only non-empty voxels contribute to mass /// - Empty voxels are ignored (zero mass, no inertia) - /// - The voxel state is checked using `vox.state.is_empty()` + /// - A voxel is considered empty if its + /// [`QueriedVoxel::voxel_type`](crate::shape::QueriedVoxel::voxel_type) is + /// [`VoxelType::Empty`] /// /// # See Also /// @@ -182,8 +184,8 @@ impl MassProperties { let block_ref_mprops = MassProperties::from_cuboid(density, voxels.voxel_size() / 2.0); for vox in voxels.voxels() { - if !vox.state.is_empty() { - com += vox.center; + if vox.voxel_type() != VoxelType::Empty { + com += vox.center(); num_not_empty += 1; } } @@ -191,9 +193,9 @@ impl MassProperties { com /= num_not_empty as Real; for vox in voxels.voxels() { - if !vox.state.is_empty() { + if vox.voxel_type() != VoxelType::Empty { angular_inertia += - block_ref_mprops.construct_shifted_inertia_matrix(vox.center - com); + block_ref_mprops.construct_shifted_inertia_matrix(vox.center() - com); } } diff --git a/src/query/contact_manifolds/contact_manifolds_voxels_ball.rs b/src/query/contact_manifolds/contact_manifolds_voxels_ball.rs index 99b7ca06..291dfbb3 100644 --- a/src/query/contact_manifolds/contact_manifolds_voxels_ball.rs +++ b/src/query/contact_manifolds/contact_manifolds_voxels_ball.rs @@ -2,7 +2,8 @@ use crate::bounding_volume::BoundingVolume; use crate::math::{Pose, Real, Vector, VectorExt}; use crate::query::{ContactManifold, PointQuery, TrackedContact}; use crate::shape::{ - Ball, Cuboid, OctantPattern, PackedFeatureId, Shape, VoxelQuery, VoxelState, VoxelType, + Ball, Cuboid, OctantPattern, PackedFeatureId, QueriedVoxel, Shape, VoxelQuery, VoxelState, + VoxelType, }; use alloc::vec::Vec; @@ -58,7 +59,7 @@ pub fn contact_manifolds_voxels_ball<'a, ManifoldData, ContactData, V>( let aabb2 = ball2.aabb(pos12).loosened(prediction / 2.0); if let Some(aabb_intersection) = aabb1.intersection(&aabb2) { for vox1 in voxels1.voxels_intersecting_local_aabb(&aabb_intersection) { - match vox1.state.voxel_type() { + match vox1.voxel_type() { #[cfg(feature = "dim2")] VoxelType::Vertex | VoxelType::Face => { /* Ok */ } #[cfg(feature = "dim3")] @@ -68,9 +69,9 @@ pub fn contact_manifolds_voxels_ball<'a, ManifoldData, ContactData, V>( detect_hit_voxel_ball( *pos12, - vox1.center, + vox1.center(), radius1, - vox1.state, + vox1.voxel_state(), center2, radius2, prediction, diff --git a/src/query/contact_manifolds/contact_manifolds_voxels_composite_shape.rs b/src/query/contact_manifolds/contact_manifolds_voxels_composite_shape.rs index 14d832fe..ca74a58b 100644 --- a/src/query/contact_manifolds/contact_manifolds_voxels_composite_shape.rs +++ b/src/query/contact_manifolds/contact_manifolds_voxels_composite_shape.rs @@ -7,7 +7,9 @@ use crate::query::{ ContactManifold, ContactManifoldsWorkspace, PersistentQueryDispatcher, PointQuery, TypedWorkspaceData, WorkspaceData, }; -use crate::shape::{CompositeShape, Cuboid, Shape, SupportMap, VoxelQuery, VoxelType}; +use crate::shape::{ + CompositeShape, Cuboid, QueriedVoxel, Shape, SupportMap, VoxelQuery, VoxelType, +}; use crate::utils::hashmap::Entry; use crate::utils::PoseOpt; use alloc::{boxed::Box, vec::Vec}; @@ -92,7 +94,7 @@ pub fn contact_manifolds_voxels_composite_shape( if let Some(intersection_aabb1) = aabb1.intersection(&aabb2_1) { for vox1 in voxels1.voxels_intersecting_local_aabb(&intersection_aabb1) { - let vox_type1 = vox1.state.voxel_type(); + let vox_type1 = vox1.voxel_type(); // TODO: would be nice to have a strategy to handle interior voxels for depenetration. if vox_type1 == VoxelType::Empty || vox_type1 == VoxelType::Interior { @@ -138,7 +140,7 @@ pub fn contact_manifolds_voxels_composite_shape( timestamp: new_timestamp, }; - let vox_id = vox1.linear_id; + let vox_id = vox1.linear_id(); let (id1, id2) = if flipped { (leaf2, vox_id) } else { @@ -240,7 +242,8 @@ pub fn contact_manifolds_voxels_composite_shape( // interior of the infinitely expanded canonical shape by checking if // the opposite normal had led to a better vector. let cuboid1 = Cuboid::new(radius1); - let sp1 = cuboid1.local_support_point(-penetration_dir1) + vox1.center; + let sp1 = + cuboid1.local_support_point(-penetration_dir1) + vox1.center(); let sm2 = part_shape2 .as_support_map() .expect("Unsupported collision pair."); @@ -257,9 +260,9 @@ pub fn contact_manifolds_voxels_composite_shape( } let pt_in_voxel_space = if flipped { - manifold.subshape_pos2().transform_point(pt.local_p2) - vox1.center + manifold.subshape_pos2().transform_point(pt.local_p2) - vox1.center() } else { - manifold.subshape_pos1().transform_point(pt.local_p1) - vox1.center + manifold.subshape_pos1().transform_point(pt.local_p1) - vox1.center() }; sub_detector.selected_contacts |= (test_voxel.contains_local_point(pt_in_voxel_space) as u32) << i; diff --git a/src/query/contact_manifolds/contact_manifolds_voxels_shape.rs b/src/query/contact_manifolds/contact_manifolds_voxels_shape.rs index f1131681..71a6d933 100644 --- a/src/query/contact_manifolds/contact_manifolds_voxels_shape.rs +++ b/src/query/contact_manifolds/contact_manifolds_voxels_shape.rs @@ -4,7 +4,7 @@ use crate::query::{ ContactManifold, ContactManifoldsWorkspace, PersistentQueryDispatcher, PointQuery, TypedWorkspaceData, WorkspaceData, }; -use crate::shape::{AxisMask, Cuboid, Shape, SupportMap, VoxelData, VoxelQuery, VoxelType}; +use crate::shape::{AxisMask, Cuboid, QueriedVoxel, Shape, SupportMap, VoxelQuery, VoxelType}; use crate::utils::hashmap::{Entry, HashMap}; use crate::utils::PoseOpt; use alloc::{boxed::Box, vec::Vec}; @@ -151,7 +151,7 @@ pub fn contact_manifolds_voxels_shape( if let Some(intersection_aabb1) = aabb1.intersection(&aabb2_1) { for vox1 in voxels1.voxels_intersecting_local_aabb(&intersection_aabb1) { - let vox_type1 = vox1.state.voxel_type(); + let vox_type1 = vox1.voxel_type(); // TODO: would be nice to have a strategy to handle interior voxels for depenetration. if vox_type1 == VoxelType::Empty || vox_type1 == VoxelType::Interior { @@ -188,7 +188,7 @@ pub fn contact_manifolds_voxels_shape( timestamp: new_timestamp, }; - let vid = vox1.linear_id; + let vid = vox1.linear_id(); let (id1, id2) = if flipped { (0, vid) } else { (vid, 0) }; manifolds.push(ContactManifold::with_data( id1, @@ -285,7 +285,7 @@ pub fn contact_manifolds_voxels_shape( // interior of the infinitely expanded canonical shape by checking if // the opposite normal had led to a better vector. let cuboid1 = Cuboid::new(radius1); - let sp1 = cuboid1.local_support_point(-penetration_dir1) + vox1.center; + let sp1 = cuboid1.local_support_point(-penetration_dir1) + vox1.center(); let sm2 = shape2 .as_support_map() .expect("Unsupported collision pair."); @@ -301,9 +301,9 @@ pub fn contact_manifolds_voxels_shape( } let pt_in_voxel_space = if flipped { - manifold.subshape_pos2().transform_point(pt.local_p2) - vox1.center + manifold.subshape_pos2().transform_point(pt.local_p2) - vox1.center() } else { - manifold.subshape_pos1().transform_point(pt.local_p1) - vox1.center + manifold.subshape_pos1().transform_point(pt.local_p1) - vox1.center() }; sub_detector.selected_contacts |= (test_voxel.contains_local_point(pt_in_voxel_space) as u32) << i; @@ -337,8 +337,11 @@ pub(crate) struct CanonicalVoxelShape { } impl CanonicalVoxelShape { - pub fn from_voxel(voxels: &V, vox: &VoxelData) -> Self { - let mut key_low = vox.grid_coords; + pub fn from_voxel<'a, 'b, V: ?Sized + VoxelQuery>( + voxels: &V, + vox: &'a impl QueriedVoxel<'b>, + ) -> Self { + let mut key_low = vox.grid_coords(); let mut key_high = key_low; // NOTE: the mins/maxs here are offset by 1 so we can expand past the last voxel if it @@ -347,7 +350,7 @@ impl CanonicalVoxelShape { let mins = voxels.domain()[0] - IVector::splat(1); let maxs = voxels.domain()[1]; let counts = maxs - mins; - let mask1 = vox.state.free_faces(); + let mask1 = vox.voxel_state().free_faces(); let adjust_canon = |axis: AxisMask, i: usize, key: &mut IVector, val: Int| { if !mask1.contains(axis) { @@ -384,10 +387,10 @@ impl CanonicalVoxelShape { } } - pub fn cuboid( + pub fn cuboid<'a, 'b, V: ?Sized + VoxelQuery>( &self, voxels: &V, - vox: &VoxelData, + vox: &'a impl QueriedVoxel<'b>, domain2_1: Aabb, ) -> (Vector, Cuboid) { let radius = voxels.voxel_size() / 2.0; @@ -395,11 +398,11 @@ impl CanonicalVoxelShape { let mut canonical_maxs = voxels.voxel_center(self.range[1]); for k in 0..DIM { - if self.range[0].ivget(k) != vox.grid_coords.ivget(k) { + if self.range[0].ivget(k) != vox.grid_coords().ivget(k) { canonical_mins.vset(k, canonical_mins.vget(k).max(domain2_1.mins.vget(k))); } - if self.range[1].ivget(k) != vox.grid_coords.ivget(k) { + if self.range[1].ivget(k) != vox.grid_coords().ivget(k) { canonical_maxs.vset(k, canonical_maxs.vget(k).min(domain2_1.maxs.vget(k))); } } diff --git a/src/query/contact_manifolds/contact_manifolds_voxels_voxels.rs b/src/query/contact_manifolds/contact_manifolds_voxels_voxels.rs index e3c6b5b0..96a68c85 100644 --- a/src/query/contact_manifolds/contact_manifolds_voxels_voxels.rs +++ b/src/query/contact_manifolds/contact_manifolds_voxels_voxels.rs @@ -6,7 +6,7 @@ use crate::query::{ ContactManifold, ContactManifoldsWorkspace, PersistentQueryDispatcher, PointQuery, TypedWorkspaceData, WorkspaceData, }; -use crate::shape::{Cuboid, Shape, SupportMap, VoxelData, VoxelQuery, VoxelType}; +use crate::shape::{Cuboid, QueriedVoxel, Shape, SupportMap, VoxelQuery, VoxelType}; use crate::utils::hashmap::Entry; use crate::utils::PoseOpt; use alloc::{boxed::Box, vec::Vec}; @@ -98,8 +98,8 @@ pub fn contact_manifolds_voxels_voxels<'a, ManifoldData, ContactData, V1, V2>( let mut detect_hit = |canon1: CanonicalVoxelShape, canon2: CanonicalVoxelShape, - vox1: &VoxelData, - vox2: &VoxelData| { + vox1: &V1::Voxel<'_>, + vox2: &V2::Voxel<'_>| { // Compute canonical shapes and dispatch. let workspace_key = [ canon1.workspace_key[0], @@ -139,8 +139,8 @@ pub fn contact_manifolds_voxels_voxels<'a, ManifoldData, ContactData, V1, V2>( }; manifolds.push(ContactManifold::with_data( - vox1.linear_id, - vox2.linear_id, + vox1.linear_id(), + vox2.linear_id(), ManifoldData::default(), )); @@ -214,9 +214,9 @@ pub fn contact_manifolds_voxels_voxels<'a, ManifoldData, ContactData, V1, V2>( // the opposite normal had led to a better vector. let cuboid1 = Cuboid::new(radius1); let cuboid2 = Cuboid::new(radius2); - let sp1 = cuboid1.local_support_point(-penetration_dir1) + vox1.center; + let sp1 = cuboid1.local_support_point(-penetration_dir1) + vox1.center(); let sp2 = cuboid2.support_point( - &(pos12 * Pose::from_translation(vox2.center)), + &(pos12 * Pose::from_translation(vox2.center())), penetration_dir1, ); let test_dist = (sp2 - sp1).dot(-penetration_dir1); @@ -230,9 +230,9 @@ pub fn contact_manifolds_voxels_voxels<'a, ManifoldData, ContactData, V1, V2>( } let pt_in_voxel_space1 = - manifold.subshape_pos1().transform_point(pt.local_p1) - vox1.center; + manifold.subshape_pos1().transform_point(pt.local_p1) - vox1.center(); let pt_in_voxel_space2 = - manifold.subshape_pos2().transform_point(pt.local_p2) - vox2.center; + manifold.subshape_pos2().transform_point(pt.local_p2) - vox2.center(); sub_detector.selected_contacts |= ((test_voxel1.contains_local_point(pt_in_voxel_space1) as u32) << i) & ((test_voxel2.contains_local_point(pt_in_voxel_space2) as u32) << i); @@ -240,7 +240,7 @@ pub fn contact_manifolds_voxels_voxels<'a, ManifoldData, ContactData, V1, V2>( }; for vox1 in voxels1.voxels_intersecting_local_aabb(&intersection_aabb1) { - let type1 = vox1.state.voxel_type(); + let type1 = vox1.voxel_type(); match type1 { #[cfg(feature = "dim2")] VoxelType::Vertex => { /* Ok */ } @@ -251,10 +251,10 @@ pub fn contact_manifolds_voxels_voxels<'a, ManifoldData, ContactData, V1, V2>( let canon1 = CanonicalVoxelShape::from_voxel(voxels1, &vox1); let centered_aabb1_2 = Cuboid::new(radius1 + Vector::splat(prediction)) - .compute_aabb(&(pos21 * Pose::from_translation(vox1.center))); + .compute_aabb(&(pos21 * Pose::from_translation(vox1.center()))); for vox2 in voxels2.voxels_intersecting_local_aabb(¢ered_aabb1_2) { - let type2 = vox2.state.voxel_type(); + let type2 = vox2.voxel_type(); #[cfg(feature = "dim2")] match (type1, type2) { @@ -281,7 +281,7 @@ pub fn contact_manifolds_voxels_voxels<'a, ManifoldData, ContactData, V1, V2>( } for vox2 in voxels2.voxels_intersecting_local_aabb(&intersection_aabb2) { - let type2 = vox2.state.voxel_type(); + let type2 = vox2.voxel_type(); match type2 { #[cfg(feature = "dim2")] VoxelType::Vertex => { /* Ok */ } @@ -292,10 +292,10 @@ pub fn contact_manifolds_voxels_voxels<'a, ManifoldData, ContactData, V1, V2>( let canon2 = CanonicalVoxelShape::from_voxel(voxels2, &vox2); let centered_aabb2_1 = Cuboid::new(radius2 + Vector::splat(prediction)) - .compute_aabb(&(pos12 * Pose::from_translation(vox2.center))); + .compute_aabb(&(pos12 * Pose::from_translation(vox2.center()))); for vox1 in voxels1.voxels_intersecting_local_aabb(¢ered_aabb2_1) { - let type1 = vox1.state.voxel_type(); + let type1 = vox1.voxel_type(); #[cfg(feature = "dim2")] match (type1, type2) { diff --git a/src/query/intersection_test/intersection_test_voxels_shape.rs b/src/query/intersection_test/intersection_test_voxels_shape.rs index 362850d1..9ed63b39 100644 --- a/src/query/intersection_test/intersection_test_voxels_shape.rs +++ b/src/query/intersection_test/intersection_test_voxels_shape.rs @@ -1,6 +1,6 @@ use crate::math::Pose; use crate::query::PersistentQueryDispatcher; -use crate::shape::{Cuboid, Shape, VoxelQuery, VoxelType}; +use crate::shape::{Cuboid, QueriedVoxel, Shape, VoxelQuery, VoxelType}; /// Checks for any intersection between voxels and an arbitrary shape, both represented as a `Shape` trait-object. pub fn intersection_test_voxels_shape_shapes( @@ -33,13 +33,13 @@ pub fn intersection_test_voxels_shape( if let Some(intersection_aabb1) = aabb1.intersection(&aabb2_1) { for vox1 in voxels1.voxels_intersecting_local_aabb(&intersection_aabb1) { - let vox_type1 = vox1.state.voxel_type(); + let vox_type1 = vox1.voxel_type(); if vox_type1 == VoxelType::Empty { continue; } - let center1 = vox1.center; + let center1 = vox1.center(); let cuboid1 = Cuboid::new(radius1); let cuboid_pose12 = Pose::from_translation(-center1) * pos12; diff --git a/src/query/nonlinear_shape_cast/nonlinear_shape_cast_voxels_shape.rs b/src/query/nonlinear_shape_cast/nonlinear_shape_cast_voxels_shape.rs index 565cd491..b2961c64 100644 --- a/src/query/nonlinear_shape_cast/nonlinear_shape_cast_voxels_shape.rs +++ b/src/query/nonlinear_shape_cast/nonlinear_shape_cast_voxels_shape.rs @@ -1,7 +1,7 @@ use crate::bounding_volume::BoundingVolume; use crate::math::{IVector, IVectorExt, Real, Vector, VectorExt}; use crate::query::{NonlinearRigidMotion, QueryDispatcher, ShapeCastHit}; -use crate::shape::{Cuboid, Shape, VoxelQuery}; +use crate::shape::{Cuboid, QueriedVoxel, Shape, VoxelQuery, VoxelType}; /// Time Of Impact of a voxels shape with any other shape, under a rigid motion (translation + rotation). /// @@ -61,9 +61,9 @@ where let mut check_voxels_in_range = |search_domain: [IVector; 2]| { for vox in g1.voxels_in_range(search_domain[0], search_domain[1]) { - if !vox.state.is_empty() { + if vox.voxel_type() != VoxelType::Empty { // PERF: could we check the canonical shape instead, and deduplicate accordingly? - let center = g1.voxel_center(vox.grid_coords); + let center = g1.voxel_center(vox.grid_coords()); let cuboid = Cuboid::new(g1.voxel_size() / 2.0); let vox_motion1 = motion1.prepend_translation(center); if let Some(new_hit) = dispatcher diff --git a/src/query/point/point_voxels.rs b/src/query/point/point_voxels.rs index 61de069c..75525f54 100644 --- a/src/query/point/point_voxels.rs +++ b/src/query/point/point_voxels.rs @@ -1,7 +1,9 @@ use crate::bounding_volume::{Aabb, BoundingVolume}; use crate::math::{Real, Vector}; use crate::query::{PointProjection, PointQuery}; -use crate::shape::{Cuboid, FeatureId, VoxelQuery, Voxels, VoxelsChunkRef}; +use crate::shape::{ + Cuboid, FeatureId, QueriedVoxel, VoxelQuery, VoxelType, Voxels, VoxelsChunkRef, +}; /// Projects a point on a voxel shape represented by any storage implementing [`VoxelQuery`]. /// @@ -28,8 +30,8 @@ pub fn project_local_point_on_voxels( let key_at_pt = voxels.voxel_at_point(pt); if solid && voxels - .voxel_state(key_at_pt) - .is_some_and(|state| !state.is_empty()) + .voxel(key_at_pt) + .is_some_and(|vox| vox.voxel_type() != VoxelType::Empty) { return Some(( PointProjection::new(true, pt), @@ -49,16 +51,16 @@ pub fn project_local_point_on_voxels( let mut best_dist = Real::MAX; for vox in voxels.voxels_intersecting_local_aabb(&search_aabb) { - if vox.state.is_empty() { + if vox.voxel_type() == VoxelType::Empty { continue; } - let mut candidate = base_cuboid.project_local_point(pt - vox.center, solid); - candidate.point += vox.center; + let mut candidate = base_cuboid.project_local_point(pt - vox.center(), solid); + candidate.point += vox.center(); let candidate_dist = (candidate.point - pt).length(); if candidate_dist < best_dist { - best = Some((candidate, vox.linear_id)); + best = Some((candidate, vox.linear_id())); best_dist = candidate_dist; } } @@ -132,8 +134,8 @@ impl<'a> VoxelsChunkRef<'a> { let mut result_vox_id = 0; for vox in self.voxels() { - let mut candidate = base_cuboid.project_local_point(pt - vox.center, solid); - candidate.point += vox.center; + let mut candidate = base_cuboid.project_local_point(pt - vox.center(), solid); + candidate.point += vox.center(); let candidate_dist = (candidate.point - pt).length(); if candidate_dist < smallest_dist { diff --git a/src/query/ray/ray_voxels.rs b/src/query/ray/ray_voxels.rs index a1b1ded8..5da2b245 100644 --- a/src/query/ray/ray_voxels.rs +++ b/src/query/ray/ray_voxels.rs @@ -1,7 +1,7 @@ use crate::math::{IVector, IVectorExt, Real, Vector, VectorExt}; use crate::partitioning::BvhNode; use crate::query::{Ray, RayCast, RayIntersection}; -use crate::shape::{FeatureId, VoxelQuery, Voxels, VoxelsChunkRef}; +use crate::shape::{FeatureId, QueriedVoxel, VoxelQuery, VoxelType, Voxels, VoxelsChunkRef}; /// Casts a ray on a voxel shape represented by any storage implementing [`VoxelQuery`]. /// @@ -48,17 +48,18 @@ pub fn cast_local_ray_on_voxels( loop { let aabb = voxels.voxel_aabb(voxel_key); - if let Some(voxel) = voxels.voxel_state(voxel_key) { - if !voxel.is_empty() { - // We hit a voxel! - let hit = aabb.cast_local_ray_and_get_normal(ray, max_t, solid); - - if let Some(mut hit) = hit { - hit.feature = voxels - .linear_id(voxel_key) - .map_or(FeatureId::Unknown, FeatureId::Face); - return Some(hit); - } + if voxels + .voxel(voxel_key) + .is_some_and(|vox| vox.voxel_type() != VoxelType::Empty) + { + // We hit a voxel! + let hit = aabb.cast_local_ray_and_get_normal(ray, max_t, solid); + + if let Some(mut hit) = hit { + hit.feature = voxels + .linear_id(voxel_key) + .map_or(FeatureId::Unknown, FeatureId::Face); + return Some(hit); } } diff --git a/src/query/shape_cast/shape_cast_voxels_shape.rs b/src/query/shape_cast/shape_cast_voxels_shape.rs index 80aa53d8..55ce67e6 100644 --- a/src/query/shape_cast/shape_cast_voxels_shape.rs +++ b/src/query/shape_cast/shape_cast_voxels_shape.rs @@ -1,6 +1,6 @@ use crate::math::{IVector, IVectorExt, Pose, Real, Vector, VectorExt}; use crate::query::{QueryDispatcher, ShapeCastHit, ShapeCastOptions}; -use crate::shape::{Cuboid, Shape, VoxelQuery}; +use crate::shape::{Cuboid, QueriedVoxel, Shape, VoxelQuery, VoxelType}; /// Time Of Impact of a voxels shape with any other shape, under a translational movement. /// @@ -25,9 +25,9 @@ where let mut check_voxels_in_range = |search_domain: [IVector; 2]| { for vox in g1.voxels_in_range(search_domain[0], search_domain[1]) { - if !vox.state.is_empty() { + if vox.voxel_type() != VoxelType::Empty { // PERF: could we check the canonical shape instead, and deduplicate accordingly? - let center = g1.voxel_center(vox.grid_coords); + let center = g1.voxel_center(vox.grid_coords()); let cuboid = Cuboid::new(g1.voxel_size() / 2.0); let vox_pos12 = Pose::from_translation(center).inverse() * pos12; if let Some(mut new_hit) = dispatcher diff --git a/src/shape/mod.rs b/src/shape/mod.rs index 4b33c877..62c169b7 100644 --- a/src/shape/mod.rs +++ b/src/shape/mod.rs @@ -23,8 +23,8 @@ pub use self::{ polyline::Polyline, shared_shape::SharedShape, voxels::{ - AxisMask, OctantPattern, VoxelData, VoxelQuery, VoxelState, VoxelType, Voxels, - VoxelsChunkRef, + AxisMask, OctantPattern, QueriedVoxel, VoxelData, VoxelQuery, VoxelState, VoxelType, + Voxels, VoxelsChunkRef, }, }; diff --git a/src/shape/voxels/voxel_query.rs b/src/shape/voxels/voxel_query.rs index a0d33aed..6545c699 100644 --- a/src/shape/voxels/voxel_query.rs +++ b/src/shape/voxels/voxel_query.rs @@ -1,7 +1,7 @@ -use crate::math::{ivect_to_vect, vect_to_ivect, IVector, Vector}; +use crate::math::{ivect_to_vect, vect_to_ivect, IVector, IVectorExt, Vector, DIM}; use crate::bounding_volume::Aabb; -use crate::shape::{VoxelData, VoxelState, Voxels}; +use crate::shape::{AxisMask, VoxelData, VoxelState, VoxelType, Voxels}; /// Abstraction over the storage of a shape made of axis-aligned, uniformly sized voxels. /// @@ -21,13 +21,21 @@ use crate::shape::{VoxelData, VoxelState, Voxels}; /// `(key + 0.5) * voxel_size`. Grid ranges are always given as semi-open intervals /// `[mins, maxs)`: `mins` is included, `maxs` is excluded. /// -/// # Neighborhood states +/// # Voxel views and neighborhood states /// -/// Each non-empty voxel must know which of its immediate axis-aligned neighbors are also -/// non-empty, exposed as a [`VoxelState`]. This is what allows collision-detection to avoid -/// hitting the "internal edges" between adjacent voxels. Implementors can either store this -/// information (like [`Voxels`] does, one byte per voxel), or derive it on the fly from -/// occupancy data using [`VoxelState::with_filled_neighbors`]. +/// Lookups and iterators don't yield a fixed data struct: they yield storage-defined voxel +/// *views* ([`Self::Voxel`], bounded by [`QueriedVoxel`]). A view exposes cheap per-voxel +/// data — grid coordinates, center, and the coarse [`QueriedVoxel::voxel_type`], which a +/// sparse storage can pack in two bits per stored voxel (with empty voxels simply absent). +/// +/// Contact-manifold computation additionally needs to know *which* of a voxel's immediate +/// axis-aligned neighbors are filled — a [`VoxelState`] — to avoid hitting the "internal +/// edges" between adjacent voxels. It obtains this from [`QueriedVoxel::voxel_state`], and +/// only for the few voxels that are actual contact candidates, never during bulk iteration. +/// Since views can borrow from their storage, they can compute the state on demand from +/// local context (e.g. leaf-local reads in a sparse tree); [`Self::derive_voxel_state`] +/// provides a fallback derivation based purely on occupancy, while storages like [`Voxels`] +/// that persist the state (one byte per voxel) just hand out the stored value. /// /// # Note for implementors /// @@ -36,93 +44,16 @@ use crate::shape::{VoxelData, VoxelState, Voxels}; /// physics pipeline, wrap it in a type implementing [`Shape`](crate::shape::Shape) (typically /// with [`ShapeType::Custom`](crate::shape::ShapeType::Custom)) and dispatch to the generic /// voxel query functions from a custom `QueryDispatcher`. -/// -/// # Example -/// -/// Implementing `VoxelQuery` for a dense boolean grid, then running one of parry's generic -/// algorithms on it: -/// -/// ``` -/// # #[cfg(all(feature = "dim3", feature = "f32"))] { -/// use parry3d::mass_properties::MassProperties; -/// use parry3d::math::{IVector, Vector}; -/// use parry3d::shape::{AxisMask, VoxelData, VoxelQuery, VoxelState}; -/// -/// const N: i32 = 4; -/// -/// /// A dense 4×4×4 grid of voxels, each of size 1×1×1. -/// struct DenseGrid { -/// cells: [[[bool; 4]; 4]; 4], -/// } -/// -/// impl DenseGrid { -/// fn filled(&self, key: IVector) -> bool { -/// key.cmpge(IVector::ZERO).all() -/// && key.cmplt(IVector::splat(N)).all() -/// && self.cells[key.x as usize][key.y as usize][key.z as usize] -/// } -/// } -/// -/// impl VoxelQuery for DenseGrid { -/// fn voxel_size(&self) -> Vector { -/// Vector::splat(1.0) -/// } -/// -/// fn domain(&self) -> [IVector; 2] { -/// [IVector::ZERO, IVector::splat(N)] -/// } -/// -/// fn voxel_state(&self, key: IVector) -> Option { -/// if !self.filled(key) { -/// return Some(VoxelState::EMPTY); -/// } -/// -/// let mut mask = AxisMask::empty(); -/// if self.filled(key + IVector::new(1, 0, 0)) { mask |= AxisMask::X_POS; } -/// if self.filled(key - IVector::new(1, 0, 0)) { mask |= AxisMask::X_NEG; } -/// if self.filled(key + IVector::new(0, 1, 0)) { mask |= AxisMask::Y_POS; } -/// if self.filled(key - IVector::new(0, 1, 0)) { mask |= AxisMask::Y_NEG; } -/// if self.filled(key + IVector::new(0, 0, 1)) { mask |= AxisMask::Z_POS; } -/// if self.filled(key - IVector::new(0, 0, 1)) { mask |= AxisMask::Z_NEG; } -/// Some(VoxelState::with_filled_neighbors(mask)) -/// } -/// -/// fn linear_id(&self, key: IVector) -> Option { -/// self.filled(key) -/// .then(|| (key.x * N * N + key.y * N + key.z) as u32) -/// } -/// -/// fn voxels_in_range( -/// &self, -/// mins: IVector, -/// maxs: IVector, -/// ) -> impl Iterator { -/// let mins = mins.max(IVector::ZERO); -/// let maxs = maxs.min(IVector::splat(N)); -/// (mins.x..maxs.x).flat_map(move |x| { -/// (mins.y..maxs.y).flat_map(move |y| { -/// (mins.z..maxs.z).filter_map(move |z| { -/// let key = IVector::new(x, y, z); -/// let state = self.voxel_state(key)?; -/// (!state.is_empty()).then(|| VoxelData { -/// linear_id: self.linear_id(key).unwrap(), -/// grid_coords: key, -/// center: self.voxel_center(key), -/// state, -/// }) -/// }) -/// }) -/// }) -/// } -/// } -/// -/// let mut grid = DenseGrid { cells: [[[true; 4]; 4]; 4] }; -/// // Any of parry's generic voxel algorithms now runs on `DenseGrid` directly: -/// let props = MassProperties::from_voxels(1.0, &grid); -/// assert_eq!(props.mass(), 64.0); -/// # } -/// ``` pub trait VoxelQuery { + /// The view type this storage hands out for a single voxel. + /// + /// Views can borrow from the storage (e.g. hold a cursor into a sparse tree), letting + /// [`QueriedVoxel::voxel_state`] read neighborhood information from local context + /// instead of independent whole-storage lookups. + type Voxel<'a>: QueriedVoxel<'a> + where + Self: 'a; + /// The size of each voxel along each local coordinate axis. fn voxel_size(&self) -> Vector; @@ -132,11 +63,14 @@ pub trait VoxelQuery { /// range, but the range may also cover empty voxels. fn domain(&self) -> [IVector; 2]; - /// The state of the voxel at the given grid coordinates. + /// The voxel at the given grid coordinates, or `None` if the storage holds nothing + /// there (empty voxel, or coordinates outside the tracked domain). /// - /// Both `None` and `Some(VoxelState::EMPTY)` designate an empty voxel; by convention, - /// `None` is returned when `key` falls outside of the storage's tracked domain. - fn voxel_state(&self, key: IVector) -> Option; + /// Implementations should return `None` for empty voxels rather than a view whose + /// [`QueriedVoxel::voxel_type`] is [`VoxelType::Empty`]: the provided + /// [`Self::derive_voxel_state`] treats any `Some` as a filled voxel. Callers, on the + /// other hand, must treat `None` and empty-typed views the same. + fn voxel(&self, key: IVector) -> Option>; /// A stable identifier of the voxel at the given grid coordinates. /// @@ -152,20 +86,24 @@ pub trait VoxelQuery { /// /// Implementations must yield every non-empty voxel with grid coordinates in /// `[mins, maxs)` exactly once. They may additionally yield empty voxels within that - /// range (callers filter on [`VoxelData::state`]), but must never yield a voxel outside - /// of the range. - fn voxels_in_range(&self, mins: IVector, maxs: IVector) -> impl Iterator; + /// range (callers filter on [`QueriedVoxel::voxel_type`]), but must never yield a + /// voxel outside of the range. + fn voxels_in_range( + &self, + mins: IVector, + maxs: IVector, + ) -> impl Iterator>; /// Iterates through every voxel of this shape. /// /// This is equivalent to [`Self::voxels_in_range`] applied to the whole [`Self::domain`]. - fn voxels(&self) -> impl Iterator { + fn voxels(&self) -> impl Iterator> { let [mins, maxs] = self.domain(); self.voxels_in_range(mins, maxs) } /// Iterates through every voxel intersecting the given local-space AABB. - fn voxels_intersecting_local_aabb(&self, aabb: &Aabb) -> impl Iterator { + fn voxels_intersecting_local_aabb(&self, aabb: &Aabb) -> impl Iterator> { let [mins, maxs] = self.voxel_range_intersecting_local_aabb(aabb); self.voxels_in_range(mins, maxs) } @@ -224,7 +162,66 @@ pub trait VoxelQuery { } } +/// A single voxel handed out by a [`VoxelQuery`] storage. +/// +/// This is the item type of the [`VoxelQuery`] lookups and iterators. Storages define their +/// own implementor (see [`VoxelQuery::Voxel`]), which may borrow from the storage so that +/// [`Self::voxel_state`] can be computed lazily from local context. +/// +/// The coarse [`Self::voxel_type`] must be cheap: it is read during bulk iteration. The full +/// [`Self::voxel_state`] is only requested for contact-candidate voxels; implementations +/// must keep the two consistent (`self.voxel_state().voxel_type() == self.voxel_type()`). +pub trait QueriedVoxel<'a> { + /// The type of this voxel: empty, or how it is exposed on the shape's surface. + fn voxel_type(&self) -> VoxelType; + + /// The neighborhood state of this voxel, indicating which of its immediate + /// axis-aligned neighbors are filled. + /// + /// Storages that don't track neighborhood information can fall back to + /// [`VoxelQuery::derive_voxel_state`]. + fn voxel_state(&self) -> VoxelState; + + /// A stable, storage-defined identifier of this voxel. + /// + /// For the [`Voxels`] shape this is the flattened form of [`Voxels::linear_index`]. + /// This identifier can be invalidated after the voxels shape is modified (e.g. by a call + /// to [`Voxels::set_voxel`], or [`Voxels::crop`]). + /// For stable references to voxels, always use [`Self::grid_coords`]. Only meaningful + /// for non-empty voxels. + fn linear_id(&self) -> u32; + + /// The voxel's integer grid coordinates. + fn grid_coords(&self) -> IVector; + + /// The voxel's center position in the local-space of the voxels shape it is part of. + fn center(&self) -> Vector; +} + +impl QueriedVoxel<'_> for VoxelData { + fn voxel_type(&self) -> VoxelType { + self.state.voxel_type() + } + + fn voxel_state(&self) -> VoxelState { + self.state + } + + fn linear_id(&self) -> u32 { + self.linear_id + } + + fn grid_coords(&self) -> IVector { + self.grid_coords + } + + fn center(&self) -> Vector { + self.center + } +} impl VoxelQuery for Voxels { + type Voxel<'a> = VoxelData; + #[inline] fn voxel_size(&self) -> Vector { self.voxel_size() @@ -236,8 +233,16 @@ impl VoxelQuery for Voxels { } #[inline] - fn voxel_state(&self, key: IVector) -> Option { - self.voxel_state(key) + fn voxel(&self, key: IVector) -> Option { + let id = self.linear_index(key)?; + let state = self.chunks[id.chunk_id].states[id.id_in_chunk]; + // Tracked-but-empty voxels (allocated chunk, empty cell) read as `None` too. + (!state.is_empty()).then(|| VoxelData { + linear_id: id.flat_id() as u32, + grid_coords: key, + center: self.voxel_center(key), + state, + }) } #[inline] diff --git a/src/shape/voxels/voxels.rs b/src/shape/voxels/voxels.rs index 36bee7c4..5b3fe128 100644 --- a/src/shape/voxels/voxels.rs +++ b/src/shape/voxels/voxels.rs @@ -219,6 +219,12 @@ impl OctantPattern { #[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))] pub struct VoxelState(pub(super) u8); +impl Default for VoxelState { + fn default() -> Self { + Self::EMPTY + } +} + impl VoxelState { /// The value of empty voxels. pub const EMPTY: VoxelState = VoxelState(EMPTY_FACE_MASK); @@ -297,7 +303,9 @@ impl VoxelState { /// Information associated to a voxel. /// /// This structure provides complete information about a single voxel including its position -/// in both grid coordinates and world space, as well as its state (empty/filled and neighborhood). +/// in both grid coordinates and world space, as well as its state (empty/filled and +/// neighborhood). It is the view type ([`VoxelQuery::Voxel`](crate::shape::VoxelQuery::Voxel)) +/// handed out by the [`Voxels`] shape. /// /// # Note /// @@ -319,11 +327,9 @@ impl VoxelState { /// /// // Iterate through all voxels /// for voxel in voxels.voxels() { -/// if !voxel.state.is_empty() { -/// println!("Voxel at grid position {:?}", voxel.grid_coords); -/// println!(" World center: {:?}", voxel.center); -/// println!(" Type: {:?}", voxel.state.voxel_type()); -/// } +/// println!("Voxel at grid position {:?}", voxel.grid_coords); +/// println!(" World center: {:?}", voxel.center); +/// println!(" Type: {:?}", voxel.state.voxel_type()); /// } /// # } /// ``` @@ -468,10 +474,8 @@ pub struct VoxelData { /// /// // Iterate through all non-empty voxels /// for voxel in voxels.voxels() { -/// if !voxel.state.is_empty() { -/// println!("Voxel at grid {:?}, world center {:?}", -/// voxel.grid_coords, voxel.center); -/// } +/// println!("Voxel at grid {:?}, world center {:?}", +/// voxel.grid_coords, voxel.center); /// } /// # } /// ``` @@ -515,9 +519,7 @@ pub struct VoxelData { /// /// // Find voxels intersecting an AABB /// let query_aabb = Aabb::new(Vector::new(-0.5, -0.5, -0.5), Vector::new(1.5, 1.5, 1.5)); -/// let count = voxels.voxels_intersecting_local_aabb(&query_aabb) -/// .filter(|v| !v.state.is_empty()) -/// .count(); +/// let count = voxels.voxels_intersecting_local_aabb(&query_aabb).count(); /// println!("Found {} voxels in AABB", count); /// /// // Get the overall domain bounds @@ -684,9 +686,7 @@ impl Voxels { /// let voxels = Voxels::from_points(Vector::new(1.0, 1.0, 1.0), &points); /// /// // Only 3 unique voxels created (first two points merged) - /// let filled_count = voxels.voxels() - /// .filter(|v| !v.state.is_empty()) - /// .count(); + /// let filled_count = voxels.voxels().count(); /// assert_eq!(filled_count, 3); /// # } /// ``` @@ -752,9 +752,7 @@ impl Voxels { /// /// // Iterate through filled voxels (more efficient than iterating domain) /// for voxel in voxels.voxels() { - /// if !voxel.state.is_empty() { - /// println!("Filled voxel at {:?}", voxel.grid_coords); - /// } + /// println!("Filled voxel at {:?}", voxel.grid_coords); /// } /// # } /// ``` @@ -935,8 +933,6 @@ impl Voxels { } /// Iterates through every voxel intersecting the given aabb. - /// - /// Returns the voxel’s linearized id, center, and state. pub fn voxels_intersecting_local_aabb( &self, aabb: &Aabb, @@ -957,7 +953,8 @@ impl Voxels { ) } - /// Iterate through the data of all the voxels within the given (semi-open) voxel grid indices. + /// Iterate through the data of all the non-empty voxels within the given (semi-open) + /// voxel grid indices. /// /// Note that this only yields non-empty voxels within the range. This does not /// include any voxel that falls outside [`Self::domain`]. diff --git a/src/shape/voxels/voxels_chunk.rs b/src/shape/voxels/voxels_chunk.rs index 1266379a..e00bb7ac 100644 --- a/src/shape/voxels/voxels_chunk.rs +++ b/src/shape/voxels/voxels_chunk.rs @@ -137,9 +137,7 @@ impl VoxelsChunk { /// /// // Query voxels within this chunk /// for voxel in chunk_ref.voxels() { -/// if !voxel.state.is_empty() { -/// println!("Voxel at {:?}", voxel.grid_coords); -/// } +/// println!("Voxel at {:?}", voxel.grid_coords); /// } /// /// // Get chunk's AABB diff --git a/src/shape/voxels/voxels_edition.rs b/src/shape/voxels/voxels_edition.rs index 3c962b02..a0d8138e 100644 --- a/src/shape/voxels/voxels_edition.rs +++ b/src/shape/voxels/voxels_edition.rs @@ -89,9 +89,7 @@ impl Voxels { /// } /// /// // Count filled voxels - /// let filled = voxels.voxels() - /// .filter(|v| !v.state.is_empty()) - /// .count(); + /// let filled = voxels.voxels().count(); /// assert_eq!(filled, 9); /// # } /// ``` @@ -199,9 +197,7 @@ impl Voxels { /// voxels.crop(IVector::new(1, 0, 0), IVector::new(2, 0, 0)); /// /// // Only two voxels remain - /// let count = voxels.voxels() - /// .filter(|v| !v.state.is_empty()) - /// .count(); + /// let count = voxels.voxels().count(); /// assert_eq!(count, 2); /// # } /// ``` diff --git a/src/transformation/to_outline/voxels_to_outline.rs b/src/transformation/to_outline/voxels_to_outline.rs index dd9fb0f6..b0635b95 100644 --- a/src/transformation/to_outline/voxels_to_outline.rs +++ b/src/transformation/to_outline/voxels_to_outline.rs @@ -1,6 +1,6 @@ use crate::bounding_volume::Aabb; use crate::math::Vector; -use crate::shape::{VoxelType, Voxels}; +use crate::shape::{QueriedVoxel, VoxelType, Voxels}; use alloc::{vec, vec::Vec}; impl Voxels { @@ -29,9 +29,10 @@ impl Voxels { let vtx = aabb.vertices(); for vox in self.voxels() { - match vox.state.voxel_type() { + let state = vox.voxel_state(); + match vox.voxel_type() { VoxelType::Vertex => { - let mask = vox.state.feature_mask(); + let mask = state.feature_mask(); for edge in Aabb::EDGES_VERTEX_IDS { if mask & (1 << edge.0) != 0 || mask & (1 << edge.1) != 0 { @@ -41,7 +42,7 @@ impl Voxels { } VoxelType::Edge => { let vtx = aabb.vertices(); - let mask = vox.state.feature_mask(); + let mask = state.feature_mask(); for (i, edge) in Aabb::EDGES_VERTEX_IDS.iter().enumerate() { if mask & (1 << i) != 0 { diff --git a/src/transformation/to_polyline/voxels_to_polyline.rs b/src/transformation/to_polyline/voxels_to_polyline.rs index 727f3f40..e0cc7a10 100644 --- a/src/transformation/to_polyline/voxels_to_polyline.rs +++ b/src/transformation/to_polyline/voxels_to_polyline.rs @@ -1,6 +1,6 @@ use crate::bounding_volume::Aabb; use crate::math::{Vector, Vector2}; -use crate::shape::{VoxelType, Voxels}; +use crate::shape::{QueriedVoxel, VoxelType, Voxels}; use alloc::{vec, vec::Vec}; impl Voxels { @@ -25,9 +25,10 @@ impl Voxels { let vtx = aabb.vertices(); for vox in self.voxels() { - match vox.state.voxel_type() { + let state = vox.voxel_state(); + match state.voxel_type() { VoxelType::Vertex => { - let mask = vox.state.feature_mask(); + let mask = state.feature_mask(); for edge in Aabb::FACES_VERTEX_IDS { if mask & (1 << edge.0) != 0 || mask & (1 << edge.1) != 0 { @@ -37,7 +38,7 @@ impl Voxels { } VoxelType::Face => { let vtx = aabb.vertices(); - let mask = vox.state.feature_mask(); + let mask = state.feature_mask(); for (i, edge) in Aabb::FACES_VERTEX_IDS.iter().enumerate() { if mask & (1 << i) != 0 { diff --git a/src/transformation/to_trimesh/voxels_to_trimesh.rs b/src/transformation/to_trimesh/voxels_to_trimesh.rs index 676cc68b..10d97f54 100644 --- a/src/transformation/to_trimesh/voxels_to_trimesh.rs +++ b/src/transformation/to_trimesh/voxels_to_trimesh.rs @@ -1,6 +1,6 @@ -use crate::bounding_volume::Aabb; use crate::math::Vector; use crate::shape::Voxels; +use crate::{bounding_volume::Aabb, shape::QueriedVoxel}; use alloc::{vec, vec::Vec}; impl Voxels { @@ -15,7 +15,8 @@ impl Voxels { let mut vtx = vec![]; let mut idx = vec![]; for vox in self.voxels() { - let mask = vox.state.free_faces(); + let state = vox.voxel_state(); + let mask = state.free_faces(); for i in 0..6 { if mask.bits() & (1 << i) != 0 { let fvid = Aabb::FACES_VERTEX_IDS[i]; From b7a75156010ad6eff6be02b942a2e9c1ecf4c1a9 Mon Sep 17 00:00:00 2001 From: 0xbeefd1ed Date: Sun, 6 Sep 2026 15:19:57 -0700 Subject: [PATCH 4/9] Remove temporary methods --- src/mass_properties/mass_properties_voxels.rs | 4 +- src/query/point/mod.rs | 2 - src/query/point/point_voxels.rs | 82 ------------- src/query/ray/mod.rs | 2 - src/query/ray/ray_voxels.rs | 111 ------------------ src/shape/voxels/voxel_query.rs | 37 ------ 6 files changed, 2 insertions(+), 236 deletions(-) diff --git a/src/mass_properties/mass_properties_voxels.rs b/src/mass_properties/mass_properties_voxels.rs index d4ac8bd7..57ec61a7 100644 --- a/src/mass_properties/mass_properties_voxels.rs +++ b/src/mass_properties/mass_properties_voxels.rs @@ -2,7 +2,7 @@ use crate::mass_properties::MassProperties; #[cfg(feature = "dim3")] use crate::math::Matrix; use crate::math::{Real, Vector}; -use crate::shape::{QueriedVoxel, VoxelQuery, VoxelType}; +use crate::shape::{QueriedVoxel, VoxelQuery, VoxelType, Voxels}; impl MassProperties { /// Computes the mass properties of a voxel grid. @@ -174,7 +174,7 @@ impl MassProperties { /// - `Voxels::set_voxel()`: Add or remove voxels /// - `from_trimesh()`: Alternative for precise shapes /// - `from_compound()`: Combine multiple shapes efficiently - pub fn from_voxels(density: Real, voxels: &V) -> Self { + pub fn from_voxels(density: Real, voxels: &Voxels) -> Self { let mut com = Vector::ZERO; let mut num_not_empty = 0; #[cfg(feature = "dim2")] diff --git a/src/query/point/mod.rs b/src/query/point/mod.rs index d16ef499..3ecf3250 100644 --- a/src/query/point/mod.rs +++ b/src/query/point/mod.rs @@ -4,8 +4,6 @@ pub use self::point_query::{PointProjection, PointQuery, PointQueryWithLocation}; #[cfg(feature = "alloc")] pub use self::point_support_map::local_point_projection_on_support_map; -#[cfg(feature = "alloc")] -pub use self::point_voxels::project_local_point_on_voxels; mod point_aabb; mod point_ball; diff --git a/src/query/point/point_voxels.rs b/src/query/point/point_voxels.rs index 75525f54..f13c6a4d 100644 --- a/src/query/point/point_voxels.rs +++ b/src/query/point/point_voxels.rs @@ -5,88 +5,6 @@ use crate::shape::{ Cuboid, FeatureId, QueriedVoxel, VoxelQuery, VoxelType, Voxels, VoxelsChunkRef, }; -/// Projects a point on a voxel shape represented by any storage implementing [`VoxelQuery`]. -/// -/// Returns the projection and the [`linear_id`](VoxelQuery::linear_id) of the voxel the -/// projected point lies on, or `None` if the shape contains no voxel. If `solid` is `true` and -/// the point lies inside a non-empty voxel, the point itself is returned as the projection. -/// -/// This searches voxels in growing regions centered on the point, without relying on any -/// acceleration structure. The concrete [`Voxels`] shape implements [`PointQuery`] with a -/// faster search based on its internal BVH; this function is mostly useful for implementing -/// point queries on custom voxel storages. -/// -/// Like the [`PointQuery`] implementation of [`Voxels`], the non-solid projection of a point -/// lying inside of the shape is approximated: the point is projected on the boundary of the -/// closest voxel, which isn't necessarily on the boundary of the union of all the voxels. -pub fn project_local_point_on_voxels( - voxels: &V, - pt: Vector, - solid: bool, -) -> Option<(PointProjection, u32)> { - let base_cuboid = Cuboid::new(voxels.voxel_size() / 2.0); - - // Fast path: the point lies inside a non-empty voxel. - let key_at_pt = voxels.voxel_at_point(pt); - if solid - && voxels - .voxel(key_at_pt) - .is_some_and(|vox| vox.voxel_type() != VoxelType::Empty) - { - return Some(( - PointProjection::new(true, pt), - voxels.linear_id(key_at_pt).unwrap_or(0), - )); - } - - // The distance from `pt` to the domain’s AABB is a lower-bound of the distance from `pt` - // to its projection on the shape. Use it to initialize the search radius. - let domain_aabb = voxels.local_aabb(); - let mut search_radius = - domain_aabb.distance_to_local_point(pt, true) + voxels.voxel_size().max_element(); - - loop { - let search_aabb = Aabb::from_half_extents(pt, Vector::splat(search_radius)); - let mut best: Option<(PointProjection, u32)> = None; - let mut best_dist = Real::MAX; - - for vox in voxels.voxels_intersecting_local_aabb(&search_aabb) { - if vox.voxel_type() == VoxelType::Empty { - continue; - } - - let mut candidate = base_cuboid.project_local_point(pt - vox.center(), solid); - candidate.point += vox.center(); - - let candidate_dist = (candidate.point - pt).length(); - if candidate_dist < best_dist { - best = Some((candidate, vox.linear_id())); - best_dist = candidate_dist; - } - } - - if let Some(best) = best { - if best_dist <= search_radius { - // Any voxel closer to `pt` than `best_dist` would have intersected the - // search region, so this projection is optimal. - return Some(best); - } - - // The projection found lies outside of the search region: another voxel closer - // to its boundary could still be a better candidate. Re-run with a search region - // that covers every possibly-better voxel. - search_radius = best_dist; - } else { - if search_aabb.contains(&domain_aabb) { - // The whole shape was searched and no voxel was found. - return None; - } - - search_radius *= 2.0; - } - } -} - impl PointQuery for Voxels { #[inline] fn project_local_point(&self, pt: Vector, solid: bool) -> PointProjection { diff --git a/src/query/ray/mod.rs b/src/query/ray/mod.rs index 5ed735e6..543dcae7 100644 --- a/src/query/ray/mod.rs +++ b/src/query/ray/mod.rs @@ -9,8 +9,6 @@ pub use self::ray_support_map::local_ray_intersection_with_support_map_with_para pub use self::ray_triangle::local_ray_intersection_with_triangle; #[cfg(all(feature = "dim3", feature = "alloc"))] pub use self::ray_trimesh::RayCullingMode; -#[cfg(feature = "alloc")] -pub use self::ray_voxels::cast_local_ray_on_voxels; pub use self::simd_ray::SimdRay; #[doc(hidden)] diff --git a/src/query/ray/ray_voxels.rs b/src/query/ray/ray_voxels.rs index 5da2b245..4e95188d 100644 --- a/src/query/ray/ray_voxels.rs +++ b/src/query/ray/ray_voxels.rs @@ -3,118 +3,7 @@ use crate::partitioning::BvhNode; use crate::query::{Ray, RayCast, RayIntersection}; use crate::shape::{FeatureId, QueriedVoxel, VoxelQuery, VoxelType, Voxels, VoxelsChunkRef}; -/// Casts a ray on a voxel shape represented by any storage implementing [`VoxelQuery`]. -/// -/// This performs a DDA traversal of the voxels along the ray, across the shape's whole -/// [`domain`](VoxelQuery::domain), without relying on any acceleration structure. The concrete -/// [`Voxels`] shape implements [`RayCast`] with a faster traversal that skips empty space using -/// its internal BVH; this function is mostly useful for implementing ray-casting on custom -/// voxel storages. -/// -/// On a hit, the intersection's [`FeatureId`] is a face containing the hit voxel's -/// [`linear_id`](VoxelQuery::linear_id). -pub fn cast_local_ray_on_voxels( - voxels: &V, - ray: &Ray, - max_time_of_impact: Real, - solid: bool, -) -> Option { - use num_traits::Bounded; - - let aabb = voxels.local_aabb(); - let (min_t, mut max_t) = aabb.clip_ray_parameters(ray)?; - - #[cfg(feature = "dim2")] - let ii = [0, 1]; - #[cfg(feature = "dim3")] - let ii = [0, 1, 2]; - - if min_t > max_time_of_impact { - return None; - } - - let [domain_mins, domain_maxs] = voxels.domain(); - if domain_maxs.cmple(domain_mins).any() { - // Empty or degenerate domain. - return None; - } - - max_t = max_t.min(max_time_of_impact); - let clip_ray_a = ray.point_at(min_t); - let mut voxel_key = voxels - .voxel_at_point(clip_ray_a) - .clamp(domain_mins, domain_maxs - IVector::splat(1)); - - loop { - let aabb = voxels.voxel_aabb(voxel_key); - - if voxels - .voxel(voxel_key) - .is_some_and(|vox| vox.voxel_type() != VoxelType::Empty) - { - // We hit a voxel! - let hit = aabb.cast_local_ray_and_get_normal(ray, max_t, solid); - - if let Some(mut hit) = hit { - hit.feature = voxels - .linear_id(voxel_key) - .map_or(FeatureId::Unknown, FeatureId::Face); - return Some(hit); - } - } - - /* - * Find the next voxel to cast the ray on. - */ - let toi = ii.map(|i| { - if ray.dir.vget(i) > 0.0 { - let t = (aabb.maxs.vget(i) - ray.origin.vget(i)) / ray.dir.vget(i); - if t < 0.0 { - (Real::max_value(), true) - } else { - (t, true) - } - } else if ray.dir.vget(i) < 0.0 { - let t = (aabb.mins.vget(i) - ray.origin.vget(i)) / ray.dir.vget(i); - if t < 0.0 { - (Real::max_value(), false) - } else { - (t, false) - } - } else { - (Real::max_value(), false) - } - }); - #[cfg(feature = "dim2")] - if toi[0].0 > max_t && toi[1].0 > max_t { - break; - } - - #[cfg(feature = "dim3")] - if toi[0].0 > max_t && toi[1].0 > max_t && toi[2].0 > max_t { - break; - } - - let imin = Vector::from(toi.map(|t| t.0)).min_position(); - - if toi[imin].1 { - if voxel_key.ivget(imin) < domain_maxs.ivget(imin) - 1 { - voxel_key.ivset(imin, voxel_key.ivget(imin) + 1); - } else { - // Leaving the shape's bounds. - break; - } - } else if voxel_key.ivget(imin) > domain_mins.ivget(imin) { - voxel_key.ivset(imin, voxel_key.ivget(imin) - 1); - } else { - // Leaving the shape’s bounds. - break; - } - } - - None -} impl RayCast for Voxels { #[inline] diff --git a/src/shape/voxels/voxel_query.rs b/src/shape/voxels/voxel_query.rs index 6545c699..a578d8e8 100644 --- a/src/shape/voxels/voxel_query.rs +++ b/src/shape/voxels/voxel_query.rs @@ -63,25 +63,6 @@ pub trait VoxelQuery { /// range, but the range may also cover empty voxels. fn domain(&self) -> [IVector; 2]; - /// The voxel at the given grid coordinates, or `None` if the storage holds nothing - /// there (empty voxel, or coordinates outside the tracked domain). - /// - /// Implementations should return `None` for empty voxels rather than a view whose - /// [`QueriedVoxel::voxel_type`] is [`VoxelType::Empty`]: the provided - /// [`Self::derive_voxel_state`] treats any `Some` as a filled voxel. Callers, on the - /// other hand, must treat `None` and empty-typed views the same. - fn voxel(&self, key: IVector) -> Option>; - - /// A stable identifier of the voxel at the given grid coordinates. - /// - /// The identifier must be unique among the currently stored voxels and must match the - /// value of [`VoxelData::linear_id`] yielded by [`Self::voxels_in_range`] for the same - /// voxel. It is used to build [`FeatureId`](crate::shape::FeatureId)s in query results - /// and to match contact points across frames, so it should remain stable as long as the - /// shape isn't modified. Returns `None` if no identifier is associated to this - /// coordinate (e.g. empty voxel in unallocated storage). - fn linear_id(&self, key: IVector) -> Option; - /// Iterates through the voxels within the given semi-open grid coordinate range. /// /// Implementations must yield every non-empty voxel with grid coordinates in @@ -232,24 +213,6 @@ impl VoxelQuery for Voxels { self.domain() } - #[inline] - fn voxel(&self, key: IVector) -> Option { - let id = self.linear_index(key)?; - let state = self.chunks[id.chunk_id].states[id.id_in_chunk]; - // Tracked-but-empty voxels (allocated chunk, empty cell) read as `None` too. - (!state.is_empty()).then(|| VoxelData { - linear_id: id.flat_id() as u32, - grid_coords: key, - center: self.voxel_center(key), - state, - }) - } - - #[inline] - fn linear_id(&self, key: IVector) -> Option { - self.linear_index(key).map(|id| id.flat_id() as u32) - } - #[inline] fn voxels_in_range(&self, mins: IVector, maxs: IVector) -> impl Iterator { self.voxels_in_range(mins, maxs) From 79fdb843bb7c119c80f2ae332a2a7cb88945eaf5 Mon Sep 17 00:00:00 2001 From: 0xbeefd1ed Date: Sun, 6 Sep 2026 15:27:38 -0700 Subject: [PATCH 5/9] Add changelog --- CHANGELOG.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 507f40ec..ff09e800 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,52 @@ ## Unreleased +### Breaking changes + +- `VoxelData::linear_id` is now a flat `u32` (the flattened form of `Voxels::linear_index`) instead of + a `VoxelIndex`. Code that destructured `linear_id` into `chunk_id`/`id_in_chunk` must go through + `Voxels::linear_index` instead. +- The voxel query functions are now generic over the voxel storage instead of taking `&Voxels`: + `contact_manifolds_voxels_shape`, `contact_manifolds_voxels_ball`, + `contact_manifolds_voxels_composite_shape`, `contact_manifolds_voxels_voxels`, + `intersection_test_voxels_shape`, `intersection_test_shape_voxels`, `cast_shapes_voxels_shape`, + `cast_shapes_shape_voxels`, `cast_shapes_nonlinear_voxels_shape`, + `cast_shapes_nonlinear_shape_voxels`, and `MassProperties::from_voxels`. Calls that pass a + `&Voxels` keep working unchanged; callers naming the functions with explicit turbofish generics + gain one extra type parameter. +- Reading a voxel's type, state, center, or grid coordinates from the item yielded by + `Voxels::voxels`, `Voxels::voxels_in_range`, and `Voxels::voxels_intersecting_local_aabb` should + go through the new `QueriedVoxel` trait methods (`voxel_type()`, `voxel_state()`, `center()`, + `grid_coords()`, `linear_id()`) for code that must also work with custom storages. The public + fields of `VoxelData` remain available. + ### Added - `CompoundFlags::FIX_INTERNAL_EDGES` makes a `Compound` treat the edges (2D) or faces (3D) its parts share as interior to the union, so a body sliding across the cut between two parts of a convex decomposition no longer catches on it. `Compound::PartNormalConstraints` is now `CompoundPseudoNormals`, matching what `TriMesh` and `Polyline` already provide. +- `VoxelQuery` trait, an abstraction over the storage of a shape made of axis-aligned, uniformly + sized voxels. Implementing it for a custom sparse structure (chunked grid, octree, VDB-like tree) + lets Parry's voxel collision algorithms (contact manifolds, intersection tests, linear and + nonlinear shape-casting, mass properties) run directly on that structure without copying it into a + `Voxels` shape. Implementors provide `voxel_size`, `domain`, and `voxels_in_range`; grid helpers + such as `voxel_at_point`, `voxel_center`, `voxel_aabb`, `voxel_range_intersecting_local_aabb`, + `align_aabb_to_grid`, and `local_aabb` have default implementations. +- `QueriedVoxel` trait describing the per-voxel view handed out by a `VoxelQuery` storage. Views + may borrow from their storage so that `voxel_state()` can be computed lazily from local context, + while `voxel_type()` stays cheap for bulk iteration. +- `Voxels` implements `VoxelQuery` with `VoxelData` as its voxel view, and `VoxelData` implements + `QueriedVoxel`. +- `VoxelState::with_filled_neighbors(AxisMask)` builds the state of a non-empty voxel from the set + of its filled axis-aligned neighbors, for custom storages that only track occupancy. +- `Default` implementations for `VoxelType` (`Empty`) and `VoxelState` (`EMPTY`). +- `contact_manifolds_voxels_ball` is now re-exported from `parry::query`, alongside the other + voxel contact-manifold functions. + +### Modified + +- `Voxels::voxels_in_range` documentation now states that only non-empty voxels are yielded, and + the `Voxels` examples no longer filter out empty voxels redundantly. ## 0.30.2 From ae4229f72c4bbbfdcfd8ff81ab73ebeb895dc555 Mon Sep 17 00:00:00 2001 From: 0xbeefd1ed Date: Mon, 7 Sep 2026 10:24:08 -0700 Subject: [PATCH 6/9] cargo fmt --- CHANGELOG.md | 8 -------- crates/parry3d/tests/geometry/mod.rs | 1 - src/query/point/point_voxels.rs | 6 ++---- src/query/ray/ray_voxels.rs | 6 ++---- src/shape/voxels/voxel_query.rs | 4 ++-- 5 files changed, 6 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff09e800..10f463f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,6 @@ ### Breaking changes -- `VoxelData::linear_id` is now a flat `u32` (the flattened form of `Voxels::linear_index`) instead of - a `VoxelIndex`. Code that destructured `linear_id` into `chunk_id`/`id_in_chunk` must go through - `Voxels::linear_index` instead. - The voxel query functions are now generic over the voxel storage instead of taking `&Voxels`: `contact_manifolds_voxels_shape`, `contact_manifolds_voxels_ball`, `contact_manifolds_voxels_composite_shape`, `contact_manifolds_voxels_voxels`, @@ -43,11 +40,6 @@ - `contact_manifolds_voxels_ball` is now re-exported from `parry::query`, alongside the other voxel contact-manifold functions. -### Modified - -- `Voxels::voxels_in_range` documentation now states that only non-empty voxels are yielded, and - the `Voxels` examples no longer filter out empty voxels redundantly. - ## 0.30.2 ### Added diff --git a/crates/parry3d/tests/geometry/mod.rs b/crates/parry3d/tests/geometry/mod.rs index 1c1d91db..25a4e755 100644 --- a/crates/parry3d/tests/geometry/mod.rs +++ b/crates/parry3d/tests/geometry/mod.rs @@ -10,4 +10,3 @@ mod time_of_impact3; mod trimesh_connected_components; mod trimesh_intersection; mod trimesh_trimesh_toi; -mod voxel_query_custom_storage; diff --git a/src/query/point/point_voxels.rs b/src/query/point/point_voxels.rs index f13c6a4d..6f1787c1 100644 --- a/src/query/point/point_voxels.rs +++ b/src/query/point/point_voxels.rs @@ -1,9 +1,7 @@ -use crate::bounding_volume::{Aabb, BoundingVolume}; +use crate::bounding_volume::BoundingVolume; use crate::math::{Real, Vector}; use crate::query::{PointProjection, PointQuery}; -use crate::shape::{ - Cuboid, FeatureId, QueriedVoxel, VoxelQuery, VoxelType, Voxels, VoxelsChunkRef, -}; +use crate::shape::{Cuboid, FeatureId, QueriedVoxel, VoxelQuery, Voxels, VoxelsChunkRef}; impl PointQuery for Voxels { #[inline] diff --git a/src/query/ray/ray_voxels.rs b/src/query/ray/ray_voxels.rs index 4e95188d..0d90997b 100644 --- a/src/query/ray/ray_voxels.rs +++ b/src/query/ray/ray_voxels.rs @@ -1,9 +1,7 @@ -use crate::math::{IVector, IVectorExt, Real, Vector, VectorExt}; +use crate::math::{IVectorExt, Real, Vector, VectorExt}; use crate::partitioning::BvhNode; use crate::query::{Ray, RayCast, RayIntersection}; -use crate::shape::{FeatureId, QueriedVoxel, VoxelQuery, VoxelType, Voxels, VoxelsChunkRef}; - - +use crate::shape::{FeatureId, QueriedVoxel, VoxelQuery, Voxels, VoxelsChunkRef}; impl RayCast for Voxels { #[inline] diff --git a/src/shape/voxels/voxel_query.rs b/src/shape/voxels/voxel_query.rs index a578d8e8..fb6bc183 100644 --- a/src/shape/voxels/voxel_query.rs +++ b/src/shape/voxels/voxel_query.rs @@ -1,7 +1,7 @@ -use crate::math::{ivect_to_vect, vect_to_ivect, IVector, IVectorExt, Vector, DIM}; +use crate::math::{ivect_to_vect, vect_to_ivect, IVector, IVectorExt, Vector}; use crate::bounding_volume::Aabb; -use crate::shape::{AxisMask, VoxelData, VoxelState, VoxelType, Voxels}; +use crate::shape::{VoxelData, VoxelState, VoxelType, Voxels}; /// Abstraction over the storage of a shape made of axis-aligned, uniformly sized voxels. /// From 7387e3cf7911db8bd0abbf23b4ba90af105cb9ab Mon Sep 17 00:00:00 2001 From: 0xbeefd1ed Date: Mon, 7 Sep 2026 10:44:40 -0700 Subject: [PATCH 7/9] Update docs --- src/mass_properties/mass_properties_voxels.rs | 4 ++-- src/shape/voxels/voxel_query.rs | 9 +++------ 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/mass_properties/mass_properties_voxels.rs b/src/mass_properties/mass_properties_voxels.rs index 57ec61a7..c74c4b07 100644 --- a/src/mass_properties/mass_properties_voxels.rs +++ b/src/mass_properties/mass_properties_voxels.rs @@ -18,7 +18,7 @@ impl MassProperties { /// - In 3D: kg/m³ (mass per unit volume) /// - In 2D: kg/m² (mass per unit area) /// * `voxels` - Any voxel storage implementing [`VoxelQuery`], e.g. the - /// [`Voxels`](crate::shape::Voxels) shape + /// [`Voxels`] shape /// - Each voxel is a small cube/square of uniform size /// - Voxels can be empty or filled /// - Since v0.25.0, `Voxels` uses sparse storage internally for efficiency @@ -165,7 +165,7 @@ impl MassProperties { /// - Only non-empty voxels contribute to mass /// - Empty voxels are ignored (zero mass, no inertia) /// - A voxel is considered empty if its - /// [`QueriedVoxel::voxel_type`](crate::shape::QueriedVoxel::voxel_type) is + /// [`QueriedVoxel::voxel_type`] is /// [`VoxelType::Empty`] /// /// # See Also diff --git a/src/shape/voxels/voxel_query.rs b/src/shape/voxels/voxel_query.rs index fb6bc183..70f34081 100644 --- a/src/shape/voxels/voxel_query.rs +++ b/src/shape/voxels/voxel_query.rs @@ -33,9 +33,9 @@ use crate::shape::{VoxelData, VoxelState, VoxelType, Voxels}; /// edges" between adjacent voxels. It obtains this from [`QueriedVoxel::voxel_state`], and /// only for the few voxels that are actual contact candidates, never during bulk iteration. /// Since views can borrow from their storage, they can compute the state on demand from -/// local context (e.g. leaf-local reads in a sparse tree); [`Self::derive_voxel_state`] -/// provides a fallback derivation based purely on occupancy, while storages like [`Voxels`] -/// that persist the state (one byte per voxel) just hand out the stored value. +/// local context (e.g. leaf-local reads in a sparse tree); +/// [`VoxelState::with_filled_neighbors`] builds the state from occupancy alone, while storages +/// like [`Voxels`] that persist the state (one byte per voxel) just hand out the stored value. /// /// # Note for implementors /// @@ -158,9 +158,6 @@ pub trait QueriedVoxel<'a> { /// The neighborhood state of this voxel, indicating which of its immediate /// axis-aligned neighbors are filled. - /// - /// Storages that don't track neighborhood information can fall back to - /// [`VoxelQuery::derive_voxel_state`]. fn voxel_state(&self) -> VoxelState; /// A stable, storage-defined identifier of this voxel. From 9fcb0215c3f51fb4538d79a64ed65f934064617f Mon Sep 17 00:00:00 2001 From: 0xbeefd1ed Date: Mon, 7 Sep 2026 11:08:56 -0700 Subject: [PATCH 8/9] Final polish --- CHANGELOG.md | 8 +++----- src/mass_properties/mass_properties_voxels.rs | 5 ++--- src/query/point/point_voxels.rs | 3 +-- src/query/ray/ray_voxels.rs | 2 +- src/shape/voxels/voxel_query.rs | 4 ++-- 5 files changed, 9 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10f463f5..5657af8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,8 @@ `contact_manifolds_voxels_composite_shape`, `contact_manifolds_voxels_voxels`, `intersection_test_voxels_shape`, `intersection_test_shape_voxels`, `cast_shapes_voxels_shape`, `cast_shapes_shape_voxels`, `cast_shapes_nonlinear_voxels_shape`, - `cast_shapes_nonlinear_shape_voxels`, and `MassProperties::from_voxels`. Calls that pass a - `&Voxels` keep working unchanged; callers naming the functions with explicit turbofish generics - gain one extra type parameter. + `cast_shapes_nonlinear_shape_voxels`. Calls that pass a `&Voxels` keep working unchanged; + callers naming the functions with explicit turbofish generics gain one extra type parameter. - Reading a voxel's type, state, center, or grid coordinates from the item yielded by `Voxels::voxels`, `Voxels::voxels_in_range`, and `Voxels::voxels_intersecting_local_aabb` should go through the new `QueriedVoxel` trait methods (`voxel_type()`, `voxel_state()`, `center()`, @@ -25,8 +24,7 @@ - `VoxelQuery` trait, an abstraction over the storage of a shape made of axis-aligned, uniformly sized voxels. Implementing it for a custom sparse structure (chunked grid, octree, VDB-like tree) lets Parry's voxel collision algorithms (contact manifolds, intersection tests, linear and - nonlinear shape-casting, mass properties) run directly on that structure without copying it into a - `Voxels` shape. Implementors provide `voxel_size`, `domain`, and `voxels_in_range`; grid helpers + nonlinear shape-casting) run directly on that structure without copying it into a `Voxels` shape. Implementors provide `voxel_size`, `domain`, and `voxels_in_range`; grid helpers such as `voxel_at_point`, `voxel_center`, `voxel_aabb`, `voxel_range_intersecting_local_aabb`, `align_aabb_to_grid`, and `local_aabb` have default implementations. - `QueriedVoxel` trait describing the per-voxel view handed out by a `VoxelQuery` storage. Views diff --git a/src/mass_properties/mass_properties_voxels.rs b/src/mass_properties/mass_properties_voxels.rs index c74c4b07..0e9ea409 100644 --- a/src/mass_properties/mass_properties_voxels.rs +++ b/src/mass_properties/mass_properties_voxels.rs @@ -2,7 +2,7 @@ use crate::mass_properties::MassProperties; #[cfg(feature = "dim3")] use crate::math::Matrix; use crate::math::{Real, Vector}; -use crate::shape::{QueriedVoxel, VoxelQuery, VoxelType, Voxels}; +use crate::shape::{QueriedVoxel, VoxelType, Voxels}; impl MassProperties { /// Computes the mass properties of a voxel grid. @@ -17,8 +17,7 @@ impl MassProperties { /// * `density` - The material density /// - In 3D: kg/m³ (mass per unit volume) /// - In 2D: kg/m² (mass per unit area) - /// * `voxels` - Any voxel storage implementing [`VoxelQuery`], e.g. the - /// [`Voxels`] shape + /// * `voxels` - A [`Voxels`] shape containing the voxel grid /// - Each voxel is a small cube/square of uniform size /// - Voxels can be empty or filled /// - Since v0.25.0, `Voxels` uses sparse storage internally for efficiency diff --git a/src/query/point/point_voxels.rs b/src/query/point/point_voxels.rs index 6f1787c1..6b42e025 100644 --- a/src/query/point/point_voxels.rs +++ b/src/query/point/point_voxels.rs @@ -1,7 +1,6 @@ -use crate::bounding_volume::BoundingVolume; use crate::math::{Real, Vector}; use crate::query::{PointProjection, PointQuery}; -use crate::shape::{Cuboid, FeatureId, QueriedVoxel, VoxelQuery, Voxels, VoxelsChunkRef}; +use crate::shape::{Cuboid, FeatureId, QueriedVoxel, Voxels, VoxelsChunkRef}; impl PointQuery for Voxels { #[inline] diff --git a/src/query/ray/ray_voxels.rs b/src/query/ray/ray_voxels.rs index 0d90997b..4da55478 100644 --- a/src/query/ray/ray_voxels.rs +++ b/src/query/ray/ray_voxels.rs @@ -1,7 +1,7 @@ use crate::math::{IVectorExt, Real, Vector, VectorExt}; use crate::partitioning::BvhNode; use crate::query::{Ray, RayCast, RayIntersection}; -use crate::shape::{FeatureId, QueriedVoxel, VoxelQuery, Voxels, VoxelsChunkRef}; +use crate::shape::{FeatureId, Voxels, VoxelsChunkRef}; impl RayCast for Voxels { #[inline] diff --git a/src/shape/voxels/voxel_query.rs b/src/shape/voxels/voxel_query.rs index 70f34081..08f10b95 100644 --- a/src/shape/voxels/voxel_query.rs +++ b/src/shape/voxels/voxel_query.rs @@ -1,4 +1,4 @@ -use crate::math::{ivect_to_vect, vect_to_ivect, IVector, IVectorExt, Vector}; +use crate::math::{ivect_to_vect, vect_to_ivect, IVector, Vector}; use crate::bounding_volume::Aabb; use crate::shape::{VoxelData, VoxelState, VoxelType, Voxels}; @@ -6,7 +6,7 @@ use crate::shape::{VoxelData, VoxelState, VoxelType, Voxels}; /// Abstraction over the storage of a shape made of axis-aligned, uniformly sized voxels. /// /// Parry's voxel collision-detection algorithms (contact manifolds, intersection tests, -/// linear and nonlinear shape-casting, ray-casting, point projection, mass properties) are +/// linear and nonlinear shape-casting) are /// written against this trait rather than against the concrete [`Voxels`] shape. Implementing /// it for a custom sparse data-structure (chunked grid, octree, VDB-like tree, etc.) lets these /// algorithms run directly on that structure without copying it into a [`Voxels`] shape, From b019b062a47bf30d9a67ef2e54735b2a117fea62 Mon Sep 17 00:00:00 2001 From: 0xbeefd1ed Date: Mon, 7 Sep 2026 11:15:45 -0700 Subject: [PATCH 9/9] more polish --- src/mass_properties/mass_properties_voxels.rs | 4 +-- src/shape/voxels/voxels.rs | 27 ------------------- 2 files changed, 2 insertions(+), 29 deletions(-) diff --git a/src/mass_properties/mass_properties_voxels.rs b/src/mass_properties/mass_properties_voxels.rs index 0e9ea409..83b98bee 100644 --- a/src/mass_properties/mass_properties_voxels.rs +++ b/src/mass_properties/mass_properties_voxels.rs @@ -17,10 +17,10 @@ impl MassProperties { /// * `density` - The material density /// - In 3D: kg/m³ (mass per unit volume) /// - In 2D: kg/m² (mass per unit area) - /// * `voxels` - A [`Voxels`] shape containing the voxel grid + /// * `voxels` - A `Voxels` structure containing the voxel grid /// - Each voxel is a small cube/square of uniform size /// - Voxels can be empty or filled - /// - Since v0.25.0, `Voxels` uses sparse storage internally for efficiency + /// - Since v0.25.0, uses sparse storage internally for efficiency /// /// # Returns /// diff --git a/src/shape/voxels/voxels.rs b/src/shape/voxels/voxels.rs index 5b3fe128..1a206733 100644 --- a/src/shape/voxels/voxels.rs +++ b/src/shape/voxels/voxels.rs @@ -236,33 +236,6 @@ impl VoxelState { } /// The state of a **non-empty** voxel given the set of its non-empty axis-aligned neighbors. - /// - /// This is mostly useful for implementing [`VoxelQuery`](crate::shape::VoxelQuery) on a - /// custom voxel storage that only tracks per-voxel occupancy: the [`VoxelState`] of a filled - /// voxel is fully determined by which of its (up to 6 in 3D, 4 in 2D) immediate neighbors - /// along the coordinate axes are filled. - /// - /// Passing a mask with every axis direction set yields [`VoxelState::INTERIOR`]. Note that - /// the state of an *empty* voxel is always [`VoxelState::EMPTY`], regardless of its - /// neighborhood. - /// - /// # Example - /// - /// ``` - /// # #[cfg(all(feature = "dim3", feature = "f32"))] { - /// use parry3d::shape::{AxisMask, VoxelState, VoxelType}; - /// - /// // A voxel with filled neighbors in every direction is an interior voxel. - /// let state = VoxelState::with_filled_neighbors(AxisMask::all()); - /// assert_eq!(state, VoxelState::INTERIOR); - /// - /// // A voxel from a flat ground layer: neighbors on ±x and ±z, nothing above or below. - /// let state = VoxelState::with_filled_neighbors( - /// AxisMask::X_POS | AxisMask::X_NEG | AxisMask::Z_POS | AxisMask::Z_NEG, - /// ); - /// assert_eq!(state.voxel_type(), VoxelType::Face); - /// assert_eq!(state.free_faces(), AxisMask::Y_POS | AxisMask::Y_NEG); - /// # } /// ``` pub const fn with_filled_neighbors(filled_neighbors: AxisMask) -> Self { // The `AxisMask` bits match the internal neighborhood bit layout: