[Bullet] Linking a jointed model to a Bullet softbody rope


#1

SUMMARY:
This is a demonstration on how to pass on the deformations of a Bullet softbody rope object (as made with ‘BulletSoftBodyNode.makeRope’) to an Actor-type rigged model using a continuous task. This is done by calling ‘lookAt’ on joints in our model and passing the position of nodes in the softbody rope as arguments.

It requires a model that is set up with an armature containing a sequence of joints that are made to work with the specific length and subdivisions settings you will use for your softbody rope. Therefore, it is recommended that you decide on the final settings of your softbody rope before rigging your model.

You can find a self-contained example attached at the bottom, illustrating the process in code. It contains additional commentary regarding details of its specific setup.

WHY THIS IS NECESSARY:
While a softbody rope has a method called ‘linkGeom’, it only allows the linking of line-like geometry that is to be rendered in wireframe mode with a thickness setting. This is the same type as produced by ‘BulletHelper.makeGeomFromLinks’ (as described in the manual here). In its current state, linking any other type of geometry will simply make it invisible (as far as I can tell, due to scaling it to 0 in the X and Y axes, among other operations).

It allows, separately, to link and deform a NURBS curve, which is enough in some cases as it can receives shading, materials and textures, but not if you want a rope-like object with more complex geometry (e.g. chains).

HOW IT WORKS:
N.B.: All uses of ‘parent’ refer to the common parent object of both your softbody rope and the model (the model as a whole, not the parent joint of the joint. In most cases, ‘render’ will be the parent).

A Bullet softbody rope is represented by a sequence of nodes. Call ‘getNodes()’ on the softbody rope, and they will be provided as a list, sorted from start node to end node.
While these nodes are a class of their own, you can still get their position using ‘getPos()’. You can pass this position onto another object, like a joint in this example. If you have a joint for every node in the rope, performing this operation every render frame will produce the illusion that the targets are perfectly linked to the softbody rope.

The most basic way to do this would be using ‘joint.setPos(parent, node.getPos())’. However, doing this will only displace the target, not orient it. This effect can have uses of its own, but is not accurate in most cases.

Instead, one of the easiest ways is to use the ‘lookAt’ method of the parent joint to the joint you’re trying to place, but it requires preparation. See a full description in HOW TO RIG YOUR MODEL below.

Once the model is loaded as an Actor-type in your Panda3D project, parent it to the same object as your softbody rope is parented to, and align the position of the root joint in your rig with the position of the start node of the rope.

With the exception of your root joint, all joints in your armature are children of their previous joint and therefore both positioned and oriented in relationship to it. So simply calling ‘parent_joint.lookAt(parent, node.getPos())’ will orient the parent joint correctly while aligning the child joint with the target node, assuming the spacings are identical:

Then, all you have to do is to call ‘lookAt’ this way for every ‘jointN, nodeN+1’ pair in order to perfectly line up the model with the softbody rope every frame, providing the illusion of a link.

One solution is to set up a continuous task as shown below in CODE MOCK-UP.

HOW TO RIG YOUR MODEL:
It is best to rig the model only once you’ve decided on the final settings of your softbody rope. Once this is done, count the number of nodes N it has, calculate the constant spacing S between each node, then rig your model with an armature that has a sequence of N joints that are each of length S.

As far as vertex weighting goes, there are most likely different ways to do it, but the one I used and tested had each joint controlling the vertices that were at its tail, while the vertices at the joint’s head were controlled by its child joint, and so on for all joints in the sequence until the last joint, which also controlled the vertices at the head at 100% weight.

For models that have vertices between the head and tail of each joint, it is probably best to assign them all to the joint that they overlay at 100% weight, except for the vertices at level with the head of the joint.
It might be possible to get a softening/curving effect by using a gradient weighting, reaching 0% at the head of the joint, but I have not tested this, and might be difficult to set up depending on the complexity of your model.

CODE MOCK-UP:
It is assumed that you have built up a softbody rope as described in the manual under “Bullet SoftBody Rope” as well as loaded an Actor-type model rigged as described above.

First, here is how to properly control these joints:

