scene_queries_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
let ray = Ray::new(Vector::new(1.0, 2.0), Vector::new(0.0, 1.0));
let max_toi = 4.0;
let solid = true;
let filter = QueryFilter::default();
let query_pipeline = world.query_pipeline_with_filter(filter);
if let Some((handle, toi)) = query_pipeline.cast_ray(
&ray, max_toi, solid
) {
// The first collider hit has the handle `handle` and it hit after
// the ray travelled a distance equal to `ray.dir * toi`.
let hit_point = ray.point_at(toi); // Same as: `ray.origin + ray.dir * toi`
println!("Collider {:?} hit at point {}", handle, hit_point);
}
if let Some((handle, intersection)) = query_pipeline.cast_ray_and_get_normal(
&ray, max_toi, solid
) {
// This is similar to `QueryPipeline::cast_ray` illustrated above except
// that it also returns the normal of the collider shape at the hit point.
let hit_point = ray.point_at(intersection.time_of_impact);
let hit_normal = intersection.normal;
println!("Collider {:?} hit at point {} with normal {}", handle, hit_point, hit_normal);
}
for (handle, _, intersection) in query_pipeline.intersect_ray(ray, max_toi, solid) {
// Callback called on each collider hit by the ray.
let hit_point = ray.point_at(intersection.time_of_impact);
let hit_normal = intersection.normal;
println!("Collider {:?} hit at point {} with normal {}", handle, hit_point, hit_normal);
}
let ray = Ray::new(Vector::new(1.0, 2.0, 3.0), Vector::new(0.0, 1.0, 0.0));
let max_toi = 4.0;
let solid = true;
let filter = QueryFilter::default();
let query_pipeline = world.query_pipeline_with_filter(filter);
if let Some((handle, toi)) = query_pipeline.cast_ray(
&ray, max_toi, solid
) {
// The first collider hit has the handle `handle` and it hit after
// the ray travelled a distance equal to `ray.dir * toi`.
let hit_point = ray.point_at(toi); // Same as: `ray.origin + ray.dir * toi`
println!("Collider {:?} hit at point {}", handle, hit_point);
}
if let Some((handle, intersection)) = query_pipeline.cast_ray_and_get_normal(
&ray, max_toi, solid
) {
// This is similar to `QueryPipeline::cast_ray` illustrated above except
// that it also returns the normal of the collider shape at the hit point.
let hit_point = ray.point_at(intersection.time_of_impact);
let hit_normal = intersection.normal;
println!("Collider {:?} hit at point {} with normal {}", handle, hit_point, hit_normal);
}
for (handle, _, intersection) in query_pipeline.intersect_ray(ray, max_toi, solid) {
// Callback called on each collider hit by the ray.
let hit_point = ray.point_at(intersection.time_of_impact);
let hit_normal = intersection.normal;
println!("Collider {:?} hit at point {} with normal {}", handle, hit_point, hit_normal);
}
- 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.
- Example 2D
- Example 3D
let ray = new RAPIER.Ray({ x: 1.0, y: 2.0 }, { x: 0.0, y: 1.0 });
let maxToi = 4.0;
let solid = true;
let hit = world.castRay(ray, maxToi, solid);
if (hit != null) {
// The first collider hit has the handle `hit.colliderHandle` and it hit after
// the ray travelled a distance equal to `ray.dir * toi`.
let hitPoint = ray.pointAt(hit.timeOfImpact); // Same as: `ray.origin + ray.dir * toi`
console.log("Collider", hit.collider, "hit at point", hitPoint);
}
let hitWithNormal = world.castRayAndGetNormal(ray, maxToi, solid);
if (hitWithNormal != null) {
// This is similar to `QueryPipeline::cast_ray` illustrated above except
// that it also returns the normal of the collider shape at the hit point.
let hitPoint = ray.pointAt(hitWithNormal.timeOfImpact);
console.log("Collider", hitWithNormal.collider, "hit at point", hitPoint, "with normal", hitWithNormal.normal);
}
world.intersectionsWithRay(ray, maxToi, solid, (hit) => {
// Callback called on each collider hit by the ray.
let hitPoint = ray.pointAt(hit.timeOfImpact);
console.log("Collider", hit.collider, "hit at point", hitPoint, "with normal", hit.normal);
return true; // Return `false` instead if we want to stop searching for other hits.
});
let ray = new RAPIER.Ray({ x: 1.0, y: 2.0, z: 3.0 }, { x: 0.0, y: 1.0, z: 0.0 });
let maxToi = 4.0;
let solid = true;
let hit = world.castRay(ray, maxToi, solid);
if (hit != null) {
// The first collider hit has the handle `hit.colliderHandle` and it hit after
// the ray travelled a distance equal to `ray.dir * toi`.
let hitPoint = ray.pointAt(hit.timeOfImpact); // Same as: `ray.origin + ray.dir * toi`
console.log("Collider", hit.collider, "hit at point", hitPoint);
}
let hitWithNormal = world.castRayAndGetNormal(ray, maxToi, solid);
if (hitWithNormal != null) {
// This is similar to `QueryPipeline::cast_ray` illustrated above except
// that it also returns the normal of the collider shape at the hit point.
let hitPoint = ray.pointAt(hitWithNormal.timeOfImpact);
console.log("Collider", hitWithNormal.collider, "hit at point", hitPoint, "with normal", hitWithNormal.normal);
}
world.intersectionsWithRay(ray, maxToi, solid, (hit) => {
// Callback called on each collider hit by the ray.
let hitPoint = ray.pointAt(hit.timeOfImpact);
console.log("Collider", hit.collider, "hit at point", hitPoint, "with normal", hit.normal);
return true; // Return `false` instead if we want to stop searching for other hits.
});
- Example 2D
- Example 3D
R2Vector ray_origin = r2Vector(1.0, 2.0);
R2Vector ray_dir = r2Vector(0.0, 1.0);
R2Real max_toi = 4.0;
R2Bool solid = 1;
R2QueryOptions options = r2DefaultQueryOptions();
R2RayToi toi = r2CastRayToi(world, &options, ray_origin, ray_dir, max_toi, solid);
if (toi.found) {
// The first collider hit has the handle `toi.collider` and it hit after
// the ray travelled a distance equal to `ray_dir * toi.toi`.
R2Vector hit_point = r2VectorAdd(ray_origin, r2VectorScale(ray_dir, toi.toi));
printf("Collider %u hit at point (%f, %f)\n", toi.collider.index, (double)hit_point.x,
(double)hit_point.y);
}
R2OptionalRayHit result = r2TryCastRay(world, &options, ray_origin, ray_dir, max_toi, solid);
if (result.found) {
R2RayHit hit = result.hit;
// This is similar to `r2CastRayToi` illustrated above except
// that it also returns the normal of the collider shape at the hit point.
R2Vector hit_point = r2VectorAdd(ray_origin, r2VectorScale(ray_dir, hit.time_of_impact));
R2Vector hit_normal = hit.normal;
printf("Collider %u hit at point (%f, %f) with normal (%f, %f)\n",
hit.collider.index, (double)hit_point.x, (double)hit_point.y,
(double)hit_normal.x, (double)hit_normal.y);
}
R3Vector ray_origin = r3Vector(1.0, 2.0, 3.0);
R3Vector ray_dir = r3Vector(0.0, 1.0, 0.0);
R3Real max_toi = 4.0;
R3Bool solid = 1;
R3QueryOptions options = r3DefaultQueryOptions();
R3RayToi toi = r3CastRayToi(world, &options, ray_origin, ray_dir, max_toi, solid);
if (toi.found) {
// The first collider hit has the handle `toi.collider` and it hit after
// the ray travelled a distance equal to `ray_dir * toi.toi`.
R3Vector hit_point = r3VectorAdd(ray_origin, r3VectorScale(ray_dir, toi.toi));
printf("Collider %u hit at point (%f, %f, %f)\n", toi.collider.index, (double)hit_point.x,
(double)hit_point.y, (double)hit_point.z);
}
R3OptionalRayHit result = r3TryCastRay(world, &options, ray_origin, ray_dir, max_toi, solid);
if (result.found) {
R3RayHit hit = result.hit;
// This is similar to `r3CastRayToi` illustrated above except
// that it also returns the normal of the collider shape at the hit point.
R3Vector hit_point = r3VectorAdd(ray_origin, r3VectorScale(ray_dir, hit.time_of_impact));
R3Vector hit_normal = hit.normal;
printf("Collider %u hit at point (%f, %f, %f) with normal (%f, %f, %f)\n",
hit.collider.index, (double)hit_point.x, (double)hit_point.y,
(double)hit_point.z, (double)hit_normal.x, (double)hit_normal.y,
(double)hit_normal.z);
}
r3CastRayToi only gives the handle of the first collider hit and the time-of-impact, whereas r3TryCastRay also
gives the world-space normal of the collider's shape at the hit point, as well as the feature of the shape that was
hit (a vertex, an edge, or a face, identified by feature_type and feature_id). Both set the found field of their
result to 0 if the ray doesn't hit anything. Note that r3CastRay gives the same result as r3TryCastRay but reports
a miss as the R3_NOT_FOUND error: it is only suitable if the ray is expected to always hit something.
Finally, r3IntersectRay gives the hits of every collider intersected by the ray (in no particular order), with the
same details as r3TryCastRay:
- Example 2D
- Example 3D
// Get the number of colliders hit by the ray, then copy all their hits.
size_t count = r2IntersectRay(world, &options, ray_origin, ray_dir, max_toi, solid, NULL, 0);
R2RayHit *hits = malloc(count * sizeof(*hits));
count = r2IntersectRay(world, &options, ray_origin, ray_dir, max_toi, solid, hits, count);
for (size_t i = 0; i < count; i++) {
// Loop on each collider hit by the ray.
R2Vector hit_point = r2VectorAdd(ray_origin, r2VectorScale(ray_dir, hits[i].time_of_impact));
R2Vector hit_normal = hits[i].normal;
printf("Collider %u hit at point (%f, %f) with normal (%f, %f)\n",
hits[i].collider.index, (double)hit_point.x, (double)hit_point.y,
(double)hit_normal.x, (double)hit_normal.y);
}
free(hits);
// Get the number of colliders hit by the ray, then copy all their hits.
size_t count = r3IntersectRay(world, &options, ray_origin, ray_dir, max_toi, solid, NULL, 0);
R3RayHit *hits = malloc(count * sizeof(*hits));
count = r3IntersectRay(world, &options, ray_origin, ray_dir, max_toi, solid, hits, count);
for (size_t i = 0; i < count; i++) {
// Loop on each collider hit by the ray.
R3Vector hit_point = r3VectorAdd(ray_origin, r3VectorScale(ray_dir, hits[i].time_of_impact));
R3Vector hit_normal = hits[i].normal;
printf("Collider %u hit at point (%f, %f, %f) with normal (%f, %f, %f)\n",
hits[i].collider.index, (double)hit_point.x, (double)hit_point.y,
(double)hit_point.z, (double)hit_normal.x, (double)hit_normal.y,
(double)hit_normal.z);
}
free(hits);
ray = rp.Ray(origin=(1.0, 2.0, 3.0), dir=(0.0, 1.0, 0.0))
max_toi = 4.0
solid = True
query_filter = rp.QueryFilter()
query_pipeline = world.query_pipeline
hit = query_pipeline.cast_ray(ray, max_toi, solid, filter=query_filter)
if hit is not None:
handle, toi = hit
# The first collider hit has the handle `handle` and it hit after
# the ray travelled a distance equal to `ray.dir * toi`.
hit_point = ray.point_at(toi) # Same as: `ray.origin + ray.dir * toi`
print(f"Collider {handle} hit at point {hit_point}")
hit = query_pipeline.cast_ray_and_get_normal(ray, max_toi, solid, filter=query_filter)
if hit is not None:
handle, intersection = hit
# This is similar to `QueryPipeline.cast_ray` illustrated above except
# that it also returns the normal of the collider shape at the hit point.
hit_point = ray.point_at(intersection.time_of_impact)
hit_normal = intersection.normal
print(f"Collider {handle} hit at point {hit_point} with normal {hit_normal}")
def on_ray_hit(handle, intersection):
# Callback called on each collider hit by the ray.
hit_point = ray.point_at(intersection.time_of_impact)
hit_normal = intersection.normal
print(f"Collider {handle} hit at point {hit_point} with normal {hit_normal}")
return True # Return `False` to stop the search.
query_pipeline.intersect_ray(ray, max_toi, solid, on_ray_hit, filter=query_filter)
QueryPipeline.cast_ray only gives the handle of the first collider hit and the time-of-impact, whereas
QueryPipeline.cast_ray_and_get_normal also gives a RayIntersection with the world-space normal of the collider's
shape at the hit point, as well as the feature of the shape that was hit (a vertex, an edge, or a face, identified by
a FeatureId). Both return None if the ray doesn't hit anything. Finally, QueryPipeline.intersect_ray calls the
given function with the handle and the RayIntersection of every collider intersected by the ray (in no particular
order), until this function returns False.
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_toimaxToimax_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 atmax_toiray.origin moving at a linear velocity equal tooriginray.dir . Therefore,directionmax_toilimits the ray-cast to the segment:[ray.origin, ray.origin + ray.dir * max_toi] .[origin, origin + direction * max_toi]solid: this argument controls the behavior of the ray-cast ifray.origin is inside of a shape: iforiginsolidistruetrue1 then the hit point will be the ray origin itself (Truetoi = 0.0) because the interior of the shape will be assumed to be filled with material. Ifsolidisfalsefalse0 then 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:False
In addition, it is possible to only apply the scene query to a subsets of the colliders using a query filter.