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::{math::RVec3, prelude::*};
44//! use bevy::prelude::*;
45//!
46//! # #[cfg(feature = "3d")]
47//! fn setup(mut commands: Commands) {
48//! // Spawn a ray caster at the center with the rays travelling right
49//! commands.spawn(RayCaster::new(RVec3::ZERO, Dir3::X));
50//! // ...spawn colliders and other things
51//! }
52//!
53//! # #[cfg(feature = "3d")]
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.get_global_point(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::{math::RVec3, prelude::*};
99//! use bevy::prelude::*;
100//!
101//! # #[cfg(feature = "3d")]
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//! RVec3::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
153mod query_filter;
154mod ray_caster;
155#[cfg(any(feature = "parry-f32", feature = "parry-f64"))]
156mod shape_caster;
157#[cfg(any(feature = "parry-f32", feature = "parry-f64"))]
158mod system_param;
159
160mod diagnostics;
161pub use diagnostics::SpatialQueryDiagnostics;
162
163pub use query_filter::*;
164pub use ray_caster::*;
165#[cfg(any(feature = "parry-f32", feature = "parry-f64"))]
166pub use shape_caster::*;
167#[cfg(any(feature = "parry-f32", feature = "parry-f64"))]
168pub use system_param::*;
169
170use crate::prelude::*;
171use bevy::{
172 ecs::{intern::Interned, schedule::ScheduleLabel},
173 prelude::*,
174};
175
176/// Handles component-based [spatial queries](spatial_query) like [raycasting](spatial_query#raycasting)
177/// and [shapecasting](spatial_query#shapecasting) with [`RayCaster`] and [`ShapeCaster`].
178pub struct SpatialQueryPlugin {
179 schedule: Interned<dyn ScheduleLabel>,
180}
181
182impl SpatialQueryPlugin {
183 /// Creates a [`SpatialQueryPlugin`] with the schedule that is used for running the [`PhysicsSchedule`].
184 ///
185 /// The default schedule is `FixedPostUpdate`.
186 pub fn new(schedule: impl ScheduleLabel) -> Self {
187 Self {
188 schedule: schedule.intern(),
189 }
190 }
191}
192
193impl Default for SpatialQueryPlugin {
194 fn default() -> Self {
195 Self::new(FixedPostUpdate)
196 }
197}
198
199impl Plugin for SpatialQueryPlugin {
200 fn build(&self, app: &mut App) {
201 app.configure_sets(
202 self.schedule,
203 SpatialQuerySystems.after(TransformSystems::Propagate),
204 );
205
206 app.add_systems(
207 self.schedule,
208 (
209 update_ray_caster_positions,
210 #[cfg(all(
211 feature = "default-collider",
212 any(feature = "parry-f32", feature = "parry-f64")
213 ))]
214 (update_shape_caster_positions, raycast, shapecast).chain(),
215 )
216 .chain()
217 .in_set(SpatialQuerySystems),
218 );
219 }
220
221 fn finish(&self, app: &mut App) {
222 // Register timer diagnostics for spatial queries.
223 app.register_physics_diagnostics::<SpatialQueryDiagnostics>();
224 }
225}
226
227/// Responsible for updating spatial query components like [`RayCaster`] and [`ShapeCaster`].
228///
229/// See [`SpatialQueryPlugin`].
230#[derive(SystemSet, Clone, Copy, Debug, PartialEq, Eq, Hash)]
231pub struct SpatialQuerySystems;
232
233type RayCasterPositionQueryComponents = (
234 &'static mut RayCaster,
235 Option<&'static Position>,
236 Option<&'static Rotation>,
237 Option<&'static ChildOf>,
238 Option<&'static GlobalTransform>,
239);
240
241#[allow(clippy::type_complexity)]
242fn update_ray_caster_positions(
243 mut rays: Query<RayCasterPositionQueryComponents>,
244 parents: Query<
245 (
246 Option<&Position>,
247 Option<&Rotation>,
248 Option<&GlobalTransform>,
249 ),
250 With<Children>,
251 >,
252) {
253 for (mut ray, position, rotation, parent, transform) in &mut rays {
254 let origin = ray.origin;
255 let direction = ray.direction;
256
257 let global_position = position.copied().or(transform.map(Position::from));
258 let global_rotation = rotation.copied().or(transform.map(Rotation::from));
259
260 if let Some(global_position) = global_position {
261 ray.set_global_origin(
262 global_position.0 + rotation.map_or(origin, |rot| rot.real() * origin),
263 );
264 } else if parent.is_none() {
265 ray.set_global_origin(origin);
266 }
267
268 if let Some(global_rotation) = global_rotation {
269 let global_direction = global_rotation * ray.direction;
270 ray.set_global_direction(global_direction);
271 } else if parent.is_none() {
272 ray.set_global_direction(direction);
273 }
274
275 if let Some(Ok((parent_position, parent_rotation, parent_transform))) =
276 parent.map(|&ChildOf(parent)| parents.get(parent))
277 {
278 let parent_position = parent_position
279 .copied()
280 .or(parent_transform.map(Position::from));
281 let parent_rotation = parent_rotation
282 .copied()
283 .or(parent_transform.map(Rotation::from));
284
285 // Apply parent transformations
286 if global_position.is_none()
287 && let Some(position) = parent_position
288 {
289 let rotation = global_rotation.unwrap_or(parent_rotation.unwrap_or_default());
290 ray.set_global_origin(position.0 + rotation.real() * origin);
291 }
292 if global_rotation.is_none()
293 && let Some(rotation) = parent_rotation
294 {
295 let global_direction = rotation * ray.direction;
296 ray.set_global_direction(global_direction);
297 }
298 }
299 }
300}
301
302#[cfg(any(feature = "parry-f32", feature = "parry-f64"))]
303type ShapeCasterPositionQueryComponents = (
304 &'static mut ShapeCaster,
305 Option<&'static Position>,
306 Option<&'static Rotation>,
307 Option<&'static ChildOf>,
308 Option<&'static GlobalTransform>,
309);
310
311#[cfg(any(feature = "parry-f32", feature = "parry-f64"))]
312#[allow(clippy::type_complexity)]
313fn update_shape_caster_positions(
314 mut shape_casters: Query<ShapeCasterPositionQueryComponents>,
315 parents: Query<
316 (
317 Option<&Position>,
318 Option<&Rotation>,
319 Option<&GlobalTransform>,
320 ),
321 With<Children>,
322 >,
323) {
324 for (mut shape_caster, position, rotation, parent, transform) in &mut shape_casters {
325 let origin = shape_caster.origin;
326 let shape_rotation = shape_caster.shape_rotation;
327 let direction = shape_caster.direction;
328
329 let global_position = position.copied().or(transform.map(Position::from));
330 let global_rotation = rotation.copied().or(transform.map(Rotation::from));
331
332 if let Some(global_position) = global_position {
333 shape_caster.set_global_origin(
334 global_position.0 + rotation.map_or(origin, |rot| rot.real() * origin),
335 );
336 } else if parent.is_none() {
337 shape_caster.set_global_origin(origin);
338 }
339
340 if let Some(global_rotation) = global_rotation.map(Rot::from) {
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.set_global_shape_rotation(shape_rotation * global_rotation);
346 }
347 #[cfg(feature = "3d")]
348 {
349 shape_caster.set_global_shape_rotation(shape_rotation * global_rotation);
350 }
351 } else if parent.is_none() {
352 shape_caster.set_global_direction(direction);
353 #[cfg(feature = "2d")]
354 {
355 shape_caster.set_global_shape_rotation(shape_rotation);
356 }
357 #[cfg(feature = "3d")]
358 {
359 shape_caster.set_global_shape_rotation(shape_rotation);
360 }
361 }
362
363 if let Some(Ok((parent_position, parent_rotation, parent_transform))) =
364 parent.map(|&ChildOf(parent)| parents.get(parent))
365 {
366 let parent_position = parent_position
367 .copied()
368 .or(parent_transform.map(Position::from));
369 let parent_rotation = parent_rotation
370 .copied()
371 .or(parent_transform.map(Rotation::from));
372
373 // Apply parent transformations
374 if global_position.is_none()
375 && let Some(position) = parent_position
376 {
377 let rotation = global_rotation.unwrap_or(parent_rotation.unwrap_or_default());
378 shape_caster.set_global_origin(position.0 + rotation.real() * origin);
379 }
380 if global_rotation.is_none()
381 && let Some(rotation) = parent_rotation.map(Rot::from)
382 {
383 let global_direction = rotation * shape_caster.direction;
384 shape_caster.set_global_direction(global_direction);
385 #[cfg(feature = "2d")]
386 {
387 shape_caster.set_global_shape_rotation(shape_rotation * rotation);
388 }
389 #[cfg(feature = "3d")]
390 {
391 shape_caster.set_global_shape_rotation(shape_rotation * rotation);
392 }
393 }
394 }
395 }
396}
397
398#[cfg(any(feature = "parry-f32", feature = "parry-f64"))]
399fn raycast(
400 mut rays: Query<(Entity, &mut RayCaster, &mut RayHits)>,
401 spatial_query: SpatialQuery,
402 mut diagnostics: ResMut<SpatialQueryDiagnostics>,
403) {
404 let start = crate::utils::Instant::now();
405
406 for (entity, mut ray_caster, mut hits) in &mut rays {
407 if ray_caster.enabled {
408 ray_caster.cast(entity, &mut hits, &spatial_query);
409 } else if !hits.is_empty() {
410 hits.clear();
411 }
412 }
413
414 diagnostics.update_ray_casters = start.elapsed();
415}
416
417#[cfg(any(feature = "parry-f32", feature = "parry-f64"))]
418fn shapecast(
419 mut shape_casters: Query<(Entity, &mut ShapeCaster, &mut ShapeHits)>,
420 spatial_query: SpatialQuery,
421 mut diagnostics: ResMut<SpatialQueryDiagnostics>,
422) {
423 let start = crate::utils::Instant::now();
424
425 for (entity, mut shape_caster, mut hits) in &mut shape_casters {
426 if shape_caster.enabled {
427 shape_caster.cast(entity, &mut hits, &spatial_query);
428 } else if !hits.is_empty() {
429 hits.clear();
430 }
431 }
432
433 diagnostics.update_shape_casters = start.elapsed();
434}