Add protocols for authoring simulation environments. This is for preview only, we discourage users from using this in production code.
PiperOrigin-RevId: 897857927 Change-Id: I82ce0a23465429e7025334e83ba00dfbe5085649
This commit is contained in:
+876
@@ -0,0 +1,876 @@
|
||||
---
|
||||
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 |
|
||||
Reference in New Issue
Block a user