Convex hulls vs triangle mesh colliders
Almost every collision-geometry decision in robotics simulation comes back to one constraint: solvers are fast because they assume convexity. That is why your detailed mesh gets replaced by a hull you did not ask for, and why the exact-geometry option quietly stops working the moment the object starts moving.
Updated 2026-08-31
A convex hull is the smallest convex shape enclosing a mesh — no dents, no interior pockets — and it is what physics solvers collide against directly. A triangle mesh collider uses the mesh's actual triangles, concavities included. The hull is cheaper and works on moving bodies; the triangle mesh is exact and, in most engines, is restricted to static geometry. That restriction, not accuracy, is usually what decides the question.
Side-by-side comparison
| Dimension | Convex hullsConvex approximation, any body | Triangle mesh collidersExact surface, usually static |
|---|---|---|
| What the engine collides against | A convex shell wrapped around the mesh's vertices | The mesh's own triangles, concavities included |
| Concave features (cups, shelves, handles) | Lost — a mug's interior fills in | Preserved exactly |
| Dynamic (non-kinematic) rigid bodies | Supported everywhere | Generally not supported — see the engine rows below |
| Static and kinematic geometry | Supported | Supported — this is its normal use |
| MuJoCo | The only option: collision is limited to convex geoms, and meshes are replaced by their convex hulls | Not available for standard geoms; height fields and SDF plugins are the documented exceptions |
| PhysX (Isaac Sim, Omniverse) | Supported on any actor | Rejected as a simulation shape on non-kinematic dynamic actors |
| Bullet (PyBullet) | Supported on any body | btBvhTriangleMeshShape is static-only; btGImpactMeshShape handles moving concave meshes |
| Authoring in OpenUSD | physics:approximation = "convexHull" or "convexDecomposition" | physics:approximation = "none" — the schema default |
| Authoring in SDF (Gazebo) | <mesh optimization="convex_hull"> or "convex_decomposition" | The default: no optimization attribute set |
| Size limits | PhysX caps a cooked convex mesh at 256 vertices and polygons, 64 for GPU-compatible meshes | Bounded by the mesh itself, so a scanned asset can carry very large triangle counts |
| Modelling a concave *moving* object | Decompose it into several hulls on one body | Not the intended path in any of the engines above |
| Effect on mass and inertia | MuJoCo can compute inertia from the hull (inertia="convex") | MuJoCo's inertia="exact" uses the true mesh, but needs a watertight, well-oriented one |
| Typical use | Anything the robot picks up, pushes, or throws | Floors, walls, terrain, fixed fixtures, scanned environments |
Why solvers insist on convexity
The convexity requirement is not a limitation anyone chose for its own sake; it is what makes narrow-phase collision fast. Algorithms like GJK/EPA and MPR work on convex sets, where a support function answers "how far does this shape extend in that direction" in one pass, and where two shapes overlap in exactly one connected region. Concave geometry breaks both properties. MuJoCo states the consequence plainly: with the exception of its SDF plugins, collision detection is limited to convex geoms, all primitive types are convex, and meshes supplied by the user "are replaced with their convex hulls", computed by the qhull library. Note the split that follows from this — the mesh you see is not the mesh you collide with. MuJoCo renders your non-convex mesh exactly as authored while simulating its hull, which is why a gripper can visibly pass through the opening of a cup and still report contact.
Static or dynamic is the line that actually decides it
Most write-ups frame this as accuracy versus speed. In practice the engine decides for you, based on whether the object moves. PhysX documents that attaching a triangle mesh, heightfield or plane geometry shape configured as a simulation shape is not supported for non-kinematic dynamic actors — so in Isaac Sim and anything else built on PhysX, a triangle mesh collider is available for the floor and the workcell, and unavailable for the part being picked up. Bullet draws the same line in its source: btBvhTriangleMeshShape is "a static-triangle mesh shape, it can only be used for fixed/non-moving objects", and the header directs anyone needing moving concave meshes to perform convex decomposition or to use btGImpactMeshShape instead. If your object is static, the exact mesh is usually available and usually fine. If it moves, the question answers itself and the real work becomes how to decompose it well.
The exceptions worth knowing
The rule has documented exits, and they matter because they are where the field is moving. MuJoCo treats height fields as a collection of triangular prisms rather than convex geoms, and its SDF plugins step outside convex collision entirely. PhysX supports signed-distance-field colliders that give a dynamic body collision behaviour following its true surface, but with a hard constraint: SDF colliders only work when the GPU solver is used, enabled via the scene's GPU dynamics flag. Bullet's GImpact path handles moving concave meshes on the CPU. None of these makes triangle-soup collision the default choice — MuJoCo's documentation notes that a general-purpose "triangle soup" collider can be installed through a custom callback but explicitly does not recommend it, on the grounds that pre-processing into convex pieces "pays off at runtime and yields both faster and more stable simulation".
The hull leaks into mass properties, not just contact
Collision geometry is usually discussed as a contact question, but in MuJoCo it also feeds inertia. The mesh inertia attribute selects between convex (volume and inertia from the convex hull, uniform density), exact (true values even for non-convex meshes, requiring a watertight, well-oriented mesh), shell (mass on the surface), and legacy. legacy is still the default for backward compatibility, and the documentation is blunt that it "leads to volume overcounting for non-convex meshes" and is not recommended. So a hollow or heavily concave object can end up both colliding as a solid block and weighing more than it should, from a single unset attribute. Throughput pushes the same way: MJX's guidance is to keep convex meshes small, suggesting maxhullvert of 64 or less, roughly 200 vertices or fewer for collisions against primitives, and fewer than 32 vertices for convex-convex pairs.
When to choose each
Anything that moves under the solver — objects being manipulated, thrown, or pushed — plus any asset destined for GPU-parallel training, where vertex budgets are tight. For concave moving objects, this means several hulls on one body rather than one loose shell.
Static geometry where exactness is cheap and valuable: floors, walls, terrain, shelving, fixed fixtures, and scanned environments a robot navigates but never lifts. Also the right call for kinematic bodies you drive directly rather than simulate.
Where Rigyd fits
Rigyd generates collision geometry as part of preparing an asset, emitting convex decompositions for the movable objects and carrying the choice through to both OpenUSD and MJCF so the same object behaves consistently in both. The honest limit: if your scene is a static environment — a scanned warehouse the robot drives through and never touches — you do not need decomposition at all, and a triangle mesh collider straight from the source mesh is the better answer. Rigyd is worth reaching for when objects move, when the same asset has to exist in more than one simulator, or when the number of assets makes hand-tuning hulls impractical. For a handful of hero objects, a careful engineer with the right tools still wins.