Scene queries
Scene queries are geometric queries that take all the colliders of the physics world into account. These queries are
available through the RapierContext (obtained from the ReadRapierContext system parameter).
The RapierContext is obtained with ReadRapierContext::single (or WriteRapierContext::single). Its scene queries
reuse the acceleration data-structure (BVH) of the broad-phase, which is automatically updated by each
simulation step. Therefore the scene queries take into account the positions of the colliders at the end of the last
timestep: a Transform modified since then won't be taken into account before the next step.
Each scene query of RapierContext creates a temporary RapierQueryPipeline configured by its QueryFilter argument.
If you need to run several queries with the same filter, or if you prefer iterators rather than closures for the
queries returning multiple results, that RapierQueryPipeline can be accessed directly with
RapierContext::with_query_pipeline. It also exposes a few other queries not detailed in this guide (e.g.
project_point_and_get_feature, distance_to_shape, closest_points_to_shape,
contact_with_shape, and the bvh itself for custom traversals):
- Example 2D
- Example 3D
/* Run several scene queries sharing the same filter inside of a system. */
fn run_queries_with_pipeline(rapier_context: ReadRapierContext) {
let rapier_context = rapier_context.single().unwrap();
let filter = QueryFilter::exclude_dynamic();
rapier_context.with_query_pipeline(filter, |query_pipeline| {
// The scene queries take into account the positions of the colliders at the end of
// the last timestep.
let ray_pos = Vec2::new(1.0, 2.0);
let ray_dir = Vec2::new(0.0, -1.0);
if let Some((entity, toi)) = query_pipeline.cast_ray(ray_pos, ray_dir, 4.0, true) {
println!(
"Entity {:?} hit at point {}",
entity,
ray_pos + ray_dir * toi
);
}
// The methods of the `RapierQueryPipeline` return iterators instead of
// calling a closure for each result.
for (entity, collider) in query_pipeline.intersect_point(ray_pos) {
println!(
"The entity {:?} contains the point. Is it a sensor? {}",
entity,
collider.is_sensor()
);
}
});
}
/* Run several scene queries sharing the same filter inside of a system. */
fn run_queries_with_pipeline(rapier_context: ReadRapierContext) {
let rapier_context = rapier_context.single().unwrap();
let filter = QueryFilter::exclude_dynamic();
rapier_context.with_query_pipeline(filter, |query_pipeline| {
// The scene queries take into account the positions of the colliders at the end of
// the last timestep.
let ray_pos = Vec3::new(1.0, 2.0, 3.0);
let ray_dir = Vec3::new(0.0, -1.0, 0.0);
if let Some((entity, toi)) = query_pipeline.cast_ray(ray_pos, ray_dir, 4.0, true) {
println!(
"Entity {:?} hit at point {}",
entity,
ray_pos + ray_dir * toi
);
}
// The methods of the `RapierQueryPipeline` return iterators instead of
// calling a closure for each result.
for (entity, collider) in query_pipeline.intersect_point(ray_pos) {
println!(
"The entity {:?} contains the point. Is it a sensor? {}",
entity,
collider.is_sensor()
);
}
});
}
The queries involving a shape (e.g. intersect_shape, cast_shape, or contact_with_shape) accept any type
implementing the AsShape trait: a Collider can be given directly, as well as any Rapier shape (e.g. a Ball, or
&*shared_shape for a SharedShape). Note that the scene queries rely on the query dispatcher of the physics context,
so a custom dispatcher given to RapierContextSimulation::set_query_dispatcher is taken into account by the scene
queries as well as by the character controller.
Ray-casting
Ray-casting is a geometric query that finds one or several colliders intersecting a half-line. Ray-casting is an extremely common operation that covers a wide variety of use-cases: firing bullets, character controllers, rendering (for ray-tracing), etc.
A ray is defined by its origin and its direction: it can be interpreted as a single point moving in a straight line towards the ray direction.
In addition to the ray geometric information, ray-casting method allow additional control over the behavior of the ray cast like limiting the length of the ray and ignoring some colliders. See the detailed ray-cast arguments description after the next example.
There are multiple ray-casting methods yielding more or less detailed results (see example below). The more results you get, the more computationally expensive the ray-cast will be.
- Example 2D
- Example 3D
/* Cast a ray inside of a system. */
fn cast_ray(rapier_context: ReadRapierContext) {
let rapier_context = rapier_context.single().unwrap();
let ray_pos = Vec2::new(1.0, 2.0);
let ray_dir = Vec2::new(0.0, 1.0);
let max_toi = 4.0;
let solid = true;
let filter = QueryFilter::default();
if let Some((entity, toi)) = rapier_context.cast_ray(ray_pos, ray_dir, max_toi, solid, filter) {
// The first collider hit has the entity `entity` and it hit after
// the ray travelled a distance equal to `ray_dir * toi`.
let hit_point = ray_pos + ray_dir * toi;
println!("Entity {:?} hit at point {}", entity, hit_point);
}
if let Some((entity, intersection)) =
rapier_context.cast_ray_and_get_normal(ray_pos, ray_dir, max_toi, solid, filter)
{
// This is similar to `RapierContext::cast_ray` illustrated above except
// that it also returns the normal of the collider shape at the hit point.
let hit_point = intersection.point;
let hit_normal = intersection.normal;
println!(
"Entity {:?} hit at point {} with normal {}",
entity, hit_point, hit_normal
);
}
rapier_context.intersect_ray(
ray_pos,
ray_dir,
max_toi,
solid,
filter,
|entity, _collider, intersection| {
// Callback called on each collider hit by the ray.
let hit_point = intersection.point;
let hit_normal = intersection.normal;
println!(
"Entity {:?} hit at point {} with normal {}",
entity, hit_point, hit_normal
);
true // Return `false` instead if we want to stop searching for other hits.
},
);
}
/* Cast a ray inside of a system. */
fn cast_ray(rapier_context: ReadRapierContext) {
let rapier_context = rapier_context.single().unwrap();
let ray_pos = Vec3::new(1.0, 2.0, 3.0);
let ray_dir = Vec3::new(0.0, 1.0, 0.0);
let max_toi = 4.0;
let solid = true;
let filter = QueryFilter::default();
if let Some((entity, toi)) = rapier_context.cast_ray(ray_pos, ray_dir, max_toi, solid, filter) {
// The first collider hit has the entity `entity` and it hit after
// the ray travelled a distance equal to `ray_dir * toi`.
let hit_point = ray_pos + ray_dir * toi;
println!("Entity {:?} hit at point {}", entity, hit_point);
}
if let Some((entity, intersection)) =
rapier_context.cast_ray_and_get_normal(ray_pos, ray_dir, max_toi, solid, filter)
{
// This is similar to `RapierContext::cast_ray` illustrated above except
// that it also returns the normal of the collider shape at the hit point.
let hit_point = intersection.point;
let hit_normal = intersection.normal;
println!(
"Entity {:?} hit at point {} with normal {}",
entity, hit_point, hit_normal
);
}
rapier_context.intersect_ray(
ray_pos,
ray_dir,
max_toi,
solid,
filter,
|entity, _collider, intersection| {
// Callback called on each collider hit by the ray.
let hit_point = intersection.point;
let hit_normal = intersection.normal;
println!(
"Entity {:?} hit at point {} with normal {}",
entity, hit_point, hit_normal
);
true // Return `false` instead if we want to stop searching for other hits.
},
);
}
The results identify the collider hit by the entity it is attached to. The resulting RayIntersection contains the
world-space hit point and normal, as well as the index of the part of the shape that was hit (subshape) for
shapes composed of several pieces (compound shapes, triangle meshes, polylines, heightfields, voxels). The closure
given to RapierContext::intersect_ray is also given the Rapier collider (rapier::geometry::Collider, not to be
confused with the Collider component) that was hit, which gives access to its shape, position, parent rigid-body,
etc., without needing an additional ECS query. Returning false from that closure stops the search for other hits.
Aside from the ray being cast, all these ray-casting methods take a few extra parameters for controlling the behavior of the ray-cast:
max_toi: is the maximum "time-of-impact" that can be reported by the ray-cast. The notion of "time-of-impact" refer to the fact that a ray can be seen as a point starting atray.originmoving at a linear velocity equal toray.dir. Therefore,max_toilimits the ray-cast to the segment:[ray.origin, ray.origin + ray.dir * max_toi].solid: this argument controls the behavior of the ray-cast ifray.originis inside of a shape: ifsolidistruethen the hit point will be the ray origin itself (toi = 0.0) because the interior of the shape will be assumed to be filled with material. Ifsolidisfalsethen the shape will be assumed to have an empty interior and the hit point will be the first time the ray hits the shape's boundary. The following 2D example illustrates the difference between the two scenarios. The ray is in green and the resulting hit point circled in red:
In addition, it is possible to only apply the scene query to a subsets of the colliders using a query filter.
Shape-casting
Shape-casting (aka. sweep tests) is the big brother of ray-casting. The only difference with ray-cast is that instead of being a point travelling along a straight line, we have a complete shape travelling along a straight line. This is typically used for character controllers in games to determine by how much the player can move before it hits the environment.
Just like ray-casting, it is possible to control the behavior of the shape-casting like limiting the distance
travelled by the shape cast, and ignoring some colliders. See the details about the
max_toi and filter arguments in the ray-casting section.
The shape-casting along a straight line is performed by RapierContext::cast_shape.
This method has similar arguments as RapierContext::cast_ray except
that the ray is replaced by three arguments: the shape being cast, the initial position of the shape (this is analog to ray.origin) and
the linear velocity the shape is travelling at (this is analog to ray.dir):
- Example 2D
- Example 3D
/* Cast a shape inside of a system. */
fn cast_shape(rapier_context: ReadRapierContext) {
let rapier_context = rapier_context.single().unwrap();
let shape = Collider::cuboid(1.0, 2.0);
let shape_pos = Vec2::new(1.0, 2.0);
let shape_rot = 0.8;
let shape_vel = Vec2::new(0.1, 0.4);
let filter = QueryFilter::default();
let options = ShapeCastOptions {
max_time_of_impact: 4.0,
target_distance: 0.0,
stop_at_penetration: false,
compute_impact_geometry_on_penetration: false,
};
if let Some((entity, hit)) =
rapier_context.cast_shape(shape_pos, shape_rot, shape_vel, &shape, options, filter)
{
// The first collider hit has the entity `entity`. The `hit` is a
// structure containing details about the hit configuration.
println!(
"Hit the entity {:?} with the configuration: {:?}",
entity, hit
);
}
}
/* Cast a shape inside of a system. */
fn cast_shape(rapier_context: ReadRapierContext) {
let rapier_context = rapier_context.single().unwrap();
let shape = Collider::cuboid(1.0, 2.0, 3.0);
let shape_pos = Vec3::new(1.0, 2.0, 3.0);
let shape_rot = Quat::from_rotation_z(0.8);
let shape_vel = Vec3::new(0.1, 0.4, 0.2);
let filter = QueryFilter::default();
let options = ShapeCastOptions {
max_time_of_impact: 4.0,
target_distance: 0.0,
stop_at_penetration: false,
compute_impact_geometry_on_penetration: false,
};
if let Some((entity, hit)) =
rapier_context.cast_shape(shape_pos, shape_rot, shape_vel, &shape, options, filter)
{
// The first collider hit has the entity `entity`. The `hit` is a
// structure containing details about the hit configuration.
println!(
"Hit the entity {:?} with the configuration: {:?}",
entity, hit
);
}
}
The result of the shape-casting includes the entity of the first collider being hit, as well as detailed information about the geometry of the hit:
hit.time_of_impact: indicates the time of impact between the shape and the collider hit. This means that after travelling a distance ofshape_vel * hit.time_of_impactthe collider and the cast shape are exactly touching. Ifhit.time_of_impact == 0.0then the shape is already intersecting a collider at its initial position.hit.witness1: indicates the contact point on the collider hit when the cast shape and the collider are touching, expressed in world-space.hit.witness2: indicates the contact point on the cast shape when the cast shape and the collider are touching, expressed in the local-space of the cast shape.hit.normal1: indicates the outward normal of the collider hit at the contact pointhit.witness1, expressed in world-space.hit.normal2: indicates the outward normal of the cast shape at the contact pointhit.witness2, expressed in the local-space of the cast shape.
Because the cast shape moved, hit.witness2 and hit.normal2 can be converted to world-space by applying the pose of
the cast shape at the time of impact, i.e., its initial pose translated by shape_vel * hit.time_of_impact.
The witness points and normals are grouped into hit.details (a ShapeCastHitDetails). These details are None if
the shape was already intersecting a collider at its initial position (hit.status is then
ShapeCastStatus::PenetratingOrWithinTargetDist) unless ShapeCastOptions::compute_impact_geometry_on_penetration is
set to true. Finally, hit.subshape1 is the index of the part of the collider that was hit if its shape is composed
of several pieces (compound shapes, triangle meshes, etc.)
Note that the frames are different for Collider::cast_shape and Collider::cast_shape_nonlinear, which cast a
collider against another one outside of any physics context: there, every witness point and normal is expressed in the
local-space of its own shape.
Nonlinear shape-casting
The shape-casting above only moves the shape along a straight line: its orientation doesn't change during the cast.
If the rotation of the shape matters, RapierContext::cast_shape_nonlinear performs a
nonlinear shape-casting: the shape follows a rigid motion combining a constant linear velocity and a constant
angular velocity. This motion is described by a NonlinearMotion which contains the initial pose of the
shape, its linear and angular velocities, and the local-space point around which the shape rotates. At time , the
shape is rotated by the angular velocity times around that point, and translated by the linear velocity times .
The first impact is searched for between the start_time and end_time arguments. This is typically useful to
predict if a rotating object (e.g. a spinning blade, a swinging door, or the collider of a rigid-body with a non-zero
angular velocity) will hit something during a timestep.
If the shape is already intersecting a collider at start_time, setting stop_at_penetration to true makes the cast
report that collider with a time of impact equal to start_time. If it is false, that penetration is ignored when the
motion is separating the shapes, and the cast searches for a later impact that would result in tunnelling. The result
has the same form as for cast_shape (the details are None if the hit reported is a penetration at
start_time). Nonlinear shape-casting is more expensive than the linear one, so it is recommended to
use cast_shape whenever the shape doesn't rotate.
Point projection
Point projection will either project a point on the closest collider of the scene (RapierContext::project_point),
or will enumerate every collider containing given point (RapierContext::intersect_point).
- Example 2D
- Example 3D
/* Project a point inside of a system. */
fn project_point(rapier_context: ReadRapierContext) {
let rapier_context = rapier_context.single().unwrap();
let point = Vec2::new(1.0, 2.0);
let max_dist = 4.0; // Colliders further than this distance are ignored.
let solid = true;
let filter = QueryFilter::default();
if let Some((entity, projection)) = rapier_context.project_point(point, max_dist, solid, filter)
{
// The collider closest to the point is attached to `entity`.
println!(
"Projected point on entity {:?}. Point projection: {}",
entity, projection.point
);
println!(
"Point was inside of the collider shape: {}",
projection.is_inside
);
}
rapier_context.intersect_point(point, filter, |entity, _collider| {
// Callback called on each collider with a shape containing the point.
println!("The entity {:?} contains the point.", entity);
// Return `false` instead if we want to stop searching for other colliders containing this point.
true
});
}
/* Project a point inside of a system. */
fn project_point(rapier_context: ReadRapierContext) {
let rapier_context = rapier_context.single().unwrap();
let point = Vec3::new(1.0, 2.0, 3.0);
let max_dist = 4.0; // Colliders further than this distance are ignored.
let solid = true;
let filter = QueryFilter::default();
if let Some((entity, projection)) = rapier_context.project_point(point, max_dist, solid, filter)
{
// The collider closest to the point is attached to `entity`.
println!(
"Projected point on entity {:?}. Point projection: {}",
entity, projection.point
);
println!(
"Point was inside of the collider shape: {}",
projection.is_inside
);
}
rapier_context.intersect_point(point, filter, |entity, _collider| {
// Callback called on each collider with a shape containing the point.
println!("The entity {:?} contains the point.", entity);
// Return `false` instead if we want to stop searching for other colliders containing this point.
true
});
}
The resulting PointProjection also contains the index of the part of the shape the point was projected on
(subshape) for shapes composed of several pieces (compound shapes, triangle meshes, etc.) Just like for ray-casting,
the closure given to RapierContext::intersect_point is given the entity of each collider containing the point, as well
as its Rapier collider, and can return false to stop the search.
It is possible to only apply the scene query to a subsets of the colliders using a query filter
Intersection test
Intersection tests will find all the colliders with a shape intersecting a given shape. This can be useful for, e.g., selecting all the objects that intersect a given area. There are two kind of intersection tests:
- The exact intersection test
RapierContext::intersect_shapesearches for all the colliders with shapes intersecting the given shape. - The approximate intersection test
RapierContext::intersect_aabb_conservativesearches for all the colliders with an AABB intersecting the given AABB. This does not check if the actual shapes of these colliders intersect the AABB. Note that the AABB taken into account is the one currently stored in the BVH of the broad-phase (updated by each simulation step): it isn't recomputed from the latest collider positions.
See the ray-casting section for details about intersection tests between a ray and the colliders on the scene. And see the point projection section for details about the intersection test between the colliders and a point.
- Example 2D
- Example 3D
/* Test intersections inside of a system. */
fn test_intersections(rapier_context: ReadRapierContext) {
let rapier_context = rapier_context.single().unwrap();
let shape = Collider::cuboid(1.0, 2.0);
let shape_pos = Vec2::new(0.0, 1.0);
let shape_rot = 0.8;
let filter = QueryFilter::default();
rapier_context.intersect_shape(shape_pos, shape_rot, &shape, filter, |entity, _collider| {
println!("The entity {:?} intersects our shape.", entity);
true // Return `false` instead if we want to stop searching for other colliders intersecting our shape.
});
let aabb = Aabb2d::new(Vec2::new(-1.0, -2.0), Vec2::new(1.0, 2.0));
rapier_context.intersect_aabb_conservative(aabb, filter, |entity, _collider| {
println!(
"The entity {:?} has an AABB intersecting our test AABB",
entity
);
true // Return `false` instead if we want to stop searching for other colliders with an intersecting AABB.
});
}
/* Test intersections inside of a system. */
fn test_intersections(rapier_context: ReadRapierContext) {
let rapier_context = rapier_context.single().unwrap();
let shape = Collider::cuboid(1.0, 2.0, 3.0);
let shape_pos = Vec3::new(0.0, 1.0, 2.0);
let shape_rot = Quat::from_rotation_z(0.8);
let filter = QueryFilter::default();
rapier_context.intersect_shape(shape_pos, shape_rot, &shape, filter, |entity, _collider| {
println!("The entity {:?} intersects our shape.", entity);
true // Return `false` instead if we want to stop searching for other colliders intersecting our shape.
});
let aabb = Aabb3d::new(Vec3::new(-1.0, -2.0, -3.0), Vec3::new(1.0, 2.0, 3.0));
rapier_context.intersect_aabb_conservative(aabb, filter, |entity, _collider| {
println!(
"The entity {:?} has an AABB intersecting our test AABB",
entity
);
true // Return `false` instead if we want to stop searching for other colliders with an intersecting AABB.
});
}
The closures given to these methods are called with the entity of each collider found, as well as its Rapier collider
(rapier::geometry::Collider), and can return false to stop the search. The AABB to test is given as a Bevy
Aabb2d in 2D, or Aabb3d in 3D.
It is possible to only apply the scene query to a subsets of the colliders using a query filter
Query filters
It is common to exclude some colliders from being considered by a scene query. For example, a ray-cast performed for a
character controller will usually want to skip the character itself. Sometimes, we may even want it to ignore both the
character and any collider attached to a dynamic rigid-body, and ignore all sensors. To allow this filtering, most
scene queries take a QueryFilter argument that lets you describe what needs to be excluded. In particular
its fields:
flagsallows you to discard whole families of colliders based on their types or their parent types (e.g. exclude all sensors and all the colliders attached to a dynamic rigid-body).groupsis used to apply the collision group rules for the scene query. The scene query will only consider hits with colliders with collision groups compatible with this collision group (using the bitwise test described in the collision groups section).exclude_collideris the handle of one collider the query must ignore.exclude_rigid_bodyis the handle of one rigid-body with attached colliders the query must ignore.predicateis a user-defined closure to apply any filtering rule. This can be used if the other filtering options above are not flexible enough.
The exclude_collider and exclude_rigid_body fields are set to the entity of the collider or rigid-body to
exclude (instead of its handle). The predicate is given the entity of each collider as well as its Rapier collider
(rapier::geometry::Collider), so its shape, position, or parent can be read without any additional ECS query. Since
the filter only holds a reference to the predicate closure, that closure can borrow other system parameters, e.g., a
Query for reading the components of the collider's entity.
Here is an an example of usage of the query filters with ray-casting:
/* Cast a ray inside of a system. */
fn cast_ray_filtered(
rapier_context: ReadRapierContext,
player_query: Query<Entity, With<Player>>,
custom_data_query: Query<&CustomData>,
) {
let rapier_context = rapier_context.single().unwrap();
let player_handle = player_query.single().unwrap();
let ray_pos = Vec2::new(1.0, 2.0);
let ray_dir = Vec2::new(0.0, 1.0);
let max_toi = 4.0;
let solid = true;
let predicate = |entity, _collider: &_| {
// We can use a query to bevy inside the predicate.
custom_data_query
.get(entity)
.is_ok_and(|custom_data| custom_data.data == 10)
};
let filter = QueryFilter::exclude_dynamic()
.exclude_sensors()
.exclude_rigid_body(player_handle)
.groups(CollisionGroups::new(
Group::GROUP_1 | Group::GROUP_2,
Group::GROUP_1,
))
.predicate(&predicate);
if let Some((entity, toi)) = rapier_context.cast_ray(ray_pos, ray_dir, max_toi, solid, filter) {
// Handle the hit.
}
}