In order for a joint controlled with ‘controlJoint’ to accept a reference object as first argument to methods likes ‘lookAt’ or ‘setPos’, you must provide a dummy NodePath that is parented to the parent joint as first argument to ‘controlJoint’. This is done as follows:

parent_joint: The parent joint of the one you are trying to control, either already provided through a previous call to ‘controlJoint’, or can be obtained with ‘exposeJoint’ if you do not intend to animate it.
local_position: The position of the child joint in its parent’s local space. If your joint sequence is perfectly straight and each joint is of length 2, ‘local_position’ would be (0,2,0).
model_rope: The Actor-type model, rigged and loaded to be linked to the softbody rope.

dummy = parent_joint.attachNewNode('dummy')
child_joint = model_rope.controlJoint(dummy, 'modelRoot', 'child_joint_name’)
child_joint.setPos(local_position)

This operation is only necessary on child joints. If the root joint of your armature is the first in the sequence, giving ‘None’ as first argument will work.

root_joint = model_rope.controlJoint(None, 'modelRoot', 'root_joint_name’)

You would then collect these joints in a list, for example, sorted from first to last. (First being the joint that is to be linked to the start node of the rope, and last being the one to be linked to the end node.)

Next, here’s how to link the joints to the softbody rope nodes.

parent : the common parent object for both the softbody rope and the jointed model. (e.g. ‘render’, in most cases.)
model_rope_joints : a collection(e.g. a tuple) containing all joints that you wish to link, sorted from the start joint to the end joint in the sequence, as demonstrated above.
softbody_rope_nodes : a collection containing all nodes of the softbody rope, sorted from start node to end node, as given by ‘rope.getNodes()’.

def _task_animaterope(self, task):
    parent = self.parent
    model_rope_joints = self.model_rope_joints
    softbody_rope_nodes = self.bullet_softbody_rope_nodes

    def positionjoint(index):
        if index > 0:
            node = softbody_rope_nodes[index]
            pos = node.getPos()
            parent_joint = model_rope_joints[index - 1]
            parent_joint.lookAt(parent, pos, (1, 0, 0))

    map_rope_joints_positions = map(positionjoint, range(len(model_rope_joints)))
    tuple(map_rope_joints_positions)
    return task.cont

taskMgr.add(self._task_animaterope, '_task_animaterope', sort=2)

The ‘(1,0,0)’ given in 3rd position to ‘lookAt’ is an ‘up’ vector given to minimize unwanted rolling of the joints.
I add the task with ‘sort=2’ as I have my Bullet ‘doPhysics’ task set up at ‘sort=1’ and wish to ensure that the latter always runs first.

For a full, contextual implementation, see the example project provided in attachment at the bottom.

UNTESTED THINGS:
a) Anchoring settings:
I have not tested how this setup works with other anchoring settings (the ‘fixeds’ argument to ‘makeRope’, in 5th and last place), so if you want, for example, a rope that is not attached to anything, or a rope that is attached at its end node, you might have to do some additional testing.

b) Collisions and other physics effects:
I have not tested how the softbody rope reacts to collisions or how one would best set up collisions for the rope model. Parenting individual rigid body shapes to the joints might do the trick, but don’t take my word.

c) Rope nodes’ position as given by ‘node.getPos()’
I did not test what the position given by ‘node.getPos()’ is calculated in relationship to. I assume it is in relationship to the parent object of the softbody rope as a whole. If you parented your rope to something other than ‘render’ and encounter issues, this could be a cause.

d) Joints as only a rope-like part of a bigger armature (e.g. long strands of hair)
While I haven’t tested how best to set it up, it should be perfectly possible to have additional joints in your armature that are not linked to nodes in the softbody rope and even have them animate or be controlled separately. They could be children of joints in your rope sequence, or parents/siblings of the rope sequence itself. In the latter case, you’d most likely have to parent your softbody rope to the bigger object they are part of, and calculate rope node positions in relationship to that.

