Panda Bullet

Thank you so much for your fast help! I did’t find time to try it until now, but I am quite optimistic it will solve my problem :slight_smile:

Hi all,

Does anyone know what the best way is to connect a hitbox to coppertop’s kcc?

The code uses a capsuleshape, but I’d like to overlay that with a lowpoly hitbox, so the capsule is inside the hitbox.

The hitbox should collide with the world, but not collide with the kcc. The kcc should contrain itself to the hitbox.

I’ve tried parenting the kcc to my hitbox and setting collide masks so they don’t collide with each other but for some reason the kcc would always collide with my hitbox.

Anybody done something similar?

I am having trouble using BulletRigidBodyNode.addShape(shape, transform) in the Python API. It appears that when you explicitly specify a transform using a TransformState object, it does not interact with the node’s existing scaling properly. The main negative effect of this problem is that when a BulletRigidBodyNode has non-(1, 1, 1) scaling I cannot easily add child shapes with non-identity positions. Here is a minimal demo of the problem, adapted from ennox’s samples (my system: Ubuntu 12.10 64-bit, latest official NVIDIA drivers, Panda3d devel branch from “http://archive.panda3d.org/ubuntu quantal-dev main” upgraded today):

import sys
import direct.directbase.DirectStart
from direct.showbase.DirectObject import DirectObject
from direct.showbase.InputStateGlobal import inputState
from panda3d.core import AmbientLight
from panda3d.core import DirectionalLight
from panda3d.core import Mat4
from panda3d.core import Quat
from panda3d.core import Vec3
from panda3d.core import Vec4
from panda3d.core import Point3
from panda3d.core import TransformState
from panda3d.core import BitMask32

from panda3d.bullet import BulletWorld
from panda3d.bullet import BulletBoxShape
from panda3d.bullet import BulletPlaneShape
from panda3d.bullet import BulletRigidBodyNode
from panda3d.bullet import BulletDebugNode
from pdb import set_trace as BP

class MinimalDemo(DirectObject):
  def __init__(self):
    base.setBackgroundColor(0.1, 0.1, 0.8, 1)
    base.cam.setPos(0, -40, 0)
    base.cam.lookAt(0, 0, 0)
    # Light
    alight = AmbientLight('ambientLight')
    alight.setColor(Vec4(0.5, 0.5, 0.5, 1))
    alightNP = render.attachNewNode(alight)
    dlight = DirectionalLight('directionalLight')
    dlight.setDirection(Vec3(1, 1, -1))
    dlight.setColor(Vec4(0.7, 0.7, 0.7, 1))
    dlightNP = render.attachNewNode(dlight)
    render.clearLight()
    render.setLight(alightNP)
    render.setLight(dlightNP)

    self.scale_type = "Scaled"
    self.ground_type = "Box"
        
    # Input
    self.accept('escape', self.doExit)
    self.accept('r', self.doReset)
    self.accept('s', self.toggleScale)
    self.accept('g', self.toggleGround)

    # Task
    taskMgr.add(self.update, 'updateWorld')

    # Physics
    self.setup()


  # _____HANDLER_____

  def doExit(self):
    self.cleanup()
    sys.exit(1)

  def doReset(self):
    self.cleanup()
    self.setup()

  def toggleGround(self):
    if self.ground_type == "Plane":
      self.ground_type = "Box"
    else:
      self.ground_type = "Plane"
    self.cleanup()
    self.setup()

  def toggleScale(self):
    if self.scale_type == "Scaled":
      self.scale_type = "Unscaled"
    else:
      self.scale_type = "Scaled"
    self.cleanup()
    self.setup()

  # ____TASK___

  def update(self, task):
    dt = globalClock.getDt()
    self.world.doPhysics(dt, 5, 1.0/180.0)
    return task.cont

  def cleanup(self):
    for obj in self.objs:
      self.world.remove(obj.node())
    self.world = None

    self.debugNP = None
    self.objs = []

    self.worldNP.removeNode()

  def setup(self):

    def create_box(name, param=(0.5, 0.5, 0.5)):
      shape = BulletBoxShape(Vec3(param))
      box = self.worldNP.attachNewNode(BulletRigidBodyNode(name))
      box.setCollideMask(BitMask32.allOn())
      box.node().setFriction(0.3)
      box.node().setRestitution(0.1)
      box.node().setDeactivationEnabled(False)
      return box, shape

    self.worldNP = render.attachNewNode('World')

    # World
    self.debugNP = self.worldNP.attachNewNode(BulletDebugNode('Debug'))
    self.debugNP.show()
    self.debugNP.node().showWireframe(True)
    self.debugNP.node().showConstraints(True)
    self.debugNP.node().showBoundingBoxes(False)
    self.debugNP.node().showNormals(True)
    #self.debugNP.showTightBounds()
    #self.debugNP.showBounds()

    self.world = BulletWorld()
    self.world.setGravity(Vec3(0, 0, -9.81))
    self.world.setDebugNode(self.debugNP.node())

    ground = self.worldNP.attachNewNode(BulletRigidBodyNode(self.ground_type))
    if self.ground_type == "Plane":
      # Ground plane
      shape = BulletPlaneShape(Vec3(0, 0, 1), 0)
      ground.setPos(0, 0, 0)
    else:
      # Ground box
      shape = BulletBoxShape(Vec3(15, 15, 15))
      ground.setPos(0, 0, -15)
    ground.node().addShape(shape)
    ground.setCollideMask(BitMask32.allOn())
    ground.node().setFriction(0.3)
    ground.node().setRestitution(0.1)
    self.world.attach(ground.node())

    if self.scale_type == "Scaled":
      self.scale = Vec3(4, 4, 4)
    else:
      self.scale = Vec3(1, 1, 1)
    q = Quat()
    q.setFromAxisAngle(30., Vec3(1, 0, 0))

    # Box 1
    obj, shape = create_box("Obj")
    obj.setPos(0, 0, 6)
    obj.setScale(self.scale)
    obj.setQuat(q)
    mat = obj.getMat()
    obj.node().addShape(shape, TransformState.makePos(Point3(-3, 0, 0)))
    obj.node().addShape(shape, TransformState.makePos(Point3(3, 0, 0)))
    obj.setMat(mat)  # Must apply scale after adding shape.
    obj.node().setMass(1.0)
    print "Obj: No transform is explicitly specified and it works correctly."
    print obj.node().getShapeMat(0)
    print obj.node().getShapeMat(1)    

    # Store the objects.
    self.objs = [ground, obj]

    # Attach the objs to the BulletWorld.
    for obj in self.objs:
      self.world.attach(obj.node())

    print "\n\nUsing scale_type: %s\n" % self.scale_type
    print "\n\nUsing ground_type: %s\n" % self.ground_type

    # render.ls()

demo = MinimalDemo()
run()

Keys:
“r” resets the scene
“s” toggles between scaled and unscaled boxes. The scale factor is (4, 4, 4)
“g” toggles between a ground plane and a ground box

When you run the demo you’ll see that the boxes, which have one BulletRigidBodyNode and were added as child shapes of a compund shape (line 154-155), bounce strangely. The rigid body node’s scale is set to (4, 4, 4). If you press “s”, the scales are set to (1, 1, 1) and it bounces properly. If you look closely you’ll see that when the ground object is a BulletBoxShape (use “g” to toggle between grounds), the boxes penetrate the ground – see the next demo for details about this.

I am not confident that I understand or am properly using the addShape method, so here is some more example code that demonstrates the issue:

import sys
import direct.directbase.DirectStart
from direct.showbase.DirectObject import DirectObject
from direct.showbase.InputStateGlobal import inputState
from panda3d.core import AmbientLight
from panda3d.core import DirectionalLight
from panda3d.core import Mat4
from panda3d.core import Quat
from panda3d.core import Vec3
from panda3d.core import Vec4
from panda3d.core import Point3
from panda3d.core import TransformState
from panda3d.core import BitMask32

from panda3d.bullet import BulletWorld
from panda3d.bullet import BulletBoxShape
from panda3d.bullet import BulletPlaneShape
from panda3d.bullet import BulletRigidBodyNode
from panda3d.bullet import BulletDebugNode
from pdb import set_trace as BP


class Demo(DirectObject):

  def __init__(self):
    base.setBackgroundColor(0.1, 0.1, 0.8, 1)
    base.setFrameRateMeter(True)

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

    # Light
    alight = AmbientLight('ambientLight')
    alight.setColor(Vec4(0.5, 0.5, 0.5, 1))
    alightNP = render.attachNewNode(alight)

    dlight = DirectionalLight('directionalLight')
    dlight.setDirection(Vec3(1, 1, -1))
    dlight.setColor(Vec4(0.7, 0.7, 0.7, 1))
    dlightNP = render.attachNewNode(dlight)

    render.clearLight()
    render.setLight(alightNP)
    render.setLight(dlightNP)

    self.scale_type = "Scaled"
    self.ground_type = "Box"

    # Input
    self.accept('escape', self.doExit)
    self.accept('r', self.doReset)
    self.accept('s', self.toggleScale)
    self.accept('g', self.toggleGround)

    # Task
    taskMgr.add(self.update, 'updateWorld')

    # Physics
    self.setup()


  # _____HANDLER_____

  def doExit(self):
    self.cleanup()
    sys.exit(1)

  def doReset(self):
    self.cleanup()
    self.setup()

  def toggleGround(self):
    if self.ground_type == "Plane":
      self.ground_type = "Box"
    else:
      self.ground_type = "Plane"
    self.cleanup()
    self.setup()

  def toggleScale(self):
    if self.scale_type == "Scaled":
      self.scale_type = "Unscaled"
    else:
      self.scale_type = "Scaled"
    self.cleanup()
    self.setup()

  # ____TASK___

  def update(self, task):
    dt = globalClock.getDt()
    self.world.doPhysics(dt, 5, 1.0/180.0)
    return task.cont

  def cleanup(self):
    for obj in self.objs:
      self.world.remove(obj.node())
    self.world = None

    self.debugNP = None
    self.objs = []

    self.worldNP.removeNode()

  def setup(self):

    def create_box(name, param=(0.5, 0.5, 0.5)):
      shape = BulletBoxShape(Vec3(param))
      box = self.worldNP.attachNewNode(BulletRigidBodyNode(name))
      box.setCollideMask(BitMask32.allOn())
      box.node().setFriction(0.3)
      box.node().setRestitution(0.1)
      box.node().setDeactivationEnabled(False)
      return box, shape

    self.worldNP = render.attachNewNode('World')

    # World
    self.debugNP = self.worldNP.attachNewNode(BulletDebugNode('Debug'))
    self.debugNP.show()
    self.debugNP.node().showWireframe(True)
    self.debugNP.node().showConstraints(True)
    self.debugNP.node().showBoundingBoxes(False)
    self.debugNP.node().showNormals(True)
    #self.debugNP.showTightBounds()
    #self.debugNP.showBounds()

    self.world = BulletWorld()
    self.world.setGravity(Vec3(0, 0, -9.81))
    self.world.setDebugNode(self.debugNP.node())

    ground = self.worldNP.attachNewNode(BulletRigidBodyNode(self.ground_type))
    if self.ground_type == "Plane":
      # Ground plane
      shape = BulletPlaneShape(Vec3(0, 0, 1), 0)
      ground.setPos(0, 0, 0)
    else:
      # Ground box
      shape = BulletBoxShape(Vec3(15, 15, 15))
      ground.setPos(0, 0, -15)
    ground.node().addShape(shape)
    ground.setCollideMask(BitMask32.allOn())
    ground.node().setFriction(0.3)
    ground.node().setRestitution(0.1)
    self.world.attach(ground.node())

    if self.scale_type == "Scaled":
      self.scale = Vec3(4, 4, 4)
    else:
      self.scale = Vec3(1, 1, 1)
    q = Quat()
    q.setFromAxisAngle(30., Vec3(1, 0, 0))

    # Box 1
    box1, shape = create_box("Box1")
    box1.setPos(-10, 0, 6)
    box1.setScale(self.scale)
    box1.setQuat(q)
    mat = box1.getMat()
    box1.node().addShape(shape)
    box1.setMat(mat)  # Must apply scale after adding shape.
    box1.node().setMass(1.0)
    print "Box1: No transform is explicitly specified and it works correctly."
    print box1.node().getShapeMat(0)
    print "Inertia:", box1.node().getInertia()

    # Box 2
    box2, shape = create_box("Box2")
    box2.setPos(-5, 0, 6)
    box2.setScale(self.scale)
    box2.setQuat(q)
    mat = box2.getMat()
    box2.node().addShape(shape, TransformState.makeIdentity())
    box2.setMat(mat)  # Must apply scale after adding shape.
    box2.node().setMass(1.0)
    print "Box2: Any transform, even identity, causes problems with the inertia tensor and collision geometry."
    print box2.node().getShapeMat(0)
    print "Inertia:", box2.node().getInertia()

    # Box 3
    box3, shape = create_box("Box3")
    box3.setPos(0, 0, 6)
    box3.setScale(self.scale)
    box3.setQuat(q)
    mat = box3.getMat()
    box3.node().addShape(shape, TransformState.makeIdentity())
    box3.setMat(mat)  # Must apply scale after adding shape.
    box3.node().setMass(1.0)
    box3.node().setInertia(box1.node().getInertia())  # Set inertia to box1's.
    print "Box3: Setting the inertia to box1's does fix the collision geometry issue, evident when using the ground box."
    print box3.node().getShapeMat(0)
    print "Inertia:", box3.node().getInertia()

    # Box 4
    box4, shape = create_box("Box4")
    box4.setPos(5, 0, 6)
    box4.setScale(1, 1, 1)
    box4.setQuat(q)
    mat = box4.getMat()
    box4.node().addShape(shape, TransformState.makePosQuatScale(Point3.zero(), q, self.scale))
    box4.setMat(mat)  # Must apply scale after adding shape.
    box4.node().setMass(1.0)
    print "Box4: One thought would be to try to apply the scale in the shape transform, but it doesn't work."
    print box4.node().getShapeMat(0)
    print "Inertia:", box4.node().getInertia()

    # Box 5
    box5, shape = create_box("Box5", param=self.scale / 2.)
    box5.setPos(10, 0, 6)
    box5.setScale(1, 1, 1)
    box5.setQuat(q)
    mat = box5.getMat()
    box5.node().addShape(shape, TransformState.makeIdentity())
    box5.setMat(mat)  # Must apply scale after adding shape.
    box5.node().setMass(1.0)
    print "Box5: If we apply the scale to the BulletShape's parameters, specifying a transform appears to work properly."
    print box5.node().getShapeMat(0)
    print "Inertia:", box5.node().getInertia()

    # Store the objects.
    self.objs = [ground, box1, box2, box3, box4, box5]

    # Attach the objs to the BulletWorld.
    for obj in self.objs:
      self.world.attach(obj.node())

    print "\n\nUsing scale_type: %s\n" % self.scale_type
    print "\n\nUsing ground_type: %s\n" % self.ground_type

    # render.ls()

demo = Demo()
run()

Same keys as above (“r”, “s”, “g”).

I refer to the 5 boxes as Box1…5 from left to right.

Box1 shows a falling box with no explicit transform specified in addShape().

Box2 shows the same but with an identity transform specified. The collision is strange. When the ground is set to be a box (“g” toggles this), Box2’s geometry penetrates it before the collision is detected – as if it’s using Box2’s unscaled geometry for detection, but then computing collision resolution forces using the distance from the scaled geometry. This doesn’t seem to happen when the ground is a plane.

Box3 is the same as Box2, but with Box1’s inertia set. Here the collision with the ground plane appears to be OK, but not with the ground box.

Box4 is an attempt to apply the scaling within the transformation itself. This fails regardless of whether you use scale=(1, 1, 1) scale on the box4 node, or scale=(4, 4, 4).

Box5 applies the scale to the shape’s parameters and this appears to work. But this is an unpreferred solution because it requires explicit shape-specific handling of the scaling, and also forces you to not scale the body node when trying to specify shape transforms.

Could someone tell me if I am making a mistake, or if this is a real bug that is already known (I haven’t found any discussion of it so far)? If it is new and needs to be addressed I can work on it in a month or two when I have more free time.

***** EDIT *****
Ok I’ve learned a little bit more about what is happening. The problem is related to transforms (e.g., setMat/setPos/setQuat/setScale/etc) that are applied to the BulletRigidBodyNode after adding the shapes. Note that I call this after addShape():

    box2.setMat(mat)  # Must apply scale after adding shape.

When you don’t call that after adding the shape, the shape (that were added using the ‘addShape(shape, transform)’ pattern) doesn’t inherit the BulletRigidBodyNode’s transform as it would when you use the addShape(shape) pattern. When you do apply a transform after adding the shapes via ‘addShape(shape, transform)’, the inertia and collision geometry problem happens.

There is also a small issue when you apply a transform after adding the shapes via ‘addShape(shape)’. Here’s a demo:

from panda3d.core import TransformState
from panda3d.core import Vec3
from panda3d.bullet import BulletBoxShape
from panda3d.bullet import BulletRigidBodyNode
from pandac.PandaModules import NodePath

def create_box(name):
  shape = BulletBoxShape(Vec3((0.5, 0.5, 0.5)))
  box = NodePath(BulletRigidBodyNode(name))
  return box, shape

scale = Vec3(4, 4, 4)

# Box 1
box1, shape = create_box("Box1")
box1.setPos(-5, 0, 6)
box1.setScale(scale)
box1.node().setMass(1.0)
box1.node().addShape(shape)
print "Box1: addShape(shape)"
print "Box1: pre-transform inertia:", box1.node().getInertia()
box1.setMat(box1.getMat())
print "Box1: post-transform inertia:", box1.node().getInertia()   
print

# Box 2
box2, shape = create_box("Box2")
box2.setPos(5, 0, 6)
box2.setScale(scale)
box2.node().setMass(1.0)
box2.node().addShape(shape, TransformState.makeIdentity())
print "Box2: addShape(shape, transform)"   
print "Box2: pre-transform inertia:", box2.node().getInertia()
box2.setMat(box2.getMat())
print "Box2: post-transform inertia:", box2.node().getInertia()

The inertia is supposed to change when the object is transformed so that’s OK, but it isn’t clear to me why the inertia that is calculated after addShape(shape) doesn’t include the BulletRigidBodyNode’s existing transformation. And if you look at the inertia values for Box1 and Box2 that are printed, you’ll see that Box2’s sticks with Box1’s initial (and incorrect one).

Something similar happens with static bodies. Their inertia is calculated from the mass and geometry, so since their mass is 0, it must be the geometry’s scaling that is getting messed up. For a demo, add that second line of code in after the line that adds the ground’s shape in my 2nd example from above:

    ground.node().addShape(shape)
    ground.setMat(ground.getMat())

See how the blocks bounce off the ground with even greater energy than before you added that line? So it seems that the geometry transforms are interacting badly with the BulletRigidBodyNode transforms, and the changing inertia values are a consequence of that.
***** END EDIT *****

Thanks very much, I really appreciate what you Panda3d devs are doing for us.

Peter

So I finally managed to test this (RL interfered and then took me awhile to figure out problems on my end).
Actually works pretty well so farr apart from one issue i describe below, so thanks Ennox for exposing this.

In case someone else is as clueless as I am and finds this post in their own search for documentation, basically you need to set up your generic constraint and for each axis motor you want to use you need to get a motor object to use (unlike say the hinge constraint motor where you set parameters against the hinge object itself). for example:

rotMotor = generic.getRotationalLimitMotor(index) #index 0,1,2 depending if you want x,y or z rotation
rotMotor.setMotorEnabled(True)
rotMotor.setTargetVelocity(5.0)

against the rotMotor object you can pretty much set any parameter you would use on the hinge motor.

One issue to be aware of is motor on axis 1 (y) seems buggy in my simple test. Whereas constant velocity on motors 0 and 2 leads to rotation around the respective axis as expected, motor 1 seems to reverse every 180 degrees with constant velocity. If you have limits set on another axis this can cause the constraint to go crazy. It seems to occur at the point when getCurrentPosition() on Axis 2 reverses sign due to 360^ wrap around, so i suspect the “positive” direction around the Y axis reversed itself in render space. This may be a Bullet Issue and I haven’t checked their forums yet to see if that is common

Anyhow, that is as far as i managed to play with it. If I manage to discover anything else useful I will post.

cheers

Had a chance to check out the Bullet forums and the funky behavior i mentioned in the above post is indeed a Bullet issue. In particular Axis 1 (y) really needs to be limited to +/- Pi/2 if you don’t want to get messy motor behavior.

Sorry for the long delay. I finally found the problem - it has been a silly bug in my code. Whenn applying a new scale I iterated over all child shapes and applied the scale directly to the shapes. When using multi-shape bodies (i. e. the rigid body has a btCoumpoundShape) I should set the scale only on this btCompoundShape. I just checked in a fix. The next developer shapshot should have picked it up.

Please not that you have do I tiny change in your code too. You can not reuse the same shape within a rigid body. This is because btCompundShape forwards the scaling to the child shapes, and the child shapes handle the scaling themselves. So if you have only one instance this gets “rescaled” multiple times, causing the weired bounce effects.

    # Box 1
    d = 0.5
    shape1 = BulletBoxShape(Vec3(d,d,d))
    shape2 = BulletBoxShape(Vec3(d,d,d))

    obj = create_box("Obj")
    obj.setPosQuat(Point3(0, 0, 6), q)
    obj.setScale(3)
    obj.node().addShape(shape1, TransformState.makePos(Point3(-1, 0, 0)))
    obj.node().addShape(shape2, TransformState.makePos(Point3(1, 0, 0)))
    obj.node().setMass(1.0)

Sidenote: local transforms are affected by scale too, in the same way as the Panda3D scene graph. So with a scale of 3 and a local transform of (-1,0,0) you end up with a shape located at (-3,0,0) in global coordinates.

ennox: Thank you very much, I appreciate your help. I’ll check out the fix. Thanks for the shape tip too.

Peter

It looks like there’s another issue now. The body’s inertia is not updated until you call BulletRigidBodyNode.setMass(). In the demo below, see how the inertia only changes once setMass() is called after the addShape() call?

I’m using: buildbot.panda3d.org/downloads/d … _amd64.deb

from panda3d.core import TransformState
from panda3d.core import Vec3
from panda3d.bullet import BulletBoxShape
from panda3d.bullet import BulletRigidBodyNode
from pandac.PandaModules import NodePath

def create_box(name):
  shape = BulletBoxShape(Vec3((0.5, 0.5, 0.5)))
  box = NodePath(BulletRigidBodyNode(name))
  return box, shape

scale = Vec3(1, 4, 4)

# Box 1
box1, shape = create_box("Box1")
box1.setPos(-5, 0, 6)
box1.setScale(scale)
box1.node().setMass(10.0)
box1.node().addShape(shape)
print "Box1: addShape(shape)"
print "Box1: pre-setMass inertia:", box1.node().getInertia()
box1.node().setMass(10.0)
print "Box1: post-setMass inertia:", box1.node().getInertia()   
print

# Box 2
box2, shape = create_box("Box2")
box2.setPos(5, 0, 6)
box2.setScale(scale)
box2.node().setMass(10.0)
box2.node().addShape(shape, TransformState.makeIdentity())
print "Box2: addShape(shape, transform)"   
print "Box2: pre-setMass inertia:", box2.node().getInertia()
box2.node().setMass(10.0)
print "Box2: post-setMass inertia:", box2.node().getInertia()

I’m not sure what’s going on because in the C++ source, bulletBodyNode’s add_shape() calls shape_changed(), and bulletRigidBodyNode’s implementation of shape_changed() calls set_mass(). Seems like it should work.

Any ideas?

Sure. Restoring the scaling before calling shape_changed in BulletBodyNode::add_shape.

This is an ugly hack for some behaviour I don’t understand in Bullet. Setting local scale for a shape doesn’t seem to be idempotent - the size of the shapes changes as expected, but no their local translations. to work around this I first reset local scaling to 1.0, then add the shape, and then re-apply the previous scale.

Now if I force an update of mass/inertia (shape_changed, which just does “set_mass(get_mass())”) before restoring the local scaling the the inertia are calculated for an unscaled objects. Exactly what you observer. I check in another fix right now.

By the way: The best approach is not to use any scaling at all when working with physics. Scale the model right in your modelling application.

Quick (hopefully) question - is ‘BulletWorld.setContactAddedCallback’ in Panda 1.8.1?

I’m trying to call it to get some manifold data on collision, but I get an attribute error:
AttributeError: ‘libpandabullet.BulletWorld’ object has no attribute ‘setContactAddedCallback’

I can get the ‘bullet-contact-added’ event to fire, but it looks like I can’t get collision data apart from the two nodes from it.

Hmm… seems that the Bullet version which is included in Panda3D 1.8.1 is more than half a year old. No idea why.

Thanks, just installed devel and its working now :slight_smile:

It works. Thanks very much.

Today I built panda but seems that BulletShape::set_local_scale() doesn’t scale the shape.

This is a sample code I use

	//Load world mesh
	NodePath worldMesh = window->load_model(window->get_render(),"world_mesh.egg");
	worldMesh.set_pos(0.0, 0.0, 0.0);
	worldMesh.set_scale(scale);
	//attach bullet body
	BulletTriangleMesh* triMesh = new BulletTriangleMesh();
	//add geoms from geomNodes to the mesh
	NodePathCollection geomNodes = worldMesh.find_all_matches("**/+GeomNode");
	for (int i = 0; i < geomNodes.get_num_paths(); ++i)
	{
		PT(GeomNode)geomNode = DCAST(GeomNode,
				geomNodes.get_path(i).node());
		CPT(TransformState) ts = geomNode->get_transform();
		GeomNode::Geoms geoms = geomNode->get_geoms();
		for (int j = 0; j < geoms.get_num_geoms(); ++j)
		{
			triMesh->add_geom(geoms.get_geom(j), true, ts.p());
		}
	}
	PT(BulletShape)collisionShape = new BulletTriangleMeshShape(triMesh, false);
	collisionShape->set_local_scale(worldMesh.get_scale()); //<--NOT SCALE
	PT(BulletRigidBodyNode)meshNode = new BulletRigidBodyNode("meshNode"));
	meshNode->add_shape(collisionShape);
	meshNode->set_mass(0.0);
	meshNode->set_kinematic(false);
	meshNode->set_static(true);
	meshNode->set_deactivation_enabled(true);
	meshNode->set_active(false);
	bulletWorld->attach(meshNode);
	//attach to scene
       window->get_render().attach_new_node(meshNode);

