Panda3d Bullet Issues -- floating eggs -- bullet collision mesh problem?

I am quite interested in using bullet with Panda3d. I know bullet is not perfect and I wanted to see issues such as jitter etc… in action to assess it. I installed the standard Panda3d 1.10.4.1 SDK with python 3.7 and then got hold of bullet-samples:
https://www.panda3d.org/manual/?title=Bullet_Samples
I updated them for Python3 but sample 20_BowlAndEggs.py is ok right away. However, I noticed the collisions are off: I tweaked 3 lines: to get closer, to remove the bowl scaling and to add more eggs. This lets you see the eggs up close and the fact that they are floating.

  base.cam.setPos(0, -1, 0.2)
  #self.bowlNP.setScale(2)
  for i in range(20):

A related issue is that the eggs don’t really come close to each other – it is almost like their collision shapes are much bigger than the mesh shape. However, when you turn on collision wireframe mesh debug (cyan mesh) it is exactly the same size and shape as the drawn eggs. I did notice that the bounding boxes (AABB) are much bigger than the eggs but that presumably is just for a first pass? The bullet-sample code is very simple too – it just uses the egg input geometry to make a convex hull collision shape for bullet.

Another comment is that bullet seems very jittery on this example – the eggs never stop moving. This seems to mean they could never go to sleep and bullet eats CPU all the time which rules out large numbers of objects at once (Deactivation is off in the original example but it would never kick in anyway).

Full example code below (based on bullet-samples/20_BowlAndEggs.py with above 3 line edit). The bottom line is that I didn’t change anything that matters – there seems to be an issue with this bullet example or perhaps bullet in Panda generally? The set scale change is optional actually.

#from pandac.PandaModules import loadPrcFileData
#loadPrcFileData('', 'load-display tinydisplay')

import sys
import random
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 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 BulletRigidBodyNode
from panda3d.bullet import BulletDebugNode
from panda3d.bullet import BulletPlaneShape
from panda3d.bullet import BulletConvexHullShape
from panda3d.bullet import BulletTriangleMesh
from panda3d.bullet import BulletTriangleMeshShape
from panda3d.bullet import ZUp

class Game(DirectObject):

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

    base.cam.setPos(0, -1, .2)
    base.cam.lookAt(0, 0, 0.2)

    # 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)

    # Input
    self.accept('escape', self.doExit)
    self.accept('r', self.doReset)
    self.accept('f1', self.toggleWireframe)
    self.accept('f2', self.toggleTexture)
    self.accept('f3', self.toggleDebug)
    self.accept('f5', self.doScreenshot)

    inputState.watchWithModifiers('up', 'w')
    inputState.watchWithModifiers('left', 'a')
    inputState.watchWithModifiers('down', 's')
    inputState.watchWithModifiers('right', 'd')

    # 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 toggleWireframe(self):
    base.toggleWireframe()

  def toggleTexture(self):
    base.toggleTexture()

  def toggleDebug(self):
    if self.debugNP.isHidden():
      self.debugNP.show()
    else:
      self.debugNP.hide()

  def doScreenshot(self):
    base.screenshot('Bullet')

  # ____TASK___

  def processInput(self, dt):
    force = Vec3(0, 0, 0)

    if inputState.isSet('up'): force.setY( 1.0)
    if inputState.isSet('down'): force.setY(-1.0)
    if inputState.isSet('left'):    force.setX(-1.0)
    if inputState.isSet('right'):   force.setX( 1.0)

    force *= 300.0

    self.bowlNP.node().setActive(True)
    self.bowlNP.node().applyCentralForce(force)

  def update(self, task):
    dt = globalClock.getDt()

    self.processInput(dt)
    self.world.doPhysics(dt)

    return task.cont

  def cleanup(self):
    self.world = None
    self.worldNP.removeNode()

  def setup(self):
    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(False)

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

    # Ground
    shape = BulletPlaneShape(Vec3(0, 0, 1), 0)

    body = BulletRigidBodyNode('Ground')
    bodyNP = self.worldNP.attachNewNode(body)
    bodyNP.node().addShape(shape)
    bodyNP.setPos(0, 0, 0)
    bodyNP.setCollideMask(BitMask32.allOn())
    self.world.attachRigidBody(bodyNP.node())

    # Bowl
    visNP = loader.loadModel('models/bowl.egg')

    geom = visNP.findAllMatches('**/+GeomNode').getPath(0).node().getGeom(0)
    mesh = BulletTriangleMesh()
    mesh.addGeom(geom)
    shape = BulletTriangleMeshShape(mesh, dynamic=True)

    body = BulletRigidBodyNode('Bowl')
    bodyNP = self.worldNP.attachNewNode(body)
    bodyNP.node().addShape(shape)
    bodyNP.node().setMass(10.0)
    bodyNP.setPos(0, 0, 0)
    bodyNP.setCollideMask(BitMask32.allOn())
    self.world.attachRigidBody(bodyNP.node())

    visNP.reparentTo(bodyNP)

    self.bowlNP = bodyNP
    #self.bowlNP.setScale(2)

    # Eggs
    self.eggNPs = []
    for i in range(20):
      x = random.gauss(0, 0.1)
      y = random.gauss(0, 0.1)
      z = random.gauss(0, 0.1) + 1
      h = random.random() * 360
      p = random.random() * 360
      r = random.random() * 360

      visNP = loader.loadModel('models/egg.egg')

      geom = visNP.findAllMatches('**/+GeomNode').getPath(0).node().getGeom(0)
      shape = BulletConvexHullShape()
      shape.addGeom(geom)

      body = BulletRigidBodyNode('Egg-%i' % i)
      bodyNP = self.worldNP.attachNewNode(body)
      bodyNP.node().setMass(1.0)
      bodyNP.node().addShape(shape)
      bodyNP.node().setDeactivationEnabled(False)
      bodyNP.setCollideMask(BitMask32.allOn())
      bodyNP.setPosHpr(x, y, z, h, p, r)
      #bodyNP.setScale(1.5)
      self.world.attachRigidBody(bodyNP.node())

      visNP.reparentTo(bodyNP)

      self.eggNPs.append(bodyNP)

