Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
scripting ropes with pin constraints & tethers
#1
Hi –

I’m working on a scene made up of hanging chains of objects, each connected by a short rope: the first particle of each rope is pinned to an object at the top, and the last particle is pinned to an object at the bottom. The first object of the first rope is set to kinematic (to anchor the whole chain at the top). It’s all working pretty well, but I have a few questions.

The final scene will have lots of ropes; each chain contains about 6 ropes, and there will be more than 20 chains in total, so that means more than 100 ropes. Putting all of this together in the editor was impractical, so I’ve written a script (based on ObiRopeHelper.cs) to help automate the process. Script included below.

When the simulation starts, the rope springs like a rubber band that’s just been let go and then settles down. I'd like to have everything begin in a resting state (unmoving). In this thread: http://obi.virtualmethodstudio.com/forum...hp?tid=175 the solution suggested is disable the rope and wait for the next FixedUpdate before reenabling. I’ve tried to implement that in my script, but it doesn’t seem to be having any effect.

Next, I would like the ropes to be as non-elastic as possible. I realize that the recommended way of doing this is via tethers, but because my ropes aren’t fixed, tethers wouldn’t seem to be an option. However, in this thread: http://obi.virtualmethodstudio.com/forum....php?tid=6 there’s a method described in which the rope is fixed, tethers are generated, then the rope is unfixed. Once again, I’ve tried to implement this in my script, but it doesn’t seem to be having any effect.

I’ve therefore tried to set the rope length by using reduced values for stretchingScale (in ObiDistanceConstraints). I’m sure this isn’t ideal, but it basically seems to work (but maybe this is causing the initial rubber band behavior?)

Finally, I’ve found that each individual rope has to have its own dedicated solver; if I try to share a single solver across multiple ropes, Unity immediately crashes. I’m guessing this has something to do with removing and adding multiple pin constraints from the solver simultaneously/asynchronously? (Just a guess). Anyway, my question is whether there will be any problems (like a performance hit) by having upwards of 100 solvers in a single scene.


Code:
using UnityEngine;
using System.Collections;

namespace Obi {
    [RequireComponent(typeof(ObiRope))]
    [RequireComponent(typeof(ObiCatmullRomCurve))]
    [RequireComponent(typeof(ObiPinConstraints))]
    [RequireComponent(typeof(ObiDistanceConstraints))]
    [RequireComponent(typeof(ObiTetherConstraints))]

    public class ObiConnectorRope : MonoBehaviour {

        public ObiSolver solver;
        public ObiActor actor;
        
        public ObiRopeSection section;
        public Material material;
        
        public float ropeLength = 1.0f; // desired rope length
        public float uvYMult = 2.5f; // keep UVScale Y consistent across rope lengths
        
        public ObiCollider upperPinObject;
        public ObiCollider lowerPinObject;
        
        private Transform point1; // offset at top of object
        private Transform point2; // offset at bottom of object
        
        private ObiRope rope;
        private ObiCatmullRomCurve path;
        
        private ObiPinConstraints pinConstraints;
        private ObiPinConstraintBatch constraintsBatch;
        private ObiDistanceConstraints distanceConstraints;
    
        void Start () {
            
            actor.enabled = false;
            
            // move rope position to match upper pinned object
            transform.position = upperPinObject.transform.position;
            
            // Get all needed components and interconnect them:
            rope = GetComponent<ObiRope>();
            path = GetComponent<ObiCatmullRomCurve>();
            rope.Solver = solver;
            rope.ropePath = path;    
            rope.Section = section;
            GetComponent<MeshRenderer>().material = material;
            
            // use stretching scale to set approximate rope length & UVScale Y
            distanceConstraints = rope.GetComponent<ObiDistanceConstraints>();
            distanceConstraints.stretchingScale = (ropeLength * 0.14f) - 0.05f; // ad hoc formula
            rope.UVScale = new Vector2(1.0f, ropeLength * uvYMult);
            
            // register offsets
            point1 = upperPinObject.transform.Find("pinPointB").gameObject.transform;
            point2 = lowerPinObject.transform.Find("pinPointA").gameObject.transform;
            
            // Calculate rope start/end and direction in local space:
            Vector3 localStart = transform.InverseTransformPoint(upperPinObject.transform.position);
            Vector3 localEnd = transform.InverseTransformPoint(lowerPinObject.transform.position);
            Vector3 direction = (localEnd-localStart).normalized;
            // Add control points
            path.controlPoints.Add(localStart-direction);
            path.controlPoints.Add(localStart);
            path.controlPoints.Add(localEnd);
            path.controlPoints.Add(localEnd+direction);

            // Setup the simulation:
            StartCoroutine(Setup());
            
            // Wait for next FixedUpdate
            StartCoroutine(WaitOne());
            
            actor.enabled = true;
        }

