parry3d/query/shape_cast/
shape_cast.rs

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