Moving a player over BulletTriangleMeshShape is getting caught on the triangle edges

I’m using a triangle mesh as the ground for a grid-based level, which is using two triangles per cell. I am also using a capsule as the player collision shape. When I move the player over the edges of the mesh, it catches a little bit (see example).

I think this is called Internal Edge Collision:


But I can’t make heads or tails of the answers listed above. Does anyone have any ideas for improving the example to move better?

    from direct.showbase.InputStateGlobal import inputState
    from direct.showbase.ShowBase import ShowBase
    from panda3d.core import NodePath
    from panda3d.core import Vec3

    from panda3d.bullet import (
        BulletCapsuleShape,
        BulletCharacterControllerNode,
        BulletDebugNode,
        BulletRigidBodyNode,
        BulletTriangleMesh,
        BulletTriangleMeshShape,
        BulletWorld,
        ZUp,
    )


    LEVEL_VERTICES = [(-0.5, 0.5, 0.0), (-0.5, -0.5, 0.0), (0.5, 0.5, 0.0), (0.5, -0.5, 0.0), (-0.5, 1.5, 0.0), (0.5, 1.5, 0.0), (-0.5, 2.5, 0.0), (0.5, 2.5, 0.0), (-0.5, 3.5, 0.0), (0.5, 3.5, 0.0), (-0.5, 4.5, 0.0), (0.5, 4.5, 0.0), (-0.5, 5.5, 0.0), (0.5, 5.5, 0.0), (-0.5, 6.5, 0.0), (0.5, 6.5, 0.0), (-0.5, 7.5, 0.0), (0.5, 7.5, 0.0), (-0.5, 8.5, 0.0), (0.5, 8.5, 0.0), (-0.5, 9.5, 0.0), (0.5, 9.5, 0.0), (1.5, 0.5, 0.0), (1.5, -0.5, 0.0), (1.5, 1.5, 0.0), (1.5, 2.5, 0.0), (1.5, 3.5, 0.0), (1.5, 4.5, 0.0), (1.5, 5.5, 0.0), (1.5, 6.5, 0.0), (1.5, 7.5, 0.0), (1.5, 8.5, 0.0), (1.5, 9.5, 0.0), (2.5, 0.5, 0.0), (2.5, -0.5, 0.0), (2.5, 1.5, 0.0), (2.5, 2.5, 0.0), (2.5, 3.5, 0.0), (2.5, 4.5, 0.0), (2.5, 5.5, 0.0), (2.5, 6.5, 0.0), (2.5, 7.5, 0.0), (2.5, 8.5, 0.0), (2.5, 9.5, 0.0), (3.5, 0.5, 0.0), (3.5, -0.5, 0.0), (3.5, 1.5, 0.0), (3.5, 2.5, 0.0), (3.5, 3.5, 0.0), (3.5, 4.5, 0.0), (3.5, 5.5, 0.0), (3.5, 6.5, 0.0), (3.5, 7.5, 0.0), (3.5, 8.5, 0.0), (3.5, 9.5, 0.0), (4.5, 0.5, 0.0), (4.5, -0.5, 0.0), (4.5, 1.5, 0.0), (4.5, 2.5, 0.0), (4.5, 3.5, 0.0), (4.5, 4.5, 0.0), (4.5, 5.5, 0.0), (4.5, 6.5, 0.0), (4.5, 7.5, 0.0), (4.5, 8.5, 0.0), (4.5, 9.5, 0.0), (5.5, 0.5, 0.0), (5.5, -0.5, 0.0), (5.5, 1.5, 0.0), (5.5, 2.5, 0.0), (5.5, 3.5, 0.0), (5.5, 4.5, 0.0), (5.5, 5.5, 0.0), (5.5, 6.5, 0.0), (5.5, 7.5, 0.0), (5.5, 8.5, 0.0), (5.5, 9.5, 0.0), (6.5, 0.5, 0.0), (6.5, -0.5, 0.0), (6.5, 1.5, 0.0), (6.5, 2.5, 0.0), (6.5, 3.5, 0.0), (6.5, 4.5, 0.0), (6.5, 5.5, 0.0), (6.5, 6.5, 0.0), (6.5, 7.5, 0.0), (6.5, 8.5, 0.0), (6.5, 9.5, 0.0), (7.5, 0.5, 0.0), (7.5, -0.5, 0.0), (7.5, 1.5, 0.0), (7.5, 2.5, 0.0), (7.5, 3.5, 0.0), (7.5, 4.5, 0.0), (7.5, 5.5, 0.0), (7.5, 6.5, 0.0), (7.5, 7.5, 0.0), (7.5, 8.5, 0.0), (7.5, 9.5, 0.0), (8.5, 0.5, 0.0), (8.5, -0.5, 0.0), (8.5, 1.5, 0.0), (8.5, 2.5, 0.0), (8.5, 3.5, 0.0), (8.5, 4.5, 0.0), (8.5, 5.5, 0.0), (8.5, 6.5, 0.0), (8.5, 7.5, 0.0), (8.5, 8.5, 0.0), (8.5, 9.5, 0.0), (9.5, 0.5, 0.0), (9.5, -0.5, 0.0), (9.5, 1.5, 0.0), (9.5, 2.5, 0.0), (9.5, 3.5, 0.0), (9.5, 4.5, 0.0), (9.5, 5.5, 0.0), (9.5, 6.5, 0.0), (9.5, 7.5, 0.0), (9.5, 8.5, 0.0), (9.5, 9.5, 0.0)]
    LEVEL_TRIANGLES = [(0, 1, 2), (2, 1, 3), (4, 0, 5), (5, 0, 2), (6, 4, 7), (7, 4, 5), (8, 6, 9), (9, 6, 7), (10, 8, 11), (11, 8, 9), (12, 10, 13), (13, 10, 11), (14, 12, 15), (15, 12, 13), (16, 14, 17), (17, 14, 15), (18, 16, 19), (19, 16, 17), (20, 18, 21), (21, 18, 19), (2, 3, 22), (22, 3, 23), (5, 2, 24), (24, 2, 22), (7, 5, 25), (25, 5, 24), (9, 7, 26), (26, 7, 25), (11, 9, 27), (27, 9, 26), (13, 11, 28), (28, 11, 27), (15, 13, 29), (29, 13, 28), (17, 15, 30), (30, 15, 29), (19, 17, 31), (31, 17, 30), (21, 19, 32), (32, 19, 31), (22, 23, 33), (33, 23, 34), (24, 22, 35), (35, 22, 33), (25, 24, 36), (36, 24, 35), (26, 25, 37), (37, 25, 36), (27, 26, 38), (38, 26, 37), (28, 27, 39), (39, 27, 38), (29, 28, 40), (40, 28, 39), (30, 29, 41), (41, 29, 40), (31, 30, 42), (42, 30, 41), (32, 31, 43), (43, 31, 42), (33, 34, 44), (44, 34, 45), (35, 33, 46), (46, 33, 44), (36, 35, 47), (47, 35, 46), (37, 36, 48), (48, 36, 47), (38, 37, 49), (49, 37, 48), (39, 38, 50), (50, 38, 49), (40, 39, 51), (51, 39, 50), (41, 40, 52), (52, 40, 51), (42, 41, 53), (53, 41, 52), (43, 42, 54), (54, 42, 53), (44, 45, 55), (55, 45, 56), (46, 44, 57), (57, 44, 55), (47, 46, 58), (58, 46, 57), (48, 47, 59), (59, 47, 58), (49, 48, 60), (60, 48, 59), (50, 49, 61), (61, 49, 60), (51, 50, 62), (62, 50, 61), (52, 51, 63), (63, 51, 62), (53, 52, 64), (64, 52, 63), (54, 53, 65), (65, 53, 64), (55, 56, 66), (66, 56, 67), (57, 55, 68), (68, 55, 66), (58, 57, 69), (69, 57, 68), (59, 58, 70), (70, 58, 69), (60, 59, 71), (71, 59, 70), (61, 60, 72), (72, 60, 71), (62, 61, 73), (73, 61, 72), (63, 62, 74), (74, 62, 73), (64, 63, 75), (75, 63, 74), (65, 64, 76), (76, 64, 75), (66, 67, 77), (77, 67, 78), (68, 66, 79), (79, 66, 77), (69, 68, 80), (80, 68, 79), (70, 69, 81), (81, 69, 80), (71, 70, 82), (82, 70, 81), (72, 71, 83), (83, 71, 82), (73, 72, 84), (84, 72, 83), (74, 73, 85), (85, 73, 84), (75, 74, 86), (86, 74, 85), (76, 75, 87), (87, 75, 86), (77, 78, 88), (88, 78, 89), (79, 77, 90), (90, 77, 88), (80, 79, 91), (91, 79, 90), (81, 80, 92), (92, 80, 91), (82, 81, 93), (93, 81, 92), (83, 82, 94), (94, 82, 93), (84, 83, 95), (95, 83, 94), (85, 84, 96), (96, 84, 95), (86, 85, 97), (97, 85, 96), (87, 86, 98), (98, 86, 97), (88, 89, 99), (99, 89, 100), (90, 88, 101), (101, 88, 99), (91, 90, 102), (102, 90, 101), (92, 91, 103), (103, 91, 102), (93, 92, 104), (104, 92, 103), (94, 93, 105), (105, 93, 104), (95, 94, 106), (106, 94, 105), (96, 95, 107), (107, 95, 106), (97, 96, 108), (108, 96, 107), (98, 97, 109), (109, 97, 108), (99, 100, 110), (110, 100, 111), (101, 99, 112), (112, 99, 110), (102, 101, 113), (113, 101, 112), (103, 102, 114), (114, 102, 113), (104, 103, 115), (115, 103, 114), (105, 104, 116), (116, 104, 115), (106, 105, 117), (117, 105, 116), (107, 106, 118), (118, 106, 117), (108, 107, 119), (119, 107, 118), (109, 108, 120), (120, 108, 119)]


    class MyApp(ShowBase):
        def __init__(self):
            super().__init__()
            self.disableMouse()
            self.accept("escape", exit)

            self.moveTask = self.taskMgr.add(self.moveTask, 'moveTask')

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

            self.physics_root = BulletRigidBodyNode('level_physics')

            # Set up level.
            self.mesh = BulletTriangleMesh()
            self.mesh.setWeldingDistance(0.1)

            for i, j, k in LEVEL_TRIANGLES:
                self.mesh.addTriangle(
                    LEVEL_VERTICES[i],
                    LEVEL_VERTICES[j],
                    LEVEL_VERTICES[k],
                )

            ground_shape = BulletTriangleMeshShape(self.mesh, dynamic=False)
            self.physics_root.addShape(ground_shape)
            self.world.attachRigidBody(self.physics_root)

            self.camera.setPosHpr(4, -10, 10, 0, -35, 0)  # Set the camera

            self.player_capsule = BulletCapsuleShape(0.5, 1.0/2.0, ZUp)

            self.playerNode = BulletCharacterControllerNode(self.player_capsule, 0.4, 'Player')
            self.physics_np = NodePath(self.playerNode)
            self.physics_np.reparentTo(self.render)

            self.world.attach(self.physics_np.node())

            self.debugactive = True
            debugNode = BulletDebugNode("Debug")
            debugNode.showWireframe(True)
            debugNode.showConstraints(True)
            debugNode.showBoundingBoxes(True)
            debugNode.showNormals(True)
            debugNP = self.render.attachNewNode(debugNode)
            debugNP.show()

            self.world.setDebugNode(debugNode)

            self.taskMgr.add(self.updatePhysicsBullet, 'task_physicsUpdater_Bullet', priority=-20)

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

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

        def moveTask(self, task):
            movement = Vec3()
            if inputState.isSet('up'):
                movement += Vec3(0, 1, 0)
            if inputState.isSet('down'):
                movement += Vec3(0, -1, 0)

            if inputState.isSet('left'):
                movement += Vec3(-1, 0, 0)
            if inputState.isSet('right'):
                movement += Vec3(1, 0, 0)

            self.playerNode.setLinearMovement(movement, True)

            return task.cont


    def main():
        app = MyApp()
        app.run()


    if __name__ == '__main__':
        main()

Actually, you need to use a beam test for the earth and a capsule for objects.

Thanks for the reply. Do you know of any reference I could use?

https://www.panda3d.org/manual/?title=Bullet_Queries

You must use calculations to keep the player above the surface.

Okay I have a ray casting to the ground and can detect it, but how do I keep the BulletCharacterControllerNode or the BulletCapsuleShape above the ground? I can’t find any methods to set the position in the documentation.

You can use a node for control. Something like this example:

I suspect that you want to make a character using physical modeling. However, this is a dead end; behavior will be completely unpredictable.

Yes I am trying to model the character using bullet. Is this generally not done, even if the rest of the game uses bullet physics?

In general, this can be done using a beam test. In this case, you will not lose 100% control over the character. But you have to write a few lines of logic, for example, the influence of gravity and so on.

I remember running into some issues like this before, and adjusting the margins helped (BulletShape.margin). I do not remember if it was the triangle mesh or the dynamic object that required the non-zero margins (maybe both).