Core concept

An MJCF file describes a kinematic tree. The root is the world. Every <body> is a rigid body whose frame is placed relative to its parent body. Inside a body you put four kinds of things:

Element What it is Has physics?
<joint> a degree of freedom between this body and its parent; without one, the body is welded to its parent yes: defines motion
<geom> a shape: collides, and (by default) contributes mass and inertia to its body; also what you see yes
<site> a named frame with no mass and no collision: a marker for sensors, actuators, cameras, targets no
<body> a child body, placed in this body’s frame yes

The parent-child structure is the robot’s structure. A seven-joint arm is seven nested bodies, each with one hinge, and a scene with three free objects has three more bodies directly under the world, each with a <freejoint/>.

[!established] Frames are relative, positions are in metres, angles depend on one setting The pos and orientation of a body, geom, site or camera are expressed in the frame of the parent body. Positions are in metres. Angles in euler, axisangle and joint range are in degrees unless the model says <compiler angle="radian"/>; MuJoCo’s documentation gives the default as “degree” for MJCF, and the compiler converts to radians, so mjModel always stores radians. Every model in this course sets angle="radian" explicitly.

Visual intuition

The playground below holds a two-link arm written for this lesson. Its comments list four edits; make them one at a time and press Run (Ctrl+Enter). Turn on body frames and joint axes to see each frame and each joint’s axis drawn in the scene.

```lab playground {“dock”: true, “model”: “tutorial_tree”, “height”: 250, “editorHeight”: 260, “title”: “Edit the tree, then Run”}


The view draws three arrows per body frame (red, green, blue for $x$, $y$, $z$) and one arrow per joint axis (orange for a hinge, blue for a slide). After edit 3, notice that rotating `link2` carries along everything inside it: its joint axis, its geom, its site, and any child body.

## The attributes that place things

### Position

`pos="x y z"` places the element's frame origin in the parent frame. For a geom of type capsule, cylinder, box or ellipsoid, `fromto="x1 y1 z1 x2 y2 z2"` sets position, orientation and length at once: the geom's long axis ($+z$) runs from the first point to the second. `fromto` is the most readable way to write a link.

### Orientation, five ways

> [!established] The five orientation attributes
> A frame's orientation can be given by at most one of `quat` (w, x, y, z; normalized at compile time; MuJoCo's own format), `axisangle` (axis, then angle in compiler units), `euler` (three angles, applied in the order of `compiler/eulerseq`, default `"xyz"`, lower case meaning axes that move with the frame), `xyaxes` (the frame's $x$ axis, then its $y$ axis, made orthogonal for you) or `zaxis` (the $z$ axis only; MuJoCo uses the minimal rotation from $(0,0,1)$). Whatever you write, the compiler stores a unit quaternion.

Which one to use is a readability question. For a camera, `xyaxes` says exactly where the image's right and up directions point. For a symmetric geom such as a cylinder, `zaxis` is enough. For anything a human will edit, `euler` in degrees is the least error-prone, provided you remember the sequence convention. When a model is saved by MuJoCo, every orientation is written as `quat`.

```io
INPUT: two small MJCF strings embedded in the script
PROCESS: compile one 90-degree rotation written five ways; compile one hinge range under two angle settings
OUTPUT: the stored quaternions and ranges

