# Introduction to Gazebo Sim with ROS2

卡片名称：Gazebo中使用URDF创建机器人

描述：学习如何使用 URDF 语言在 Gazebo 仿真中创建机器人，最终完成一个可在 Gazebo 中正确加载和验证的移动机器人模型。

## 目录

- 1 简介
- 2 Gazebo Sim、Ignition Gazebo 与 Gazebo Classic 对比
- 3 为什么使用 URDF 而不是 SDF？
- 4 创建移动机器人
- 5 连杆与关节
- 6 网格模型
- 7 使用 Gazebo Sim 服务生成机器人
- 8 使用 ROS2 Launch 文件生成机器人
- 9 总结

<div style="page-break-after: always;"></div>

## 1 简介

预计完成时间：**30 秒**


在本单元中，你将先梳理 **Gazebo Sim**、**Ignition Gazebo** 与 **Gazebo Classic** 的关系，再学习如何使用 **URDF** 语言为 Gazebo 仿真创建机器人。后续内容会逐步覆盖连杆、关节、网格模型以及机器人生成方式，最终完成一个可在 Gazebo 中正确加载和验证的移动机器人模型。

<div style="page-break-after: always;"></div>

## 2 Gazebo Sim、Ignition Gazebo 与 Gazebo Classic 对比

预计完成时间：**5 分钟**

### 什么是 Gazebo Sim？

**Gazebo Sim** 是一个 3D 动态模拟器，能够在复杂的室内和室外环境中准确高效地模拟机器人群体。虽然与游戏引擎类似，但 **Gazebo Sim** 提供了更高保真度的物理模拟、一套完整的传感器系统，以及面向用户和程序的接口。

**开源机器人基金会（OSRF）** 开发并维护 **Gazebo Sim**。

### 什么是 Ignition Gazebo？

**Gazebo Sim** 前身被称为 **Ignition Gazebo**。2022 年 4 月，Open Robotics 团队宣布弃用 **Ignition Gazebo** 这一名称，改用 **Gazebo Sim**。从 **Garden** 版本开始，**Ignition Gazebo** 已经完成重新品牌化，本课程也基于 **Garden** 版本构建，因此统一使用新的 **Gazebo Sim** 品牌和以 `gz` 开头的命令行工具。

例如，本课程里会使用 `gz model --list` 或 `gz topic -t /chatter "Hello!"` 这样的命令。如果你计划使用较早的 **Fortress** 版本，则需要把 `gz` 命令替换成 `ign` 命令。

