Obi Official Forum

Full Version: How to tune/set constraints of a particle group from an obiActor during runtime
You're currently viewing a stripped down version of our content. View the full version with proper formatting.
Hi all,

I am trying to write a script, that allows me to tune/set the constraints of a selected obi particle group at runtime.

If I understood correctly, I should know the offset of that actor's particle group constraints, inside the solver batch, to set the constraints.

The problem that I am facing, is that I am not able to locate the constraints from the particle group in the solver.
My idea was to loop through the constraints of the actor, and if the constraint is also present in the particle group, set the constraints.
Is this the way to go? (As I cannot seem to figure that part out).

Any help would be greatly appreciated.
- Jasper

My current implementation on setting a softbody ShapeMatchingConstraints  look something like this, where I still need to fill in the "ConstraintIsInParticleGroup()" function:
Code:
using Obi;
using System.Collections.Generic;
using UnityEngine;

public class ParticleGroupTest : MonoBehaviour
{
    [SerializeField] private ObiActor obiActor = null;
    [SerializeField] private ObiParticleGroup obiParticleGroup = null;
    private ObiSoftbody obiSoftbody = null;


    bool ConstraintIsInParticleGroup()
    {
        // ???
        return true;
    }

    void Start()
    {
        obiSoftbody = obiActor.GetComponent<ObiSoftbody>();
    }

