Spatial queries

In addition to collision callbacks w/ triggers, Obi supports several kinds of spatial queries. These are useful to know which simplices are nearby a given position, which simplices are inside a given shape, or if a ray hits any simplex.

Query methods

The ObiSolver class allows you to perform spatial queries on all particles/simplices being simulated by the solver.

Queries take a QueryShape struct and a AffineTransform struct as input. The former determines the geometry of the query being performed, the latter allows you to set the position/rotation/scale of the query geometry.

Query execution is asynchronous by default, all enqueued queries are sent for execution at soon as a simulation step start. The solver's OnSpatialQueryResults event (passing a list of QueryResult structs) will be called once query results are ready. You can enqueue new queries at any point during execution, but since they won't actually be processed until a simulation step takes place it typically makes sense to do so in the solver's OnSimulate event (which is called right before starting a step).

You can set the solver's synchronousSpatialQueries boolean to true if you want the main thread to wait for query results, however this can be very costly (specially so if using the Compute backend, as this forces the CPU to read query results back from the GPU on the same frame they were issued). Unless your queries depend on the result of other queries enqueued during the same frame, it is recommended to leave synchronousSpatialQueries to its default value (false).

Here's a list of query methods available:

// Multiple general queries: pass a list of heterogeneous query shapes, returns query index for the first query
// All queries are performed in parallel.
int EnqueueSpatialQueries(ObiNativeQueryShapeList shapes, ObiNativeAffineTransformList transforms);
// Single general query: pass a single query shape, returns query index.
int EnqueueSpatialQuery(QueryShape shape, AffineTransform transform);
// Single raycast: pass a single ray, results query index.
int EnqueueRaycast(Ray ray, int filter, float maxDistance = 100, float rayThickness = 0);

QueryShape

Queries take QueryShape structs as input, that describe the query being performed. These are the contents of a QueryShape:

type (QueryType)
Type of query being performed: Box, Sphere, or Ray.
center (Vector4)
Center of the query shape, in the local space defined by the corresponding AffineTransform.
size (Vector4)
Size of the query shape, in the local space defined by the corresponding AffineTransform.
contactOffset (float)
Additional thickness added to this query's shape.
maxDistance (float)
Maximum allowable distance between a simplex and this query shape
filter ()int
32 bit integer describing the collision filter used for this query. Most significant 16 bits encode a collision mask, less significant 16 bits encode a collision category. See collisions.

QueryResult

All queries will return one or more QueryResult structs, containing information about simplices that are close to a queried shape, or hit by a ray. These are the contents of a QueryResult:

simplexIndex (int)
index of the simplex.
queryIndex (int)
index of the query in the input ObiNativeQueryShapeList. Use this to correlate each QueryResult to the query that spawned it.
simplexBary (Vector4)
Barycentric coords of nearest point/ray hit in simplex.
queryPoint (Vector4)
Nearest point in query shape, expressed in solver space.
normal (Vector4)
Shortest direction between simplex and query shape, expressed in solver space.
distance (float)
Distance between simplex and query shape.
distanceAlongRay (float)
for ray queries, distance along the ray of the point closest to the simplex.

Typical usage is to enqueue as many spatial queries as desired, and subscribe to the solver's OnSpatialQueryResults event to get the list of results as soon as they're ready.

Examples

Determining distance from a point for all simplices within a radius of it

The trick here is to use a sphere of radius 0 to represent the point. Then use maxDistance to indicate we are interested in all simplices within a given radius of the point. Note that this sample assumes there are only 0-simplices (particles) in the solver.

using UnityEngine;
using Obi;

public class DistanceToPoint : MonoBehaviour
{
    public ObiSolver solver;
    public float radius = 1;

    int queryIndex;

    private void Start()
    {
        solver.OnSpatialQueryResults += Solver_OnSpatialQueryResults;
        solver.OnSimulate += Solver_OnSimulate;
    }

    private void OnDestroy()
    {
        solver.OnSpatialQueryResults -= Solver_OnSpatialQueryResults;
        solver.OnSimulate -= Solver_OnSimulate;
    }

    private void Solver_OnSimulate(ObiSolver s, float simulatedTime, float substepTime)
    {
        int filter = ObiUtils.MakeFilter(ObiUtils.CollideWithEverything, 0);

        var query = new QueryShape(QueryShape.QueryType.Sphere, Vector3.zero, Vector3.zero, 0, radius, filter);
        var xform = new AffineTransform(transform.position, transform.rotation, transform.localScale);
        queryIndex = solver.EnqueueSpatialQuery(query, xform);
    }