        IEnumerator Setup(){
            
            // Generate particles and add them to solver:        
            yield return StartCoroutine(rope.GeneratePhysicRepresentationForMesh());
            
            rope.AddToSolver(null);
            
            // Generate tethers
            // Fix first particle:
            float tempMass = rope.invMasses[0];
            rope.invMasses[0] = 0;
            Oni.SetParticleInverseMasses(solver.OniSolver,new float[]{0},1,rope.particleIndices[0]);
            rope.GenerateTethers(ObiActor.TetherType.AnchorToFixed);
            // Unfix first particle:
            rope.invMasses[0] = tempMass;
            Oni.SetParticleInverseMasses(solver.OniSolver,new float[]{0},1,rope.particleIndices[0]);
            actor.PushDataToSolver(ParticleData.INV_MASSES);
            
            // Set pin constraints    
            pinConstraints = rope.GetComponent<ObiPinConstraints> ();
            constraintsBatch = (ObiPinConstraintBatch)pinConstraints.GetBatches() [0];
            pinConstraints.RemoveFromSolver(null);    
            // Add top pin constraint
            constraintsBatch.AddConstraint(rope.particleIndices[rope.particleIndices[0]], upperPinObject.GetComponent<ObiCollider>(), point1.localPosition, 0f);
            // Add bottom pin constraint
            constraintsBatch.AddConstraint(rope.particleIndices[rope.UsedParticles-1], lowerPinObject.GetComponent<ObiCollider>(), point2.localPosition, 0f);

            pinConstraints.AddToSolver(null);
            
            pinConstraints.PushDataToSolver();
        }    
        
        IEnumerator WaitOne(){
            yield return new WaitForFixedUpdate();
        }    
    }
}
Reply
#2
Hi there,

Quote:When the simulation starts, the rope springs like a rubber band that’s just been let go and then settles down. I'd like to have everything begin in a resting state (unmoving). In this thread: http://obi.virtualmethodstudio.com/forum...hp?tid=175 the solution suggested is disable the rope and wait for the next FixedUpdate before reenabling. I’ve tried to implement that in my script, but it doesn’t seem to be having any effect.

Upon initialization, the initial distance between the rope particles is used to set as the "relaxed" state of the rope. Changing the distance constraints "scale" parameter will make the rope longer or shorter right after initializing it, causing it to suddenly elongate or contract. This of course will make the rope spring back to the size dictated by the scale.

