Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add spatial queries #53

Merged
merged 31 commits into from Jul 6, 2023
Merged

Add spatial queries #53

merged 31 commits into from Jul 6, 2023

Conversation

Jondolf
Copy link
Owner

@Jondolf Jondolf commented Jun 29, 2023

This pull request adds a SpatialQueryPlugin that is responsible for ray casting, shape casting, point projection, and intersection tests. There is also a SpatialQueryFilter that can be used to select specific collision layers or to exclude entities.

The SpatialQueryPipeline resource has a Qbvt that is used as an acceleration structure. The implementation of the pipeline and the queries is mostly based on Rapier's QueryPipeline, but modified to fit Bevy better and to provide a more consistent API.

API

There are two types of APIs for spatial queries: the SpatialQuery system parameter and the component based RayCaster and ShapeCaster.

SpatialQuery

SpatialQuery is a new system parameter that provides similar querying methods as Rapier's global context. It is the most feature-rich option and great when you need a lot of control over when and how you want to ray cast for example.

It looks like this:

fn print_hits(spatial_query: SpatialQuery) {
    let hits = spatial_query.ray_hits(
        Vec3::ZERO,                    // Origin
        Vec3::X,                       // Direction
        100.0,                         // Maximum time of impact (travel distance)
        20,                            // Maximum number of hits
        true,                          // Does the ray treat colliders as "solid"
        SpatialQueryFilter::default(), // Query filter
    );

    for hit in hits.iter() {
        println!("Hit entity {:?}", hit.entity);
    }
}

There are similar methods for shape casts, point projection and intersection tests.

RayCaster

RayCaster allows for a more component-based approach to ray casting that can often be more convenient than manually calling query methods. A system is run once per physics frame to fill the RayHits component with the intersections between the ray and the world's colliders.

Unlike the methods in SpatialQuery, RayCaster uses a local origin and direction, so it will move with the body it is attached to or its parent. For example, if you cast rays from a car for a driving AI, you don't need to compute the new origin and direction manually, as the RayCaster will follow the car.

Using RayCaster looks like this:

fn setup(mut commands: Commands) {
    // Spawn a ray at the center travelling right
    commands.spawn(RayCaster::new(Vec3::ZERO, Vec3::X));
    // ...spawn colliders and other things
}

fn print_hits(query: Query<(&RayCaster, &RayHits)>) {
    for (ray, hits) in &query {
        // For the faster iterator that isn't sorted, use `.iter()`
        for hit in hits.iter_sorted() {
            println!(
                "Hit entity {:?} at {} with normal {}",
                hit.entity,
                ray.origin + ray.direction * hit.time_of_impact,
                hit.normal,
            );
        }
    }
}

You can configure the ray caster's properties using various builder methods.

One caveat that isn't instantly clear is that the hits aren't in order by default because of the underlying Qbvh traversal algorithm. If the number of hits is larger than the ray caster's max_hits property, some hits will be missed, and this could be any hit including the closest one. If you want to guarantee that the closest hit is included, max_hits must either be 1 or a value large enough to hold all of the hits.

ShapeCaster

ShapeCaster is very similar to RayCaster, but it also has an associated shape and rotation, and it can only get the first hit.

Using ShapeCaster looks like this:

fn setup(mut commands: Commands) {
    // Spawn a shape caster with a ball shape travelling right starting from the origin
    commands.spawn(ShapeCaster::new(
        Collider::ball(0.5),
        Vec3::ZERO,
        Quat::default(),
        Vec3::X
    ));
}

fn print_hits(query: Query<(&ShapeCaster, &ShapeHit)>) {
    for (shape_caster, hit) in &query {
        if let Some(hit) = hit.0 {
            println!("Hit entity {:?}", hit.entity);
        }
    }
}

SpatialQueryFilter

The SpatialQueryFilter can be used to only include colliders with specific CollisionLayers or to exclude specific entities from spatial queries. There are no flags or predicate yet unlike in Rapier.

Using a SpatialQueryFilter looks like this: (although this example doesn't make sense usage-wise)

fn setup(mut commands: Commands) {
    let object = commands.spawn(Collider::ball(0.5)).id();

    // A query filter that only includes one layer and excludes the `object` entity
    let query_filter = SpatialQueryFilter::new()
        .with_layers(CollisionLayers::from_bits(0b1111, 0b0001))
        .without_entities([object]);

    // Spawn a ray caster with the query filter
    commands.spawn(RayCaster::default().with_query_filter(query_filter));
}

Todo

  • Add remaining intersection test methods to SpatialQuery
    • point_intersections
    • aabb_intersections_with_aabb
    • shape_intersections
  • Add proper documentation
  • Add example

@Jondolf Jondolf added the enhancement New feature or request label Jun 29, 2023
@Jondolf Jondolf merged commit 2966c2b into main Jul 6, 2023
3 checks passed
@Jondolf Jondolf deleted the spatial-queries branch July 6, 2023 21:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

1 participant