[!TIP]
Related skills:
- mujoco-python — Simulation lifecycle, stepping, and diagnostics.
- mujoco-rendering — Offscreen image rendering and camera setup.
- mujoco-studio — Interactive viewers and Studio plugins.
1. Procedural Modeling with MjSpec
MjSpec is a mutable C++ and Python data structure that mirrors the MJCF XML
schema, allowing models to be constructed and modified programmatically.
import mujoco
spec = mujoco.MjSpec()
# Add a body with a freejoint and sphere geom
body = spec.worldbody.add_body(name='ball', pos=[0, 0, 1.0])
body.add_freejoint()
body.add_geom(type=mujoco.mjtGeom.mjGEOM_SPHERE, size=[0.1], rgba=[1, 0, 0, 1])
# Compile to MjModel
model = spec.compile()
In C++:
#include <mujoco/mujoco.h>
mjSpec* spec = mj_makeSpec();
mjsBody* body = mjs_addBody(mjs_findBody(spec, "world"), nullptr);
body->pos[2] = 1.0;
mjs_addFreeJoint(body);
mjsGeom* geom = mjs_addGeom(body, nullptr);
geom->type = mjGEOM_SPHERE;
geom->size[0] = 0.1;
mjModel* model = mj_compile(spec, nullptr);
if (!model) printf("%s\n", mjs_getError(spec));
mj_deleteSpec(spec);
2. Building Hierarchies: Bodies, Geoms, Joints & Sites
Adding Elements
body = spec.worldbody.add_body(name='arm', pos=[0, 0, 0.5])
# Geom with capsule shape
geom = body.add_geom(
name='arm_geom',
type=mujoco.mjtGeom.mjGEOM_CAPSULE,
size=[0.05, 0.25],
rgba=[0.2, 0.6, 1.0, 1.0],
)
# Hinge joint
joint = body.add_joint(
name='shoulder',
type=mujoco.mjtJoint.mjJNT_HINGE,
axis=[0, 1, 0],
range=[-1.57, 1.57],
)
# Sensor site and camera
site = body.add_site(name='grip_site', pos=[0, 0, 0.25])
cam = body.add_camera(name='tool_cam', pos=[0, -0.5, 0.25], xyaxes=[1, 0, 0, 0, 0, 1])
Geom Dimensions Reference
| Geom Type | size Parameter Semantics |
|---|---|
mjGEOM_SPHERE |
[radius] |
mjGEOM_CAPSULE |
[radius, half_length] (or [radius] if using fromto) |
mjGEOM_CYLINDER |
[radius, half_length] (or [radius] if using fromto) |
mjGEOM_BOX |
[half_x, half_y, half_z] |
mjGEOM_ELLIPSOID |
[radius_x, radius_y, radius_z] |
mjGEOM_PLANE |
[half_x, half_y, grid_spacing] |
[!WARNING]
When
fromto=[x1, y1, z1, x2, y2, z2]is specified for capsules or cylinders,sizetakes only one value ([radius]); length is calculated from endpoints.
Orientation Keywords
Only one orientation specification can be used at a time:
quat=[w, x, y, z](native representation)euler=[a1, a2, a3](sequence set byspec.compiler.eulerseq, default'xyz')axisangle=[x, y, z, angle]xyaxes=[x_x, x_y, x_z, y_x, y_y, y_z]zaxis=[x, y, z]
Angles in euler and axisangle are in degrees unless
spec.compiler.degree = False.
3. Sub-model Composition & Attachments
Compose models modularly by attaching sub-specs with prefix namespaces:
parent = mujoco.MjSpec()
child = mujoco.MjSpec()
child_body = child.worldbody.add_body(name='gripper')
child_body.add_geom(size=[0.05], type=mujoco.mjtGeom.mjGEOM_SPHERE)
# Attach child spec to a mount site on the parent
mount_site = parent.worldbody.add_site(name='mount', pos=[0, 0, 1.0])
parent.attach(child, site=mount_site, prefix='gripper_')
# Child body is renamed to 'gripper_gripper' in parent
Cross-Spec Equality Constraints
When linking bodies from two different child specs (e.g. dual-arm setups), attach both children to the common parent first, then declare constraints on the parent spec:
left_mount = parent.worldbody.add_frame(pos=[0, 0.3, 0])
right_mount = parent.worldbody.add_frame(pos=[0, -0.3, 0])
parent.attach(arm1_spec, frame=left_mount, prefix='left_')
parent.attach(arm2_spec, frame=right_mount, prefix='right_')
eq = parent.add_equality(
type=mujoco.mjtEq.mjEQ_CONNECT,
objtype=mujoco.mjtObj.mjOBJ_BODY,
name1='left_hand',
name2='right_hand',
)
eq.data[0:3] = [0, 0, 0] # connect anchor, in name1's body frame
[!NOTE]
attach()requires exactly one offrame=orsite=; omitting both raisesValueError.
4. Deleting & Dynamically Modifying Elements
Deleting Elements
Pass the element object to spec.delete(); deleting a body removes its subtree:
spec.delete(spec.body('unwanted_body'))
spec.delete(spec.geom('old_geom'))
spec.delete(spec.joint('redundant_joint'))
spec.delete(spec.actuator('motor'))
State-Preserving Recompilation
When changing model architecture at runtime, use spec.recompile to keep
existing physics state:
# Add new tool to robot
spec.worldbody.add_geom(name='tool_tip', size=[0.02], type=mujoco.mjtGeom.mjGEOM_SPHERE)
# Recompile keeping positions/velocities intact
model, data = spec.recompile(model, data)
5. Actuators & Servos
Always explicitly set the transmission type (trntype):
# 1. Torque Motor
motor = spec.add_actuator(
name='torque_actuator',
target='shoulder',
trntype=mujoco.mjtTrn.mjTRN_JOINT,
)
motor.set_to_motor()
# 2. Stiff Position Servo (Recommended for position control)
servo = spec.add_actuator(
name='pos_servo',
target='shoulder',
trntype=mujoco.mjtTrn.mjTRN_JOINT,
)
servo.set_to_position(kp=200.0)
servo.forcelimited = True
servo.forcerange = [-50.0, 50.0] # Peak torque in Nm
servo.ctrllimited = True
servo.ctrlrange = [-1.57, 1.57] # Target angle in radians
# 3. Velocity Actuator
vel = spec.add_actuator(
name='vel_driver',
target='wheel_joint',
trntype=mujoco.mjtTrn.mjTRN_JOINT,
)
vel.set_to_velocity(kv=10.0)
[!CAUTION]
The helper functions
set_to_position()andset_to_motor()configure gains but do not set the transmission. You must always specifytrntype=mujoco.mjtTrn.mjTRN_JOINT(or another valid transmission type).
6. Default Classes (Inheritance & Styling)
Default classes (MjsDefault) provide cascaded styling and properties:
# Create child class under root 'main' class
main_class = spec.default
rubber = spec.add_default('rubber', main_class)
rubber.geom.friction = [1.5, 0.01, 0.001]
rubber.geom.density = 1200.0
# Assign class at creation
body.add_geom(rubber, name='tire', type=mujoco.mjtGeom.mjGEOM_CYLINDER, size=[0.1, 0.05])
# Assign class post-creation (must pass MjsDefault object, NOT a string)
other_geom.classname = rubber
7. Collisions & Friction Tuning
Collision Filtering Bitmasks
Two geoms collide if and only if:
(g1.contype & g2.conaffinity) || (g2.contype & g1.conaffinity)
# Visual-only geom: never collides
body.add_geom(size=[0.1], contype=0, conaffinity=0, group=1)
# Team A collides only with Team B
geom_a = body.add_geom(size=[0.1], contype=1, conaffinity=2)
geom_b = body.add_geom(size=[0.1], contype=2, conaffinity=1)
Contact Dimension (condim) & Friction
condim |
Active Friction Components | Friction Index Used |
|---|---|---|
| 1 | Normal force only (frictionless) | — |
| 3 | Tangential sliding friction | friction[0] |
| 4 | Tangential + torsional (twisting) | friction[0:2] |
| 6 | Tangential + torsional + rolling | friction[0:3] |
# High-friction contact for grasping
finger.add_geom(
type=mujoco.mjtGeom.mjGEOM_CAPSULE,
size=[0.01, 0.03],
condim=4,
friction=[1.2, 0.02, 0.0001],
)
8. Procedural Meshes, Materials & Sensors
Procedural Meshes
vertices = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 0], [0, 0, 1]], dtype=np.float32)
faces = np.array([[0, 2, 1], [0, 1, 3], [0, 3, 2], [1, 2, 3]], dtype=np.int32)
mesh = spec.add_mesh(name='tetra', uservert=vertices.flatten(), userface=faces.flatten())
body.add_geom(type=mujoco.mjtGeom.mjGEOM_MESH, meshname='tetra')
Procedural Sensors
# IMU Accelerometer and Gyroscope
spec.add_sensor(name='torso_acc', type=mujoco.mjtSensor.mjSENS_ACCELEROMETER,
objtype=mujoco.mjtObj.mjOBJ_SITE, objname='imu_site')
spec.add_sensor(name='torso_gyro', type=mujoco.mjtSensor.mjSENS_GYRO,
objtype=mujoco.mjtObj.mjOBJ_SITE, objname='imu_site')
9. Common Gotchas & Best Practices
- Non-zero geom sizes: Geoms with radius or half-extents of zero cause immediate compilation errors.
classnametakes an object:geom.classname = 'rubber'raises aTypeError. Pass theMjsDefaultinstance:geom.classname = rubber.- Always assign
recompile():spec.recompile(m, d)produces newly allocated model and data instances. Reassign them:m, d = spec.recompile(m, d).