```python file=examples/l2_1_frames_and_units.py “"”Lesson 2.1: five ways to write one orientation, and the degrees trap.

INPUT MJCF strings written in this file PROCESS (1) compile five bodies meant to be rotated 90 degrees about z, each with a different orientation attribute, and compare the quaternions MuJoCo stores (zaxis cannot express a rotation about z: watch its output); (2) compile the same hinge with range=”-90 90” under the default compiler setting (degrees) and under angle=”radian” OUTPUT the compiled body quaternions and joint ranges

Run: python examples/l2_1_frames_and_units.py “””

import mujoco import numpy as np

FIVE_WAYS = “””

”””

HINGE = “””

{compiler}

”””

def five_ways() -> None: model = mujoco.MjModel.from_xml_string(FIVE_WAYS) for b in range(1, model.nbody): print(f” {model.body(b).name:<10s} body_quat = {np.round(model.body_quat[b], 6)}”)

def degrees_trap() -> None: for compiler in (“”, ‘’): model = mujoco.MjModel.from_xml_string(HINGE.format(compiler=compiler)) lo, hi = model.jnt_range[0] label = compiler or “(no compiler element: MJCF default)” print(f” {label:<38s} range = [{lo:.4f}, {hi:.4f}] rad = [{np.degrees(lo):.1f}, {np.degrees(hi):.1f}] deg”)

if name == “main”: print(“one rotation (90 deg about z), five specifications:”) five_ways() print(“hinge written with range="-90 90":”) degrees_trap()

Output:

```text
one rotation (90 deg about z), five specifications:
  quat       body_quat = [0.707107 0.       0.       0.707107]
  axisangle  body_quat = [0.707107 0.       0.       0.707107]
  euler      body_quat = [0.707107 0.       0.       0.707107]
  xyaxes     body_quat = [0.707107 0.       0.       0.707107]
  zaxis      body_quat = [1. 0. 0. 0.]
hinge written with range="-90 90":
  (no compiler element: MJCF default)    range = [-1.5708, 1.5708] rad = [-90.0, 90.0] deg
  <compiler angle="radian"/>             range = [-90.0000, 90.0000] rad = [-5156.6, 5156.6] deg

Four specifications agree. zaxis="0 0 1" gives the identity: it only says where $z$ points, and a rotation about $z$ leaves $z$ where it is. The second block is the trap. The same text means $\pm 90°$ in one model and $\pm 90$ radians (fourteen full turns, effectively unlimited) in another. Copying a joint from a model with one convention into a model with the other silently changes its limits by a factor of 57.3.

Joints

A <joint> sits inside the body it moves. Its pos (default: the body’s origin) is the pivot, its axis is expressed in the body’s frame, and its type is hinge (default), slide, ball or free (<freejoint/> is a shorthand). With <compiler autolimits="true"/> (the default), giving a range makes the joint limited.

[!warning] The axis is in the child’s frame, before the joint moves it A joint’s axis is written in the frame of the body that contains the joint. If you rotate that body with euler, the axis rotates with it. Reading an axis as if it were in world coordinates is the usual reason a robot model bends “the wrong way”. The joint-axes overlay draws data.xaxis, the axis MuJoCo actually uses, in world coordinates.

Geoms

The geom types are plane, sphere, capsule, ellipsoid, cylinder, box, mesh, hfield and sdf. size means different things per type: radius for a sphere; radius and half-length for capsules and cylinders; three half-sizes for boxes and ellipsoids; for a plane, half-sizes for drawing only (a plane is infinite for collision). Half-sizes, not sizes, is the second most common unit bug after degrees.

Sites

A <site> is a frame with a name. It has no mass, does not collide and does not move anything. It is where you attach sensors (framepos, touch, force), where you aim a camera, where an end-effector controller reads its pose, and where a task marks a goal. A robot model with good sites is easy to control and evaluate. In edit 4, moving the site changes what the tip_pos sensor reports and nothing else.

Mathematics

Composition of frames is multiplication of transforms. If body $b$’s parent has world pose $(R_p, \mathbf p_p)$ and $b$ is placed at $(R_{\text{rel}}, \mathbf p_{\text{rel}})$ in the parent frame, then

\[R_b = R_p R_{\text{rel}}, \qquad \mathbf p_b = \mathbf p_p + R_p\, \mathbf p_{\text{rel}}.\]

With a joint, the relative transform also includes the joint’s motion: for a hinge with unit axis $\hat{\mathbf a}$ (in the child frame) through pivot $\mathbf c$, rotating by angle $q$ inserts $R(\hat{\mathbf a}, q)$ about $\mathbf c$. Running this from the root to the leaves is forward kinematics, which mj_kinematics does in every step and which Level 6 builds by hand. Level 4.1 covers rotations, quaternions and the transform algebra properly.

