URDF and MJCF cheat sheet
URDF and MuJoCo MJCF elements side by side for links, joints, inertia, geometry, limits, actuators and frames, one small arm written in both formats, and the conversion traps between them.
This sheet is for anyone who has a robot in one format and needs it in the other, or who is writing a model by hand. It maps the elements of URDF, the ROS robot description format, to MJCF, MuJoCo’s native format. It then gives one small arm in both formats and lists what changes in conversion. Element names were checked against the ROS URDF specification and the MuJoCo XML reference on 14 November 2026. For a guided first model, see simulate an arm in MuJoCo.
Element map
| Concept | URDF | MJCF | Watch for |
|---|---|---|---|
| Root | <robot name> |
<mujoco model>, bodies inside <worldbody> |
MJCF also holds options, defaults, assets, actuators and sensors |
| Rigid body | <link>, a flat list |
<body>, nested |
URDF builds the tree from joints. MJCF builds it from nesting |
| Joint | <joint> between a parent and a child link |
<joint> inside the child body |
A body with no joint is welded to its parent |
| Joint types | revolute, continuous, prismatic, fixed, floating, planar |
hinge, slide, ball, free |
revolute and continuous become hinge with or without range. fixed becomes no joint. floating becomes free |
| Joint axis | <axis xyz>, default (1, 0, 0), in the joint frame |
axis, default (0, 0, 1), in the body frame |
The defaults differ. Always write the axis explicitly |
| Joint placement | <origin xyz rpy> on the joint: parent link frame to child link frame |
pos and quat (or euler, axisangle) on the child <body> |
URDF rpy is fixed-axis x, y, z. In MJCF that is eulerseq="XYZ" |
| Position limits | <limit lower upper>, required for revolute and prismatic |
range on the joint. With autolimits="true" (the default) a range turns the limit on |
MJCF reads hinge range in degrees unless <compiler angle="radian"/> |
| Effort and velocity limits | <limit effort velocity>, both required |
Effort: actuatorfrcrange on the joint or forcerange on the actuator. No joint velocity limit |
MuJoCo turns URDF effort into actuatorfrcrange when it loads a URDF |
| Mass and inertia | <inertial>: <origin>, <mass value>, <inertia ixx ixy ixz iyy iyz izz> |
<inertial pos mass diaginertia> or fullinertia (Ixx, Iyy, Izz, Ixy, Ixz, Iyz) |
The six numbers are in a different order. If <inertial> is absent, MuJoCo computes it from the geoms |
| Geometry | <box size> full lengths, <cylinder radius length>, <sphere radius>, <mesh> |
<geom type size>: box sizes are half-lengths, cylinder and capsule take a radius and half-length, or fromto |
Halve every box and cylinder length on the way to MJCF |
| Visual and collision | Separate <visual> and <collision> |
A geom collides and renders by default. Set contype="0" conaffinity="0" for visual-only geoms |
MuJoCo discards URDF visuals by default (discardvisual is true for URDF) |
| Joint dynamics | <dynamics damping friction> |
damping, frictionloss, armature on the joint |
armature (reflected rotor inertia) has no URDF equivalent |
| Actuators | <transmission> with <actuator> and <mechanicalReduction>. ros2_control uses a <ros2_control> tag |
<actuator> section: motor, position, velocity, general and more, with gear, ctrlrange, forcerange |
URDF has no actuator dynamics. Decide the actuator model yourself |
| Tool and sensor frames | A massless link on a fixed joint |
<site> inside a body |
MuJoCo fuses fixed-joint links into their parent by default when it loads a URDF (fusestatic) |
| Sensors | <sensor> (camera, ray) is specified on the ROS wiki |
<sensor> section: jointpos, gyro, accelerometer, force, touch, rangefinder and more |
|
| Contacts | None, every collision pair is checked by the consumer | Parent and child bodies are excluded, unless the parent is welded to the world. <contact><exclude> for other pairs |
See the exclude in the MJCF below |
| Units | SI: metres, kilograms, radians | No fixed units. Angles in degrees by default | Set angle="radian" and keep SI to match URDF |
One arm, two formats
Two revolute joints (shoulder and elbow) on a fixed base, both turning about y. The upper arm is 0.30 m long and 1.0 kg. The forearm is 0.25 m long and 0.6 kg. Inertias are those of solid cylinders about their centres. The tool frame sits at the forearm tip.
URDF
<?xml version="1.0"?>
<robot name="two_link_arm">
<link name="base_link">
<visual>
<origin xyz="0 0 0.05" rpy="0 0 0"/>
<geometry><box size="0.2 0.2 0.1"/></geometry>
</visual>
<collision>
<origin xyz="0 0 0.05" rpy="0 0 0"/>
<geometry><box size="0.2 0.2 0.1"/></geometry>
</collision>
<inertial>
<origin xyz="0 0 0.05" rpy="0 0 0"/>
<mass value="2.0"/>
<inertia ixx="0.008333" ixy="0" ixz="0" iyy="0.008333" iyz="0" izz="0.013333"/>
</inertial>
</link>
<joint name="shoulder" type="revolute">
<parent link="base_link"/>
<child link="upper_arm"/>
<origin xyz="0 0 0.1" rpy="0 0 0"/>
<axis xyz="0 1 0"/>
<limit lower="-1.57" upper="1.57" effort="20" velocity="2.0"/>
<dynamics damping="0.5" friction="0.0"/>
</joint>
<link name="upper_arm">
<visual>
<origin xyz="0 0 0.15" rpy="0 0 0"/>
<geometry><cylinder radius="0.03" length="0.3"/></geometry>
</visual>
<collision>
<origin xyz="0 0 0.15" rpy="0 0 0"/>
<geometry><cylinder radius="0.03" length="0.3"/></geometry>
</collision>
<inertial>
<origin xyz="0 0 0.15" rpy="0 0 0"/>
<mass value="1.0"/>
<inertia ixx="0.007725" ixy="0" ixz="0" iyy="0.007725" iyz="0" izz="0.00045"/>
</inertial>
</link>
<joint name="elbow" type="revolute">
<parent link="upper_arm"/>
<child link="forearm"/>
<origin xyz="0 0 0.3" rpy="0 0 0"/>
<axis xyz="0 1 0"/>
<limit lower="-2.5" upper="2.5" effort="10" velocity="3.0"/>
<dynamics damping="0.3" friction="0.0"/>
</joint>
<link name="forearm">
<visual>
<origin xyz="0 0 0.125" rpy="0 0 0"/>
<geometry><cylinder radius="0.025" length="0.25"/></geometry>
</visual>
<collision>
<origin xyz="0 0 0.125" rpy="0 0 0"/>
<geometry><cylinder radius="0.025" length="0.25"/></geometry>
</collision>
<inertial>
<origin xyz="0 0 0.125" rpy="0 0 0"/>
<mass value="0.6"/>
<inertia ixx="0.003219" ixy="0" ixz="0" iyy="0.003219" iyz="0" izz="0.000188"/>
</inertial>
</link>
<joint name="tool_joint" type="fixed">
<parent link="forearm"/>
<child link="tool0"/>
<origin xyz="0 0 0.25" rpy="0 0 0"/>
</joint>
<link name="tool0"/>
<transmission name="shoulder_trans">
<type>transmission_interface/SimpleTransmission</type>
<joint name="shoulder">
<hardwareInterface>hardware_interface/EffortJointInterface</hardwareInterface>
</joint>
<actuator name="shoulder_motor">
<mechanicalReduction>100</mechanicalReduction>
</actuator>
</transmission>
<transmission name="elbow_trans">
<type>transmission_interface/SimpleTransmission</type>
<joint name="elbow">
<hardwareInterface>hardware_interface/EffortJointInterface</hardwareInterface>
</joint>
<actuator name="elbow_motor">
<mechanicalReduction>100</mechanicalReduction>
</actuator>
</transmission>
</robot>
MJCF
<mujoco model="two_link_arm">
<compiler angle="radian" autolimits="true"/>
<option timestep="0.002" gravity="0 0 -9.81"/>
<default>
<joint type="hinge" axis="0 1 0"/>
</default>
<worldbody>
<geom name="floor" type="plane" size="1 1 0.1"/>
<body name="base_link">
<inertial pos="0 0 0.05" mass="2.0" diaginertia="0.008333 0.008333 0.013333"/>
<geom type="box" pos="0 0 0.05" size="0.1 0.1 0.05"/>
<body name="upper_arm" pos="0 0 0.1">
<joint name="shoulder" range="-1.57 1.57" damping="0.5" armature="0.01"/>
<inertial pos="0 0 0.15" mass="1.0" diaginertia="0.007725 0.007725 0.00045"/>
<geom type="cylinder" fromto="0 0 0 0 0 0.3" size="0.03"/>
<body name="forearm" pos="0 0 0.3">
<joint name="elbow" range="-2.5 2.5" damping="0.3" armature="0.005"/>
<inertial pos="0 0 0.125" mass="0.6" diaginertia="0.003219 0.003219 0.000188"/>
<geom type="cylinder" fromto="0 0 0 0 0 0.25" size="0.025"/>
<site name="tool0" pos="0 0 0.25"/>
</body>
</body>
</body>
</worldbody>
<contact>
<exclude body1="base_link" body2="upper_arm"/>
</contact>
<actuator>
<position name="shoulder_pos" joint="shoulder" kp="50" kv="2" ctrlrange="-1.57 1.57" forcerange="-20 20"/>
<position name="elbow_pos" joint="elbow" kp="30" kv="1" ctrlrange="-2.5 2.5" forcerange="-10 10"/>
</actuator>
</mujoco>
Line by line, the differences are these. Box and cylinder sizes in MJCF are half-lengths, or given by fromto. The base box sits at pos="0 0 0.05" with half-size 0.05. The joint axes are written once, in a <default> class. Limits are radians because of <compiler angle="radian"/>. The base and upper arm are welded parent and child, so MuJoCo would let them collide. The <exclude> stops that. Without it, the shoulder stays pinned against the base and never reaches its target.
Load and step the MJCF
This block loads the model, checks the tool position against the forward kinematics written by hand, then drives both joints to a target with the position actuators.
import numpy as np
import mujoco
MJCF = """
<mujoco model="two_link_arm">
<compiler angle="radian" autolimits="true"/>
<option timestep="0.002" gravity="0 0 -9.81"/>
<default>
<joint type="hinge" axis="0 1 0"/>
</default>
<worldbody>
<geom name="floor" type="plane" size="1 1 0.1"/>
<body name="base_link">
<inertial pos="0 0 0.05" mass="2.0" diaginertia="0.008333 0.008333 0.013333"/>
<geom type="box" pos="0 0 0.05" size="0.1 0.1 0.05"/>
<body name="upper_arm" pos="0 0 0.1">
<joint name="shoulder" range="-1.57 1.57" damping="0.5" armature="0.01"/>
<inertial pos="0 0 0.15" mass="1.0" diaginertia="0.007725 0.007725 0.00045"/>
<geom type="cylinder" fromto="0 0 0 0 0 0.3" size="0.03"/>
<body name="forearm" pos="0 0 0.3">
<joint name="elbow" range="-2.5 2.5" damping="0.3" armature="0.005"/>
<inertial pos="0 0 0.125" mass="0.6" diaginertia="0.003219 0.003219 0.000188"/>
<geom type="cylinder" fromto="0 0 0 0 0 0.25" size="0.025"/>
<site name="tool0" pos="0 0 0.25"/>
</body>
</body>
</body>
</worldbody>
<contact>
<exclude body1="base_link" body2="upper_arm"/>
</contact>
<actuator>
<position name="shoulder_pos" joint="shoulder" kp="50" kv="2" ctrlrange="-1.57 1.57" forcerange="-20 20"/>
<position name="elbow_pos" joint="elbow" kp="30" kv="1" ctrlrange="-2.5 2.5" forcerange="-10 10"/>
</actuator>
</mujoco>
"""
model = mujoco.MjModel.from_xml_string(MJCF)
data = mujoco.MjData(model)
data.qpos[:] = [0.5, -0.8] # shoulder, elbow (rad)
mujoco.mj_kinematics(model, data)
q1, q2 = data.qpos
analytic = np.array([0.3 * np.sin(q1) + 0.25 * np.sin(q1 + q2), 0.0,
0.1 + 0.3 * np.cos(q1) + 0.25 * np.cos(q1 + q2)])
print("tool0 from MuJoCo:", np.round(data.site("tool0").xpos, 4))
print("tool0 analytic: ", np.round(analytic, 4))
mujoco.mj_resetData(model, data)
data.ctrl[:] = [0.8, -1.2] # position targets (rad)
for _ in range(int(3.0 / model.opt.timestep)):
mujoco.mj_step(model, data)
print("joint angles after 3 s:", np.round(data.qpos, 3), "(gravity pulls them off target)")
print("total mass (kg):", round(float(model.body_mass.sum()), 3))
The P-only position servo leaves a steady error under gravity: the shoulder settles at 0.843 rad for a target of 0.8 rad. Raise kp, add kv, or add gravity compensation (actuatorgravcomp on the joint in recent MuJoCo versions) if the tracking matters.
Converting
| Direction | Route | What to check afterwards |
|---|---|---|
| URDF to MuJoCo | mujoco.MjModel.from_xml_path("arm.urdf") loads a URDF directly. Add a <mujoco><compiler .../></mujoco> child to the URDF for mesh paths and options. Save with mujoco.mj_saveLastXML to get MJCF |
Bodies fused away by fusestatic, visuals dropped, no actuators (add them), joint effort now in actuatorfrcrange, contact exclusions |
| MJCF to URDF | The MuJoCo documentation describes loading URDF, not writing it. Write the URDF by hand or use a third-party tool | Ball joints and tendons have no URDF form. Free joints become floating. Joint equality constraints map only partly, to <mimic>. Sites become fixed-joint links |
| xacro to URDF | ros2 run xacro xacro robot.urdf.xacro > robot.urdf |
Run the result through check_urdf, which ships with ROS 2 Jazzy, before you load it anywhere |
When we loaded the URDF above with MuJoCo 3.16, base_link was fused into the world body, the visual geoms were dropped (3 geoms remain), the damping values carried over, and the joint efforts became force ranges of plus or minus 20 N m and 10 N m.
Pitfalls
- The URDF spec requires
<limit>onrevoluteandprismaticjoints. Not every parser enforces it:urdf_parser_pyaccepts a revolute joint without one, so check your files yourself. - Inertia must be about the centre of mass and positive definite. A link with zero mass and no inertia under a moving joint makes simulators unstable.
- The URDF joint
<origin>places the child link frame. The visual, collision and inertial<origin>tags are offsets inside the link. Mixing them up moves the mass but not the shape. - MJCF
euleruses the compiler’seulerseq, which defaults to intrinsicxyz, not URDF’s fixed-axis order. See rotation representations. - MJCF quaternions are (w, x, y, z).
- Collide with primitives where you can, and keep detailed meshes for visuals.
Related pages
- Simulate an arm in MuJoCo and the two-link arm project
- DH and product of exponentials for the kinematics these files describe
- MuJoCo platform page and the simulation hub
- MuJoCo modelling guide, including its section on URDF extensions