parry3d/query/ray/
ray.rs

1//! Traits and structure needed to cast rays.
2
3use crate::math::{Pose, Real, Vector};
4use crate::shape::FeatureId;
5
6#[cfg(feature = "alloc")]
7use crate::partitioning::BvhLeafCost;
8
9/// A ray for ray-casting queries.
10///
11/// A ray is a half-infinite line starting at an origin point and extending
12/// infinitely in a direction. Rays are fundamental for visibility queries,
13/// shooting mechanics, and collision prediction.
14///
15/// # Structure
16///
17/// - **origin**: The starting point of the ray
18/// - **dir**: The direction vector (does NOT need to be normalized)
19///
20/// # Direction Vector
21///
22/// The direction can be any non-zero vector:
23/// - **Normalized**: `dir` with length 1.0 gives time-of-impact in world units
24/// - **Not normalized**: Time-of-impact is scaled by `dir.length()`
25///
26/// Most applications use normalized directions for intuitive results.
27///
28/// # Use Cases
29///
30/// - **Shooting/bullets**: Check what a projectile hits
31/// - **Line of sight**: Check if one object can "see" another
32/// - **Mouse picking**: Select objects by clicking
33/// - **Laser beams**: Simulate light or laser paths
34/// - **Proximity sensing**: Detect obstacles in a direction
35///
36/// # Example
37///
38/// ```rust
39/// # #[cfg(all(feature = "dim3", feature = "f32"))] {
40/// use parry3d::query::{Ray, RayCast};
41/// use parry3d::shape::Ball;
42/// use parry3d::math::{Vector, Pose};
43///
44/// // Create a ray from origin pointing along +X axis
45/// let ray = Ray::new(
46///     Vector::ZERO,
47///     Vector::new(1.0, 0.0, 0.0)  // Normalized direction
48/// );
49///
50/// // Create a ball at position (5, 0, 0) with radius 1
51/// let ball = Ball::new(1.0);
52/// let ball_pos = Pose::translation(5.0, 0.0, 0.0);
53///
54/// // Cast the ray against the ball
55/// if let Some(toi) = ball.cast_ray(&ball_pos, &ray, 100.0, true) {
56///     // Ray hits at t=4.0 (center at 5.0 minus radius 1.0)
57///     assert_eq!(toi, 4.0);
58///
59///     // Compute the actual hit point
60///     let hit_point = ray.point_at(toi);
61///     assert_eq!(hit_point, Vector::new(4.0, 0.0, 0.0));
62/// }
63/// # }
64/// ```
65#[derive(Debug, Clone, Copy)]
66#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
67#[cfg_attr(
68    feature = "rkyv",
69    derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)
70)]
71#[repr(C)]
72pub struct Ray {
73    /// Starting point of the ray.
74    ///
75    /// This is where the ray begins. Vectors along the ray are computed as
76    /// `origin + dir * t` for `t ≥ 0`.
77    pub origin: Vector,
78
79    /// Direction vector of the ray.
80    ///
81    /// This vector points in the direction the ray travels. It does NOT need
82    /// to be normalized, but using a normalized direction makes time-of-impact
83    /// values represent actual distances.
84    pub dir: Vector,
85}
86
87impl Ray {
88    /// Creates a new ray from an origin point and direction vector.
89    ///
90    /// # Arguments
91    ///
92    /// * `origin` - The starting point of the ray
93    /// * `dir` - The direction vector (typically normalized but not required)
94    ///
95    /// # Example
96    ///
97    /// ```
98    /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
99    /// use parry3d::query::Ray;
100    /// use parry3d::math::Vector;
101    ///
102    /// // Horizontal ray pointing along +X axis
103    /// let ray = Ray::new(
104    ///     Vector::new(0.0, 5.0, 0.0),
105    ///     Vector::new(1.0, 0.0, 0.0)
106    /// );
107    ///
108    /// // Ray starts at (0, 5, 0) and points along +X
109    /// assert_eq!(ray.origin, Vector::new(0.0, 5.0, 0.0));
110    /// assert_eq!(ray.dir, Vector::new(1.0, 0.0, 0.0));
111    /// # }
112    /// ```
113    pub fn new(origin: Vector, dir: Vector) -> Ray {
114        Ray { origin, dir }
115    }
116
117    /// Transforms this ray by the given isometry (translation + rotation).
118    ///
119    /// Both the origin and direction are transformed.
120    ///
121    /// # Example
122    ///
123    /// ```
124    /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
125    /// use parry3d::query::Ray;
126    /// use parry3d::math::{Pose, Vector};
127    ///
128    /// let ray = Ray::new(Vector::ZERO, Vector::X);
129    ///
130    /// // Translate by (5, 0, 0)
131    /// let transform = Pose::translation(5.0, 0.0, 0.0);
132    /// let transformed = ray.transform_by(&transform);
133    ///
134    /// assert_eq!(transformed.origin, Vector::new(5.0, 0.0, 0.0));
135    /// assert_eq!(transformed.dir, Vector::X);
136    /// # }
137    /// ```
138    #[inline]
139    pub fn transform_by(&self, m: &Pose) -> Self {
140        Self::new(m * self.origin, m.rotation * self.dir)
141    }
142
143    /// Transforms this ray by the inverse of the given isometry.
144    ///
145    /// This is equivalent to transforming the ray to the local space of an object.
146    ///
147    /// # Example
148    ///
149    /// ```
150    /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
151    /// use parry3d::query::Ray;
152    /// use parry3d::math::{Pose, Vector};
153    ///
154    /// let ray = Ray::new(Vector::new(10.0, 0.0, 0.0), Vector::X);
155    ///
156    /// let transform = Pose::translation(5.0, 0.0, 0.0);
157    /// let local_ray = ray.inverse_transform_by(&transform);
158    ///
159    /// // Origin moved back by the translation
160    /// assert_eq!(local_ray.origin, Vector::new(5.0, 0.0, 0.0));
161    /// # }
162    /// ```
163    #[inline]
164    pub fn inverse_transform_by(&self, m: &Pose) -> Self {
165        Self::new(
166            m.inverse_transform_point(self.origin),
167            m.rotation.inverse() * self.dir,
168        )
169    }
170
171    /// Translates this ray by the given vector.
172    ///
173    /// Only the origin is moved; the direction remains unchanged.
174    ///
175    /// # Example
176    ///
177    /// ```
178    /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
179    /// use parry3d::query::Ray;
180    /// use parry3d::math::Vector;
181    ///
182    /// let ray = Ray::new(Vector::ZERO, Vector::X);
183    /// let translated = ray.translate_by(Vector::new(10.0, 5.0, 0.0));
184    ///
185    /// assert_eq!(translated.origin, Vector::new(10.0, 5.0, 0.0));
186    /// assert_eq!(translated.dir, Vector::X); // Direction unchanged
187    /// # }
188    /// ```
189    #[inline]
190    pub fn translate_by(&self, v: Vector) -> Self {
191        Self::new(self.origin + v, self.dir)
192    }
193
194    /// Computes a point along the ray at parameter `t`.
195    ///
196    /// Returns `origin + dir * t`. For `t ≥ 0`, this gives points along the ray.
197    ///
198    /// # Arguments
199    ///
200    /// * `t` - The parameter (typically the time-of-impact from ray casting)
201    ///
202    /// # Example
203    ///
204    /// ```
205    /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
206    /// use parry3d::query::Ray;
207    /// use parry3d::math::Vector;
208    ///
209    /// let ray = Ray::new(
210    ///     Vector::ZERO,
211    ///     Vector::new(1.0, 0.0, 0.0)
212    /// );
213    ///
214    /// // Vector at t=5.0
215    /// assert_eq!(ray.point_at(5.0), Vector::new(5.0, 0.0, 0.0));
216    ///
217    /// // Vector at t=0.0 is the origin
218    /// assert_eq!(ray.point_at(0.0), ray.origin);
219    /// # }
220    /// ```
221    #[inline]
222    pub fn point_at(&self, t: Real) -> Vector {
223        self.origin + self.dir * t
224    }
225}
226
227/// Result of a successful ray cast against a shape.
228///
229/// This structure contains all information about where and how a ray intersects
230/// a shape, including the time of impact, surface normal, and geometric feature hit.
231///
232/// # Fields
233///
234/// - **time_of_impact**: The `t` parameter where the ray hits (use with `ray.point_at(t)`)
235/// - **normal**: The surface normal at the hit point
236/// - **feature**: Which geometric feature was hit (vertex, edge, or face)
237///
238/// # Time of Impact
239///
240/// The time of impact is the parameter `t` in the ray equation `origin + dir * t`:
241/// - If `dir` is normalized: `t` represents the distance traveled
242/// - If `dir` is not normalized: `t` represents time (distance / speed)
243///
244/// # Normal Direction
245///
246/// The normal behavior depends on the ray origin and `solid` parameter:
247/// - **Outside solid shape**: Normal points outward from the surface
248/// - **Inside non-solid shape**: Normal points inward (toward the interior)
249/// - **At t=0.0**: Normal may be unreliable due to numerical precision
250///
251/// # Example
252///
253/// ```rust
254/// # #[cfg(all(feature = "dim3", feature = "f32"))] {
255/// use parry3d::query::{Ray, RayCast};
256/// use parry3d::shape::Cuboid;
257/// use parry3d::math::{Vector, Pose};
258///
259/// let cuboid = Cuboid::new(Vector::new(1.0, 1.0, 1.0));
260/// let ray = Ray::new(
261///     Vector::new(-5.0, 0.0, 0.0),
262///     Vector::new(1.0, 0.0, 0.0)
263/// );
264///
265/// if let Some(intersection) = cuboid.cast_ray_and_get_normal(
266///     &Pose::identity(),
267///     &ray,
268///     100.0,
269///     true
270/// ) {
271///     // Ray hits the -X face of the cuboid at x=-1.0
272///     assert_eq!(intersection.time_of_impact, 4.0); // Distance from -5 to -1
273///
274///     // Hit point is at (-1, 0, 0) - the surface of the cuboid
275///     let hit_point = ray.point_at(intersection.time_of_impact);
276///     assert_eq!(hit_point.x, -1.0);
277///
278///     // Normal points outward (in -X direction for the -X face)
279///     assert_eq!(intersection.normal, Vector::new(-1.0, 0.0, 0.0));
280/// }
281/// # }
282/// ```
283#[derive(Copy, Clone, Debug)]
284#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
285#[cfg_attr(
286    feature = "rkyv",
287    derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)
288)]
289pub struct RayIntersection {
290    /// The time of impact (parameter `t`) where the ray hits the shape.
291    ///
292    /// The exact hit point can be computed with `ray.point_at(time_of_impact)`.
293    /// If the ray direction is normalized, this represents the distance traveled.
294    pub time_of_impact: Real,
295
296    /// The surface normal at the intersection point.
297    ///
298    /// - Typically points outward from the shape
299    /// - May point inward if ray origin is inside a non-solid shape
300    /// - May be unreliable if `time_of_impact` is exactly zero
301    ///
302    /// Note: This should be a unit vector but is not enforced by the type system yet.
303    // TODO: use a Vector instead.
304    pub normal: Vector,
305
306    /// The geometric feature (vertex, edge, or face) that was hit.
307    ///
308    /// This can be used for more detailed collision response or to identify
309    /// exactly which part of the shape was struck.
310    pub feature: FeatureId,
311}
312
313impl RayIntersection {
314    #[inline]
315    /// Creates a new `RayIntersection`.
316    #[cfg(feature = "dim3")]
317    pub fn new(time_of_impact: Real, normal: Vector, feature: FeatureId) -> RayIntersection {
318        RayIntersection {
319            time_of_impact,
320            normal,
321            feature,
322        }
323    }
324
325    #[inline]
326    /// Creates a new `RayIntersection`.
327    #[cfg(feature = "dim2")]
328    pub fn new(time_of_impact: Real, normal: Vector, feature: FeatureId) -> RayIntersection {
329        RayIntersection {
330            time_of_impact,
331            normal,
332            feature,
333        }
334    }
335
336    #[inline]
337    pub fn transform_by(&self, transform: &Pose) -> Self {
338        RayIntersection {
339            time_of_impact: self.time_of_impact,
340            normal: transform.rotation * self.normal,
341            feature: self.feature,
342        }
343    }
344}
345
346#[cfg(feature = "alloc")]
347impl BvhLeafCost for RayIntersection {
348    #[inline]
349    fn cost(&self) -> Real {
350        self.time_of_impact
351    }
352}
353
354/// Traits of objects which can be transformed and tested for intersection with a ray.
355pub trait RayCast {
356    /// Computes the time of impact between this transform shape and a ray.
357    fn cast_local_ray(&self, ray: &Ray, max_time_of_impact: Real, solid: bool) -> Option<Real> {
358        self.cast_local_ray_and_get_normal(ray, max_time_of_impact, solid)
359            .map(|inter| inter.time_of_impact)
360    }
361
362    /// Computes the time of impact, and normal between this transformed shape and a ray.
363    fn cast_local_ray_and_get_normal(
364        &self,
365        ray: &Ray,
366        max_time_of_impact: Real,
367        solid: bool,
368    ) -> Option<RayIntersection>;
369
370    /// Tests whether a ray intersects this transformed shape.
371    #[inline]
372    fn intersects_local_ray(&self, ray: &Ray, max_time_of_impact: Real) -> bool {
373        self.cast_local_ray(ray, max_time_of_impact, true).is_some()
374    }
375
376    /// Computes the time of impact between this transform shape and a ray.
377    fn cast_ray(&self, m: &Pose, ray: &Ray, max_time_of_impact: Real, solid: bool) -> Option<Real> {
378        let ls_ray = ray.inverse_transform_by(m);
379        self.cast_local_ray(&ls_ray, max_time_of_impact, solid)
380    }
381
382    /// Computes the time of impact, and normal between this transformed shape and a ray.
383    fn cast_ray_and_get_normal(
384        &self,
385        m: &Pose,
386        ray: &Ray,
387        max_time_of_impact: Real,
388        solid: bool,
389    ) -> Option<RayIntersection> {
390        let ls_ray = ray.inverse_transform_by(m);
391        self.cast_local_ray_and_get_normal(&ls_ray, max_time_of_impact, solid)
392            .map(|inter| inter.transform_by(m))
393    }
394
395    /// Tests whether a ray intersects this transformed shape.
396    #[inline]
397    fn intersects_ray(&self, m: &Pose, ray: &Ray, max_time_of_impact: Real) -> bool {
398        let ls_ray = ray.inverse_transform_by(m);
399        self.intersects_local_ray(&ls_ray, max_time_of_impact)
400    }
401}