Debugging

The model compiles but a link points the wrong way. Turn on body frames and joint axes. Most often: a fromto with the points swapped, an euler read in the wrong sequence, or an axis written in world coordinates.

Joint limits never engage, or engage immediately. Degrees versus radians. Print model.jnt_range and convert.

Error: size 0 must be positive in geom. The default geom is a sphere with no size; every geom needs a size unless fromto or a mesh determines it.

Error: fromto requires capsule, cylinder, box or ellipsoid in geom. The default geom type is sphere; add type="capsule". (This course’s own example script hit exactly this error while being written.)

Exercise

Without the playground, write on paper the world position of the tip site in tutorial_tree.xml when the shoulder is at 0.5 rad and the elbow at -0.8 rad. Then check it in the playground (set the angles with the inspector’s state or with a keyframe) and in Python with data.site("tip").xpos after mj_forward. Agreement to $10^{-12}$ is the target.

Challenge

Add a camera to tutorial_tree.xml that is attached to link2, sits 5 cm above the elbow, and looks along the lower link toward the tip, with the image’s “up” direction pointing toward world $+z$ when the arm hangs straight down. Specify it with xyaxes. Verify it by rendering from it (in Python with mujoco.Renderer, or by showing camera frustums in the playground).

Research connection

Robot description files are written by people and contain errors. The common ones, reversed axes, wrong units, joint frames at the wrong pivot, inertias in the wrong frame, do not prevent a simulation from running; they change its answers. A research group that shares models should test them: limits in the expected units, home poses free of collisions, Jacobians of full rank, tip positions matching the robot’s documentation. This course’s own tests/test_models.py is a small example of such a test suite.

{"id": "2.1-check", "title": "Knowledge check", "questions": [
  {"kind": "mcq", "q": "A body has <code>pos=\"0 0 0.3\"</code> and its parent is at world position (1, 0, 0) with no rotation. Where is the body's origin in the world?",
   "options": ["(0, 0, 0.3)", "(1, 0, 0.3)", "(1, 0, 0)", "It depends on the joint"],
   "answer": 1,
   "explain": "<p>Positions are relative to the parent body: $\\mathbf p_b = \\mathbf p_p + R_p \\mathbf p_{rel}$ = (1, 0, 0.3) with $R_p = I$. A joint would move the body from there, but at $q = 0$ it is at this pose.</p>"},
  {"kind": "mcq", "q": "A model without a <code>&lt;compiler&gt;</code> element has <code>&lt;joint range=\"0 1.57\"/&gt;</code>. What range does MuJoCo use?",
   "options": ["0 to 90 degrees", "0 to 1.57 degrees, about 0.027 rad", "0 to 1.57 rad", "Unlimited"],
   "answer": 1,
   "explain": "<p>MJCF angles default to degrees. The author probably meant radians; the joint is now almost locked.</p>"},
  {"kind": "mcq", "q": "Which element would you add to mark the end-effector point a controller should track?",
   "options": ["A geom with zero mass", "A site", "A body with no joint", "A camera"],
   "answer": 1,
   "explain": "<p>A site is a massless, non-colliding named frame, exactly what controllers, sensors and goal markers need. A massless geom would still collide unless you disabled it.</p>"},
  {"kind": "open", "q": "Write the MJCF for a body hanging from the world on a ball joint, with a 0.4 m capsule pointing down from the joint and a site at its lower end.",
   "reference": "<p>One correct answer: <code>&lt;body name=\"bob\" pos=\"0 0 1\"&gt;&lt;joint type=\"ball\"/&gt;&lt;geom type=\"capsule\" fromto=\"0 0 0 0 0 -0.4\" size=\"0.02\"/&gt;&lt;site name=\"end\" pos=\"0 0 -0.4\"/&gt;&lt;/body&gt;</code>. Check it in the playground: <code>nq</code> should be 4 and <code>nv</code> 3.</p>"}
]}

Next

Lesson 2.2 covers the physical side of a model: where mass and inertia come from, how default classes keep a large model consistent, and which global options matter.