| name | mujoco-python |
| description | Guide for building and controlling MuJoCo physics simulations in Python. Use this skill whenever the user wants to: create or run a MuJoCo simulation, load an MJCF/XML model, step physics, render frames or take screenshots (offscreen or with viewer), read sensors or joint positions, control joints or actuators, configure the camera, change visualization flags or geom groups, or tune solver/integrator settings. Also trigger when the user mentions mujoco, mjcf, humanoid simulation, robot simulation in mujoco, or physics rendering with mujoco — even if they don't say "MuJoCo" explicitly but describe tasks like "simulate a robot", "render a physics scene", or "read joint angles from the sim".
|
MuJoCo Python Skill
This skill covers the MuJoCo native Python bindings (pip install mujoco).
For installation details, read references/SETUP.md in this skill directory.
For the most up-to-date API reference, consult: https://mujoco.readthedocs.io/en/stable/python.html
Agent Control (persistent simulation)
For interactive, persistent control of a running simulation (step, screenshot, read sensors
without restarting), two approaches are provided:
- HTTP API (
scripts/sim_server.py + scripts/sim_client.py) — start a background server,
control it from Python scripts or CLI. Zero extra deps. See references/HTTP_API.md.
- MCP Server (
scripts/sim_mcp.py) — register as an MCP server so tools appear natively.
Requires pip install mcp. See references/MCP_SERVER.md.
The HTTP approach is best for ad-hoc use: start the server, write a Python script using
SimClient to batch multiple operations (step, screenshot, read sensors) in one call.
The MCP approach is best for persistent setups where you always want simulation tools available.
The server architecture is two classes:
SimApp — generic HTTP micro-framework with @app.get() / @app.post() decorators.
MujocoSimApp(SimApp) — adds all MuJoCo routes. Accepts scene_path or existing model/data.
Custom routes can be added, and built-in routes overridden, via decorators. See references/HTTP_API.md.
Quick-Start Pattern
Every MuJoCo script follows the same skeleton:
import mujoco
import numpy as np
model = mujoco.MjModel.from_xml_path("scene.xml")
data = mujoco.MjData(model)
model.opt.timestep = 0.002
while data.time < duration:
data.ctrl[:] = compute_controls(model, data)
mujoco.mj_step(model, data)
with mujoco.Renderer(model, height=480, width=640) as renderer:
renderer.update_scene(data)
pixels = renderer.render()
Example Model
For a ready-made humanoid, use the Unitree G1 from MuJoCo Menagerie:
https://raw.githubusercontent.com/google-deepmind/mujoco_menagerie/refs/heads/main/unitree_g1/scene_with_hands.xml
This model references other files (meshes, textures) so it must be cloned locally:
git clone https://github.com/google-deepmind/mujoco_menagerie.git
Core Concepts
model vs data
MjModel — static description: bodies, joints, actuators, geometry, solver options. Immutable during simulation (except model.opt and a few tunables).
MjData — mutable runtime state: positions, velocities, forces, sensor readings. Changes every step.
Key size fields on model
| Field | Meaning |
|---|
model.nq | Generalized position DOFs |
model.nv | Generalized velocity DOFs |
model.nu | Number of actuators |
model.nbody | Number of bodies |
model.njnt | Number of joints |
model.nsensor | Number of sensors |
Freejoint convention
A floating-base robot has a freejoint as its first joint. This means:
qpos[0:3] = position (x, y, z)
qpos[3:7] = orientation quaternion (w, x, y, z)
qpos[7:] = joint angles
qvel[0:3] = linear velocity
qvel[3:6] = angular velocity
qvel[6:] = joint velocities
Loading Models
model = mujoco.MjModel.from_xml_path("/path/to/scene.xml")
model = mujoco.MjModel.from_xml_string(xml_string, assets={"mesh.stl": mesh_bytes})
model = mujoco.MjModel.from_binary_path("/path/to/model.mjb")
Stepping and Resetting
mujoco.mj_step(model, data)
mujoco.mj_step(model, data, nstep=20)
mujoco.mj_forward(model, data)
mujoco.mj_resetData(model, data)
mujoco.mj_resetDataKeyframe(model, data, key_id)
Split-stepping (set controls between constraint and integration phases):
mujoco.mj_step1(model, data)
data.ctrl[:] = my_controls
mujoco.mj_step2(model, data)
Reading Joint Positions and Velocities
all_qpos = data.qpos.copy()
all_qvel = data.qvel.copy()
q = data.joint("left_knee").qpos[0]
dq = data.joint("left_knee").qvel[0]
jnt_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_JOINT, "left_knee")
qpos_adr = model.jnt_qposadr[jnt_id]
qvel_adr = model.jnt_dofadr[jnt_id]
Controlling Joints / Actuators
data.ctrl[:] = np.zeros(model.nu)
data.ctrl[0] = 1.0
data.qfrc_applied[dof_index] = torque
data.xfrc_applied[body_id] = [fx, fy, fz, tx, ty, tz]
data.ctrl[:] = data.qfrc_bias[6:]
PD control pattern
def pd_control(model, data, target_qpos, kp=100.0, kd=10.0):
"""Simple PD controller for all actuated joints."""
free = model.nq > model.nu
qoff = 7 if free else 0
voff = 6 if free else 0
for i in range(model.nu):
q = data.qpos[qoff + i]
dq = data.qvel[voff + i]
data.ctrl[i] = kp * (target_qpos[i] - q) + kd * (0.0 - dq)
Reading Sensors
all_sensors = data.sensordata.copy()
imu_data = data.sensor("imu_gyro").data.copy()
sensor_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_SENSOR, "imu_gyro")
Camera Control
cam = mujoco.MjvCamera()
cam.type = mujoco.mjtCamera.mjCAMERA_FREE
cam.lookat[:] = [0, 0, 1.0]
cam.distance = 3.0
cam.azimuth = 90
cam.elevation = -20
cam.type = mujoco.mjtCamera.mjCAMERA_FIXED
cam.fixedcamid = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_CAMERA, "front_cam")
cam.type = mujoco.mjtCamera.mjCAMERA_TRACKING
cam.trackbodyid = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_BODY, "torso")
Offscreen Rendering and Screenshots
The mujoco.Renderer class handles offscreen rendering without needing a window.
from PIL import Image
with mujoco.Renderer(model, height=720, width=1280) as renderer:
renderer.update_scene(data)
renderer.update_scene(data, camera="front_cam")
renderer.update_scene(data, camera=cam)
pixels = renderer.render()
Image.fromarray(pixels).save("screenshot.png")
renderer.enable_depth_rendering()
renderer.update_scene(data)
depth = renderer.render()
renderer.disable_depth_rendering()
renderer.enable_segmentation_rendering()
renderer.update_scene(data)
seg = renderer.render()
renderer.disable_segmentation_rendering()
With visualization options
opt = mujoco.MjvOption()
opt.flags[mujoco.mjtVisFlag.mjVIS_JOINT] = 1
opt.flags[mujoco.mjtVisFlag.mjVIS_CONTACTPOINT] = 1
renderer.update_scene(data, camera=cam, scene_option=opt)
pixels = renderer.render()
Interactive Viewer
import mujoco.viewer
mujoco.viewer.launch(model, data)
with mujoco.viewer.launch_passive(model, data) as viewer:
while viewer.is_running():
data.ctrl[:] = compute_controls(model, data)
mujoco.mj_step(model, data)
viewer.sync()
with viewer.lock():
viewer.opt.flags[mujoco.mjtVisFlag.mjVIS_CONTACTPOINT] = 1
Quitting the simulation
viewer.close() — programmatically close the viewer window
viewer.is_running() — returns False when the user closes the window
- Set a duration and
break out of the loop
Visualization Flags and Groups
Visualization flags (MjvOption.flags)
Toggle what's visible in the scene. Key flags:
| Flag | What it shows |
|---|
mjVIS_JOINT | Joint axes |
mjVIS_ACTUATOR | Actuator indicators |
mjVIS_CONTACTPOINT | Contact points |
mjVIS_CONTACTFORCE | Contact force vectors |
mjVIS_COM | Center of mass |
mjVIS_TRANSPARENT | Transparent bodies |
mjVIS_CONVEXHULL | Convex hull of meshes |
mjVIS_TEXTURE | Textures on/off |
mjVIS_TENDON | Tendon lines |
mjVIS_INERTIA | Inertia boxes |
mjVIS_CONSTRAINT | Constraint indicators |
opt = mujoco.MjvOption()
opt.flags[mujoco.mjtVisFlag.mjVIS_CONTACTPOINT] = 1
opt.flags[mujoco.mjtVisFlag.mjVIS_JOINT] = 1
opt.flags[mujoco.mjtVisFlag.mjVIS_TRANSPARENT] = 1
Render flags (scene-level)
renderer.scene.flags[mujoco.mjtRndFlag.mjRND_WIREFRAME] = 1
renderer.scene.flags[mujoco.mjtRndFlag.mjRND_SHADOW] = 0
renderer.scene.flags[mujoco.mjtRndFlag.mjRND_REFLECTION] = 0
Geom / Site / Joint Groups
MuJoCo assigns geoms, sites, joints to groups 0-5. Toggle group visibility:
opt = mujoco.MjvOption()
opt.geomgroup[:] = [1, 1, 0, 0, 0, 0]
opt.sitegroup[:] = [1, 0, 0, 0, 0, 0]
opt.jointgroup[:] = [1, 1, 1, 0, 0, 0]
Solver and Algorithm Configuration
All physics algorithm settings live in model.opt (an mjOption struct).
Integrator
model.opt.integrator = mujoco.mjtIntegrator.mjINT_IMPLICITFAST
model.opt.integrator = mujoco.mjtIntegrator.mjINT_EULER
model.opt.integrator = mujoco.mjtIntegrator.mjINT_RK4
model.opt.integrator = mujoco.mjtIntegrator.mjINT_IMPLICIT
Solver
model.opt.solver = mujoco.mjtSolver.mjSOL_NEWTON
model.opt.solver = mujoco.mjtSolver.mjSOL_PGS
model.opt.solver = mujoco.mjtSolver.mjSOL_CG
Common parameters
model.opt.timestep = 0.002
model.opt.iterations = 100
model.opt.tolerance = 1e-10
model.opt.gravity[:] = [0, 0, -9.81]
model.opt.impratio = 1.0
model.opt.density = 0.0
model.opt.viscosity = 0.0
model.opt.wind[:] = [0, 0, 0]
Noslip solver (for better friction)
model.opt.noslip_iterations = 20
model.opt.noslip_tolerance = 1e-6
Common Patterns
Headless simulation with logging
import mujoco
import numpy as np
import json
model = mujoco.MjModel.from_xml_path("scene.xml")
data = mujoco.MjData(model)
log = {"time": [], "qpos": [], "qvel": [], "ctrl": []}
duration = 5.0
while data.time < duration:
data.ctrl[:] = np.zeros(model.nu)
mujoco.mj_step(model, data)
log["time"].append(float(data.time))
log["qpos"].append(data.qpos.copy().tolist())
log["qvel"].append(data.qvel.copy().tolist())
log["ctrl"].append(data.ctrl.copy().tolist())
with open("sim_log.json", "w") as f:
json.dump(log, f)
Multi-camera screenshot sweep
with mujoco.Renderer(model, height=480, width=640) as renderer:
for az in [0, 90, 180, 270]:
cam = mujoco.MjvCamera()
cam.lookat[:] = [0, 0, 1]
cam.distance = 3.0
cam.azimuth = az
cam.elevation = -20
renderer.update_scene(data, camera=cam)
Image.fromarray(renderer.render()).save(f"view_{az}.png")
Body position tracking
body_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_BODY, "torso")
pos = data.xpos[body_id].copy()
quat = data.xquat[body_id].copy()