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