How to simulate the force to bend a tube?

I have a 3D tube, and there are two rope inside the tube. For example,

The two red color indicates the rope. Then, I pull rope b, and the tube would be bend.

Intuitively, I think the tube would bend into a arc. I want to know how to simulate this situation?

I have tried to use multiple cylinder to simulate this situation, and the pull actoin is simulated as a force on the top of tube. My simulation code is:

import direct.directbase.DirectStart
from panda3d.core import Vec3, TransformState, Point3, BitMask32, LPoint3, LVector3, Material
from panda3d.bullet import BulletWorld, BulletDebugNode, BulletCylinderShape
from panda3d.bullet import BulletPlaneShape, BulletSphericalConstraint, BulletConeTwistConstraint
from panda3d.bullet import BulletRigidBodyNode, BulletTriangleMesh, BulletTriangleMeshShape
from panda3d.bullet import BulletBoxShape
from direct.showbase.InputStateGlobal import inputState
import numpy as np

base.cam.setPos(0, -10, 0)
base.cam.lookAt(0, 0, 0)

debugNode = BulletDebugNode('Debug')
debugNode.showWireframe(True)
debugNode.showConstraints(True)
debugNode.showBoundingBoxes(True)
debugNode.showNormals(False)
debugNP = render.attachNewNode(debugNode)
debugNP.show()

# World
world = BulletWorld()
world.setGravity(Vec3(0, 0, 0))
world.setDebugNode(debugNP.node())

# Plane
shape = BulletPlaneShape(Vec3(0, 0, 1), 0)
node = BulletRigidBodyNode('Ground')
node.addShape(shape)
nodePath = render.attachNewNode(node)
nodePath.setPos(0, 0, 0)
world.attachRigidBody(node)

inputState.watchWithModifiers('x_add', 'd')
inputState.watchWithModifiers('x_minu', 'a')


num_segment = 30 # 5段圆柱
segment_length = 1
segment_radius = 1

shape = BulletCylinderShape(radius=segment_radius, height=segment_length)
previous_node = None
node_paths = []
for i in range(num_segment):
    node = BulletRigidBodyNode(f'segment_{i}')
    node.addShape(shape)
    node.setMass(1.0)

    if i == 0:
        # node.set_static(True)
        node_path = render.attachNewNode(node)
        node_path.setPos(0, 0, segment_length/2)

        constraint = BulletSphericalConstraint(node, Point3(0, 0, -segment_length / 2))
        world.attach_constraint(constraint)
    else:
        # # 将前一个节点连接到当前节点
        constraint = BulletConeTwistConstraint(previous_node, node, TransformState.make_pos(Vec3(0, 0, segment_length/2)),
                                               TransformState.make_pos(Vec3(0, 0, -segment_length/2)))
        constraint.setLimit(5, 5, 0, 100, relaxation=10)
        # constraint.set_damping(100)

        world.attach_constraint(constraint)

        node_path = render.attachNewNode(node)
        node_path.setPos(previous_node_path.get_pos() + Vec3(0, 0, segment_length))

    previous_node = node
    previous_node_path = node_path
    world.attachRigidBody(node)
    node_paths.append(node_path)

def process_input():
    force = LVector3(0, 0, 0)

    if inputState.isSet('x_minu'): force.x = -1.0
    if inputState.isSet('x_add'): force.x = 1.0

    force*=30

    last = node_paths[-1]
    last.node().set_active(True)
    last.node().apply_central_force(force)

def update(task):

    dt = globalClock.getDt()
    process_input()
    world.doPhysics(dt)
    return task.cont

taskMgr.add(update, 'update')
base.run()

This code has three problems:

  1. The tube would be bend when I press a, but the BulletConeTwistConstraint would try to reset it. And I don’t know how to fix it.

  2. The tube would no bend into a arc. For example:

  3. The BulletConeTwistConstraint would make this tube acting behave like the wind: the BulletConeTwistConstraint would alternately make the tube bend to right and left. I hope it bend and keep the status.

Is there any solution can simulate the tube and the inside rope? Any suggestion is appreciated~~~

I have made changes that are to achieve the effect of the stick.

  1. You did not take into account the collision of segments between each other, which led to unexpected results, I added group collision masks to eliminate this.

  2. Your stick contains too many segments, which made it difficult to return to rest.

  3. If you want to keep the stick shape, maybe you need to write your own algorithm for this, I have no idea what you can do with it, as an option not to update the physical world or something else.

