Skip to main content

URDF and SDF Modeling

Robot modeling is fundamental to simulation and real-world deployment. This guide covers creating accurate robot descriptions using URDF (Unified Robot Description Format) and SDF (Simulation Description Format).

URDF Overview​

URDF is an XML format for describing robot kinematics, dynamics, and visual properties. It's the standard for ROS robot descriptions.

URDF Structure​

Basic URDF Syntax​

<?xml version="1.0"?>
<robot name="simple_robot">

<!-- Base link -->
<link name="base_link">
<visual>
<geometry>
<box size="0.5 0.3 0.2"/>
</geometry>
<material name="blue">
<color rgba="0 0 0.8 1"/>
</material>
</visual>

<collision>
<geometry>
<box size="0.5 0.3 0.2"/>
</geometry>
</collision>

<inertial>
<mass value="10.0"/>
<inertia ixx="0.0875" ixy="0.0" ixz="0.0"
iyy="0.2292" iyz="0.0"
izz="0.2917"/>
</inertial>
</link>

<!-- Wheel link -->
<link name="left_wheel">
<visual>
<geometry>
<cylinder radius="0.1" length="0.05"/>
</geometry>
<material name="black">
<color rgba="0 0 0 1"/>
</material>
</visual>

<collision>
<geometry>
<cylinder radius="0.1" length="0.05"/>
</geometry>
</collision>

<inertial>
<mass value="1.0"/>
<inertia ixx="0.0013" ixy="0.0" ixz="0.0"
iyy="0.0013" iyz="0.0"
izz="0.0025"/>
</inertial>
</link>

<!-- Joint connecting base to wheel -->
<joint name="left_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="left_wheel"/>
<origin xyz="0 0.2 0" rpy="-1.5707 0 0"/>
<axis xyz="0 0 1"/>
</joint>

</robot>

Visual: Appearance in simulation/visualization

<visual>
<origin xyz="0 0 0" rpy="0 0 0"/>
<geometry>
<mesh filename="package://my_robot/meshes/body.dae" scale="1 1 1"/>
</geometry>
<material name="gray">
<color rgba="0.5 0.5 0.5 1"/>
</material>
</visual>

Collision: Simplified geometry for collision detection

<collision>
<origin xyz="0 0 0" rpy="0 0 0"/>
<geometry>
<box size="0.5 0.3 0.2"/>
</geometry>
</collision>

Inertial: Mass and inertia properties

<inertial>
<origin xyz="0 0 0.05" rpy="0 0 0"/>
<mass value="5.0"/>
<inertia ixx="0.0347" ixy="0" ixz="0"
iyy="0.0505" iyz="0"
izz="0.0347"/>
</inertial>

Joint Types​

TypeDescriptionUse Case
revoluteRotates around axis with limitsRobot arm joints
continuousRotates around axis without limitsWheels
prismaticSlides along axis with limitsLinear actuators
fixedNo movementMounting sensors
floating6 DOFFree-flying robots
planarMoves in planeMobile bases

SDF Format​

SDF is Gazebo's native format, offering more features than URDF.

URDF vs SDF​

FeatureURDFSDF
Multiple modelsNoYes
Closed kinematic chainsNoYes
Joint frictionLimitedComprehensive
Plugin systemBasicAdvanced
World descriptionNoYes
Physics enginesOneMultiple

SDF Model Example​

<?xml version="1.0" ?>
<sdf version="1.8">
<model name="differential_drive_robot">
<pose>0 0 0.15 0 0 0</pose>

<!-- Base link -->
<link name="chassis">
<inertial>
<mass>15.0</mass>
<inertia>
<ixx>0.1875</ixx>
<iyy>0.4583</iyy>
<izz>0.5208</izz>
</inertia>
</inertial>

<collision name="collision">
<geometry>
<box>
<size>0.6 0.4 0.2</size>
</box>
</geometry>
</collision>

<visual name="visual">
<geometry>
<box>
<size>0.6 0.4 0.2</size>
</box>
</geometry>
<material>
<ambient>0.2 0.2 0.8 1</ambient>
<diffuse>0.2 0.2 0.8 1</diffuse>
<specular>0.1 0.1 0.1 1</specular>
</material>
</visual>
</link>