If you want the rope to keep a given initial shape, set the control points of the Catmull-Rom curve accordingly. (This thread is relevant: http://obi.virtualmethodstudio.com/forum...hp?tid=233)

The method outlined in http://obi.virtualmethodstudio.com/forum...hp?tid=175 works when "teleporting" a rope to a different place in the scene, usually you do not want the abrupt change in location to affect the physics of the rope. It works by disabling the rope, changing its transform, and then re-enabling it. However it will not work in your case as (afaik) you're not changing the rope transform (position/rotation/scale), but the constraint properties. Applying this method will have no effect.

Quote:Next, I would like the ropes to be as non-elastic as possible. I realize that the recommended way of doing this is via tethers, but because my ropes aren’t fixed, tethers wouldn’t seem to be an option. However, in this thread: http://obi.virtualmethodstudio.com/forum....php?tid=6 there’s a method described in which the rope is fixed, tethers are generated, then the rope is unfixed. Once again, I’ve tried to implement this in my script, but it doesn’t seem to be having any effect.

Regarding rope elasticity: have you tried to simply increase the amount of distance/pin constraint iterations in the solver, or reduce Unity's physics timestep? This should be tried before resorting to tethers. Tethers (also known in physics jargon as LRAs or long range attachments) are useful when dealing with large mass ratios (heavy objects pinned to light ropes), as they give iterative solvers a really hard time. This doesn't seem to be your case.

If you want to use the fix/add tethers/unfix method, make sure tether constraints are enabled both in the rope and the solver!

Quote:Finally, I’ve found that each individual rope has to have its own dedicated solver; if I try to share a single solver across multiple ropes, Unity immediately crashes. I’m guessing this has something to do with removing and adding multiple pin constraints from the solver simultaneously/asynchronously? (Just a guess). Anyway, my question is whether there will be any problems (like a performance hit) by having upwards of 100 solvers in a single scene.

I've tried to add multiple ropes to the same solver with no issues. Can you share the code you used to do this?

Having multiple solvers in the same scene will incur a slight performance cost for each additional solver. The best approach is usually to group ropes by spatial proximity under the same solver, that way they can be culled together. If your ropes are scattered across the level, then it's best to use a single solver for each one (hint: use prefabs).

cheers!
Reply
#3
Hi Jose – 

Based on your suggestions, I've been attempting to fix all the problems I'm having, but nothing seemed to be helping. So I started to suspect that there was something more going on, and I thought it might be useful to build a test scene with a simple comparison between an normal ObiRope and the one I'm trying to script. 

For some reason, this forum won't let me attach either a Unity package or a zip file ("The type of file that you attached is not allowed" – is there some sort of trick?) so I've put it up on Dropbox (the package and the zipped package just in case – both are the same) – here's the link:

https://www.dropbox.com/sh/00ax1phu767el...CLb2a?dl=0

The package includes the scene (ropeComparison3), my script (ObiRopeScriptTest.cs), a prefab (Breeze) with an ObiAmbientForceZone and a simple script (Rotator.cs) to move the ropes around a bit.

As you'll see when you run the scene, the rope that I'm creating through my script is behaving nothing like one that that was built in the Unity editor. 

There are two ropes: the one to the left is the normal one, and the one to the right is the one I'm scripting. Each is pinned to an kinematic rigidbody at the top and a second rigidbody at the bottom. There are two solvers, one for each rope. 

The scripted rope has a much longer resting state than the normal rope. Why?

The normal rope has a Resolution of 0.25, which generates six particles. If I set the resolution of my scripted rope to 0.25, I get about 24 particles. To generate six particles, I had to lower the Resolution to 0.06. Why?

The scripted rope is much more jumpy when it's initialized. Why?

Each rope has its own solver. If I assign the same solver to both ropes, the scripted rope fails. Why?

Thanks again for all your help!

- pH
Reply
#4
(29-12-2017, 07:20 PM)phoberman Wrote: Hi Jose – 

Based on your suggestions, I've been attempting to fix all the problems I'm having, but nothing seemed to be helping. So I started to suspect that there was something more going on, and I thought it might be useful to build a test scene with a simple comparison between an normal ObiRope and the one I'm trying to script. 

For some reason, this forum won't let me attach either a Unity package or a zip file ("The type of file that you attached is not allowed" – is there some sort of trick?) so I've put it up on Dropbox (the package and the zipped package just in case – both are the same) – here's the link:

https://www.dropbox.com/sh/00ax1phu767el...CLb2a?dl=0

The package includes the scene (ropeComparison3), my script (ObiRopeScriptTest.cs), a prefab (Breeze) with an ObiAmbientForceZone and a simple script (Rotator.cs) to move the ropes around a bit.

As you'll see when you run the scene, the rope that I'm creating through my script is behaving nothing like one that that was built in the Unity editor. 

There are two ropes: the one to the left is the normal one, and the one to the right is the one I'm scripting. Each is pinned to an kinematic rigidbody at the top and a second rigidbody at the bottom. There are two solvers, one for each rope. 

The scripted rope has a much longer resting state than the normal rope. Why?

The normal rope has a Resolution of 0.25, which generates six particles. If I set the resolution of my scripted rope to 0.25, I get about 24 particles. To generate six particles, I had to lower the Resolution to 0.06. Why?

The scripted rope is much more jumpy when it's initialized. Why?

Each rope has its own solver. If I assign the same solver to both ropes, the scripted rope fails. Why?

Thanks again for all your help!

- pH

Hi there!

First thing that catches my eye is that your scripted rope's spline already has some control points (CPs for short) created in the editor, but you're adding more trough scripting. So the resulting rope is of course longer. Changing the code in ObiRopeScriptTest.cs to this:

Code:
path.controlPoints.Clear();
path.controlPoints.Add(localStart+(localStart-localEnd));
path.controlPoints.Add(localStart);
path.controlPoints.Add(localEnd);
path.controlPoints.Add(localEnd+(localEnd-localStart));

makes things all better Sonrisa. What I'm doing here is first clearing the CP list (so that we start with a fresh spline, instead of appending additional CPs to the existing ones), then adding 4 cps: first tangent handle, first control point, second control point, second tangent handle. As pointed out in http://obi.virtualmethodstudio.com/forum...hp?tid=233, splines mathematically require duplicated endpoints to control curvature, so the minimum amount of CPs to create a meaningful curve is always 4. You could use 2 or 3 points, but the resulting curve would be "collapsed" into a point.

As you can imagine, this also solves the issue regarding the amount of particles: a shorter rope needs less particles, so you can set the resolution back to 0.25. A rope with less particles has less constraints, so the convergence speed is the same for both ropes --> both have the same "stretchiness".

Also you'll see some minor differences in the way pin constraints are created: lowerPinObject B has a Y coordinate of -1.1, while lowerPinObjectA is at -1.0. ropeA's spline CPs are not exactly at the center of the upper/lower pin objects, so the pin constraints have been created with a non-zero offset value (that is, they pin the rope to points away from the pinObject's center). However the scripted rope creates the pin constraints with a offset of zero, so the behavior is slightly different.

Ironing out these small things yields twin ropes.
Reply
#5
Thanks Jose – that gets me much, much closer.

But it leaves two remaining questions unanswered:

1. The scripted rope still jumps around much more at initialization. Any way to fix that?

2. Assigning both ropes to the same solver (both with SolverA entered in the ObiRopeScriptTest inspector) causes the scripted rope to fail. Multiple "normal" ropes can be assigned to the same solver, but each scripted rope fails if it doesn't have its own dedicated solver.

Thanks once more!

- pH
Reply
#6
(30-12-2017, 07:25 PM)phoberman Wrote: Thanks Jose – that gets me much, much closer.

But it leaves two remaining questions unanswered:

1. The scripted rope still jumps around much more at initialization. Any way to fix that?

2. Assigning both ropes to the same solver (both with SolverA entered in the ObiRopeScriptTest inspector) causes the scripted rope to fail. Multiple "normal" ropes can be assigned to the same solver, but each scripted rope fails if it doesn't have its own dedicated solver.

Thanks once more!

- pH


1.- Since you must wait for the GeneratePhysicRepresentationForMesh() coroutine to finish generating the rope at runtime (which takes more than 1 frame), this allows the rigidbody at the bottom of the procedural rope to fall freely for a few moments. When the rope is finally created, the rigidbody is slightly lower and the rope springs back with slightly more force than its in-editor counterpart. To prevent this, freeze the rigidbody in place (make it kinematic) until the generation coroutine is finished.

2.- At line 54/56 of ObiRopeScriptTest.cs you're doing this:
Code:
constraintsBatch.AddConstraint(rope.particleIndices[rope.particleIndices[0]], upperPinObject.GetComponent<ObiCollider>(), Vector3.zero, 0f);
constraintsBatch.AddConstraint(rope.particleIndices[rope.UsedParticles-1], lowerPinObject.GetComponent<ObiCollider>(), Vector3.zero, 0f);

This doesn't really make sense, but it works for the first rope added to a solver: The first particle in the first rope will be the first particle in the solver, so rope.particleIndices[rope.particleIndices[0]] will evaluate to 0, which is an existing particle. However for the second, third, fourth rope in the solver, this will likely result in an out of bounds access inside the solver, which as pointed out in the docs, leads to undefined behaviour including the possibility of a crash.

The low-level physics library does not perform range checks for performance reasons (it assumes the caller does), so if you pass invalid indices you're in trouble. Also remember that the indices passed to AddConstraint() are actor indices, not solver indices. So if you pass 0, that means first particle in the rope. If you pass rope.UsedParticles-1, that means last particle in the rope.

With all this in mind, you can do this instead and it will work fine:
Code:
constraintsBatch.AddConstraint(0, upperPinObject.GetComponent<ObiCollider>(), Vector3.zero, 0f);
constraintsBatch.AddConstraint(rope.UsedParticles-1, lowerPinObject.GetComponent<ObiCollider>(), Vector3.zero, 0f);

cheers! Sonrisa
Reply
#7
Hi Jose –

Perfect! Everything's working now.

For whatever reason, unfreezing the rigidbodies immediately after the generation coroutine didn't stop the bouncing, but waiting a bit longer (via a short WaitForSeconds delay) took care of it.

And I've got multiple ropes using a single solver now.

By the way, Obi Rope is without question one of the best designed & most useful Unity add-ons ever – but what really puts it over the top is the level of support here on the forums. 

So one more time, thanks!

- pH
Reply