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