KNOWN ISSUES:
a) Unwanted rotations:
Since the softbody rope is built as a collection of points, these points have strictly speaking no spatial orientation. Due to this, the model to which the deformations are passed can and will rotate a lot without any physical impulse.
Most of the rotations that would occur can be alleviated by passing an ‘up’ vector to ‘lookAt’. Some will still occur. Certain anchoring settings in your setup might result in an increase of unwanted rotations. Writing in some separate calculations to keep the rope’s rotations constant or logical might therefore be necessary.

b) Mass glitch:
If, like in the example file, you have a bullet rigid body anchored to a rope node, certain combinations of masses for the rope and the rigid body may cause a glitch where the rigid body is pulled towards the previous node of the rope and has some sort of self-collision.
EDIT 2019/03/08
This glitch can be solved by tuning the softbody rope’s configuration. Here’s how to do that.
On the same post as the link is a workaround you can use should the first solution fail, for some reason.

NOTES:
One big stumbling block was passing global data to controlled joints. If you’ve tried figuring this out, you might have stumbled upon the following thread - specifically this post by drwr describing what to do if you want joints to accept non-local values (e.g. so that calling ‘joint.setPos(render, (0,1,2))’ works, for example). While most of this works exactly as described, it is NOT necessary to call ‘exposeJoint’ on the parent joint, at least not if you’ve already called and stored a pointer to it via ‘controlJoint’. In fact, calling ‘exposeJoint’ will cause glitches in the model’s deformations.

CREDITS:
Many thanks to rdb for figuring out the problem described in NOTES.
Also thanks to the people in the IRC channel for patiently listening to me trying to figure all this out.
And another thank you to Joel Stienlet to finding a solution to the appended mass glitch.

EXAMPLE FILE:

Bullet_Softbody_Rope_to_Jointed_Model.zip (17.3 KB)


#2

EDIT: 2019/03/08

This workaround is no longer necessary, as the mass glitch can be fixed by modifying the number of positions solver the softbody rope uses. This can be done by getting the cfg object of the rope with cfg = rope.getCfg() and setting a higher value via cfg.setPositionsSolverIterations(x), where x is a higher value that whatever the current value is where you are still experiencing the glitch.

This solution was discovered by @JoelStienlet on the Github issue report about this problem.

Mass Glitch Workaround:

You can anchor softbody ropes to heavy objects without causing this glitch by providing a bullet node with a lower mass (‘connector’) as an intermediate anchor point.
This is not a perfect solution, but might be enough in most cases.

Here is a step-by-step:

1 - Set up your scene, objects and rope as you intend.
2 - Instead of anchoring the chosen node of your softobody rope directly to the chosen object, create a ‘connector’. Here is an example of one:

from panda3d.bullet import BulletBoxShape, BulletRigidBodyNode

shape = BulletBoxShape(.2)
connector = BulletRigidBodyNode('connector')
connector.addShape(shape)
connector.setMass(0.01)

bullet_world.attach(connector)
render.attachNewNode(connector)

3 - Set up a “BulletGenericConstraint” with limits set so as to be a ‘parent’-type constraint between your ‘connector’ and the object you wish to anchor. Here is an example of such a constraint:

anchor_object : The object to which you wish to anchor your rope.

from panda3d.bullet import BulletGenericConstraint
from panda3d.core import TransformState

tsA = TransformState.makePosHpr((0,0,0)(0,0,0))
tsB = TransformState.makePosHpr(connector.getPos(anchor_object),(0,0,0))

parent_constraint = BulletGenericConstraint(connector.node(),anchor_object.node(), tsA, tsB, True)

parent_constraint.setAngularLimit(0, 0, 0)
parent_constraint.setAngularLimit(1, 0, 0)
parent_constraint.setAngularLimit(2, 0, 0)

bullet_world.attachConstraint(parent_constraint, True)

The last argument in ‘attachConstraint’, ‘True’, ensures the connector and anchor_object do not collide with eachother. As a result, a collision mask is not necessary to prevent it.

4 - Then, anchor the chosen node of your rope to the ‘connector’, as described in the original post.

NOTE: Softbody rope anchors are not immobile; which is to say that they can stretch if a force is imparted to them. As a result, if a heavy weight or another force pulls on a rope that is anchored to another object, the anchor points will stretch and move under the force thusly enacted. I have not yet found a solution for this.