Before MuJoCo can compute a contact force it must know which surfaces touch, where, and along which normal. Collision detection answers that every step, for every pair of geoms that might touch, and it has to be fast: a scene with $n$ geoms has $n(n-1)/2$ pairs, 80 000 for 400 balls.
MuJoCo splits the work into stages. A broad phase finds pairs of bodies whose bounding boxes might overlap (a sweep-and-prune along the direction in which geoms are most spread out), a mid phase narrows that to geom pairs with a bounding-volume hierarchy, filters drop pairs the model says should never collide, and a narrow phase runs the exact test for each remaining pair’s geometry types and returns contact points, normals and distances. What you control as a modeller is mostly the filtering and two distances per geom, margin and gap.
[!established] The rules, from MuJoCo’s computation chapter Two geoms are never checked if they belong to the same body, or to a parent and a child body unless the parent is the world (the
filterparentflag, on by default, controls this second rule). They must pass the bit test(contype1 & conaffinity2) || (contype2 & conaffinity1), with both defaults equal to 1. Body pairs listed in<exclude>are skipped. A<pair>element names two geoms explicitly and bypasses the parent and bit filters. A contact is detected when the surface distance is at mostmargin + gap(both summed over the two geoms), and it produces force only when the distance is at mostmargin. Collision geometry is convex: a mesh collides as its convex hull.
The lab drops a ball onto a plane whose margin and gap you set; the contact overlay shows a contact as soon as one is detected, and the readouts say whether it carries force.
```lab simlab
{“dock”: true, “title”: “Margin and gap”, “height”: 230,
“xml”: “
## Mathematics
### Which pairs are checked
The filters are boolean rules, so they can be predicted exactly before running anything. The script overlaps geoms in each situation and lists the pairs that produce contacts; every absence has a rule behind it, and turning off `filterparent` brings the parent-child pair back. The bit test is worth decoding once: `contype` says "what I am", `conaffinity` says "what I collide with", and a pair collides if either geom's type matches the other's affinity. Two geoms with `contype="2" conaffinity="2"` and `contype="1" conaffinity="1"` share no bit and never collide; one with `contype="1" conaffinity="2"` collides with both.
### Margin and gap
For surface distance $\delta$ (negative when penetrating), with margin $m$ and gap $g$ summed over the pair:
$$\delta > m + g:\ \text{no contact}, \qquad m < \delta \le m + g:\ \text{inactive contact}, \qquad \delta \le m:\ \text{active contact}.$$
The constraint's softness (Lesson 9.2) is then evaluated on $r = \delta - m$, so a margin shifts the whole contact outward: the ball rests where $r$ equals its usual resting penetration of $-0.367$ mm, which is $\delta = m - 0.367$ mm, measured as 9.633 mm in output (2). Margin is for contacts that should act early (a soft layer, a safety distance); gap is for contacts that should be known early without acting (sensing, adhesion actuators).
### Contacts per pair
Each pair-specific collider returns as many contact points as it needs to support the shapes stably: one for a sphere on a plane, two for a capsule lying down, several for flat faces. A box resting on a plane gets four, one per corner in contact, each carrying part of the weight. More points are not more accuracy: they are what makes a flat face stable without spinning about a single point.
## Implementation
```io
INPUT: small scenes written in the script
PROCESS: test every filtering rule; measure the margin and gap regimes and the resting height with a margin; count contacts per pair; time collision detection for 10 to 400 bodies; drop a ball into a non-convex mesh
OUTPUT: printed tables
```python file=examples/l9_1_collision.py “"”Lesson 9.1: collision detection: which pairs are checked, margin and gap, contacts per pair.
INPUT small scenes written below
PROCESS (1) overlap geoms in every filtering situation and list which pairs make contacts:
same body, parent-child, child of the world, contype/conaffinity bits,
Run: python examples/l9_1_collision.py “””
import os import time
import mujoco import numpy as np
FILTERS = “””
”””
MARGIN = “””
”””
SHAPES = “””
”””
def filters() -> None:
model = mujoco.MjModel.from_xml_string(FILTERS)
data = mujoco.MjData(model)
mujoco.mj_forward(model, data)
found = sorted({“ / “.join(sorted((model.geom(c.geom1).name, model.geom(c.geom2).name)))
for c in data.contact[:data.ncon]})
print(f” contacts between overlapping geoms ({len(found)} pairs):”)
for pair in found:
print(f” {pair}”)
expected_missing = {
“a1 / a2”: “same body: never checked”,
“a1 / child”: “parent and child: filtered (filterparent on by default)”,
“a1 / b_bits”: “contype 1 & conaffinity 2 = 0 and contype 2 & conaffinity 1 = 0: incompatible bits”,
“a1 / d_excluded”: “bodies a and d excluded with
def margin_and_gap() -> None: model = mujoco.MjModel.from_xml_string(MARGIN) data = mujoco.MjData(model) print(f” {‘distance (mm)’:>14}{‘contacts’:>10}{‘active rows’:>13}{‘normal force (N)’:>18}”) for dist in (0.020, 0.014, 0.012, 0.008, 0.002): mujoco.mj_resetData(model, data) data.qpos[2] = 0.05 + dist data.qvel[2] = -0.5 # approaching at 0.5 m/s mujoco.mj_forward(model, data) f = np.zeros(6) force = 0.0 if data.ncon: mujoco.mj_contactForce(model, data, 0, f) force = f[0] active = sum(1 for c in data.contact[:data.ncon] if c.efc_address >= 0) print(f” {1000 * dist:>14.1f}{data.ncon:>10}{active:>13}{force:>18.3f}”) for margin in (0.0, 0.01): xml = MARGIN.replace(‘’, “”).replace(‘margin=”0.01” gap=”0.005”’, f’margin=”{margin}”’) m2 = mujoco.MjModel.from_xml_string(xml) d2 = mujoco.MjData(m2) for _ in range(2000): mujoco.mj_step(m2, d2) print(f” under gravity with margin {1000 * margin:.0f} mm, the ball comes to rest with its surface “ f”{1000 * (d2.qpos[2] - 0.05):+.3f} mm from the plane”)
def contacts_per_pair() -> None:
bodies = {
“sphere on plane”: ‘<body pos="0 0 0.2">
def collision_cost() -> None:
reps = 200 if os.environ.get(“MJC_FAST”) == “1” else 2000
for n in (10, 100, 400):
side = int(np.ceil(np.sqrt(n)))
balls = ““.join(f’<body pos="{0.12 * (i % side)} {0.12 * (i // side)} 0.05">
def convex_hull() -> None: # A U-shaped bowl, 20 cm wide inside, as one mesh: two walls joined by a floor. spec = mujoco.MjSpec() spec.worldbody.add_geom(type=mujoco.mjtGeom.mjGEOM_PLANE, size=[1, 1, 0.1]) verts = [] for x0, x1 in ((-0.15, -0.10), (0.10, 0.15), (-0.15, 0.15)): z1 = 0.15 if (x0, x1) != (-0.15, 0.15) else 0.02 for x in (x0, x1): for y in (-0.1, 0.1): for z in (0.0, z1): verts += [x, y, z] spec.add_mesh(name=”u”, uservert=verts) spec.worldbody.add_geom(type=mujoco.mjtGeom.mjGEOM_MESH, meshname=”u”, rgba=[0.6, 0.6, 0.7, 1]) ball = spec.worldbody.add_body(pos=[0, 0, 0.4]) ball.add_freejoint() ball.add_geom(size=[0.03, 0, 0]) model = spec.compile() data = mujoco.MjData(model) for _ in range(1500): mujoco.mj_step(model, data) print(f” a ball dropped into the U (walls 15 cm high, floor 2 cm thick) rests with its centre at z = {data.qpos[2]:.3f} m: “ f”on the convex hull’s top (0.15 m + radius 0.03 m = 0.18 m), not on the 2 cm floor”)
if name == “main”: print(“(1) which overlapping geoms make contacts”) filters() print(“(2) margin 10 mm and gap 5 mm on the plane, ball (radius 50 mm) approaching at 0.5 m/s”) margin_and_gap() print(“(3) contacts per pair at rest”) contacts_per_pair() print(“(4) cost of collision detection”) collision_cost() print(“(5) non-convex meshes collide as their convex hull”) convex_hull()
Output:
```text
(1) which overlapping geoms make contacts
contacts between overlapping geoms (6 pairs):
a1 / c_bits
a1 / e_ghost
a2 / c_bits
b_bits / c_bits
c_bits / child
ground / world_child
absent: a1 / a2 same body: never checked
absent: a1 / child parent and child: filtered (filterparent on by default)
absent: a1 / b_bits contype 1 & conaffinity 2 = 0 and contype 2 & conaffinity 1 = 0: incompatible bits
absent: a1 / d_excluded bodies a and d excluded with <exclude>
with the filterparent flag disabled, a1 / child collides: True
(2) margin 10 mm and gap 5 mm on the plane, ball (radius 50 mm) approaching at 0.5 m/s
distance (mm) contacts active rows normal force (N)
20.0 0 0 0.000
14.0 1 0 0.000
12.0 1 0 0.000
8.0 1 1 55.000
2.0 1 1 70.000
under gravity with margin 0 mm, the ball comes to rest with its surface -0.367 mm from the plane
under gravity with margin 10 mm, the ball comes to rest with its surface +9.633 mm from the plane
(3) contacts per pair at rest
sphere on plane 1 contact(s) at rest
capsule lying on plane 2 contact(s) at rest
cylinder upright on plane 3 contact(s) at rest
box on plane 4 contact(s) at rest
box on box 4 contact(s) at rest
(4) cost of collision detection
10 balls on a plane: 55 geom pairs in principle, 10 contacts, mj_collision 2.5 us (this machine)
100 balls on a plane: 5050 geom pairs in principle, 100 contacts, mj_collision 19.6 us (this machine)
400 balls on a plane: 80200 geom pairs in principle, 400 contacts, mj_collision 48.3 us (this machine)
(5) non-convex meshes collide as their convex hull
a ball dropped into the U (walls 15 cm high, floor 2 cm thick) rests with its centre at z = 0.180 m: on the convex hull's top (0.15 m + radius 0.03 m = 0.18 m), not on the 2 cm floor
Reading the output:
<exclude>. The geom with contype="0" conaffinity="0" collides only with the geom its explicit <pair> names.Two objects pass through each other. Check the bits (contype/conaffinity of both), any <exclude> between their bodies, and whether they are parent and child. Print model.geom_contype and model.geom_conaffinity for both geoms.
A link of a robot collides with the link it is attached to. It cannot, unless filterparent was disabled or the two are not parent and child (for example, two links separated by a third). Use <exclude> for pairs that touch by design.
An object floats above the floor or a gripper grips air. A nonzero margin, possibly inherited from a default class (output 2).
A part of a mesh behaves as if it were filled in. Collision uses the convex hull (output 5). Decompose non-convex objects into convex pieces, each its own geom.
Many contacts are listed but nothing pushes. They are inactive contacts inside the gap. Check efc_address of each contact: $-1$ means no force.
Give a gripper’s two pads contype="2" conaffinity="2", a cube contype="2" conaffinity="1", and the table contype="1" conaffinity="1". Predict, before running, which of the pairs pad-pad, pad-cube, pad-table and cube-table can collide, then check it in the playground.
Build a cup as a non-convex mesh and show that a ball cannot go into it. Then decompose the cup into convex pieces (a floor and four walls, or a ring of boxes), show that the ball can now rest inside, and measure the cost of collision detection for both versions.
[!research] Collision geometry is a modelling decision Robot and object models used in learning are often taken from meshes made for rendering. Their convex hulls fill handles, cavities and gaps, which changes which grasps exist in simulation (inference). Convex decomposition, primitive approximations and contact margins are part of the model, and a sim-to-real study that ignores them may be measuring the collision geometry rather than the policy.
{"id": "9.1-check", "title": "Knowledge check", "questions": [
{"kind": "mcq", "q": "Geom A has contype=2, conaffinity=1. Geom B has contype=1, conaffinity=1. Do they collide?",
"options": ["Yes: B's contype matches A's conaffinity", "No: their contypes differ", "Only with an explicit pair", "Only if their margin is positive"],
"answer": 0,
"explain": "<p>$(2 \\,\\&\\, 1) \\,||\\, (1 \\,\\&\\, 1) = 0 \\,||\\, 1$: true.</p>"},
{"kind": "numeric", "q": "A ball rests on a plane with margin 5 mm (default contact parameters, gravity on). How far above the plane is its surface, in mm?",
"answer": 4.633, "tol": 0.01, "unit": "mm",
"explain": "<p>The constraint is evaluated on distance minus margin, which settles at the usual −0.367 mm: 5 − 0.367 = 4.633 mm.</p>"},
{"kind": "mcq", "q": "A contact has distance between margin and margin + gap. What does it do?",
"options": ["Produces a reduced force", "It is listed in mjData.contact but produces no force", "It is not detected", "It produces an adhesive force"],
"answer": 1,
"explain": "<p>An inactive contact: detected, with <code>efc_address = -1</code>.</p>"},
{"kind": "mcq", "q": "Why can the hand of a robot not collide with the forearm it is attached to by a joint?",
"options": ["Their contypes differ", "Parent and child bodies are filtered by default", "Meshes never collide", "The margin is negative"],
"answer": 1,
"explain": "<p>The parent filter skips pairs of a body and its parent (unless the parent is the world). Disable <code>filterparent</code> to turn it off.</p>"}
]}
Lesson 9.2 opens up what happens once a contact is active: the soft-constraint model that turns penetration and velocity into force, and the optimization problem MuJoCo solves for all constraint forces at once.