Keel’s 3D physics layer bridges pybullet using the same ECS-first pattern as the 2D bridge: describe bodies and shapes as components, and the bridge keeps pybullet in sync automatically every frame. The simulation always runs inDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/VKSFY/keel/llms.txt
Use this file to discover all available pages before exploring further.
DIRECT (headless) mode — GUI mode is never used. pybullet reads Transform3D on the way in and writes position, rotation, and velocity back on the way out.
Installation
pybullet is an optional dependency. Install thephysics3d extra:
setup_physics_3d without pybullet installed raises a clear ImportError:
Setting up physics
Callsetup_physics_3d once, immediately after creating your App. It registers a physics_3d_system at Phase.POST_UPDATE and returns the Physics3D bridge object:
setup_physics_3d is idempotent — a second call returns the existing bridge. The function signature is:
gravity_x=0.0 and gravity_z=0.0; only gravity_y is exposed through setup_physics_3d.
gravity_y=-9.81 is in meters per second squared, matching pybullet’s metric default. Scale your scene accordingly — pybullet works best with objects in the 0.1 – 10 m range.Components
Every 3D physics entity requires three components:Transform3D for position and rotation, RigidBody3D for mass and velocity, and Collider3D for shape. Missing RigidBody3D or Collider3D emits a one-time RuntimeWarning and skips the entity.
RigidBody3D
body_type with the keel.BodyType enum:
damping and ang_damping map to pybullet’s linearDamping and angularDamping respectively, applied via changeDynamics when the body is created.
Collider3D
shape_type selects which size fields the bridge reads:
SPHERE
Uses
radius. Best for balls and round projectiles.BOX
Uses
size_x, size_y, size_z as half-extents. Good for crates and platforms.CAPSULE
Uses
size_x as radius and size_y as height. Good for character controllers.MESH
Intends to use
mesh_id, but is not yet implemented — falls back to a sphere shape with radius, and emits a RuntimeWarning.ShapeType3D.BOX half-extents in pybullet differ from ShapeType2D.BOX in pymunk, which uses full width/height. A size_x=1.0 3D box is 2 units wide total.Spawning physics entities
Collision events
CollisionEvent3D is emitted by the bridge for every contact point pybullet reports in a given step.
Phase.POST_UPDATE:
CollisionEvent2D, CollisionEvent3D does not carry an impulse field — it carries the contact point and contact normal in world space, sourced from pybullet’s getContactPoints.
Direct physics controls
ThePhysics3D bridge object exposes the same style of direct-control helpers as the 2D bridge:
Raycasting
Physics3D.raycast_3d performs a pybullet rayTest and returns hits sorted nearest-first by fraction (0.0 = start, 1.0 = end):
dict with keys: entity_id, point, normal, and fraction.
Integration with Transform3D
The bridge reads and writesTransform3D fields every frame:
| Direction | Fields |
|---|---|
| ECS → pybullet | x, y, z, rot_x, rot_y, rot_z (Euler angles → quaternion) |
| pybullet → ECS | x, y, z, rot_x, rot_y, rot_z (quaternion → Euler), vel_x, vel_y, vel_z, ang_vel_x, ang_vel_y, ang_vel_z |
Transform3D each frame, so you drive them by writing Transform3D directly (or by calling phys.set_position).
ECS tick order
The bridge runs inside a singlePhase.POST_UPDATE system in this fixed order:
sync_to_physics
Reads
Transform3D, RigidBody3D, and Collider3D from every matching archetype and creates or updates the corresponding pybullet bodies. Bodies for despawned entities are removed here.step
Calls
pybullet.stepSimulation one to ten times (clamped substeps based on dt / (1/240)), then drains pybullet.getContactPoints into the internal collision buffer.sync_from_physics
Writes pybullet body state (position, orientation as Euler, linear velocity, angular velocity) back into
Transform3D and RigidBody3D in place.DIRECT mode only
Keel’s pybullet bridge always connects withpybullet.DIRECT. GUI mode is never used. Each Physics3D instance holds its own physicsClientId, so multiple instances (in tests, editors, or parallel worlds) never share state.
setup_physics_3d. You can also call phys.disconnect() (or its alias phys.cleanup()) manually — both are idempotent.
Full example: falling sphere
Troubleshooting
ImportError: 3D physics requires pybullet
ImportError: 3D physics requires pybullet
Run
pip install keelpy[physics3d]. If you are on macOS or Linux and pybullet has no wheel for your Python version, you will need to build it from source or use Windows.CollisionEvent3D never fires
CollisionEvent3D never fires
Ensure at least one of the two bodies is
DYNAMIC. STATIC vs STATIC and KINEMATIC vs STATIC pairs do not emit events in pybullet. See Body Types.MESH shape falls back to sphere
MESH shape falls back to sphere
ShapeType3D.MESH is not yet implemented. The bridge emits a RuntimeWarning and creates a sphere using the entity’s radius field as a fallback.