Bullet Collision Shapes

On the previous page we have been introduced to Bullet basics. Two simple collision shapes - a box and a plane - have been used in this simple script. This page will now introduce more collision shapes provided by Bullet, starting with primitive shapes and then moving on to more complex ones.

Primitive shapes:

  • Sphere Shape

  • Plane Shape

  • Box Shape

  • Cylinder Shape

  • Capsule Shape

  • Cone Shape

Complex shapes:

  • Compound Shape

  • Convex Hull Shape

  • Triangle Mesh Shape

  • Heightfield Shape

  • Soft Body Shape

  • Multi Sphere Shape

  • Convex Point Cloud Shape

Sphere Shape

The most basic collision shape, a sphere with radius radius. The sphere is centered around its origin.

from panda3d.bullet import BulletSphereShape
radius = 0.5
shape = BulletSphereShape(radius)

Plane Shape

Another primitive collision shape, an infinite plane. To create a plane you have to pass both the plane’s normal vector (Vec3(nx, ny, nz)) and the plane constant (d, which is the distance of the plane’s origin. Planes can only be used for static objects.

from panda3d.bullet import BulletPlaneShape
normal = Vec3(0, 0, 1)
d = 0
shape = BulletPlaneShape(normal, d)

Box Shape

A box-shaped primitive collision shape. To create a box you have to pass a vector with the half-extents (Vec3(dx, dx, dx)). The full extents of the box will be twice the half extents, e. g. from -dx to +dx on the local x-axis.

from panda3d.bullet import BulletBoxShape
dx = 0.5
dy = 0.5
dz = 1.0
shape = BulletBoxShape(Vec3(dx, dy, dz))

Cylinder Shape

A primitive collision shape which is represents a cylinder. We can create a cylinder shape by either passing it’s radius, height and cylinder axis, or by passing a vector with half extents and the cylinder axis. The following example creates two cylinder shapes, both with radius 0.5 and height 1.4.

from panda3d.bullet import BulletCylinderShape
radius = 0.5
height = 1.4
shape1 = BulletCylinderShape(radius, height, ZUp)
shape2 = BulletCylinderShape(Vec3(radius, 0, 0.5 * height), ZUp)

Capsule Shape

A primitive collision shape which is a “capped” cylinder. “Capped” means that there are half-spheres at both ends, unlike the real cylinder which has flat ends. Capsule shapes are a good choice for character controllers, since they are fast, symmetrical, and allow smooth movement over steps.

To create a capsule shape we have to pass the capsule’s radius, the height of the cylindrical part, and the up-axis. The total height of the capsule will be the height of the cylindrical part, plus twice the radius.

from panda3d.bullet import BulletCapsuleShape
radius = 0.5
height = 1.0
shape = BulletCapsuleShape(radius, height, ZUp)

Cone Shape

Again a primitive collision shape, which represents a cone. We have to pass the radius of the circular base of the cone, and it’s height.

from panda3d.bullet import BulletConeShape
radius = 0.6
height = 1.0
shape = BulletConeShape(radius, height, ZUp)

Compound Shape

Compound shapes are assemblies made up from two or more individual shapes. For example you could create a collision shape for a table from five box shapes. One “flat” box for the table plate, and four “thin” ones for the table legs.

The Panda3D Bullet module has no specialized class for compound shapes. It automatically creates a compound shape if more than one shape is added to a body node.

The following code snippet will create such a compound shape, resembling the before mentioned table.

shape1 = BulletBoxShape((1.3, 1.3, 0.2))
shape2 = BulletBoxShape((0.1, 0.1, 0.5))
shape3 = BulletBoxShape((0.1, 0.1, 0.5))
shape4 = BulletBoxShape((0.1, 0.1, 0.5))
shape5 = BulletBoxShape((0.1, 0.1, 0.5))

bodyNP.node().addShape(shape1, TransformState.makePos(Point3(0, 0, 0.1)))
bodyNP.node().addShape(shape2, TransformState.makePos(Point3(-1, -1, -0.5)))
bodyNP.node().addShape(shape3, TransformState.makePos(Point3(-1, 1, -0.5)))
bodyNP.node().addShape(shape4, TransformState.makePos(Point3(1, -1, -0.5)))
bodyNP.node().addShape(shape5, TransformState.makePos(Point3(1, 1, -0.5)))

Convex Hull Shape

The first of the non-primitive collision shapes. A good analogy for a convex hull is an elastic membrane or balloon under pressure which is placed around a given set of vertices. When released the membrane will assume the shape of the convex hull. Convex hull shapes should be used for dynamic objects, if it is not possible to find a good approximation of the objects shape using collision primitives.

Convex hull shapes can be created is several ways:

from panda3d.bullet import BulletConvexHullShape

# Add each vertex separately
shape1 = BulletConvexHullShape()
shape1.addPoint(Point3(1, 1, 2))
shape1.addPoint(Point3(0, 0, 0))
shape1.addPoint(Point3(2, 0, 0))
shape1.addPoint(Point3(0, 2, 0))
shape1.addPoint(Point3(2, 2, 0))

# Add several vertices with a single call
shape2 = BulletConvexHullShape()
shape2.addArray([
   Point3(1, 1, 2),
   Point3(0, 0, 0),
   Point3(2, 0, 0),
   Point3(0, 2, 0),
   Point3(2, 2, 0),
])

# Add all vertices which can be found in a Geom object
geomNodes = loader.loadModel(path).findAllMatches('**/+GeomNode')
geomNode = geomNodes.getPath(0).node()
geom = geomNode.getGeom(0)
shape3 = BulletConvexHullShape()
shape3.addGeom(geom)

Triangle Mesh Shape

Another non-primitive collision shape. A triangle mesh shape is similar to the convex hull shape, except that it is not restricted to convex geometry; it can contain concave parts. A typical use case for triangle mesh shapes is the static geometry of a game level. However, it is possible to use triangle mesh shapes for dynamic objects too. We have to explicitly tell Bullet if we want a static or dynamic triangle mesh shape at the time the shape is created.

To create a triangle mesh shape, we first have to create a triangle mesh object. The following example will create a simple quad composed of two triangles.

from panda3d.bullet import BulletTriangleMeshShape
p0 = Point3(-10, -10, 0)
p1 = Point3(-10, 10, 0)
p2 = Point3(10, -10, 0)
p3 = Point3(10, 10, 0)
mesh = BulletTriangleMesh()
mesh.addTriangle(p0, p1, p2)
mesh.addTriangle(p1, p2, p3)
shape = BulletTriangleMeshShape(mesh, dynamic=False)

We can use a convenience method to add all triangles from a Geom object with one method call. The geom will be decomposed first, so it does not have to contain only triangles; for example, it can contain triangle strips too.

from panda3d.bullet import BulletTriangleMesh
mesh = BulletTriangleMesh()
mesh.addGeom(geom)

Heightfield Shape

A special non-primitive collision shape. Give a heightfield image we can construct a terrain mesh with only a few lines of code.

from panda3d.core import Filename
from panda3d.core import PNMImage
from panda3d.bullet import BulletHeightfieldShape
from panda3d.bullet import ZUp
height = 10.0
img = PNMImage(Filename('elevation.png'))
shape = BulletHeightfieldShape(img, height, ZUp)

The heightfield shape will be oriented the same way as a GeoMipTerrain created from the same image, but GeoMipTerrain and BulletHeightfieldShape have different origins. The BulletHeightfieldShape is centered around the origin, while the GeoMipTerrain uses the lower left corner as its origin. However, this can be easily corrected by positioning the GeoMipTerrain with an offset relative to the static rigid body node.

If you are using ShaderTerrainMesh, then you need to use a Texture object as a height map. This will ensure that the shape of the physical body corresponds to the visible geometry.

from panda3d.core import Filename
offset = img.getXSize() / 2.0 - 0.5
terrain = GeoMipTerrain('terrain')
terrain.setHeightfield(img)
terrainNP = terrain.getRoot()
terrainNP.setSz(height)
terrainNP.setPos(-offset, -offset, -height / 2.0)

Soft Body Shape

This special collision shape is used in connection with soft bodies. It can not be created directly. Soft bodies will be discussed later within this manual.