Obi Official Forum
Help Creating and Removing Pin Constraints from ObiPinConstraintsBatch - Printable Version

+- Obi Official Forum (https://obi.virtualmethodstudio.com/forum)
+-- Forum: Obi Users Category (https://obi.virtualmethodstudio.com/forum/forum-1.html)
+--- Forum: Obi Cloth (https://obi.virtualmethodstudio.com/forum/forum-2.html)
+--- Thread: Help Creating and Removing Pin Constraints from ObiPinConstraintsBatch (/thread-3973.html)



Creating and Removing Pin Constraints from ObiPinConstraintsBatch - sebjf - 15-08-2023

Hi, 

I am trying to use ObiCloth to make an adhesive pad in VR, which the user can place on a surface, and pull away.

The way I am approaching this, is to use the collision callback to detect when particles are within a given distance of the target surface, and at the end of the frame create new Pin Constraints for all those that are not already adhered. At the end of each step, the breaking threshold is used to remove the constraints based on ObiParticleAttachment::BreakDynamicAttachment().

To avoid adding particles multiple times, I keep a list of particles that have been added to the ObiPinConstraintsBatch, and remove them from this list when they are removed in my version of BreakDynamicAttachment. However, this step often attempts to remove particles that don't exist in the list, and it appears others remain in the list even though there don't appear to be any active attachments affecting the cloth.

I have tried approaches using RemoveConstraint and DeactivateConstraint. I have successfully used a procedural ObiPinConstraintsBatch to facilitate grasping of the cloth, but in this case I can clear the ObiPinConstraintsBatch each time I need to update it.

I suspect therefore my approach to adding and removing particles from the ObiPinConstraintsBatch is wrong.

What is the best way to handle creating and destroying a small number of Pin Constraints for this purpose?

Would it be better, for example, to create an ObiPinConstraintsBatch with a single constraint for each particle at startup, then update/Activate/Deactivate these as they come into and out of contact?


Or perhaps I am getting the index for a broken constraint the wrong way - is this code correct?

Code:
// The particlesToStick is populated in the collision callback, with solver indices

private void UpdateAdhesionConstraintsBatch()
{
    if (particlesToStick.Count > 0)
    {
        var bindMatrix = Dummy.transform.worldToLocalMatrix * actor.solver.transform.localToWorldMatrix;

        if(adhesionBatch == null)
        {
            adhesionBatch = new ObiPinConstraintsBatch();
        }

        foreach (var solverIndex in particlesToStick)
        {
            var positionOffset = bindMatrix.MultiplyPoint3x4(actor.solver.positions[solverIndex]);
            var orientationOffset = bindMatrix.rotation * actor.solver.orientations[solverIndex];

            adhesionBatch.AddConstraint(
                solverIndex,
                Dummy,
                positionOffset,
                orientationOffset,
                0,
                0,
                0.1f
            );

            //q: why is this needed?
            adhesionBatch.activeConstraintCount++;

            stuck.Add(solverIndex);

            actor.SetConstraintsDirty(Oni.ConstraintType.Pin);
        }

        particlesToStick.Clear();

        UpdateAdhesion(); [color=#333333][size=small][font=Monaco, Consolas, Courier, monospace]// This adds or removes the batch depending on whether there are any active constraints, and tells the solver to rebuild the pins[/font][/size][/color]
    }
}

// And to break the constraints, at the end of each step

private void BreakDynamicAttachment(float stepTime)
{
    if (actor.isLoaded)
    {
        var actorConstraints = actor.GetConstraintsByType(Oni.ConstraintType.Pin) as ObiConstraints<ObiPinConstraintsBatch>;
        var solverConstraints = actor.solver.GetConstraintsByType(Oni.ConstraintType.Pin) as ObiConstraints<ObiPinConstraintsBatch>;

        if (actorConstraints != null && adhesionBatch != null)
        {
            int batchIndex = actorConstraints.batches.IndexOf(adhesionBatch);
            if (batchIndex >= 0 && batchIndex < actor.solverBatchOffsets[(int)Oni.ConstraintType.Pin].Count)
            {
                int offset = actor.solverBatchOffsets[(int)Oni.ConstraintType.Pin][batchIndex];
                var solverBatch = solverConstraints.batches[batchIndex];

                float sqrTime = stepTime * stepTime;
                for (int i = 0; i < adhesionBatch.constraintCount; i++)
                {
                    // in case the constraint has been broken:

                    var b = -solverBatch.lambdas[(offset + i) * 4 + 3] / sqrTime;
                    if (b > adhesionBatch.breakThresholds[i])
                    {
                        var solverIndex = adhesionBatch.particleIndices[i];
                        if (!stuck.Remove(solverIndex))
                        {
                            Debug.Log("Error");
                        }
                        adhesionBatch.RemoveConstraint(i);
                    }
                }
            }

            UpdateAdhesion(); // This adds or removes the batch depending on whether there are any active constraints, and tells the solver to rebuild the pins
        }
    }
}



RE: Creating and Removing Pin Constraints from ObiPinConstraintsBatch - sebjf - 18-08-2023

Hi, 

As a test, I updated my code to create an ObiPinConstraintsBatch for each sticking particle, and remove the batch entirely when its single constraint exceeds the threshold.

This works successfully, however I am not sure of the efficiency of creating and destroying entire batches this way for one particle each.

As this demonstrates the problem is indeed in trying to update an ObiPinConstraintsBatch, what is the recommended approach to do so?


RE: Creating and Removing Pin Constraints from ObiPinConstraintsBatch - josemendez - 21-08-2023

(18-08-2023, 07:15 PM)sebjf Wrote: Hi, 

As a test, I updated my code to create an ObiPinConstraintsBatch for each sticking particle, and remove the batch entirely when its single constraint exceeds the threshold.

This works successfully, however I am not sure of the efficiency of creating and destroying entire batches this way for one particle each.

As this demonstrates the problem is indeed in trying to update an ObiPinConstraintsBatch, what is the recommended approach to do so?

This:

Code:
for (int i = 0; i < adhesionBatch.constraintCount; i++)
{
    [...]
    adhesionBatch.RemoveConstraint(i);
}

Will invariably lead to bugs since you're iterating a collection while removing items from it, skipping items in it. You have several options, listed from "cleanest" to "hackiest":

- Iterate the batch backwards.
- Store the indices of the items you want to remove as you iterate the list, then remove them all at the end.
- Use a while loop instead.
- Decrement the counter variable "i" every time you remove an item.

Note this applies to all programming languages since removing an item from a compact, array-like data structure moves all following items back one position. (see: https://stackoverflow.com/questions/1582285/how-to-remove-elements-from-a-generic-list-while-iterating-over-it)

How you're dealing with particle indices looks correct, but can't really tell for sure since the code omits how "particlesToStick" is populated during collisions. Could you share this bit of code as well?

kind regards,


RE: Creating and Removing Pin Constraints from ObiPinConstraintsBatch - sebjf - 21-08-2023

Thank you, I see! 

I have updated it to iterate it in reverse, but unfortunately it still doesn't work.

Below are all the methods involved in the adhesion. I have also attached the entire Component for reference. The issue is the same (trying to remove the wrong indices from the stuck list.)


Code:
// These next functions are concerned with the adhesion behaviour

//q: this will resolve for all particles - isnt there quite an overhead calling this when we are only interested in a subset?
// can we only get this for a few particles or does this mean creating multiple solvers?

private void Solver_OnCollision(ObiSolver solver, ObiSolver.ObiCollisionEventArgs e)
{
    foreach (var item in e.contacts)
    {
        if (item.bodyB == Dummy.Handle.index && item.distance < 0.01)
        {
            var particle = item.bodyA;
            if (!stuck.Contains(particle) && !particlesToStick.Contains(particle))
            {
                particlesToStick.Add(particle);
            }
        }
    }
}

private void AddAdhesionConstraints()
{
    if (particlesToStick.Count > 0)
    {
        var bindMatrix = Dummy.transform.worldToLocalMatrix * actor.solver.transform.localToWorldMatrix;

        if (adhesionBatch == null)
        {
            adhesionBatch = new ObiPinConstraintsBatch();
        }

        foreach (var solverIndex in particlesToStick)
        {
            var positionOffset = bindMatrix.MultiplyPoint3x4(actor.solver.positions[solverIndex]);
            var orientationOffset = bindMatrix.rotation * actor.solver.orientations[solverIndex];

            adhesionBatch.AddConstraint(
                solverIndex,
                Dummy,
                positionOffset,
                orientationOffset,
                0,
                0,
                0.1f
            );

            //q: why is this needed?
            adhesionBatch.activeConstraintCount++;

            if(stuck.Contains(solverIndex))
            {
                Debug.Log("Error Type 1");
            }
            stuck.Add(solverIndex);

            actor.SetConstraintsDirty(Oni.ConstraintType.Pin);
        }

        particlesToStick.Clear();

        UpdateAdhesionBatch(); // This adds or removes the batch depending on whether there are any active constraints, and tells the solver to rebuild the pins[/font][/size][/color]
    }
}

private void BreakAdhesionConstraints(float stepTime)
{
    if (actor.isLoaded)
    {
        var actorConstraints = actor.GetConstraintsByType(Oni.ConstraintType.Pin) as ObiConstraints<ObiPinConstraintsBatch>;
        var solverConstraints = actor.solver.GetConstraintsByType(Oni.ConstraintType.Pin) as ObiConstraints<ObiPinConstraintsBatch>;

        if (actorConstraints != null && adhesionBatch != null)
        {
            int batchIndex = actorConstraints.batches.IndexOf(adhesionBatch);
            if (batchIndex >= 0 && batchIndex < actor.solverBatchOffsets[(int)Oni.ConstraintType.Pin].Count)
            {
                int offset = actor.solverBatchOffsets[(int)Oni.ConstraintType.Pin][batchIndex];
                var solverBatch = solverConstraints.batches[batchIndex];

                float sqrTime = stepTime * stepTime;
                for (int i = adhesionBatch.constraintCount - 1; i >= 0; i--)
                {
                    // in case the constraint has been broken:

                    var b = -solverBatch.lambdas[(offset + i) * 4 + 3] / sqrTime;
                    if (b > adhesionBatch.breakThresholds[i])
                    {
                        var solverIndex = adhesionBatch.particleIndices[i];
                        if (!stuck.Remove(solverIndex))
                        {
                            Debug.Log("Error Type 2");
                        }
                        adhesionBatch.RemoveConstraint(i);

                        actor.SetConstraintsDirty(Oni.ConstraintType.Pin);
                    }
                }
            }

            UpdateAdhesionBatch(); // This adds or removes the batch depending on whether there are any active constraints, and tells the solver to rebuild the pins
        }
    }
}

/// <summary>
/// Adds or removes adhesionBatch from the solver, and marks it dirty,
/// depending on whether it holds any, or any new, constraints.
/// </summary>
private void UpdateAdhesionBatch()
{
    if(adhesionBatch == null)
    {
        return;
    }

    if(prevAdhesionConstraintCount == 0 && adhesionBatch.constraintCount > 0)
    {
        var pins = actor.GetConstraintsByType(Oni.ConstraintType.Pin) as ObiPinConstraintsData;
        pins.AddBatch(adhesionBatch);
        prevAdhesionConstraintCount = adhesionBatch.constraintCount;
        actor.SetConstraintsDirty(Oni.ConstraintType.Pin);
    }

    if (prevAdhesionConstraintCount > 0 && adhesionBatch.constraintCount == 0)
    {
        var pins = actor.GetConstraintsByType(Oni.ConstraintType.Pin) as ObiPinConstraintsData;
        pins.RemoveBatch(adhesionBatch);
        adhesionBatch = null;
        prevAdhesionConstraintCount = 0;
        actor.SetConstraintsDirty(Oni.ConstraintType.Pin);
    }           
}



RE: Creating and Removing Pin Constraints from ObiPinConstraintsBatch - josemendez - 22-08-2023

Your code for detecting particles upon collision is wrong, this bit in particular:

Code:
var particle = item.bodyA;

You're using item.bodyA as if it were a particle index, but it's not: it is a body index (simplex or collider), in this particular case a simplex index. Accessing the particle(s) in that simplex can be done in one of two ways, depending on whether you're using surface collisions or not:

If using them, each simplex will have more than one particle in it:

Code:
// 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];
}

If not using them, each simplex is guaranteed to be just a single particle:

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

This is explained in the manual page for collision callbacks:
http://obi.virtualmethodstudio.com/manual/6.3/scriptingcollisions.html

Quote://q: this will resolve for all particles - isnt there quite an overhead calling this when we are only interested in a subset?
// can we only get this for a few particles or does this mean creating multiple solvers?

Solver.OnCollision will return all contacts detected during the frame by that particular solver, regardless of which particle/collider pairs are involved. You can't get only a subset, but you can use a variety of solutions to make checking them much faster: use multithreading, exploit temporal coherence, etc.

kind regards,


RE: Creating and Removing Pin Constraints from ObiPinConstraintsBatch - sebjf - 25-08-2023

I see, thank you! 

I have updated the code and it appears to be working better, in that the correct constraints are destroyed. However the lists are still not being kept in sync.


It appears the arrays in the Batch appear to grow over time even though I am calling RemoveConstraint(). Below, the last particle to have a Pin constraint added is 190, which is at the end of particleIndices, even though the active constraint count is 1, and the loop is examining the first indices. (m_IDToIndex points to 0 too.)

Is there another method that needs to be called in order to clean up the batch?


RE: Creating and Removing Pin Constraints from ObiPinConstraintsBatch - sebjf - 25-08-2023

I've managed to get it almost completely working now. 

Instead of adding constraints, I find if there is an existing constraint for that solver index. If there is, I update the batch's array properties and activate the index. Similarly, instead of removing constraints they are deactivated.

Code:
bool found = false;
for (int i = 0; i < adhesionBatch.constraintCount; i++)
{
if (adhesionBatch.particleIndices[i] == solverIndex)
{
adhesionBatch.pinBodies[i] = Dummy.Handle;
adhesionBatch.colliderIndices[i] = Dummy.Handle.index;
adhesionBatch.offsets[i] = positionOffset;
adhesionBatch.restDarbouxVectors[i] = orientationOffset;
// And everything else is the same...
found = true;
adhesionBatch.ActivateConstraint(i);
break;
}
}


What is odd is that even after this, when I created constraints on-demand (if they were not found), I would get occasions where the lists would lose sync still! However, if I pre-create a constraint for each actor particle's solver index in Start(), then it works correctly (which makes me think it is something to do with growing the array between activations/deactivations).

I prefer this solution regardless as it avoids reallocations. 

The only thing I don't like is needing to search the particleIndices when activating a constraint. Though my cloth has very few particles, I will try and use the m_ID and m_IDToIndex to persistently associate solver particles with an identity.


This is what I have so far, 

Code:
private void Start()
{
    actor.solver.OnCollision += Solver_OnCollision;
    graspBatch = new ObiPinConstraintsBatch();
    adhesionBatch = new ObiPinConstraintsBatch();

    // Pre-create the adhesion constraints

    actorIndexToConstraintId = new List<int>();
    for (int i = 0; i < actor.particleCount; i++)
    {
        var solverIndex = actor.solverIndices[i];
        adhesionBatch.AddConstraint(
            solverIndex,
            Dummy,
            Vector3.zero,
            Quaternion.identity,
            0,
            0,
            0.1f
        );
        actorIndexToConstraintId.Add(i);
    }
}

And then to look up the constraint from the solver index:

Code:
// Lookup the constraint index
var i = adhesionBatch.GetConstraintIndex(actorIndexToConstraintId[actor.solver.particleToActor[solverIndex].indexInActor]);

However this fails, and I need for now to get the constraintIndex by iterating the particles:

Code:
for (int j = 0; j < adhesionBatch.constraintCount; j++)
{
    if (adhesionBatch.particleIndices[j] == solverIndex)
    {
        if(i != j)
        {
            Debug.Log("Constraint Id Changed");
        }

        i = j;
        break;
    }
}

If I can get the above to work I can do away with the lists entirely and just store flags for each actor particle, which would be much nicer.