How to understand apply_central_force

I want to stack some capsules to simulate a tube. It looks like:

Then, I apply the force to the top capsule to make this tube bending, and I expect these capsules bending to arc.

In order to achieve my purpose, I calculate the coordinate x/y/z for the top capsule, and calculate the force:

    x = node_paths[-1].getMat()
    m = np.array(x)
    x = [m[0][0], m[1][0], m[2][0]]
    y = [m[0][1], m[1][1], m[2][1]]
    z = [m[0][2], m[1][2], m[2][2]]

    f = xforce*np.array(x) + yforce*np.array(y)
    last.node().set_active(True)
    last.node().apply_central_force(force)

When press the keyboard d, the xforce would increase one: xforce = xforce + 1. Finally, the result is:

And it do not change even I press d.

The complete code to reproduce my experiment is:

import time

import direct.directbase.DirectStart
from panda3d.core import Vec3, TransformState, Point3, BitMask32, LPoint3, LVector3, Material
from panda3d.bullet import BulletWorld, BulletDebugNode, BulletCylinderShape, BulletCapsuleShape
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')
inputState.watchWithModifiers('y_add', 'w')
inputState.watchWithModifiers('y_minu', 's')
# inputState.watchWithModifiers('turnLeft', 'q')
# inputState.watchWithModifiers('turnRight', 'e')
inputState.watchWithModifiers('up', 'u')

num_segment = 10
segment_length = 1
segment_radius = 1


shape = BulletCapsuleShape(radius=segment_radius, height=segment_length)
previous_node = None
node_paths = []

# pos = [75.81992094545193, 314.34202388786053, 9.5]
for i in range(num_segment):
    node = BulletRigidBodyNode(f'segment_{i}')
    node.addShape(shape)
    node.setMass(1.0)
    node.set_linear_damping(0.1)
    node.set_angular_damping(0.1)

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

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


    else:
        # # 将前äø€äøŖ节ē‚¹čæžęŽ„åˆ°å½“å‰čŠ‚ē‚¹
        constraint = BulletConeTwistConstraint(previous_node, node,
                                               TransformState.make_pos(Vec3(0, 0, segment_length / 2 + segment_radius)),
                                               TransformState.make_pos(
                                                   Vec3(0, 0, -segment_length / 2 - segment_radius)))
        world.attach_constraint(constraint)

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

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

uselessConstrain = []

global xforce, yforce
xforce = 0
yforce = 0
def process_input():
    global xforce, yforce

    if inputState.isSet('y_add'):
        yforce += 1
        time.sleep(0.2)
    if inputState.isSet('y_minu'):
        yforce -= 1
        time.sleep(0.2)
    if inputState.isSet('x_minu'):
        xforce += 1
        time.sleep(0.2)
    if inputState.isSet('x_add'):
        xforce -= 1
        time.sleep(0.2)

    x = node_paths[-1].getMat()
    m = np.array(x)
    x = [m[0][0], m[1][0], m[2][0]]
    y = [m[0][1], m[1][1], m[2][1]]
    z = [m[0][2], m[1][2], m[2][2]]

    f = xforce*np.array(x) + yforce*np.array(y)
    print(xforce, yforce)

    force = LVector3(f[0], f[1], f[2])

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


# Update
def update(task):
    dt = globalClock.getDt()
    process_input()
    world.doPhysics(dt)
    return task.cont


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

Any suggestion is appreciated~~~~

I donā€™t have numpy installed, so I removed that with a quick hack, andā€¦ when I press ā€œdā€ I see the column of capsules curve over. Pressing other buttons causes them to curl around. Is that what should happen?

See the screenshots below:


If that is the intended behaviour, then it looks like the problem that youā€™re seeing comes from the way that youā€™re handling the forceā€“it looks like youā€™re creating some sort of matrix, and then extracting a vector from that?

What I did was rather simpler: I constructed the force directly from the ā€œxforceā€ and ā€œyforceā€ values, presuming the ā€œxforceā€ to be the x-component of the force and ā€œyforceā€ to be the y-component. Like so:

force = LVector3(xforce, yforce, 0)