**参考资料：** [https://www.openrobotics.org/blog/2022/4/6/a-new-era-for-gazebo](https://www.openrobotics.org/blog/2022/4/6/a-new-era-for-gazebo)

### Gazebo Sim 对机器人开发者有什么帮助？

**Gazebo Sim** 允许你在真实环境的动态模拟中测试机器人和算法。你可以选择多种物理引擎、高级 3D 图形、传感器及噪声模型、机器人插件以及现成的机器人模型库。此外，它运行在 TCP/IP 传输层之上，这使得网络集成变得简单，就像 ROS2 一样。

虽然 **Gazebo Classic**（版本 11）曾是 ROS 的默认模拟器，但它已经进入生命周期末期，且不适用于 `Humble` 之后的新版本 ROS2。新的 **Gazebo Sim** 会逐步取代 **Gazebo Classic**。

#### 可视化和调试

运行模拟可以提供你所需的关于环境和机器人的所有信息。你甚至可以调试软件和模拟本身，定位问题发生的位置。如果你想测试某种特定情况，也可以强制设定某个场景，反复运行软件直到获得足够的信息。

#### "快速失败，更快学习"

你不需要在真实机器人上测试软件来发现问题和调整参数。在模拟机器人上失败更快、成本更低，也能比在真实机器人上试错更快地改进软件。

#### 构建机器人太昂贵了！

每个人都想在真实机器人上测试算法，但现实是拥有一台真实机器人太昂贵了。不仅购买构建机器人所需的全部组件很贵，把所有部件组装并协调工作也非常耗时，同时你还需要处理机械、电子和软件等多个方面的问题。

#### 社区支持

**Gazebo Sim** 是开源软件，由 **OSRF** 维护。它拥有一个充满活力的社区，鼓励用户使用、改进并彼此提供支持。

### Gazebo Sim 与 Gazebo Classic 的比较

Gazebo Sim 为仿真带来了新的工作方式，提供了一整套开发库和云服务工具来简化模拟流程。

它可以帮助你利用高保真的传感器数据流，在接近真实的环境中快速迭代物理设计；你也可以在安全环境下测试控制策略，并把模拟充分用在持续集成测试中。

虽然 **Gazebo Classic** 的大多数功能已经在 **Gazebo Sim** 中实现，但仍有部分功能尚未完全补齐。例如，Gazebo Sim 中长期被提及的一个问题是没有完整的“缩放模式”，模型暂时无法像 Classic 那样直接缩放。

**参考资料：** [https://gazebosim.org/docs/fortress/comparison](https://gazebosim.org/docs/fortress/comparison)

在场景渲染方面，两者差异比较明显。**Gazebo Classic** 的画面更暗，在相同光照条件下阴影不够清晰；相比之下，**Gazebo Sim** 能更好地渲染亮度、光照和阴影细节。

| Gazebo Classic | Gazebo Sim |
| --- | --- |
| ![Gazebo Classic Scene](assets/unit1/01-gazebo-classic-scene.png) | ![Gazebo Sim Scene](assets/unit1/02-gazebo-sim-scene.png) |

<div style="page-break-after: always;"></div>

## 3 为什么使用 URDF 而不是 SDF？

预计完成时间：**30 秒**


Gazebo Sim 使用 **SDF** 格式来描述**世界**、**模型**、**机器人**以及构建完整仿真所需的其他组件。那么，为什么这里要用 **URDF** 来构建机器人呢？

**ROS2** 的许多工具都是围绕 **URDF** 构建的，例如 **RVIZ2**。当你在 Gazebo 仿真中生成一个用 URDF 定义的机器人时，Gazebo Sim 会在底层自动把 URDF 文件转换为 SDF。

在下一单元中，当你把机器人生成到 Gazebo 仿真里并让它与 ROS2 配合工作时，会进一步看到这一点。

<div style="page-break-after: always;"></div>

## 4 创建移动机器人

预计完成时间：**7 分钟**


要创建一个新的机器人模型，你需要按照特定的目录结构来组织机器人的定义文件和文件夹。

除了机器人的定义文件，也就是 **URDF** 文件之外，你还需要使用 launch 文件来调试机器人的创建过程。

创建一个功能包，并在其中新建以下文件夹：

**在 Web Shell 1 中执行**

```
cd /workspace/src
```

```
ros2 pkg create robot_description --build-type ament_python --dependencies rclpy urdf xacro
```

```
mkdir -p robot_description/urdf
```

```
touch robot_description/urdf/robot.urdf
```

**URDF** 文件夹将用于存放机器人的描述文件。

将下面的内容粘贴到 **robot.urdf** 文件中：

**robot.urdf**

```
<?xml version="1.0"?>

<robot name="robot">
    <!-- Link - chassis -->
    <link name="link_chassis">
        <inertial>
            <mass value="10" />
            <origin xyz="0 0 0.3" rpy="0 0 0" />
            <inertia ixx="1.5417" ixy="0" ixz="0" iyy="3.467" iyz="0" izz="4.742" />
        </inertial>

        <collision>
            <geometry>
                <box size="2 1.3 0.4" />
            </geometry>
        </collision>

        <visual>
            <geometry>
                <box size="2 1.3 0.4" />
            </geometry>
            <material name="Red">
                <color rgba="1 0 0 1" />
            </material>
        </visual>
    </link>
</robot>
```

使用下面的命令在该功能包中创建一个新的 launch 文件：

**在 Web Shell 1 中执行**

```
cd /workspace/src
```

```
mkdir -p robot_description/launch
```

```
touch robot_description/launch/rviz.launch.py
```

**launch** 文件夹将用于存放使用 **RVIZ2** 调试该机器人模型所需的 launch 文件。

复制并粘贴以下内容：

**rviz.launch.py**

```
import os
from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.substitutions import Command
from launch_ros.actions import Node

# ROS2 Launch System will look for this function definition #
def generate_launch_description():

    # Get Package Description and Directory #
    package_description = "robot_description"
    package_directory = get_package_share_directory(package_description)

    # Load URDF File #
    urdf_file = 'robot.urdf'
    robot_desc_path = os.path.join(package_directory, "urdf", urdf_file)
    print("URDF Loaded !")

    # Robot State Publisher (RSP) #
    robot_state_publisher_node = Node(
        package='robot_state_publisher',
        executable='robot_state_publisher',
        name='robot_state_publisher_node',
        output="screen",
        emulate_tty=True,
        parameters=[{'use_sim_time': True,
                     'robot_description': Command(['xacro ', robot_desc_path])}]
    )

    # RViz2 Launch Configuration (RViz) #
    rviz_node = Node(
        package='rviz2',
        executable='rviz2',
        name='rviz2_node',
        output='screen',
        emulate_tty=True,
        parameters=[{'use_sim_time': True}],
    )

    # Create and Return the Launch Description Object #
    return LaunchDescription(
        [
            robot_state_publisher_node,
            rviz_node,
        ]
    )
```

修改 `setup.py` 文件，确保 ROS2 Launch 系统能够检测并执行你在上面步骤中创建的 launch 文件。

将下面的内容复制到 `setup.py` 文件中，直接替换原有内容。

**setup.py**

```
import os
from glob import glob
from setuptools import setup

package_name = 'robot_description'

setup(
    name=package_name,
    version='0.0.0',
    packages=[package_name],
    data_files=[
        ('share/ament_index/resource_index/packages',
            ['resource/' + package_name]),
        ('share/' + package_name, ['package.xml']),
        (os.path.join('share', package_name, 'urdf'), glob('urdf/*.urdf')),
        (os.path.join('share', package_name, 'launch'), glob('launch/*.launch.*')),
    ],
    install_requires=['setuptools'],
    zip_safe=True,
    maintainer='user',
    maintainer_email='user@todo.todo',
    description='TODO: Package description',
    license='TODO: License declaration',
    entry_points={
        'console_scripts': [
        ],
    },
)
```

新旧文件之间真正不同的只有下面这几行：

```
# imports
import os
from glob import glob
...

# paths to urdf and launch files that must be copied to 'share' folder
(os.path.join('share', package_name, 'urdf'), glob('urdf/*.urdf')),
(os.path.join('share', package_name, 'launch'), glob('launch/*.launch.*')),
```
在平台右上角点击“分割右侧”和“远程桌面”，并等待连接。

![RViz](assets/unit1/03-1.png)

按照下面的命令来启动你刚刚创建的文件：

**在 Web Shell 1 中执行**

```
cd /workspace
```

```
colcon build --packages-select robot_description && source install/setup.bash
```

```
ros2 launch robot_description rviz.launch.py
```

启动该文件后，等待**图形化工具**打开。你应该会看到下图所示的结果。然后，在显示面板中添加 **RobotModel**，并将 `/robot_description` 设为**描述主题**。接着，把 RVIZ2 中的**固定坐标系**改为 **link_chassis**。这样就会把机器人的底盘连杆设为 **RVIZ2** 模型可视化的参考坐标系。

![RViz URDF First Launch](assets/unit1/03-rviz-urdf-first-launch.gif)

把这个配置保存在你的工作区中，这样下次启动 RVIZ2 时，机器人模型就会被正确显示。
按照下面的命令来创建rviz目录：

**新建终端，在 Web Shell 2 中执行**

```
cd /workspace/src
```

```
mkdir -p robot_description/rviz
```

在rviz2界面使用“ctrl+shift+s“，保存当前的 RVIZ2 配置到正确的工作目录下。
![RViz](assets/unit1/03-2.png)
将当前的 RVIZ2 配置保存为 `config.rviz`，并放到你的 ROS2 `robot_description` 功能包中新建的 `rviz` 目录里。

把 `rviz` 文件夹添加到 `setup.py` 中，这样下次编译时就能检测到 `config.rviz`。

此时你的 `setup.py` 文件应该包含下面这三行：

```
(os.path.join('share', package_name, 'urdf'), glob('urdf/*.urdf')),
(os.path.join('share', package_name, 'rviz'), glob('rviz/*.rviz')),
(os.path.join('share', package_name, 'launch'), glob('launch/*.launch.*')),
```

最后，把 `config.rviz` 文件作为参数传给 launch 文件中的 RVIZ 节点。

```
# Load RViz Configuration File #
    rviz_config_file = "config.rviz"
    rviz_config_path = os.path.join(package_directory, "rviz", rviz_config_file)
    print("RViz Config Loaded !")

    # Rviz2 Launch Configuration (RViz)#
    rviz_node = Node(
        package='rviz2',
        executable='rviz2',
        name='rviz2_node',
        output='screen',
        emulate_tty=True,
        parameters=[{'use_sim_time': True}],
        arguments=['-d', rviz_config_path],
    )
```

现在你的 launch 文件应该如下所示：

**rviz.launch.py**

```
import os
from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.substitutions import Command
from launch_ros.actions import Node

# ROS2 Launch System will look for this function definition #
def generate_launch_description():

    # Get Package Description and Directory #
    package_description = "robot_description"
    package_directory = get_package_share_directory(package_description)

    # Load URDF File #
    urdf_file = 'robot.urdf'
    robot_desc_path = os.path.join(package_directory, "urdf", urdf_file)
    print("URDF Loaded !")

    # Robot State Publisher (RSP) #
    robot_state_publisher_node = Node(
        package='robot_state_publisher',
        executable='robot_state_publisher',
        name='robot_state_publisher_node',
        output="screen",
        emulate_tty=True,
        parameters=[{'use_sim_time': True,
                     'robot_description': Command(['xacro ', robot_desc_path])}]
    )

    # Load RViz Configuration File #
    rviz_config_file = "config.rviz"
    rviz_config_path = os.path.join(package_directory, "rviz", rviz_config_file)
    print("RViz Config Loaded !")

    # Rviz2 Launch Configuration (RViz)#
    rviz_node = Node(
        package='rviz2',
        executable='rviz2',
        name='rviz2_node',
        output='screen',
        emulate_tty=True,
        parameters=[{'use_sim_time': True}],
        arguments=['-d', rviz_config_path],
    )

    # Create and Return the Launch Description Object #
    return LaunchDescription(
        [
            robot_state_publisher_node,
            rviz_node,
        ]
    )
```

<div style="page-break-after: always;"></div>

## 5 连杆与关节

预计完成时间：**15 分钟**


### 连杆

你已经创建了第一个只有单一元素的机器人模型，也就是一个连杆。那么，一个连杆由哪些部分组成呢？

每个连杆都必须包含以下属性：

##### 惯性属性

它包含 **质量（kg）**、**原点（m）** 和 **惯性矩阵（kg·m²）**。

```
<inertial>
    <mass value="10" />
    <origin
        xyz="0 0 0.3"
        rpy="0 0 0" />
    <inertia
         ixx="1.5417"
         ixy="0"
         ixz="0"
         iyy="3.467"
         iyz="0"
         izz="4.742" />
</inertial>
```

##### 碰撞与可视属性

在这个例子中，**collision** 和 **visual** 看起来是相同的，但它们的用途不同。模拟器使用 **collision** 来计算连杆之间的物理交互；**visual** 仅用于渲染显示，不会影响连杆与其他对象的交互方式。

如果你的可视网格非常复杂，但又不希望在物理交互上做不必要的高强度计算，或者想节省算力，就可以让 **collision** 和 **visual** 使用不同的几何定义。

```
<collision>
    <geometry>
        <box size="2 1.3 0.4" />
    </geometry>
</collision>

<visual>
    <geometry>
        <box size="2 1.3 0.4" />
    </geometry>
</visual>
```

### 关节

关节用于连接各个连杆，并在它们之间建立运动关系。在本单元中，你要构建的是移动机器人，因此接下来要给你创建的底盘加上一只车轮。

继续在 **robot.urdf** 文件中，在 'link' 结束标签之后添加以下代码：

**复制并粘贴**

```
<!-- Joint - chassis / left wheel -->
<joint name="joint_chassis_left_wheel" type="continuous">
    <origin rpy="0 0 0" xyz="-0.5 0.65 0" />
    <child link="link_left_wheel" />
    <parent link="link_chassis" />
    <axis rpy="0 0 0" xyz="0 1 0" />
    <limit effort="10000" velocity="1000" />
    <joint_properties damping="1.0" friction="1.0" />
</joint>

<!-- Link - left wheel -->
<link name="link_left_wheel">
    <inertial>
        <mass value="1" />
        <origin xyz="0 0 0" rpy="0 0 0" />
        <inertia ixx="0.002526666666667" ixy="0" ixz="0" iyy="0.002526666666667" iyz="0" izz="0.005"/>
    </inertial>

    <collision>
        <origin rpy="0 0 1.5707" xyz="0 0.18 0" />
        <geometry>
            <cylinder length="0.12" radius="0.4"/>
        </geometry>
    </collision>
    <visual>
        <origin rpy="0 0 1.5707" xyz="0 0.18 0" />
        <geometry>
            <cylinder length="0.12" radius="0.4"/>
        </geometry>
    </visual>

    <collision>
        <origin rpy="0 0 1.5707" xyz="0 0.06 0" />
        <geometry>
            <cylinder length="0.12" radius="0.08"/>
        </geometry>
    </collision>
    <visual>
        <origin rpy="0 0 1.5707" xyz="0 0.06 0" />
        <geometry>
            <cylinder length="0.12" radius="0.08"/>
        </geometry>
    </visual>
</link>
```

### 车轮连杆定义

这里你有两组 **collision** 和 **visual** 标签，它们各自对应不同的部分。一个连杆中可以包含多组这样的标签，数量并不受限。它们都会按照关节的位置关系显示出来。

在这个例子里，你分别为车轮本体和它的轮轴创建了一组可视与碰撞定义。

重新启动 **rviz.launch.py** 文件。

**在 Web Shell 1 中执行**

```
cd /workspace
```

```
colcon build --packages-select robot_description && source install/setup.bash
```

```
ros2 launch robot_description rviz.launch.py
```

你应该得到如下结果：

![RViz URDF Links Launch](assets/unit1/04-no-wheel1.png)

但是等等……车轮去哪了？这里发生了什么？

这是因为你当前只运行了 **Robot State Publisher** 和 RViz。这个节点只会发布机器人**固定关节**的状态（这里是底盘），不会发布**可动关节**的状态（例如车轮）。如果想把这些可动关节显示出来，你还需要一个能够提供关节状态的节点，这就是 **Joint State Publisher GUI** 的作用。

要启动 **Joint State Publisher GUI**，请在一个新的 web shell 中运行以下命令：

**在 Web Shell 2 中执行**

```
ros2 run joint_state_publisher_gui joint_state_publisher_gui
```
**提示：** 如果无法运行请先执行：

```
sudo apt update
```

```
sudo apt install ros-humble-joint-state-publisher-gui
```

```
unset PYTHONPATH
```

```
source /opt/ros/humble/setup.bash
```

现在你会看到类似下面的界面：

![RViz URDF Links Launch](assets/unit1/05-rotate-wheel1.gif)

注意，现在你已经可以通过滑块来控制车轮旋转了。

当然，这还不是车轮期望的安装位置或旋转方式。那么问题出在哪里呢？

### 关节定义

来看一下关节定义：

```
<joint name="joint_chassis_left_wheel" type="continuous">
    <origin rpy="0 0 0" xyz="-0.5 0.88 0" />
    <child link="link_left_wheel" />
    <parent link="link_chassis" />
    <axis rpy="0 0 0" xyz="0 1 0" />
    <limit effort="10000" velocity="1000" />
    <joint_properties damping="1.0" friction="1.0" />
</joint>
```

第一个属性 **type** 用来定义该连杆能够进行哪一种运动。常见类型包括 **continuous**、**revolute**、**prismatic** 和 **fixed**。除此之外还有其他类型，你可以在这里查看完整参考：[http://wiki.ros.org/urdf/XML/joint](http://wiki.ros.org/urdf/XML/joint)。

**origin** 属性用于定义关节相对于底盘的位置。底盘连杆起始于 **0 0 0**，长度为 2 米，宽度为 1.3 米。底盘会向两侧延伸，因此它的侧边界位于 **1.3/2**，也就是 **0.65 米** 的位置。

接着，需要定义 **child** 和 **parent** 连杆。再比如 **axis**，它用来指定转动轴。你还可以通过 **limit** 设置最大力矩和速度，不过现在这部分并不是重点。当前阶段你主要是在设计机器人的几何结构，而与运动动力学相关的 **joint properties** 会在后续再讨论。

最后来看 **axis** 定义。这里决定该连杆绕哪一个坐标轴旋转。在这个例子中，你希望它绕 Y 轴旋转，这个设置是正确的。

### 修正连杆定义以匹配关节旋转

问题在于，你的这个 **link** 圆柱几何体（长度和半径）是按照 Z 轴方向定义的，因此需要绕 X 轴旋转一下才能修正。

回到左轮的连杆定义，把 **collision** 和 **visual** 标签中的 **origin** 属性改成绕 X 轴旋转 90 度：

```
<link name="link_left_wheel">
    ...
    <collision>
        <origin rpy="1.5707 0 0" xyz="0 0.18 0" />
        <geometry>
            <cylinder length="0.12" radius="0.4"/>
        </geometry>
    </collision>
    <visual>
        <origin rpy="1.5707 0 0" xyz="0 0.18 0" />
        <geometry>
            <cylinder length="0.12" radius="0.4"/>
        </geometry>
    </visual>

    <collision>
        <origin rpy="1.5707 0 0" xyz="0 0.06 0" />
        <geometry>
            <cylinder length="0.12" radius="0.08"/>
        </geometry>
    </collision>
    <visual>
        <origin rpy="1.5707 0 0" xyz="0 0.06 0" />
        <geometry>
            <cylinder length="0.12" radius="0.08"/>
        </geometry>
    </visual>
    ...
```

重新启动 **rviz.launch**，并同时运行 **Joint State Publisher GUI**。

**在 Web Shell 1 中执行**

```
cd /workspace
```

```
colcon build --packages-select robot_description && source install/setup.bash
```

```
ros2 launch robot_description rviz.launch.py
```

**在 Web Shell 2 中执行**

```
unset PYTHONPATH
source /opt/ros/humble/setup.bash
ros2 run joint_state_publisher_gui joint_state_publisher_gui
```

![RViz URDF Links Joints Launch](assets/unit1/06-rviz-urdf-links-joints-launch.gif)

查看下图中的机器人尺寸，你会更容易理解为什么 XML 定义里使用的是这些数值：

![Robot Dimensions](assets/unit1/07-robot-dimensions.png)

**练习 2.5.1**

完善当前模型并把它补全！

在这个练习中，再补充以下几个部分，让机器人更完整：

- 添加**右侧车轮**
- 给两个车轮都添加新的 **visual** 元素，让旋转效果更明显（例如轮毂盖）
- 在机器人前方添加一个通过 **fixed** 关节连接的**脚轮**
- 修改并补充底盘和车轮的**材质颜色**，让它看起来尽量接近下图

![RViz URDF Ex22 Launch](assets/unit1/08-rviz-urdf-ex22-launch.gif)

请参考下图来创建脚轮的固定关节以及可视部件。

![Robot Dimensions Extended](assets/unit1/09-robot-dimensions-extended.png)

**注意：** 当前 ROS2 RVIZ2 中存在一个 Bug：URDF 中父连杆的颜色会传递到子连杆上，因此 RVIZ 中显示的颜色可能与 URDF 文件中的定义并不完全一致。更多信息请参考 RVIZ2 的[颜色 Bug](https://github.com/ros-visualization/rviz/issues/843)。

**练习 2.5.1 结束**

<div style="page-break-after: always;"></div>

**练习 2.5.1 参考答案**

在你自己尝试完成练习之前，不要先看答案。完成之后再回头对照参考答案，会更有助于比较和检查自己的实现。

如果你在完成练习时遇到问题，可以到论坛寻求支持。

[robot.urdf](assets/files/robot1.urdf)

**练习 2.5.1 参考答案结束**

#### 参考资料

你可以在下面查看连杆和关节的完整规范（链接指向 ROS1 文档）：

- [http://wiki.ros.org/urdf/XML/link](http://wiki.ros.org/urdf/XML/link)
- [http://wiki.ros.org/urdf/XML/joint](http://wiki.ros.org/urdf/XML/joint)

在使用 XML 为 ROS2 定义机器人模型时，这些说明依然适用。

<div style="page-break-after: always;"></div>

## 6 网格模型

预计完成时间：**10 分钟**


网格模型可以用来创建更复杂的三维几何形状。设想一下，你手上有一台真实机器人，整个项目是用 CAD 软件完成的，现在你想对它进行仿真，那么你通常会直接拥有模型文件。URDF 支持 **.stl** 和 **.dae** 文件，其中后者通常能提供更好的显示效果。

下面来看如何在你当前制作的机器人中使用网格模型。

进入底盘的 **visual** 部分，并将下面这段代码替换掉：

```
<geometry>
    <box size="2 1.3 0.4" />
</geometry>
```

替换为以下内容：

**复制并粘贴**

```
<geometry>
    <mesh filename="package://robot_description/meshes/chassis.stl" />
</geometry>
```

URDF 支持 `package://` 这种写法，可以通过相对路径更方便地找到文件。这也意味着你的功能包中必须有一个 `meshes` 文件夹。先创建这个文件夹，然后上传下面的文件：

**新建终端，在 Web Shell 3 中执行**

```
mkdir -p /workspace/src/robot_description/meshes/
```

将这个 [chassis.stl](assets/files/chassis.stl) 文件放进你在 `robot_description` 功能包里创建的 `meshes` 文件夹。

把 `meshes` 文件夹添加到 `setup.py` 中，这样下次编译时就能检测到 `*.stl` 文件。

此时你的 `setup.py` 文件应该包含下面这四行：

```
(os.path.join('share', package_name, 'urdf'), glob('urdf/*.urdf')),
(os.path.join('share', package_name, 'rviz'), glob('rviz/*.rviz')),
(os.path.join('share', package_name, 'meshes'), glob('meshes/*.stl')),
(os.path.join('share', package_name, 'launch'), glob('launch/*.launch.*')),
```

重新启动 **rviz.launch** 和 **Joint State Publisher GUI**，你应该看到如下结果：

![Robot Chassis Mesh](assets/unit1/11-robot-chassis-mesh.png)

注意，这个 **.stl** 文件会沿用基础几何体所设置的材质颜色。

**练习 2.6.1**

为车轮也添加一个网格模型文件。

使用下面这个文件：

- [wheel.stl](assets/files/wheel.stl)

调整轮毂盖的可视效果，让机器人尽量接近下图所示的样子：

![Robot Wheel Mesh](assets/unit1/12-robot-wheel-mesh.png)

**练习 2.6.1 结束**

<div style="page-break-after: always;"></div>

**练习 2.6.1 参考答案**

在自己尝试完成练习之前，不要先看答案。完成后再查看参考答案，会更方便你对比自己的实现。

如果你在完成练习时遇到问题，可以到论坛寻求支持。

[robot.urdf](assets/files/robot2.urdf)

**练习 2.6.1 参考答案结束**

<div style="page-break-after: always;"></div>

## 7 使用 Gazebo Sim 服务生成机器人

预计完成时间：**5 分钟**


你已经把机器人搭建好了，它已经有了底盘和车轮，因此已经可以用于仿真了。下面来看看如何把它放进 Gazebo 仿真环境中。

你可以通过两种方式把机器人模型生成到 **Gazebo Sim** 中：

1. 使用 Gazebo Sim 生成服务
2. 使用 ROS 2 Launch 文件

#### 使用 Gazebo Sim 生成服务

虽然这不是把模型生成到 Gazebo Sim 中的首选方法，但你仍然可以借此观察模型在 Gazebo Sim 中生成后的行为。

这种方式会使用 Gazebo Sim 的 `create` 服务，它采用的是 **EntityFactory** 消息类型。

**注意：**
开始之前，请关闭并终止所有 web shell 中正在运行的任务。对所有活跃的 web shell 按下 `Ctrl + C` 来停止当前进程。

修改“远程桌面”面板，切换到“仿真器”。

![gazebo](assets/unit1/12-1.png)

首先需要设置环境变量，为 Gazebo Sim 指定资源路径。

把模型的 URDF 文件路径以及相关网格文件路径都配置进去。

**在 Web Shell 1 中执行**

```
export GZ_SIM_RESOURCE_PATH=/workspace/src/robot_description/urdf:/workspace/src
```

现在以无头模式启动 Gazebo Sim 世界，这里我们使用空世界。

**在 Web Shell 1 中执行**

```
gz sim -s -r empty.sdf
```

**在 Web Shell 2 中执行**

```
bash /workspace/src/run_websocket.sh
```

在另一个终端中，使用下面的命令列出所有可用的服务：

**在 Web Shell 3 中执行**

```
gz service --list
```

执行完上面的命令后，你会在仿真器里看到加载的空世界，并在输出中看到一个名为 `/world/empty/create` 的服务。通用的服务名称格式为 `/world/<name_of_world>/create`。例如，如果你的 world 文件中的世界名称是 `factory_floor`，那么服务名就会是 `/world/factory_floor/create`。

继续在同一个终端中，查看该服务的信息，以确认请求和响应所使用的消息类型。

**注意：** 和 ROS2 命令不同，这个命令不支持 Tab 自动补全，所以需要手动输入完整命令。

**在 Web Shell 3 中执行**

```
gz service --info --service /world/empty/create
```

执行完该命令后，你应该会看到与下图类似的输出：

```
Service providers [Address, Request Message Type, Response Message Type]:
  tcp://192.168.32.7:37923, gz.msgs.EntityFactory, gz.msgs.Boolean
```

然后在同一个终端中输入下面的命令，将你的 URDF 模型生成到 **Gazebo Sim** 中：

**注意：** 和 ROS2 命令不同，这个命令不支持 Tab 自动补全，所以需要手动输入完整命令。

**在 Web Shell 3 中执行**

```
cd /workspace/src/robot_description/urdf
```

```
gz service --service /world/empty/create \
  --reqtype gz.msgs.EntityFactory \
  --reptype gz.msgs.Boolean \
  --timeout 1000 \
  --req 'sdf_filename: "robot.urdf" name: "my_robot"'
```

**注意：**`--req` 参数中的内容**不能**包含空格。如果你写入了空格，就会收到下面这样的报错：

`The following arguments were not expected: "my_robot" name: "robot.urdf",`

如果你正确完成了前面的步骤，那么执行生成服务命令后应该会返回 `data: true`。

查看 Gazebo Sim 界面时，你会发现机器人模型已经生成在 `x = 0.0, y = 0.0, and z = 0.0` 的位置。

<div style="page-break-after: always;"></div>

## 8 使用 ROS2 Launch 文件生成机器人

预计完成时间：**10 分钟**


这是把模型生成到 Gazebo Sim 中最推荐的方法。通过这种可控的生成方式，你可以在需要时自动把模型插入 Gazebo Sim。

在 **launch** 文件夹中创建一个新文件 **empty_world.launch.py**，并写入相应内容。

**在 Web Shell 3 中执行**

```
touch /workspace/src/robot_description/launch/empty_world.launch.py
```

现在，把下面的代码复制到你刚刚创建的 launch 文件中。

**empty_world.launch.py**

```
import os
from ament_index_python.packages import (get_package_prefix, get_package_share_directory)
from launch import LaunchDescription
from launch.actions import (DeclareLaunchArgument, IncludeLaunchDescription)
from launch.substitutions import (PathJoinSubstitution, LaunchConfiguration)
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch_ros.actions import SetParameter

# ROS2 Launch System will look for this function definition #
def generate_launch_description():

    # Get Package Description and Directory #
    package_description = "robot_description"
    package_directory = get_package_share_directory(package_description)

    # Set the path to robot resources before launching Gazebo Sim #
    install_dir_path = (get_package_prefix(package_description) + "/share")
    robot_meshes_path = os.path.join(package_directory, "meshes")
    gazebo_resource_paths = [install_dir_path, robot_meshes_path]
    if "GZ_SIM_RESOURCE_PATH" in os.environ:
        for resource_path in gazebo_resource_paths:
            if resource_path not in os.environ["GZ_SIM_RESOURCE_PATH"]:
                os.environ["GZ_SIM_RESOURCE_PATH"] += (':' + resource_path)
    else:
        os.environ["GZ_SIM_RESOURCE_PATH"] = (':'.join(gazebo_resource_paths))

    # Load the empty world in headless server mode #
    world_file = "empty.sdf"
    world_config = LaunchConfiguration("world")
    declare_world_arg = DeclareLaunchArgument(
        "world",
        default_value=["-s -r ", world_file],
        description="Gazebo Sim world and startup flags",
    )

    # Declare GazeboSim Launch #
    gzsim_pkg = get_package_share_directory("ros_gz_sim")
    gz_sim = IncludeLaunchDescription(
        PythonLaunchDescriptionSource(
            PathJoinSubstitution([gzsim_pkg, "launch", "gz_sim.launch.py"])),
            launch_arguments={"gz_args": world_config}.items(),
    )

    # Create and Return the Launch Description Object #
    return LaunchDescription(
        [
            declare_world_arg,
            # Sets use_sim_time for all nodes started below (doesn't work for nodes started from ignition gazebo) #
            SetParameter(name="use_sim_time", value=True),
            gz_sim,
        ]
    )
```

启动仿真：

**在 Web Shell 1 中执行**

```
cd /workspace
```

```
colcon build --packages-select robot_description && source install/setup.bash
```

```
ros2 launch robot_description empty_world.launch.py
```

**在 Web Shell 2 中执行**

```
bash /workspace/src/run_websocket.sh
```

等待仿真器界面刷新，你现在启动的是一个空世界。建议把生成机器人的逻辑放到单独的文件中，这样在开发过程中就可以不重启整个仿真，而是直接删除并重新生成机器人。

在launch目录下新建一个 **spawn.launch.py** 文件，并填入下面的内容：

**在 Web Shell 3 中执行**

```
touch src/robot_description/launch/spawn.launch.py
```

**spawn.launch.py**

```
import os
from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import (Command, LaunchConfiguration)
from launch_ros.actions import (Node, SetParameter)

# ROS2 Launch System will look for this function definition #
def generate_launch_description():

    # Get Package Description and Directory #
    package_description = "robot_description"
    package_directory = get_package_share_directory(package_description)

    # Load URDF File #
    urdf_file = 'robot.urdf'
    robot_desc_path = os.path.join(package_directory, "urdf", urdf_file)
    print("URDF Loaded !")

    # Robot State Publisher (RSP) #
    robot_state_publisher_node = Node(
        package='robot_state_publisher',
        executable='robot_state_publisher',
        name='robot_state_publisher_node',
        output="screen",
        emulate_tty=True,
        parameters=[{'use_sim_time': True,
                     'robot_description': Command(['xacro ', robot_desc_path])}]
    )

    # Spawn the Robot #
    declare_spawn_x = DeclareLaunchArgument("x", default_value="0.0",
                                            description="Model Spawn X Axis Value")
    declare_spawn_y = DeclareLaunchArgument("y", default_value="0.0",
                                            description="Model Spawn Y Axis Value")
    declare_spawn_z = DeclareLaunchArgument("z", default_value="0.5",
                                            description="Model Spawn Z Axis Value")
    gz_spawn_entity = Node(
        package="ros_gz_sim",
        executable="create",
        name="my_robot_spawn",
        arguments=[
            "-name", "my_robot",
            "-allow_renaming", "true",
            "-topic", "robot_description",
            "-x", LaunchConfiguration("x"),
            "-y", LaunchConfiguration("y"),
            "-z", LaunchConfiguration("z"),
        ],
        output="screen",
    )

    # Create and Return the Launch Description Object #
    return LaunchDescription(
        [
            # Sets use_sim_time for all nodes started below (doesn't work for nodes started from ignition gazebo) #
            SetParameter(name="use_sim_time", value=True),
            robot_state_publisher_node,
            declare_spawn_x,
            declare_spawn_y,
            declare_spawn_z,
            gz_spawn_entity,
        ]
    )
```

这个 CLI 允许你传入三个参数：X、Y 和 Z。运行这个 **ros_gz_sim/create** 节点后，它会把 URDF 模型转换并插入当前正在运行的 Gazebo 仿真中。

生成机器人：

**在 Web Shell 3 中执行**

```
cd /workspace
```

```
colcon build --packages-select robot_description && source install/setup.bash
```

```
ros2 launch robot_description spawn.launch.py
```

结果应当类似下图：

![Robot Spawn View](assets/unit1/14-robot-spawn-view.png)

![Robot Spawn View CloseUp](assets/unit1/15-robot-spawn-view-closeup.png)


**练习 2.8.1**

为 launch 文件新增一个参数，用来设置插入模型的名称。它需要把固定写死的 **my_robot** 名称替换为 CLI 中传入的名称。

预期结果是：执行下面的命令后，得到如下效果：

```
ros2 launch robot_description spawn.launch.py
# 生成完后退出
```

```
ros2 launch robot_description spawn.launch.py x:=5 y:=5 model_name:=my_robot_2
```

![Expected Result Ex24](assets/unit1/17-expected-result-ex24.png)

**练习 2.8.1 结束**

<div style="page-break-after: always;"></div>

**练习 2.8.1 参考答案**

在自己动手尝试之前，不要先看答案。完成练习之后再查看参考答案，会更方便你对比自己的实现。

如果你在完成练习时遇到问题，可以到论坛寻求支持。

[spawn.launch.py](assets/files/spawn.launch.py)

**练习 2.8.1 参考答案结束**

<div style="page-break-after: always;"></div>

## 9 总结

预计完成时间：**30 秒**


在本单元中，你学习了使用 **URDF** 构建机器人模型所需的基础元素，包括连杆、关节以及其他关键组件。你还动手创建了自己的机器人，并把它生成到 Gazebo 中，确保模型能够被正确加载，并准备好在仿真环境中进行交互。