Right. The method will be removed soon. Use NodePath::set_scale or PandaNode::set_transform.

Here’s a bug that I have been struggling with for several days now. I have a rigid body node connected with a hinge constraint to another rigid body node, which is its parent in the scene graph. When I call set_pos to teleport the parent, though, the net transform of the child naturally changes, but this information is not specified to Bullet - the constraint does make sure that the child is moved eventually, but it gains a huge amount of angular momentum due to the move.

Right now, I have to force transform_changed to be triggered on the child by doing this:

parent.set_pos(x, y, z)
child.set_pos(random.random() * 0.0001, 0, 0)

Needless to say, this is quite an ugly hack, and I fear that it will break determinism in my simulation. If there is no easy fix for this problem, is there perhaps a method to manually mark a transform as dirty as a workaround?

I assume you have a scene graph like this

PandaNode
  BulletRigidBodyNode "A"
    BulletRigidBodyNode "B"

“A” and “B” are connected via a hinge constraint. Now you call set_pos on “A”. Since “B” is a child of “A” it is valid to expect that the transform of “B” changes too. But it does not.

In this case it is a problem I am aware of, but don’t have the right solution so far. the base problem is not related to constraints. Consider a more simple setup:

PandaNode X
  BulletRigidBodyNode Y

Now change the transform of X. Y won’t be moved. The problem is that the hook transform_changed is not called on the node Y, if the transform on X changes. In other words: the transform_changed notification does not propagate down the scene graph.