In the example-code that you gave, that line replaces everything from the last ā€œtime.sleep(0.2)ā€ (i.e. starting with ā€œx = node_paths[-1].getMat()ā€), up to (and including) the line ā€œforce = LVector3(f[0], f[1], f[2])ā€.

The ā€œprocess_inputā€ method thus ends up looking like this:

def process_input():
    global xforce, yforce

    if inputState.isSet('y_add'):
        yforce += 1
        time.sleep(0.2)
    if inputState.isSet('y_minu'):
        yforce -= 1
        time.sleep(0.2)
    if inputState.isSet('x_minu'):
        xforce += 1
        time.sleep(0.2)
    if inputState.isSet('x_add'):
        xforce -= 1
        time.sleep(0.2)

    force = LVector3(xforce, yforce, 0)

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

Thanks very much for your reply.

I have tried force = LVector3(xforce, yforce, 0) before. The reason why I chaneg the force to current version is: I hope the capsules would turn to be a circle. In this way, I hope the force would be vertical to the capsule direction. For example:

The black line represent the capsules, and the red arrow is the force. I want the force would always be vertical to the direction of capsule. In this way, the force would finally make the capsules turn to be a circle. Is it possible?

Or how can I make the capsules turn to be circle by external force?

Ah, I see!

Well, you could perhaps take the component vectors of the capsuleā€™s orientation (its ā€œforwardā€, ā€œrightā€, and ā€œupā€ vectors) and use those as the basis of your force.

Specificallyā€“if Iā€™m reading your diagram correctly; adjust as called for if notā€“you might have the x- and y- forces be applied to the ā€œrightā€ and ā€œforwardā€ vectors.

And since these vectors represent the capsuleā€™s orientation, they effectively ā€œrotateā€ with the capsule, allowing the sort of capsule-relative force that you seem to be describing.

Something like this:

# First, get the orientation quaternion relative to
# the base "render" node--that is, relative to the "world".
# (By default, orientation is returned relative
#  to the given node's parent, which may not be
#  what we want here.)
orientationQuaternion = node_paths[-1].getQuat(render)

# (Don't worry if you don't know quaternions--that's not
#  really important for this process.)

# Now, get the component vectors from that quaternion
forwardVector = orientationQuaternion.getForward()
rightVector = orientationQuaternion.getRight()
# You probably don't need this next one, but I'll include
# it to illustrate that it's present
#upVector = orientationQuaternion.getUp()

# Next, apply the x-force and y-force to the relevant
#  vectors; again, adjust which vector is used for
#  which force if the below doesn't match your intentions
xVector = rightVector * xforce
yVector = forwardVector * yforce

# And finally, construct the force to be applied to the node
force = xVector + yVector

I have tried the forward/right method, but the capsules become weird twisty, for example:

image

And it need much time (>5 minutes in 11th Core i7 2.8GHz) to achieve static balance.

How to make it not twisty? And how to make it balance faster?

Those Iā€™m not sure aboutā€“Iā€™ll leave those questions for others better-versed in Bulletā€™s physics!

Thanks for your kindly reply.

This really make me confuzed~~~~

1 Like

Hi there, while I do not have a full solution, I did do a quick rewrite which may get you a bit closer to understanding the dynamics here.

Quick summary:

  1. I used a BulletHingeConstraint instead of a BulletConeTwistConstraint
  2. I anchored the starting segment to a fixed world position
  3. I applied the force using a relative vector with a simplified force:
force = LVector3(50, 0, 0)
force = render.get_relative_vector(node_paths[-1], force)
  1. I removed the ground collision mesh

You had a series of capsules linked by BulletConeTwistConstraint instead of a hinge constraint which have a complex force applied to them without a relative force application. This encourages a highly chaotic simulation, which logically does twist around as it self-interacts around sphere-like hinges. Additionally once the assemblage hits the ground, this imparts yet another force, which makes the dynamics even more chaotic looking.

My version of the code:

import time

