← Back to Blog
simulationsimulationmujocotutorial

Getting Started with MuJoCo: From Installation to First Robot Simulation

Hands-on MuJoCo tutorial — installation, MJCF model creation, robot arm simulation, and visualization with Python.

Nguyen Anh Tuan30 tháng 3, 202610 min read
Getting Started with MuJoCo: From Installation to First Robot Simulation

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:

Robot arm simulation in MuJoCo environment

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

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:

MuJoCo viewer interface with robot arm visualization

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:

Summary

In this article, you learned:

  1. Install MuJoCo with just pip install mujoco
  2. MJCF format: body, joint, geom, actuator, sensor
  3. Create 3-DOF robot arm from scratch with XML
  4. Simulate and read data in Python
  5. Visualize with matplotlib and interactive viewer
  6. PD controller — control robot moving to target
  7. 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

Related Posts

Deep DiveDigital Twins và ROS 2: Simulation trong sản xuất
simulationros2digital-twinPart 6

Digital Twins và ROS 2: Simulation trong sản xuất

Ứng dụng simulation trong công nghiệp — digital twins, ROS 2 + Gazebo/Isaac integration cho nhà máy thông minh.

3/4/202611 min read
TutorialSim-to-Real Pipeline: Từ training đến robot thật
simulationsim2realtutorialPart 5

Sim-to-Real Pipeline: Từ training đến robot thật

End-to-end guide: train policy trong sim, evaluate, domain randomization, deploy lên robot thật và iterate.

2/4/202615 min read
TutorialNVIDIA Isaac Lab: GPU-accelerated RL training từ zero
simulationisaac-simrlPart 3

NVIDIA Isaac Lab: GPU-accelerated RL training từ zero

Setup Isaac Lab, train locomotion policy với 4096 parallel environments và domain randomization trên GPU.

1/4/202611 min read