Scripting constraints

Constraints are conditions imposed on particles, that largely determine their behavior. Obi spends most of the time in a physics step enforcing constraints. In order to efficiently process all constraints in the shortest amount of time possible, Obi can process multiple constraints at the same time thanks to multithreading.

However, multithreading requires that no constraints affecting the same particle are processed at exactly the same time. Otherwise multiple threads would attempt to modify the same particle at once, leading to incorrect results and/or bad performance. To ensure correctness, constraints need to be processed in a very specific order.

Obi organizes constraints of the same type into batches. A batch cannot contain constraints that affect the same particle, so all constraints in a batch can be safely processed in parallel. All batches for a given constraint type are processed sequentially, then all batches for the next constraint type, until all constraints have been processed.

A square 4x4 piece of cloth, showing particles (orange circles) & distance constraints (colored lines). Constraints are colored according to the batch they belong to.

Arranging constraints into batches can be a very time consuming process (known as graph-coloring), so it is generally not done at runtime: it happens as part of blueprint generation, so blueprints store constraints pre-grouped into batches. Nevertheless, this batch-based data layout determines the way in which constraints must be accessed and modified at runtime.

Per-actor parameters

At runtime, you can enable/disable/modify all constraints of a given type in an actor. Actor expose these parameters as public properties.

Here's a small example: a component that sets cloth distance constraint stretching compliance to 2 m/N upon pressing any key:

using UnityEngine;
using Obi;

[RequireComponent(typeof(ObiCloth))]
public class ElasticCloth : MonoBehaviour
{
	void Update ()
	{
		if (Input.anyKeyDown)
			GetComponent<ObiCloth>().stretchingCompliance = 2;
	}
}
			

Another small example that deactivates bend constraints in a rope:

using UnityEngine;
using Obi;

[RequireComponent(typeof(ObiRope))]
public class BendableRope : MonoBehaviour
{
	void Update ()
	{
		if (Input.anyKeyDown)
			GetComponent<ObiRope>().bendConstraintsEnabled = false;
	}
}
			

For a detailed list of all constraint parameters exposed in an actor, check the API documentation.

Per-constraint parameters

In addition to modifying parameters for all constraints in an actor at once, a more fine-grained approach allows you to modify each individual constraint. In order to do this, you need to get access to the constraint batches. Each batch contains multiple data arrays that you can read from/write to. The amount of arrays and the data contained in each one depends on the constraint type, check the API documentation for details.

Very much like particle data, constraint data isn't laid out in the solver the same way it is in an actor. When an actor gets added to a solver, all its constraints are merged with the existing constraints in the solver to maximize performance. This means each constraint batch in the solver contains constraints belonging to different actors, so if we want to access data for a certain actor in particular, we need to know the offset of that actor's constraints inside the solver batch.

This information is contained in the actor.solverBatchOffsets array: For each constraint type, it contains a list of offsets in the solver constraint batches.

For instance, let's say our actor has 5 distance constraint batches. When loaded into a solver, the data in these batches will be appended to the existing distance constraint batches in that solver. So when we later try to access this actor's distance constraints, their data will not start at index 0 in the solver batches. We can then get the exact offset for each batch like this:

var distanceOffsets = actor.solverBatchOffsets[(int)Oni.ConstraintType.Distance];
int firstBatchOffset  = distanceOffsets[0];
int secondBatchOffset = distanceOffsets[1];
int thirdBatchOffset  = distanceOffsets[2];
int fourthBatchOffset = distanceOffsets[3];
int fifthBatchOffset  = distanceOffsets[4];
// ... and so on, if our actor had more distance constraint batches.

// access the distance constraints currently simulated by the solver:
var solverConstraints = actor.solver.GetConstraintsByType(Oni.ConstraintType.Distance)
			as ObiConstraints<ObiDistanceConstraintsBatch>;

// change the rest length of this actor's 21st constraint in the third batch:
solverConstraints.batches[2].restLengths[thirdBatchOffset + 20] = 1.25f;
	

Let's see an example:

using UnityEngine;
using Obi;

[RequireComponent(typeof(ObiSkinnedCloth))]
public class SkinAdjustments : MonoBehaviour
{
	void Update ()
	{
		if (Input.anyKey)
		{
			var cloth = GetComponent<ObiSkinnedCloth>();

			// get constraints stored in the actor:
			var actorConstraints = cloth.GetConstraintsByType(Oni.ConstraintType.Skin)
						as ObiConstraints<ObiSkinConstraintsBatch>;

			// get runtime constraints in the solver:
			var solverConstraints = cloth.solver.GetConstraintsByType(Oni.ConstraintType.Skin)
						as ObiConstraints<ObiSkinConstraintsBatch>;

			// there's only one skin constraint per particle,
			// so particleIndex == constraintIndex,
			// and there is only need for one skin constraints batch:
			var actorSkinBatch = actorConstraints.batches[0];
			var solverSkinBatch = solverConstraints.batches[0];

			// get the offset of our first -and only- skin batch in the solver:
			int offset = cloth.solverBatchOffsets[(int)Oni.ConstraintType.Skin][0];

			// iterate over all active skin constraints in the batch,
			// setting their properties:
			for (int i = 0; i < actorSkinBatch.activeConstraintCount; ++i)
			{
				int index = i + offset;

				// skin radius
				solverSkinBatch.skinRadiiBackstop[index * 3] = 0.05f;

				// backstop sphere radius
				solverSkinBatch.skinRadiiBackstop[index * 3 + 1] = 0.1f;

				// backstop sphere distance
				solverSkinBatch.skinRadiiBackstop[index * 3 + 2] = 0;
			}
		}
	}
}

Uploading constrant data to the GPU

When using the Compute backend, the GPU is used to perform the simulation. Typically CPU and GPU memory are physically separate, and data must be uploaded from the CPU to the GPU or readback from the GPU to the CPU. Writing data to the constraint arrays as in the above example will only modify the CPU copy. You need to upload any changes made to constraint data arrays to the GPU by calling their Upload() method. In the above example, this means adding the following line right after the for loop:

// upload changes to the GPU:
solverSkinBatch.skinRadiiBackstop.Upload();

Adding/removing constraints

Sometimes you want to add new constraints to existing actors, or remove constraints from them. Most common case is adding pin constraints manually, instead of using attachments. Remember that constraints are grouped into batches, and that constraints in the same batch cannot share particles. After you've added or removed particles/colliders, you will need to call actor.SetConstraintsDirty(). This rebuilds all solver batches of the given constraint type.

Here's an example of how to add a couple pin constraints to a rope:

// get a hold of the constraint type we want, in this case, pin constraints:
var pinConstraints = rope.GetConstraintsByType(Oni.ConstraintType.Pin) as ObiConstraints<ObiPinConstraintsBatch>;

// remove all batches from it, so we start clean:
pinConstraints.Clear();

// create a new pin constraints batch
var batch = new ObiPinConstraintsBatch();

// Add a couple constraints to it, pinning the first and last particles in the rope:
batch.AddConstraint(rope.solverIndices[0], colliderA, Vector3.zero, Quaternion.identity, 0, 0, float.PositiveInfinity);
batch.AddConstraint(rope.solverIndices[blueprint.activeParticleCount - 1], colliderB, Vector3.zero, Quaternion.identity, 0, 0, float.PositiveInfinity);

// set the amount of active constraints in the batch to 2 (the ones we just added).
batch.activeConstraintCount = 2;

// append the batch to the pin constraints:
pinConstraints.AddBatch(batch);

// this will cause the solver to rebuild pin constraints at the beginning of the next frame:
rope.SetConstraintsDirty(Oni.ConstraintType.Pin);