Write links and joints in XML, then see it in RViz
— building one manipulator from scratch
A file that records a robot's shape in a machine-readable form. Several similar formats exist, so we sort out what each is for — and what URDF cannot express.
<link>, motion is <joint><?xml version="1.0" ?> <robot name="testbot"> <link name="base"/> <link name="link1"> ... </link> <joint name="base_joint" type="fixed"> <parent link="base"/> <child link="link1"/> </joint> </robot>
| format | what it describes | main tool |
|---|---|---|
URDF | links · joints — shape and motion | RViz |
SDF | simulation data, physics and world included | Gazebo |
SRDF | groups, path planning, collision checks | MoveIt |
gz sdf -p model.urdf > model.sdf gives you SDF, and Setup Assistant generates SRDF.
A link has exactly one parent. Closed loops — parallel mechanisms such as a delta robot — cannot be expressed in URDF.
that is why check_urdf prints a tree
Linked bodies are taken never to bend or stretch. Flexible parts like cables and springs are not supported.
solve those on the Gazebo plugin side
A robot that moves like a human arm is called a manipulator. Before writing XML, we look at how it splits into parts and which of them URDF actually cares about.
Arrows run parent → child. The side nearer the base is always the parent, and links and joints alternate as the arm gets longer.
Name · shape · mass (kg) · moment of inertia (kg·m²).
Shapes default to primitives — cylinder, cone, box. Complex bodies come from stl·dae meshes.
Name · type · axis of motion · min/max values · effort and velocity.
It always describes the relation between two links. A joint alone means nothing.
By convention a ROS robot model lives in a robotname_description package.
We create testbot_description and put the outermost URDF skeleton inside it.
robotname_description<robot> is a list of links and a list of joints<?xml version="1.0" ?> <robot name="testbot"> <!-- parts: what exists --> <link name="base"/> <link name="link1"> ... </link> <link name="link2"> ... </link> <!-- connections: how they attach and move --> <joint name="base_joint" type="fixed"> ... </joint> <joint name="link1_link2" type="revolute"> ... </joint> </robot>
<robot> is declared first as the document root. Links connect only through joints.
How to write one link properly. The shape you see, the shape that collides, and the mass and inertia used for physics — what each is, and why they are kept apart.
<visual> — the shape drawn on screen<collision> — the volume used for collision checks<inertial> — mass and inertia tensor, for physics<origin> and <geometry><link name="link1"> <visual> <origin xyz="0 0 0.25" rpy="0 0 0"/> <geometry><box size="0.1 0.1 0.5"/></geometry> <material name="green"/> </visual> <collision> ... </collision> <inertial> ... </inertial> </link>
<visual> is enough.
box·cylinder·sphere by default. Awkward shapes load from CAD files such as STL or DAE.
the point is looking right
Same tags, different purpose. It is sometimes made larger than the visual to leave a safety margin.
the point is being fast and safe
<visual>, plain boxes and cylinders in <collision>. It looks good and collision stays cheap.
<!-- lift a 0.5-long box by half in z --> <origin xyz="0.0 0.0 0.25" rpy="0.0 0.0 0.0"/> <geometry> <box size="0.1 0.1 0.5"/> </geometry> <!-- yaw 90 deg = pi/2 = 1.5708 rad --> <origin xyz="0 0 0" rpy="0 0 1.5708"/>
| 3×3 inertia matrix | x | y | z |
|---|---|---|---|
| x | ixx | ixy | ixz |
| y | ixy | iyy | iyz |
| z | ixz | iyz | izz |
ixy = iyx, the six values ixx · ixy · ixz · iyy · iyz · izz are enough. <mass> in kg, <inertia> in kg·m².
<!-- define once near the top of the robot --> <material name="green"> <color rgba="0 0.6 0 1" /> </material> <material name="orange"> <color rgba="1.0 0.4 0.0 1.0"/> </material> <!-- a link refers to it by name --> <visual> ... <material name="green"/> </visual>
<texture> can map a png instead.
A joint connects two links and decides how that connection may move. We look at the six types, plus the axis and limit values that shape the real motion.
| type | motion | analogy |
|---|---|---|
fixed | does not move | base bolted to link1 |
revolute | rotation with angle limits | a fan panning side to side |
continuous | unlimited continuous rotation | a car wheel |
prismatic | straight travel along one axis | a sliding drawer |
floating | 6-DoF travel + rotation | a free-floating body |
planar | travel + rotation on one plane | an object gliding on a plane |
<parent>·<child> — the side nearer the base is the parent<origin> — joint position relative to the parent linktype decides how this connection moves<joint name="link1_link2" type="revolute"> <origin xyz="0.0 0.0 0.5" rpy="0.0 0.0 0.0"/> <parent link="link1"/> <child link="link2"/> <axis xyz="0 0 1"/> <limit lower="-2.617" upper="2.617" effort="30.0" velocity="1.571"/> </joint>
link1_link2, and you can read the tree by eye.
<axis> and <limit> decide the real motionaxis xyz="0 0 1" — rotates about z. Pans like a fan's neckaxis xyz="0 1 0" — rotates about y. Bends up and down like an elbowlower·upper — rotation range, in radianseffort — force at the joint (N), velocity — speed (rad/s)<!-- joint that turns like a neck --> <axis xyz="0.0 0.0 1.0"/> <!-- joint that bends like an elbow --> <axis xyz="0.0 1.0 0.0"/> <limit lower="-2.617" upper="2.617" effort="30.0" velocity="1.571"/>
The file alone tells you little. Launch three nodes and the robot appears on screen, with sliders to turn its joints by hand.
Arrows show which way topics flow. The URDF itself is handed to robot_state_publisher up front via the robot_description parameter.
def generate_launch_description(): urdf_file = os.path.join( get_package_share_directory('testbot_description'), 'urdf', 'testbot.urdf') with open(urdf_file, 'r') as infp: robot_description_file = infp.read() robot_state_publisher = Node( package='robot_state_publisher', executable='robot_state_publisher', parameters=[{'robot_description': robot_description_file}]) joint_state_publisher_gui = Node( package='joint_state_publisher_gui', executable='joint_state_publisher_gui') rviz2 = Node( package='rviz2', executable='rviz2', arguments=['-d', rviz_display_config_file]) return LaunchDescription([robot_state_publisher, joint_state_publisher_gui, rviz2])
robot_description parameter. The rest is boilerplate that starts nodes.
| node | subscribes | publishes |
|---|---|---|
joint_state_publisher | — | /joint_states |
robot_state_publisher | /joint_states | /tf · /tf_static · /robot_description |
rviz2 | /tf · /robot_description | — |
alias cba='colcon build --symlink-install' and the loop gets much lighter.
# install the urdf_tutorial package $ sudo apt install ros-humble-urdf-tutorial # pass only the model path and RViz opens $ ros2 launch urdf_tutorial display.launch.py \ model:=/home/parallels/robot_ws/src/testbot_description/urdf/testbot.urdf
Commands that check syntax and the tree before RViz ever opens,
plus xacro, which cuts repetition once a URDF gets long.
check_urdf — syntax and link tree at once| tool | what it shows |
|---|---|
check_urdf | syntax errors and the link tree |
urdf_to_graphiz | link · joint relations and relative transforms as a PDF diagram |
ros2 run tf2_tools view_frames | the running TF tree as a PDF |
rqt | how nodes and topics are actually wired |
| URDF for VSCode | syntax highlighting and snippets for .urdf·.xacro |
rviz2 | the final render — where you really check |
xacro turns repeated XML into macros<xacro:property> — manage a value by namepi·radians(x)·sqrt(x) are available<xacro:macro> — link · joint templates taking arguments<xacro:if> — insert a different block by condition<xacro:property name="the_radius" value="2.1" /> <geometry type="cylinder" radius="${the_radius}" /> <!-- no need to compute radians by hand --> <xacro:property name="upper" value="${radians(150)}"/> <xacro:macro name="link_box" params="suffix"> <link name="link_${suffix}"> ... </link> </xacro:macro> <xacro:link_box suffix="1" />
<link> for parts, <joint> for connection and motion.
visual·collision·inertial·origin·axis·limit are all details of those two
Changing values and relaunching is the fastest way to understand URDF.
but check the tree with check_urdf first
/joint_states, combines it with the URDF into transforms, and publishes /tf·/tf_static. The joint values themselves come from joint_state_publisher. → Slide 23 · 25check_urdf for syntax and tree, then ros2 launch urdf_tutorial display.launch.py model:=... to put it straight into RViz. → Slide 27 · 29