{
"cells": [
{
"cell_type": "markdown",
"id": "bbd9b37d",
"metadata": {},
"source": [
"[](https://colab.research.google.com/github/tayalmanan28/MuJoCo-Tutorial/blob/main/tutorial/06_xml_modeling.ipynb)\n",
"\n",
"# Tutorial 6: MJCF Modeling Guide\n",
"\n",
"Build custom robots and environments in MuJoCo's XML format."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "add7be57",
"metadata": {},
"outputs": [],
"source": [
"!pip install -q mujoco mediapy"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "af6b5e42",
"metadata": {},
"outputs": [],
"source": [
"import mujoco\n",
"import numpy as np\n",
"import mediapy as media"
]
},
{
"cell_type": "markdown",
"id": "0b294778",
"metadata": {},
"source": [
"## Body-Joint-Geom Hierarchy\n",
"\n",
"MJCF has a tree structure:\n",
"```\n",
"mujoco\n",
"\u2514\u2500\u2500 worldbody\n",
" \u2514\u2500\u2500 body (has joints & geoms)\n",
" \u2514\u2500\u2500 body (child body)\n",
" \u2514\u2500\u2500 ...\n",
"```\n",
"\n",
"- **Bodies** define frames in the kinematic tree\n",
"- **Joints** connect bodies (add degrees of freedom)\n",
"- **Geoms** define collision/visual shapes"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "8898b1cb",
"metadata": {},
"outputs": [],
"source": [
"# Build a 2-link arm step by step\n",
"arm_xml = \"\"\"\n",
"\n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
"\n",
"\"\"\"\n",
"\n",
"model = mujoco.MjModel.from_xml_string(arm_xml)\n",
"data = mujoco.MjData(model)\n",
"mujoco.mj_forward(model, data)\n",
"\n",
"renderer = mujoco.Renderer(model, height=400, width=500)\n",
"renderer.update_scene(data)\n",
"media.show_image(renderer.render())"
]
},
{
"cell_type": "markdown",
"id": "e5293af3",
"metadata": {},
"source": [
"## Using Defaults\n",
"\n",
"The `` element lets you set common properties once.\n",
"You can also use **default classes** for different groups:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "8337a5b8",
"metadata": {},
"outputs": [],
"source": [
"xml_defaults = \"\"\"\n",
"\n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
"\n",
"\"\"\"\n",
"m = mujoco.MjModel.from_xml_string(xml_defaults)\n",
"print(f\"Geoms: {m.ngeom} (visual + collision separated via classes)\")"
]
},
{
"cell_type": "markdown",
"id": "9f75a056",
"metadata": {},
"source": [
"## Composite Objects\n",
"\n",
"MuJoCo can auto-generate soft bodies, ropes, and grids:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "1c8ff8c4",
"metadata": {},
"outputs": [],
"source": [
"rope_xml = \"\"\"\n",
"\n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
"\n",
"\"\"\"\n",
"\n",
"model_r = mujoco.MjModel.from_xml_string(rope_xml)\n",
"data_r = mujoco.MjData(model_r)\n",
"renderer_r = mujoco.Renderer(model_r, height=400, width=500)\n",
"\n",
"frames = []\n",
"while data_r.time < 3.0:\n",
" mujoco.mj_step(model_r, data_r)\n",
" if len(frames) < data_r.time * 30:\n",
" renderer_r.update_scene(data_r)\n",
" frames.append(renderer_r.render().copy())\n",
"\n",
"renderer_r.close()\n",
"media.show_video(frames, fps=30)"
]
},
{
"cell_type": "markdown",
"id": "31a664db",
"metadata": {},
"source": [
"## Loading External Models\n",
"\n",
"Use `` to compose models, or load from [MuJoCo Menagerie](https://github.com/google-deepmind/mujoco_menagerie):"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "4933bf73",
"metadata": {},
"outputs": [],
"source": [
"# Example: programmatic model editing (MuJoCo 3.x spec API)\n",
"spec = mujoco.MjSpec()\n",
"spec.worldbody.add_light(pos=[0, 0, 2])\n",
"spec.worldbody.add_geom(type=mujoco.mjtGeom.mjGEOM_PLANE, size=[1, 1, 0.1])\n",
"\n",
"body = spec.worldbody.add_body(name='dynamic_box', pos=[0, 0, 1])\n",
"body.add_joint(type=mujoco.mjtJoint.mjJNT_FREE)\n",
"body.add_geom(type=mujoco.mjtGeom.mjGEOM_BOX, size=[0.1, 0.1, 0.1], mass=1.0, rgba=[0, 0.7, 0, 1])\n",
"\n",
"model_spec = spec.compile()\n",
"data_spec = mujoco.MjData(model_spec)\n",
"mujoco.mj_forward(model_spec, data_spec)\n",
"\n",
"renderer_s = mujoco.Renderer(model_spec, height=300, width=400)\n",
"renderer_s.update_scene(data_spec)\n",
"media.show_image(renderer_s.render())\n",
"renderer_s.close()\n",
"print(\"Model created programmatically with MjSpec!\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Tendons\n",
"\n",
"Tendons are length constraints that connect joints or spatial points.\n",
"They enable **coupled joints**, **pulleys**, and **muscle routing**.\n",
"\n",
"Types:\n",
"- `` \u2014 weighted sum of joint positions (coupling)\n",
"- `` \u2014 routes through via-points and wrapping surfaces"
]
},
{
"cell_type": "code",
"metadata": {},
"source": [
"xml_tendon = \"\"\"\n",
"\n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
"\n",
"\"\"\"\n",
"\n",
"model_t = mujoco.MjModel.from_xml_string(xml_tendon)\n",
"data_t = mujoco.MjData(model_t)\n",
"\n",
"# Drive left joint \u2014 right joint follows (opposite direction)\n",
"times, left_angles, right_angles = [], [], []\n",
"while data_t.time < 4.0:\n",
" data_t.ctrl[0] = 3.0 * np.sin(2 * data_t.time)\n",
" mujoco.mj_step(model_t, data_t)\n",
" times.append(data_t.time)\n",
" left_angles.append(np.degrees(data_t.qpos[0]))\n",
" right_angles.append(np.degrees(data_t.qpos[1]))\n",
"\n",
"plt.figure(figsize=(10, 3))\n",
"plt.plot(times, left_angles, label='Left joint')\n",
"plt.plot(times, right_angles, label='Right joint (coupled)')\n",
"plt.xlabel('Time (s)'); plt.ylabel('Angle (deg)')\n",
"plt.title('Tendon-Coupled Joints (differential)'); plt.legend(); plt.grid(True); plt.show()\n",
"print('The tendon forces the joints to move in opposite directions!')"
],
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Equality Constraints\n",
"\n",
"Equality constraints enforce geometric relationships:\n",
"\n",
"| Type | Effect |\n",
"|------|--------|\n",
"| `` | Two bodies share a point (ball joint without a body) |\n",
"| `` | Two bodies are rigidly attached |\n",
"| `` | One joint follows another (gear ratio) |\n",
"| `` | Tendon length is fixed |\n",
"\n",
"These are soft constraints (solved with impedance), controlled by `solref` and `solimp`."
]
},
{
"cell_type": "code",
"metadata": {},
"source": [
"xml_equality = \"\"\"\n",
"\n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
" \n",
"\n",
"\"\"\"\n",
"\n",
"model_eq = mujoco.MjModel.from_xml_string(xml_equality)\n",
"data_eq = mujoco.MjData(model_eq)\n",
"\n",
"times, master_angles, follower_angles = [], [], []\n",
"while data_eq.time < 4.0:\n",
" data_eq.ctrl[0] = 3.0 * np.sin(data_eq.time)\n",
" mujoco.mj_step(model_eq, data_eq)\n",
" times.append(data_eq.time)\n",
" master_angles.append(np.degrees(data_eq.qpos[0]))\n",
" follower_angles.append(np.degrees(data_eq.qpos[1]))\n",
"\n",
"plt.figure(figsize=(10, 3))\n",
"plt.plot(times, master_angles, label='Master')\n",
"plt.plot(times, follower_angles, label='Follower (2:1 ratio)')\n",
"plt.xlabel('Time (s)'); plt.ylabel('Angle (deg)')\n",
"plt.title('Equality Constraint: Gear Ratio'); plt.legend(); plt.grid(True); plt.show()\n",
"print('Follower moves at 2x the master angle \u2014 like a gear train!')"
],
"outputs": []
}
],
"metadata": {
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 5
}