    void Update()
    {
        // get constraints stored in the actor:
        var actorConstraints = obiSoftbody.GetConstraintsByType(Oni.ConstraintType.ShapeMatching)
                    as ObiConstraints<ObiShapeMatchingConstraintsBatch>;
        // get runtime constraints in the solver:
        var solverConstraints = obiSoftbody.solver.GetConstraintsByType(Oni.ConstraintType.ShapeMatching)
                    as ObiConstraints<ObiShapeMatchingConstraintsBatch>;

        // get batches and offsets
        List<ObiShapeMatchingConstraintsBatch> actorSoftBodyBatches = actorConstraints.batches;
        List<ObiShapeMatchingConstraintsBatch> solverSoftBodyBatches = solverConstraints.batches;
        List<int> offsets = obiSoftbody.solverBatchOffsets[(int)Oni.ConstraintType.ShapeMatching];


        for (int i_batch = 0; i_batch < actorConstraints.batches.Count; ++i_batch) // loop over batches
            for (int i_constraint = 0; i_constraint < actorSoftBodyBatches[i_batch].activeConstraintCount; ++i_constraint) // loop over constraints
                if (ConstraintIsInParticleGroup())
                {
                    int index = i_constraint + offsets[i_batch]; //apply offset to constraint index per batch

                    solverSoftBodyBatches[i_batch].materialParameters[index * 5] = 0.0f; // deformation resistance
                    solverSoftBodyBatches[i_batch].materialParameters[index * 5 + 1] = 1.0f; // plasticYield
                    solverSoftBodyBatches[i_batch].materialParameters[index * 5 + 2] = 1.0f; // plasticCreep;
                    solverSoftBodyBatches[i_batch].materialParameters[index * 5 + 3] = 1.0f; // plasticRecovery
                    solverSoftBodyBatches[i_batch].materialParameters[index * 5 + 4] = 1.0f; // maxDeformation
                }
    }
To clarify my initial question:

Lets say I have an particle index of 3 in the actor, which corresponds to an index of 6 in the solver.
What would be the corresponding constraint(s) index/indices in the solver?


From obi scripting constraints documentation, it is said that:
Code:
// there's only one skin constraint per particle,
// so particleIndex == constraintIndex,
However, this does not seem te be the case for shape matching constraints in softbodies?


Hope you are able to give me some pointers.

Kind regards,
Jasper
Hi Jasper,

Constraints reference particles. However, particles do not reference constraints in any way. So in general, there's no way to know which constraints affect a given particle except for checking all constraints.

(19-10-2021, 08:57 AM)Jschol Wrote: [ -> ]Lets say I have an particle index of 3 in the actor, which corresponds to an index of 6 in the solver.
What would be the corresponding constraint(s) index/indices in the solver?

There's no efficient way to know this. Data layout is optimized to know which particles are affected by a given constraint, not the other way around. The only possible approach is to go over all constraints of a given type, then check the particleIndices array for each constraint to see if a given constraint affects the particle(s) you're interested in. This is of course extremely costly if done for all particles and constraints, since it requires P * C checks, where P is the amount of particles and C the amount of constraints.

(19-10-2021, 08:57 AM)Jschol Wrote: [ -> ]From obi scripting constraints documentation, it is said that:
Code:
// there's only one skin constraint per particle,
// so particleIndex == constraintIndex,
However, this does not seem te be the case for shape matching constraints in softbodies?

Nope. This applies only to skin constraints, since each skin constraint only affects 1 particle and there's exactly 1 constraint per particle. Shape matching constraints in particular can affect any number of particles each, and there can be an arbitrary number of constraints affecting a single particle.

If you take a look at the API docs for ObiShapeMatchingConstraintsBatch, you'll see it has 2 arrays (in addition to the particleIndices array, that all constraint types have):

Quote:firstIndex: index of the first particle in each constraint.
numIndices: amount of particles in each constraint.

so constraint k affects particles (firstIndex[k]....firstIndex[k]  + numIndices[k]). These are indices into the particleIndices array. There's also a GetParticlesInvolved(int index, List<int> particles) method that fills a list with the particles affected by a given constraint. This is implemented as:

Code:
int first = firstIndex[index];
int num = numIndices[index];
for (int i = first; i < first + num; ++i)
     particles.Add(particleIndices[i]);

Again, this is specific for shape matching constraints. They're probably the most complex case since they can affect an arbitrary number of particles each. Other constraint types store indices in a different way. For instance, distance constraints affect *exactly* 2 particles each. They're stored as consecutive pairs in the constraint's particleIndices array. So the GetParticlesInvolved method looks like this:

Code:
particles.Add(particleIndices[index * 2]);
particles.Add(particleIndices[index * 2 + 1]);

As you can see this is much simpler than shape matching constraints.

Hope this makes sense. Let me know if you need further help!
Awesome!
Thanks for the detailed answer Sonrisa 

Got it to work by looping over the constraints and particle indices as you described and a minimal working example is implemented below.

Performance decrease did not seem too bad (so far).
Since it is crucial for my application, I also looked into initializing at startup only.
Here I noticed that the solverBatchOffsets returned an empty list when called in the void start()?

Code:
    void Start()
    {
        obiSoftbody = obiActor.GetComponent<ObiSoftbody>();
        particlesInGroup = obiParticleGroup.particleIndices;
        List<int> offsets = obiSoftbody.solverBatchOffsets[(int)Oni.ConstraintType.ShapeMatching];
        Debug.Log(offsets.Count); // returns 0
    }
I circumvent this problem by setting the constraints only in the first update of the updateloop.


Minimal working example is given below:


Code:
using Obi;
using System.Collections.Generic;
using UnityEngine;

public class ParticleGroupTest : MonoBehaviour
{
    [SerializeField] private ObiActor obiActor = null;
    [SerializeField] private ObiParticleGroup obiParticleGroup = null;

    [SerializeField] private bool setConstraintsEveryUpdate = false;

    [SerializeField] private float deformationResistance = 0.1f;
    [SerializeField] private float plasticYield = 0.0f;
    [SerializeField] private float plasticCreep = 0.0f;
    [SerializeField] private float plasticRecovery = 0.0f;
    [SerializeField] private float maxDeformation = 0.0f;


    private ObiSoftbody obiSoftbody = null;
    private List<int> particlesInGroup;

    private bool updatedOnce = false;


    void Start()
    {
        obiSoftbody = obiActor.GetComponent<ObiSoftbody>();
        particlesInGroup = obiParticleGroup.particleIndices;
    }


