Issues with Bullet Physics and Collisions

Hey Everyone,

EDIT: Does anyone know how to detect the total weight of an object that has several other objects stacked on top of it? My scene has a bunch of marbles that fall into a container and I believe the uneven force of the top marbles pushing down on the lower marbles are causing the lower marbles to spin forever in-place even though they should be holding still. I’m thinking of implementing something that raises and lowers the angular damping depending on how much weight is being applied downward on the marbles so that the lower marbles stop spinning. As a band-aid fix I’ve currently got it iterating through all previous marbles each time a new one is dropped into the container and it sets their current angular damping to itself plus 0.01. That does cause the lowest marbles to eventually stop spinning but it’s not very dynamic. I want these to be able to go back to their original angular damping value if the top marbles are ever moved off the stack.

My code is below.

from direct.showbase.ShowBase import ShowBase
from direct.showbase.DirectObject import DirectObject
from direct.showbase.InputStateGlobal import inputState
from direct.task import Task
from panda3d.core import Vec4, Vec3, Vec2, Point3, LVector3, LColor, Material, BitMask32, PerspectiveLens, OrthographicLens, AmbientLight, Spotlight, WindowProperties, Lens, CollisionRay, ButtonThrower, InputDeviceNode, loadPrcFileData, AntialiasAttrib
from panda3d.bullet import BulletWorld, BulletPlaneShape, BulletSphereShape, BulletBoxShape, BulletRigidBodyNode, BulletGhostNode, BulletDebugNode, BulletContactResult
import sys, random

activeObstacles = []
marbleActors = []
marbleCount = 0
maxObstacleCount = 5
maxMarbleCount = 1000
aspect_ratio = 0

