URDF and Xacro: describing a robot to ROS
Every ROS-based robot ships a URDF — the XML file that defines its links, joints, meshes, and inertias. Here's the minimal mental model and the Xacro patterns that keep big URDFs sane.
A URDF (Unified Robot Description Format) is how you tell ROS what your robot looks like. Links, joints, meshes, masses — without it, half the ROS ecosystem (RViz, Gazebo, MoveIt, robot_state_publisher) doesn't work. The good news: it's just XML.
The mental model
A robot is a tree of links connected by joints. Each link is a rigid body (a chassis, an arm segment, a wheel). Each joint defines how one link moves relative to another (revolute, prismatic, fixed, continuous, planar, floating).
Minimal URDF
<?xml version="1.0"?>
<robot name="my_robot">
<link name="base_link">
<visual>
<geometry><box size="0.4 0.3 0.1"/></geometry>
<material name="orange"><color rgba="1 0.5 0 1"/></material>
</visual>
<collision>
<geometry><box size="0.4 0.3 0.1"/></geometry>
</collision>
<inertial>
<mass value="2.0"/>
<inertia ixx="0.02" iyy="0.04" izz="0.06" ixy="0" ixz="0" iyz="0"/>
</inertial>
</link>
<link name="left_wheel">
<visual>
<geometry><cylinder radius="0.06" length="0.02"/></geometry>
</visual>
</link>
<joint name="left_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="left_wheel"/>
<origin xyz="0 0.16 0" rpy="1.5708 0 0"/>
<axis xyz="0 0 1"/>
</joint>
</robot>
That's the entire URDF format. Three sections per link (visual, collision, inertial — only visual is strictly required), and a joint per connection. RViz can render this immediately; Gazebo can simulate it (with a few more tags); MoveIt can plan with it.
The five joint types you actually use
- fixed — no relative motion. Sensor mounts, structural attachments.
- revolute — rotates around an axis with a limit. Most arm joints.
- continuous — rotates around an axis, no limit. Wheels.
- prismatic — translates along an axis with a limit. Linear actuators.
- planar / floating — rare, mostly for free-flying / floating-base robots.
Inertia: the tag everyone gets wrong
The <inertial> tag specifies mass and the 3×3 inertia tensor. Wrong values here mean the robot in Gazebo behaves nothing like the real one. Two rules:
- Never leave inertias blank or use placeholder values like 1e-3. Gazebo won't fail loudly; it'll just produce robots that wobble like jello or refuse to move.
- Compute them. Use a CAD tool, OnShape's mass-properties, or the cookbook formulas (box: I = m/12 · diag(h² + d², w² + d², w² + h²)).
Why Xacro exists
A real robot URDF gets to 1000+ lines. You need macros, parameters, and includes. Plain XML doesn't have those. Xacro (XML Macros) is a preprocessor that adds them. Files are named robot.xacro; the build system runs xacro robot.xacro > robot.urdf at build time.
Xacro features that matter
<?xml version="1.0"?>
<robot xmlns:xacro="http://www.ros.org/wiki/xacro" name="my_robot">
<!-- Parameters at the top -->
<xacro:property name="wheel_radius" value="0.06"/>
<xacro:property name="wheel_separation" value="0.32"/>
<!-- Macro for a wheel link + joint -->
<xacro:macro name="wheel" params="prefix y_offset">
<link name="${prefix}_wheel">
<visual>
<geometry><cylinder radius="${wheel_radius}" length="0.02"/></geometry>
</visual>
</link>
<joint name="${prefix}_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="${prefix}_wheel"/>
<origin xyz="0 ${y_offset} 0" rpy="1.5708 0 0"/>
<axis xyz="0 0 1"/>
</joint>
</xacro:macro>
<!-- Use it -->
<xacro:wheel prefix="left" y_offset="${wheel_separation/2}"/>
<xacro:wheel prefix="right" y_offset="${-wheel_separation/2}"/>
</robot>
Two wheels from one macro definition. Multiply by every repeating subassembly on a real robot — torso links, arm joints, leg modules — and Xacro becomes non-optional.
How to load a URDF in ROS 2
Standard pattern in a launch file:
from launch_ros.actions import Node
from launch.substitutions import Command, PathJoinSubstitution
from launch_ros.substitutions import FindPackageShare
robot_description = Command([
'xacro ',
PathJoinSubstitution([FindPackageShare('my_robot'), 'urdf', 'robot.xacro'])
])
return LaunchDescription([
Node(package='robot_state_publisher', executable='robot_state_publisher',
parameters=[{'robot_description': robot_description}]),
Node(package='joint_state_publisher_gui', executable='joint_state_publisher_gui'),
])
robot_state_publisher reads the URDF, listens to /joint_states, and publishes the TF tree. Without it, RViz shows nothing. With it, you can see your robot move.
Common URDF mistakes
- Mesh paths use
package://URIs, not file paths.package://my_robot/meshes/arm.stl, not/home/.../arm.stl. - Joint origin is the parent-to-child transform, not the location of the joint axis itself. Place it at the child's frame origin.
- Inertia tensors must be expressed in the link's COM frame, not its visual frame.
- Visual and collision geometry don't have to match. A simplified collision (capsule, box) is much faster than the visual mesh.
Visualizing a URDF
ros2 launch urdf_tutorial display.launch.py model:=path/to/your.urdf
This brings up RViz with the URDF, a joint slider GUI, and a TF tree. Best ten-second sanity check that exists.
What URDF doesn't cover (and what does)
- SDF (Simulation Description Format) — Gazebo's preferred format; richer than URDF (lights, plugins, more sensors). Auto-converts from URDF for most uses.
- USD (Universal Scene Description) — NVIDIA's format used by Isaac Sim. Conversion tools exist but lossy.
- MJCF — MuJoCo's format. More control over actuator dynamics. Convert from URDF with
mujoco.from_urdf.
For your first robot, write a Xacro that exports URDF. URDF gives you ROS, RViz, MoveIt, Nav2 for free. Add SDF tags later if you specifically need Gazebo features.
Next
ros2_control — the standard hardware-interface layer that turns the URDF + a controller config into actual hardware commands.
Comments
Sign in to post a comment.