<!-- Left wheel -->
<link name="left_wheel">
<pose relative_to="chassis">0 0.25 0 -1.5707 0 0</pose>
<inertial>
<mass>1.0</mass>
<inertia>
<ixx>0.0013</ixx>
<iyy>0.0025</iyy>
<izz>0.0013</izz>
</inertia>
</inertial>

<collision name="collision">
<geometry>
<cylinder>
<radius>0.15</radius>
<length>0.05</length>
</cylinder>
</geometry>
<surface>
<friction>
<ode>
<mu>1.0</mu>
<mu2>1.0</mu2>
</ode>
</friction>
</surface>
</collision>

<visual name="visual">
<geometry>
<cylinder>
<radius>0.15</radius>
<length>0.05</length>
</cylinder>
</geometry>
<material>
<ambient>0.1 0.1 0.1 1</ambient>
<diffuse>0.1 0.1 0.1 1</diffuse>
</material>
</visual>
</link>

<!-- Joint -->
<joint name="left_wheel_joint" type="revolute">
<pose relative_to="left_wheel"/>
<parent>chassis</parent>
<child>left_wheel</child>
<axis>
<xyz>0 0 1</xyz>
<limit>
<lower>-1.79769e+308</lower>
<upper>1.79769e+308</upper>
</limit>
</axis>
</joint>

<!-- Repeat for right wheel -->
<link name="right_wheel">
<pose relative_to="chassis">0 -0.25 0 -1.5707 0 0</pose>
<inertial>
<mass>1.0</mass>
<inertia>
<ixx>0.0013</ixx>
<iyy>0.0025</iyy>
<izz>0.0013</izz>
</inertia>
</inertial>

<collision name="collision">
<geometry>
<cylinder>
<radius>0.15</radius>
<length>0.05</length>
</cylinder>
</geometry>
<surface>
<friction>
<ode>
<mu>1.0</mu>
<mu2>1.0</mu2>
</ode>
</friction>
</surface>
</collision>

<visual name="visual">
<geometry>
<cylinder>
<radius>0.15</radius>
<length>0.05</length>
</cylinder>
</geometry>
<material>
<ambient>0.1 0.1 0.1 1</ambient>
<diffuse>0.1 0.1 0.1 1</diffuse>
</material>
</visual>
</link>

<joint name="right_wheel_joint" type="revolute">
<pose relative_to="right_wheel"/>
<parent>chassis</parent>
<child>right_wheel</child>
<axis>
<xyz>0 0 1</xyz>
<limit>
<lower>-1.79769e+308</lower>
<upper>1.79769e+308</upper>
</limit>
</axis>
</joint>

<!-- Caster wheel for stability -->
<link name="caster">
<pose relative_to="chassis">-0.25 0 -0.1 0 0 0</pose>
<inertial>
<mass>0.5</mass>
<inertia>
<ixx>0.0001</ixx>
<iyy>0.0001</iyy>
<izz>0.0001</izz>
</inertia>
</inertial>

<collision name="collision">
<geometry>
<sphere>
<radius>0.05</radius>
</sphere>
</geometry>
<surface>
<friction>
<ode>
<mu>0.1</mu>
<mu2>0.1</mu2>
</ode>
</friction>
</surface>
</collision>

<visual name="visual">
<geometry>
<sphere>
<radius>0.05</radius>
</sphere>
</geometry>
</visual>
</link>

<joint name="caster_joint" type="ball">
<parent>chassis</parent>
<child>caster</child>
</joint>

<!-- Differential drive plugin -->
<plugin filename="gz-sim-diff-drive-system"
name="gz::sim::systems::DiffDrive">
<left_joint>left_wheel_joint</left_joint>
<right_joint>right_wheel_joint</right_joint>
<wheel_separation>0.5</wheel_separation>
<wheel_radius>0.15</wheel_radius>
<odom_publish_frequency>50</odom_publish_frequency>
<topic>cmd_vel</topic>
</plugin>
</model>
</sdf>

Adding Sensors​

Camera Sensor​

<!-- Add to a link in URDF -->
<link name="camera_link">
<visual>
<geometry>
<box size="0.05 0.05 0.05"/>
</geometry>
</visual>

<collision>
<geometry>
<box size="0.05 0.05 0.05"/>
</geometry>
</collision>

<inertial>
<mass value="0.1"/>
<inertia ixx="0.00002" ixy="0" ixz="0"
iyy="0.00002" iyz="0"
izz="0.00002"/>
</inertial>
</link>