game = Game()
run()

I would do so. If I needed the behavior that you described.

import sys
import random
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 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 BulletRigidBodyNode
from panda3d.bullet import BulletDebugNode
from panda3d.bullet import BulletPlaneShape
from panda3d.bullet import BulletConvexHullShape
from panda3d.bullet import BulletTriangleMesh
from panda3d.bullet import BulletTriangleMeshShape
from panda3d.bullet import ZUp

class Game(DirectObject):

  def __init__(self):
    base.setBackgroundColor(0.1, 0.1, 0.8, 1)
    base.setFrameRateMeter(True)
    # 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)

    # Input
    self.accept('escape', self.doExit)
    self.accept('r', self.doReset)
    self.accept('f1', self.toggleWireframe)
    self.accept('f2', self.toggleTexture)
    self.accept('f3', self.toggleDebug)
    self.accept('f5', self.doScreenshot)

    inputState.watchWithModifiers('up', 'w')
    inputState.watchWithModifiers('left', 'a')
    inputState.watchWithModifiers('down', 's')
    inputState.watchWithModifiers('right', 'd')

    # 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 toggleWireframe(self):
    base.toggleWireframe()

  def toggleTexture(self):
    base.toggleTexture()

  def toggleDebug(self):
    if self.debugNP.isHidden():
      self.debugNP.show()
    else:
      self.debugNP.hide()

  def doScreenshot(self):
    base.screenshot('Bullet')

  # ____TASK___

  def processInput(self, dt):
    force = Vec3(0, 0, 0)

    if inputState.isSet('up'): force.setY( 1.0)
    if inputState.isSet('down'): force.setY(-1.0)
    if inputState.isSet('left'):    force.setX(-1.0)
    if inputState.isSet('right'):   force.setX( 1.0)

    force *= 300.0

    self.bowlNP.node().setActive(True)
    self.bowlNP.node().applyCentralForce(force)

  def update(self, task):
    dt = globalClock.getDt()

    self.processInput(dt)
    self.world.doPhysics(dt)

    return task.cont

  def cleanup(self):
    self.world = None
    self.worldNP.removeNode()

  def setup(self):
    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(False)

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


    # Bowl
    visNP = loader.loadModel('models/bowl.egg')
    visNP.setScale(2)
    visNP.flattenLight()
    
    geom = visNP.findAllMatches('**/+GeomNode').getPath(0).node().getGeom(0)
    mesh = BulletTriangleMesh()
    mesh.addGeom(geom)
    shape = BulletTriangleMeshShape(mesh, dynamic=False)

    body = BulletRigidBodyNode('Bowl')
    bodyNP = self.worldNP.attachNewNode(body)
    bodyNP.node().addShape(shape)
    bodyNP.setCollideMask(BitMask32.allOn())
    self.world.attachRigidBody(bodyNP.node())

    visNP.reparentTo(bodyNP)

    self.bowlNP = bodyNP

    # Eggs
    self.eggNPs = []
    
    for i in range(5):
    
      x = random.gauss(0, 0.1)
      y = random.gauss(0, 0.1)
      z = random.gauss(0, 0.1) + 0.4
      h = random.random() * 360
      p = random.random() * 360
      r = random.random() * 360

      visNP = loader.loadModel('models/egg.egg')
      visNP.setScale(1.2)
      visNP.flattenLight()
      
      geom = visNP.findAllMatches('**/+GeomNode').getPath(0).node().getGeom(0)
      mesh = BulletTriangleMesh()
      mesh.addGeom(geom)
      shape = BulletTriangleMeshShape(mesh, dynamic=True)

      body = BulletRigidBodyNode('Egg-%i' % i)
      bodyNP = self.worldNP.attachNewNode(body)
      bodyNP.node().setMass(1.0)
      bodyNP.node().addShape(shape)
      bodyNP.node().setDeactivationEnabled(True)
      bodyNP.setPosHpr(x, y, z, h, p, r)

      self.world.attachRigidBody(bodyNP.node())
      
      visNP.reparentTo(bodyNP)

      self.eggNPs.append(bodyNP)

