parry3d/query/shape_cast/
shape_cast.rs

1use na::Unit;
2
3use crate::math::{Isometry, Point, Real, Vector};
4use crate::query::{DefaultQueryDispatcher, QueryDispatcher, Unsupported};
5use crate::shape::Shape;
6
7#[cfg(feature = "alloc")]
8use crate::partitioning::BvhLeafCost;
9
10/// The status of the time-of-impact computation algorithm.
11#[derive(Copy, Clone, Debug, PartialEq, Eq)]
12pub enum ShapeCastStatus {
13    /// The shape-casting algorithm ran out of iterations before achieving convergence.
14    ///
15    /// The content of the `ShapeCastHit` will still be a conservative approximation of the actual result so
16    /// it is often fine to interpret this case as a success.
17    OutOfIterations,
18    /// The shape-casting algorithm converged successfully.
19    Converged,
20    /// Something went wrong during the shape-casting, likely due to numerical instabilities.
21    ///
22    /// The content of the `ShapeCastHit` will still be a conservative approximation of the actual result so
23    /// it is often fine to interpret this case as a success.
24    Failed,
25    /// The two shape already overlap, or are separated by a distance smaller than
26    /// [`ShapeCastOptions::target_distance`] at the time 0.
27    ///
28    /// The witness points and normals provided by the `ShapeCastHit` will have unreliable values unless
29    /// [`ShapeCastOptions::compute_impact_geometry_on_penetration`] was set to `true` when calling
30    /// the time-of-impact function.
31    PenetratingOrWithinTargetDist,
32}
33
34/// The result of a shape casting..
35#[derive(Copy, Clone, Debug)]
36pub struct ShapeCastHit {
37    /// The time at which the objects touch.
38    pub time_of_impact: Real,
39    /// The local-space closest point on the first shape at the time of impact.
40    ///
41    /// This value is unreliable if `status` is [`ShapeCastStatus::PenetratingOrWithinTargetDist`]
42    /// and [`ShapeCastOptions::compute_impact_geometry_on_penetration`] was set to `false`.
43    pub witness1: Point<Real>,
44    /// The local-space closest point on the second shape at the time of impact.
45    ///
46    /// This value is unreliable if `status` is [`ShapeCastStatus::PenetratingOrWithinTargetDist`]
47    /// and both [`ShapeCastOptions::compute_impact_geometry_on_penetration`] was set to `false`
48    /// when calling the time-of-impact function.
49    pub witness2: Point<Real>,
50    /// The local-space outward normal on the first shape at the time of impact.
51    ///
52    /// This value is unreliable if `status` is [`ShapeCastStatus::PenetratingOrWithinTargetDist`]
53    /// and both [`ShapeCastOptions::compute_impact_geometry_on_penetration`] was set to `false`
54    /// when calling the time-of-impact function.
55    pub normal1: Unit<Vector<Real>>,
56    /// The local-space outward normal on the second shape at the time of impact.
57    ///
58    /// This value is unreliable if `status` is [`ShapeCastStatus::PenetratingOrWithinTargetDist`]
59    /// and both [`ShapeCastOptions::compute_impact_geometry_on_penetration`] was set to `false`
60    /// when calling the time-of-impact function.
61    pub normal2: Unit<Vector<Real>>,
62    /// The way the shape-casting algorithm terminated.
63    pub status: ShapeCastStatus,
64}
65
66impl ShapeCastHit {
67    /// Swaps every data of this shape-casting result such that the role of both shapes are swapped.
68    ///
69    /// In practice, this makes it so that `self.witness1` and `self.normal1` are swapped with
70    /// `self.witness2` and `self.normal2`.
71    pub fn swapped(self) -> Self {
72        Self {
73            time_of_impact: self.time_of_impact,
74            witness1: self.witness2,
75            witness2: self.witness1,
76            normal1: self.normal2,
77            normal2: self.normal1,
78            status: self.status,
79        }
80    }
81
82    /// Transform `self.witness1` and `self.normal1` by `pos`.
83    pub fn transform1_by(&self, pos: &Isometry<Real>) -> Self {
84        Self {
85            time_of_impact: self.time_of_impact,
86            witness1: pos * self.witness1,
87            witness2: self.witness2,
88            normal1: pos * self.normal1,
89            normal2: self.normal2,
90            status: self.status,
91        }
92    }
93}
94
95#[cfg(feature = "alloc")]
96impl BvhLeafCost for ShapeCastHit {
97    #[inline]
98    fn cost(&self) -> Real {
99        self.time_of_impact
100    }
101}
102
103/// Configuration for controlling the behavior of time-of-impact (i.e. shape-casting) calculations.
104#[derive(Copy, Clone, Debug, PartialEq)]
105pub struct ShapeCastOptions {
106    /// The maximum time-of-impacts that can be computed.
107    ///
108    /// Any impact occurring after this time will be ignored.
109    pub max_time_of_impact: Real,
110    /// The shapes will be considered as impacting as soon as their distance is smaller or
111    /// equal to this target distance. Must be positive or zero.
112    ///
113    /// If the shapes are separated by a distance smaller than `target_distance` at time 0, the
114    /// calculated witness points and normals are only reliable if
115    /// [`Self::compute_impact_geometry_on_penetration`] is set to `true`.
116    pub target_distance: Real,
117    /// If `false`, the time-of-impact algorithm will automatically discard any impact at time
118    /// 0 where the velocity is separating (i.e., the relative velocity is such that the distance
119    /// between the objects projected on the impact normal is increasing through time).
120    pub stop_at_penetration: bool,
121    /// If `true`, witness points and normals will be calculated even when the time-of-impact is 0.
122    pub compute_impact_geometry_on_penetration: bool,
123}
124
125impl ShapeCastOptions {
126    // Constructor for the most common use-case.
127    /// Crates a [`ShapeCastOptions`] with the default values except for the maximum time of impact.
128    pub fn with_max_time_of_impact(max_time_of_impact: Real) -> Self {
129        Self {
130            max_time_of_impact,
131            ..Default::default()
132        }
133    }
134}
135
136impl Default for ShapeCastOptions {
137    fn default() -> Self {
138        Self {
139            max_time_of_impact: Real::MAX,
140            target_distance: 0.0,
141            stop_at_penetration: true,
142            compute_impact_geometry_on_penetration: true,
143        }
144    }
145}
146
147/// Computes when two moving shapes will collide (shape casting / swept collision detection).
148///
149/// This function determines the **time of impact** when two shapes moving with constant
150/// linear velocities will first touch. This is essential for **continuous collision detection**
151/// (CCD) to prevent fast-moving objects from tunneling through each other.
152///
153/// # What is Shape Casting?
154///
155/// Shape casting extends ray casting to arbitrary shapes:
156/// - **Ray casting**: Point moving in a direction (infinitely thin)
157/// - **Shape casting**: Full shape moving in a direction (has volume)
158///
159/// The shapes move linearly (no rotation) from their initial positions along their
160/// velocities until they touch or the time limit is reached.
161///
162/// # Behavior
163///
164/// - **Will collide**: Returns `Some(hit)` with time of first impact
165/// - **Already touching**: Returns `Some(hit)` with `time_of_impact = 0.0`
166/// - **Won't collide**: Returns `None` (no impact within time range)
167/// - **Moving apart**: May return `None` depending on `stop_at_penetration` option
168///
169/// # Arguments
170///
171/// * `pos1` - Initial position and orientation of the first shape
172/// * `vel1` - Linear velocity of the first shape (units per time)
173/// * `g1` - The first shape
174/// * `pos2` - Initial position and orientation of the second shape
175/// * `vel2` - Linear velocity of the second shape
176/// * `g2` - The second shape
177/// * `options` - Configuration options (max time, target distance, etc.)
178///
179/// # Options
180///
181/// Configure behavior with [`ShapeCastOptions`]:
182/// - `max_time_of_impact`: Maximum time to check (ignore later impacts)
183/// - `target_distance`: Consider "close enough" when within this distance
184/// - `stop_at_penetration`: Stop if initially penetrating and moving apart
185/// - `compute_impact_geometry_on_penetration`: Compute reliable witnesses at t=0
186///
187/// # Returns
188///
189/// * `Ok(Some(hit))` - Impact found, see [`ShapeCastHit`] for details
190/// * `Ok(None)` - No impact within time range
191/// * `Err(Unsupported)` - This shape pair is not supported
192///
193/// # Example: Basic Shape Casting
194///
195/// ```rust
196/// # #[cfg(all(feature = "dim3", feature = "f32"))] {
197/// use parry3d::query::{cast_shapes, ShapeCastOptions};
198/// use parry3d::shape::Ball;
199/// use nalgebra::{Isometry3, Vector3};
200///
201/// let ball1 = Ball::new(1.0);
202/// let ball2 = Ball::new(1.0);
203///
204/// // Ball 1 at origin, moving right at speed 2.0
205/// let pos1 = Isometry3::translation(0.0, 0.0, 0.0);
206/// let vel1 = Vector3::new(2.0, 0.0, 0.0);
207///
208/// // Ball 2 at x=10, stationary
209/// let pos2 = Isometry3::translation(10.0, 0.0, 0.0);
210/// let vel2 = Vector3::zeros();
211///
212/// let options = ShapeCastOptions::default();
213///
214/// if let Ok(Some(hit)) = cast_shapes(&pos1, &vel1, &ball1, &pos2, &vel2, &ball2, options) {
215///     // Time when surfaces touch
216///     // Distance to cover: 10.0 - 1.0 (radius) - 1.0 (radius) = 8.0
217///     // Speed: 2.0, so time = 8.0 / 2.0 = 4.0
218///     assert_eq!(hit.time_of_impact, 4.0);
219///
220///     // Position at impact
221///     let impact_pos1 = pos1.translation.vector + vel1 * hit.time_of_impact;
222///     // Ball 1 moved 8 units to x=8.0, touching ball 2 at x=10.0
223/// }
224/// # }
225/// ```
226///
227/// # Example: Already Penetrating
228///
229/// ```rust
230/// # #[cfg(all(feature = "dim3", feature = "f32"))] {
231/// use parry3d::query::{cast_shapes, ShapeCastOptions, ShapeCastStatus};
232/// use parry3d::shape::Ball;
233/// use nalgebra::{Isometry3, Vector3};
234///
235/// let ball1 = Ball::new(2.0);
236/// let ball2 = Ball::new(2.0);
237///
238/// // Overlapping balls (centers 3 units apart, radii sum to 4)
239/// let pos1 = Isometry3::translation(0.0, 0.0, 0.0);
240/// let pos2 = Isometry3::translation(3.0, 0.0, 0.0);
241/// let vel1 = Vector3::x();
242/// let vel2 = Vector3::zeros();
243///
244/// let options = ShapeCastOptions::default();
245///
246/// if let Ok(Some(hit)) = cast_shapes(&pos1, &vel1, &ball1, &pos2, &vel2, &ball2, options) {
247///     // Already penetrating
248///     assert_eq!(hit.time_of_impact, 0.0);
249///     assert_eq!(hit.status, ShapeCastStatus::PenetratingOrWithinTargetDist);
250/// }
251/// # }
252/// ```
253///
254/// # Use Cases
255///
256/// - **Continuous collision detection**: Prevent tunneling at high speeds
257/// - **Predictive collision**: Know when collision will occur
258/// - **Sweep tests**: Moving platforms, sliding objects
259/// - **Bullet physics**: Fast projectiles that need CCD
260///
261/// # Performance
262///
263/// Shape casting is more expensive than static queries:
264/// - Uses iterative root-finding algorithms
265/// - Multiple distance/contact queries per iteration
266/// - Complexity depends on shape types and relative velocities
267///
268/// # See Also
269///
270/// - [`cast_shapes_nonlinear`](crate::query::cast_shapes_nonlinear()) - For rotating shapes
271/// - [`Ray::cast_ray`](crate::query::RayCast::cast_ray) - For point-like casts
272/// - [`ShapeCastOptions`] - Configuration options
273/// - [`ShapeCastHit`] - Result structure
274pub fn cast_shapes(
275    pos1: &Isometry<Real>,
276    vel1: &Vector<Real>,
277    g1: &dyn Shape,
278    pos2: &Isometry<Real>,
279    vel2: &Vector<Real>,
280    g2: &dyn Shape,
281    options: ShapeCastOptions,
282) -> Result<Option<ShapeCastHit>, Unsupported> {
283    let pos12 = pos1.inv_mul(pos2);
284    let vel12 = pos1.inverse_transform_vector(&(vel2 - vel1));
285    DefaultQueryDispatcher.cast_shapes(&pos12, &vel12, g1, g2, options)
286}