<!-- Gazebo-specific camera configuration -->
<gazebo reference="camera_link">
<sensor name="camera" type="camera">
<camera>
<horizontal_fov>1.047</horizontal_fov>
<image>
<width>640</width>
<height>480</height>
</image>
<clip>
<near>0.1</near>
<far>100</far>
</clip>
</camera>
<always_on>1</always_on>
<update_rate>30</update_rate>
<visualize>true</visualize>
<topic>camera/image_raw</topic>
</sensor>
</gazebo>

LiDAR Sensor (SDF)​

<link name="lidar_link">
<pose relative_to="chassis">0.2 0 0.15 0 0 0</pose>

<inertial>
<mass>0.5</mass>
<inertia>
<ixx>0.0001</ixx>
<iyy>0.0001</iyy>
<izz>0.0001</izz>
</inertia>
</inertial>

<collision name="collision">
<geometry>
<cylinder>
<radius>0.04</radius>
<length>0.06</length>
</cylinder>
</geometry>
</collision>

<visual name="visual">
<geometry>
<cylinder>
<radius>0.04</radius>
<length>0.06</length>
</cylinder>
</geometry>
<material>
<ambient>0.1 0.1 0.1 1</ambient>
<diffuse>0.1 0.1 0.1 1</diffuse>
</material>
</visual>

<sensor name="lidar" type="gpu_lidar">
<topic>scan</topic>
<update_rate>10</update_rate>
<lidar>
<scan>
<horizontal>
<samples>360</samples>
<resolution>1</resolution>
<min_angle>-3.14159</min_angle>
<max_angle>3.14159</max_angle>
</horizontal>
</scan>
<range>
<min>0.1</min>
<max>30.0</max>
<resolution>0.01</resolution>
</range>
<noise>
<type>gaussian</type>
<mean>0.0</mean>
<stddev>0.01</stddev>
</noise>
</lidar>
<visualize>true</visualize>
</sensor>
</link>

<joint name="lidar_joint" type="fixed">
<parent>chassis</parent>
<child>lidar_link</child>
</joint>

IMU Sensor​

<link name="imu_link">
<pose relative_to="chassis">0 0 0.05 0 0 0</pose>

<inertial>
<mass>0.01</mass>
<inertia>
<ixx>0.000001</ixx>
<iyy>0.000001</iyy>
<izz>0.000001</izz>
</inertia>
</inertial>

<sensor name="imu" type="imu">
<topic>imu</topic>
<update_rate>100</update_rate>
<imu>
<angular_velocity>
<x>
<noise type="gaussian">
<mean>0.0</mean>
<stddev>0.009</stddev>
</noise>
</x>
<y>
<noise type="gaussian">
<mean>0.0</mean>
<stddev>0.009</stddev>
</noise>
</y>
<z>
<noise type="gaussian">
<mean>0.0</mean>
<stddev>0.009</stddev>
</noise>
</z>
</angular_velocity>
<linear_acceleration>
<x>
<noise type="gaussian">
<mean>0.0</mean>
<stddev>0.017</stddev>
</noise>
</x>
<y>
<noise type="gaussian">
<mean>0.0</mean>
<stddev>0.017</stddev>
</noise>
</y>
<z>
<noise type="gaussian">
<mean>0.0</mean>
<stddev>0.017</stddev>
</noise>
</z>
</linear_acceleration>
</imu>
</sensor>
</link>

<joint name="imu_joint" type="fixed">
<parent>chassis</parent>
<child>imu_link</child>
</joint>

Depth Camera​

<link name="depth_camera_link">
<pose relative_to="chassis">0.25 0 0.1 0 0 0</pose>

<inertial>
<mass>0.2</mass>
<inertia>
<ixx>0.00004</ixx>
<iyy>0.00004</iyy>
<izz>0.00004</izz>
</inertia>
</inertial>

<visual name="visual">
<geometry>
<box>
<size>0.02 0.09 0.025</size>
</box>
</geometry>
</visual>

<sensor name="depth_camera" type="depth_camera">
<topic>depth_camera</topic>
<update_rate>30</update_rate>
<camera>
<horizontal_fov>1.57</horizontal_fov>
<image>
<width>640</width>
<height>480</height>
<format>R_FLOAT32</format>
</image>
<clip>
<near>0.1</near>
<far>10.0</far>
</clip>
</camera>
</sensor>
</link>

<joint name="depth_camera_joint" type="fixed">
<parent>chassis</parent>
<child>depth_camera_link</child>
</joint>