from panda3d.core import loadPrcFileData
loadPrcFileData("", "bullet-filter-algorithm groups-mask")

import direct.directbase.DirectStart
from panda3d.core import Vec3, TransformState, Point3, BitMask32, LPoint3, LVector3, Material
from panda3d.bullet import BulletWorld, BulletDebugNode, BulletCylinderShape
from panda3d.bullet import BulletPlaneShape, BulletSphericalConstraint, BulletConeTwistConstraint
from panda3d.bullet import BulletRigidBodyNode, BulletTriangleMesh, BulletTriangleMeshShape
from panda3d.bullet import BulletBoxShape
from direct.showbase.InputStateGlobal import inputState

base.cam.setPos(0, -10, 0)
base.cam.lookAt(0, 0, 0)

debugNode = BulletDebugNode('Debug')
debugNode.showWireframe(True)
debugNode.showConstraints(True)
debugNode.showBoundingBoxes(True)
debugNode.showNormals(False)
debugNP = render.attachNewNode(debugNode)
debugNP.show()

# World
world = BulletWorld()
world.setGravity(Vec3(0, 0, 0))
world.setDebugNode(debugNP.node())
world.setGroupCollisionFlag(0, 0, False)

# Plane
shape = BulletPlaneShape(Vec3(0, 0, 1), 0)
node = BulletRigidBodyNode('Ground')
node.addShape(shape)
nodePath = render.attachNewNode(node)
nodePath.setPos(0, 0, 0)
world.attachRigidBody(node)

inputState.watchWithModifiers('x_add', 'd')
inputState.watchWithModifiers('x_minu', 'a')

num_segment = 5 # 5段圆柱
segment_length = 5
segment_radius = 1

shape = BulletCylinderShape(radius=segment_radius, height=segment_length)
previous_node = None
node_paths = []
for i in range(num_segment):
    node = BulletRigidBodyNode(f'segment_{i}')
    node.addShape(shape)
    node.setMass(1)
    node.setIntoCollideMask(BitMask32.bit(0))

    if i == 0:
        # node.set_static(True)
        node_path = render.attachNewNode(node)
        node_path.setPos(0, 0, segment_length/2)
        node_path.node().set_angular_factor(False)

        constraint = BulletSphericalConstraint(node, Point3(0, 0, -segment_length / 2))
        world.attach_constraint(constraint)

    else:
        # # 将前一个节点连接到当前节点
        constraint = BulletConeTwistConstraint(previous_node, node, TransformState.make_pos(Vec3(0, 0, segment_length/2)),
                                               TransformState.make_pos(Vec3(0, 0, -segment_length/2)))
        constraint.setLimit(3, 3, 0, 0, relaxation=2)
        #constraint.set_damping(100)

        world.attach_constraint(constraint)

        node_path = render.attachNewNode(node)
        node_path.setPos(previous_node_path.get_pos() + Vec3(0, 0, segment_length))

    previous_node = node
    previous_node_path = node_path
    world.attachRigidBody(node)
    node_paths.append(node_path)

def process_input():
    force = LVector3(0, 0, 0)

    if inputState.isSet('x_minu'): force.x = -1.0
    if inputState.isSet('x_add'): force.x = 1.0

    force*=30
 
    last = node_paths[-1]
    last.node().set_active(True)
    last.node().apply_central_force(force)

def update(task):

    dt = globalClock.getDt()
    process_input()
    world.doPhysics(dt)
    return task.cont

taskMgr.add(update, 'update')
base.run()

The rest means all cylinder is vertical. The cylinder tube turn to be bend by a force, then the force disappear and the BulletConeTwistConstraint would try to make the cylinder tube to be rest.

My question is: is it possible to add a continuous force to achieve a balance between added force and the BulletConeTwistConstraint force? In this way, the stick may keep the shape.

Then, I modify my code as:

...
num_segment = 50
segment_length = 1
segment_radius = 1
...

def process_input():
    force = LVector3(1, 0, 0)

    # if inputState.isSet('x_minu'): force.x = -1.0
    # if inputState.isSet('x_add'): force.x = 1.0

    force *= 1

    last = node_paths[-1]
    last.node().set_active(True)
    last.node().apply_central_force(force)

