avian3d/spatial_query/
mod.rs

1//! Functionality for performing ray casts, shape casts, and other spatial queries.
2//!
3//! Spatial queries query the world for geometric information about [`Collider`s](Collider)
4//! and various types of intersections. Currently, four types of spatial queries are supported:
5//!
6//! - [Raycasts](#raycasting)
7//! - [Shapecasts](#shapecasting)
8//! - [Point projection](#point-projection)
9//! - [Intersection tests](#intersection-tests)
10//!
11//! All spatial queries can be done using the various methods provided by the [`SpatialQuery`] system parameter.
12//!
13//! Raycasting and shapecasting can also be done with a component-based approach using the [`RayCaster`] and
14//! [`ShapeCaster`] components. They enable performing casts every frame in a way that is often more convenient
15//! than the normal [`SpatialQuery`] methods. See their documentation for more information.
16//!
17//! # Raycasting
18//!
19//! **Raycasting** is a spatial query that finds intersections between colliders and a half-line. This can be used for
20//! a variety of things like getting information about the environment for character controllers and AI,
21//! and even rendering using ray tracing.
22//!
23//! For each hit during raycasting, the hit entity, a distance, and a normal will be stored in [`RayHitData`].
24//! The distance is the distance from the ray origin to the point of intersection, indicating how far the ray travelled.
25//!
26//! There are two ways to perform raycasts.
27//!
28//! 1. For simple raycasts, use the [`RayCaster`] component. It returns the results of the raycast
29//!    in the [`RayHits`] component every frame. It uses local coordinates, so it will automatically follow the entity
30//!    it's attached to or its parent.
31//! 2. When you need more control or don't want to cast every frame, use the raycasting methods provided by
32//!    [`SpatialQuery`], like [`cast_ray`](SpatialQuery::cast_ray), [`ray_hits`](SpatialQuery::ray_hits) or
33//!    [`ray_hits_callback`](SpatialQuery::ray_hits_callback).
34//!
35//! See the documentation of the components and methods for more information.
36//!
37//! A simple example using the component-based method looks like this:
38//!
39//! ```
40//! # #[cfg(feature = "2d")]
41//! # use avian2d::prelude::*;
42//! # #[cfg(feature = "3d")]
43//! use avian3d::prelude::*;
44//! use bevy::prelude::*;
45//!
46//! # #[cfg(all(feature = "3d", feature = "f32"))]
47//! fn setup(mut commands: Commands) {
48//!     // Spawn a ray caster at the center with the rays travelling right
49//!     commands.spawn(RayCaster::new(Vec3::ZERO, Dir3::X));
50//!     // ...spawn colliders and other things
51//! }
52//!
53//! # #[cfg(all(feature = "3d", feature = "f32"))]
54//! fn print_hits(query: Query<(&RayCaster, &RayHits)>) {
55//!     for (ray, hits) in &query {
56//!         // For the faster iterator that isn't sorted, use `.iter()`
57//!         for hit in hits.iter_sorted() {
58//!             println!(
59//!                 "Hit entity {} at {} with normal {}",
60//!                 hit.entity,
61//!                 ray.origin + *ray.direction * hit.distance,
62//!                 hit.normal,
63//!             );
64//!         }
65//!     }
66//! }
67//! ```
68//!
69//! To specify which colliders should be considered in the query, use a [spatial query filter](`SpatialQueryFilter`).
70//!
71//! # Shapecasting
72//!
73//! **Shapecasting** or **sweep testing** is a spatial query that finds intersections between colliders and a shape
74//! that is travelling along a half-line. It is very similar to [raycasting](#raycasting), but instead of a "point"
75//! we have an entire shape travelling along a half-line. One use case is determining how far an object can move
76//! before it hits the environment.
77//!
78//! For each hit during shapecasting, the hit entity, a distance, two world-space points of intersection and two world-space
79//! normals will be stored in [`ShapeHitData`]. The distance refers to how far the shape travelled before the initial hit.
80//!
81//! There are two ways to perform shapecasts.
82//!
83//! 1. For simple shapecasts, use the [`ShapeCaster`] component. It returns the results of the shapecast
84//!    in the [`ShapeHits`] component every frame. It uses local coordinates, so it will automatically follow the entity
85//!    it's attached to or its parent.
86//! 2. When you need more control or don't want to cast every frame, use the shapecasting methods provided by
87//!    [`SpatialQuery`], like [`cast_shape`](SpatialQuery::cast_shape), [`shape_hits`](SpatialQuery::shape_hits) or
88//!    [`shape_hits_callback`](SpatialQuery::shape_hits_callback).
89//!
90//! See the documentation of the components and methods for more information.
91//!
92//! A simple example using the component-based method looks like this:
93//!
94//! ```
95//! # #[cfg(feature = "2d")]
96//! # use avian2d::prelude::*;
97//! # #[cfg(feature = "3d")]
98//! use avian3d::prelude::*;
99//! use bevy::prelude::*;
100//!
101//! # #[cfg(all(feature = "3d", feature = "f32"))]
102//! fn setup(mut commands: Commands) {
103//!     // Spawn a shape caster with a sphere shape at the center travelling right
104//!     commands.spawn(ShapeCaster::new(
105//!         Collider::sphere(0.5), // Shape
106//!         Vec3::ZERO,            // Origin
107//!         Quat::default(),       // Shape rotation
108//!         Dir3::X                // Direction
109//!     ));
110//!     // ...spawn colliders and other things
111//! }
112//!
113//! fn print_hits(query: Query<(&ShapeCaster, &ShapeHits)>) {
114//!     for (shape_caster, hits) in &query {
115//!         for hit in hits.iter() {
116//!             println!("Hit entity {}", hit.entity);
117//!         }
118//!     }
119//! }
120//! ```
121//!
122//! To specify which colliders should be considered in the query, use a [spatial query filter](`SpatialQueryFilter`).
123//!
124//! # Point projection
125//!
126//! **Point projection** is a spatial query that projects a point on the closest collider. It returns the collider's
127//! entity, the projected point, and whether the point is inside of the collider.
128//!
129//! Point projection can be done with the [`project_point`](SpatialQuery::project_point) method of the [`SpatialQuery`]
130//! system parameter. See its documentation for more information.
131//!
132//! To specify which colliders should be considered in the query, use a [spatial query filter](`SpatialQueryFilter`).
133//!
134//! # Intersection tests
135//!
136//! **Intersection tests** are spatial queries that return the entities of colliders that are intersecting a given
137//! shape or area.
138//!
139//! There are three types of intersection tests. They are all methods of the [`SpatialQuery`] system parameter,
140//! and they all have callback variants that call a given callback on each intersection.
141//!
142//! - [`point_intersections`](SpatialQuery::point_intersections): Finds all entities with a collider that contains
143//!   the given point.
144//! - [`aabb_intersections_with_aabb`](SpatialQuery::aabb_intersections_with_aabb):
145//!   Finds all entities with a [`ColliderAabb`] that is intersecting the given [`ColliderAabb`].
146//! - [`shape_intersections`](SpatialQuery::shape_intersections): Finds all entities with a [collider](Collider)
147//!   that is intersecting the given shape.
148//!
149//! See the documentation of the components and methods for more information.
150//!
151//! To specify which colliders should be considered in the query, use a [spatial query filter](`SpatialQueryFilter`).
152
153#[cfg(any(feature = "parry-f32", feature = "parry-f64"))]
154mod pipeline;
155mod query_filter;
156mod ray_caster;
157#[cfg(any(feature = "parry-f32", feature = "parry-f64"))]
158mod shape_caster;
159#[cfg(any(feature = "parry-f32", feature = "parry-f64"))]
160mod system_param;
161
162mod diagnostics;
163pub use diagnostics::SpatialQueryDiagnostics;
164
165#[cfg(any(feature = "parry-f32", feature = "parry-f64"))]
166pub use pipeline::*;
167pub use query_filter::*;
168pub use ray_caster::*;
169#[cfg(any(feature = "parry-f32", feature = "parry-f64"))]
170pub use shape_caster::*;
171#[cfg(any(feature = "parry-f32", feature = "parry-f64"))]
172pub use system_param::*;
173
174use crate::prelude::*;
175use bevy::prelude::*;
176
177/// Initializes the [`SpatialQueryPipeline`] resource and handles component-based [spatial queries](spatial_query)
178/// like [raycasting](spatial_query#raycasting) and [shapecasting](spatial_query#shapecasting) with
179/// [`RayCaster`] and [`ShapeCaster`].
180pub struct SpatialQueryPlugin;
181
182impl Plugin for SpatialQueryPlugin {
183    fn build(&self, app: &mut App) {
184        #[cfg(all(
185            feature = "default-collider",
186            any(feature = "parry-f32", feature = "parry-f64")
187        ))]
188        app.init_resource::<SpatialQueryPipeline>();
189
190        let physics_schedule = app
191            .get_schedule_mut(PhysicsSchedule)
192            .expect("add PhysicsSchedule first");
193
194        physics_schedule.add_systems(
195            (
196                update_ray_caster_positions,
197                #[cfg(all(
198                    feature = "default-collider",
199                    any(feature = "parry-f32", feature = "parry-f64")
200                ))]
201                (
202                    update_shape_caster_positions,
203                    update_spatial_query_pipeline,
204                    raycast,
205                    shapecast,
206                )
207                    .chain(),
208            )
209                .chain()
210                .in_set(PhysicsStepSet::SpatialQuery),
211        );
212    }
213
214    fn finish(&self, app: &mut App) {
215        // Register timer diagnostics for spatial queries.
216        app.register_physics_diagnostics::<SpatialQueryDiagnostics>();
217    }
218}
219
220/// Updates the [`SpatialQueryPipeline`].
221#[cfg(all(
222    feature = "default-collider",
223    any(feature = "parry-f32", feature = "parry-f64")
224))]
225pub fn update_spatial_query_pipeline(
226    mut spatial_query: SpatialQuery,
227    mut diagnostics: ResMut<SpatialQueryDiagnostics>,
228) {
229    let start = crate::utils::Instant::now();
230
231    spatial_query.update_pipeline();
232
233    diagnostics.update_pipeline = start.elapsed();
234}
235
236type RayCasterPositionQueryComponents = (
237    &'static mut RayCaster,
238    Option<&'static Position>,
239    Option<&'static Rotation>,
240    Option<&'static ChildOf>,
241    Option<&'static GlobalTransform>,
242);
243
244#[allow(clippy::type_complexity)]
245fn update_ray_caster_positions(
246    mut rays: Query<RayCasterPositionQueryComponents>,
247    parents: Query<
248        (
249            Option<&Position>,
250            Option<&Rotation>,
251            Option<&GlobalTransform>,
252        ),
253        With<Children>,
254    >,
255) {
256    for (mut ray, position, rotation, parent, transform) in &mut rays {
257        let origin = ray.origin;
258        let direction = ray.direction;
259
260        let global_position = position.copied().or(transform.map(Position::from));
261        let global_rotation = rotation.copied().or(transform.map(Rotation::from));
262
263        if let Some(global_position) = global_position {
264            ray.set_global_origin(global_position.0 + rotation.map_or(origin, |rot| rot * origin));
265        } else if parent.is_none() {
266            ray.set_global_origin(origin);
267        }
268
269        if let Some(global_rotation) = global_rotation {
270            let global_direction = global_rotation * ray.direction;
271            ray.set_global_direction(global_direction);
272        } else if parent.is_none() {
273            ray.set_global_direction(direction);
274        }
275
276        if let Some(Ok((parent_position, parent_rotation, parent_transform))) =
277            parent.map(|&ChildOf(parent)| parents.get(parent))
278        {
279            let parent_position = parent_position
280                .copied()
281                .or(parent_transform.map(Position::from));
282            let parent_rotation = parent_rotation
283                .copied()
284                .or(parent_transform.map(Rotation::from));
285
286            // Apply parent transformations
287            if global_position.is_none() {
288                if let Some(position) = parent_position {
289                    let rotation = global_rotation.unwrap_or(parent_rotation.unwrap_or_default());
290                    ray.set_global_origin(position.0 + rotation * origin);
291                }
292            }
293            if global_rotation.is_none() {
294                if let Some(rotation) = parent_rotation {
295                    let global_direction = rotation * ray.direction;
296                    ray.set_global_direction(global_direction);
297                }
298            }
299        }
300    }
301}
302
303#[cfg(any(feature = "parry-f32", feature = "parry-f64"))]
304type ShapeCasterPositionQueryComponents = (
305    &'static mut ShapeCaster,
306    Option<&'static Position>,
307    Option<&'static Rotation>,
308    Option<&'static ChildOf>,
309    Option<&'static GlobalTransform>,
310);
311
312#[cfg(any(feature = "parry-f32", feature = "parry-f64"))]
313#[allow(clippy::type_complexity)]
314fn update_shape_caster_positions(
315    mut shape_casters: Query<ShapeCasterPositionQueryComponents>,
316    parents: Query<
317        (
318            Option<&Position>,
319            Option<&Rotation>,
320            Option<&GlobalTransform>,
321        ),
322        With<Children>,
323    >,
324) {
325    for (mut shape_caster, position, rotation, parent, transform) in &mut shape_casters {
326        let origin = shape_caster.origin;
327        let shape_rotation = shape_caster.shape_rotation;
328        let direction = shape_caster.direction;
329
330        let global_position = position.copied().or(transform.map(Position::from));
331        let global_rotation = rotation.copied().or(transform.map(Rotation::from));
332
333        if let Some(global_position) = global_position {
334            shape_caster
335                .set_global_origin(global_position.0 + rotation.map_or(origin, |rot| rot * origin));
336        } else if parent.is_none() {
337            shape_caster.set_global_origin(origin);
338        }
339
340        if let Some(global_rotation) = global_rotation {
341            let global_direction = global_rotation * shape_caster.direction;
342            shape_caster.set_global_direction(global_direction);
343            #[cfg(feature = "2d")]
344            {
345                shape_caster
346                    .set_global_shape_rotation(shape_rotation + global_rotation.as_radians());
347            }
348            #[cfg(feature = "3d")]
349            {
350                shape_caster.set_global_shape_rotation(shape_rotation * global_rotation.0);
351            }
352        } else if parent.is_none() {
353            shape_caster.set_global_direction(direction);
354            #[cfg(feature = "2d")]
355            {
356                shape_caster.set_global_shape_rotation(shape_rotation);
357            }
358            #[cfg(feature = "3d")]
359            {
360                shape_caster.set_global_shape_rotation(shape_rotation);
361            }
362        }
363
364        if let Some(Ok((parent_position, parent_rotation, parent_transform))) =
365            parent.map(|&ChildOf(parent)| parents.get(parent))
366        {
367            let parent_position = parent_position
368                .copied()
369                .or(parent_transform.map(Position::from));
370            let parent_rotation = parent_rotation
371                .copied()
372                .or(parent_transform.map(Rotation::from));
373
374            // Apply parent transformations
375            if global_position.is_none() {
376                if let Some(position) = parent_position {
377                    let rotation = global_rotation.unwrap_or(parent_rotation.unwrap_or_default());
378                    shape_caster.set_global_origin(position.0 + rotation * origin);
379                }
380            }
381            if global_rotation.is_none() {
382                if let Some(rotation) = parent_rotation {
383                    let global_direction = rotation * shape_caster.direction;
384                    shape_caster.set_global_direction(global_direction);
385                    #[cfg(feature = "2d")]
386                    {
387                        shape_caster
388                            .set_global_shape_rotation(shape_rotation + rotation.as_radians());
389                    }
390                    #[cfg(feature = "3d")]
391                    {
392                        shape_caster.set_global_shape_rotation(shape_rotation * rotation.0);
393                    }
394                }
395            }
396        }
397    }
398}
399
400#[cfg(any(feature = "parry-f32", feature = "parry-f64"))]
401fn raycast(
402    mut rays: Query<(Entity, &mut RayCaster, &mut RayHits)>,
403    spatial_query: SpatialQuery,
404    mut diagnostics: ResMut<SpatialQueryDiagnostics>,
405) {
406    let start = crate::utils::Instant::now();
407
408    for (entity, mut ray, mut hits) in &mut rays {
409        if ray.enabled {
410            ray.cast(entity, &mut hits, &spatial_query.query_pipeline);
411        } else if !hits.is_empty() {
412            hits.clear();
413        }
414    }
415
416    diagnostics.update_ray_casters = start.elapsed();
417}
418
419#[cfg(any(feature = "parry-f32", feature = "parry-f64"))]
420fn shapecast(
421    mut shape_casters: Query<(Entity, &ShapeCaster, &mut ShapeHits)>,
422    spatial_query: SpatialQuery,
423    mut diagnostics: ResMut<SpatialQueryDiagnostics>,
424) {
425    let start = crate::utils::Instant::now();
426
427    for (entity, shape_caster, mut hits) in &mut shape_casters {
428        if shape_caster.enabled {
429            shape_caster.cast(entity, &mut hits, &spatial_query.query_pipeline);
430        } else if !hits.is_empty() {
431            hits.clear();
432        }
433    }
434
435    diagnostics.update_shape_casters = start.elapsed();
436}