You would expect that Calling Y.set_transform(y.get_transform()) triggers the transform_changed hook on Y, but the implementation of PandaNode::set_transform is smart - it compares the old transform and the new transform, and only if they are different calls the transform_changed hook.

Here are a few thoughts on possible solutions.

(1)
Moving the child node twice

parent.set_pos(x, y, z)
child.set_pos(1000, 0, 0)
child.set_pos(0, 0, 0)

The transform_changed hook should be called twice. Ugly, but should work as long as the intermediate transform does not cause collisions with other objects near the intermediate position. Just make the intermediate transform far away/outside the scene.

(2)
If there was a PandaNode::net_transform_changed hook in addition to the transform_changed hook this would be perfect from my side. From PandaNode::set_transform we would simply call PandaNode::net_transform_changed for all children of a node (not just direct children - or propagate the invocation down the scene graph). It might be a huge performance drain if applied to the scene graph in general, so this probably should be controlled “per-node”, maybe via a flag on PandaNode.

(3)
We add a method to BulletNode which simply calls this->transform_changed, maybe “force_sync”. Most simple solution, but also a bit of a hack. Such things should be handled more-or-less transparent.

(4)
From BulletRigidbodyNode::transform_changed I could propagate the call to all Bullet nodes below the node where transform_changed is invoked. I could optimize this by setting a flag “has bullet children” on a bullet node, so I do not have to search for childred each time if there are no children. This would solve your problem, but not the more general case with “X” and “Y” (where X is not a bullet node).

