Skip to main content

parry2d/query/contact_manifolds/
contact_manifolds_voxels_shape.rs

1use crate::bounding_volume::{Aabb, BoundingVolume};
2use crate::math::{IVector, IVectorExt, Int, Pose, Real, Vector, VectorExt, DIM};
3use crate::query::{
4    ContactManifold, ContactManifoldsWorkspace, PersistentQueryDispatcher, PointQuery,
5    TypedWorkspaceData, WorkspaceData,
6};
7use crate::shape::{AxisMask, Cuboid, Shape, SupportMap, VoxelData, VoxelType, Voxels};
8use crate::utils::hashmap::{Entry, HashMap};
9use crate::utils::PoseOpt;
10use alloc::{boxed::Box, vec::Vec};
11use num::AsPrimitive;
12
13#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
14#[cfg_attr(
15    feature = "rkyv",
16    derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)
17)]
18#[derive(Clone)]
19pub(crate) struct VoxelsShapeSubDetector {
20    pub manifold_id: usize,
21    pub selected_contacts: u32,
22    pub timestamp: bool,
23}
24
25#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
26#[derive(Clone, Eq, Hash, PartialEq)]
27pub(crate) struct VoxelsWorkspaceKey<const N: usize> {
28    #[cfg_attr(feature = "serde-serialize", serde(with = "serde_arrays"))]
29    idx: [u32; N],
30}
31
32impl<const N: usize> From<[u32; N]> for VoxelsWorkspaceKey<N> {
33    fn from(idx: [u32; N]) -> Self {
34        Self { idx }
35    }
36}
37
38// NOTE: this is using a similar kind of cache as compound shape and height-field.
39//       It is different from the trimesh cash though. Which one is better?
40/// A workspace for collision-detection against voxels shape.
41#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
42#[derive(Clone, Default)]
43pub struct VoxelsShapeContactManifoldsWorkspace<const N: usize> {
44    pub(crate) timestamp: bool,
45    pub(crate) sub_detectors: HashMap<VoxelsWorkspaceKey<N>, VoxelsShapeSubDetector>,
46}
47
48impl<const N: usize> VoxelsShapeContactManifoldsWorkspace<N> {
49    /// A new empty workspace for collision-detection against voxels shape.
50    pub fn new() -> Self {
51        Self::default()
52    }
53
54    pub(crate) fn ensure_exists(workspace: &mut Option<ContactManifoldsWorkspace>)
55    where
56        Self: WorkspaceData,
57    {
58        if workspace
59            .as_ref()
60            .and_then(|w| {
61                w.0.downcast_ref::<VoxelsShapeContactManifoldsWorkspace<N>>()
62            })
63            .is_some()
64        {
65            return;
66        }
67
68        *workspace = Some(ContactManifoldsWorkspace(Box::new(
69            VoxelsShapeContactManifoldsWorkspace::new(),
70        )));
71    }
72}
73
74impl WorkspaceData for VoxelsShapeContactManifoldsWorkspace<2> {
75    fn as_typed_workspace_data(&self) -> TypedWorkspaceData<'_> {
76        TypedWorkspaceData::VoxelsShapeContactManifoldsWorkspace(self)
77    }
78
79    fn clone_dyn(&self) -> Box<dyn WorkspaceData> {
80        Box::new(self.clone())
81    }
82}
83
84/// Computes the contact manifold between a convex shape and a voxels shape, both represented as a `Shape` trait-object.
85pub fn contact_manifolds_voxels_shape_shapes<ManifoldData, ContactData>(
86    dispatcher: &dyn PersistentQueryDispatcher<ManifoldData, ContactData>,
87    pos12: &Pose,
88    shape1: &dyn Shape,
89    shape2: &dyn Shape,
90    prediction: Real,
91    manifolds: &mut Vec<ContactManifold<ManifoldData, ContactData>>,
92    workspace: &mut Option<ContactManifoldsWorkspace>,
93) where
94    ManifoldData: Default + Clone,
95    ContactData: Default + Copy,
96{
97    if let Some(voxels1) = shape1.as_voxels() {
98        contact_manifolds_voxels_shape(
99            dispatcher, pos12, voxels1, shape2, prediction, manifolds, workspace, false,
100        );
101    } else if let Some(voxels2) = shape2.as_voxels() {
102        contact_manifolds_voxels_shape(
103            dispatcher,
104            &pos12.inverse(),
105            voxels2,
106            shape1,
107            prediction,
108            manifolds,
109            workspace,
110            true,
111        );
112    }
113}
114
115/// Computes the contact manifold between a convex shape and a voxels shape.
116pub fn contact_manifolds_voxels_shape<ManifoldData, ContactData>(
117    dispatcher: &dyn PersistentQueryDispatcher<ManifoldData, ContactData>,
118    pos12: &Pose,
119    voxels1: &Voxels,
120    shape2: &dyn Shape,
121    prediction: Real,
122    manifolds: &mut Vec<ContactManifold<ManifoldData, ContactData>>,
123    workspace: &mut Option<ContactManifoldsWorkspace>,
124    flipped: bool,
125) where
126    ManifoldData: Default + Clone,
127    ContactData: Default + Copy,
128{
129    VoxelsShapeContactManifoldsWorkspace::<2>::ensure_exists(workspace);
130    let workspace: &mut VoxelsShapeContactManifoldsWorkspace<2> =
131        workspace.as_mut().unwrap().0.downcast_mut().unwrap();
132    let new_timestamp = !workspace.timestamp;
133    workspace.timestamp = new_timestamp;
134
135    // TODO: avoid reallocating the new `manifolds` vec at each step.
136    let mut old_manifolds = core::mem::take(manifolds);
137
138    let radius1 = voxels1.voxel_size() / 2.0;
139
140    let aabb1 = voxels1.local_aabb();
141    // Loosen by `prediction` (like the trimesh/heightfield/compound manifold generators do)
142    // so that speculative contacts within the prediction distance are generated too.
143    let aabb2_1 = shape2.compute_aabb(pos12).loosened(prediction);
144    let domain2_1 = Aabb {
145        mins: aabb2_1.mins - radius1 * 10.0,
146        maxs: aabb2_1.maxs + radius1 * 10.0,
147    };
148
149    if let Some(intersection_aabb1) = aabb1.intersection(&aabb2_1) {
150        for vox1 in voxels1.voxels_intersecting_local_aabb(&intersection_aabb1) {
151            let vox_type1 = vox1.state.voxel_type();
152
153            // TODO: would be nice to have a strategy to handle interior voxels for depenetration.
154            if vox_type1 == VoxelType::Empty || vox_type1 == VoxelType::Interior {
155                continue;
156            }
157
158            let canon1 = CanonicalVoxelShape::from_voxel(voxels1, &vox1);
159
160            // TODO: could we refactor the workspace system between Voxels, HeightField, and CompoundShape?
161            //       (and maybe TriMesh too but it’s using a different approach).
162            let (sub_detector, manifold_updated) =
163                match workspace.sub_detectors.entry(canon1.workspace_key.into()) {
164                    Entry::Occupied(entry) => {
165                        let sub_detector = entry.into_mut();
166
167                        if sub_detector.timestamp != new_timestamp {
168                            let manifold = old_manifolds[sub_detector.manifold_id].take();
169                            *sub_detector = VoxelsShapeSubDetector {
170                                manifold_id: manifolds.len(),
171                                timestamp: new_timestamp,
172                                selected_contacts: 0,
173                            };
174
175                            manifolds.push(manifold);
176                            (sub_detector, false)
177                        } else {
178                            (sub_detector, true)
179                        }
180                    }
181                    Entry::Vacant(entry) => {
182                        let sub_detector = VoxelsShapeSubDetector {
183                            manifold_id: manifolds.len(),
184                            selected_contacts: 0,
185                            timestamp: new_timestamp,
186                        };
187
188                        let vid = vox1.linear_id.flat_id() as u32;
189                        let (id1, id2) = if flipped { (0, vid) } else { (vid, 0) };
190                        manifolds.push(ContactManifold::with_data(
191                            id1,
192                            id2,
193                            ManifoldData::default(),
194                        ));
195
196                        (entry.insert(sub_detector), false)
197                    }
198                };
199
200            /*
201             * Update the contact manifold if needed.
202             */
203            let manifold = &mut manifolds[sub_detector.manifold_id];
204
205            if !manifold_updated {
206                let (canonical_center1, canonical_pseudo_cube1) =
207                    canon1.cuboid(voxels1, &vox1, domain2_1);
208
209                let canonical_shape1 = &canonical_pseudo_cube1 as &dyn Shape;
210                let canonical_pos12 = Pose::from_translation(-canonical_center1) * pos12;
211
212                // If we already computed contacts in the previous simulation step, their
213                // local points are relative to the previously calculated canonical shape
214                // which might not have the same local center as the one computed in this
215                // step (because it’s based on the position of shape2 relative to voxels1).
216                // So we need to adjust the local points to account for the position difference
217                // and keep the point at the same "canonica-shape-space" location as in the previous frame.
218                let prev_center = if flipped {
219                    manifold
220                        .subshape_pos2()
221                        .as_ref()
222                        .map(|p| p.translation)
223                        .unwrap_or_default()
224                } else {
225                    manifold
226                        .subshape_pos1()
227                        .as_ref()
228                        .map(|p| p.translation)
229                        .unwrap_or_default()
230                };
231                let delta_center = canonical_center1 - prev_center;
232
233                if flipped {
234                    for pt in &mut manifold.points {
235                        pt.local_p2 -= delta_center;
236                    }
237                } else {
238                    for pt in &mut manifold.points {
239                        pt.local_p1 -= delta_center;
240                    }
241                }
242
243                // Update contacts.
244                if flipped {
245                    manifold.set_subshape_pos2(Some(Pose::from_translation(canonical_center1)));
246                    let _ = dispatcher.contact_manifold_convex_convex(
247                        &canonical_pos12.inverse(),
248                        shape2,
249                        canonical_shape1,
250                        None,
251                        None,
252                        prediction,
253                        manifold,
254                    );
255                } else {
256                    manifold.set_subshape_pos1(Some(Pose::from_translation(canonical_center1)));
257                    let _ = dispatcher.contact_manifold_convex_convex(
258                        &canonical_pos12,
259                        canonical_shape1,
260                        shape2,
261                        None,
262                        None,
263                        prediction,
264                        manifold,
265                    );
266                }
267            }
268
269            /*
270             * Filter-out points that don’t belong to this block.
271             */
272            let test_voxel = Cuboid::new(radius1 + Vector::splat(1.0e-2));
273            let penetration_dir1 = if flipped {
274                manifold.local_n2
275            } else {
276                manifold.local_n1
277            };
278
279            for (i, pt) in manifold.points.iter().enumerate() {
280                if pt.dist < 0.0 {
281                    // If this is a penetration, double-check that we are not hitting the
282                    // interior of the infinitely expanded canonical shape by checking if
283                    // the opposite normal had led to a better vector.
284                    let cuboid1 = Cuboid::new(radius1);
285                    let sp1 = cuboid1.local_support_point(-penetration_dir1) + vox1.center;
286                    let sm2 = shape2
287                        .as_support_map()
288                        .expect("Unsupported collision pair.");
289                    let sp2 = sm2.support_point(pos12, penetration_dir1);
290                    let test_dist = (sp2 - sp1).dot(-penetration_dir1);
291                    let keep = test_dist < pt.dist;
292
293                    if !keep {
294                        // We don’t want to keep this point as it is an incorrect penetration
295                        // caused by the canonical shape expansion.
296                        continue;
297                    }
298                }
299
300                let pt_in_voxel_space = if flipped {
301                    manifold.subshape_pos2().transform_point(pt.local_p2) - vox1.center
302                } else {
303                    manifold.subshape_pos1().transform_point(pt.local_p1) - vox1.center
304                };
305                sub_detector.selected_contacts |=
306                    (test_voxel.contains_local_point(pt_in_voxel_space) as u32) << i;
307            }
308        }
309    }
310
311    // Remove contacts marked as ignored.
312    for sub_detector in workspace.sub_detectors.values() {
313        if sub_detector.timestamp == new_timestamp {
314            let manifold = &mut manifolds[sub_detector.manifold_id];
315            let mut k = 0;
316            manifold.points.retain(|_| {
317                let keep = (sub_detector.selected_contacts & (1 << k)) != 0;
318                k += 1;
319                keep
320            });
321        }
322    }
323
324    // Remove detectors which no longer index a valid manifold.
325    workspace
326        .sub_detectors
327        .retain(|_, detector| detector.timestamp == new_timestamp);
328}
329
330#[derive(Copy, Clone, Debug)]
331pub(crate) struct CanonicalVoxelShape {
332    pub range: [IVector; 2],
333    pub workspace_key: [u32; 2],
334}
335
336impl CanonicalVoxelShape {
337    pub fn from_voxel(voxels: &Voxels, vox: &VoxelData) -> Self {
338        let mut key_low = vox.grid_coords;
339        let mut key_high = key_low;
340
341        // NOTE: the mins/maxs here are offset by 1 so we can expand past the last voxel if it
342        //       happens to also be infinite along the same axis (due to cross-voxels internal edges
343        //       detection).
344        let mins = voxels.domain()[0] - IVector::splat(1);
345        let maxs = voxels.domain()[1];
346        let counts = maxs - mins;
347        let mask1 = vox.state.free_faces();
348
349        let adjust_canon = |axis: AxisMask, i: usize, key: &mut IVector, val: Int| {
350            if !mask1.contains(axis) {
351                key.ivset(i, val.as_());
352            }
353        };
354
355        adjust_canon(AxisMask::X_POS, 0, &mut key_high, maxs.x);
356        adjust_canon(AxisMask::X_NEG, 0, &mut key_low, mins.x);
357        adjust_canon(AxisMask::Y_POS, 1, &mut key_high, maxs.y);
358        adjust_canon(AxisMask::Y_NEG, 1, &mut key_low, mins.y);
359
360        #[cfg(feature = "dim3")]
361        {
362            adjust_canon(AxisMask::Z_POS, 2, &mut key_high, maxs.z);
363            adjust_canon(AxisMask::Z_NEG, 2, &mut key_low, mins.z);
364        }
365
366        #[cfg(feature = "dim2")]
367        let workspace_id = |vox: IVector| {
368            let local = vox - mins;
369            (local.x + local.y * counts.x) as u32
370        };
371
372        #[cfg(feature = "dim3")]
373        let workspace_id = |vox: IVector| {
374            let local = vox - mins;
375            (local.x + local.y * counts.x + local.z * counts.x * counts.y) as u32
376        };
377
378        Self {
379            range: [key_low, key_high],
380            workspace_key: [workspace_id(key_low), workspace_id(key_high)],
381        }
382    }
383
384    pub fn cuboid(&self, voxels: &Voxels, vox: &VoxelData, domain2_1: Aabb) -> (Vector, Cuboid) {
385        let radius = voxels.voxel_size() / 2.0;
386        let mut canonical_mins = voxels.voxel_center(self.range[0]);
387        let mut canonical_maxs = voxels.voxel_center(self.range[1]);
388
389        for k in 0..DIM {
390            if self.range[0].ivget(k) != vox.grid_coords.ivget(k) {
391                canonical_mins.vset(k, canonical_mins.vget(k).max(domain2_1.mins.vget(k)));
392            }
393
394            if self.range[1].ivget(k) != vox.grid_coords.ivget(k) {
395                canonical_maxs.vset(k, canonical_maxs.vget(k).min(domain2_1.maxs.vget(k)));
396            }
397        }
398
399        let canonical_half_extents = (canonical_maxs - canonical_mins) / 2.0 + radius;
400        let canonical_center = canonical_mins.midpoint(canonical_maxs);
401        let canonical_cube = Cuboid::new(canonical_half_extents);
402
403        (canonical_center, canonical_cube)
404    }
405}