In this modified process_input, there is alway a force on the last cylinder. I expect this continous force may achieve a balance with the BulletConeTwistConstraint force. But, the stick is still hard to keep static.

Why the continuous force do not ahieve the balance?

Because you are using a simulation of physical laws, in reality you will also not be able to hold an object in a certain state. As I said earlier, you can develop an algorithm for constructing a composite object in the form you need using segment transformation data. When removing the force, you can return the object to its previous state, by the reverse action.

However, this requires effort in terms of implementation, there is no simple solution.

A short example that needs improvement.

from panda3d.core import loadPrcFileData
loadPrcFileData("", "bullet-filter-algorithm groups-mask")

import direct.directbase.DirectStart
from panda3d.core import Vec3, TransformState, Point3, BitMask32, LPoint3, LVector3, Material
from panda3d.bullet import BulletWorld, BulletDebugNode, BulletCylinderShape
from panda3d.bullet import BulletPlaneShape, BulletSphericalConstraint, BulletConeTwistConstraint
from panda3d.bullet import BulletRigidBodyNode, BulletTriangleMesh, BulletTriangleMeshShape
from panda3d.bullet import BulletBoxShape
from direct.showbase.InputStateGlobal import inputState

base.cam.setPos(0, -10, 0)
base.cam.lookAt(0, 0, 0)

debugNode = BulletDebugNode('Debug')
debugNode.showWireframe(True)
#debugNode.showConstraints(True)
#debugNode.showBoundingBoxes(True)
#debugNode.showNormals(False)
debugNP = render.attachNewNode(debugNode)
debugNP.show()

# World
world = BulletWorld()
world.setGravity(Vec3(0, 0, 0))
world.setDebugNode(debugNP.node())
world.setGroupCollisionFlag(0, 0, False)

# Plane
shape = BulletPlaneShape(Vec3(0, 0, 1), 0)
node = BulletRigidBodyNode('Ground')
node.addShape(shape)
nodePath = render.attachNewNode(node)
nodePath.setPos(0, 0, 0)
world.attachRigidBody(node)

inputState.watchWithModifiers('x_add', 'd')
inputState.watchWithModifiers('x_minu', 'a')
inputState.watchWithModifiers('hold', 'space')

num_segment = 5 # 5段圆柱
segment_length = 5
segment_radius = 1

shape = BulletCylinderShape(radius=segment_radius, height=segment_length)
previous_node = None
node_paths = []
for i in range(num_segment):
    node = BulletRigidBodyNode(f'segment_{i}')
    node.addShape(shape)
    node.setMass(1)
    node.setIntoCollideMask(BitMask32.bit(0))

    if i == 0:
        # node.set_static(True)
        node_path = render.attachNewNode(node)
        node_path.setPos(0, 0, segment_length/2)
        node_path.node().set_angular_factor(False)

        constraint = BulletSphericalConstraint(node, Point3(0, 0, -segment_length / 2))
        world.attach_constraint(constraint)

    else:
        # # 将前一个节点连接到当前节点
        constraint = BulletConeTwistConstraint(previous_node, node, TransformState.make_pos(Vec3(0, 0, segment_length/2)),
                                               TransformState.make_pos(Vec3(0, 0, -segment_length/2)))
        constraint.setLimit(5, 5, 0, 0.2, relaxation = 10)
        #constraint.set_damping(100)

        world.attach_constraint(constraint)

        node_path = render.attachNewNode(node)
        node_path.setPos(previous_node_path.get_pos() + Vec3(0, 0, segment_length))

    previous_node = node
    previous_node_path = node_path
    world.attachRigidBody(node)
    node_paths.append(node_path)

state = 1

def process_input():
    force = LVector3(0, 0, 0)

    if inputState.isSet('x_minu'): force.x = -1.0
    if inputState.isSet('x_add'): force.x = 1.0

    force*=50
 
    last = node_paths[-1]
    last.node().set_active(True)
    last.node().apply_central_force(force)

def update(task):

    process_input()

    if inputState.isSet('hold'): 
        state = 0
    else:
        state = 1

    if state == 1:
        dt = globalClock.getDt()
        world.doPhysics(dt)

    return task.cont

taskMgr.add(update, 'update')
base.run()

Press the space to fix the shape, then you can build an algorithm to create a duplicate in this position and add it to the graph as a new object. If you want to return dynamic properties, then simply unfreeze the previous state, and delete the object with a fixed shape.