Xacro for Modular URDF​

Xacro (XML Macros) enables parameterized, reusable URDF components.

Basic Xacro Syntax​

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

<!-- Parameters -->
<xacro:property name="wheel_radius" value="0.15"/>
<xacro:property name="wheel_width" value="0.05"/>
<xacro:property name="wheel_mass" value="1.0"/>
<xacro:property name="chassis_length" value="0.6"/>
<xacro:property name="chassis_width" value="0.4"/>
<xacro:property name="chassis_height" value="0.2"/>

<!-- Macros -->
<xacro:macro name="wheel" params="prefix reflect">
<link name="${prefix}_wheel">
<visual>
<geometry>
<cylinder radius="${wheel_radius}" length="${wheel_width}"/>
</geometry>
<material name="black">
<color rgba="0 0 0 1"/>
</material>
</visual>

<collision>
<geometry>
<cylinder radius="${wheel_radius}" length="${wheel_width}"/>
</geometry>
</collision>

<inertial>
<mass value="${wheel_mass}"/>
<inertia ixx="${wheel_mass * (3 * wheel_radius * wheel_radius + wheel_width * wheel_width) / 12}"
iyy="${wheel_mass * wheel_radius * wheel_radius / 2}"
izz="${wheel_mass * (3 * wheel_radius * wheel_radius + wheel_width * wheel_width) / 12}"
ixy="0" ixz="0" iyz="0"/>
</inertial>
</link>

<joint name="${prefix}_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="${prefix}_wheel"/>
<origin xyz="0 ${reflect * chassis_width/2} 0" rpy="${-pi/2} 0 0"/>
<axis xyz="0 0 1"/>
</joint>
</xacro:macro>

<!-- Base link -->
<link name="base_link">
<visual>
<geometry>
<box size="${chassis_length} ${chassis_width} ${chassis_height}"/>
</geometry>
<material name="blue">
<color rgba="0.2 0.2 0.8 1"/>
</material>
</visual>

<collision>
<geometry>
<box size="${chassis_length} ${chassis_width} ${chassis_height}"/>
</geometry>
</collision>

<inertial>
<mass value="15.0"/>
<inertia ixx="${15.0 * (chassis_width * chassis_width + chassis_height * chassis_height) / 12}"
iyy="${15.0 * (chassis_length * chassis_length + chassis_height * chassis_height) / 12}"
izz="${15.0 * (chassis_length * chassis_length + chassis_width * chassis_width) / 12}"
ixy="0" ixz="0" iyz="0"/>
</inertial>
</link>

<!-- Instantiate wheels -->
<xacro:wheel prefix="left" reflect="1"/>
<xacro:wheel prefix="right" reflect="-1"/>

<!-- Include sensor package -->
<xacro:include filename="$(find my_robot_description)/urdf/sensors.xacro"/>

<!-- Instantiate sensors -->
<xacro:lidar_sensor parent="base_link"/>
<xacro:camera_sensor parent="base_link" xyz="0.25 0 0.15"/>

</robot>

Sensor Macro Library​

Create sensors.xacro:

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

<!-- LiDAR macro -->
<xacro:macro name="lidar_sensor" params="parent">
<link name="lidar_link">
<visual>
<geometry>
<cylinder radius="0.04" length="0.06"/>
</geometry>
<material name="black"/>
</visual>

<collision>
<geometry>
<cylinder radius="0.04" length="0.06"/>
</geometry>
</collision>

<inertial>
<mass value="0.5"/>
<inertia ixx="0.0001" iyy="0.0001" izz="0.0001"
ixy="0" ixz="0" iyz="0"/>
</inertial>
</link>

<joint name="lidar_joint" type="fixed">
<parent link="${parent}"/>
<child link="lidar_link"/>
<origin xyz="0.2 0 0.15" rpy="0 0 0"/>
</joint>

<gazebo reference="lidar_link">
<sensor name="lidar" type="gpu_lidar">
<topic>scan</topic>
<update_rate>10</update_rate>
<lidar>
<scan>
<horizontal>
<samples>360</samples>
<resolution>1</resolution>
<min_angle>-3.14159</min_angle>
<max_angle>3.14159</max_angle>
</horizontal>
</scan>
<range>
<min>0.1</min>
<max>30.0</max>
</range>
</lidar>
</sensor>
</gazebo>
</xacro:macro>

<!-- Camera macro -->
<xacro:macro name="camera_sensor" params="parent xyz">
<link name="camera_link">
<visual>
<geometry>
<box size="0.05 0.05 0.05"/>
</geometry>
</visual>

<inertial>
<mass value="0.1"/>
<inertia ixx="0.00002" iyy="0.00002" izz="0.00002"
ixy="0" ixz="0" iyz="0"/>
</inertial>
</link>

<joint name="camera_joint" type="fixed">
<parent link="${parent}"/>
<child link="camera_link"/>
<origin xyz="${xyz}" rpy="0 0 0"/>
</joint>

<gazebo reference="camera_link">
<sensor name="camera" type="camera">
<camera>
<horizontal_fov>1.047</horizontal_fov>
<image>
<width>640</width>
<height>480</height>
</image>
<clip>
<near>0.1</near>
<far>100</far>
</clip>
</camera>
<update_rate>30</update_rate>
<topic>camera/image_raw</topic>
</sensor>
</gazebo>
</xacro:macro>

</robot>

Processing Xacro Files​

# Convert xacro to URDF
xacro robot.xacro > robot.urdf

# With arguments
xacro robot.xacro use_lidar:=true wheel_radius:=0.2 > robot.urdf

# Check URDF validity
check_urdf robot.urdf

# Visualize robot
urdf_to_graphviz robot.urdf

Best Practices​

1. Coordinate Frames​

Follow ROS REP 105 conventions:

  • X: Forward
  • Y: Left
  • Z: Up

2. Inertia Calculation​

Use accurate inertia tensors:

# Box inertia
def box_inertia(mass, x, y, z):
ixx = mass * (y*y + z*z) / 12
iyy = mass * (x*x + z*z) / 12
izz = mass * (x*x + y*y) / 12
return ixx, iyy, izz

# Cylinder inertia (along z-axis)
def cylinder_inertia(mass, radius, length):
ixx = mass * (3*radius*radius + length*length) / 12
iyy = ixx
izz = mass * radius * radius / 2
return ixx, iyy, izz

# Sphere inertia
def sphere_inertia(mass, radius):
i = 2 * mass * radius * radius / 5
return i, i, i

3. Collision Geometry​

  • Keep collision geometry simple (boxes, cylinders, spheres)
  • Use convex hulls for complex shapes
  • Avoid self-collisions

4. Naming Conventions​

robot_name/
base_link
left_wheel_link
right_wheel_link
sensor_link

5. File Organization​

my_robot_description/
urdf/
robot.urdf.xacro # Main robot file
robot_base.xacro # Base platform
robot_wheels.xacro # Wheel components
sensors.xacro # Sensor macros
meshes/
base.dae
wheel.stl
config/
controllers.yaml
launch/
display.launch.py

Key Takeaways​

  • URDF is ROS-standard for robot description, SDF is Gazebo-native
  • Accurate inertia and collision geometry are crucial for realistic simulation
  • Xacro enables modular, maintainable robot descriptions
  • Sensor integration requires both URDF links and Gazebo plugins
  • Follow naming conventions and coordinate frame standards
  • Use macros for repeated components (wheels, sensors)

Additional Resources​

Hands-On Exercises​

Exercise 1: Complete Mobile Robot​

Create a differential drive robot with:

  • Rectangular chassis (0.6m x 0.4m x 0.2m)
  • Two driven wheels (0.15m radius)
  • One caster wheel
  • LiDAR on top
  • Front-facing camera
  • IMU in center

Deliverables:

  • Complete URDF/SDF file
  • Launch file for visualization
  • Gazebo world file
  • RViz configuration

Exercise 2: Sensor Integration​

Add multiple sensors to your robot:

  • 2D LiDAR (360-degree, 10Hz)
  • RGB camera (640x480, 30Hz)
  • Depth camera (640x480, 15Hz)
  • IMU (100Hz)
  • GPS (1Hz)

Test each sensor in Gazebo and verify ROS topics.

Exercise 3: Xacro Modular Design​

Refactor Exercise 1 using Xacro:

  • Parameterize all dimensions
  • Create macros for wheels
  • Create macros for sensors
  • Support configuration variants (2-wheel vs 4-wheel)
  • Add arguments for sensor selection

Goal: Generate 3 different robot variants from same Xacro files.


Next: Physics Simulation - Master physics engines and dynamics

Previous: Environment Setup