ROS 2 URDF & Xacro Guide 2026Robot Modeling · Inertia · Joints · Launch
Complete 2026 guide to URDF and Xacro for ROS 2. Covers writing a differential drive robot from scratch, all joint types, inertia calculations, Xacro properties and macros to eliminate boilerplate, include files for large robots, check_urdf validation, and launching robot_state_publisher with a Python launch file.
URDF Joint Types
| Type | Motion | Use Case |
|---|---|---|
| fixed | None | Sensor mounts, static chassis parts, structural joints |
| continuous | Unlimited rotation | Drive wheels, spinning sensors, propellers |
| revolute | Rotation with limits | Robot arm joints, grippers, pan-tilt heads |
| prismatic | Linear slide with limits | Linear actuators, telescoping links, drawer joints |
| planar | Motion in a plane | Rare — 2D sliding on a surface |
| floating | 6 DOF (no constraint) | Base of free-floating robots, spacecraft |
Write a URDF From Scratch
Minimal URDF — Two Links and One Joint
Start with the smallest valid URDF: a base_link, a child link, and a fixed joint connecting them.
<?xml version="1.0"?>
<robot name="minimal_robot">
<!-- Base link (root — must have no parent) -->
<link name="base_link">
<visual>
<geometry>
<box size="0.3 0.2 0.1"/>
</geometry>
<material name="gray">
<color rgba="0.5 0.5 0.5 1.0"/>
</material>
</visual>
<collision>
<geometry>
<box size="0.3 0.2 0.1"/>
</geometry>
</collision>
<inertial>
<mass value="5.0"/>
<!-- Solid box inertia: m/12*(y²+z²), m/12*(x²+z²), m/12*(x²+y²) -->
<inertia ixx="0.0108" ixy="0.0" ixz="0.0"
iyy="0.0283" iyz="0.0"
izz="0.0342"/>
</inertial>
</link>
<!-- Lidar sensor link -->
<link name="lidar_link">
<visual>
<geometry>
<cylinder radius="0.04" length="0.07"/>
</geometry>
<material name="black">
<color rgba="0.1 0.1 0.1 1.0"/>
</material>
</visual>
<collision>
<geometry>
<cylinder radius="0.04" length="0.07"/>
</geometry>
</collision>
<inertial>
<mass value="0.2"/>
<inertia ixx="0.000108" ixy="0.0" ixz="0.0"
iyy="0.000108" iyz="0.0"
izz="0.000160"/>
</inertial>
</link>
<!-- Fixed joint: lidar mounted 0.15 m forward, 0.08 m above base -->
<joint name="base_to_lidar" type="fixed">
<parent link="base_link"/>
<child link="lidar_link"/>
<origin xyz="0.15 0.0 0.08" rpy="0.0 0.0 0.0"/>
</joint>
</robot>💡 Joint types: fixed (no motion), continuous (unlimited rotation), revolute (rotation with limits), prismatic (linear slide).
Differential Drive Robot URDF
Full differential drive robot with two drive wheels, one caster, and a lidar sensor link.
<?xml version="1.0"?>
<robot name="diff_drive_robot">
<link name="base_link">
<visual>
<geometry><box size="0.4 0.3 0.12"/></geometry>
<material name="blue"><color rgba="0.2 0.4 0.8 1.0"/></material>
</visual>
<collision><geometry><box size="0.4 0.3 0.12"/></geometry></collision>
<inertial>
<mass value="10.0"/>
<inertia ixx="0.0860" ixy="0.0" ixz="0.0"
iyy="0.1367" iyz="0.0" izz="0.2083"/>
</inertial>
</link>
<!-- Left wheel -->
<link name="left_wheel">
<visual>
<geometry><cylinder radius="0.08" length="0.04"/></geometry>
<material name="dark"><color rgba="0.1 0.1 0.1 1.0"/></material>
</visual>
<collision><geometry><cylinder radius="0.08" length="0.04"/></geometry></collision>
<inertial>
<mass value="0.5"/>
<inertia ixx="0.001600" ixy="0.0" ixz="0.0"
iyy="0.001600" iyz="0.0" izz="0.003200"/>
</inertial>
</link>
<joint name="left_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="left_wheel"/>
<!-- Wheel axle is along Y-axis → rotate wheel 90° around X -->
<origin xyz="-0.1 0.17 -0.04" rpy="1.5708 0.0 0.0"/>
<axis xyz="0 0 1"/>
</joint>
<!-- Right wheel (mirror of left) -->
<link name="right_wheel">
<visual>
<geometry><cylinder radius="0.08" length="0.04"/></geometry>
<material name="dark"><color rgba="0.1 0.1 0.1 1.0"/></material>
</visual>
<collision><geometry><cylinder radius="0.08" length="0.04"/></geometry></collision>
<inertial>
<mass value="0.5"/>
<inertia ixx="0.001600" ixy="0.0" ixz="0.0"
iyy="0.001600" iyz="0.0" izz="0.003200"/>
</inertial>
</link>
<joint name="right_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="right_wheel"/>
<origin xyz="-0.1 -0.17 -0.04" rpy="1.5708 0.0 0.0"/>
<axis xyz="0 0 1"/>
</joint>
<!-- Caster wheel (ball caster — modeled as sphere with zero friction) -->
<link name="caster_link">
<visual>
<geometry><sphere radius="0.04"/></geometry>
<material name="gray"><color rgba="0.6 0.6 0.6 1.0"/></material>
</visual>
<collision><geometry><sphere radius="0.04"/></geometry></collision>
<inertial>
<mass value="0.1"/>
<inertia ixx="0.000064" ixy="0.0" ixz="0.0"
iyy="0.000064" iyz="0.0" izz="0.000064"/>
</inertial>
</link>
<joint name="caster_joint" type="fixed">
<parent link="base_link"/>
<child link="caster_link"/>
<origin xyz="0.15 0.0 -0.04" rpy="0.0 0.0 0.0"/>
</joint>
<!-- Lidar -->
<link name="lidar_link">
<visual>
<geometry><cylinder radius="0.035" length="0.065"/></geometry>
<material name="black"><color rgba="0.1 0.1 0.1 1.0"/></material>
</visual>
<collision><geometry><cylinder radius="0.035" length="0.065"/></geometry></collision>
<inertial>
<mass value="0.18"/>
<inertia ixx="0.0000868" ixy="0.0" ixz="0.0"
iyy="0.0000868" iyz="0.0" izz="0.0001103"/>
</inertial>
</link>
<joint name="lidar_joint" type="fixed">
<parent link="base_link"/>
<child link="lidar_link"/>
<origin xyz="0.0 0.0 0.095" rpy="0.0 0.0 0.0"/>
</joint>
</robot>Xacro — Parametric URDF
Xacro Basics — Properties, Macros, Math
Xacro eliminates URDF boilerplate: define constants once and compute inertias with math expressions.
<?xml version="1.0"?>
<robot name="my_robot" xmlns:xacro="http://www.ros.org/wiki/xacro">
<!-- ===== Properties (constants) ===== -->
<xacro:property name="base_length" value="0.4"/>
<xacro:property name="base_width" value="0.3"/>
<xacro:property name="base_height" value="0.12"/>
<xacro:property name="base_mass" value="10.0"/>
<xacro:property name="wheel_radius" value="0.08"/>
<xacro:property name="wheel_width" value="0.04"/>
<xacro:property name="wheel_mass" value="0.5"/>
<xacro:property name="wheel_offset_x" value="-0.1"/>
<xacro:property name="wheel_offset_y" value="0.17"/>
<xacro:property name="wheel_offset_z" value="-0.04"/>
<!-- ===== Inertia macros ===== -->
<xacro:macro name="box_inertia" params="m x y z">
<inertial>
<mass value="${m}"/>
<inertia
ixx="${m/12.0*(y*y + z*z)}" ixy="0.0" ixz="0.0"
iyy="${m/12.0*(x*x + z*z)}" iyz="0.0"
izz="${m/12.0*(x*x + y*y)}"/>
</inertial>
</xacro:macro>
<xacro:macro name="cylinder_inertia" params="m r l">
<inertial>
<mass value="${m}"/>
<inertia
ixx="${m/12.0*(3*r*r + l*l)}" ixy="0.0" ixz="0.0"
iyy="${m/12.0*(3*r*r + l*l)}" iyz="0.0"
izz="${m/2.0*r*r}"/>
</inertial>
</xacro:macro>
<!-- ===== Wheel macro (reused for left and right) ===== -->
<xacro:macro name="wheel" params="prefix y_reflect">
<link name="${prefix}_wheel">
<visual>
<geometry>
<cylinder radius="${wheel_radius}" length="${wheel_width}"/>
</geometry>
<material name="dark"><color rgba="0.1 0.1 0.1 1.0"/></material>
</visual>
<collision>
<geometry>
<cylinder radius="${wheel_radius}" length="${wheel_width}"/>
</geometry>
</collision>
<xacro:cylinder_inertia m="${wheel_mass}" r="${wheel_radius}" l="${wheel_width}"/>
</link>
<joint name="${prefix}_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="${prefix}_wheel"/>
<origin xyz="${wheel_offset_x} ${y_reflect*wheel_offset_y} ${wheel_offset_z}"
rpy="1.5708 0.0 0.0"/>
<axis xyz="0 0 1"/>
</joint>
</xacro:macro>
<!-- ===== Base link ===== -->
<link name="base_link">
<visual>
<geometry>
<box size="${base_length} ${base_width} ${base_height}"/>
</geometry>
<material name="blue"><color rgba="0.2 0.4 0.8 1.0"/></material>
</visual>
<collision>
<geometry>
<box size="${base_length} ${base_width} ${base_height}"/>
</geometry>
</collision>
<xacro:box_inertia m="${base_mass}"
x="${base_length}" y="${base_width}" z="${base_height}"/>
</link>
<!-- ===== Instantiate wheels ===== -->
<xacro:wheel prefix="left" y_reflect="1"/>
<xacro:wheel prefix="right" y_reflect="-1"/>
</robot>⚡ Convert a .xacro file to plain URDF: `xacro robot.urdf.xacro > robot.urdf`. Pass parameters with `xacro robot.urdf.xacro wheel_radius:=0.1`.
Xacro Include Files
Split a large robot description into per-component xacro files included in the main file.
<?xml version="1.0"?>
<!-- main.urdf.xacro -->
<robot name="full_robot" xmlns:xacro="http://www.ros.org/wiki/xacro">
<!-- Include shared macros (inertia helpers, materials, etc.) -->
<xacro:include filename="$(find my_robot_description)/urdf/macros.xacro"/>
<!-- Include sub-assemblies -->
<xacro:include filename="$(find my_robot_description)/urdf/base.xacro"/>
<xacro:include filename="$(find my_robot_description)/urdf/arm.xacro"/>
<xacro:include filename="$(find my_robot_description)/urdf/sensors.xacro"/>
<!-- Pass parameters to included macros -->
<xacro:arg name="use_gpu_lidar" default="false"/>
</robot>Launch robot_state_publisher
Launch robot_state_publisher with Xacro
Python launch file that processes the Xacro file and passes robot_description to robot_state_publisher.
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import Command, LaunchConfiguration, PathJoinSubstitution
from launch_ros.actions import Node
from launch_ros.substitutions import FindPackageShare
import os
def generate_launch_description():
pkg_share = FindPackageShare("my_robot_description")
urdf_file = PathJoinSubstitution([pkg_share, "urdf", "robot.urdf.xacro"])
robot_description_content = Command([
"xacro ", urdf_file,
" use_gpu_lidar:=", LaunchConfiguration("use_gpu_lidar"),
])
return LaunchDescription([
DeclareLaunchArgument(
"use_gpu_lidar", default_value="false",
description="Use GPU-accelerated lidar plugin"
),
Node(
package="robot_state_publisher",
executable="robot_state_publisher",
name="robot_state_publisher",
output="screen",
parameters=[{
"robot_description": robot_description_content,
"use_sim_time": False,
}],
),
# Publish dummy joint states for visualization (no controller)
Node(
package="joint_state_publisher_gui",
executable="joint_state_publisher_gui",
name="joint_state_publisher_gui",
),
])Validation & Visualization
URDF Check & Visualization Tools
Validate your URDF and visualize it in RViz2 from the command line.
# Install URDF tools
sudo apt install ros-jazzy-urdf-tutorial ros-jazzy-joint-state-publisher-gui
sudo apt install ros-jazzy-xacro ros-jazzy-robot-state-publisher
# Check URDF for syntax errors
check_urdf robot.urdf
# Output: robot name is: my_robot
# ---------- Successfully Parsed XML ---------------
# root Link: base_link has 3 child(ren)
# Convert Xacro to URDF and check in one step
xacro robot.urdf.xacro | check_urdf /dev/stdin
# Visualize in RViz2 (one command with joint slider GUI)
ros2 launch urdf_tutorial display.launch.py model:=$(pwd)/robot.urdf.xacro
# View TF tree generated from the URDF
ros2 run tf2_tools view_frames
# → Saves frames.pdf in current directory