Where you are. MuJoCo is installed and you have made a ball fall. This lesson is the format that ball was described in, and by the end you will have written a two-link arm from nothing and driven it.
Where is the gripper?
Open any MuJoCo model of a robot arm and try to find the gripper’s position. Search for the number you think it should be. You will not find it, and not because it is hidden somewhere clever. Nowhere in the file does it say where the gripper is.
What the file says is that the gripper sits four centimetres along from the wrist. And the wrist sits twenty-two centimetres along from the elbow. And the elbow sits thirty centimetres along from the shoulder. And the shoulder is bolted to the world, sixty centimetres up. Every line states an offset from one specific other thing, and that other thing is always the line one indentation level out.
You have written this file before, in a different notation. In Module 1 you found the hand by multiplying a chain of transforms in order, and you found out the hard way that getting the order wrong put the hand somewhere else entirely. That chain is this file. The multiplication order is the indentation.
The idea in one paragraph
MJCF is MuJoCo’s XML scene format, and it has one load-bearing idea: <body> elements nest, and nesting means parenthood. A child body’s pos is measured from its parent’s frame, never from the world, so the file is a kinematic chain written as indentation. Everything else hangs off that. Joints say how a body is allowed to move relative to its parent, and they are the only things in the file that change while the simulation runs. Geoms are shapes that give a body both its appearance and, unless you say otherwise, its mass. Sites are labelled points with neither. Actuators and sensors live outside the tree in flat ordered lists and reach into it by name. And the whole file is compiled, once, into a fixed structure, in which the compiler has filled in a great deal you never typed.
Nesting is parenthood
Wider than the screen; scroll it sideways.
<worldbody>
<body name="upper_arm" pos="0 0 0.6">
<joint name="shoulder"/>
<geom name="upper_arm" fromto="0 0 0 0.30 0 0"/>
<body name="forearm" pos="0.30 0 0">
<joint name="elbow"/>
<geom name="forearm" fromto="0 0 0 0.22 0 0"/>
<site name="tip" pos="0.22 0 0" size="0.012"/>
</body>
</body>
</worldbody>
<worldbody> is the root and the only body that never moves. upper_arm sits 0.6 m above the world origin. forearm sits 0.30 m along the upper arm’s own x-axis, which is where the upper arm’s capsule ends. Nobody ever wrote down the forearm’s world position, and nobody could: it depends on the shoulder angle, which is a runtime quantity.
Move that </body> line so the forearm closes outside the upper arm rather than inside it, and you have not made a formatting change. You have unbolted the elbow. The forearm becomes a second arm rooted at the world, 0.30 m along the world’s x-axis, and it will hang there while the upper arm swings past it. This is the single most useful thing to know about MJCF: the shape of the file is the shape of the machine.
Joints are the only things that vary
Four kinds, and you will use two of them constantly.
| Type | What it allows | Numbers in qpos |
|---|---|---|
hinge | rotation about one axis; the default | 1 |
slide | translation along one axis | 1 |
ball | any rotation, no translation | 4 (a quaternion) |
free | any rotation and any translation, six degrees of freedom | 7 (position, then quaternion) |
<freejoint/> is shorthand for the last one, and it is what you give to a loose object: a cube on a table has no parent to hinge against, it simply floats in the world and gravity does the rest.
The joints in the tree, read top to bottom, decide the layout of the state vector. data.qpos is one flat array of numbers, and each joint owns a contiguous slice of it, in tree order.
Wider than the screen; scroll it sideways.
Geoms carry the mass
A geom is a shape: plane, sphere, capsule, box, cylinder, ellipsoid, or mesh for anything hand-modelled. It is drawn, and it collides, and unless you write an explicit <inertial> block it is also where the body’s mass comes from.
Two attributes decide whether a geom takes part in physics at all. Setting contype="0" conaffinity="0" removes it from collision detection entirely; it is then decoration. Drop a ball onto a floor with those attributes set and the ball simply keeps going, 43 metres down and still falling. Serious models use this deliberately: one set of pretty mesh geoms for the eye, and a second set of cheap boxes and spheres doing the actual contact work. The next lesson opens a real arm model where exactly that split is in play.
Outside the tree
Actuators and sensors are not part of the body hierarchy. They are flat, ordered lists that name their targets.
<actuator>
<position name="shoulder" joint="shoulder" ctrlrange="-3.14 3.14"/>
<position name="elbow" joint="elbow" ctrlrange="-2.60 2.60"/>
</actuator>
<sensor>
<framepos name="tip_pos" objtype="site" objname="tip"/>
<jointpos name="shoulder_q" joint="shoulder"/>
</sensor>
Order in these lists is the whole interface. The first <actuator> is data.ctrl[0], the second is data.ctrl[1], and a sensor’s readings land in a slice of data.sensordata at an offset the compiler assigns. Insert a new actuator in the middle of the list and every index in your controller silently shifts by one. Address them by name when you can.
A <position> actuator is worth naming properly now, because it is what the whole course runs on. It is a PD controller living inside the simulator: you write a target angle to data.ctrl, and it applies a torque proportional to the error, minus a term proportional to the velocity. That is feedback control and PID from Module 1, with the I term left out. Which means the steady-state error you proved must exist there shows up here: command the arm to radians with gravity pulling on it and it settles at , and no amount of waiting closes the gap.
Defaults are a stylesheet
Every element in the arm above inherits attributes nobody wrote on it.
<default>
<joint type="hinge" axis="0 1 0" damping="0.4"/>
<geom type="capsule" size="0.02" rgba="0.18 0.36 0.49 1"/>
<position kp="12" kv="1.2"/>
<default class="cube">
<geom type="box" size="0.03 0.03 0.03"/>
</default>
</default>
Two more sections round the file out. <asset> holds meshes, textures and materials, referenced by name from geoms. <keyframe> holds named saved states: <key name="home" qpos="..."/> is a pose you can jump to with mujoco.mj_resetDataKeyframe(model, data, 0), and the numbers in it are in exactly the qpos order the tree dictated.
It is a compile step, not a parse
Wider than the screen; scroll it sideways.
Because it is a compiler, its errors are compile errors, and they are unusually good. Give a keyframe the wrong number of numbers:
Error: keyframe 'home': invalid qpos size, expected 1, got 2
Element name 'home', id 0
Point an actuator at a joint that does not exist:
Error: unknown transmission target 'nosuchjoint' for actuator id = 0
Element name 'p', id 0, line 2
Element name, id, and often a line number. Treat these the way you treat a type error: the fix is nearly always literally what it says, and the alternative to a load-time failure would be a robot that behaves subtly wrong for an hour.
The other format you will meet
MJCF is MuJoCo’s own. The other format in robotics is URDF, the Unified Robot Description Format, which came out of the ROS ecosystem and is what most vendors ship. MuJoCo will load a URDF file directly, and you will end up doing that when you import a model somebody else built.
| MJCF | URDF | |
|---|---|---|
| Tree of links and joints | yes | yes |
| Physics settings: solver, timestep, friction cones | yes | no |
| Actuators and sensors | yes | not in the core format |
| A world: floor, lights, cameras, other objects | yes | no, the robot only |
| Defaults and classes | yes | no |
| Keyframes | yes | no |
| Portable across engines | no, MuJoCo only | yes, near-universal |
Various ecosystems have bolted extension tags onto URDF to cover some of the gaps, but an extension is not the format, and nothing obliges the tool you load the file into to read them. Treat the table as what you can count on.
The split is honest on both sides. URDF describes a robot; MJCF describes a simulation. So a URDF import gives you a correct kinematic tree and nothing to drive it with, which is why a converted model is a starting point rather than a finished one. The curated models you will meet next are MJCF files derived from URDF and then hand-tuned: collision primitives added, contact parameters set for grasping, actuators written, a scene built around them. That tuning is most of the value, and reading it is the next lesson.
Check yourself
1. You move a </body> line so a forearm closes outside its parent instead of inside it. The file still loads and the arm still renders. What changed?
The forearm is no longer a child of the upper arm; it is a sibling rooted at the world. Its pos is now measured from the world origin rather than from the end of the upper arm, so it sits in a different place, and the shoulder joint no longer moves it at all. The kinematic chain has been cut. Nothing about this is a syntax error, which is why indentation in MJCF deserves the attention you would give to a matrix multiplication order.
2. A capsule sits in your scene under gravity and never moves. You have checked gravity is set and the timestep is sane. What is wrong?
It has no joint, so it is welded to its parent, which is the world. nq is 0 for that body: there is no state to integrate, and no force can change a quantity that does not exist. A joint is not a physical hinge you add for realism, it is the declaration that something is allowed to vary. Add <freejoint/> and it falls; add <joint type="hinge"/> and it swings.
3. Your scene has two hinge joints and one free-jointed cube. What are nq and nv, and why do they differ?
nq is 9: one for each hinge, plus seven for the free joint, which stores three position numbers and a four-number quaternion. nv is 8, because a change in orientation only needs three numbers even though storing an orientation takes four. So qpos and qvel have different lengths and their indices do not line up past the first free or ball joint. This is the same over-parameterisation you met when you chose quaternions over Euler angles in Module 1, showing up in the state layout.
4. Nobody typed a mass anywhere in the file, but the compiled model says the upper arm weighs 0.4105 kg. Where did that come from, and when should it worry you?
From the geom. MuJoCo computes mass and inertia from a geom’s volume at a default density of 1000 kg/m³, the density of water. For the capsule in question the volume works out to m³, which times 1000 is the number reported. It should worry you as soon as dynamics matter: real links are hollow aluminium and plastic, not solid water, so a model with default masses has correct-looking geometry and wrong inertia, and any torque, force or contact result computed from it is wrong with it.
5. An actuator’s kp is nowhere on the <position> element, yet the compiled model reports kp = 12. Where do you look, and what is the general rule?
In the <default> blocks. MJCF defaults work like CSS: a top-level <default> sets attributes for every element of that type, a nested <default class="..."> is a class applied with class= on the element or childclass= on an enclosing body, and attributes written directly on an element override both. The general rule when a value is not where you expect: it is not missing, it is inherited, so read outward.
6. You are handed a URDF of an arm and asked to make it pick something up in MuJoCo. What is already done and what is not?
Done: the tree of links and joints, their offsets, and the collision and visual geometry. Not done: anything that makes it a simulation. There are no actuators, so nothing can drive the joints; no sensors; no world, so no floor, no lights and no object to pick up; no physics settings, so solver, timestep and friction are all whatever the defaults happen to be; and no keyframes. A URDF describes a robot, while an MJCF describes a scene containing one, and the gap between those two is the work.
Do this
Forty minutes. You are going to write the arm, not read it.
1. Finish code/two_link.xml. Four # TODO(you) markers, in the file as XML comments:
- the
forearmbody, nested insideupper_arm, with its own joint, geom andtipsite; - two
<position>actuators, one per joint; - two sensors, a
frameposon the site and ajointposon the shoulder; - a
homekeyframe with the right number ofqposentries.
2. Finish code/mjcf_tour.py and run it from inside module-02-simulation/code/:
python mjcf_tour.py
Two # TODO(you) markers: print_tree, which walks the body hierarchy and prints it as the tree it is, and run, which resets to your keyframe, commands a pose and reports what actually happened. The script checks the structure before it runs anything, so an unfinished XML file tells you which TODO is still open rather than failing somewhere confusing. Working output is in solutions/.
3. Break it on purpose, and follow the errors. Delete the <joint name="elbow"/> line and run. The compiler now walks you through the dependency graph of your own file, one message at a time:
unknown transmission target 'elbow' for actuator id = 1. The elbow actuator points at a joint that is gone. Delete that actuator line.keyframe 'home': invalid qpos size, expected 8, got 9. Losing the joint lost a slot inqpos. Drop a number.- It loads. The elbow is welded,
nqis 8, and the arm moves as one piece.
Put all three back, then try two more one-line changes:
- Move the forearm’s
</body>so it closes outsideupper_arm. Read whatprint_treereports as the forearm’s parent, then read the message the structure check gives you. - Add
size="0.05"to the forearm’s geom, overriding the<default>. The mass the tour prints goes from 0.3100 kg to 2.2515 kg and the arm droops further under the same commanded angle. You changed a drawing dimension and got new dynamics.
4. One prediction, written down before you run it. Change the cube’s <freejoint/> to <joint name="cube_slide" type="slide" axis="0 0 1"/>. Before running, write down what nq will be. Then run it: the compiler refuses, and the number in its complaint is the answer. Resize the keyframe to match, run again, and see what the cube can and cannot do now.
What you can now do
You can read an MJCF file and say what every element in it is for: which parts are the tree, which are flat lists that reference it by name, and which are settings. You can write a jointed, actuated, sensored arm from an empty file. You can predict the layout of qpos from the shape of the tree, explain why nv is smaller than nq, and find an attribute that is not written anywhere near the element it applies to. And you know what a URDF gives you and what it leaves you to write, which is the difference between importing a robot and having a simulation.