import direct.directbase.DirectStart
from panda3d.core import Vec3, TransformState, Point3, BitMask32, LPoint3, LVector3, Material
from panda3d.bullet import BulletWorld, BulletDebugNode, BulletCylinderShape, BulletCapsuleShape, BulletBoxShape
from panda3d.bullet import BulletPlaneShape, BulletSphericalConstraint, BulletConeTwistConstraint, BulletHingeConstraint
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, -60, 3)
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')
inputState.watchWithModifiers('y_add', 'w')
inputState.watchWithModifiers('y_minu', 's')
# inputState.watchWithModifiers('turnLeft', 'q')
# inputState.watchWithModifiers('turnRight', 'e')
inputState.watchWithModifiers('up', 'u')

num_segment = 10
segment_length = 1
segment_radius = 1


# shape = BulletCapsuleShape(radius=segment_radius, height=segment_length)
shape = BulletBoxShape(1)
previous_node = None
node_paths = []

# pos = [75.81992094545193, 314.34202388786053, 9.5]
for i in range(num_segment):
    node = BulletRigidBodyNode(f'segment_{i}')
    node.addShape(shape)
    node.setMass(1.0)
    # node.set_linear_damping(0.1)
    # node.set_angular_damping(0.1)

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

        # constraint = BulletSphericalConstraint(node, Point3(0, 0, -segment_length / 2 - segment_radius))
        # print(base.render.find(f'segment_{i}'))
        constraint = BulletHingeConstraint(node, Point3(0, 0, -segment_length / 2 - segment_radius), Vec3(0,0,1))
        world.attach_constraint(constraint)


    else:
        # # 将前äø€äøŖ节ē‚¹čæžęŽ„åˆ°å½“å‰čŠ‚ē‚¹
        constraint = BulletHingeConstraint(previous_node, node,TransformState.make_pos(Vec3(0, 0, segment_length / 2 + segment_radius)),TransformState.make_pos(Vec3(0, 0, -segment_length / 2 - segment_radius)))
        world.attach_constraint(constraint)

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

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

uselessConstrain = []

print(base.render.find_all_matches('**/segment_*'))

global xforce, yforce
xforce = 0
yforce = 0
def process_input():
    global xforce, yforce

    if inputState.isSet('y_add'):
        yforce += 1
        time.sleep(0.2)
    if inputState.isSet('y_minu'):
        yforce -= 1
        time.sleep(0.2)
    if inputState.isSet('x_minu'):
        xforce += 1
        time.sleep(0.2)
    if inputState.isSet('x_add'):
        xforce -= 1
        time.sleep(0.2)

    # force = LVector3(xforce, yforce, 0)
    force = LVector3(50, 0, 0)
    force = render.get_relative_vector(node_paths[-1], force)

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


# Update
def update(task):
    dt = globalClock.getDt()
    process_input()
    world.doPhysics(dt)
    return task.cont


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

1 Like

@Simulan Thanks for your solution.

When I set force = LVector3(50, 50, 0), I expect the tube would be static in the first quadrant, like the black line in the following figure:

However, the fact is that the tube can not be static for 5 minutes (I stop it after 5 minutes), and it turn around in the 2/3/4 quadrant.

The force force = LVector3(50, 50, 0) should make the tube bend in the first quadrant, but I donā€™t know why it would go to 2/3/4 quadrant.

How to make it balance between the applied force and the resistance of constraint?

Your problem is that you expect things that shouldnā€™t be. In reality, physics does not behave like this. In order to bend something into an arc in reality, for example, a ruler, you need to take at least two hands and make an effort in a mirror relative to two points.

For a ruler, if I fix one endpoint and apply a force to the other endpoint, it should bend into an arc. In this simulation, there are actually two hands. One hand is used to fix one endpoint, and second hand is used to apply force. Thus, it should bend into an arc.

Currently, the capsules has been bend into an arc, but it is hard to ahieve the balance between applied force and the resistance of constraint. Actually, when the force is LVector3(50, 0, 0), it can achieve balance in a short time. But if the force is LVector3(50, 50, 0), it can not achieve force balance. Thatā€™s make me confused.

I think you didnā€™t take into account the rules of working with vectors.

from direct.showbase.ShowBase import ShowBase
from panda3d.core import LVector3

