Both (Bullet and PhysX) layers support this. The difference is that the ODE module allows to pass a NodePath, and then automatically collects all Geoms below this node. Bullet doesn’t do this automatically for you - you have to find the GeomNodes and Geoms yourself. For example like this:
mesh = BulletTriangleMesh()
geomNodeCollection = nodepath.findAllMatches('**/+GeomNode')
for nodePath in geomNodeCollection.asList():
geomNode = nodePath.node()
for i in range(geomNode.getNumGeoms()):
geom = geomNode.getGeom(i)
mesh.addGeom(geom)
Nox_firegalaxy has considered this a problem with regard to performance, since loops in Python are slower than loops in C++. My opinion is that the tiny loss in performance is made up by the gain in flexibility.
Bullet also has the capability to create shapes from collison solids, not only from GeomNode/Geoms:
BulletBodyNode.addShapesFromCollisionSolids(CollisionNode cnode)
By the way, both Bullet and PhysX support arbitrary convex collision geometry (“convex hulls”).