class MyApp(ShowBase):
    
    def __init__(self):
      
        ShowBase.__init__(self)
        global aspect_ratio
        
        # Enable contact events.
        loadPrcFileData('', 'bullet-enable-contact-events true')
        #loadPrcFileData('', 'framebuffer-multisample 1')
        #loadPrcFileData('', 'multisamples 2')
        #self.accept('bullet-contact-added', self.doAdded)
        #self.accept('bullet-contact-destroyed', self.doDestroyed)
        
        # Pressing any button quits the app. Need to prevent it if the mouse is clicked in the corner so that it starts game mode.
        base.buttonThrowers[0].node().setButtonDownEvent('buttonDown')
        self.accept("buttonDown", sys.exit)
      
        screenWidth = int(self.pipe.getDisplayWidth())
        screenHeight = int(self.pipe.getDisplayHeight())
        windowProperties = WindowProperties()
        windowProperties.setSize(screenWidth,screenHeight)
        windowProperties.setForeground(1)
        windowProperties.set_undecorated(True)
        windowProperties.setTitle("Marbles")
        windowProperties.set_icon_filename("Icons\Window_Icon.ico")
        self.win.requestProperties(windowProperties)
        self.setBackgroundColor((0, 0, 0, 1))

        #"""
        # Setup the lens and camera.
        self.disableMouse()
        self.lens = OrthographicLens()
        self.lens.setNearFar(-1000, 1000)
        self.filmSize = 20
        self.windowSizeChanged
        self.camLens.change_event = "Window Size Changed"
        self.accept("Window Size Changed", self.windowSizeChanged)
        #"""

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

        # Setup up BulletWorld.
        self.world = BulletWorld()
        self.world.setGravity(Vec3(0, 0, -15))
        self.world.setDebugNode(debugNP.node())
        
        # Enable anti-aliasing.
        #render.setAntialias(AntialiasAttrib.MAuto)

        # Create Top Border
        topBorderNode = BulletGhostNode('Top Border')
        topBorderNode.addShape(BulletPlaneShape(Vec3(0, 0, -1), 0))
        self.topBorderNP = render.attachNewNode(topBorderNode)
        self.world.attach(topBorderNode)

         # Create Bottom Border
        bottomBorderNode = BulletRigidBodyNode('Bottom Border')
        bottomBorderNode.addShape(BulletPlaneShape(Vec3(0, 0, 1), 0))
        bottomBorderNode.setRestitution(0.75)
        bottomBorderNode.setFriction(500)
        self.bottomBorderNP = render.attachNewNode(bottomBorderNode)
        self.world.attach(bottomBorderNode)

        # Create Left Border
        leftBorderNode = BulletRigidBodyNode('Left Border')
        leftBorderNode.addShape(BulletPlaneShape(Vec3(1, 0, 0), 0))
        leftBorderNode.setRestitution(1)
        leftBorderNode.setFriction(500)
        self.world.attach(leftBorderNode)
        self.leftBorderNP = render.attachNewNode(leftBorderNode)

        # Create Right Wall
        rightBorderNode = BulletRigidBodyNode('Right Border')
        rightBorderNode.addShape(BulletPlaneShape(Vec3(-1, 0, 0), 0))
        rightBorderNode.setRestitution(1)
        rightBorderNode.setFriction(500)
        self.world.attach(rightBorderNode)
        self.rightBorderNP = render.attachNewNode(rightBorderNode)
        
        """
        # Create Back Wall
        rightBorderNode = BulletRigidBodyNode('Back Border')
        rightBorderNode.addShape(BulletPlaneShape(Vec3(0, -1, 0), 0))
        rightBorderNode.setRestitution(1)
        rightBorderNode.setFriction(500)
        self.world.attach(rightBorderNode)
        self.rightBorderNP = render.attachNewNode(rightBorderNode)
        """
        
        # Load all models.
        # Marble Model and Material
        self.marble = loader.loadModel('Models/Marble')
        material = Material()
        material.setSpecular((1, 1, 1, 1))
        material.setShininess(90)
        self.marble.setMaterial(material, 1)

        # Industrial Obstacles
        obstacleModels = ["Models/Screw Head"]
        obstacleModelObjects = []
        for model in obstacleModels:
            obstacleModelObjects.append(loader.loadModel(model))

        # Load all textures.
        # Marble Textures
        Textures = ["Models/Textures/Marble_1.png", "Models/Textures/Marble_2.png", "Models/Textures/Marble_3.png", "Models/Textures/Marble_4.png", "Models/Textures/Marble_5.png"]
        self.marbleTextureObjects = []
        for texture in Textures:
            self.marbleTextureObjects.append(loader.loadTexture(texture))
            
        # Screw Head Textures
        Textures = ["Models/Textures/Philips Head Round.png", "Models/Textures/Flat Head Round.png"]
        screwTextureObjects = []
        for texture in Textures:
            screwTextureObjects.append(loader.loadTexture(texture))

        # Create all obstacles.
        if len(activeObstacles) < maxObstacleCount:
            for i in range(maxObstacleCount):
                randomScale = round(random.uniform(0.5, 1), 2)
                node = BulletRigidBodyNode('Screw Head')
                node.setMass(1000)
                node.setRestitution(0.5)
                node.setFriction(0)
                node.setAnisotropicFriction(0)
                node.linear_factor = LVector3(0, 0, 0)
                node.setAngularVelocity(Vec3(0, random.uniform(0,5), 0))
                node.addShape(BulletSphereShape(randomScale))
                #node.notifyCollisions(True)
                nodePath = render.attachNewNode(node)
                #nodePath.setCollideMask(BitMask32.allOn())
                nodePath.setAntialias(AntialiasAttrib.MAuto)
                self.windowSizeChanged
                self.world.attach(node)
                model = random.choice(obstacleModelObjects)
                model.setScale(randomScale, randomScale, randomScale)
                model.setTexture(random.choice(screwTextureObjects))
                material = Material()
                material.setSpecular((0.5, 0.5, 0.5, 1))
                material.setShininess(15)
                model.setMaterial(material, 1)
                activeObstacles.append(nodePath)
                model.reparentTo(nodePath)
                model.copyTo(nodePath)

        # Attach a point light to the camera so that the marbles are always lit.
        spotLight = Spotlight("Spot Light")
        spotLight.setLens(PerspectiveLens())
        spotLightNodePath = render.attachNewNode(spotLight)
        spotLightNodePath.setPos(random.uniform(-40, 40), -30, 40)
        spotLightNodePath.lookAt(self.bottomBorderNP)
        render.setLight(spotLightNodePath)

        # Add ambient lighting to the scene.
        ambientLight = AmbientLight("Ambient Light")
        ambientLight.setColor(Vec4(0.2, 0.2, 0.2, 1))
        ambientLightNodePath = render.attachNewNode(ambientLight)
        render.setLight(ambientLightNodePath)
        
        render.setShaderAuto()

        self.taskMgr.add(self.createMarbles, "Create Marbles")
        self.taskMgr.add(self.updatePhysics, 'Update Physics')
        
    # Trigger this event each time the size of the window changes.
    def windowSizeChanged(self, lens):
        global aspect_ratio

        aspect_ratio = self.get_aspect_ratio(self.win)
        self.lens.film_size = (self.filmSize, self.filmSize / aspect_ratio)
        self.cam.node().setLens(self.lens)

        # Set position of borders.
        self.topBorderNP.setPos(0, 0, self.filmSize * 0.5 / aspect_ratio)
        self.bottomBorderNP.setPos(0, 0, -self.filmSize * 0.5 / aspect_ratio)
        self.leftBorderNP.setPos(-self.filmSize * 0.5, 0, 0)
        self.rightBorderNP.setPos(self.filmSize * 0.5, 0, 0)
        
        # Set position of obstacles.
        for obstacle in activeObstacles:
            obstacle.setPos(random.uniform((-self.filmSize * 0.5), (self.filmSize * 0.5)), 0, random.uniform((-self.filmSize * 0.5 / aspect_ratio), (self.filmSize * 0.5 / aspect_ratio)))
            dt = globalClock.getDt()
            self.world.doPhysics(dt)
            # Make sure there are no collisions happening with the obstacles.
            while (self.world.contactTest(obstacle.node())).getNumContacts() > 0:
                obstacle.setPos(random.uniform((-self.filmSize * 0.5), (self.filmSize * 0.5)), 0, random.uniform((-self.filmSize * 0.5 / aspect_ratio), (self.filmSize * 0.5 / aspect_ratio)))
                dt = globalClock.getDt()
                self.world.doPhysics(dt)


    # This task runs every 2 seconds.
    def createMarbles(self, task):
        global marbleCount
        global aspect_ratio
        
        if task.time < 0.1:
            return Task.cont

        if marbleCount < maxMarbleCount:
            marbleCount = marbleCount + 1
            
            # Set rotation. (X, Y, Z)
            #marble.flattenLight()
            # Randomize the scale of the model.
            randomScale = round(random.uniform(0.2, 0.2), 2)
            self.marble.setScale(randomScale, randomScale, randomScale)
            # Set a random texture for the marble.
            self.marble.setTexture(random.choice(self.marbleTextureObjects))
            node = BulletRigidBodyNode('Marble')
            node.linear_factor = LVector3(1, 0, 1)
            node.setMass(1)
            node.setRestitution(0.6)
            node.setFriction(5)
            #node.setAnisotropicFriction(Vec3(0,0,0))
            node.addShape(BulletSphereShape(randomScale))
            nodePath = render.attachNewNode(node)
            nodePath.setPos(random.uniform((-self.filmSize * 0.5), (self.filmSize * 0.5)), 0, random.uniform((self.filmSize * 0.5 / aspect_ratio + 5), (self.filmSize * 0.5 / aspect_ratio + 10)))
            nodePath.setColor(random.uniform(0, 1), random.uniform(0, 1), random.uniform(0, 1), random.uniform(0, 1))
            node.angular_damping = 0.5
            node.linear_damping = 0.0
            for marble in marbleActors:
                if marble.angular_damping < 1:
                    marble.angular_damping += 0.01
                    marble.linear_damping += 0.01
            node.setAngularVelocity(Vec3(random.randint(-100,100), random.randint(-100,100), random.randint(-100,100)))
            node.setLinearVelocity(Vec3(random.randint(-5,5), 0, random.randint(-5,0)))
            self.world.attach(node)
            self.marble.copyTo(nodePath)
            marbleActors.append(node)
                
        return Task.again

        # Tell when something collided.
    def doAdded(self, node1, node2):
        print('added:', node1.getName(), node2.getName())
        
        # Tell us when something disconnected from collision.
    def doDestroyed(self, node1, node2):
        print('destroyed:', node1.getName(), node2.getName())

      # Update Physics
    def updatePhysics(self, task):
        dt = globalClock.getDt()
        self.world.doPhysics(dt)
        return Task.cont
      
app = MyApp()
app.run()