class MyApp(ShowBase):

    def __init__(self):
        ShowBase.__init__(self)

        # Your representation is not correct.
        print(LVector3(50, 0, 0).length())
        print(LVector3(50, 50, 0).length())

        # If you want to set the direction, then first you need to normalize the vector and then multiply it by the force you need. 
        # Find the difference.
        vec1 = LVector3(1, 0, 0)
        vec1.normalize()

        vec2 = LVector3(1, 1, 0)
        vec2.normalize()

        print((vec1*50).length())
        print((vec2*50).length())

app = MyApp()
app.run()

You can take a real ruler and find that the shape is closer to a parabola than to an arc.

I donā€™t need a ā€œstrictā€ arc, and parabola is acceptable.

But, the current problem is that it is hard to achieve the force balance between the applied force with the constraint resistance.

I donā€™t need a ā€œstrictā€ arc, and parabola is acceptable.

The vec2 multiply a constant value would result in LVector3(50, 50, 0). Actually, (30, 30, 0) would also cause the tube not balance.

The current problem is that it is hard to achieve the force balance between the applied force with the constraint resistance.

You could perhaps try looking at setting theā€¦ I think that itā€™s the ā€œangular dampingā€ value. Thatā€“if I recall correctlyā€“may reduce the degree to which the simulation swings around.

The damping value would reduce the degree of swing around, but the swing around still happen.

I wonder why the swing around always hanppen? Why the balance between applied force and constrain resistance can not be achieved?

It may simply be that there are a lot of forces at play in the simulation, enough that it becomes (seemingly) chaotic.

After all, the force that you apply is, I daresay, not the only one in effect: there are presumably forces acting between the capsules (applied by the constraints) that keep them connected and within their limits. Those may well be flowing back and forth, affecting the resultant movement.

[edit]
Actually, trying out Simulanā€™s version of the code, I realise that thereā€™s also the element of ā€œspringinessā€ (ā€œrestitution forceā€, I suppose it would be called?): the constraints attempt to bring the simulation back to its ā€œrestā€ state, presumably adding forces to the simulation in order to do so. This, too, complicates the simulation, I daresay.

[edit 2]
You can see this in action, I believe, if you modify Simulanā€™s code such that the force is applied only once. That is, just move the call to ā€œprocess_input()ā€ out of the ā€œupdateā€ method and to the line just above ā€œtaskMgr.add(update, 'update')ā€ Like so:

# Update
def update(task):
    dt = globalClock.getDt()
    world.doPhysics(dt)
    return task.cont

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

If you do this, you should see the objects bend as the force takes holdā€“and then sway back and forth as the constraints attempt to bring them back into line.

[edit 3]
Although you may have to increase the intensity of the force in order to see this properlyā€“after all, itā€™s being applied just once, rather than continuously.

Yes, thatā€™s why I add the continuously force, because I hope the continuously force would keep balance with the constraint resistance.

And I also agree that the applied force, the constraint resistance may make the simulation chaotic.

However, the force (50, 0, 0) would give a good result, but force (50, 50, 0) give a chaotic result. It is really strange.

So headache.

1 Like

I donā€™t see any headache. Itā€™s not entirely clear what you mean by the balance. You have several ways to elegantly interfere with the behavior of physical objects. For example, you can calm the rocking in this way. However, you cannot influence the physical simulation itself. For example, the rattling can be corrected visually already on the stick model, simply discarding the details of the fine movement for the bones. Unless, of course, you want to visualize it that way.

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(False)
debugNode.showBoundingBoxes(False)
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('y_add', 'w')
inputState.watchWithModifiers('y_minu', 's')

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))
    #node.set_angular_factor(False)

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

        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(dt):

    force = LVector3(0, 0, 0)

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

    force.normalize()

    force*=50

    if force != LVector3(0, 0, 0):
        for i in node_paths:
            i.node().setActive(True)
            i.node().setAngularDamping(0)
            if i.node().hasTag("root"):
                i.node().setAngularFactor(False)

        i.node().applyCentralForce(force)

    else:
        for i in node_paths:
            i.node().setAngularDamping(1)

def update(task):
    dt = globalClock.getDt()
    process_input(dt)
    world.doPhysics(dt)

    return task.cont

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