PyBullet vs MuJoCo
Both are free, Python-first physics engines aimed at robotics and reinforcement learning, and for years the choice between them was mostly taste. It is less even now. The two differ on how contact is formulated, on how they scale, and — the part most comparisons skip — on how actively each one is still being released.
Updated 2026-09-07
PyBullet is the Python binding to the Bullet Physics SDK: an LCP-family rigid-body engine that reads URDF, SDF and MJCF, renders cameras on CPU or GPU, and runs as a client to a physics server. MuJoCo is Google DeepMind's engine for articulated systems in contact, built on a convex relaxation of contact rather than a complementarity problem, with a JAX implementation (MJX) for massively parallel training. Choose MuJoCo for contact-rich manipulation and locomotion, for large-scale RL, and for anything you will still be maintaining in three years. Choose PyBullet when you need its format breadth, its client-server architecture, or its zlib licence — and accept that its release cadence has slowed sharply.
Side-by-side comparison
| Dimension | PyBulletBullet Physics, from Python | MuJoCoConvex contact, actively released |
|---|---|---|
| What it is | Python bindings to the Bullet Physics SDK, a general-purpose rigid- and soft-body engine | A physics engine for multi-joint dynamics with contact, with its own Python module |
| Maintainer | Erwin Coumans, Yunfei Bai and Jasmine Hsu, per the package metadata | Google DeepMind; originally written by Roboti LLC |
| Licence | zlib | Apache 2.0 |
| Latest release on PyPI | 3.2.7, January 2025 | 3.12.0, August 2026 |
| Recent release cadence | Two releases since mid-2022; the repository's issue tracker is closed | Eight releases between March and August 2026 |
| Contact formulation | LCP family — sequential impulse, projected Gauss-Seidel and Dantzig solvers are selectable | Convex relaxation that drops strict complementarity; Newton by default, plus CG and a generalized PGS |
| Articulated bodies | Featherstone's algorithm via btMultiBody, with an opt-in maximal-coordinate path | Generalized coordinates throughout |
| Formats it reads | URDF, SDF, MJCF, Bullet's own .bullet worlds, and soft bodies from OBJ | MJCF natively; URDF, MJZ archives and OpenUSD (documented as experimental) |
| Parallel and GPU | No batched-GPU physics in the Python module; scale by running more DIRECT clients or processes | MJX runs the engine under JAX on NVIDIA and AMD GPUs, Apple Silicon and Cloud TPUs |
| Process model | Client-server: DIRECT, GUI and shared-memory modes let several clients attach to one server | In-process library; parallel sampling by sharing one mjModel across many mjData instances |
| Camera rendering | getCameraImage via a pure-software TinyRenderer or hardware OpenGL, with segmentation masks | Native OpenGL visualiser with debug overlays — contact forces, inertia boxes, joint and actuator axes |
| Sensors | rayTest and rayTestBatch, contact and closest-point queries, joint force/torque | Declarative sensors — touch, accelerometer, gyro, velocimeter, force, torque, magnetometer, rangefinder — with noise, cutoff and delay attributes |
| ROS / ROS 2 | Not mentioned in the project's README or Python module; community bridges only | Not mentioned anywhere in the project's documentation; community bridges only |
| Curated model library | Sample robots and scenes ship with the package and its example environments | MuJoCo Menagerie, a curated model collection maintained by Google DeepMind |
Two different bets on how contact works
The clearest technical difference between these engines is how each formulates contact, and both projects are unusually explicit about it. Bullet sits in the LCP family: PyBullet exposes CONSTRAINT_SOLVER_LCP_SI, CONSTRAINT_SOLVER_LCP_PGS and CONSTRAINT_SOLVER_LCP_DANTZIG as selectable constraint solvers in its module definition, and its articulated bodies run Featherstone's algorithm with that solver attached. MuJoCo took the other road. Its computation chapter describes dropping the strict complementarity constraint “at the heart of the LCP formulation”, which turns contact into a convex optimization problem with a uniquely defined inverse; the overview names the resulting algorithms as a Newton solver by default, with a conjugate gradient method and a generalized projected Gauss-Seidel that handles elliptic friction cones. What is worth noticing is that MuJoCo's own documentation does not sell this as a straight win — it says dropping complementarity means “force and velocity in the contact normal direction can be simultaneously positive, and frictional forces may not be maximally dissipative”, calls those effects “numerically small yet undesirable”, and states that the authors “see both LCP models and convex models as different approximations to physical reality, each with its strengths and weaknesses”. The case for the convex model is that real contact surfaces deform, so soft contact is the more honest approximation. If your work depends on strict complementarity at the contact, that is a genuine reason to stay with an LCP engine.
Check the maintenance gap before anything else
This is the part of the comparison that has changed most since the two were last a coin flip, and it is easy to verify yourself. PyPI lists pybullet at version 3.2.7, published in January 2025, with only one release before it going back to May 2022. The Bullet repository's README states plainly that “the Issue tracker was flooded with support questions and is closed until it is cleaned up”, directing users to the forums instead. mujoco on PyPI, by contrast, is at 3.12.0, published in August 2026 — the eighth release since March of the same year. None of that means PyBullet is broken, and it is worth being fair about what a slow release cadence actually costs. An engine that stops changing also stops breaking your scripts, and the large body of published PyBullet baselines still runs as it did. But it does change what to expect over the next few years: fixes arrive on nobody's schedule, support for new hardware and new Python versions is not guaranteed, and your questions get answered by other users rather than by maintainers. For a weekend project that is irrelevant. For a robot programme with a three-year horizon it outweighs most of the rows in the table above.
Scale is where the two genuinely diverge
If you are training policies, the deciding factor is usually how many environment steps you can afford, and here the architectures are not comparable. MuJoCo ships MJX, a separate mujoco-mjx package that provides a JAX API to MuJoCo and runs on “all compute hardware supported by the XLA compiler” — NVIDIA and AMD GPUs, Apple Silicon, and Google Cloud TPUs — so a batch of environments steps as one vectorised call, with a Warp implementation targeting NVIDIA GPUs specifically. PyBullet has no equivalent in its Python module: Bullet's README describes the SDK's OpenCL GPU path as experimental and incomplete, and it is not what PyBullet uses, so you scale the older way — many DIRECT clients across processes and cores. Two caveats keep this from being a clean sweep. MJX does not have full feature parity with MuJoCo: its documentation lists unsupported geom types and collision pairs, restricts the available integrators, and will refuse to move a model onto the device if that model uses something unsupported. And PyBullet's client-server design buys something MJX does not — GUI, DIRECT and shared-memory connection modes mean a viewer, a controller and the physics server can be separate processes, which is convenient for teleoperation and hardware-in-the-loop rigs. We have no benchmark of our own to cite, and published step-rate figures depend so heavily on model, contact count and hardware that quoting someone else's would mislead. Measure it on your model.
PyBullet reads more formats — including MuJoCo's own
This is the asymmetry a three-way simulator comparison never has room for, and it decides more migrations than the solver does. PyBullet's module definition exposes loadURDF, loadSDF, loadMJCF, loadBullet and loadSoftBody, so a MuJoCo model file can be pointed at PyBullet directly. MuJoCo's modeling guide lists MJCF as native and URDF, MJZ archives and OpenUSD as the other formats it loads; SDFormat is not among them. The door swings one way. Loading is not matching, though: the same MJCF stepped by an LCP solver and by a convex solver will not produce the same trajectory, and MuJoCo's guide is explicit that URDF “can only represent a subset of the model elements available in MuJoCo” and that URDF files, unlike MJCF, are not checked against a schema, so mis-typed attributes are silently ignored. Treat a cross-format load as a starting point for re-tuning rather than a port. The other direction worth knowing about is OpenUSD: MuJoCo now documents USD import and export, built on the standard UsdPhysics schemas with mjcPhysics extensions for what UsdPhysics does not cover, plus a file-format plugin that lets a native USD application treat an MJCF file as a USD layer. Its own documentation labels that support experimental and subject to frequent change, so plan accordingly — but the direction of travel is clear, and PyBullet has nothing comparable. MuJoCo also has Menagerie, a curated collection of high-quality models maintained by Google DeepMind, which for many teams is worth as much as any engine feature.
When to choose each
Projects that need its format breadth — SDF and .bullet worlds in particular — teleoperation and hardware-in-the-loop rigs that suit the client-server design, reproducing existing PyBullet baselines, and codebases where the zlib licence is easier to clear than Apache 2.0.
Contact-rich manipulation and locomotion, reinforcement learning at scale via MJX, work that needs modelled sensor noise and delay or analytically invertible dynamics, and any project whose maintenance horizon is measured in years rather than months.
Where Rigyd fits
Rigyd turns a 3D model, an image, or a text description into validated OpenUSD and MJCF, with collision geometry and mass properties derived once and written into both. That lines up cleanly with MuJoCo: MJCF is its native format, and its experimental USD support means the other output is not wasted either. It lines up less cleanly with PyBullet. PyBullet can read MJCF, but URDF is the format its ecosystem is actually built around, and Rigyd does not emit URDF — so if your pipeline is URDF-first PyBullet, our output is not a drop-in and you would be converting. Worth saying plainly rather than implying otherwise. The problem we do remove is the one that shows up when a team runs both engines: the same object described twice, by hand, with physics properties that quietly drift apart until the two simulations disagree and nobody can say which one is right.