    void Update()
    {
        if (setConstraintsEveryUpdate == true) SetConstraints(); //constantly update the constraint of the given particle group;
        else if (setConstraintsEveryUpdate == false && updatedOnce == false)
        {
            // set constraints only in the first loop, need to do this in the update since the line:
            // List<int> offsets = obiSoftbody.solverBatchOffsets[(int)Oni.ConstraintType.ShapeMatching];
            // returns an empty list when being called in: void Start()
            SetConstraints();
            updatedOnce = true;
        }
    }


    void SetConstraints()
    {
        // Sets the all the constraints that are tied to the particles in the particle group

        // get constraints stored in the actor:
        var actorConstraints = obiSoftbody.GetConstraintsByType(Oni.ConstraintType.ShapeMatching)
                    as ObiConstraints<ObiShapeMatchingConstraintsBatch>;
        // get runtime constraints in the solver:
        var solverConstraints = obiSoftbody.solver.GetConstraintsByType(Oni.ConstraintType.ShapeMatching)
                    as ObiConstraints<ObiShapeMatchingConstraintsBatch>;

        // get batches and offsets
        List<ObiShapeMatchingConstraintsBatch> actorSoftBodyBatches = actorConstraints.batches;
        List<ObiShapeMatchingConstraintsBatch> solverSoftBodyBatches = solverConstraints.batches;
        List<int> offsets = obiSoftbody.solverBatchOffsets[(int)Oni.ConstraintType.ShapeMatching];

        for (int i_batch = 0; i_batch < actorSoftBodyBatches.Count; ++i_batch) // loop over batches   
            for (int i_constraint = 0; i_constraint < actorConstraints.batches[i_batch].activeConstraintCount; ++i_constraint) // constraints
                if (ConstraintIsReferencedInParticleGroup(actorConstraints.batches[i_batch], i_constraint, particlesInGroup))
                {
                    int index = i_constraint + offsets[i_batch];

                    solverSoftBodyBatches[i_batch].materialParameters[index * 5] = deformationResistance;
                    solverSoftBodyBatches[i_batch].materialParameters[index * 5 + 1] = plasticYield;
                    solverSoftBodyBatches[i_batch].materialParameters[index * 5 + 2] = plasticCreep;
                    solverSoftBodyBatches[i_batch].materialParameters[index * 5 + 3] = plasticRecovery;
                    solverSoftBodyBatches[i_batch].materialParameters[index * 5 + 4] = maxDeformation;
                }
    }

    bool ConstraintIsReferencedInParticleGroup(ObiShapeMatchingConstraintsBatch actorSoftBodyBatch,
                                                                        int constraint_i, List<int> particlesInGroup)
    {
        // If constraint_i is reference by any (maybe even single) particle in the particle group, return true, else false
        int firstIndex = actorSoftBodyBatch.firstIndex[constraint_i]; // first index of particle affected by constraint i
        int numIndices = actorSoftBodyBatch.numIndices[constraint_i]; // amount of particles affected by constraint i

        for (int i = firstIndex; i < firstIndex + numIndices; ++i) // loop over particles affected by constraint i
        {
            int particle_index = actorSoftBodyBatch.particleIndices[i]; // get particle index
            if (particlesInGroup.Contains(particle_index)) return true;
        }
        return false;
    }
}
(19-10-2021, 03:29 PM)Jschol Wrote: [ -> ]Here I noticed that the solverBatchOffsets returned an empty list when called in the void start()?

Actors are added to solvers in their Start() method. The order in which Unity calls Start, Awake, Update etc for different components is undefined by default. So if you have a script that accesses solver/actor data in Start() it might run before he actor has been added to the solver.

Either set your script execution order so that your script runs after ObiSolver/ObiActor, or wait one frame after Start. See:
https://docs.unity3d.com/Manual/ExecutionOrder.html
https://docs.unity3d.com/Manual/class-MonoManager.html