Scripting collisions

Obi allows you to retrieve detailed contact information every frame (for instance, to determine which objects collide with a given particle) and programmatically set collision filters. In this page we will learn to do both.

Collision callbacks

Obi does not work with the default Unity collision callbacks (OnCollisionEnter,OnCollisionExit,OnCollisionStay...) because of performance and flexibility reasons. Instead, the ObiSolver component can provide a list of all contacts generated during the current frame. You can then iterate this list of contacts and perform any custom logic you need.

Contacts can happen between a simplex and a collider, or between two simplices. To request the simplex-collider contact list from the solver, subscribe to its OnCollision event. To retrieve the simplex-simplex contact list, subscribe to its OnParticleCollision event.

The Oni.Contact struct contains the following information:

BodyA

Index of the first simplex involved in the contact.

BodyB

Index of the collider / second simplex involved in the contact. In case of simplex-collider contacts, this index can be used to retrieve the actual collider object, see below.

PointA

Barycentric coordinates of the contact point on the surface of the first simplex.

PointB

Barycentric coordinates of the contact point on the surface of the second simplex. For simplex-collider contacts, solver-space contact point on the surface of the collider.

Distance

Distance between both bodies (simplex-collider or simplex-simplex) before resolving the collision, if any. The distance can be either positive (bodies do not overlap each other) or negative (both bodies partially or completely overlap). A value of zero means the bodies are just touching each other.

Obi uses a continuous-collision detection method known as speculative contacts, which can generate contacts even when an actual collision isnĀ“t taking place. If you want to prune all speculative contacts and consider actual collisions only, check for distances below a small threshold (e.g 0.01).

Normal

The collider's surface normal at the contact point.

Tangent

The collider's surface tangent at the contact point.

Bitangent

The collider's surface bitangent at the contact point.

Normal impulse

Impulse magnitude applied by the contact in the normal direction. The larger this value, the stronger the initial collision force was. It's expressed in Newtons x second2 (lagrange multiplier).

Tangent impulse

Impulse magnitude applied by the contact in the tangent direction. The larger this value, the higher the friction between the particle and the collider. It's expressed in Newtons x second.

Bitangent impulse

Impulse magnitude applied by the contact in the bitangent direction. The larger this value, the higher the friction between the particle and the collider. It's expressed in Newtons x second.

Stick impulse

Impulse magnitude applied by the contact against the normal direction. This impulse keeps the particle stuck to the collider, only generated for sticky materials. It's expressed in Newtons x second2 (lagrange multiplier).

Retrieving the collider involved in a contact

You can get a reference to the collider involved in any contact by doing:

ObiColliderBase collider = ObiColliderWorld.GetInstance().colliderHandles[contact.bodyB].owner;

Retrieving the particle involved in a contact

In Obi 6.X, collision detection is simplex-based. A simplex can be a triangle (3 particles), an edge (2 particles) or a single particle. The solver.simplices array stores simplices as particle index tuples (see surface collisions for more info). To retrieve the solver indices of the particles in a simplex, you can do the following:

// retrieve the offset and size of the simplex in the solver.simplices array:
int simplexStart = solver.simplexCounts.GetSimplexStartAndSize(contact.bodyA, out int simplexSize);

// starting at simplexStart, iterate over all particles in the simplex:
for (int i = 0; i < simplexSize; ++i)
{
	int particleIndex = solver.simplices[simplexStart + i];

	// do something with each particle, for instance get its position:
	var position = solver.positions[particleIndex];
}

In case there isn't any actor using surface collisions in your solver, all simplices will have size 1 and the code above can be simplified to:

// get the particle index directly, as all simplices are guaranteed to have size 1:
int particleIndex = solver.simplices[contact.bodyA];

// do something with the particle, for instance get its position:
var position = solver.positions[particleIndex];

Retrieving the actor involved in a contact

You can get a reference to the ObiActor (ObiCloth,ObiRope,ObiEmitter...) involved in any contact by doing:

ObiSolver.ParticleInActor pa = solver.particleToActor[particleIndex];

See the above section to learn how to retrieve particle indices from the contact.

The ParticleInActor struct has two public variables:

Actor

A reference to the ObiActor to which this particle belongs.

IndexInActor

The index of this particle in the actor's arrays. For instance, you can kill a ObiEmitter particle upon collision by doing this:

ObiSolver.ParticleInActor pa = solver.particleToActor[particleIndex];
ObiEmitter emitter = pa.actor as ObiEmitter;

if (emitter != null)
    emitter.life[pa.indexInActor] = 0;

Some examples

Here's an example of a component that subscribes to the solver's OnCollision event and iterates over all the contacts. Then, it retrieves the collider object for all actual (non-speculative) contacts:

using UnityEngine;
using Obi;

[RequireComponent(typeof(ObiSolver))]
public class CollisionEventHandler : MonoBehaviour {

 	ObiSolver solver;

	void Awake(){
		solver = GetComponent<ObiSolver>();
	}