Feedback is welcome, of course.

  1. This is indeed what I ended up doing after I found out that that works as well.

  2. I was thinking something like that, too. I like that solution, but the problem is that it adds overhead to scene graphs that don’t need this feature. We could add a flag, but it would have to propagate upward, and this would have to remain working when you restructure the scene graph. It could work, though, since we do it with get_net_draw_show_mask, but it does add a lot of complexity.

  3. It might still be nice to have this as a workaround (perhaps expose a set_dirty method?), although I agree that it’s not nice to have to do this in the first place.

  4. This wouldn’t really be a solution because (as you admit) the problem can occur when moving any parent node, not just a bullet one. Such a solution would perhaps work if you had something like a BulletSceneNode, of some sort, which stood at the root of the bullet scene (and which all transforms in the bullet world were relative to).

Ok. I have checked in solution #3 (BulletNode::set_transform_dirty).

I’m not very enthusiastic about major changes to the current Bullet 2.80/2.81 code, since Bullet 3.x is about to be released, and I expect it to have major differences from the Bullet 2.x API.

My plan is to move to Bullet 3.x ASAP, but my expectation is that the Panda3D Bullet module needs to be rewritten from scratch. This is - in my opinion - the right place for getting rid of design flaws in the current module.

Thanks for the link to our old discussion about this topic, by the way.

Great, thank you!