我们将学习统一机器人描述格式(URDF) 的基础知识,并搭建一台功能完整、搭载激光雷达传感器的两轮移动机器人,为仿真环境做好准备。
现在,让我们深入探究机器人的「骨骼」结构。
目录
[第一章:基础 XML 框架与连杆](#第一章:基础 XML 框架与连杆)
[1.1 XML 框架](#1.1 XML 框架)
[1.2 连杆(link)](#1.2 连杆(link))
[1.3 第一个实战任务](#1.3 第一个实战任务)
[任务要求:编写机器人的初始 XML 代码](#任务要求:编写机器人的初始 XML 代码)
[2.1 父 / 子层级结构](#2.1 父 / 子层级结构)
[2.2 关节类型](#2.2 关节类型)
[2.3 关节语法](#2.3 关节语法)
[2.4 下一任务](#2.4 下一任务)
[第三章:原点与坐标变换(xyz 平移与 rpy 旋转)](#第三章:原点与坐标变换(xyz 平移与 rpy 旋转))
[3.1 原点与坐标变换基础](#3.1 原点与坐标变换基础)
[3.2 在三维空间中放置车轮](#3.2 在三维空间中放置车轮)
[3.3 为什么左车轮渲染后显示在右侧?](#3.3 为什么左车轮渲染后显示在右侧?)
[1. 观察视角](#1. 观察视角)
[2. 旋转参数(rpy)](#2. 旋转参数(rpy))
[3. 坐标标准](#3. 坐标标准)
[3.4 下一任务](#3.4 下一任务)
[4.1 固定关节](#4.1 固定关节)
[4.2 激光雷达的安装位置](#4.2 激光雷达的安装位置)
[4.3 下一任务](#4.3 下一任务)
[5.1 碰撞标签](#5.1 碰撞标签)
[5.2 惯性标签](#5.2 惯性标签)
[5.3 下一任务](#5.3 下一任务)
[第六章:Gazebo 插件(让机器人 "活" 起来)](#第六章:Gazebo 插件(让机器人 “活” 起来))
[6.1 标签](#6.1 标签)
[6.2 任务](#6.2 任务)
[第七章:用于在 Gazebo 中生成机器人的 ROS2 启动文件](#第七章:用于在 Gazebo 中生成机器人的 ROS2 启动文件)
[7.1 机器人状态发布器](#7.1 机器人状态发布器)
[7.2 启动文件框架](#7.2 启动文件框架)
[7.3 下一任务](#7.3 下一任务)
[7.4 在 Gazebo 中生成机器人](#7.4 在 Gazebo 中生成机器人)
[7.5 下一任务](#7.5 下一任务)
[终端 1(启动 Gazebo)](#终端 1(启动 Gazebo))
[1. 修复车轮问题(缺失物理属性)](#1. 修复车轮问题(缺失物理属性))
[2. 添加核心「系统插件」](#2. 添加核心「系统插件」)
[修复激光雷达(Harmonic 版本语法](#修复激光雷达(Harmonic 版本语法))
第一章:基础 XML 框架与连杆
URDF 是基于 XML 语言的描述格式,广泛应用于机器人操作系统(ROS) 中,用于定义机器人的物理结构。
1.1 XML 框架
所有 URDF 文件都必须以标准的 XML 声明开头,随后用根标签 <robot> 包裹整个机器人模型。
XML
<?xml version="1.0"?>
<robot name="two_wheeled_bot">
</robot>
1.2 连杆(link)
连杆代表机器人的刚体部件,你可以把它理解为骨骼模型中的单块「骨头」。车轮是连杆、底盘是连杆、传感器支架也是连杆。
在 <link> 标签内部,我们通常定义三个核心属性:
<visual>:连杆的可视化外观 (在 RViz、Gazebo 等仿真工具中可见的模型),内部包含<geometry>标签,用于定义几何形状(如立方体、圆柱体、球体)。<collision>:用于物理引擎计算碰撞的物理边界。为节省算力,该形状通常是可视化几何模型的简化版本。<inertial>:物理属性,包含质量 和质心,是实现真实物理仿真的关键参数(我们将在后续章节添加)。
以下是立方体可视化几何模型的基础语法:
<visual>
<geometry>
<box size="0.5 0.3 0.1"/>
</geometry>
</visual>
1.3 第一个实战任务
我们来搭建机器人的主体(底盘)。在机器人技术中,核心结构连杆的命名固定为 base_link(基连杆)。
任务要求:编写机器人的初始 XML 代码
- 以 XML 声明和
<robot>根标签开头(机器人名称可自定义); - 在根标签内创建一个名称为
base_link的<link>; - 为
base_link添加<visual>标签,并定义立方体几何形状,自定义合适的尺寸作为矩形底盘。
python
<?xml version="1.0"?>
<robot name="roamer_bot">
<!-- The chassis of the robot -->
<link name="base_link">
<visual>
<geometry>
<!-- a box 0.6m long, 0.4m wide, and 0.2m high -->
<box size="0.6 0.4 0.2"/>
</geometry>
<material name="blue">
<color rgba="0 0 1 1"/>
</material>
</visual>
</link>
</robot>
使用网址 https://viewer.robotsfan.com (或 https://browserbotics.com/urdf-editor),按下方效果渲染 URDF 文件。

注意:你需要创建一个后缀为 .urdf 的文件,并将其上传才能完成渲染。
第二章:关节与层级结构("铰链")
在 URDF 描述语言中,机器人的构建结构类似于家族树 。它从一个根连杆 (即我们的 base_link 基础连杆)开始,向外延伸分支。我们使用 <joint>(关节)标签,将一个新的「子连杆」连接到「父连杆」上。
2.1 父 / 子层级结构
每个关节仅连接一个父连杆和一个子连杆 。由于 base_link 是机器人的核心部件,它将作为父连杆 ,我们安装的车轮则作为子连杆。
2.2 关节类型
和人类的关节一样,机器人关节拥有不同的运动范围。构建简易移动机器人时,最常用的两种关节类型为:
- fixed(固定关节):部件之间刚性连接,无法相对运动(非常适合后续安装激光雷达传感器)。
- continuous(连续旋转关节) :关节可围绕一根轴无限旋转、无角度限制 ,这是车轮专用的关节类型。补充说明:revolute(有限旋转关节) 与它类似,但拥有严格的最大 / 最小角度限制,就像人类的膝盖或手肘关节!
2.3 关节语法
以下是基础关节的编写格式。我们会在第三章(原点)中详细讲解关节的具体位置配置,现阶段只需完成连杆的连接即可:
<joint name="left_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="left_wheel"/>
</joint>
2.4 下一任务
如果车轮连杆还未定义,我们就无法为它安装关节!在本任务中,我们需要定义一个车轮连杆 以及对应的关节。请将以下代码添加到 <robot> 包裹标签内,紧接在 base_link 标签的下方。
你的任务要求:
-
创建一个新的
<link>(连杆),命名为left_wheel(左车轮); -
在该连杆内部,添加
<visual>(可视化)标签,并使用圆柱体 作为几何形状。提示:圆柱体的语法格式为<cylinder radius="0.1" length="0.05"/>;你可以按需为它设置颜色! -
在该连杆下方,创建一个
<joint>(关节),命名为left_wheel_joint; -
将关节类型设置为
continuous(连续旋转); -
设置父连杆为
base_link,子连杆为left_wheel。
添加上节点后,机器人的渲染效果如下

正如我们在上方的 URDF 渲染效果中看到的,左车轮目前被 **"嵌套"** 在机器人正中心的蓝色立方体(车身)内部。
在 URDF 中,如果我们不为关节指定 <origin>(原点)标签,它的默认参数为 xyz="0 0 0"。由于我们的 base_link(基础连杆)是宽体立方体,而左车轮是细长圆柱体,因此车身会将车轮完全包裹住。我们将在下一章节深入学习 <origin> 标签的用法。
第三章:原点与坐标变换(xyz 平移与 rpy 旋转)
3.1 原点与坐标变换基础
默认情况下,URDF 会将所有子连杆的中心点精准对齐到其父连杆的中心点。如果我们不为车轮设置偏移量,它们就会完全嵌入底盘的中心内部!
我们在关节内部使用 <origin> 标签来设置子连杆的偏移量,该标签包含两个核心属性:
- xyz(平移) :沿 X 轴(前后)、Y 轴(左右)、Z 轴(上下)移动子连杆,单位为米。
- rpy(旋转) :通过横滚(绕 X 轴)、俯仰(绕 Y 轴)、偏航(绕 Z 轴) 实现子连杆旋转,单位为弧度,而非角度!
3.2 在三维空间中放置车轮
我们通过 xyz(平移)和 rpy(旋转)坐标,详细拆解车轮的正确摆放位置:
- 平移(xyz) 我们的底盘在 Y 轴方向宽度为 0.4 米。这意味着从中心点计算,左侧边缘坐标为
Y=0.2m,右侧边缘坐标为Y=-0.2m。
我们将车轮略微向外移出底盘边缘,设置为:左车轮 Y=0.225、右车轮 Y=-0.225。同时,将车轮向机器人后方小幅移动(X=-0.15),并向下调整高度,让底盘脱离地面(Z=-0.1)。
-
旋转(rpy) 如前所述,圆柱体默认竖直生成。为了让车轮水平放置并实现滚动,我们必须将其绕 X 轴旋转 90 度(横滚)。90 度对应的弧度值约为 1.5708,因此旋转参数设置为
rpy="1.5708 0 0"。 -
旋转轴(
<axis>) 由于车轮旋转后,其 Z 轴指向侧面,车轮需要绕该 Z 轴旋转才能向前滚动。因此,我们在关节中添加<axis xyz="0 0 1"/>。
左车轮的完整关节代码如下所示:
XML
<?xml version="1.0"?>
<robot name="roamer_bot">
<!-- The chassis of the robot -->
<link name="base_link">
<visual>
<geometry>
<!-- a box 0.6m long, 0.4m wide, and 0.2m high -->
<box size="0.6 0.4 0.2"/>
</geometry>
<material name="blue">
<color rgba="0 0 1 1"/>
</material>
</visual>
</link>
<!-- The Left Wheel -->
<link name="left_wheel">
<visual>
<geometry>
<!-- A cylinder for the wheel -->
<cylinder radius="0.1" length="0.05"/>
</geometry>
<material name="black">
<color rgba="0 0 0 1"/>
</material>
</visual>
</link>
<joint name="left_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="left_wheel"/>
<origin xyz="-0.15 0.225 -0.1" rpy="1.5708 0 0"/>
<axis xyz="0 0 1"/>
</joint>
</robot>

3.3 为什么左车轮渲染后显示在右侧?
在机器人学和计算机图形学中,模型显示为「左侧」或「右侧」,完全取决于坐标系的朝向(即坐标系的前进方向)。
我们命名的左车轮(left_wheel)在视图中出现在右侧,大概率是以下三个原因之一:
1. 观察视角
我们是从机器人正前方 观察模型的。在机器人学规范中(遵循 REP 103 标准),X 轴代表前进方向。
- 从机器人自身视角来看,Y 轴正方向(0.225)在数学定义中为左侧;
- 当你正对机器人观察时,它的左侧会出现在你的右侧,就像一个人面向你站立时的左右对称关系。
2. 旋转参数(rpy)
代码中配置了旋转数值。如果该数值为负数(例如 -1.5708),会翻转子连杆坐标系的朝向,导致位置显示不符合直观认知。
3. 坐标标准
绝大多数 URDF 查看器遵循右手定则:
- X 轴(红色):前进方向
- Y 轴(绿色):左侧
- Z 轴(蓝色):上方如果旋转相机视角,让 ** 前进方向(X 轴)** 朝向远离你的一侧,你就会看到左车轮出现在屏幕的左侧。
3.4 下一任务
让我们完成机器人底盘的完整装配!我们需要将底盘和两个车轮都正确摆放。
作业要求:
在你已编写的 URDF 文件基础上,补充缺失的部件,确保 <robot> 根标签内完整包含以下内容:
base_link(底盘连杆)left_wheel(左车轮连杆)以及left_wheel_joint(左车轮关节,使用上文中的原点和旋转轴参数)right_wheel(右车轮连杆)以及right_wheel_joint(右车轮关节,务必将原点 Y 轴参数设置为-0.225,使车轮位于底盘的另一侧)
XML
<?xml version="1.0"?>
<robot name="roamer_bot">
<!-- The chassis of the robot -->
<link name="base_link">
<visual>
<geometry>
<!-- a box 0.6m long, 0.4m wide, and 0.2m high -->
<box size="0.6 0.4 0.2"/>
</geometry>
<material name="blue">
<color rgba="0 0 1 1"/>
</material>
</visual>
</link>
<!-- The Left Wheel -->
<link name="left_wheel">
<visual>
<geometry>
<!-- A cylinder for the wheel -->
<cylinder radius="0.1" length="0.05"/>
</geometry>
<material name="black">
<color rgba="0 0 0 1"/>
</material>
</visual>
</link>
<joint name="left_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="left_wheel"/>
<origin xyz="-0.15 0.225 -0.1" rpy="1.5708 0 0"/>
<axis xyz="0 0 1"/>
</joint>
<!-- The Right Wheel -->
<link name="right_wheel">
<visual>
<geometry>
<cylinder radius="0.1" length="0.05"/>
</geometry>
<material name="black">
<color rgba="0 0 0 1"/>
</material>
</visual>
</link>
<joint name="right_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="right_wheel"/>
<!-- Positioned toward the back (-0.15x) and right (-0.225y) -->
<origin xyz="-0.15 -0.225 -0.1" rpy="1.5708 0 0"/>
<axis xyz="0 0 1"/>
</joint>
</robot>

第四章:传感器与插件(添加激光雷达)
如果移动机器人无法感知周围环境,就谈不上真正的自主运行!在二维建图与导航领域,最常用的传感器是二维激光雷达(Lidar,激光探测与测距)。
要为机器人添加激光雷达,首先需要定义它的实体模型。和车轮一样,激光雷达也是一个独立的 <link>(连杆),并通过 <joint>(关节)连接到 base_link(底盘)上。
4.1 固定关节
与车轮不同,传感器不能随底盘独立旋转或移动,必须刚性固定安装 。因此我们使用 fixed(固定关节) 。固定关节无需配置 <axis>(旋转轴)标签,因为它没有运动自由度。
XML
<joint name="my_fixed_joint" type="fixed">
<parent link="base_link"/>
<child link="my_sensor_link"/>
<origin xyz="X Y Z" rpy="0 0 0"/>
</joint>
4.2 激光雷达的安装位置
我们应该把激光雷达安装在哪里?通常情况下,激光雷达会放置在顶部中心 或稍靠前的位置,以获得无遮挡的探测视野。
- X 轴(前后方向):我们将其安装在靠近前端的位置。底盘长度为 0.6 米,因此前端边缘的坐标为 X=0.3,我们把激光雷达设置在 X=0.2 处。
- Y 轴(左右方向):正中心位置,即 Y=0。
- Z 轴(上下方向):底盘高度为 0.2 米。由于立方体的原点位于正中心,因此底盘上表面的坐标为 Z=0.1。我们将传感器直接安装在顶部,即 Z=0.1 处。(注意:根据圆柱体的高度,你可能需要在 Z 轴坐标上增加半个圆柱体的高度,避免模型嵌入底盘;通常设置 Z=0.12 是最稳妥的选择!)
4.3 下一任务
让我们为你的漫游机器人(roamer_bot)顶部安装一台激光雷达。
作业要求:
- 创建一个新的连杆,命名为
lidar_link; - 为该连杆添加
<visual>可视化标签,使用圆柱体 作为几何形状,尺寸设置小一些:半径radius="0.05"、长度length="0.04",并将其设置为红色,便于区分; - 创建一个关节,命名为
lidar_joint; - 将关节类型设置为
fixed(固定关节); - 设置父连杆为
base_link,子连杆为lidar_link; - 设置原点参数:
xyz="0.2 0 0.12",rpy="0 0 0"(无需添加旋转轴<axis>标签)。
XML
<?xml version="1.0"?>
<robot name="roamer_bot">
<!-- The chassis of the robot -->
<link name="base_link">
<visual>
<geometry>
<!-- a box 0.6m long, 0.4m wide, and 0.2m high -->
<box size="0.6 0.4 0.2"/>
</geometry>
<material name="blue">
<color rgba="0 0 1 1"/>
</material>
</visual>
</link>
<!-- The Left Wheel -->
<link name="left_wheel">
<visual>
<geometry>
<!-- A cylinder for the wheel -->
<cylinder radius="0.1" length="0.05"/>
</geometry>
<material name="black">
<color rgba="0 0 0 1"/>
</material>
</visual>
</link>
<joint name="left_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="left_wheel"/>
<origin xyz="-0.15 0.225 -0.1" rpy="1.5708 0 0"/>
<axis xyz="0 0 1"/>
</joint>
<!-- The Right Wheel -->
<link name="right_wheel">
<visual>
<geometry>
<cylinder radius="0.1" length="0.05"/>
</geometry>
<material name="black">
<color rgba="0 0 0 1"/>
</material>
</visual>
</link>
<joint name="right_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="right_wheel"/>
<!-- Positioned toward the back (-0.15x) and right (-0.225y) -->
<origin xyz="-0.15 -0.225 -0.1" rpy="1.5708 0 0"/>
<axis xyz="0 0 1"/>
</joint>
<!-- The Lidar Sensor -->
<link name="lidar_link">
<visual>
<geometry>
<!-- A small cylinder for the sensor -->
<cylinder radius="0.05" length="0.04"/>
</geometry>
<material name="red">
<color rgba="1 0 0 1"/>
</material>
</visual>
</link>
<!-- The Fixed Joint for the Lidar -->
<joint name="lidar_joint" type="fixed">
<parent link="base_link"/>
<child link="lidar_link"/>
<!-- Positioned at the front (+0.2x) and on top (+0.12z) of the box -->
<origin xyz="0.2 0 0.12" rpy="0 0 0"/>
</joint>
</robot>

我们的漫游机器人(roamer_bot)视觉建模已全部完成。我们完成了底盘的精准定位、车轮的滚动姿态调整,也将传感器安装在了指定位置。
但目前,我们的机器人本质上只是一个全息投影模型 。如果将它直接导入 Gazebo 等物理仿真器,它会直接穿透地面掉落!为了让机器人能与物理世界产生真实交互,我们需要回顾第一章,补全剩余的核心物理骨架 :碰撞属性 和惯性属性。
第五章:物理仿真配置(碰撞属性与惯性属性)
要实现物理仿真,仿真器必须知晓每个连杆的两个核心参数:
- 实体碰撞边界(与其他物体发生碰撞的范围)
- 质量与重量分布
5.1 <collision> 碰撞标签
碰撞标签用于定义连杆的物理碰撞盒(Hitbox) 。绝大多数情况下(尤其立方体、圆柱体等简单几何体),我们可以直接复制 <visual> 可视化标签中使用的几何形状参数。
<collision>
<geometry>
<box size="0.6 0.4 0.2"/>
</geometry>
</collision>
5.2 <inertial> 惯性标签
该标签用于告知物理引擎:连杆的重量、重量分布方式,这直接决定了物体的加速度与旋转特性 。配置惯性标签必须包含两个参数:<mass>(质量) 和 <inertia> 矩阵(惯性张量)。
计算三维几何体的 3×3 惯性张量 ,需要通过复杂的数学公式描述物体沿不同坐标轴的转动惯量:

由于手动计算每种几何体的惯性参数十分繁琐,ROS机器人开发者通常会使用宏命令、CAD 软件或标准公式自动生成这些参数。对于我们的底盘(质量为 5 千克的立方体),预先计算完成的惯性模块代码如下:
<inertial>
<mass value="5.0"/>
<inertia ixx="0.083" ixy="0.0" ixz="0.0" iyy="0.166" iyz="0.0" izz="0.216"/>
</inertial>
5.3 下一任务
让我们为你的基础连杆(base_link)添加实体碰撞属性 和质量属性!
作业要求:
更新你的 base_link 代码,使其包含全部三个核心组件:
- 完全保留 你现有的
<visual>(可视化)标签,不做任何修改; - 在可视化标签正下方 添加
<collision>(碰撞)标签,使用与可视化标签完全相同的立方体几何参数; - 添加上文提供的
<inertial>(惯性)标签,使底盘的质量为 5 千克。
XML
<?xml version="1.0"?>
<robot name="roamer_bot">
<!-- The chassis of the robot -->
<link name="base_link">
<!-- Visual: What you see -->
<visual>
<geometry>
<!-- a box 0.6m long, 0.4m wide, and 0.2m high -->
<box size="0.6 0.4 0.2"/>
</geometry>
<material name="blue">
<color rgba="0 0 1 1"/>
</material>
</visual>
<!-- Collision: What the physics engine "feels" -->
<collision>
<geometry>
<box size="0.6 0.4 0.2"/>
</geometry>
</collision>
<!-- Inertial: How heavy it is and how it balances -->
<inertial>
<mass value="5.0"/>
<origin xyz="0 0 0" rpy="0 0 0"/>
<inertia ixx="0.083" ixy="0" ixz="0" iyy="0.167" iyz="0" izz="0.217"/>
</inertial>
</link>
<!-- The Left Wheel -->
<link name="left_wheel">
<visual>
<geometry>
<!-- A cylinder for the wheel -->
<cylinder radius="0.1" length="0.05"/>
</geometry>
<material name="black">
<color rgba="0 0 0 1"/>
</material>
</visual>
</link>
<joint name="left_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="left_wheel"/>
<origin xyz="-0.15 0.225 -0.1" rpy="1.5708 0 0"/>
<axis xyz="0 0 1"/>
</joint>
<!-- The Right Wheel -->
<link name="right_wheel">
<visual>
<geometry>
<cylinder radius="0.1" length="0.05"/>
</geometry>
<material name="black">
<color rgba="0 0 0 1"/>
</material>
</visual>
</link>
<joint name="right_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="right_wheel"/>
<!-- Positioned toward the back (-0.15x) and right (-0.225y) -->
<origin xyz="-0.15 -0.225 -0.1" rpy="1.5708 0 0"/>
<axis xyz="0 0 1"/>
</joint>
<!-- The Lidar Sensor -->
<link name="lidar_link">
<visual>
<geometry>
<!-- A small cylinder for the sensor -->
<cylinder radius="0.05" length="0.04"/>
</geometry>
<material name="red">
<color rgba="1 0 0 1"/>
</material>
</visual>
</link>
<!-- The Fixed Joint for the Lidar -->
<joint name="lidar_joint" type="fixed">
<parent link="base_link"/>
<child link="lidar_link"/>
<!-- Positioned at the front (+0.2x) and on top (+0.12z) of the box -->
<origin xyz="0.2 0 0.12" rpy="0 0 0"/>
</joint>
</robot>
由于 https://viewer.robotsfan.com/ 仅用于模型可视化,因此页面显示效果会和之前完全一致

第六章:Gazebo 插件(让机器人 "活" 起来)
目前,我们的激光雷达连杆(lidar_link)仅仅是一个具备物理质量的红色圆柱体。它既无法发射激光,也不能采集环境数据。URDF 仅负责描述机器人的物理外观与结构 ,若要让零部件在 Gazebo 等仿真器中实现实际功能 ,我们必须使用插件。
插件是一段编译进仿真器的 C++ 代码,它会绑定到 URDF 模型的指定部件上,为其赋予专属功能(例如作为摄像头、激光扫描仪或电机控制器)。
6.1 <gazebo> 标签
要将插件与机器人模型关联,我们需要使用 <gazebo> 标签。通过 reference(引用)属性,我们可以精准告知 Gazebo:该插件归属哪一个连杆或关节。
以下是标准的 Gazebo 二维激光雷达插件代码示例。它会指令仿真器发射 360° 激光射线,最大探测距离为 10 米,并发布探测数据,让我们的机器人实现环境感知("看见" 世界):
XML
<gazebo reference="lidar_link">
<sensor type="ray" name="lidar_sensor">
<pose>0 0 0 0 0 0</pose>
<visualize>true</visualize>
<update_rate>10</update_rate>
<ray>
<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.3</min>
<max>10.0</max>
</range>
</ray>
<plugin name="gazebo_ros_laser" filename="libgazebo_ros_ray_sensor.so">
<ros>
<remapping>~/out:=scan</remapping>
</ros>
<output_type>sensor_msgs/LaserScan</output_type>
<frame_name>lidar_link</frame_name>
</plugin>
</sensor>
</gazebo>
6.2 任务
由于 Gazebo 插件的配置参数十分具体、代码篇幅较长,机器人工程师几乎都会直接复制上述这类标准模板并进行微调,而非从零手动编写。
作业要求:
- 复制上文提供的完整
<gazebo>代码块; - 将其粘贴到你的 URDF 文件中。最佳实践 :把
<gazebo>标签放在文件的最底部,紧邻最终的闭合标签</robot>上方。
XML
<?xml version="1.0"?>
<robot name="roamer_bot">
<!-- The chassis of the robot -->
<link name="base_link">
<!-- Visual: What you see -->
<visual>
<geometry>
<!-- a box 0.6m long, 0.4m wide, and 0.2m high -->
<box size="0.6 0.4 0.2"/>
</geometry>
<material name="blue">
<color rgba="0 0 1 1"/>
</material>
</visual>
<!-- Collision: What the physics engine "feels" -->
<collision>
<geometry>
<box size="0.6 0.4 0.2"/>
</geometry>
</collision>
<!-- Inertial: How heavy it is and how it balances -->
<inertial>
<mass value="5.0"/>
<origin xyz="0 0 0" rpy="0 0 0"/>
<inertia ixx="0.083" ixy="0" ixz="0" iyy="0.167" iyz="0" izz="0.217"/>
</inertial>
</link>
<!-- The Left Wheel -->
<link name="left_wheel">
<visual>
<geometry>
<!-- A cylinder for the wheel -->
<cylinder radius="0.1" length="0.05"/>
</geometry>
<material name="black">
<color rgba="0 0 0 1"/>
</material>
</visual>
</link>
<joint name="left_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="left_wheel"/>
<origin xyz="-0.15 0.225 -0.1" rpy="1.5708 0 0"/>
<axis xyz="0 0 1"/>
</joint>
<!-- The Right Wheel -->
<link name="right_wheel">
<visual>
<geometry>
<cylinder radius="0.1" length="0.05"/>
</geometry>
<material name="black">
<color rgba="0 0 0 1"/>
</material>
</visual>
</link>
<joint name="right_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="right_wheel"/>
<!-- Positioned toward the back (-0.15x) and right (-0.225y) -->
<origin xyz="-0.15 -0.225 -0.1" rpy="1.5708 0 0"/>
<axis xyz="0 0 1"/>
</joint>
<!-- The Lidar Sensor -->
<link name="lidar_link">
<visual>
<geometry>
<!-- A small cylinder for the sensor -->
<cylinder radius="0.05" length="0.04"/>
</geometry>
<material name="red">
<color rgba="1 0 0 1"/>
</material>
</visual>
</link>
<!-- The Fixed Joint for the Lidar -->
<joint name="lidar_joint" type="fixed">
<parent link="base_link"/>
<child link="lidar_link"/>
<!-- Positioned at the front (+0.2x) and on top (+0.12z) of the box -->
<origin xyz="0.2 0 0.12" rpy="0 0 0"/>
</joint>
<?xml version="1.0"?>
<robot name="roamer_bot">
<!-- The chassis of the robot -->
<link name="base_link">
<!-- Visual: What you see -->
<visual>
<geometry>
<!-- a box 0.6m long, 0.4m wide, and 0.2m high -->
<box size="0.6 0.4 0.2"/>
</geometry>
<material name="blue">
<color rgba="0 0 1 1"/>
</material>
</visual>
<!-- Collision: What the physics engine "feels" -->
<collision>
<geometry>
<box size="0.6 0.4 0.2"/>
</geometry>
</collision>
<!-- Inertial: How heavy it is and how it balances -->
<inertial>
<mass value="5.0"/>
<origin xyz="0 0 0" rpy="0 0 0"/>
<inertia ixx="0.083" ixy="0" ixz="0" iyy="0.167" iyz="0" izz="0.217"/>
</inertial>
</link>
<!-- The Left Wheel -->
<link name="left_wheel">
<visual>
<geometry>
<!-- A cylinder for the wheel -->
<cylinder radius="0.1" length="0.05"/>
</geometry>
<material name="black">
<color rgba="0 0 0 1"/>
</material>
</visual>
</link>
<joint name="left_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="left_wheel"/>
<origin xyz="-0.15 0.225 -0.1" rpy="1.5708 0 0"/>
<axis xyz="0 0 1"/>
</joint>
<!-- The Right Wheel -->
<link name="right_wheel">
<visual>
<geometry>
<cylinder radius="0.1" length="0.05"/>
</geometry>
<material name="black">
<color rgba="0 0 0 1"/>
</material>
</visual>
</link>
<joint name="right_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="right_wheel"/>
<!-- Positioned toward the back (-0.15x) and right (-0.225y) -->
<origin xyz="-0.15 -0.225 -0.1" rpy="1.5708 0 0"/>
<axis xyz="0 0 1"/>
</joint>
<!-- The Lidar Sensor -->
<link name="lidar_link">
<visual>
<geometry>
<!-- A small cylinder for the sensor -->
<cylinder radius="0.05" length="0.04"/>
</geometry>
<material name="red">
<color rgba="1 0 0 1"/>
</material>
</visual>
</link>
<!-- The Fixed Joint for the Lidar -->
<joint name="lidar_joint" type="fixed">
<parent link="base_link"/>
<child link="lidar_link"/>
<!-- Positioned at the front (+0.2x) and on top (+0.12z) of the box -->
<origin xyz="0.2 0 0.12" rpy="0 0 0"/>
</joint>
</robot>
<gazebo reference="lidar_link">
<sensor type="ray" name="lidar_sensor">
<pose>0 0 0 0 0 0</pose>
<visualize>true</visualize>
<update_rate>10</update_rate>
<ray>
<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.3</min>
<max>10.0</max>
</range>
</ray>
<plugin name="gazebo_ros_laser" filename="libgazebo_ros_ray_sensor.so">
<ros>
<remapping>~/out:=scan</remapping>
</ros>
<output_type>sensor_msgs/LaserScan</output_type>
<frame_name>lidar_link</frame_name>
</plugin>
</sensor>
</gazebo>
</robot>
第七章:用于在 Gazebo 中生成机器人的 ROS2 启动文件
见证奇迹的时刻到了!看着我们编写的代码在三维世界中变成一个具备物理属性、可以移动的实体,是机器人开发中最有成就感的环节。
我们将这部分内容拆解为简单易懂的小模块,首先学习最重要的 ROS 节点:机器人状态发布器(robot_state_publisher)。
7.1 机器人状态发布器
要将漫游机器人(roamer_bot)导入 Gazebo,Gazebo 必须先知道机器人的模型结构。但我们不会直接把 URDF 文件传给 Gazebo ,而是将它交给 ROS2 的一个核心节点 ------robot_state_publisher。
robot_state_publisher 相当于机器人的信息播报员 。它读取 URDF 文件,并将完整的 XML 格式模型数据通过名为 /robot_description 的话题(topic)广播到整个 ROS 网络中。这样一来,Gazebo、RViz(可视化工具)和导航代码就可以同时读取完全一致的机器人模型。
7.2 启动文件框架
一个标准的 ROS2 Python 启动文件,核心是一个名为 generate_launch_description() 的主函数。在这个函数内部,我们定义需要运行的节点(Nodes,独立程序),最后以列表形式返回这些节点。
以下是在启动文件中启动节点的基础语法:
my_node = Node(
package='ros功能包名称',
executable='可执行程序名称',
parameters=[{'参数名称': 参数值}]
)
7.3 下一任务
作业要求:
编写 spawn.launch.py 启动文件的第一版代码。
参考代码:
python
# spawn.launch.py
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
# 1. 打开并读取我们的 URDF 文件
# 此处使用 'roamer_bot.urdf' 作为文件名
with open('roamer_bot.urdf', 'r') as file:
robot_desc_string = file.read()
# 2. 定义机器人状态发布器节点
# 该节点负责广播机器人的可视化与物理结构
rsp_node = Node(
package='robot_state_publisher',
executable='robot_state_publisher',
parameters=[{'robot_description': robot_desc_string}]
)
# 3. 将节点添加到启动描述中并返回
return LaunchDescription([
rsp_node
])
7.4 在 Gazebo 中生成机器人
要将机器人真正导入物理仿真器,我们需要使用 Gazebo ROS 桥 提供的专用脚本:spawn_entity.py。
你可以把 spawn_entity.py 理解为一个传送器 。它连接到我们刚刚创建的 /robot_description 话题,获取 URDF 数据,并将机器人 "传送" 到 Gazebo 三维仿真环境中。
生成器节点语法
这是 gazebo_ros 功能包自带的 Python 脚本,我们将其作为标准节点运行,并传入指定的命令行参数。我们需要告知它两个关键信息:
- 从哪里获取机器人模型数据(话题名称)
- 在 Gazebo 中为机器人实体命名
节点定义格式如下:
spawn_node = Node(
package='gazebo_ros',
executable='spawn_entity.py',
arguments=['-topic', 'robot_description', '-entity', 'my_robot_name']
)
7.5 下一任务
让我们把 "传送器" 添加到启动文件中,让漫游机器人正式进入仿真世界!
作业要求:
- 在你现有的
generate_launch_description()代码基础上修改 - 在
rsp_node下方,定义一个新节点spawn_node - 设置功能包为
gazebo_ros,可执行程序为spawn_entity.py - 添加参数列表,指定订阅
robot_description话题,并将机器人命名为roamer_bot - 关键步骤 :务必将
spawn_node添加到底部的LaunchDescription([ ... ])列表中,确保节点可以运行
最终参考代码:
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
# 1. 打开并读取 URDF 文件
with open('roamer_bot.urdf', 'r') as file:
robot_desc_string = file.read()
# 2. 定义机器人状态发布器节点
rsp_node = Node(
package='robot_state_publisher',
executable='robot_state_publisher',
parameters=[{'robot_description': robot_desc_string}]
)
# 3. 定义实体生成节点(传送器)
spawn_node = Node(
package='gazebo_ros',
executable='spawn_entity.py',
arguments=['-topic', 'robot_description', '-entity', 'roamer_bot'],
output='screen'
)
# 4. 将两个节点都添加到启动描述中并返回
return LaunchDescription([
rsp_node,
spawn_node
])
终端 1(启动 Gazebo)
ROS2 Jazzy 版本默认使用 Gazebo Harmonic (新一代 Gazebo),它替代了 Humble / Foxy 等旧版本中使用的传统 gazebo_ros 功能包。我们需要先安装对应的依赖包:
sudo apt update
sudo apt install ros-jazzy-ros-gz
安装完成后,使用以下命令启动 Gazebo:
ros2 launch ros_gz_sim gz_sim.launch.py

选择空场景(Empty world)(也可选用其他场景,例如 "仓库拖车机器人(Tugbot in Warehouse)")。
点击界面右下角的 RUN(运行) 按钮。
重要提醒 :Gazebo 主窗口弹出后,务必点击仿真器底部工具栏上的播放按钮 (橙白色三角形图标),以此启动物理时钟。
完成后,界面就会加载出如下空白场景。

**终端 2(生成你的机器人):**执行命令:
ros2 launch spawn.launch.py
此时,漫游机器人(roamer_bot)就会成功显示在仿真场景中。

修复「车轮消失」问题(关节状态桥接)
在新版 Gazebo 中,运动部件(车轮)的位置不会自动同步给 ROS。机器人看起来像一个实心方块,原因是 ROS 无法获知车轮关节在三维空间中的位置。
我们需要运行一个桥接节点,将关节状态从 Gazebo 回传给 ROS:
ros2 run ros_gz_bridge parameter_bridge /world/default/model/roamer_bot/joint_state@sensor_msgs/msg/JointState[gz.msgs.Model
机器人状态发布器(robot_state_publisher)需要知晓每个连杆的位置。如果关节状态(joint_states)未被正确接收或映射到 URDF,所有运动部件会默认归位到机器人原点(0,0,0),最终被 **「隐藏」在机身内部 **。
运行第二个桥接节点,确保三维位姿同步:
ros2 run ros_gz_bridge parameter_bridge /model/roamer_bot/pose@tf2_msgs/msg/TFMessage[gz.msgs.Pose_V
问题根源
我们看不到车轮和激光雷达扫描光束的原因:当前 URDF 仍在使用旧版 Gazebo 插件 (libgazebo_ros_ray_sensor.so),该插件与 Gazebo Harmonic(ROS 2 Jazzy)不兼容 。在 Gazebo Harmonic 中,只有为所有运动部件定义惯性(inertial)和碰撞(collision)标签,物理效果与传感器才能正常工作。
1. 修复车轮问题(缺失物理属性)
在 Gazebo 仿真器中,如果一个连杆没有配置 <inertial>(惯性)或 <collision>(碰撞)标签,物理引擎会直接忽略该部件,或无法正常渲染。
请按如下格式更新车轮连杆:
<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="0.5"/>
<inertia ixx="0.00135" ixy="0" ixz="0" iyy="0.00135" iyz="0" izz="0.0025"/>
</inertial>
</link>
2. 添加核心「系统插件」
要让车轮正常转动、传感器正常输出数据,必须在 <robot> 标签内部添加这些插件(推荐放在文件最底部):
<gazebo>
<!-- 车轮差速驱动物理插件 -->
<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.45</wheel_separation>
<wheel_radius>0.1</wheel_radius>
</plugin>
<!-- 向ROS回传关节位置 -->
<plugin filename="gz-sim-joint-state-publisher-system" name="gz::sim::systems::JointStatePublisher"/>
<!-- 激光雷达传感器驱动 -->
<plugin filename="gz-sim-sensors-system" name="gz::sim::systems::Sensors">
<render_engine>ogre2</render_engine>
</plugin>
</gazebo>
以上配置即可修复车轮消失问题。

修复激光雷达(Harmonic 版本语法)
gazebo_ros_laser 插件在 ROS 2 Jazzy 版本中无法使用。我们必须使用 <gz_frame_id> 标签和 Gazebo 内置的传感器系统。
将你整个 <gazebo reference="lidar_link"> 代码块替换为以下内容:
<gazebo reference="lidar_link">
<sensor name="gpu_lidar" type="gpu_lidar">
<pose>0 0 0 0 0 0</pose>
<visualize>true</visualize>
<update_rate>10</update_rate>
<lidar>
<scan>
<horizontal>
<samples>360</samples>
<min_angle>-3.14159</min_angle>
<max_angle>3.14159</max_angle>
</horizontal>
</scan>
<range>
<min>0.3</min>
<max>10.0</max>
</range>
</lidar>
<gz_frame_id>lidar_link</gz_frame_id> <!-- ROS 2 Jazzy 必需配置 -->
</sensor>
</gazebo>
运行以下桥接命令,即可在 ROS 中查看激光雷达数据:
ros2 run ros_gz_bridge parameter_bridge /scan@sensor_msgs/msg/LaserScan[gz.msgs.LaserScan