bd4ee537b8
PiperOrigin-RevId: 897857927 Change-Id: I82ce0a23465429e7025334e83ba00dfbe5085649
877 lines
31 KiB
Markdown
877 lines
31 KiB
Markdown
---
|
||
name: mujoco-python
|
||
description: >
|
||
Build, manipulate, and simulate MuJoCo physics models using the Python
|
||
bindings (MjSpec, MjModel, MjData). Covers choosing between compute
|
||
backends (C++ for full features and noslip, MJWarp for GPU batch RL). Use when constructing scenes
|
||
programmatically via the spec API, compiling and stepping simulations,
|
||
reading sensor/body/geom data, attaching sub-models, composing specs with
|
||
prefixed names, using contact sensors for fixed-size observation spaces,
|
||
configuring collision filtering, offscreen rendering (context management,
|
||
cameras, depth/segmentation), or performing spatial math (quaternion, pose,
|
||
rotation conversions via mju_). Covers gotchas around compilation
|
||
lifecycle, named indexing vs bind, geom size semantics, camera conventions,
|
||
and orientation representations.
|
||
---
|
||
|
||
# MuJoCo Python Bindings
|
||
|
||
## Compilation Lifecycle
|
||
|
||
```
|
||
MjSpec ──spec.compile()──▶ MjModel ──MjData(model)──▶ MjData
|
||
│ │ │
|
||
│ (mutable blueprint) │ (compiled, mostly frozen) │ (simulation state)
|
||
│ │ │
|
||
└── spec.recompile(m, d) ─────┴────────────────────────────┘
|
||
```
|
||
|
||
1. **MjSpec** — mutable data structure you edit to define the simulation.
|
||
2. **`spec.compile()`** — produces `MjModel` + you create `MjData(model)`.
|
||
After this, changing the spec has **no effect** until you recompile.
|
||
3. **Most `MjModel` fields are unsafe to mutate.** Changing them requires
|
||
`spec.recompile(model, data)`, which returns **new** model and data objects
|
||
(preserving physics state for existing elements).
|
||
|
||
```python
|
||
import mujoco
|
||
|
||
spec = mujoco.MjSpec()
|
||
body = spec.worldbody.add_body(pos=[0, 0, 1])
|
||
geom = body.add_geom(type=mujoco.mjtGeom.mjGEOM_SPHERE, size=[0.1])
|
||
body.add_freejoint()
|
||
|
||
model = spec.compile()
|
||
data = mujoco.MjData(model)
|
||
mujoco.mj_forward(model, data)
|
||
|
||
# Later: add another body, recompile keeping state
|
||
body2 = spec.worldbody.add_body(pos=[1, 0, 1])
|
||
body2.add_geom(size=[0.1])
|
||
body2.add_freejoint()
|
||
model, data = spec.recompile(model, data) # state preserved
|
||
```
|
||
|
||
> [!CAUTION]
|
||
> `recompile` returns **new** objects. Always reassign: `model, data = spec.recompile(model, data)`.
|
||
|
||
### Loading and Serializing
|
||
|
||
```python
|
||
spec = mujoco.MjSpec() # empty
|
||
spec = mujoco.MjSpec.from_string(xml_string) # from XML string
|
||
spec = mujoco.MjSpec.from_file('/path/to.xml') # from file
|
||
model = mujoco.MjModel.from_xml_string(xml) # direct to model (no spec)
|
||
|
||
xml_out = spec.to_xml() # serialize back
|
||
```
|
||
|
||
### Compile Error Debugging
|
||
|
||
Use the `.info` field on spec elements for traceability:
|
||
|
||
```python
|
||
geom = spec.worldbody.add_geom()
|
||
geom.info = 'created at my_file.py:42'
|
||
spec.compile() # Error: "size 0 must be positive in geom\nElement name '', id 0, created at my_file.py:42"
|
||
```
|
||
|
||
---
|
||
|
||
## Compute Backends
|
||
|
||
MuJoCo has two compute backends. **Choose early** — the backend determines
|
||
which features, solvers, and APIs are available.
|
||
|
||
| | C++ (default) | MJWarp (NVIDIA GPU) |
|
||
|---|---|---|
|
||
| **Import** | `import mujoco` | `import mujoco_warp as mjw` |
|
||
| **Optimized for** | Latency (single scene) | Throughput (big batches) |
|
||
| **Hardware** | CPU | NVIDIA GPU |
|
||
| **Solvers** | All (Newton, CG, PGS, **noslip**) | All except PGS, **noslip**, islands |
|
||
| **Plugins** | ✅ All | SDF only |
|
||
| **Precision** | float64 | float32 |
|
||
| **Named access / bind** | ✅ | Via wrapper libraries or MJX `bind()` |
|
||
| **Contact sensors** | ✅ | ✅ |
|
||
| **Sparse Jacobians** | ✅ | ❌ (dense only) |
|
||
| **Batch rendering** | ❌ | ✅ (BVH ray tracing) |
|
||
|
||
### When to use which
|
||
|
||
- **C++ (default)**: Real-time control, model predictive control, interactive
|
||
visualization, any workflow needing full feature support (noslip solver,
|
||
PGS, islands, plugins, ellipsoidal fluid model, sparse Jacobians). Also the
|
||
only backend with native `bind()`. Use this unless you need massive
|
||
parallelism. (For MJWarp, named access is available via wrapper
|
||
libraries or MJX `bind()`.)
|
||
|
||
- **MJWarp**: Reinforcement learning with large batch sizes on NVIDIA GPUs.
|
||
Scales better for contact-rich scenes and large meshes than the legacy
|
||
MJX-JAX backend. Not differentiable. May degrade for scenes beyond ~60 DoFs.
|
||
|
||
> [!IMPORTANT]
|
||
> The `noslip` solver (post-constraint velocity correction for exact zero
|
||
> slip at contacts) is **only available in the C++ backend**. If your task
|
||
> requires accurate friction modeling without any tangential sliding at
|
||
> contacts, you must use C++.
|
||
|
||
> [!WARNING]
|
||
> MJWarp uses **float32**, which can cause numerical differences vs C++
|
||
> (float64). Solver convergence, small friction values, and long rollouts
|
||
> may be sensitive to this. If you see NaNs or instability on GPU, try
|
||
> increasing solver iterations or simplifying the model.
|
||
|
||
---
|
||
|
||
## Building Models with MjSpec
|
||
|
||
### Adding elements
|
||
|
||
Most `add_*` methods accept keyword arguments matching MJCF XML attributes:
|
||
|
||
```python
|
||
spec = mujoco.MjSpec()
|
||
|
||
body = spec.worldbody.add_body(name='arm', pos=[0, 0, 1], quat=[1, 0, 0, 0])
|
||
geom = body.add_geom(
|
||
name='arm_geom',
|
||
type=mujoco.mjtGeom.mjGEOM_CAPSULE,
|
||
size=[0.05, 0.3],
|
||
rgba=[1, 0, 0, 1],
|
||
)
|
||
joint = body.add_joint(
|
||
name='hinge1',
|
||
type=mujoco.mjtJoint.mjJNT_HINGE,
|
||
axis=[0, 1, 0],
|
||
range=[-1.57, 1.57],
|
||
)
|
||
site = body.add_site(name='sensor_site', pos=[0, 0, 0.3])
|
||
cam = body.add_camera(name='arm_cam', pos=[0, -2, 0], xyaxes=[1,0,0, 0,0,1])
|
||
```
|
||
|
||
### Orientation alternatives
|
||
|
||
In addition to `quat`, you can specify orientation with `euler`, `axisangle`,
|
||
`xyaxes`, or `zaxis`. Only one can be set at a time:
|
||
|
||
```python
|
||
body.add_geom(euler=[0, 90, 0]) # Euler angles (degrees by default)
|
||
body.add_geom(axisangle=[0, 1, 0, 1.57]) # axis + angle
|
||
body.add_geom(zaxis=[0, 1, 0]) # minimal rotation to align Z
|
||
body.add_geom(xyaxes=[1,0,0, 0,0,1]) # explicit X and Y axes
|
||
```
|
||
|
||
### Top-level elements
|
||
|
||
Sensors, actuators, tendons, materials, textures, and meshes are added directly
|
||
to the spec (not to bodies):
|
||
|
||
```python
|
||
spec.add_material(name='red', rgba=[1, 0, 0, 1])
|
||
spec.add_actuator(name='motor', joint=joint.name, gear=[1, 0, 0, 0, 0, 0])
|
||
spec.add_sensor(
|
||
name='joint_pos',
|
||
type=mujoco.mjtSensor.mjSENS_JOINTPOS,
|
||
objtype=mujoco.mjtObj.mjOBJ_JOINT,
|
||
objname=joint.name,
|
||
)
|
||
```
|
||
|
||
> [!IMPORTANT]
|
||
> Always use `element.name` (e.g., `joint.name`, `geom.name`, `site.name`)
|
||
> instead of hardcoded strings when referencing spec elements. This keeps
|
||
> references correct if the element is renamed or attached with a prefix.
|
||
|
||
### Geom size semantics
|
||
|
||
| Type | Size params |
|
||
| --------- | ----------------------------------------------- |
|
||
| sphere | `[radius]` |
|
||
| capsule | `[radius, half_length]` or `[radius]` + fromto |
|
||
| cylinder | `[radius, half_length]` or `[radius]` + fromto |
|
||
| box | `[half_x, half_y, half_z]` |
|
||
| ellipsoid | `[radius_x, radius_y, radius_z]` |
|
||
| plane | `[half_x, half_y, grid_spacing]` |
|
||
|
||
> [!WARNING]
|
||
> Capsule/cylinder `size` changes meaning with `fromto`. Without `fromto`,
|
||
> `size=[radius, half_length]`. With `fromto`, `size=[radius]` only — the
|
||
> length is computed from the two endpoints.
|
||
|
||
---
|
||
|
||
## Accessing Compiled Data: Named Access vs Bind
|
||
|
||
There are **two** recommended ways to read/write compiled model and data fields.
|
||
**Prefer `bind`** when working with spec elements; use **named access** otherwise.
|
||
|
||
### 1. Named Access (on MjModel / MjData)
|
||
|
||
```python
|
||
model.geom('my_geom').size # → numpy view of geom_size for 'my_geom'
|
||
data.body('torso').xpos # → numpy view of body_xpos
|
||
data.joint('knee').qpos # → shape depends on joint type
|
||
data.actuator('motor').ctrl = 1.0 # writable view
|
||
```
|
||
|
||
Aliases: `joint` / `jnt`, `camera` / `cam`, `tendon` / `ten`, `material` / `mat`,
|
||
`texture` / `tex`, `equality` / `eq`, `keyframe` / `key`.
|
||
|
||
> [!WARNING]
|
||
> Named access returns **views, not copies.** After `mj_step`, old references
|
||
> reflect new values. Use `.copy()` when logging:
|
||
> `positions.append(data.body('torso').xpos.copy())`
|
||
|
||
### 2. Bind (bridges MjSpec elements → MjModel / MjData)
|
||
|
||
`bind()` connects spec elements (or lists of them) to their compiled
|
||
counterparts. **Use `.set()` to write through bind:**
|
||
|
||
```python
|
||
geom = spec.worldbody.add_geom(name='ball', size=[0.1], type=mujoco.mjtGeom.mjGEOM_SPHERE)
|
||
joint = body.add_joint(name='j1', type=mujoco.mjtJoint.mjJNT_HINGE)
|
||
model = spec.compile()
|
||
data = mujoco.MjData(model)
|
||
mujoco.mj_forward(model, data)
|
||
|
||
# Reading via bind
|
||
model.bind(geom).size # → array([0.1, 0., 0.])
|
||
data.bind(geom).xpos # → array([0., 0., 0.])
|
||
|
||
# Writing via bind — always use .set()
|
||
data.bind(joint).set('qpos', 1.5) # sets the joint's qpos
|
||
|
||
# Bind a list of spec elements
|
||
joints = [spec.joint('j1'), spec.joint('j2')]
|
||
data.bind(joints).qpos # → concatenated array
|
||
data.bind(joints).set('qpos', np.array([0.5, 1.0])) # write to both
|
||
```
|
||
|
||
> [!CAUTION]
|
||
> The spec must match the compiled model. If you modify the spec after
|
||
> `compile()`, you must recompile before calling `bind()`, or you get:
|
||
> `ValueError: 'The mjSpec does not match mjModel. Please recompile the mjSpec.'`
|
||
|
||
---
|
||
|
||
## Attachments: Composing Specs
|
||
|
||
Attach child specs/bodies to parent specs via frames or sites:
|
||
|
||
```python
|
||
parent = mujoco.MjSpec()
|
||
child = mujoco.MjSpec()
|
||
child_body = child.worldbody.add_body(name='arm')
|
||
child_body.add_geom(name='arm_geom', size=[0.05, 0.3], type=mujoco.mjtGeom.mjGEOM_CAPSULE)
|
||
child_body.add_joint(name='arm_joint', type=mujoco.mjtJoint.mjJNT_HINGE)
|
||
|
||
frame = parent.worldbody.add_frame(pos=[0, 0, 1])
|
||
frame.attach_body(child_body, prefix='left_')
|
||
# 'arm' → 'left_arm', 'arm_geom' → 'left_arm_geom', 'arm_joint' → 'left_arm_joint'
|
||
|
||
# Or attach entire spec to a site
|
||
site = parent.worldbody.add_site(name='attach_point', pos=[0, 0, 2])
|
||
parent.attach(child, site=site, prefix='right_', suffix='_v2')
|
||
```
|
||
|
||
> [!IMPORTANT]
|
||
> **Cross-spec references require a shared parent.**
|
||
> If you need to create an element (e.g., an equality constraint) that
|
||
> references elements from *two different child specs*, you must first
|
||
> attach both children to the same parent, then add the cross-referencing
|
||
> element to the **parent** spec using the final prefixed/suffixed names:
|
||
|
||
```python
|
||
# Two robot arms, each defined as a separate spec
|
||
arm_spec = mujoco.MjSpec()
|
||
arm_body = arm_spec.worldbody.add_body(name='hand')
|
||
arm_body.add_geom(name='hand_geom', size=[0.05])
|
||
wrist_joint = arm_body.add_joint(name='wrist', type=mujoco.mjtJoint.mjJNT_HINGE)
|
||
|
||
# Attach both to the parent with different prefixes
|
||
parent = mujoco.MjSpec()
|
||
left_prefix, right_prefix = 'left_', 'right_'
|
||
|
||
frame_l = parent.worldbody.add_frame(pos=[-0.5, 0, 1])
|
||
frame_l.attach_body(arm_body, prefix=left_prefix) # left_wrist, left_hand, ...
|
||
|
||
frame_r = parent.worldbody.add_frame(pos=[0.5, 0, 1])
|
||
frame_r.attach_body(arm_body, prefix=right_prefix) # right_wrist, right_hand, ...
|
||
|
||
# NOW add a constraint linking both arms — look up the prefixed joints
|
||
# from the parent spec, don't hardcode the names
|
||
left_wrist = parent.joint(f'{left_prefix}{wrist_joint.name}')
|
||
right_wrist = parent.joint(f'{right_prefix}{wrist_joint.name}')
|
||
parent.add_equality(type=mujoco.mjtEq.mjEQ_JOINT,
|
||
name1=left_wrist.name, name2=right_wrist.name)
|
||
model = parent.compile()
|
||
```
|
||
|
||
### Attachment Transforms
|
||
|
||
When attaching to a site or frame, the child body's position is transformed
|
||
relative to the parent's attachment point. Attachment also handles unit
|
||
conversion (degrees vs radians) between parent and child specs automatically.
|
||
|
||
### Assets Get Renamed Too
|
||
|
||
Prefix/suffix changes apply to asset filenames:
|
||
```python
|
||
child.assets = {'mesh.obj': data}
|
||
parent.attach(child, prefix='robot_')
|
||
# Asset key becomes 'robot_mesh.obj' in parent
|
||
```
|
||
|
||
---
|
||
|
||
## Cameras
|
||
|
||
### Orientation
|
||
|
||
MuJoCo cameras look down the **negative Z axis**. The camera frame is:
|
||
- **-Z** → forward (viewing direction)
|
||
- **+X** → right
|
||
- **+Y** → up
|
||
|
||
To point a camera downward (looking at the ground), set its Z axis to `[0, 0, 1]`:
|
||
|
||
```python
|
||
body.add_camera(
|
||
name='overhead',
|
||
xyaxes=[1, 0, 0, 0, 1, 0], # x=[1,0,0], y=[0,1,0] → z=[0,0,1] → looks DOWN (-z)
|
||
pos=[0, 0, 5],
|
||
)
|
||
```
|
||
|
||
### Geom Group Visibility
|
||
|
||
Each camera/viewer has 6 geom groups (0–5). Default visibility:
|
||
|
||
| Group | Default Visible | Typical Use |
|
||
|-------|----------------|-------------|
|
||
| 0 | ✅ Yes | Standard geoms (default group for new geoms) |
|
||
| 1 | ✅ Yes | Secondary visual geoms |
|
||
| 2 | ✅ Yes | Tertiary visual geoms |
|
||
| 3 | ❌ No | Collision-only or debug geoms |
|
||
| 4 | ❌ No | Hidden geoms |
|
||
| 5 | ❌ No | Hidden geoms |
|
||
|
||
A newly created geom is in **group 0** by default. Toggle visibility at runtime
|
||
via `mjvOption.geomgroup[i]`. The same 3-on/3-off default applies to sites,
|
||
joints, tendons, actuators, flexes, and skins.
|
||
|
||
---
|
||
|
||
## Contacts: Use Sensors, Not the Contact Array
|
||
|
||
### The problem with `data.contact`
|
||
|
||
`data.contact` is a **variable-length** array that changes size every timestep
|
||
depending on what's colliding. Iterating over it directly is fragile and
|
||
**incompatible with learning-based agents** and fixed-size observation spaces.
|
||
|
||
```python
|
||
# ❌ WRONG — don't iterate data.contact for reward/observation logic
|
||
for c in data.contact:
|
||
if c.geom1 == target_geom_id:
|
||
force = ... # fragile, variable-length, non-deterministic order
|
||
```
|
||
|
||
> [!CAUTION]
|
||
> Never iterate `data.contact` to build observations or compute rewards.
|
||
> The array's length and ordering can change between timesteps and even
|
||
> between MuJoCo versions. Use **contact sensors** instead.
|
||
|
||
### Contact sensors: fixed-size, declarative contact queries
|
||
|
||
A `<contact>` sensor selects contacts via declarative matching criteria, reduces
|
||
them to a fixed number of slots, and extracts requested data fields into
|
||
`data.sensordata` — always the same size, every timestep.
|
||
|
||
The pipeline has three stages:
|
||
1. **Matching** — filter contacts by geom, body, subtree, or site volume
|
||
2. **Reduction** — keep the top `num` contacts (by order, min distance, max force, or net force)
|
||
3. **Extraction** — copy requested fields (`found`, `force`, `torque`, `dist`, `pos`, `normal`, `tangent`)
|
||
|
||
### Example: detect contact force between a gripper and an object
|
||
|
||
```python
|
||
import mujoco
|
||
import numpy as np
|
||
|
||
spec = mujoco.MjSpec()
|
||
|
||
# Build a simple scene: floor + falling object
|
||
floor = spec.worldbody.add_geom(
|
||
name='floor', type=mujoco.mjtGeom.mjGEOM_PLANE, size=[1, 1, 0.01]
|
||
)
|
||
obj_body = spec.worldbody.add_body(name='obj', pos=[0, 0, 0.5])
|
||
obj_body.add_freejoint()
|
||
obj_geom = obj_body.add_geom(
|
||
name='obj_geom', type=mujoco.mjtGeom.mjGEOM_SPHERE,
|
||
size=[0.05], mass=0.1,
|
||
)
|
||
|
||
# Add a contact sensor: report force for contacts involving obj_geom
|
||
contact_sensor = spec.add_sensor(
|
||
name='obj_contact',
|
||
type=mujoco.mjtSensor.mjSENS_CONTACT,
|
||
# Match any contact involving this geom — use .name, not a literal string:
|
||
objname=obj_geom.name, objtype=mujoco.mjtObj.mjOBJ_GEOM,
|
||
)
|
||
|
||
model = spec.compile()
|
||
data = mujoco.MjData(model)
|
||
|
||
# Step the simulation until the object lands
|
||
mujoco.mj_step(model, data, nstep=500)
|
||
mujoco.mj_forward(model, data)
|
||
|
||
# Read the contact sensor via bind — always fixed-size in data.sensordata
|
||
contact_data = data.bind(contact_sensor).sensordata
|
||
print(f'Contact sensor output: {contact_data}')
|
||
```
|
||
|
||
### XML-based contact sensor (common pattern)
|
||
|
||
When loading from XML, contact sensors are even cleaner:
|
||
|
||
```xml
|
||
<sensor>
|
||
<!-- Is the gripper touching the object? Report force and normal for up to 3 contacts -->
|
||
<contact name="grip_contact"
|
||
body1="gripper" body2="object"
|
||
num="3" data="found force normal"
|
||
reduce="maxforce"/>
|
||
|
||
<!-- Total wrench from all contacts on a body -->
|
||
<contact name="object_net"
|
||
body1="object"
|
||
data="force torque"
|
||
reduce="netforce"/>
|
||
</sensor>
|
||
```
|
||
|
||
The output size is deterministic: `num × size(data fields)`. For `"found force
|
||
normal"` with `num=3`, you get 3 × (1+3+3) = 21 numbers every timestep, padded
|
||
with zeros if fewer contacts match.
|
||
|
||
### Touch sensor: simpler alternative for scalar normal force
|
||
|
||
If you only need a scalar "how hard is something pressing on this site", use a
|
||
`touch` sensor instead:
|
||
|
||
```python
|
||
site = body.add_site(name='fingertip', pos=[0, 0, 0.05], size=[0.02])
|
||
spec.add_sensor(
|
||
name='fingertip_touch',
|
||
type=mujoco.mjtSensor.mjSENS_TOUCH,
|
||
objname=site.name, objtype=mujoco.mjtObj.mjOBJ_SITE,
|
||
)
|
||
```
|
||
|
||
The touch sensor sums normal contact forces within the site volume — one scalar
|
||
output, always present in `sensordata`.
|
||
|
||
---
|
||
|
||
## Spatial Math Utilities (mju_)
|
||
|
||
MuJoCo ships a library of spatial computation functions under the `mju_`
|
||
namespace — quaternion algebra, rotation conversions, pose composition, and
|
||
coordinate transforms. **Always check for an existing `mju_` function before
|
||
implementing spatial math from scratch.** For basic vector arithmetic (add,
|
||
subtract, dot product, norm), just use NumPy/JAX/Torch directly.
|
||
|
||
### Quaternion Operations
|
||
|
||
```python
|
||
res = np.zeros(3)
|
||
mujoco.mju_rotVecQuat(res, vec, quat) # rotate vector by quaternion
|
||
|
||
quat = np.zeros(4)
|
||
mujoco.mju_mat2Quat(quat, mat3x3) # 3x3 rotation matrix → quaternion
|
||
mujoco.mju_quat2Mat(mat, quat) # quaternion → 3x3 matrix
|
||
mujoco.mju_axisAngle2Quat(quat, axis, angle) # axis-angle → quaternion
|
||
mujoco.mju_euler2Quat(quat, euler, 'xyz') # Euler angles → quaternion
|
||
mujoco.mju_mulQuat(res, q1, q2) # multiply quaternions
|
||
mujoco.mju_negQuat(res, quat) # conjugate
|
||
mujoco.mju_quatZ2Vec(quat, vec) # quat that rotates z-axis to vec
|
||
mujoco.mju_quatIntegrate(quat, vel, scale) # integrate quat with angular velocity
|
||
```
|
||
|
||
> [!TIP]
|
||
> `mju_quatZ2Vec` is particularly useful: given a target direction vector, it
|
||
> returns the quaternion that rotates the Z-axis to point in that direction.
|
||
|
||
### Pose Operations
|
||
|
||
```python
|
||
mujoco.mju_mulPose(pos_res, quat_res, pos1, quat1, pos2, quat2) # compose poses
|
||
mujoco.mju_negPose(pos_res, quat_res, pos, quat) # invert pose
|
||
mujoco.mju_trnVecPose(res, pos, quat, vec) # transform vector by pose
|
||
```
|
||
|
||
---
|
||
|
||
## Common Gotchas
|
||
|
||
### 1. Computed fields are read-only
|
||
|
||
`data.xpos`, `data.xmat`, `data.xquat`, `data.geom_xpos` are **output** fields
|
||
computed by `mj_forward()`. You cannot assign to them directly. Instead, modify
|
||
input fields (`data.qpos`, `data.qvel`, `data.ctrl`) and call `mj_forward()` or
|
||
`mj_step()`.
|
||
|
||
### 2. Duplicate names are forbidden
|
||
|
||
```python
|
||
spec.add_material(name='yellow')
|
||
spec.add_material(name='yellow') # ValueError: "repeated name 'yellow' in material"
|
||
```
|
||
|
||
Names must be unique within each element type.
|
||
|
||
### 3. Orientation keywords are mutually exclusive
|
||
|
||
```python
|
||
body.add_geom(axisangle=[1, 0, 0, 1.57], euler=[0, 0, 0])
|
||
# ValueError: 'Only one of: axisangle, xyaxes, zaxis, or euler can be set.'
|
||
```
|
||
|
||
Pick one orientation representation. Quaternion (`quat`) is the native format.
|
||
|
||
### 4. `size` must be positive for geoms
|
||
|
||
A geom with `size[0] == 0` will fail compilation. Always set at least
|
||
`size=[radius]` for spheres/capsules, or `size=[hx, hy, hz]` for boxes.
|
||
|
||
### 5. `mj_step` with `nstep` repeats the same control
|
||
|
||
```python
|
||
mujoco.mj_step(model, data, nstep=100) # 100 steps, same ctrl each step
|
||
```
|
||
|
||
This is much faster than a Python loop and is fine for passive simulation or
|
||
constant-control scenarios. But if you need to update `data.ctrl` between steps,
|
||
you must step one at a time.
|
||
|
||
### 6. Euler sequence matters
|
||
|
||
`mju_euler2Quat` takes a 3-character sequence string. Lowercase = intrinsic
|
||
rotations, uppercase = extrinsic:
|
||
|
||
```python
|
||
mujoco.mju_euler2Quat(quat, [roll, pitch, yaw], 'xyz') # intrinsic x-y-z
|
||
mujoco.mju_euler2Quat(quat, [roll, pitch, yaw], 'XYZ') # extrinsic X-Y-Z
|
||
```
|
||
|
||
The sequence must be exactly 3 characters from `xyzXYZ`.
|
||
|
||
### 7. `copy()` vs view semantics
|
||
|
||
NumPy arrays from MjModel/MjData are **views** into C memory. `mj_step` changes
|
||
them in-place. Always `.copy()` when storing values for later comparison.
|
||
|
||
### 8. Default class handling
|
||
|
||
```python
|
||
main = spec.default # global default class (always named 'main')
|
||
child_class = spec.add_default('high_friction', main)
|
||
child_class.geom.friction = [1.5, 0.005, 0.0001]
|
||
|
||
geom = body.add_geom(child_class) # use specific default class
|
||
geom = body.add_geom() # uses 'main' class implicitly
|
||
```
|
||
|
||
### 9. Gravity is -Z by default
|
||
|
||
MuJoCo convention: **+Z is up**, gravity is `[0, 0, -9.81]`. The viewer and
|
||
all built-in models assume this. Don't fight it — orient your scene accordingly.
|
||
|
||
### 10. Capsule/cylinder size with and without fromto
|
||
|
||
```python
|
||
# With explicit pos/quat: size = [radius, half_length]
|
||
body.add_geom(type=mujoco.mjtGeom.mjGEOM_CAPSULE, size=[0.05, 0.3])
|
||
|
||
# With fromto: size = [radius] only — length is inferred from endpoints
|
||
body.add_geom(
|
||
type=mujoco.mjtGeom.mjGEOM_CAPSULE,
|
||
size=[0.05],
|
||
fromto=[0, 0, 0, 0, 0, 0.6],
|
||
)
|
||
```
|
||
|
||
### 11. Collision filtering with contype/conaffinity
|
||
|
||
Two geoms collide only if `(g1.contype & g2.conaffinity) || (g2.contype & g1.conaffinity)`.
|
||
By default both are `1`, so everything collides with everything.
|
||
|
||
```python
|
||
# Visual-only geom: set contype=0, conaffinity=0 to disable collisions
|
||
body.add_geom(size=[0.1], contype=0, conaffinity=0, group=1)
|
||
|
||
# Separate collision groups using bitmasks:
|
||
robot_geom = body.add_geom(size=[0.05], contype=1, conaffinity=2)
|
||
tool_geom = body.add_geom(size=[0.03], contype=2, conaffinity=1)
|
||
# Robot and tool collide (1&1=0, but 2&2=0… wait):
|
||
# contype=1 & conaffinity=1 → collide; contype=2 & conaffinity=2 → collide
|
||
```
|
||
|
||
> [!TIP]
|
||
> **`condim` and `friction` interact.** Each geom has `friction=[tangential, torsional, rolling]`
|
||
> (default `[1, 0.005, 0.0001]`). The `condim` value controls which friction coefficients are
|
||
> *active* in a contact:
|
||
>
|
||
> | condim | Active friction | Geom `friction` indices used |
|
||
> |--------|----------------|------------------------------|
|
||
> | 1 | None (frictionless, normal force only) | — |
|
||
> | 3 | Tangential (opposes sliding) | `friction[0]` |
|
||
> | 4 | Tangential + torsional (opposes sliding and twisting around contact normal) | `friction[0:2]` |
|
||
> | 6 | Tangential + torsional + rolling (also opposes rolling around tangent axes) | `friction[0:3]` |
|
||
>
|
||
> Torsional friction models a surface contact patch resisting twist — useful for soft fingers.
|
||
> Rolling friction dissipates energy from local deformations — useful for stopping balls from rolling
|
||
> forever. Both torsional and rolling coefficients have **units of length** (roughly the contact
|
||
> patch diameter or deformation depth).
|
||
>
|
||
> ```python
|
||
> # A soft finger pad: enable torsional friction for stable grasping
|
||
> finger_geom = body.add_geom(
|
||
> type=mujoco.mjtGeom.mjGEOM_CAPSULE,
|
||
> size=[0.01, 0.02],
|
||
> condim=4,
|
||
> friction=[1.0, 0.01, 0.0001], # tangential=1.0, torsional=0.01
|
||
> )
|
||
>
|
||
> # A ball that should stop rolling on a surface
|
||
> ball_geom = body.add_geom(
|
||
> type=mujoco.mjtGeom.mjGEOM_SPHERE,
|
||
> size=[0.05],
|
||
> condim=6,
|
||
> friction=[0.8, 0.005, 0.002], # tangential=0.8, torsional=0.005, rolling=0.002
|
||
> )
|
||
> ```
|
||
|
||
---
|
||
|
||
## Offscreen Rendering
|
||
|
||
Offscreen rendering produces images (RGB, depth, segmentation) without a
|
||
display. It requires an OpenGL context — MuJoCo auto-detects the best
|
||
available backend (EGL on headless Linux, GLFW on desktop, OSMesa as
|
||
fallback).
|
||
|
||
### The `Renderer` class
|
||
|
||
`mujoco.Renderer` wraps GL context creation, scene management, and buffer
|
||
readback. **Always use it as a context manager** to ensure GPU resources are
|
||
freed:
|
||
|
||
```python
|
||
import mujoco
|
||
import numpy as np
|
||
|
||
# Define a camera in the spec and keep a reference
|
||
overhead_cam = spec.worldbody.add_camera(
|
||
name='overhead',
|
||
pos=[0, 0, 3],
|
||
quat=[0.707, 0.707, 0, 0], # looking down
|
||
fovy=60,
|
||
)
|
||
|
||
model = spec.compile()
|
||
data = mujoco.MjData(model)
|
||
|
||
# Create renderer — width/height must not exceed offscreen buffer (see below)
|
||
with mujoco.Renderer(model, height=480, width=640) as renderer:
|
||
mujoco.mj_forward(model, data)
|
||
|
||
# Use the spec element's .name — never a literal string
|
||
renderer.update_scene(data, camera=overhead_cam.name)
|
||
rgb = renderer.render() # → np.ndarray (H, W, 3), dtype=uint8
|
||
|
||
# Depth rendering
|
||
renderer.enable_depth_rendering()
|
||
renderer.update_scene(data, camera=overhead_cam.name)
|
||
depth = renderer.render() # → np.ndarray (H, W), dtype=float32 (meters)
|
||
renderer.disable_depth_rendering()
|
||
|
||
# Segmentation rendering
|
||
renderer.enable_segmentation_rendering()
|
||
renderer.update_scene(data, camera=overhead_cam.name)
|
||
seg = renderer.render() # → np.ndarray (H, W, 2), dtype=int32
|
||
# seg[:,:,0] = object ID, seg[:,:,1] = object type; background = (-1, -1)
|
||
renderer.disable_segmentation_rendering()
|
||
```
|
||
|
||
> [!WARNING]
|
||
> Forgetting to close the renderer (or not using `with`) leaks GPU memory and
|
||
> GL contexts. In loops, create the renderer **once** outside the loop.
|
||
|
||
### What the `Renderer` holds internally
|
||
|
||
When you create `mujoco.Renderer(model, height, width)`, it allocates three
|
||
internal objects that must be freed together:
|
||
|
||
1. **`GLContext`** — an offscreen OpenGL context (EGL, GLFW, or OSMesa,
|
||
auto-detected). Created with the requested `width × height`.
|
||
2. **`MjrContext`** — MuJoCo's GPU rendering resources (shaders, textures,
|
||
framebuffers), bound to the GLContext. Set to the offscreen framebuffer.
|
||
3. **`MjvScene`** — geometry buffer holding the scene snapshot passed to the
|
||
GPU each frame.
|
||
|
||
The context manager (`with Renderer(...) as r:`) calls `r.close()` on exit,
|
||
which frees the MjrContext first and then the GLContext — **order matters**.
|
||
If you use the renderer without `with`, call `renderer.close()` manually.
|
||
|
||
> [!CAUTION]
|
||
> Internally, `MjrContext.free()` must be called **before** `GLContext.free()`.
|
||
> Reversing the order leaks GPU resources or segfaults. The `Renderer` class
|
||
> handles this automatically — prefer it over manual context management.
|
||
|
||
### Offscreen framebuffer size
|
||
|
||
The renderer cannot exceed the offscreen buffer dimensions. The defaults are
|
||
640×480. Set larger buffers **before compilation** via `spec.visual`:
|
||
|
||
```python
|
||
spec.visual.global_.offwidth = 1920
|
||
spec.visual.global_.offheight = 1080
|
||
model = spec.compile()
|
||
|
||
# Now you can render up to 1920×1080
|
||
with mujoco.Renderer(model, height=1080, width=1920) as renderer:
|
||
...
|
||
```
|
||
|
||
> [!IMPORTANT]
|
||
> Increasing offscreen buffer size consumes GPU memory. For batch rendering
|
||
> of many cameras, keep the per-frame resolution modest.
|
||
|
||
### Cameras
|
||
|
||
MuJoCo has two camera systems: **fixed cameras** defined in the model, and
|
||
the **free camera** for interactive viewing.
|
||
|
||
#### Defining cameras in MjSpec
|
||
|
||
Always store the return value of `add_camera` and use its `.name` or `.id`
|
||
to reference the camera later — never hardcode literal strings:
|
||
|
||
```python
|
||
# Fixed camera on worldbody — good for evaluation/recording
|
||
overhead_cam = spec.worldbody.add_camera(
|
||
name='overhead',
|
||
pos=[0, 0, 3],
|
||
quat=[0.707, 0.707, 0, 0],
|
||
fovy=60,
|
||
)
|
||
|
||
# Camera attached to a body — moves with the body
|
||
wrist_cam = wrist_body.add_camera(
|
||
name='wrist_cam',
|
||
pos=[0.05, 0, 0],
|
||
xyaxes=[0, -1, 0, 0, 0, -1],
|
||
fovy=90,
|
||
)
|
||
```
|
||
|
||
#### Selecting a camera for rendering
|
||
|
||
`update_scene` accepts a camera **name** (str), **id** (int), or an
|
||
`MjvCamera` object. Always derive from the spec element:
|
||
|
||
```python
|
||
# By name via spec element (recommended — survives recompilation)
|
||
renderer.update_scene(data, camera=overhead_cam.name)
|
||
|
||
# By id via spec element (after compile; matches model.cam_* arrays)
|
||
renderer.update_scene(data, camera=overhead_cam.id)
|
||
|
||
# Free camera (default) — no camera argument needed
|
||
renderer.update_scene(data)
|
||
|
||
# Custom free camera with explicit lookat/distance/angles
|
||
cam = mujoco.MjvCamera()
|
||
cam.type = mujoco.mjtCamera.mjCAMERA_FREE
|
||
cam.lookat[:] = [0, 0, 0.5]
|
||
cam.distance = 3.0
|
||
cam.azimuth = 135
|
||
cam.elevation = -25
|
||
renderer.update_scene(data, camera=cam)
|
||
```
|
||
|
||
#### Camera properties reference
|
||
|
||
| Property | Type | Description |
|
||
|----------|------|-------------|
|
||
| `pos` | `real(3)` | Position in parent body frame |
|
||
| `quat` | `real(4)` | Orientation quaternion (w, x, y, z) |
|
||
| `xyaxes` | `real(6)` | Alternative orientation: `[x_axis(3), y_axis(3)]` |
|
||
| `fovy` | `real` | Vertical field of view (degrees, default 45) |
|
||
| `resolution` | `int(2)` | Sensor resolution — only for camera-based sensors |
|
||
| `targetbody` | `str` | Track this body (camera always looks at it) |
|
||
| `mode` | `str` | `"fixed"`, `"track"`, `"trackcom"`, `"targetbody"`, `"targetbodycom"` |
|
||
|
||
### Scene options
|
||
|
||
Control what is visualized via `MjvOption`:
|
||
|
||
```python
|
||
scene_option = mujoco.MjvOption()
|
||
# geomgroup is a bool array indexed by group number (0–5).
|
||
# Each geom's `group` attribute (default 0) assigns it to a group.
|
||
# Toggle visibility of each group:
|
||
scene_option.geomgroup[:] = False # hide all groups
|
||
scene_option.geomgroup[0] = True # show group 0 (e.g. ground plane)
|
||
scene_option.geomgroup[3] = True # show group 3 (e.g. visualization geoms)
|
||
|
||
# Toggle rendering flags
|
||
scene_option.flags[mujoco.mjtVisFlag.mjVIS_CONTACTFORCE] = True
|
||
scene_option.flags[mujoco.mjtVisFlag.mjVIS_JOINT] = True
|
||
|
||
renderer.update_scene(data, camera=overhead_cam.name, scene_option=scene_option)
|
||
```
|
||
|
||
### Filament backend (experimental)
|
||
|
||
MuJoCo's default renderer uses OpenGL. An alternative **Filament** backend
|
||
(Vulkan-based) is available experimentally and provides higher-quality
|
||
rendering. Filament does **not** require vertical flip (`np.flipud` is a
|
||
no-op). It is selected via build flags — see the MuJoCo Filament
|
||
[source](../src/experimental/filament) for details.
|
||
|
||
---
|
||
|
||
## Key References
|
||
|
||
### Documentation
|
||
|
||
| Document | Description |
|
||
|----------|-------------|
|
||
| [XMLreference.rst](XMLreference.rst) | Complete MJCF XML element and attribute reference |
|
||
| [python.rst](python.rst) | Python bindings API: named access, bind, enums, callbacks |
|
||
| [modeling.rst](modeling.rst) | MJCF modeling guide: coordinate frames, defaults, attachments |
|
||
| [simulation.rst](programming/simulation.rst) | Simulation loop, state, forward/inverse dynamics |
|
||
| [modeledit.rst](programming/modeledit.rst) | Procedural model editing with MjSpec |
|
||
| [visualization.rst](programming/visualization.rst) | Rendering, cameras, scene management |
|
||
| [APIfunctions.rst](APIreference/APIfunctions.rst) | C API function reference (mj_, mju_, mjv_, mjr_) |
|
||
| [APItypes.rst](APIreference/APItypes.rst) | All MuJoCo structs and enums |
|
||
|
||
### Test Files (Executable Examples)
|
||
|
||
| Test file | Key patterns demonstrated |
|
||
|-----------|--------------------------|
|
||
| [specs_test.py](../../py/mujoco/specs_test.py) | MjSpec API: compile, recompile, attach, bind, defaults, delete, actuator shortcuts |
|
||
| [bindings_test.py](../../py/mujoco/bindings_test.py) | Named indexing, mju_ functions, copy/pickle, contacts, mj_step |
|
||
| [support_test.py](../../py/mujoco/mjx/_src/support_test.py) | MJX bind `.set()` pattern, JAX functional updates |
|
||
|
||
### Source Code
|
||
|
||
| File | Description |
|
||
|------|-------------|
|
||
| [mujoco.h](../include/mujoco.h) | Main C API header with all mju_ function signatures |
|
||
| [XMLschema.rst](XMLschema.rst) | Schema-level XML structure documentation |
|