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}