Scene queries
Scene queries are geometric queries that take all the colliders of the physics world into account. These queries are
available through the functions taking the R3World as their first argument (e.g. r3TryCastRay).
The scene queries reuse the acceleration data-structure (BVH) of the broad-phase, which is automatically updated by
r3Step (and by r3DetectCollisions). Therefore the scene queries take into account the positions of the colliders
at the end of the last timestep: a collider moved since then (e.g. with r3Collider_SetTranslation) may not be found
at its new position before the next step:
- Example 2D
- Example 3D
// Game loop.
for (int i = 0; i < 10; i++) {
// Stepping the simulation updates the broad-phase the scene queries rely on.
r2Step(world, NULL, NULL);
// The scene queries take into account the positions of the colliders at the end of
// the last timestep. Run the scene queries on `world` here.
}
// Game loop.
for (int i = 0; i < 10; i++) {
// Stepping the simulation updates the broad-phase the scene queries rely on.
r3Step(world, NULL, NULL);
// The scene queries take into account the positions of the colliders at the end of
// the last timestep. Run the scene queries on `world` here.
}
Every scene query takes a pointer to an R3QueryOptions, which selects the colliders taken into account (see the
query filters section). A NULL pointer applies the default options, which
don't exclude any collider. The queries involving a shape (e.g. r3IntersectShape or r3TryCastShape) take an
R3SharedShape, created by one of the shared-shape constructors (e.g. r3CuboidSharedShape) or cloned from the
shape of an existing collider with r3Collider_CloneShape. This shape is owned by the application: it must be freed
with r3FreeSharedShape once it is no longer needed.
The queries finding several colliders copy their results into a buffer given by the application, together with its
capacity. Calling them with a NULL buffer and a zero capacity gives the number of results, then a second call with
a buffer large enough copies them. If the buffer is too small, nothing is copied: the query returns the required
number of elements and reports the R3_BUFFER_TOO_SMALL error.
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
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);
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 atoriginmoving at a linear velocity equal todirection. Therefore,max_toilimits the ray-cast to the segment:[origin, origin + direction * max_toi].solid: this argument controls the behavior of the ray-cast iforiginis inside of a shape: ifsolidis1then 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. Ifsolidis0then 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 query options arguments in the ray-casting section.
The shape-casting along a straight line is performed by r3TryCastShape.
This method has similar arguments as r3TryCastRay except
that the ray is replaced by three arguments: the shape being cast, the initial position of the shape (this is analog to origin) and
the linear velocity the shape is travelling at (this is analog to direction), and the max_toi is replaced by the R3ShapeCastOptions:
- Example 2D
- Example 3D
R2SharedShape *shape = r2CuboidSharedShape(r2Vector(1.0, 2.0));
R2Pose shape_pos = r2Pose(r2Vector(0.0, 1.0), r2Rotation(0.2));
R2Vector shape_vel = r2Vector(0.1, 0.4);
R2QueryOptions options = r2DefaultQueryOptions();
R2ShapeCastOptions cast_options = r2DefaultShapeCastOptions();
cast_options.max_time_of_impact = 4.0;
cast_options.target_distance = 0.0;
cast_options.stop_at_penetration = 0;
cast_options.compute_impact_geometry_on_penetration = 0;
R2OptionalShapeCastHit result = r2TryCastShape(world, &options, shape_pos, shape_vel, shape, cast_options);
if (result.found) {
R2ShapeCastHit hit = result.hit;
// The first collider hit has the handle `hit.collider`. The `hit` is a
// structure containing details about the hit configuration.
printf("Hit the collider %u with the time of impact %f\n", hit.collider.index,
(double)hit.time_of_impact);
}
// The shape is owned by the application.
r2FreeSharedShape(shape);
R3SharedShape *shape = r3CuboidSharedShape(r3Vector(1.0, 2.0, 3.0));
// The rotation is given as a scaled axis, i.e., an axis multiplied by the angle.
R3Vector scaled_axis = r3Vector(0.2, 0.7, 0.1);
R3Pose shape_pos = r3Pose(r3Vector(0.0, 1.0, 0.0),
r3RotationFromAxisAngle(scaled_axis, r3VectorLength(scaled_axis)));
R3Vector shape_vel = r3Vector(0.1, 0.4, 0.2);
R3QueryOptions options = r3DefaultQueryOptions();
R3ShapeCastOptions cast_options = r3DefaultShapeCastOptions();
cast_options.max_time_of_impact = 4.0;
cast_options.target_distance = 0.0;
cast_options.stop_at_penetration = 0;
cast_options.compute_impact_geometry_on_penetration = 0;
R3OptionalShapeCastHit result = r3TryCastShape(world, &options, shape_pos, shape_vel, shape, cast_options);
if (result.found) {
R3ShapeCastHit hit = result.hit;
// The first collider hit has the handle `hit.collider`. The `hit` is a
// structure containing details about the hit configuration.
printf("Hit the collider %u with the time of impact %f\n", hit.collider.index,
(double)hit.time_of_impact);
}
// The shape is owned by the application.
r3FreeSharedShape(shape);
The R3ShapeCastOptions, initialized by r3DefaultShapeCastOptions, control the behavior of the shape-casting:
max_time_of_impactplays the role of themax_toiof the ray-casts: the shape travels at mostshape_vel * max_time_of_impact.target_distancemakes the shape-casting report a hit as soon as the cast shape gets closer than this distance to a collider, instead of waiting for an actual contact.stop_at_penetrationcontrols the behavior of the shape-casting if the shape is already intersecting a collider at its initial position. If it is1, that collider is reported with a time-of-impact equal to zero. If it is0, that penetration is ignored if the motion is separating the shapes, and the shape-casting searches for a later impact.compute_impact_geometry_on_penetrationis detailed below.
r3TryCastShape sets the found field of its result to 0 if the shape doesn't hit anything, whereas r3CastShape
reports this as the R3_NOT_FOUND error.
The result of the shape-casting includes the handle of the first collider being hit (hit.collider),
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.
If the shape was already intersecting a collider at its initial position (hit.status is then
R3_SHAPE_CAST_PENETRATING), the witness points and normals are only reliable if the
compute_impact_geometry_on_penetration field of the R3ShapeCastOptions is set to 1.
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, r3TryCastShapeNonlinear 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 an R3NonlinearRigidMotion which contains the initial pose of the shape (start), its linear and angular
velocities (linvel and angvel), and the local-space point around which the shape rotates (local_center). 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 (start_time must
not be greater than end_time). 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 1 makes the cast
report that collider with a time of impact equal to start_time. If it is 0, 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 r3TryCastShape (with hit.witness1 and hit.normal1 in world-space, and hit.witness2
and hit.normal2 in the local-space of the cast shape, whose pose at the time of impact is given by
r3NonlinearRigidMotion_PositionAtTime). Nonlinear shape-casting is more expensive than the linear one, so it is
recommended to use r3TryCastShape whenever the shape doesn't rotate.
Point projection
Point projection will either project a point on the closest collider of the scene (r3TryProjectPoint),
or will enumerate every collider containing given point (r3IntersectPoint).
- Example 2D
- Example 3D
R2Vector point = r2Vector(1.0, 2.0);
R2Bool solid = 1;
R2Real max_dist = 12.0;
R2QueryOptions options = r2DefaultQueryOptions();
R2OptionalPointProjection result = r2TryProjectPoint(world, &options, point, max_dist, solid);
if (result.found) {
R2PointProjection projection = result.projection;
// The collider closest to the point has the handle `projection.collider`.
printf("Projected point on collider %u. Point projection: (%f, %f)\n", projection.collider.index,
(double)projection.point.x, (double)projection.point.y);
printf("Point was inside of the collider shape: %u\n", projection.is_inside);
}
// Get the number of colliders containing the point, then copy their handles.
size_t count = r2IntersectPoint(world, &options, point, NULL, 0);
R2ColliderHandle *handles = malloc(count * sizeof(*handles));
count = r2IntersectPoint(world, &options, point, handles, count);
for (size_t i = 0; i < count; i++) {
// Loop on each collider with a shape containing the point.
printf("The collider %u contains the point.\n", handles[i].index);
}
free(handles);
R3Vector point = r3Vector(1.0, 2.0, 3.0);
R3Bool solid = 1;
R3Real max_dist = 12.0;
R3QueryOptions options = r3DefaultQueryOptions();
R3OptionalPointProjection result = r3TryProjectPoint(world, &options, point, max_dist, solid);
if (result.found) {
R3PointProjection projection = result.projection;
// The collider closest to the point has the handle `projection.collider`.
printf("Projected point on collider %u. Point projection: (%f, %f, %f)\n", projection.collider.index,
(double)projection.point.x, (double)projection.point.y,
(double)projection.point.z);
printf("Point was inside of the collider shape: %u\n", projection.is_inside);
}
// Get the number of colliders containing the point, then copy their handles.
size_t count = r3IntersectPoint(world, &options, point, NULL, 0);
R3ColliderHandle *handles = malloc(count * sizeof(*handles));
count = r3IntersectPoint(world, &options, point, handles, count);
for (size_t i = 0; i < count; i++) {
// Loop on each collider with a shape containing the point.
printf("The collider %u contains the point.\n", handles[i].index);
}
free(handles);
The resulting R3PointProjection (the projection field of the result) contains the handle of the collider the point was projected on, the projected
point (in world-space), and whether the original point was inside of that collider (is_inside). If the point is
inside of a shape, solid controls the result just like for ray-casting: with
solid set to 1 the point is its own projection, whereas with solid set to 0 it is projected on the boundary of
the shape. r3TryProjectPoint sets the found field of its result to 0 if no collider is closer than max_dist,
whereas r3ProjectPoint reports this as the R3_NOT_FOUND error. Finally, r3IntersectPoint copies the handles
of the colliders containing the point into a buffer given by the application, as described at the beginning of this
page.
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
r3IntersectShapesearches for all the colliders with shapes intersecting the given shape. - The approximate intersection test
r3IntersectAabbConservativesearches 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 call tor3Step): 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
R2SharedShape *shape = r2CuboidSharedShape(r2Vector(1.0, 2.0));
R2Pose shape_pos = r2Pose(r2Vector(0.0, 1.0), r2Rotation(0.2));
R2QueryOptions options = r2DefaultQueryOptions();
// Get the number of colliders intersecting the shape, then copy their handles.
size_t count = r2IntersectShape(world, &options, shape_pos, shape, NULL, 0);
R2ColliderHandle *handles = malloc(count * sizeof(*handles));
count = r2IntersectShape(world, &options, shape_pos, shape, handles, count);
for (size_t i = 0; i < count; i++) {
printf("The collider %u intersects our shape.\n", handles[i].index);
}
free(handles);
r2FreeSharedShape(shape);
R2Aabb aabb = {r2Vector(-1.0, -2.0), r2Vector(1.0, 2.0)};
count = r2IntersectAabbConservative(world, &options, aabb, NULL, 0);
handles = malloc(count * sizeof(*handles));
count = r2IntersectAabbConservative(world, &options, aabb, handles, count);
for (size_t i = 0; i < count; i++) {
printf("The collider %u has an AABB intersecting our test AABB.\n", handles[i].index);
}
free(handles);
R3SharedShape *shape = r3CuboidSharedShape(r3Vector(1.0, 2.0, 3.0));
// The rotation is given as a scaled axis, i.e., an axis multiplied by the angle.
R3Vector scaled_axis = r3Vector(0.2, 0.7, 0.1);
R3Pose shape_pos = r3Pose(r3Vector(0.0, 1.0, 0.0),
r3RotationFromAxisAngle(scaled_axis, r3VectorLength(scaled_axis)));
R3QueryOptions options = r3DefaultQueryOptions();
// Get the number of colliders intersecting the shape, then copy their handles.
size_t count = r3IntersectShape(world, &options, shape_pos, shape, NULL, 0);
R3ColliderHandle *handles = malloc(count * sizeof(*handles));
count = r3IntersectShape(world, &options, shape_pos, shape, handles, count);
for (size_t i = 0; i < count; i++) {
printf("The collider %u intersects our shape.\n", handles[i].index);
}
free(handles);
r3FreeSharedShape(shape);
R3Aabb aabb = {r3Vector(-1.0, -2.0, -3.0), r3Vector(1.0, 2.0, 3.0)};
count = r3IntersectAabbConservative(world, &options, aabb, NULL, 0);
handles = malloc(count * sizeof(*handles));
count = r3IntersectAabbConservative(world, &options, aabb, handles, count);
for (size_t i = 0; i < count; i++) {
printf("The collider %u has an AABB intersecting our test AABB.\n", handles[i].index);
}
free(handles);
Both functions copy the handles of the colliders found into a buffer given by the application, as described at the
beginning of this page. The AABB to test is an R3Aabb, given by its minimum (mins) and maximum (maxs) corners.
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 an R3QueryOptions argument that lets you describe what needs to be excluded. In particular the fields of its filter (an R3QueryFilter), and its predicate:
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 callback to apply any filtering rule. This can be used if the other filtering options above are not flexible enough.
The query options are initialized by r3DefaultQueryOptions, which doesn't exclude any collider. The flags are a
combination of the R3_QUERY_EXCLUDE_* constants (e.g. R3_QUERY_EXCLUDE_SENSORS), or one of the shortcuts
R3_QUERY_ONLY_DYNAMIC, R3_QUERY_ONLY_KINEMATIC, and R3_QUERY_ONLY_FIXED. The groups are only applied if the
use_groups field is set to 1. The exclude_collider and exclude_rigid_body fields are set to an invalid handle
(e.g. R3_INVALID_COLLIDER_HANDLE) to exclude nothing. Finally, the predicate is a callback called for each collider
that passed the other filtering rules: it returns 0 to exclude that collider. It is given the userData field of the
query options, a read-only access to the world (an R3ReadContext to be given to the r3ReadCollider_* and
r3ReadRigidBody_* functions), and the handle of the collider. Other scene queries can be performed from this
callback, but the world cannot be modified until the outer query returns.
Here is an an example of usage of the query filters with ray-casting:
// The predicate is called for each collider that passed the other filtering rules.
// Returning 0 excludes the collider from the scene query.
static R2Bool RAPIER_CALL user_data_predicate(void *user_data, const R2ReadContext *read,
R2ColliderHandle handle) {
(void)user_data;
return r2ReadCollider_UserData(read, handle).low == 10;
}
static void query_filter_section(const R2World *world, R2RigidBodyHandle player_handle) {
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();
options.filter.flags = R2_QUERY_EXCLUDE_DYNAMIC | R2_QUERY_EXCLUDE_SENSORS;
options.filter.exclude_rigid_body = player_handle;
options.filter.use_groups = 1;
options.filter.groups.memberships = 0x0001 | 0x0002; // Groups 1 and 2.
options.filter.groups.filter = 0x0001; // Group 1.
options.filter.groups.test_mode = R2_GROUPS_AND;
options.predicate = user_data_predicate;
options.userData = NULL; // Given to the predicate as its first argument.
R2RayToi toi = r2CastRayToi(world, &options, ray_origin, ray_dir, max_toi, solid);
if (toi.found) {
// Handle the hit.
}
}