MuJoCo — Most-Used Physics Simulator for Robot Learning
In Part 1 of this series, I compared MuJoCo, Isaac Sim, and Gazebo. This article will be a hands-on tutorial — from installing MuJoCo to creating a robot arm model, simulating, and visualizing results with Python.
MuJoCo (Multi-Joint dynamics with Contact) is Google DeepMind's physics engine, used in most papers on robot manipulation and locomotion learning. If you want to do reinforcement learning for robotics, this is where to start.
Step 1: Install MuJoCo
MuJoCo is extremely simple to install via pip:
# Create virtual environment
python -m venv mujoco-env
source mujoco-env/bin/activate
# Install MuJoCo + dependencies
pip install mujoco numpy matplotlib
# Verify installation
python -c "import mujoco; print(f'MuJoCo {mujoco.__version__} installed successfully!')"
On Ubuntu, if you want the interactive viewer:
# Rendering dependencies (Ubuntu)
sudo apt-get install libgl1-mesa-glx libglew-dev
On macOS and Windows, viewer works out-of-the-box.
Step 2: Understand MJCF — Robot Description Language
MuJoCo uses MJCF (MuJoCo XML Format) to define robots and environments. This is the key difference from URDF (used in ROS) — MJCF is more expressive and optimized for simulation.
Basic MJCF Structure
<mujoco model="example">
<!-- Compiler settings -->
<compiler angle="radian"/>
<!-- Default values for elements -->
<default>
<joint damping="0.5"/>
<geom rgba="0.8 0.6 0.4 1"/>
</default>
<!-- Scene: lights, floor, objects -->
<worldbody>
<light diffuse="1 1 1" pos="0 0 3"/>
<geom type="plane" size="2 2 0.1" rgba="0.9 0.9 0.9 1"/>
<!-- Robot defined here -->
<body name="link1" pos="0 0 0.5">
<joint name="joint1" type="hinge" axis="0 0 1"/>
<geom type="capsule" size="0.05" fromto="0 0 0 0 0 0.4"/>
</body>
</worldbody>
<!-- Actuators (motors) -->
<actuator>
<motor joint="joint1" ctrlrange="-1 1"/>
</actuator>
</mujoco>
Main elements:
<body>: Robot link, can be nested (parent-child)<joint>: Joint between bodies (hinge, slide, ball, free)<geom>: Physics shape (capsule, box, sphere, mesh)<actuator>: Motor controlling joints<sensor>: Sensors (position, velocity, force, torque...)
Step 3: Create 3-DOF Robot Arm
Let's create a simple 3-joint robot arm — simple enough to understand but complex enough for real work.
Save as robot_arm_3dof.xml:
<mujoco model="robot_arm_3dof">
<compiler angle="radian" autolimits="true"/>
<option gravity="0 0 -9.81" timestep="0.002"/>
<default>
<joint damping="2.0" armature="0.1"/>
<geom contype="1" conaffinity="1" friction="1 0.005 0.001"
rgba="0.7 0.3 0.1 1"/>
<motor ctrlrange="-5 5" ctrllimited="true"/>
</default>
<asset>
<texture type="skybox" builtin="gradient" rgb1="0.3 0.5 0.7"
rgb2="0 0 0" width="512" height="512"/>
<texture name="grid" type="2d" builtin="checker" width="512"
height="512" rgb1="0.9 0.9 0.9" rgb2="0.8 0.8 0.8"/>
<material name="grid" texture="grid" texrepeat="8 8"/>
</asset>
<worldbody>
<!-- Lights -->
<light pos="0 0 3" dir="0 0 -1" diffuse="1 1 1"/>
<light pos="2 2 3" dir="-1 -1 -1" diffuse="0.5 0.5 0.5"/>
<!-- Floor -->
<geom type="plane" size="2 2 0.01" material="grid"
contype="1" conaffinity="1"/>
<!-- Base (fixed to world) -->
<body name="base" pos="0 0 0">
<geom type="cylinder" size="0.12 0.05" rgba="0.3 0.3 0.3 1"
mass="5"/>
<!-- Link 1: Shoulder rotation (Z-axis) -->
<body name="link1" pos="0 0 0.05">
<joint name="shoulder_yaw" type="hinge" axis="0 0 1"
range="-3.14 3.14"/>
<geom type="capsule" fromto="0 0 0 0 0 0.3" size="0.04"
rgba="0.2 0.5 0.8 1" mass="1"/>
<!-- Link 2: Shoulder pitch (Y-axis) -->
<body name="link2" pos="0 0 0.3">
<joint name="shoulder_pitch" type="hinge" axis="0 1 0"
range="-1.57 1.57"/>
<geom type="capsule" fromto="0 0 0 0.3 0 0" size="0.035"
rgba="0.8 0.5 0.2 1" mass="0.8"/>
<!-- Link 3: Elbow (Y-axis) -->
<body name="link3" pos="0.3 0 0">
<joint name="elbow" type="hinge" axis="0 1 0"
range="-2.35 0"/>
<geom type="capsule" fromto="0 0 0 0.25 0 0" size="0.03"
rgba="0.2 0.8 0.3 1" mass="0.5"/>
<!-- End effector -->
<site name="end_effector" pos="0.25 0 0" size="0.02"
rgba="1 0 0 1"/>
<!-- Sensors at end effector -->
<body name="ee_body" pos="0.25 0 0">
<geom type="sphere" size="0.025" rgba="1 0.2 0.2 1"
mass="0.1"/>
</body>
</body>
</body>
</body>
</body>
<!-- Target object -->
<body name="target" pos="0.3 0.2 0.3">
<geom type="sphere" size="0.03" rgba="0 1 0 0.5"
contype="0" conaffinity="0"/>
</body>
</worldbody>
<!-- Actuators -->
<actuator>
<motor name="shoulder_yaw_motor" joint="shoulder_yaw" gear="50"/>
<motor name="shoulder_pitch_motor" joint="shoulder_pitch" gear="50"/>
<motor name="elbow_motor" joint="elbow" gear="30"/>
</actuator>
<!-- Sensors -->
<sensor>
<jointpos name="shoulder_yaw_pos" joint="shoulder_yaw"/>
<jointpos name="shoulder_pitch_pos" joint="shoulder_pitch"/>
<jointpos name="elbow_pos" joint="elbow"/>
<jointvel name="shoulder_yaw_vel" joint="shoulder_yaw"/>
<jointvel name="shoulder_pitch_vel" joint="shoulder_pitch"/>
<jointvel name="elbow_vel" joint="elbow"/>
<framepos name="ee_pos" objtype="site" objname="end_effector"/>
</sensor>
</mujoco>
Model Explanation
- 3 joints:
shoulder_yaw(rotate around Z),shoulder_pitch(rotate around Y),elbow(rotate around Y) - Joint limits: Realistic angle ranges, e.g., elbow only bends one way
- Damping: Simulate joint friction, prevents oscillation
- Armature: Motor rotor inertia, adds stability
- Sensors: Read joint position, velocity, and end-effector position in 3D space
- Target: Green sphere for robot to aim at (no collision)
Step 4: Load and Simulate in Python
"""
MuJoCo Robot Arm Simulation
Load model, simulate, and print results.
"""
import mujoco
import numpy as np
# --- Load model ---
model = mujoco.MjModel.from_xml_path("robot_arm_3dof.xml")
data = mujoco.MjData(model)
# Print model info
print(f"Model: {model.nq} DoF, {model.nu} actuators, {model.nsensor} sensors")
print(f"Timestep: {model.opt.timestep}s")
print(f"Joint names: {[model.joint(i).name for i in range(model.njnt)]}")
print(f"Actuator names: {[model.actuator(i).name for i in range(model.nu)]}")
# --- Simulate with constant control ---
# Set control signal (torque for each motor)
data.ctrl[0] = 0.5 # shoulder_yaw: rotate gently
data.ctrl[1] = -0.3 # shoulder_pitch: lean down
data.ctrl[2] = -0.2 # elbow: fold in
# Simulate 2 seconds (1000 steps x 0.002s timestep)
n_steps = 1000
positions = np.zeros((n_steps, model.nq))
ee_positions = np.zeros((n_steps, 3))
for i in range(n_steps):
mujoco.mj_step(model, data)
# Save joint positions
positions[i] = data.qpos.copy()
# Save end-effector position (from sensor)
ee_positions[i] = data.sensordata[6:9].copy()
# Print final results
print(f"\nAfter {n_steps} steps ({n_steps * model.opt.timestep}s):")
print(f" Joint positions: {data.qpos}")
print(f" End-effector XYZ: {data.sensordata[6:9]}")
print(f" Joint velocities: {data.qvel}")
Step 5: Visualize Results with Matplotlib
"""
Visualize simulation results.
"""
import matplotlib.pyplot as plt
import numpy as np
# --- (Run simulation code above first) ---
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# Plot 1: Joint positions over time
time = np.arange(n_steps) * model.opt.timestep
ax = axes[0, 0]
joint_names = ["shoulder_yaw", "shoulder_pitch", "elbow"]
for j in range(3):
ax.plot(time, np.degrees(positions[:, j]), label=joint_names[j])
ax.set_xlabel("Time (s)")
ax.set_ylabel("Joint angle (degrees)")
ax.set_title("Joint Positions")
ax.legend()
ax.grid(True, alpha=0.3)
# Plot 2: End-effector trajectory 3D
ax3d = fig.add_subplot(2, 2, 2, projection="3d")
ax3d.plot(ee_positions[:, 0], ee_positions[:, 1], ee_positions[:, 2],
"b-", alpha=0.7, label="EE path")
ax3d.scatter(*ee_positions[0], c="green", s=100, label="Start")
ax3d.scatter(*ee_positions[-1], c="red", s=100, label="End")
ax3d.set_xlabel("X (m)")
ax3d.set_ylabel("Y (m)")
ax3d.set_zlabel("Z (m)")
ax3d.set_title("End-Effector Trajectory")
ax3d.legend()
# Plot 3: End-effector XY plane
ax = axes[1, 0]
ax.plot(ee_positions[:, 0], ee_positions[:, 1], "b-", alpha=0.7)
ax.scatter(ee_positions[0, 0], ee_positions[0, 1], c="green", s=100,
zorder=5, label="Start")
ax.scatter(ee_positions[-1, 0], ee_positions[-1, 1], c="red", s=100,
zorder=5, label="End")
ax.set_xlabel("X (m)")
ax.set_ylabel("Y (m)")
ax.set_title("EE Trajectory (top view)")
ax.legend()
ax.grid(True, alpha=0.3)
ax.set_aspect("equal")
# Plot 4: Joint velocities
ax = axes[1, 1]
# Re-simulate to get velocity data
data_v = mujoco.MjData(model)
data_v.ctrl[:] = [0.5, -0.3, -0.2]
velocities = np.zeros((n_steps, model.nv))
for i in range(n_steps):
mujoco.mj_step(model, data_v)
velocities[i] = data_v.qvel.copy()
for j in range(3):
ax.plot(time, velocities[:, j], label=joint_names[j])
ax.set_xlabel("Time (s)")
ax.set_ylabel("Velocity (rad/s)")
ax.set_title("Joint Velocities")
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig("simulation_results.png", dpi=150, bbox_inches="tight")
plt.show()
print("Saved: simulation_results.png")
Step 6: Interactive Viewer
MuJoCo has a built-in viewer for direct interaction:
"""
MuJoCo Interactive Viewer
Drag/drop robot, pause/resume, change camera.
"""
import mujoco
import mujoco.viewer
import numpy as np
import time
def simple_controller(model, data):
"""Controller called each simulation step."""
t = data.time
# Sine wave control — robot moves in pattern
data.ctrl[0] = 1.0 * np.sin(0.5 * t) # shoulder_yaw
data.ctrl[1] = -0.5 * np.sin(0.3 * t + 1.0) # shoulder_pitch
data.ctrl[2] = -0.3 * np.sin(0.7 * t + 0.5) # elbow
model = mujoco.MjModel.from_xml_path("robot_arm_3dof.xml")
data = mujoco.MjData(model)
# Launch interactive viewer with controller callback
with mujoco.viewer.launch_passive(model, data) as viewer:
start = time.time()
while viewer.is_running() and time.time() - start < 30:
step_start = time.time()
# Apply control
simple_controller(model, data)
# Step simulation
mujoco.mj_step(model, data)
# Sync viewer
viewer.sync()
# Realtime: wait for enough timestep
dt = model.opt.timestep - (time.time() - step_start)
if dt > 0:
time.sleep(dt)
print("Viewer closed.")
In the viewer, you can:
- Click + drag to rotate camera
- Scroll to zoom
- Double-click on body to select
- Ctrl + click to apply force (drag robot)
- Space to pause/resume
- Backspace to reset
Step 7: Read Sensor Data and PD Control
Instead of sending torque directly, implement PD controller — the most common control method for robot arms:
"""
PD Controller for robot arm.
Move end-effector to target position.
"""
import mujoco
import numpy as np
def pd_controller(model, data, target_qpos, kp=50.0, kd=5.0):
"""
PD control: tau = Kp * (q_target - q) - Kd * dq
Args:
target_qpos: Target joint positions (3,)
kp: Proportional gain
kd: Derivative gain
Returns:
Control torques (3,)
"""
q = data.qpos[:3] # Current joint positions
dq = data.qvel[:3] # Current joint velocities
# PD control law
torque = kp * (target_qpos - q) - kd * dq
# Clamp to actuator limits
torque = np.clip(torque, -5.0, 5.0)
return torque
# Load model
model = mujoco.MjModel.from_xml_path("robot_arm_3dof.xml")
data = mujoco.MjData(model)
# Waypoints — robot navigates through each
waypoints = [
np.array([0.0, -0.5, -1.0]), # Reach forward
np.array([1.0, -0.3, -0.5]), # Turn right
np.array([-1.0, -0.3, -0.5]), # Turn left
np.array([0.0, 0.0, 0.0]), # Return home
]
# Simulate
current_wp = 0
target = waypoints[current_wp]
step_count = 0
switch_every = 1500 # Change waypoint every 3 seconds
print("=== PD Controller Simulation ===")
print(f"Waypoints: {len(waypoints)}")
for step in range(switch_every * len(waypoints)):
# Switch waypoint
if step > 0 and step % switch_every == 0:
current_wp = (current_wp + 1) % len(waypoints)
target = waypoints[current_wp]
print(f"\n[Step {step}] Switching to waypoint {current_wp}: {target}")
# Compute and apply PD control
data.ctrl[:3] = pd_controller(model, data, target, kp=50.0, kd=5.0)
# Step simulation
mujoco.mj_step(model, data)
# Print progress every 500 steps
if step % 500 == 0:
error = np.linalg.norm(target - data.qpos[:3])
ee_pos = data.sensordata[6:9]
print(f" Step {step:5d} | Error: {error:.4f} rad | "
f"EE pos: [{ee_pos[0]:.3f}, {ee_pos[1]:.3f}, {ee_pos[2]:.3f}]")
print("\nDone!")
Load Model from MuJoCo Menagerie
Instead of creating models yourself, use MuJoCo Menagerie — collection of high-quality robot models:
# Clone MuJoCo Menagerie
git clone https://github.com/google-deepmind/mujoco_menagerie.git
"""
Load robot from MuJoCo Menagerie.
Example: Franka Emika Panda arm
"""
import mujoco
import mujoco.viewer
import numpy as np
import time
# Load Franka Panda from menagerie
model = mujoco.MjModel.from_xml_path(
"mujoco_menagerie/franka_emika_panda/panda.xml"
)
data = mujoco.MjData(model)
print(f"Franka Panda: {model.nq} DoF, {model.nu} actuators")
print(f"Joint names:")
for i in range(model.njnt):
jnt = model.joint(i)
print(f" {i}: {jnt.name} (range: {model.jnt_range[i]})")
# Sine wave control demo
with mujoco.viewer.launch_passive(model, data) as viewer:
start = time.time()
while viewer.is_running() and time.time() - start < 20:
t = data.time
# Gentle motion
for i in range(min(model.nu, 7)):
data.ctrl[i] = 0.3 * np.sin(0.5 * t + i * 0.5)
mujoco.mj_step(model, data)
viewer.sync()
dt = model.opt.timestep - (time.time() - time.time())
if dt > 0:
time.sleep(max(0, model.opt.timestep - 0.0001))
Menagerie includes robots:
- Arms: Franka Panda, UR5e, KUKA iiwa, xArm7
- Hands: Shadow Hand, Allegro Hand
- Quadrupeds: Unitree Go1/Go2, ANYmal
- Humanoids: Unitree H1, Google ALOHA
- Grippers: Robotiq 2F-85
Summary
In this article, you learned:
- Install MuJoCo with just
pip install mujoco - MJCF format: body, joint, geom, actuator, sensor
- Create 3-DOF robot arm from scratch with XML
- Simulate and read data in Python
- Visualize with matplotlib and interactive viewer
- PD controller — control robot moving to target
- MuJoCo Menagerie — use pre-built robot models
MuJoCo is ideal for starting robot simulation because it's lightweight, fast, and has a simple API. From here, you can advance to RL training with Gymnasium/MuJoCo environments or GPU-accelerated training with MJX.
In Part 3, we'll switch to NVIDIA Isaac Lab — train locomotion policy with 4,096 parallel environments on GPU.
Related Articles
- Simulation for Robotics: MuJoCo vs Isaac Sim vs Gazebo — Compare 3 leading simulators
- NVIDIA Isaac Lab: GPU-Accelerated RL Training from Zero — Train locomotion with massive parallelism
- Sim-to-Real Transfer: Train simulation, run reality — Domain randomization and sim-to-real best practices
- Python for Robot Control — Python fundamentals in robotics
- Inverse Kinematics for 6-DOF Robot Arm — From joint space to task space