In ROS2, if you have ever worked with coordinate transformations, you’ve likely encountered the acronym URDF at some point. It stands for Unified Robot Description Format. Sounds formal, almost intimidating, doesn’t it? Yet behind that technical name lies something surprisingly straightforward and incredibly powerful—the way ROS2 understands what your robot looks like, how its parts fit together, and how they move.
Think of URDF as the architectural blueprint for your robot’s digital twin. Without it, ROS2 can’t simulate your creation, visualize it, or plan its movements effectively. Whether you’re working on a robotic arm, a wheeled rover, or a complex humanoid, URDF is the essential language that bridges mechanical design with software.
In this post, we’ll understand what URDF is, how it functions conceptually, and then dive into hands-on examples. You’ll learn to define your first URDF file and visualize it using RViz2. By the end, you’ll be well-equipped to build your own robot models and bring them to life in ROS2.
By the way, if you’re new to ROS2, you might first want to review this Robot Operating System (ROS) guide before proceeding with this blog.
Table of Contents
What is URDF?
Imagine you’re assembling a LEGO robot. Each piece—the wheels, the arms, the sensors—has its own size, shape, and importantly, its own coordinate system. In the real world, every robot part occupies space with its own origin point and orientation, and these local frames need to fit together perfectly to form the whole machine. URDF is essentially your robot’s blueprint in ROS2, written in XML, that not only defines these physical parts but also carefully maps out how their individual coordinate systems relate to one another.
Why does this matter? Because your robot’s brain (ROS2) isn’t just interested in isolated parts; it needs to understand the entire robot as a coherent system where every link’s position and orientation are defined relative to others. This spatial hierarchy—how one link’s coordinate frame connects to the next via joints—is crucial. URDF manages this complexity by describing both the physical attributes and the transforms between coordinate frames, allowing ROS2 to accurately interpret and visualize the robot’s structure.
This coordination enables everything from realistic simulations to motion planning and visualization. Without URDF, your robot would be like a brain without a body—unable to make sense of its own physical form or how its parts move in relation to one another.
So, what goes into a URDF?
- Links: These are the robot’s rigid bodies. Think of them as the LEGO bricks.
- Joints: How links connect and move relative to each other. This includes revolute joints (like elbows), fixed joints (glued LEGO bricks), prismatic joints (sliding parts), etc.
- Visuals: What each link looks like—shapes, colors, meshes.
- Collisions: Simplified shapes used for physics simulation to detect when your robot bumps into things.
- Inertial Properties: Mass and inertia, so the physics engine can simulate motion realistically.
URDF files are XML, which means they’re human-readable but can get verbose. They typically live in your robot’s description package. When you launch your robot in simulation or visualization, ROS2 parses this URDF to spawn the model. Tools like xacro (which stands for XML Macros) help by letting you write more maintainable and reusable URDF files, but let’s not get ahead of ourselves. First, you gotta understand the basics.
Let’s Get Practical: Building Your First URDF
In this tutorial, we’re going to create a simple robot vehicle — nothing fancy, just enough to show you how URDF ties links and joints together into a coherent structure. Think of it as your “Hello World” in robot description.
Before we begin, you’ll need two very handy packages in your ROS2 workspace:
urdf_tutorial: This is the go-to collection of URDF examples and helper launch files. It’s a great resource because it comes with ready-made configurations for loading and viewing URDFs in RViz2. Instead of reinventing the wheel, you get a quick way to visualize your models as you build them.
urdf_launch: This package provides modern ROS2-style launch files that make it easier to load URDF models into the ROS ecosystem. It’s lightweight, but it saves you from writing a bunch of boilerplate every time you want to test your robot in RViz2.
Moreover, before starting, it is important to understand the vehicle coordinate system. The image below shows the standard X, Y and Z-axis of a standard vehicle along with corresponding rotations along individual axis.

Understanding the coordinate system will help us define the links of the robot correctly.
Now that we have the tools and information of the coordinate system, let’s start small by defining the most fundamental building block of any robot: the base link. This will serve as the “root” of our vehicle’s body — the frame everything else (wheels, sensors, arms, whatever you dream up) will eventually attach to.
Step 0: Creating a Description File for our Vehicle Robot:
The urdf_tutorial contains several sample URDF files in the urdf/ folder. Sure, one can use any of those URDF files too, but we want to create a robot of our own. So, in that folder, create a new file called simple_vehicle.urdf. This step is necessary as when we run the package to visualize the robot, it searches for the file in that particular folder.
Step 1: The Chassis (a.k.a. base_link)
Every URDF model needs a root link, and in most robots that role is played by the chassis or the body frame. Think of it as the anchor point: all other links (wheels, sensors, arms, antennas, coffee cup holders) will eventually hang off this root. Without it, your URDF is just floating XML with no reference.
Let’s define a simple rectangular box to serve as the chassis of our car. We’ll call it base_link. Inside urdf/, create a file named simple_vehicle.urdf:
<?xml version="1.0"?>
<robot name="simple_vehicle">
<link name="base_link">
<visual>
<origin xyz="0 0 0" rpy="0 0 0"/>
<geometry>
<box size="2.0 1.2 0.4"/>
</geometry>
<material name="red">
<color rgba="0.8 0.1 0.1 1.0"/>
</material>
</visual>
<collision>
<origin xyz="0 0 0" rpy="0 0 0"/>
<geometry>
<box size="2.0 1.2 0.4"/>
</geometry>
</collision>
<inertial>
<origin xyz="0 0 0" rpy="0 0 0"/>
<mass value="200.0"/>
<inertia ixx="16.0" iyy="66.7" izz="80.7" ixy="0" ixz="0" iyz="0"/>
</inertial>
</link>
</robot>Breaking it Down
- Visual: This is what you’ll actually see in RViz2. Here it’s a red box, sized 2.0 x 1.2 x 0.4 meters, with its center placed half a meter above the ground (xyz=”0 0 0.5″).
- Collision: This defines how the robot interacts physically in simulation. In this case, it’s the same box shape as the visual. Sometimes you might use simpler geometry for collisions (like a box around a complex mesh) to speed up physics computations.
- Inertial: This block defines mass and inertia. If you ever run this in a physics engine like Gazebo, these numbers determine how your robot behaves under forces. Ignore them, and your robot will feel like a ragdoll with no weight.
That’s it — the most minimal robot-vehicle body you can define in URDF. Right now, it’s a red box floating in the void, but this base_link is the foundation we’ll attach wheels to next. But let us go ahead and visualize this base_link first. Of course, we need to build the packages first:
cd ros2_ws
colcon build
source install/setup.bash
ros2 launch urdf_tutorial display.launch.py model:=urdf/simple_vehicle.urdfIf all went well, you’ll see a glorious red rectangle suspended in RViz2’s 3D world, like this:

Step 2: Adding the Wheels
Next, we will add a wheel to our ‘chassis’ (just one wheel for now; the remaining should be easy for us as it is just about copy-paste). To the simple_vehicle.urdf file, we will add a new link called 'fl_wheel':
<!-- Left front wheel -->
<link name="fl_wheel">
<visual>
<origin xyz="0.0 0.0 0.0" rpy="1.5708 0 0"/>
<geometry>
<cylinder radius="0.3" length= "0.2"/>
</geometry>
<material name="black">
<color rgba="0.1 0.1 0.1 1.0"/>
</material>
</visual>
<collision>
<origin xyz="0.0 0.0 0.0" rpy="1.5708 0 0"/>
<geometry>
<cylinder radius="0.3" length="0.2"/>
</geometry>
</collision>
<inertial>
<mass value="10.0"/>
<inertia ixx="0.45" iyy="0.45" izz="0.25" ixy="0" ixz="0" iyz="0"/>
</inertial>
</link>The link defines the left-front wheel. Since a wheel is naturally cylindrical, the <geometry> element uses a <cylinder> shape with a radius of 0.3 meters and a thickness of 0.2 meters. By default, cylinders in URDF are aligned along the z-axis, but wheels in vehicle coordinate system rotate around the y-axis.
To correct this, the <origin> tag applies a rotation of 1.5708 radians (90 degrees) about the x-axis (roll). Both the visual and collision elements are defined in the same way to ensure what is seen in RViz corresponds to how the simulator handles physical interactions. The wheel is given a dark material to distinguish it from the chassis. Finally, the inertial properties are specified, assigning a mass of 10 kg and approximate inertia values. These values don’t need to be exact for visualization purposes, but they establish the physical consistency of the wheel so that later, when joints are defined, the wheel behaves realistically within the simulation.
We are not done yet. If you notice, the origin of the wheel is set at xyz = 0 0 0. This origin corresponds the wheel coordinate system or the fl_wheel link. Because base_link is the main coordinate frame of the vehicle, it becomes the main parent frame and all the other coordinate frames are tagged as child frame. The relation between the parent frame (base_link) and this child frame (fl_wheel) is defined in the URDF as follows:
<joint name="fl_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="fl_wheel"/>
<origin xyz="0.9 0.8 0.0"/>
</joint>The <origin> tag in the <joint> section determines the position of the child frame relative to the parent frame. In this case, the coordinate frame of the front left wheel is positioned 1 m in X-direction and 0.7 m in Y-direction relative to the base_link.
This link definition provides the basis upon which the wheel can then be connected to the chassis using a joint. Save this updated URDF, build the workspace and run the ros2 command again (always remember to build your workspace after any change in the URDF file otherwise ROS would not consider the changes made):
cd ros2_ws
colcon build
ros2 launch urdf_tutorial display.launch.py model:=urdf/simple_vehicle.urdfYou should see something like this:

With the first wheel defined and connected to the chassis, we can now extend the same logic to create the remaining three wheels. The process is straightforward: copy the wheel’s link definition and its joint, then adjust two things—first, the link and joint names so that each wheel has a unique identifier, and second, the <origin> values in the <joint> to place the wheels correctly around the chassis. The geometry, material, and inertial properties remain unchanged since all four wheels are identical. Once these adjustments are made, the robot takes shape as a complete four-wheeled vehicle. If everything is set up correctly, RViz will display the chassis with its four wheels positioned in place, resembling a simple car.

If any of the wheel is misaligned, try changing the values till you get it correctly before peeking into my URDF file that is provided below:
<?xml version="1.0"?>
<robot name="simple_vehicle">
<link name="base_link">
<visual>
<origin xyz="0 0 0" rpy="0 0 0"/>
<geometry>
<box size="2.0 1.2 0.4"/>
</geometry>
<material name="red">
<color rgba="1.0 0.2 0.2 1.0"/>
</material>
</visual>
<collision>
<origin xyz="0 0 0" rpy="0 0 0"/>
<geometry>
<box size="2.0 1.2 0.4"/>
</geometry>
</collision>
<inertial>
<origin xyz="0 0 0" rpy="0 0 0"/>
<mass value="200.0"/>
<inertia ixx="16.0" iyy="66.7" izz="80.7" ixy="0" ixz="0" iyz="0"/>
</inertial>
</link>
<!-- Left front wheel -->
<link name="fl_wheel">
<visual>
<origin xyz="0.0 0.0 0.0" rpy="1.5708 0 0"/>
<geometry>
<cylinder radius="0.3" length= "0.2"/>
</geometry>
<material name="black">
<color rgba="0.1 0.1 0.1 1.0"/>
</material>
</visual>
<collision>
<origin xyz="0.0 0.0 0.0" rpy="1.5708 0 0"/>
<geometry>
<cylinder radius="0.3" length="0.2"/>
</geometry>
</collision>
<inertial>
<mass value="10.0"/>
<inertia ixx="0.45" iyy="0.45" izz="0.25" ixy="0" ixz="0" iyz="0"/>
</inertial>
</link>
<joint name="fl_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="fl_wheel"/>
<origin xyz="0.9 0.8 0.0"/>
</joint>
<!-- Right front wheel -->
<link name="fr_wheel">
<visual>
<origin xyz="0.0 0.0 0.0" rpy="1.5708 0 0"/>
<geometry>
<cylinder radius="0.3" length= "0.2"/>
</geometry>
<material name="black">
<color rgba="0.1 0.1 0.1 1.0"/>
</material>
</visual>
<collision>
<origin xyz="0.0 0.0 0.0" rpy="1.5708 0 0"/>
<geometry>
<cylinder radius="0.3" length="0.2"/>
</geometry>
</collision>
<inertial>
<mass value="10.0"/>
<inertia ixx="0.45" iyy="0.45" izz="0.25" ixy="0" ixz="0" iyz="0"/>
</inertial>
</link>
<joint name="fr_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="fr_wheel"/>
<origin xyz="0.9 -0.8 0.0"/>
</joint>
<!-- Left rear wheel -->
<link name="rl_wheel">
<visual>
<origin xyz="0.0 0.0 0.0" rpy="1.5708 0 0"/>
<geometry>
<cylinder radius="0.3" length= "0.2"/>
</geometry>
<material name="black">
<color rgba="0.1 0.1 0.1 1.0"/>
</material>
</visual>
<collision>
<origin xyz="0.0 0.0 0.0" rpy="1.5708 0 0"/>
<geometry>
<cylinder radius="0.3" length="0.2"/>
</geometry>
</collision>
<inertial>
<mass value="10.0"/>
<inertia ixx="0.45" iyy="0.45" izz="0.25" ixy="0" ixz="0" iyz="0"/>
</inertial>
</link>
<joint name="rl_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="rl_wheel"/>
<origin xyz="-0.9 0.8 0.0"/>
</joint>
<!-- Right rear wheel -->
<link name="rr_wheel">
<visual>
<origin xyz="0.0 0.0 0.0" rpy="1.5708 0 0"/>
<geometry>
<cylinder radius="0.3" length= "0.2"/>
</geometry>
<material name="black">
<color rgba="0.1 0.1 0.1 1.0"/>
</material>
</visual>
<collision>
<origin xyz="0.0 0.0 0.0" rpy="1.5708 0 0"/>
<geometry>
<cylinder radius="0.3" length="0.2"/>
</geometry>
</collision>
<inertial>
<mass value="10.0"/>
<inertia ixx="0.45" iyy="0.45" izz="0.25" ixy="0" ixz="0" iyz="0"/>
</inertial>
</link>
<joint name="rr_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="rr_wheel"/>
<origin xyz="-0.9 -0.8 0.0"/>
</joint>
</robot>And there we have it—a complete, four-wheeled robot modeled in vanilla URDF. It’s not the most glamorous vehicle in the world, but it’s structurally sound, it shows up in RViz, and it teaches you the fundamentals of how links, joints, and coordinate frames come together to describe a robot. By this point, you’ve probably noticed that the file is starting to look a little bloated. Copy-pasting the same wheel definition four times works fine for a simple demo, but as your robots grow more complex, this quickly becomes a mess to manage.
Leveling Up: Why Use Xacro?
I promised to keep it simple, but if you’re serious about robot modeling, you’ll want to avoid copy-pasting XML snippets everywhere. That’s where xacro comes in — an XML macro processor.
It lets you define parameters, loops, and includes in your URDF, making your life easier when your robot grows more complex than a two-link arm. For example, you can define link dimensions once and reuse them or generate multiple joints in a loop.
In the next post, we’ll take everything we’ve done here and rewrite it using Xacro macros, which allow us to keep our robot description clean, modular, and flexible. Instead of maintaining a giant XML blob, we’ll build a neat, reusable structure that makes scaling to larger robots (or robots with more than four wheels) far easier.
What’s Next?
Now that you’ve seen how to build a simple URDF robot and visualize it, I challenge you to try making your own. Maybe a three-link arm, or a robot that looks like your favorite kitchen appliance, or even an R2D2. Play with joint types and limits. Try integrating meshes instead of primitive shapes. When you’re ready to show off, take a screenshot your RViz2 and share it on Instagram, tagging me @machinelearningsite. I’m curious to see what wild robots you dream up.
As I mentioned above, we will rewrite our robot using Xacro which is more practical and widely used. Eventually, we are going to write a node that will move the vehicle as per the given commands. That is the ultimate goal of this short series on ROS2 vehicle robot. So stay tuned and follow me on Instagram @machinelearningsite to stay updated with this and other cool projects. And again, don’t forget to share your robots with me!

Pingback: ROS2 Tutorial: Step-by-Step Xacro Guide for effective Robot Modeling (Part 2) - Machine Learning Site