	void OnEnable () {
		solver.OnCollision += Solver_OnCollision;
	}

	void OnDisable(){
		solver.OnCollision -= Solver_OnCollision;
	}

	void Solver_OnCollision (object sender, Obi.ObiSolver.ObiCollisionEventArgs e)
	{
		var world = ObiColliderWorld.GetInstance();

		// just iterate over all contacts in the current frame:
		foreach (Oni.Contact contact in e.contacts)
		{
			// if this one is an actual collision:
			if (contact.distance < 0.01)
			{
				ObiColliderBase col = world.colliderHandles[contact.bodyB].owner;
				if (col != null)
				{
					// do something with the collider.
				}
			}
		}
	}

}
			

A script very similar to this one has been used to draw all contacts in the following image,using Unity's Gizmos class. Here you can see speculative contacts (distance > 0) drawn in green, and actual contacts (distance <= 0) drawn in red:


Note the row of speculative contacts right across the middle. Since the floor plane is made out of 2 triangles and all particles about to touch the floor lie inside both triangles' bounding boxes, Obi considers they all have a quite high probability of colliding with both triangles. So speculative contacts are generated for the closest feature on the opposite triangle: the central edge. These are ignored later on the constraint enforcement phase, so normal and tangent impulses are only calculated and applied for the red contacts. However stick impulses can still be generated for speculative contacts, if they are close enough to becoming an actual contact.

Another example: the following script will scale gravity for all particles in contact with a trigger that has the tag "zeroGravity". This is done by applying an acceleration that counteracts gravity to all particles in contact with the trigger.

using UnityEngine;
using Obi;

[RequireComponent(typeof(ObiSolver))]
public class ZeroGravityZone: MonoBehaviour
{
    ObiSolver solver;
    public float antiGravityScale = 2;

    ObiSolver.ObiCollisionEventArgs collisionEvent;

    void Awake()
    {
        solver = GetComponent<ObiSolver>();
    }

    void OnEnable()
    {
        solver.OnCollision += Solver_OnCollision;
    }

    void OnDisable()
    {
        solver.OnCollision -= Solver_OnCollision;
    }

    void Solver_OnCollision(object sender, Obi.ObiSolver.ObiCollisionEventArgs e)
    {
        // calculate an acceleration that counteracts gravity:
        Vector4 antiGravityAccel = -(Vector4)solver.parameters.gravity * antiGravityScale * Time.deltaTime;

        var world = ObiColliderWorld.GetInstance();
        foreach (Oni.Contact contact in e.contacts)
        {
            // this one is an actual collision:
            if (contact.distance < 0.01)
            {
                ObiColliderBase col = world.colliderHandles[contact.bodyB].owner;

                // if this collider is tagged as "zero gravity":
                if (col != null && col.gameObject.CompareTag("zeroGravity"))
                {
                    // get the index of the particle involved in the contact:
                    int particleIndex = solver.simplices[contact.bodyA];

                    // set the particle velocity:
                    solver.velocities[particleIndex] += antiGravityAccel;
                }
            }
        }
    }
}

no triggers, the fluid pours down and splashes on contact with the floor.
the glass box is a zeroGravity trigger, using antiGravityScale=2 in the above script will reverse gravity for all particles within the trigger.

Collision filters

A collision filter in Obi is a single 32 bit integer value associated to each particle and each collider. This section explains how to create filters in your own scripts, to learn how filters are used in-editor please see collisions.

Filters are made of a mask (referred to as "Collides with" in the editor UI) and a category (referred to as "Category" in the editor UI): the 16 most significant bits of the filter constitute the mask, and the 16 less significant bits constitute the category. Though you could use bitwise operators to build filters yourself, Obi provides a few helper tools to make this task a little easier:

The ObiUtils.MakeFilter(int mask, int category) function takes a mask and a category as arguments. Given these, it will return a filter value. Providing a category is simple, any integer value from 0 to 15 will do. The mask is slightly more involved: it's a bit mask -like Unity's layer masks- and so it can be built by or'ing together bits.

For instance, to build a filter from category 8 and a mask that collides with categories 1, 3, and 6:

int mask = (1 << 1) | (1 << 3) | (1 << 6);
int filter = ObiUtils.MakeFilter(mask, 8);

There's also pre-built masks available: set to collide with every category, or no category:

int allmask = ObiUtils.CollideWithEverything;
int nonemask = ObiUtils.CollideWithNothing;

Another example: a filter for category 0, that collides with everything:

int filter = ObiUtils.MakeFilter(ObiUtils.CollideWithEverything, 0);

You can also extract the mask and category from any given filter:

int mask = ObiUtils.GetMaskFromFilter(filter);
int category = ObiUtils.GetCategoryFromFilter(filter);

Filters can be applied at runtime to specific particles (see particle scripting). Some actor blueprints also take filters as parameters: for instance, when building a rope blueprint the blueprint's AddControlPoint() method takes a filter as a parameter. This sets the filter used for all particles close to that control point. For more info on creating blueprints programmatically, see creating actors at runtime.