    private void Solver_OnSpatialQueryResults(ObiSolver s, ObiNativeQueryResultList queryResults)
    {
        for (int i = 0; i < queryResults.count; ++i)
        {
            if (queryResults[i].queryIndex == queryIndex)
            {
                int particleIndex = solver.simplices[queryResults[i].simplexIndex];

                Vector3 pos = solver.transform.TransformPoint(solver.positions[particleIndex]);
                Debug.DrawLine(transform.position, pos, Color.yellow);
            }
        }
    }
}

Determining all simplices inside some boxes

We'll enqueue multiple box queries at once. Then, we will look for QueryResults with a negative distance, as these indicate the simplex is inside one of the boxes. Note that this sample assumes there are only 0-simplices (particles) in the solver.

using UnityEngine;
using Obi;

public class OverlapTest : MonoBehaviour
{
    public ObiSolver solver;
    public Transform[] cubes;
    int filter;

    private void Start()
    {
        solver.OnSpatialQueryResults += Solver_OnSpatialQueryResults;
    }

    private void OnDestroy()
    {
        solver.OnSpatialQueryResults -= Solver_OnSpatialQueryResults;
    }

    private void Solver_OnSpatialQueryResults(ObiSolver s, ObiNativeQueryResultList queryResults)
    {
        for (int i = 0; i < solver.colors.count; ++i)
            solver.colors[i] = Color.cyan;

        // Iterate over results and draw their distance to the center of the cube.
        // We're assuming the solver only contains 0-simplices (particles).
        for (int i = 0; i < queryResults.count; ++i)
        {
            if (queryResults[i].distance < 0)
            {
                int particleIndex = solver.simplices[queryResults[i].simplexIndex];

                // change the color of the particle depending on which box it is inside of:
                if (queryResults[i].queryIndex == 0)
                    solver.colors[particleIndex] = Color.red;
                else if (queryResults[i].queryIndex == 1)
                    solver.colors[particleIndex] = Color.yellow;
            }
        }

        // write color to the GPU right now, in case we're using the compute backend.
        solver.colors.Unmap();
    }

    private void Update()
    {
        int filter = ObiUtils.MakeFilter(ObiUtils.CollideWithEverything, 0);
        for (int i = 0; i < cubes.Length; ++i)
        {
            solver.EnqueueSpatialQuery(new QueryShape
            {
                type = QueryShape.QueryType.Box,
                center = Vector3.zero,
                size = new Vector3(1, 1, 1),
                contactOffset = 0,
                maxDistance = 0,
                filter = filter
            }, new AffineTransform(cubes[i].position, cubes[i].rotation, cubes[i].localScale));
        }
    }
}

Single raycast

Using the EnqueueRaycast method, we'll get one QueryResult for each simplex hit by the ray. In OnSpatialQueryResults, we iterate trough all results and use their distanceAlongRay to find the one that's closest to the ray origin.

using UnityEngine;
using Obi;

public class SingleRaycast : MonoBehaviour
{
    public ObiSolver solver;
    int filter;
    int queryIndex;
    QueryResult result;

    private void Start()
    {
        filter = ObiUtils.MakeFilter(ObiUtils.CollideWithEverything, 0);
        solver.OnSpatialQueryResults += Solver_OnSpatialQueryResults;
        solver.OnSimulate += Solver_OnSimulate;
    }

    private void OnDestroy()
    {
        solver.OnSpatialQueryResults -= Solver_OnSpatialQueryResults;
        solver.OnSimulate -= Solver_OnSimulate;
    }

    private void Solver_OnSimulate(ObiSolver s, float simulatedTime, float substepTime)
    {
        // perform a raycast, check if it hit anything:
        queryIndex = solver.EnqueueRaycast(new Ray(transform.position, transform.forward), filter, 100);
    }

    private void Solver_OnSpatialQueryResults(ObiSolver s, ObiNativeQueryResultList queryResults)
    {
        result = new QueryResult { distanceAlongRay = float.MaxValue, simplexIndex = -1, queryIndex = -1 };
        for (int i = 0; i < queryResults.count; ++i)
        {
            // get the first result along the ray. That is, the one with the smallest distanceAlongRay:
            if (queryResults[i].queryIndex == queryIndex &&
                queryResults[i].distanceAlongRay < result.distanceAlongRay)
                result = queryResults[i];
        }
    }

    void Update()
    {
        if (result.simplexIndex >= 0)
            Debug.DrawLine(transform.position, result.queryPoint, Color.yellow);
    }
}