Bullet Hello World

This page intends to lead through a minimal “hello world” program using Panda3D and Bullet physics.

World

In order to use Bullet physics we need to have a BulletWorld. The world is Panda3D’s term for a “space” or “scene”. The world holds physical objects like rigid bodies, soft bodies or character controllers. It controls global parameters, such a gravity, and it advances the simulation state.

from panda3d.bullet import BulletWorld
world = BulletWorld()
world.setGravity(Vec3(0, 0, -9.81))

The above code creates a new world, and it sets the worlds gravity to a downward vector with length 9.81. While Bullet is in theory independent from any particular units it is recommended to stick with SI units (kilogram, meter, second). In SI units 9.81 m/s² is the gravity on Earth’s surface.

Next we need to advance the simulation state. This is best done by a task which gets called each frame. We find out about the elapsed time (dt), and pass this value to the do_physics() method.

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

taskMgr.add(update, 'update')

The doPhysics method allows finer control on the way the simulation state is advanced. Internally Bullet splits a timestep into several substeps. We can pass a maximum number of substeps and the size of each substep, like show in the following code.

world.doPhysics(dt, 10, 1.0/180.0)

Here we have a maximum of 10 substeps, each with 1/180 seconds. Choosing smaller substeps will make the simulation more realistic, but performance will decrease too. Smaller substeps also reduce jitter.

Static bodies

So far we just have an empty world. We next need to add some objects. The most simple objects are static bodies. Static object don’t change their position or orientation with time. Typical static objects are the ground or terrain, and houses or other non-moveable obstacles. Here we create a simple plane which will serve as a ground.

from panda3d.bullet import BulletPlaneShape
from panda3d.bullet import BulletRigidBodyNode

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

node = BulletRigidBodyNode('Ground')
node.addShape(shape)

np = render.attachNewNode(node)
np.setPos(0, 0, -2)

world.attachRigidBody(node)

First we create a collision shape, in the case a BulletPlaneShape. We pass the plane’s constant and normal vector within the shape’s constructor. There is a separate page about setting up the various collision shapes offered by Bullet, so we won’t go into more detail here.

Next we create a rigid body and add the previously created shape. BulletRigidBodyNode is derived from PandaNode, and thus the rigid body can be placed within the Panda3D scene graph. you can also use methods like setPos or setH to place the rigid body node where you want it to be.

Finally we need to attach the newly created rigid body node to the world. Only rigid bodies attached to the world will be considered when advancing the simulation state.

Dynamic bodies

Dynamic bodies are similar to static bodies. Except that dynamic bodies can be moved around the world by applying force or torque. To setup a dynamic body is almost the same as for static bodies. We will have to set one additional property though, the body’s mass. Setting a positive finite mass will create a dynamic body, while setting the mass to zero will create a static body. Zero mass is a convention for setting an infinite mass, which is the same as making the body unmovable (static).

from panda3d.bullet import BulletBoxShape

shape = BulletBoxShape(Vec3(0.5, 0.5, 0.5))

node = BulletRigidBodyNode('Box')
node.setMass(1.0)
node.addShape(shape)

np = render.attachNewNode(node)
np.setPos(0, 0, 2)

world.attachRigidBody(node)

Bullet will automatically update a rigid body node’s position and orientation if is has changed after advancing the simulation state. So, if you have a GeomNode- e. g. a textured box - and reparent this geom node below the rigid body node, then the geom node will move around together with the rigid body. You don’t have to synchronize the visual world with the physics world.

The Program

Let’s put everything learned on this page together into a single script, which is shown below. It assumes that you have an .egg model of a 1 by 1 by 1 box.

when running the script you will see a box falling down onto an invisible plane. The plane is invisible simply because we didn’t parent a visual mode below the plane’s rigid body node. Of course we could have done so.

The model cube.egg used in this hello word sample can be found in the following archive: https://www.panda3d.org/download/noversion/bullet-samples.zip

import direct.directbase.DirectStart
from panda3d.core import Vec3
from panda3d.bullet import BulletWorld
from panda3d.bullet import BulletPlaneShape
from panda3d.bullet import BulletRigidBodyNode
from panda3d.bullet import BulletBoxShape

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

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

# Plane
shape = BulletPlaneShape(Vec3(0, 0, 1), 1)
node = BulletRigidBodyNode('Ground')
node.addShape(shape)
np = render.attachNewNode(node)
np.setPos(0, 0, -2)
world.attachRigidBody(node)

# Box
shape = BulletBoxShape(Vec3(0.5, 0.5, 0.5))
node = BulletRigidBodyNode('Box')
node.setMass(1.0)
node.addShape(shape)
np = render.attachNewNode(node)
np.setPos(0, 0, 2)
world.attachRigidBody(node)
model = loader.loadModel('models/box.egg')
model.flattenLight()
model.reparentTo(np)

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

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