Skip to main content

parry2d/query/shape_cast/
shape_cast.rs

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