avian3d/collision/collider/parry/contact_query.rs
1//! Geometric queries for computing information about contacts between two [`Collider`]s.
2//!
3//! This module contains the following contact queries:
4//!
5//! | Contact query | Description |
6//! | --------------------- | ------------------------------------------------------------------------- |
7//! | [`contact`] | Computes one pair of contact points between two [`Collider`]s. |
8//! | [`contact_manifolds`] | Computes all [`ContactManifold`]s between two [`Collider`]s. |
9//! | [`closest_points`] | Computes the closest points between two [`Collider`]s. |
10//! | [`distance`] | Computes the minimum distance separating two [`Collider`]s. |
11//! | [`intersection_test`] | Tests whether two [`Collider`]s are intersecting each other. |
12//! | [`time_of_impact`] | Computes when two moving [`Collider`]s hit each other for the first time. |
13//!
14//! For geometric queries that query the entire world for intersections, like raycasting, shapecasting
15//! and point projection, see [spatial queries](spatial_query).
16
17use crate::{collision::contact_types::SingleContact, prelude::*};
18use bevy::prelude::*;
19use parry::query::{PersistentQueryDispatcher, ShapeCastOptions, Unsupported};
20
21/// An error indicating that a [contact query](self) is not supported for one of the [`Collider`] shapes.
22pub type UnsupportedShape = Unsupported;
23
24/// Computes one pair of contact points between two [`Collider`]s.
25///
26/// Returns `None` if the colliders are separated by a distance greater than `prediction_distance`
27/// or if the given shapes are invalid.
28///
29/// # Example
30///
31/// ```
32/// # #[cfg(feature = "2d")]
33/// # use avian2d::{collision::collider::contact_query::contact, prelude::*};
34/// # #[cfg(feature = "3d")]
35/// use avian3d::{collision::collider::contact_query::contact, prelude::*};
36/// use bevy::prelude::*;
37///
38/// # #[cfg(all(feature = "3d", feature = "f32"))]
39/// # {
40/// let collider1 = Collider::sphere(0.5);
41/// let collider2 = Collider::cuboid(1.0, 1.0, 1.0);
42///
43/// // Compute a contact that should have a penetration depth of 0.5
44/// let contact = contact(
45/// // First collider
46/// &collider1,
47/// Vec3::default(),
48/// Quat::default(),
49/// // Second collider
50/// &collider2,
51/// Vec3::X * 0.5,
52/// Quat::default(),
53/// // Prediction distance
54/// 0.0,
55/// )
56/// .expect("Unsupported collider shape");
57///
58/// assert_eq!(
59/// contact.is_some_and(|contact| contact.penetration == 0.5),
60/// true
61/// );
62/// # }
63/// ```
64pub fn contact(
65 collider1: &Collider,
66 position1: impl Into<Position>,
67 rotation1: impl Into<Rotation>,
68 collider2: &Collider,
69 position2: impl Into<Position>,
70 rotation2: impl Into<Rotation>,
71 prediction_distance: Scalar,
72) -> Result<Option<SingleContact>, UnsupportedShape> {
73 let rotation1: Rotation = rotation1.into();
74 let rotation2: Rotation = rotation2.into();
75 let isometry1 = make_isometry(position1.into(), rotation1);
76 let isometry2 = make_isometry(position2.into(), rotation2);
77
78 parry::query::contact(
79 &isometry1,
80 collider1.shape_scaled().0.as_ref(),
81 &isometry2,
82 collider2.shape_scaled().0.as_ref(),
83 prediction_distance,
84 )
85 .map(|contact| {
86 if let Some(contact) = contact {
87 // Transform contact data into local space
88 let point1: Vector = rotation1.inverse() * Vector::from(contact.point1);
89 let point2: Vector = rotation2.inverse() * Vector::from(contact.point2);
90 let normal1: Vector = (rotation1.inverse() * Vector::from(contact.normal1)).normalize();
91 let normal2: Vector = (rotation2.inverse() * Vector::from(contact.normal2)).normalize();
92
93 // Make sure the normals are valid
94 if !normal1.is_normalized() || !normal2.is_normalized() {
95 return None;
96 }
97
98 Some(SingleContact::new(
99 point1,
100 point2,
101 normal1,
102 normal2,
103 -contact.dist,
104 ))
105 } else {
106 None
107 }
108 })
109}
110
111// TODO: Add a persistent version of this that tries to reuse previous contact manifolds
112// by exploiting spatial and temporal coherence. This is supported by Parry's contact_manifolds,
113// but requires using Parry's ContactManifold type.
114/// Computes all [`ContactManifold`]s between two [`Collider`]s.
115///
116/// Returns an empty vector if the colliders are separated by a distance greater than `prediction_distance`
117/// or if the given shapes are invalid.
118///
119/// # Example
120///
121/// ```
122/// # #[cfg(feature = "2d")]
123/// # use avian2d::{collision::collider::contact_query::contact_manifolds, prelude::*};
124/// # #[cfg(feature = "3d")]
125/// use avian3d::{collision::collider::contact_query::contact_manifolds, prelude::*};
126/// use bevy::prelude::*;
127///
128/// # #[cfg(all(feature = "3d", feature = "f32"))]
129/// # {
130/// let collider1 = Collider::sphere(0.5);
131/// let collider2 = Collider::cuboid(1.0, 1.0, 1.0);
132///
133/// // Compute contact manifolds a collision that should be penetrating
134/// let mut manifolds = Vec::new();
135/// contact_manifolds(
136/// // First collider
137/// &collider1,
138/// Vec3::default(),
139/// Quat::default(),
140/// // Second collider
141/// &collider2,
142/// Vec3::X * 0.25,
143/// Quat::default(),
144/// // Prediction distance
145/// 0.0,
146/// // Output manifolds
147/// &mut manifolds,
148/// );
149///
150/// assert_eq!(manifolds.is_empty(), false);
151/// # }
152/// ```
153pub fn contact_manifolds(
154 collider1: &Collider,
155 position1: impl Into<Position>,
156 rotation1: impl Into<Rotation>,
157 collider2: &Collider,
158 position2: impl Into<Position>,
159 rotation2: impl Into<Rotation>,
160 prediction_distance: Scalar,
161 manifolds: &mut Vec<ContactManifold>,
162) {
163 let position1: Position = position1.into();
164 let position2: Position = position2.into();
165 let rotation1: Rotation = rotation1.into();
166 let rotation2: Rotation = rotation2.into();
167 let isometry1 = make_isometry(position1, rotation1);
168 let isometry2 = make_isometry(position2, rotation2);
169 let isometry12 = isometry1.inv_mul(&isometry2);
170
171 // TODO: Reuse manifolds from previous frame to improve performance
172 let mut new_manifolds =
173 Vec::<parry::query::ContactManifold<(), ()>>::with_capacity(manifolds.len());
174 let result = parry::query::DefaultQueryDispatcher.contact_manifolds(
175 &isometry12,
176 collider1.shape_scaled().0.as_ref(),
177 collider2.shape_scaled().0.as_ref(),
178 prediction_distance,
179 &mut new_manifolds,
180 &mut None,
181 );
182
183 // Clear the old manifolds.
184 manifolds.clear();
185
186 // Fall back to support map contacts for unsupported (custom) shapes.
187 if result.is_err() {
188 if let (Some(shape1), Some(shape2)) = (
189 collider1.shape_scaled().as_support_map(),
190 collider2.shape_scaled().as_support_map(),
191 ) {
192 if let Some(contact) = parry::query::contact::contact_support_map_support_map(
193 &isometry12,
194 shape1,
195 shape2,
196 prediction_distance,
197 ) {
198 let normal = rotation1 * Vector::from(contact.normal1);
199
200 // Make sure the normal is valid
201 if !normal.is_normalized() {
202 return;
203 }
204
205 let points = [ContactPoint::new(
206 contact.point1.into(),
207 contact.point2.into(),
208 -contact.dist,
209 )];
210
211 manifolds.push(ContactManifold::new(points, normal, 0));
212 }
213 }
214 }
215
216 let mut manifold_index = 0;
217
218 manifolds.extend(new_manifolds.iter().filter_map(|manifold| {
219 // Skip empty manifolds.
220 if manifold.contacts().is_empty() {
221 return None;
222 }
223
224 let subpos1 = manifold.subshape_pos1.unwrap_or_default();
225 let subpos2 = manifold.subshape_pos2.unwrap_or_default();
226 let local_normal: Vector = subpos1
227 .rotation
228 .transform_vector(&manifold.local_n1)
229 .normalize()
230 .into();
231 let normal = rotation1 * local_normal;
232
233 // Make sure the normal is valid
234 if !normal.is_normalized() {
235 return None;
236 }
237
238 let points = manifold.contacts().iter().map(|contact| {
239 ContactPoint::new(
240 subpos1.transform_point(&contact.local_p1).into(),
241 subpos2.transform_point(&contact.local_p2).into(),
242 -contact.dist,
243 )
244 .with_feature_ids(contact.fid1.into(), contact.fid2.into())
245 });
246
247 let manifold = ContactManifold::new(points, normal, manifold_index);
248
249 manifold_index += 1;
250
251 Some(manifold)
252 }));
253}
254
255/// Information about the closest points between two [`Collider`]s.
256///
257/// The closest points can be computed using [`closest_points`].
258#[derive(Reflect, Clone, Copy, Debug, PartialEq)]
259#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
260#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
261#[reflect(Debug, PartialEq)]
262pub enum ClosestPoints {
263 /// The two shapes are intersecting each other.
264 Intersecting,
265 /// The two shapes are not intersecting each other but the distance between the closest points
266 /// is below the user-defined maximum distance.
267 ///
268 /// The points are expressed in world space.
269 WithinMargin(Vector, Vector),
270 /// The two shapes are not intersecting each other and the distance between the closest points
271 /// exceeds the user-defined maximum distance.
272 OutsideMargin,
273}
274
275/// Computes the [`ClosestPoints`] between two [`Collider`]s.
276///
277/// Returns `Err(UnsupportedShape)` if either of the collider shapes is not supported.
278///
279/// # Example
280///
281/// ```
282/// # #[cfg(feature = "2d")]
283/// # use avian2d::{collision::collider::contact_query::*, prelude::*};
284/// # #[cfg(feature = "3d")]
285/// use avian3d::{collision::collider::contact_query::*, prelude::*};
286/// use bevy::prelude::*;
287///
288/// # #[cfg(all(feature = "3d", feature = "f32"))]
289/// # {
290/// let collider1 = Collider::sphere(0.5);
291/// let collider2 = Collider::cuboid(1.0, 1.0, 1.0);
292///
293/// // The shapes are intersecting
294/// assert_eq!(
295/// closest_points(
296/// &collider1,
297/// Vec3::default(),
298/// Quat::default(),
299/// &collider2,
300/// Vec3::default(),
301/// Quat::default(),
302/// 2.0,
303/// )
304/// .expect("Unsupported collider shape"),
305/// ClosestPoints::Intersecting,
306/// );
307///
308/// // The shapes are not intersecting but the distance between the closest points is below 2.0
309/// assert_eq!(
310/// closest_points(
311/// &collider1,
312/// Vec3::default(),
313/// Quat::default(),
314/// &collider2,
315/// Vec3::X * 1.5,
316/// Quat::default(),
317/// 2.0,
318/// )
319/// .expect("Unsupported collider shape"),
320/// ClosestPoints::WithinMargin(Vec3::X * 0.5, Vec3::X * 1.0),
321/// );
322///
323/// // The shapes are not intersecting and the distance between the closest points exceeds 2.0
324/// assert_eq!(
325/// closest_points(
326/// &collider1,
327/// Vec3::default(),
328/// Quat::default(),
329/// &collider2,
330/// Vec3::X * 5.0,
331/// Quat::default(),
332/// 2.0,
333/// )
334/// .expect("Unsupported collider shape"),
335/// ClosestPoints::OutsideMargin,
336/// );
337/// # }
338/// ```
339pub fn closest_points(
340 collider1: &Collider,
341 position1: impl Into<Position>,
342 rotation1: impl Into<Rotation>,
343 collider2: &Collider,
344 position2: impl Into<Position>,
345 rotation2: impl Into<Rotation>,
346 max_distance: Scalar,
347) -> Result<ClosestPoints, UnsupportedShape> {
348 let rotation1: Rotation = rotation1.into();
349 let rotation2: Rotation = rotation2.into();
350 let isometry1 = make_isometry(position1.into(), rotation1);
351 let isometry2 = make_isometry(position2.into(), rotation2);
352
353 parry::query::closest_points(
354 &isometry1,
355 collider1.shape_scaled().0.as_ref(),
356 &isometry2,
357 collider2.shape_scaled().0.as_ref(),
358 max_distance,
359 )
360 .map(|closest_points| match closest_points {
361 parry::query::ClosestPoints::Intersecting => ClosestPoints::Intersecting,
362 parry::query::ClosestPoints::WithinMargin(point1, point2) => {
363 ClosestPoints::WithinMargin(point1.into(), point2.into())
364 }
365 parry::query::ClosestPoints::Disjoint => ClosestPoints::OutsideMargin,
366 })
367}
368
369/// Computes the minimum distance separating two [`Collider`]s.
370///
371/// Returns `0.0` if the colliders are touching or penetrating, and `Err(UnsupportedShape)`
372/// if either of the collider shapes is not supported.
373///
374/// # Example
375///
376/// ```
377/// # #[cfg(feature = "2d")]
378/// # use avian2d::{collision::collider::contact_query::distance, prelude::*};
379/// # #[cfg(feature = "3d")]
380/// use avian3d::{collision::collider::contact_query::distance, prelude::*};
381/// use bevy::prelude::*;
382///
383/// # #[cfg(all(feature = "3d", feature = "f32"))]
384/// # {
385/// let collider1 = Collider::sphere(0.5);
386/// let collider2 = Collider::cuboid(1.0, 1.0, 1.0);
387///
388/// // The distance is 1.0
389/// assert_eq!(
390/// distance(
391/// &collider1,
392/// Vec3::default(),
393/// Quat::default(),
394/// &collider2,
395/// Vec3::X * 2.0,
396/// Quat::default(),
397/// )
398/// .expect("Unsupported collider shape"),
399/// 1.0,
400/// );
401///
402/// // The colliders are penetrating, so the distance is 0.0
403/// assert_eq!(
404/// distance(
405/// &collider1,
406/// Vec3::default(),
407/// Quat::default(),
408/// &collider2,
409/// Vec3::default(),
410/// Quat::default(),
411/// )
412/// .expect("Unsupported collider shape"),
413/// 0.0,
414/// );
415/// # }
416/// ```
417pub fn distance(
418 collider1: &Collider,
419 position1: impl Into<Position>,
420 rotation1: impl Into<Rotation>,
421 collider2: &Collider,
422 position2: impl Into<Position>,
423 rotation2: impl Into<Rotation>,
424) -> Result<Scalar, UnsupportedShape> {
425 let rotation1: Rotation = rotation1.into();
426 let rotation2: Rotation = rotation2.into();
427 let isometry1 = make_isometry(position1.into(), rotation1);
428 let isometry2 = make_isometry(position2.into(), rotation2);
429
430 parry::query::distance(
431 &isometry1,
432 collider1.shape_scaled().0.as_ref(),
433 &isometry2,
434 collider2.shape_scaled().0.as_ref(),
435 )
436}
437
438/// Tests whether two [`Collider`]s are intersecting each other.
439///
440/// Returns `Err(UnsupportedShape)` if either of the collider shapes is not supported.
441///
442/// # Example
443///
444/// ```
445/// # #[cfg(feature = "2d")]
446/// # use avian2d::{collision::collider::contact_query::intersection_test, prelude::*};
447/// # #[cfg(feature = "3d")]
448/// use avian3d::{collision::collider::contact_query::intersection_test, prelude::*};
449/// use bevy::prelude::*;
450///
451/// # #[cfg(all(feature = "3d", feature = "f32"))]
452/// # {
453/// let collider1 = Collider::sphere(0.5);
454/// let collider2 = Collider::cuboid(1.0, 1.0, 1.0);
455///
456/// // These colliders should be intersecting
457/// assert_eq!(
458/// intersection_test(
459/// &collider1,
460/// Vec3::default(),
461/// Quat::default(),
462/// &collider2,
463/// Vec3::default(),
464/// Quat::default(),
465/// )
466/// .expect("Unsupported collider shape"),
467/// true,
468/// );
469///
470/// // These colliders shouldn't be intersecting
471/// assert_eq!(
472/// intersection_test(
473/// &collider1,
474/// Vec3::default(),
475/// Quat::default(),
476/// &collider2,
477/// Vec3::X * 5.0,
478/// Quat::default(),
479/// )
480/// .expect("Unsupported collider shape"),
481/// false,
482/// );
483/// # }
484/// ```
485pub fn intersection_test(
486 collider1: &Collider,
487 position1: impl Into<Position>,
488 rotation1: impl Into<Rotation>,
489 collider2: &Collider,
490 position2: impl Into<Position>,
491 rotation2: impl Into<Rotation>,
492) -> Result<bool, UnsupportedShape> {
493 let rotation1: Rotation = rotation1.into();
494 let rotation2: Rotation = rotation2.into();
495 let isometry1 = make_isometry(position1.into(), rotation1);
496 let isometry2 = make_isometry(position2.into(), rotation2);
497
498 parry::query::intersection_test(
499 &isometry1,
500 collider1.shape_scaled().0.as_ref(),
501 &isometry2,
502 collider2.shape_scaled().0.as_ref(),
503 )
504}
505
506/// The way the [time of impact](time_of_impact) computation was terminated.
507pub type TimeOfImpactStatus = parry::query::details::ShapeCastStatus;
508
509/// The result of a [time of impact](time_of_impact) computation between two moving [`Collider`]s.
510#[derive(Clone, Copy, Debug, PartialEq)]
511pub struct TimeOfImpact {
512 /// The time at which the colliders come into contact.
513 pub time_of_impact: Scalar,
514 /// The closest point on the first collider, at the time of impact,
515 /// expressed in local space.
516 pub point1: Vector,
517 /// The closest point on the second collider, at the time of impact,
518 /// expressed in local space.
519 pub point2: Vector,
520 /// The outward normal on the first collider, at the time of impact,
521 /// expressed in local space.
522 pub normal1: Vector,
523 /// The outward normal on the second collider, at the time of impact,
524 /// expressed in local space.
525 pub normal2: Vector,
526 /// The way the time of impact computation was terminated.
527 pub status: TimeOfImpactStatus,
528}
529
530/// Computes when two moving [`Collider`]s hit each other for the first time.
531///
532/// Returns `Ok(None)` if the time of impact is greater than `max_time_of_impact`
533/// and `Err(UnsupportedShape)` if either of the collider shapes is not supported.
534///
535/// # Example
536///
537/// ```
538/// # #[cfg(feature = "2d")]
539/// # use avian2d::{collision::collider::contact_query::time_of_impact, prelude::*};
540/// # #[cfg(feature = "3d")]
541/// use avian3d::{collision::collider::contact_query::time_of_impact, prelude::*};
542/// use bevy::prelude::*;
543///
544/// # #[cfg(all(feature = "3d", feature = "f32"))]
545/// # {
546/// let collider1 = Collider::sphere(0.5);
547/// let collider2 = Collider::cuboid(1.0, 1.0, 1.0);
548///
549/// let result = time_of_impact(
550/// &collider1, // Collider 1
551/// Vec3::NEG_X * 5.0, // Position 1
552/// Quat::default(), // Rotation 1
553/// Vec3::X, // Linear velocity 1
554/// &collider2, // Collider 2
555/// Vec3::X * 5.0, // Position 2
556/// Quat::default(), // Rotation 2
557/// Vec3::NEG_X, // Linear velocity 2
558/// 100.0, // Maximum time of impact
559/// )
560/// .expect("Unsupported collider shape");
561///
562/// assert_eq!(result.unwrap().time_of_impact, 4.5);
563/// # }
564/// ```
565#[allow(clippy::too_many_arguments, clippy::type_complexity)]
566pub fn time_of_impact(
567 collider1: &Collider,
568 position1: impl Into<Position>,
569 rotation1: impl Into<Rotation>,
570 velocity1: impl Into<LinearVelocity>,
571 collider2: &Collider,
572 position2: impl Into<Position>,
573 rotation2: impl Into<Rotation>,
574 velocity2: impl Into<LinearVelocity>,
575 max_time_of_impact: Scalar,
576) -> Result<Option<TimeOfImpact>, UnsupportedShape> {
577 let rotation1: Rotation = rotation1.into();
578 let rotation2: Rotation = rotation2.into();
579
580 let velocity1: LinearVelocity = velocity1.into();
581 let velocity2: LinearVelocity = velocity2.into();
582
583 let isometry1 = make_isometry(position1.into(), rotation1);
584 let isometry2 = make_isometry(position2.into(), rotation2);
585
586 parry::query::cast_shapes(
587 &isometry1,
588 &velocity1.0.into(),
589 collider1.shape_scaled().0.as_ref(),
590 &isometry2,
591 &velocity2.0.into(),
592 collider2.shape_scaled().0.as_ref(),
593 ShapeCastOptions {
594 max_time_of_impact,
595 stop_at_penetration: true,
596 ..default()
597 },
598 )
599 .map(|toi| {
600 toi.map(|toi| TimeOfImpact {
601 time_of_impact: toi.time_of_impact,
602 point1: toi.witness1.into(),
603 point2: toi.witness2.into(),
604 normal1: toi.normal1.into(),
605 normal2: toi.normal2.into(),
606 status: toi.status,
607 })
608 })
609}