game = Game()
run()

Simplified version.

import random
from direct.showbase.ShowBase import ShowBase
from panda3d.core import Vec3, BitMask32
from panda3d.bullet import BulletWorld, BulletRigidBodyNode, BulletDebugNode, BulletTriangleMesh, BulletTriangleMeshShape

class MyApp(ShowBase):
  def __init__(self):
    ShowBase.__init__(self)
    # Task
    taskMgr.add(self.update, 'updateWorld')

    worldNP = render.attachNewNode('World')

    # World
    debugNP = worldNP.attachNewNode(BulletDebugNode('Debug'))
    debugNP.show()
    debugNP.node().showWireframe(True)

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

    # Bowl
    visNP = loader.loadModel('models/bowl.egg')
    visNP.setScale(2)
    visNP.flattenLight()
    
    geom = visNP.findAllMatches('**/+GeomNode').getPath(0).node().getGeom(0)
    mesh = BulletTriangleMesh()
    mesh.addGeom(geom)
    shape = BulletTriangleMeshShape(mesh, dynamic=False)

    body = BulletRigidBodyNode('Bowl')
    bodyNP = worldNP.attachNewNode(body)
    bodyNP.node().addShape(shape)
    bodyNP.setCollideMask(BitMask32.allOn())
    self.world.attachRigidBody(bodyNP.node())

    visNP.reparentTo(bodyNP)

    # Eggs
    eggNPs = []
    
    for i in range(5):
    
      x = random.gauss(0, 0.1)
      y = random.gauss(0, 0.1)
      z = random.gauss(0, 0.1) + 0.4
      h = random.random() * 360
      p = random.random() * 360
      r = random.random() * 360

      visNP = loader.loadModel('models/egg.egg')
      visNP.setScale(1.2)
      visNP.flattenLight()
      
      geom = visNP.findAllMatches('**/+GeomNode').getPath(0).node().getGeom(0)
      mesh = BulletTriangleMesh()
      mesh.addGeom(geom)
      shape = BulletTriangleMeshShape(mesh, dynamic=True)

      body = BulletRigidBodyNode('Egg-%i' % i)
      bodyNP = worldNP.attachNewNode(body)
      bodyNP.node().setMass(1.0)
      bodyNP.node().addShape(shape)
      bodyNP.node().setDeactivationEnabled(True)
      bodyNP.setPosHpr(x, y, z, h, p, r)

      self.world.attachRigidBody(bodyNP.node())
      
      visNP.reparentTo(bodyNP)

      eggNPs.append(bodyNP)
      
  def update(self, task):
    self.world.doPhysics(globalClock.getDt())
    return task.cont

app = MyApp()
app.run()


Ok – thanks. However, you didn’t say what was broken about the original example 20 which is partly what I wanted to know.

I tried working step by step from the bullet-sample to yours:
It seems that the BulletConvexHullShape() has issues and did not work correctly,
and you replaced it with BulletTriangleMeshShape() which does work ok.

It also seems that when you apply scaling things can mess up but flattenLight() can help
avoid some scaling issues.

It would be good to make sure the bullet-samples all worked (and were updated to python3).
I am happy to try to do that if there is interest.

BulletConvexHullShape()

I have not seen this before. may need to be customized. I did not find out what it is. As for python, the bullet plugin is not related to python; This is programmed in C ++.

ADD:
You seem to mean the Python 3 API. I think this is easy to fix, so only the Python classes have changed, but not Panda 3D, because the difference is small and can be fixed on the fly.

For this reason, the objects did not go to sleep, this is set by this line.

bodyNP.node().setDeactivationEnabled(True)

Set your sleep mode after a certain calm threshold of angular or linear speed:

bodyNP.node().setAngularSleepThreshold(0.7)
bodyNP.node().setLinearSleepThreshold (0.7)

Default.

bodyNP.node().setAngularSleepThreshold(1.0)
bodyNP.node().setLinearSleepThreshold (0.8)

A detailed answer is in the manual.
https://www.panda3d.org/manual/?title=Bullet_Collision_Shapes&language=python

It is a little strange. Convex hulls are supposed to be more stable than triangle meshes. But, it’s possible that there is some sort of a bug here with synchronizing the transform between Panda and Bullet.

I’m no Bullet expert, but I happen to remember that one can set “bullet-additional-damping true” in Config.prc to reduce jitter, which can be controlled further using these variables (defaults indicated):

bullet-additional-damping-linear-factor 0.005
bullet-additional-damping-angular-factor 0.01
bullet-additional-damping-linear-threshold 0.01
bullet-additional-damping-angular-threshold 0.01

You can also set damping on a per-rigid-body basis.

There is already an effort to this end underway here: