Intermediate ROS2: Simplify URDF with Xacro

Objective: Learn techniques for reducing URDF file code using Xacro

Tutorial level: Intermediate

Time: 20 minutes

Table of Contents

  • Using Xacro

  • Constants

  • Mathematics

  • Macros

    • Simple Macros
    • Parameterized Macros
  • Practical Usage

    • Leg Macro

So far, if you've been designing your own robot at home folowing these steps, you might have grown tired of performing various mathematical calculations to correctly position a very simple robot description. Fortunately, you can use the xacro package to simplify your life. It does three very useful things.

  • Constants
  • Simple math
  • Macros

In thiss tutorial, we will look at all these shortcuts to help reduce the overall size of the URDF file and make it easier to read and maintain.

Using Xacro

As the name suggests, xacro https://index.ros.org/p/xacro/ is a macro language for XML. The xacro program runs all macros and outputs the result. A typical usage is as follows:

xacro model.xacro > model.urdf

You can also generate urdf within a launch file. This is convenient because it keeps the file up to date and doesn't take up disk space. However, generating it takes time, so be aware that your launch file may take longer to start.

To run xacro in a launch file, you need to replace the Command as a parameter for robot_state_publisher.

# Get the shared path of 'turtlebot3_description' package and concatenate to URDF file path
path_to_urdf = get_package_share_path('turtlebot3_description') / 'urdf' / 'turtlebot3_burger.urdf'


# Create a ROS node to publish the robot state
robot_state_publisher_node = launch_ros.actions.Node(
    package='robot_state_publisher',  # Specify the package of the node
    executable='robot_state_publisher',  # Specify the executable file
    parameters=[{
        'robot_description': ParameterValue(
            Command(['xacro ', str(path_to_urdf)]), value_type=str  # Use xacro command to process the URDF file
        )
    }]
)

A simpler way to load the robot model is to use the urdf_launch package to automatically load xacro/urdf.

from launch import LaunchDescription  # Import LaunchDescription class from launch module
from launch.actions import IncludeLaunchDescription  # Import IncludeLaunchDescription class from launch.actions module
from launch.substitutions import PathJoinSubstitution  # Import PathJoinSubstitution class from launch.substitutions module
from launch_ros.substitutions import FindPackageShare  # Import FindPackageShare class from launch_ros.substitutions module


def generate_launch_description():
    ld = LaunchDescription()  # Create a LaunchDescription object


    # Add an IncludeLaunchDescription action to include another launch file
    ld.add_action(IncludeLaunchDescription(
        PathJoinSubstitution([FindPackageShare('urdf_launch'), 'launch', 'display.launch.py']),  # Specify the path of the launch file to include
        launch_arguments={  # Specify arguments passed to the launch file
            'urdf_package': 'turtlebot3_description',  # Name of the urdf package
            'urdf_package_path': PathJoinSubstitution(['urdf', 'turtlebot3_burger.urdf'])  # Path to the urdf file
        }.items()  # Convert the argument dictionary to items object
    ))
    return ld  # Return the LaunchDescription object

At the top of the URDF file, you must specify a namespace so the file can parse correctly. For example, these are the first two lines of a valid xacro file:

<?xml version="1.0"?>
<robot xmlns:xacro="http://www.ros.org/wiki/xacro" name="firefighter">

Constants

Let's quickly look at the base_link in R2D2.

<link name="base_link">
  <visual>
    <geometry>
      <cylinder length="0.6" radius="0.2"/>
    </geometry>
    <material name="blue"/>
  </visual>
  <collision>
    <geometry>
      <cylinder length="0.6" radius="0.2"/>
    </geometry>
  </collision>
</link>

The information is somewhat redundant here. We specified the cylinder length and radius twice. Worse, if we want to change it, we have to change it in two different places.

Fortunately, xacro allows you to define properties as constants. Instead, we can write the above code.

<xacro:property name="width" value="0.2" />
<xacro:property name="bodylen" value="0.6" />
<link name="base_link">
    <visual>
        <geometry>
            <cylinder radius="${width}" length="${bodylen}"/>
        </geometry>
        <material name="blue"/>
    </visual>
    <collision>
        <geometry>
            <cylinder radius="${width}" length="${bodylen}"/>
        </geometry>
    </collision>
</link>
  • These two values are defined in the first two lines. They can be defined anywhere (as long as it is valid XML), at any level, before or after use. Usually, they are at the top.
  • Instead of specifying the actual radius in the geometry element, we use a dollar sign and braces to indicate the value.
  • This code generates the same code shown above.

The value inside the ${} construct is used to replace the ${}. This means you can combine it with other text in the property.

<xacro:property name="robotname" value="marvin" />
<link name="${robotname}s_leg" />

This will generate

<link name="marvins_leg" />

However, the content in side the ${} doesn't have to be just a property, which leads us to our next point...

Mathematics

You can build arbitrarily complex expressions using four basic operations (+, -, *, /), unary minus, and parentheses in the ${} construct. Examples:

<cylinder radius="${wheeldiam/2}" length="0.1"/>
<origin xyz="${reflect*(width+.02)} 0 0.25" />

You can also use more operations than basic math, such as sin and cos.

Macros

This is the largest and most useful component in the xacro package.

Simple Macros

Let's look at a simple useless macro.

<xacro:macro name="default_origin">
    <origin xyz="0 0 0" rpy="0 0 0"/>
</xacro:macro>
<xacro:default_origin />

(This is useless because if the origin is not specified, it is the same.) This code will generate the following content.

<origin rpy="0 0 0" xyz="0 0 0"/>
  • The name is not technically a required element, but you need to specify it to use it.
  • Each <xacro:$NAME /> instance is replaced by the content of the xacro:macro tag.
  • Note that although they are not exactly the same (the order of two attributes is swapped), the generated XML is equivalent.
  • If no xacro with the specified name is found, it will not be expanded, and no error will be generated.

Parameterized Macros

You can also parameterize macros so that they do not generate the same text every time. This becomes even more powerful when combined with math functions.

First, let's look at a simple macro used in R2D2.

<xacro:macro name="default_inertial" params="mass">
    <!-- Define inertial parameters -->
    <inertial>
        <!-- Mass parameter, value determined by the mass parameter passed in -->
        <mass value="${mass}" />
        <!-- Inertia matrix parameters -->
        <inertia ixx="1e-3" ixy="0.0" ixz="0.0"
                 iyy="1e-3" iyz="0.0"
                 izz="1e-3" />
    </inertial>
</xacro:macro>

This can be used with the code

<xacro:default_inertial mass="10"/>
  • The parameters act like attributes, and you can use them in expressions.

You can also use entire blocks as parameters.

<xacro:macro name="blue_shape" params="name *shape">
    <!-- Define a macro named blue_shape with name and shape parameters -->
    <link name="${name}">  <!-- Define a link element with name determined by the name parameter -->
        <visual>  <!-- Define visual elements -->
            <geometry>  <!-- Define geometric shapes -->
                <xacro:insert_block name="shape" />  <!-- Insert the geometric shape specified by the shape parameter -->
            </geometry>
            <material name="blue"/>  <!-- Define material as blue -->
        </visual>
        <collision>  <!-- Define collision elements -->
            <geometry>  <!-- Define geometric shapes -->
                <xacro:insert_block name="shape" />  <!-- Insert the geometric shape specified by the shape parameter -->
            </geometry>
        </collision>
    </link>
</xacro:macro>


<xacro:blue_shape name="base_link">  <!-- Use blue_shape macro to define a link named base_link -->
    <cylinder radius=".42" length=".01" />  <!-- Define a cylindrical shape with radius 0.42 and length 0.01 -->
</xacro:blue_shape>
  • To specify block parameters, include an asterisk before the parameter name.
  • You can use the insert_block command to insert a block.
  • Feel free to insert the block.

Practical Usage

The xacro language is quite flexible in allowing you to perform operations. Here are some useful methods of using xacro in the R2D2 model, in addition to the default inertial macro shown above.

To see the model generated by the xacro file https://github.com/ros/urdf_tutorial/blob/ros2/urdf/08-macroed.urdf.xacro, run the same command as in the previous tutorial:

ros2 launch urdf_tutorial display.launch.py model:=urdf/08-macroed.urdf.xacro
<?xml version="1.0"?>
<!-- Define a robot model named "macroed" -->
<robot name="macroed" xmlns:xacro="http://ros.org/wiki/xacro">


  <!-- Define some properties -->
  <xacro:property name="width" value="0.2" /> <!-- Width -->
  <xacro:property name="leglen" value="0.6" /> <!-- Leg length -->
  <xacro:property name="polelen" value="0.2" /> <!-- Pole length -->
  <xacro:property name="bodylen" value="0.6" /> <!-- Body length -->
  <xacro:property name="baselen" value="0.4" /> <!-- Base length -->
  <xacro:property name="wheeldiam" value="0.07" /> <!-- Wheel diameter -->


  <!-- Define some materials -->
  <material name="blue"> <!-- Blue material -->
    <color rgba="0 0 0.8 1"/>
  </material>


  <material name="black"> <!-- Black material -->
    <color rgba="0 0 0 1"/>
  </material>


  <material name="white"> <!-- White material -->
    <color rgba="1 1 1 1"/>
  </material>


  <!-- Define a macro for default inertia -->
  <xacro:macro name="default_inertial" params="mass">
    <inertial>
      <mass value="${mass}" />
      <inertia ixx="1e-3" ixy="0.0" ixz="0.0" iyy="1e-3" iyz="0.0" izz="1e-3" />
    </inertial>
  </xacro:macro>


  <!-- Define base link -->
  <link name="base_link">
    <visual>
      <geometry>
        <cylinder radius="${width}" length="${bodylen}"/> <!-- Geometry is a cylinder -->
      </geometry>
      <material name="blue"/> <!-- Use blue material -->
    </visual>
    <collision>
      <geometry>
        <cylinder radius="${width}" length="${bodylen}"/> <!-- Collision geometry is also a cylinder -->
      </geometry>
    </collision>
    <xacro:default_inertial mass="10"/> <!-- Use default inertia, mass of 10 -->
  </link>


  <!-- Define a macro for wheels -->
  <xacro:macro name="wheel" params="prefix suffix reflect">


    <link name="${prefix}_${suffix}_wheel">
      <visual>
        <origin xyz="0 0 0" rpy="${pi/2} 0 0" />
        <geometry>
          <cylinder radius="${wheeldiam/2}" length="0.1"/> <!-- Geometry is a cylinder -->
        </geometry>
        <material name="black"/> <!-- Use black material -->
      </visual>
      <collision>
        <origin xyz="0 0 0" rpy="${pi/2} 0 0" />
        <geometry>
          <cylinder radius="${wheeldiam/2}" length="0.1"/> <!-- Collision geometry is also a cylinder -->
        </geometry>
      </collision>
      <xacro:default_inertial mass="1"/> <!-- Use default inertia, mass of 1 -->
    </link>
    <joint name="${prefix}_${suffix}_wheel_joint" type="continuous">
      <axis xyz="0 1 0" rpy="0 0 0" />
      <parent link="${prefix}_base"/>
      <child link="${prefix}_${suffix}_wheel"/>
      <origin xyz="${baselen*reflect/3} 0 -${wheeldiam/2+.05}" rpy="0 0 0"/>
    </joint>


  </xacro:macro>


  <!-- Define a macro for legs -->
  <xacro:macro name="leg" params="prefix reflect">
    <link name="${prefix}_leg">
      <visual>
        <geometry>
          <box size="${leglen} 0.1 0.2"/> <!-- Geometry is a box -->
        </geometry>
        <origin xyz="0 0 -${leglen/2}" rpy="0 ${pi/2} 0"/>
        <material name="white"/> <!-- Use white material -->
      </visual>
      <collision>
        <geometry>
          <box size="${leglen} 0.1 0.2"/> <!-- Collision geometry is also a box -->
        </geometry>
        <origin xyz="0 0 -${leglen/2}" rpy="0 ${pi/2} 0"/>
      </collision>
      <xacro:default_inertial mass="10"/> <!-- Use default inertia, mass of 10 -->
    </link>


    <joint name="base_to_${prefix}_leg" type="fixed">
      <parent link="base_link"/>
      <child link="${prefix}_leg"/>
      <origin xyz="0 ${reflect*(width+.02)} 0.25" />
    </joint>


    <link name="${prefix}_base">
      <visual>
        <geometry>
          <box size="${baselen} 0.1 0.1"/> <!-- Geometry is a box -->
        </geometry>
        <material name="white"/> <!-- Use white material -->
      </visual>
      <collision>
        <geometry>
          <box size="${baselen} 0.1 0.1"/> <!-- Collision geometry is also a box -->
        </geometry>
      </collision>
      <xacro:default_inertial mass="10"/> <!-- Use default inertia, mass of 10 -->
    </link>


    <joint name="${prefix}_base_joint" type="fixed">
      <parent link="${prefix}_leg"/>
      <child link="${prefix}_base"/>
      <origin xyz="0 0 ${-leglen}" />
    </joint>
    <xacro:wheel prefix="${prefix}" suffix="front" reflect="1"/> <!-- Create front wheel -->
    <xacro:wheel prefix="${prefix}" suffix="back" reflect="-1"/> <!-- Create back wheel -->
  </xacro:macro>
  <xacro:leg prefix="right" reflect="-1" /> <!-- Create right leg -->
  <xacro:leg prefix="left" reflect="1" /> <!-- Create left leg -->


  <joint name="gripper_extension" type="prismatic">
    <parent link="base_link"/>
    <child link="gripper_pole"/>
    <limit effort="1000.0" lower="-${width*2-.02}" upper="0" velocity="0.5"/>
    <origin rpy="0 0 0" xyz="${width-.01} 0 0"/>
  </joint>


  <link name="gripper_pole">
    <visual>
      <geometry>
        <cylinder length="${polelen}" radius="0.01"/> <!-- Geometry is a cylinder -->
      </geometry>
      <origin xyz="${polelen/2} 0 0" rpy="0 ${pi/2} 0 "/>
    </visual>
    <collision>
      <geometry>
        <cylinder length="${polelen}" radius="0.01"/> <!-- Collision geometry is also a cylinder -->
      </geometry>
      <origin xyz="${polelen/2} 0 0" rpy="0 ${pi/2} 0 "/>
    </collision>
    <xacro:default_inertial mass="0.05"/> <!-- Use default inertia, mass of 0.05 -->
  </link>


  <!-- Define a macro for grippers -->
  <xacro:macro name="gripper" params="prefix reflect">
    <joint name="${prefix}_gripper_joint" type="revolute">
      <axis xyz="0 0 ${reflect}"/>
      <limit effort="1000.0" lower="0.0" upper="0.548" velocity="0.5"/>
      <origin rpy="0 0 0" xyz="${polelen} ${reflect*0.01} 0"/>
      <parent link="gripper_pole"/>
      <child link="${prefix}_gripper"/>
    </joint>
    <link name="${prefix}_gripper">
      <visual>
        <origin rpy="${(reflect-1)/2*pi} 0 0" xyz="0 0 0"/>
        <geometry>
          <mesh filename="package://urdf_tutorial/meshes/l_finger.dae"/> 
                   <!-- Use a 3D model file as the geometry -->
        </geometry>
      </visual>
      <collision>
        <geometry>
          <mesh filename="package://urdf_tutorial/meshes/l_finger.dae"/> <!-- Collision geometry also uses a 3D model file -->
        </geometry>
        <origin rpy="${(reflect-1)/2*pi} 0 0" xyz="0 0 0"/>
      </collision>
      <xacro:default_inertial mass="0.05"/> <!-- Use default inertia, mass of 0.05 -->
    </link>


    <joint name="${prefix}_tip_joint" type="fixed">
      <parent link="${prefix}_gripper"/>
      <child link="${prefix}_tip"/>
    </joint>
    <link name="${prefix}_tip">
      <visual>
        <origin rpy="${(reflect-1)/2*pi} 0 0" xyz="0.09137 0.00495 0"/>
        <geometry>
          <mesh filename="package://urdf_tutorial/meshes/l_finger_tip.dae"/> <!-- Use a 3D model file as the geometry -->
        </geometry>
      </visual>
      <collision>
        <geometry>
          <mesh filename="package://urdf_tutorial/meshes/l_finger_tip.dae"/> <!-- Collision geometry also uses a 3D model file -->
        </geometry>
        <origin rpy="${(reflect-1)/2*pi} 0 0" xyz="0.09137 0.00495 0"/>
      </collision>
      <xacro:default_inertial mass="0.05"/> <!-- Use default inertia, mass of 0.05 -->
    </link>
  </xacro:macro>


  <xacro:gripper prefix="left" reflect="1" /> <!-- Create left gripper -->
  <xacro:gripper prefix="right" reflect="-1" /> <!-- Create right gripper -->


  <link name="head">
    <visual>
      <geometry>
        <sphere radius="${width}"/> <!-- Geometry is a sphere -->
      </geometry>
      <material name="white"/> <!-- Use white material -->
    </visual>
    <collision>
      <geometry>
        <sphere radius="${width}"/> <!-- Collision geometry is also a sphere -->
      </geometry>
    </collision>
    <xacro:default_inertial mass="2"/> <!-- Use default inertia, mass of 2 -->
  </link>


  <joint name="head_swivel" type="continuous">
    <parent link="base_link"/>
    <child link="head"/>
    <axis xyz="0 0 1"/>
    <origin xyz="0 0 ${bodylen/2}"/>
  </joint>


  <link name="box">
    <visual>
      <geometry>
        <box size="0.08 0.08 0.08"/> <!-- Geometry is a box -->
      </geometry>
      <material name="blue"/> <!-- Use blue material -->
      <origin xyz="-0.04 0 0"/>
    </visual>
    <collision>
      <geometry>
        <box size="0.08 0.08 0.08"/> <!-- Collision geometry is also a box -->
      </geometry>
    </collision>
    <xacro:default_inertial mass="1"/> <!-- Use default inertia, mass of 1 -->
  </link>


  <joint name="tobox" type="fixed">
    <parent link="head"/>
    <child link="box"/>
    <origin xyz="${.707*width+0.04} 0 ${.707*width}"/>
  </joint>


</robot>

The launch file is continuously running the xacro command, but since there are no macros to expand, it is irrelevant

Leg Macro/Wheel Macro

Usually, you want to create multiple objects that look similar in different positions. You can use macros and some simple math to reduce the amount of code you need to write, just like we did for R2's two legs.

<xacro:macro name="leg" params="prefix reflect">
    <!-- Define leg link -->
    <link name="${prefix}_leg">
        <visual>
            <!-- Define visual elements -->
            <geometry>
                <!-- Define geometry as a box with dimensions leglen, 0.1, 0.2 -->
                <box size="${leglen} 0.1 0.2"/>
            </geometry>
            <!-- Define origin and rotation for visual elements -->
            <origin xyz="0 0 -${leglen/2}" rpy="0 ${pi/2} 0"/>
            <!-- Define material as white -->
            <material name="white"/>
        </visual>
        <collision>
            <!-- Define collision elements -->
            <geometry>
                <!-- Define geometry as a box with dimensions leglen, 0.1, 0.2 -->
                <box size="${leglen} 0.1 0.2"/>
            </geometry>
            <!-- Define origin and rotation for collision elements -->
            <origin xyz="0 0 -${leglen/2}" rpy="0 ${pi/2} 0"/>
        </collision>
        <!-- Define inertia parameters using default_inertial macro, mass of 10 -->
        <xacro:default_inertial mass="10"/>
    </link>


    <!-- Define leg joint -->
    <joint name="base_to_${prefix}_leg" type="fixed">
        <!-- Parent link is base_link -->
        <parent link="base_link"/>
        <!-- Child link is prefix_leg -->
        <child link="${prefix}_leg"/>
        <!-- Define joint origin -->
        <origin xyz="0 ${reflect*(width+.02)} 0.25" />
    </joint>


    <!-- Define leg base link -->
    <link name="${prefix}_base">
        <visual>
            <!-- Define visual elements -->
            <geometry>
                <!-- Define geometry as a box with dimensions baselen, 0.1, 0.1 -->
                <box size="${baselen} 0.1 0.1"/>
            </geometry>
            <!-- Define material as white -->
            <material name="white"/>
        </visual>
        <collision>
            <!-- Define collision elements -->
            <geometry>
                <!-- Define geometry as a box with dimensions baselen, 0.1, 0.1 -->
                <box size="${baselen} 0.1 0.1"/>
            </geometry>
        </collision>
        <!-- Define inertia parameters using default_inertial macro, mass of 10 -->
        <xacro:default_inertial mass="10"/>
    </link>


    <!-- Define base joint -->
    <joint name="${prefix}_base_joint" type="fixed">
        <!-- Parent link is prefix_leg -->
        <parent link="${prefix}_leg"/>
        <!-- Child link is prefix_base -->
        <child link="${prefix}_base"/>
        <!-- Define joint origin -->
        <origin xyz="0 0 ${-leglen}" />
    </joint>


    <!-- Use wheel macro to define front wheel -->
    <xacro:wheel prefix="${prefix}" suffix="front" reflect="1"/>
    <!-- Use wheel macro to define back wheel -->
    <xacro:wheel prefix="${prefix}" suffix="back" reflect="-1"/>
</xacro:macro>


<!-- Use leg macro to define right leg, reflect parameter is 1 -->
<xacro:leg prefix="right" reflect="1" />
<!-- Use leg macro to define left leg, reflect parameter is -1 -->
<xacro:leg prefix="left" reflect="-1" />
  • Common technique 1: Use name prefixes to get two objects with similar names.
  • Common technique 2: Use math to calculate joint origins. If you change the size of the robot, using properties to calculate joint offsets will save a lot of trouble.
  • Common technique 3: Use the reflect parameter and set it to 1 or -1. Look at how we use the reflect parameter to place the leg on both sides of the body in the base_to_${prefix}_leg origin.

In URDF, joints connect parent links and child links. The origin and pose of the joint are defined relative to the parent link. Therefore, joints are attached to the parent link, and the child link is connected to the parent link through the joint.

Tags: ROS2 URDF Xacro Robotics Software Engineering

Posted on Sat, 15 Aug 2026 16:48:09 +0000 by bothwell