Skip to main content

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.

info

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.

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);
}
/* 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.
},
);
}

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.

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.
});
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);
}

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:

// 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);
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_toi maxToi max_toi 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 at ray.originorigin moving at a linear velocity equal to ray.dirdirection. Therefore, max_toi limits 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 if ray.originorigin is inside of a shape: if solid is truetrue1True then 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. If solid is falsefalse0False 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:

solid ray-cast

In addition, it is possible to only apply the scene query to a subsets of the colliders using a query filter.