Heuristic:Google deepmind Mujoco Mesh Quality For Collision
| Knowledge Sources | |
|---|---|
| Domains | Optimization, Physics_Simulation |
| Last Updated | 2026-02-15 05:00 GMT |
Overview
Keep coplanar mesh faces under 20 vertices and decimate complex meshes to avoid collision detection inaccuracy and performance degradation in MJX.
Description
MJX processes convex mesh hulls for collision detection by merging coplanar triangular facets into polygons. When a coplanar face exceeds 20 vertices (`_MAX_HULL_FACE_VERTICES`), the vertices are downsampled by striding, which loses geometric accuracy. Additionally, mesh-mesh and mesh-heightfield collisions do not support margin/gap parameters in MJX, which can cause `NotImplementedError` if the model specifies non-zero margins for these geometry types.
Usage
Use this heuristic when preparing mesh assets for MJX simulation, debugging collision issues, or encountering warnings about coplanar face vertices.
The Insight (Rule of Thumb)
- Action: Decimate meshes so that no coplanar face has more than 20 vertices after convex hull computation.
- Value: `_MAX_HULL_FACE_VERTICES = 20` is the hard limit.
- Trade-off: Simpler meshes lose geometric detail but gain collision accuracy and speed. Downsampled faces (> 20 vertices) lose the most accuracy.
- Margin/Gap: Set `margin=0` and `gap=0` for mesh and heightfield geoms in MJX. Non-zero values raise `NotImplementedError`.
Reasoning
MJX's collision detection requires fixed-size arrays for JAX compilation (no dynamic shapes). The 20-vertex limit ensures collision polygons fit within pre-allocated buffers. When exceeded, every Nth vertex is sampled (stride = `face.shape[0] // 20 + 1`), creating a coarser polygon that may miss concave features or produce contact artifacts.
For mesh-mesh collisions, margin/gap support would require variable-size broadphase results, which conflicts with JAX's static shape requirement. This is a known limitation documented via `NotImplementedError`.
Code Evidence
Vertex limit and warning from `mjx/mujoco/mjx/_src/mesh.py:38,138-148`:
_MAX_HULL_FACE_VERTICES = 20
# resize faces that exceed max polygon vertices
if face.shape[0] > _MAX_HULL_FACE_VERTICES:
name = m.names[m.name_meshadr[meshid] :]
name = name[: name.find(b'\x00')].decode('utf-8')
warnings.warn(
f'Mesh "{name}" has a coplanar face with more than '
f'{_MAX_HULL_FACE_VERTICES} vertices. This may lead to performance '
'issues and inaccuracies in collision detection. Consider '
'decimating the mesh.'
)
every = face.shape[0] // _MAX_HULL_FACE_VERTICES + 1
face = face[::every]
Margin/gap check from `mjx/mujoco/mjx/_src/io.py:347-356`:
# margin/gap not supported for meshes and height fields
no_margin = {mujoco.mjtGeom.mjGEOM_MESH, mujoco.mjtGeom.mjGEOM_HFIELD}
if no_margin.intersection({t1, t2}):
# ...
if margin.any():
t1, t2 = mujoco.mjtGeom(t1), mujoco.mjtGeom(t2)
raise NotImplementedError(f'({t1}, {t2}) margin/gap not implemented.')
Edge handling for degenerate meshes from `mjx/mujoco/mjx/_src/mesh.py:78-86`:
if len(v) > 2:
# Meshes can be of poor quality and contain edges adjacent to more than
# two faces. We take the first two unique face normals.
v = np.unique(v, axis=0)[:2]
elif len(v) == 1:
# Some edges are either degenerate or _MAX_HULL_FACE_VERTICES was hit
# and face vertices were down sampled.
continue