在机器人开发中,仿真是一项至关重要的技术。它允许我们在没有实体硬件的情况下,验证算法逻辑、调试传感器数据。本篇将带你走进 ROS2 机器人仿真的世界,从零开始构建你的第一个机器人模型。


一、 机器人仿真架构概览

机器人仿真系统通常由以下几个核心层次组成:

        【控制系统】 (ROS2 Nodes)
           |             |
     [传感器数据]    [执行器指令]
           |             |
        【机器人仿真平台】 (如 Gazebo)
           |
      [物理环境 + 机器人模型]

常用仿真平台:

  • Gazebo:ROS 社区最主流的物理仿真引擎,支持复杂的物理碰撞和动力学。
  • Webots:商业级开源仿真软件,性能优异,界面友好。
  • Ignition (Gazebo Sim):Gazebo 的下一代版本,架构更现代。

二、 创建机器人模型:URDF 与 Xacro 实战

URDF (Unified Robot Description Format) 是 ROS 中用于描述机器人结构的 XML 格式文件。

1. 创建功能包

首先,我们需要创建一个专门存放机器人描述文件的功能包:

# 进入工作空间 src 目录
cd ~/chapt6_ws/src
# 创建 CMake 类型的包
ros2 pkg create fishbot_description --build-type ament_cmake --license Apache-2.0

2. 使用 URDF 创建机器人身体

新建文件 chapt6_ws/src/fishbot_description/urdf/first_robot.urdf,写入以下基础结构:

<?xml version="1.0"?>
<robot name='first_robot'>
    <!-- 机器人的身体部分 -->
    <link name='base_link'>
        <!-- 部件的外观描述 -->
        <visual>
            <!-- 沿自己几何中心的坐标系 -->
            <origin xyz="0.0 0.0 0.0" rpy="0.0 0.0 0.0"/>
            <!-- 几何形状 -->
            <geometry>
                <!-- 圆柱 半径 高度-->
                <cylinder radius="0.10" length="0.12"/>
            </geometry>
            <!-- 颜色  -->
            <material name="white">
                <color rgba="1.0 1.0 1.0 0.5"/>
            </material>
        </visual>
    </link>

    <!-- 机器人的IMU部件 惯性测量传感器-->
    <link name='imu_link'>
        <visual>
            <origin xyz="0.0 0.0 0.0" rpy="0.0 0.0 0.0"/>
            <geometry>
                <box size="0.02 0.02 0.02"/>
            </geometry>
            <material name="black">
                <color rgba="0.0 0.0 0.0 0.5"/>
            </material>
        </visual>
    </link>

    <!-- 机器人关节:用于组合机器人的部件 -->
    <joint name='imu_joint' type='fixed'>
        <parent link='base_link'/>
        <child link='imu_link'/>
        <origin xyz="0.0 0.0 0.03" rpy="0.0 0.0 0.0"/>
    </joint>

</robot>
🔍 URDF 核心标签解析:
  • <link>:描述机器人的刚体部件(如身体、传感器)。<visual> 决定了屏幕上显示的样子,<geometry> 定义形状。
  • <joint>:描述部件之间的连接关系。type='fixed' 代表固定关节,父子部件相对位置不变。

(提示:可以使用命令 urdf_to_graphviz first_robot.urdf 将模型结构导出为可视化的结构图)

3. 在 RVIZ 中可视化机器人

RVIZ 是 ROS 的 3D 可视化利器。为了让 RVIZ 能够读取并显示我们的 URDF,需要启动几个辅助节点。

3.1 安装必要工具
sudo apt update
sudo apt install ros-$ROS_DISTRO-joint-state-publisher ros-$ROS_DISTRO-robot-state-publisher -y
3.2 配置 CMakeLists.txt

为了让系统能在安装后找到我们的文件,添加安装规则:

install(DIRECTORY launch urdf
  DESTINATION share/${PROJECT_NAME}
)
3.3 编写 Launch 启动脚本

新建 chapt6_ws/src/fishbot_description/launch/display_robot.launch.py

import launch
import launch_ros
from ament_index_python.packages import get_package_share_directory
import os

def generate_launch_description():
    # 获取默认urdf路径
    urdf_package_path = get_package_share_directory('fishbot_description')
    default_urdf_path = os.path.join(urdf_package_path, 'urdf', 'first_robot.urdf')

    # 声明参数
    action_declare_arg_mode_path = launch.actions.DeclareLaunchArgument(
        name='model',default_value=str(default_urdf_path),description='加载的模型文件路径'
    )
    # 通过文件路径获取内容,并转化为参数
    Substitutions_command_result = launch.substitutions.Command(['cat ',launch.substitutions.LaunchConfiguration('model')])
    robot_description_value = launch_ros.parameter_descriptions.ParameterValue(Substitutions_command_result,value_type=str)

    # 发布机器人 TF 树
    action_robot_state_publisher = launch_ros.actions.Node(
        package='robot_state_publisher',
        executable='robot_state_publisher',
        parameters=[{'robot_description': robot_description_value}] 
    )

    # 发布关节状态
    action_joint_state_publisher = launch_ros.actions.Node(
        package='joint_state_publisher',
        executable='joint_state_publisher',
    )

    action_rviz_node = launch_ros.actions.Node(
        package='rviz2',
        executable='rviz2',
    )

    return launch.LaunchDescription([
        action_declare_arg_mode_path, 
        action_robot_state_publisher,
        action_joint_state_publisher,
        action_rviz_node,
    ])
3.4 运行与 RVIZ 配置

编译后启动:

colcon build
source install/setup.bash
ros2 launch fishbot_description display_robot.launch.py

进入 RVIZ 后:

  1. 修改 Fixed Framebase_link

  2. 点击 Add,添加 RobotModel,即可看到模型:
    RVIZ 中初步加载的机器人模型

  3. 点击 Add,添加 TF 显示坐标系关系:
    RVIZ 中显示的机器人 TF 坐标变换树

最终初始模型显示效果如下:
第一个 URDF 模型渲染结果

  1. 将配置保存至 chapt6_ws/src/fishbot_description/config/display_roboy_model.rviz

修改 CMakeLists.txt 和 Launch 文件以默认加载此 RVIZ 配置:

install(DIRECTORY launch urdf config ...)
    defailt_rviz_config_path = os.path.join(urdf_package_path, 'config', 'display_roboy_model.rviz')
    action_rviz_node = launch_ros.actions.Node(
        package='rviz2', executable='rviz2',
        arguments=['-d', defailt_rviz_config_path],
    )

4. 使用 Xacro 简化 URDF

当机器人变复杂时,纯 URDF 会产生大量冗余。Xacro (XML Macros) 允许我们定义宏、使用变量,极大地提高了开发效率。

4.1 安装 Xacro
sudo apt install ros-$ROS_DISTRO-xacro

新建 src/fishbot_description/urdf/first_robot.xacro
(💡 VS Code 提示:点击右下角“纯文本”,搜索 XML 并将此文件关联为 XML 格式以获得高亮。)

<?xml version="1.0"?>
<robot xmlns:xacro="http://www.ros.org/wiki/xacro" name='first_robot'>
    <!-- 定义基础宏 -->
    <xacro:macro name="base_link" params="length radius">
        <link name='base_link'>
            <visual>
                <origin xyz="0.0 0.0 0.0" rpy="0.0 0.0 0.0"/>
                <geometry>
                    <cylinder radius="${radius}" length="${length}"/>
                </geometry>
                <material name="white">
                    <color rgba="1.0 1.0 1.0 0.5"/>
                </material>
            </visual>
        </link>
    </xacro:macro>

    <!-- 定义 IMU 宏,支持传参指定名字和位置 -->
    <xacro:macro name="imu_link" params="imu_name xyz">
        <link name='${imu_name}_link'>
            <visual>
                <origin xyz="0.0 0.0 0.0" rpy="0.0 0.0 0.0"/>
                <geometry>
                    <box size="0.02 0.02 0.02"/>
                </geometry>
                <material name="black">
                    <color rgba="0.0 0.0 0.0 0.5"/>
                </material>
            </visual>
        </link>

        <joint name='${imu_name}_joint' type='fixed'>
            <parent link='base_link'/>
            <child link='${imu_name}_link'/>
            <origin xyz="${xyz}" rpy="0.0 0.0 0.0"/>
        </joint>
    </xacro:macro>

    <!-- 调用宏生成组件 -->
    <xacro:base_link  length="0.12" radius="0.1"/>
    <xacro:imu_link  imu_name='imu_up' xyz="0.0 0.0 0.03"/>
    <xacro:imu_link  imu_name='imu_down' xyz="0.0 0.0 -0.03"/>

</robot>
4.2 修改 Launch 解析方式

由于模型变成了 .xacro 格式,Launch 文件中的解析命令需要从 cat 改为 xacro

Substitutions_command_result = launch.substitutions.Command(['xacro ', launch.substitutions.LaunchConfiguration('model')])

再次启动后效果如图:使用 Xacro 宏生成的带双 IMU 模块的机器人模型

5. 模块化构建机器人

利用 Xacro 的 include 标签,我们可以将底盘、传感器、执行器拆分到不同的文件中独立维护。

5.1 传感器部分定义

底座 (base.urdf.xacro):

<?xml version="1.0"?>
<robot xmlns:xacro="http://www.ros.org/wiki/xacro" name='first_robot'>
    <xacro:macro name="base_xacro" params="length radius">
        <!-- 机器人的身体部分 -->
        <link name='base_link'>
            <visual>
                <origin xyz="0.0 0.0 0.0" rpy="0.0 0.0 0.0"/>
                <geometry>
                    <cylinder radius="${radius}" length="${length}"/>
                </geometry>
                <material name="white">
                    <color rgba="1.0 1.0 1.0 0.5"/>
                </material>
            </visual>
        </link>
    </xacro:macro>
</robot>

IMU (/sensor/imu.urdf.xacro):

<?xml version="1.0"?>
<robot xmlns:xacro="http://www.ros.org/wiki/xacro" name='first_robot'>
    <xacro:macro name="imu_xacro" params="xyz">
        <link name='imu_link'>
            <visual>
                <geometry>
                    <box size="0.02 0.02 0.02"/>
                </geometry>
                <material name="black">
                    <color rgba="0.0 0.0 0.0 0.5"/>
                </material>
            </visual>
        </link>
        <joint name='imu_joint' type='fixed'>
            <parent link='base_link'/>
            <child link='imu_link'/>
            <origin xyz="${xyz}" rpy="0.0 0.0 0.0"/>
        </joint>
    </xacro:macro>
</robot>

相机 (/sensor/camera.urdf.xacro):

<?xml version="1.0"?>
<robot xmlns:xacro="http://www.ros.org/wiki/xacro" name='first_robot'>
    <xacro:macro name="camera_xacro" params="xyz">
        <link name='camera_link'>
            <visual>
                <geometry>
                    <box size="0.02 0.10 0.02"/>
                </geometry>
                <material name="black">
                    <color rgba="0.0 0.0 0.0 0.5"/>
                </material>
            </visual>
        </link>
        <joint name='camera_joint' type='fixed'>
            <parent link='base_link'/>
            <child link='camera_link'/>
            <origin xyz="${xyz}" rpy="0.0 0.0 0.0"/>
        </joint>
    </xacro:macro>
</robot>

雷达 (/sensor/laser.urdf.xacro):

<?xml version="1.0"?>
<robot xmlns:xacro="http://www.ros.org/wiki/xacro" name='first_robot'>
    <xacro:macro name="laser_xacro" params="xyz">
        <!-- 雷达支撑杆 -->
        <link name='laser_cylinder_link'>
            <visual>
                <geometry>
                    <cylinder radius="0.01" length="0.10"/>
                </geometry>
                <material name="black">
                    <color rgba="0.0 0.0 0.0 1.0"/>
                </material>
            </visual>
        </link>
        <!-- 雷达本体 -->
        <link name='laser_link'>
            <visual>
                <geometry>
                    <cylinder radius="0.02" length="0.02"/>
                </geometry>
                <material name="black">
                    <color rgba="0.0 0.0 0.0 1.0"/>
                </material>
            </visual>
        </link>

        <joint name='laser_joint' type='fixed'>
            <parent link='laser_cylinder_link'/>
            <child link='laser_link'/>
            <origin xyz="0.0 0.0 0.05" rpy="0.0 0.0 0.0"/>
        </joint>
        <joint name='laser_cylinder_joint' type='fixed'>
            <parent link='base_link'/>
            <child link='laser_cylinder_link'/>
            <origin xyz="${xyz}" rpy="0.0 0.0 0.0"/>
        </joint>
    </xacro:macro>
</robot>

汇总文件 (fishbot.urdf.xacro):

<?xml version="1.0"?>
<robot xmlns:xacro="http://www.ros.org/wiki/xacro" name='fishbot'>
<!-- 引入各模块定义 -->
<xacro:include filename="$(find fishbot_description)/urdf/fishbot/base.urdf.xacro"/>
<xacro:include filename="$(find fishbot_description)/urdf/fishbot/sensor/imu.urdf.xacro"/>
<xacro:include filename="$(find fishbot_description)/urdf/fishbot/sensor/camera.urdf.xacro"/>
<xacro:include filename="$(find fishbot_description)/urdf/fishbot/sensor/laser.urdf.xacro"/>

<!-- 实例化机器人组件 -->
<xacro:base_xacro length="0.12" radius="0.10" />
<xacro:imu_xacro xyz="0.0 0.0 0.02" />
<xacro:camera_xacro xyz="0.10 0.0 0.075" />
<xacro:laser_xacro xyz="0.0 0.0 0.10" />
</robot>

启动测试:ros2 launch fishbot_description display_robot.launch.py model:=.../fishbot.urdf.xacro模块化传感器组装过程

最终传感器组装效果如下:模块化组装后的机器人(包含雷达、摄像头及 IMU 支架)

5.2 执行器部件定义 (主动轮与万向轮)

驱动轮 (/actuator/wheel.urdf.xacro):
注意关节类型变为了 continuous(连续旋转)。

<?xml version="1.0"?>
<robot xmlns:xacro="http://www.ros.org/wiki/xacro" name='first_robot'>
    <xacro:macro name="wheel_xacro" params="wheel_name xyz">
        <link name='${wheel_name}_link'>
            <visual>
                <origin xyz="0.0 0.0 0.0" rpy="1.57079 0.0 0.0"/>
                <geometry>
                    <cylinder radius="0.032" length="0.04"/>
                </geometry>
                <material name="yellow">
                    <color rgba="1.0 1.0 0.0 0.8"/>
                </material>
            </visual>
        </link>

        <joint name='${wheel_name}_joint' type='continuous'>
            <parent link='base_link'/>
            <child link='${wheel_name}_link'/>
            <origin xyz="${xyz}" rpy="0.0 0.0 0.0"/>
            <axis xyz="0 1 0"/>
        </joint>
    </xacro:macro>
</robot>

万向支撑轮 (/actuator/caster.urdf.xacro):

<?xml version="1.0"?>
<robot xmlns:xacro="http://www.ros.org/wiki/xacro" name='first_robot'>
    <xacro:macro name="caster_xacro" params="caster_name xyz">
        <link name='${caster_name}_link'>
            <visual>
                <geometry>
                    <sphere radius="0.016"/>
                </geometry>
                <material name="yellow">
                    <color rgba="1.0 1.0 0.0 0.8"/>
                </material>
            </visual>
        </link>

        <joint name='${caster_name}_joint' type='fixed'>
            <parent link='base_link'/>
            <child link='${caster_name}_link'/>
            <origin xyz="${xyz}" rpy="0.0 0.0 0.0"/>
        </joint>
    </xacro:macro>
</robot>

更新汇总文件 (fishbot.urdf.xacro):

<!-- 增加执行器引入 -->
<xacro:include filename="$(find fishbot_description)/urdf/fishbot/actuator/wheel.urdf.xacro"/>
<xacro:include filename="$(find fishbot_description)/urdf/fishbot/actuator/caster.urdf.xacro"/>

<!-- 实例化执行器 -->
<xacro:wheel_xacro wheel_name="left_wheel" xyz="0 0.10 -0.06"/>
<xacro:wheel_xacro wheel_name="right_wheel" xyz="0 -0.10 -0.06"/>
<xacro:caster_xacro caster_name="front_caster" xyz="0.08 0.0 -0.076"/>
<xacro:caster_xacro caster_name="back_caster" xyz="-0.08 0.0 -0.076"/>

构建后运行,完整的机器人形态出现:执行器部件配置状态

最终机器人形态展示:具备完整执行器(动力轮与万向轮)的机器人最终形态

6. 虚拟部件:添加 base_footprint

在机器人底部添加一个极小的虚拟点投影到地面,方便导航算法进行计算。修改底座文件:

    <xacro:macro name="base_xacro" params="length radius">
        <!-- 虚拟触地点 -->
        <link name="base_footprint"/>
        
        <link name='base_link'>
            <!-- ... 原有视觉描述 ... -->
        </link>

        <joint name='joint_name' type='fixed'>
            <parent link='base_footprint'/>
            <child link='base_link'/>
            <origin xyz="0.0 0.0 ${length/2.0+0.032-0.001}" rpy="0.0 0.0 0.0"/>
        </joint>
    </xacro:macro>

添加了 base_footprint 地面投影后的坐标关系


三、 为机器人添加物理属性

如果只有视觉模型,机器人在物理仿真环境(如 Gazebo)中会像幽灵一样穿透物体,或者因为没有质量而无法受力。因此,我们需要为各个部件添加 碰撞 (Collision)惯性 (Inertial) 属性。

1. 添加碰撞属性 (Collision)

碰撞属性定义了机器人的物理边界。通常,碰撞形状可以比视觉形状简单(例如用一个简单的圆柱体代替复杂的车轮模型),以减少物理引擎的计算量。

<link> 标签内,增加 <collision> 节点。以 base.urdf.xacro 为例:

<?xml version="1.0"?>
<robot xmlns:xacro="http://www.ros.org/wiki/xacro" name='first_robot'>
    <xacro:macro name="base_xacro" params="length radius">
        <!-- 虚拟触地点 -->
        <link name="base_footprint"/>
        
        <!-- 机器人的身体部分 -->
        <link name='base_link'>
            <!-- 视觉描述 -->
            <visual>
                <origin xyz="0.0 0.0 0.0" rpy="0.0 0.0 0.0"/>
                <geometry>
                    <cylinder radius="${radius}" length="${length}"/>
                </geometry>
                <material name="white">
                    <color rgba="1.0 1.0 1.0 0.5"/>
                </material>
            </visual>

            <!-- 碰撞描述:通常与视觉几何形状一致 -->
            <collision>
                <origin xyz="0.0 0.0 0.0" rpy="0.0 0.0 0.0"/>
                <geometry>
                    <cylinder radius="${radius}" length="${length}"/>
                </geometry>
            </collision>
        </link>

        <joint name='joint_name' type='fixed'>
            <parent link='base_footprint'/>
            <child link='base_link'/>
            <origin xyz="0.0 0.0 ${length/2.0+0.032-0.001}" rpy="0.0 0.0 0.0"/>
        </joint>
    </xacro:macro>
</robot>

(提示:使用相同的方法为 sensor 和 actuator 下的其他 5 个 xacro 文件添加 <collision> 属性。)

构建后在 RVIZ 中,关闭左侧 RobotModel 下的 Visual Enabled,打开 Collision Enabled,即可看到碰撞模型:在 RVIZ 中开启的物理碰撞 (Collision) 模型预览

2. 添加质量与惯性矩阵 (Inertial)

惯性矩阵描述了物体在受到外力旋转时的阻力分布。计算这些矩阵公式繁琐,我们可以创建一个公共文件来封装标准的几何体惯性公式。

新建 common_inertial.xacro

<?xml version="1.0"?>
<robot xmlns:xacro="http://ros.org/wiki/xacro">
    <!-- 长方体惯性矩阵 -->
    <xacro:macro name="box_inertia" params="m w h d">
        <inertial>
            <mass value="${m}" />
            <inertia ixx="${(m/12) * (h*h + d*d)}" ixy="0.0" ixz="0.0" iyy="${(m/12) * (w*w + d*d)}" iyz="0.0" izz="${(m/12) * (w*w + h*h)}" />
        </inertial>
    </xacro:macro>

    <!-- 圆柱体惯性矩阵 -->
    <xacro:macro name="cylinder_inertia" params="m r h">
        <inertial>
            <mass value="${m}" />
            <inertia ixx="${(m/12) * (3*r*r + h*h)}" ixy="0" ixz="0" iyy="${(m/12) * (3*r*r + h*h)}" iyz="0" izz="${(m/2) * (r*r)}" />
        </inertial>
    </xacro:macro>

    <!-- 球体惯性矩阵 -->
    <xacro:macro name="sphere_inertia" params="m r">
        <inertial>
            <mass value="${m}" />
            <inertia ixx="${(2/5) * m * (r*r)}" ixy="0.0" ixz="0.0" iyy="${(2/5) * m * (r*r)}" iyz="0.0" izz="${(2/5) * m * (r*r)}" />
        </inertial>
    </xacro:macro>
</robot>

修改 base.urdf.xacro 引入惯性矩阵:

<?xml version="1.0"?>
<robot xmlns:xacro="http://www.ros.org/wiki/xacro" name='first_robot'>
    <!-- 引入公共惯性计算文件 -->
    <xacro:include filename="$(find fishbot_description)/urdf/fishbot/common_inertial.xacro"/>
    
    <xacro:macro name="base_xacro" params="length radius">
        <link name="base_footprint"/>
        
        <link name='base_link'>
            <!-- 视觉与碰撞部分省略... -->
            <visual> ... </visual>
            <collision> ... </collision>

            <!-- 赋予质量 1.0kg,并计算惯性矩阵 -->
            <xacro:cylinder_inertia m="1.0" r="${radius}" h="${length}"/>
        </link>
        <!-- joint 省略... -->
    </xacro:macro>
</robot>

在 RVIZ 中打开 Mass 或 Inertial 选项,可以看到重心的分布情况:机器人的质量中心 (Center of Mass) 及惯性分布

以同样方式为其他组件赋予质量:

  • IMU & 相机: <xacro:box_inertia m="0.05" w="0.02" h="0.02" d="0.02"/>
  • 雷达组件:
    <xacro:cylinder_inertia m="0.05" r="0.01" h="0.10"/>
    <xacro:cylinder_inertia m="0.10" r="0.02" h="0.02"/>
  • 驱动轮: <xacro:cylinder_inertia m="0.05" r="0.032" h="0.04"/>
  • 万向轮: <xacro:sphere_inertia m="0.05" r="0.016"/>

结果如图,机器人现在拥有了真实的物理属性:赋予完整物理属性后的机器人模型


四、 在 Gazebo 中完成仿真

有了碰撞和质量属性,机器人就可以被投放进真实的物理环境了。

1. 安装 Gazebo 与模型库

sudo apt install gazebo
# 下载 Gazebo 离线模型库,避免仿真启动时卡在下载界面
mkdir -p ~/.gazebo
cd ~/.gazebo/
git clone https://gitee.com/ohhuo/gazebo_models.git ~/.gazebo/models

# 安装 ROS2 与 Gazebo 交互的插件桥梁
sudo apt install ros-$ROS_DISTRO-gazebo-ros-pkgs

2. 在 Gazebo 中加载机器人

先在终端输入 gazebo 启动软件,随意摆放一些障碍物,然后将世界保存至 chapt6_ws/src/fishbot_description/world/custom_room.world

编写启动脚本 launch/gazebo_sim.launch.py,实现:启动 Gazebo -> 加载 World -> 将 URDF 模型注入环境中。

import launch
import launch_ros
from ament_index_python.packages import get_package_share_directory
import os

def generate_launch_description():
    urdf_package_path = get_package_share_directory('fishbot_description')
    default_xacro_path = os.path.join(urdf_package_path, 'urdf', 'fishbot/fishbot.urdf.xacro')
    defailt_gazebo_world_path = os.path.join(urdf_package_path, 'world', 'custom_room.world')

    action_declare_arg_mode_path = launch.actions.DeclareLaunchArgument(
        name='model', default_value=str(default_xacro_path)
    )
    
    # 编译 Xacro 到 URDF
    Substitutions_command_result = launch.substitutions.Command(['xacro ', launch.substitutions.LaunchConfiguration('model')])
    robot_description_value = launch_ros.parameter_descriptions.ParameterValue(Substitutions_command_result, value_type=str)

    # 启动 robot_state_publisher 发布 TF
    action_robot_state_publisher = launch_ros.actions.Node(
        package='robot_state_publisher',
        executable='robot_state_publisher',
        parameters=[{'robot_description': robot_description_value}] 
    )
    
    # 嵌套启动 Gazebo,并加载保存的 World
    action_launch_gazebo = launch.actions.IncludeLaunchDescription(
        launch.launch_description_sources.PythonLaunchDescriptionSource(
            [get_package_share_directory('gazebo_ros'), '/launch/gazebo.launch.py']
        ),
        launch_arguments=[('world', defailt_gazebo_world_path), ('verbose', 'true')]
    )

    # 通过 spawn_entity 脚本将 robot_description 注入 Gazebo 物理世界
    action_spawn_entitry = launch_ros.actions.Node(
        package='gazebo_ros',
        executable='spawn_entity.py',
        arguments=['-topic', '/robot_description', '-entity', 'fishbot']
    )

    return launch.LaunchDescription([
        action_declare_arg_mode_path, 
        action_robot_state_publisher,
        action_launch_gazebo,
        action_spawn_entitry,
    ])

(注意:记得在 CMakeLists.txt 中将 world 目录加入安装列表)
运行 Launch 后,可以看到机器人被加载到了 (0,0,0) 的位置:Gazebo 物理世界中成功加载并生成的虚拟机器人


3. Gazebo 标签扩展与环境配置

URDF 标准标签主要用于 RVIZ 显示,但 Gazebo 需要额外的 <gazebo> 标签来定义物理摩擦力和渲染材质。

通过 <gazebo reference="部件名"> 为特定部件增加物理参数。

为雷达涂上黑色

<gazebo reference="laser_cylinder_link">
    <material>Gazebo/Black</material>
</gazebo>
<gazebo reference="laser_link">
    <material>Gazebo/Black</material>
</gazebo>

设置车轮的摩擦力 (非常关键,否则机器人会打滑):

<!-- 驱动轮:高摩擦力提供抓地力 -->
<gazebo reference="${wheel_name}_link">
    <mu1 value="20.0" />
    <mu2 value="20.0" />
    <kp value="1000000000.0" />
    <kd value="1.0" />
</gazebo>

<!-- 万向轮:极低摩擦力,充当滑动支撑 -->
<gazebo reference="${caster_name}_link">
    <mu1 value="0.0" />
    <mu2 value="0.0" />
    <kp value="1000000000.0" />
    <kd value="1.0" />
</gazebo>

4.引入 ROS-Gazebo 交互插件

插件(Plugins)是 Gazebo 与 ROS2 之间通信的桥梁。如果没有插件,机器人在 Gazebo 中只是一个受重力影响的“死铁块”,无法接收控制指令,也无法向外发送传感器数据。

新建 fishbot/plugins/gazebo_control_plugin.xacro。此插件会订阅 /cmd_vel 话题以控制车轮转动,同时发布机器人的 /odom 里程计数据和 TF 变换。

<?xml version="1.0"?>
<robot xmlns:xacro="http://www.ros.org/wiki/xacro">
    <xacro:macro name="gazebo_control_plugin">
        <gazebo>
            <!-- 引入两轮差速控制器插件 -->
            <plugin name='diff_drive' filename='libgazebo_ros_diff_drive.so'>
                <ros>
                    <!-- 命名空间与话题重映射 -->
                    <namespace>/</namespace>
                    <remapping>cmd_vel:=cmd_vel</remapping>
                    <remapping>odom:=odom</remapping>
                </ros>
                <!-- 插件更新频率 (Hz) -->
                <update_rate>30</update_rate>
                
                <!-- 绑定我们在 URDF 中定义的左右驱动轮关节 -->
                <left_joint>left_wheel_joint</left_joint>
                <right_joint>right_wheel_joint</right_joint>
                
                <!-- 机器人运动学参数:轮距和轮径 (单位:米) -->
                <wheel_separation>0.2</wheel_separation>
                <wheel_diameter>0.064</wheel_diameter>
                
                <!-- 物理限制:最大扭矩与最大加速度 -->
                <max_wheel_torque>20</max_wheel_torque>
                <max_wheel_acceleration>1.0</max_wheel_acceleration>
                
                <!-- 开启各种状态和坐标变换的输出 -->
                <publish_odom>true</publish_odom>
                <publish_odom_tf>true</publish_odom_tf>
                <publish_wheel_tf>true</publish_wheel_tf>

                <!-- 指定里程计坐标系和机器人基座坐标系 -->
                <odometry_frame>odom</odometry_frame>
                <robot_base_frame>base_footprint</robot_base_frame>
            </plugin>
        </gazebo>
   </xacro:macro>
</robot>

5. 激光雷达与 IMU 传感器插件

由于雷达和 IMU 都属于传感器,我们把它们的代码整合在一起。新建 fishbot/plugins/gazebo_sensor_plugin.xacro。我们将通过 Gazebo 的射线(ray)和 IMU 仿真器,虚拟出真实的传感器物理特性,并添加高斯白噪声。

<?xml version="1.0"?>
<robot xmlns:xacro="http://www.ros.org/wiki/xacro">
    <xacro:macro name="gazebo_sensor_plugin">
        
        <!-- ================== 激光雷达 (LiDAR) 配置 ================== -->
        <gazebo reference="laser_link">
            <sensor name="laserscan" type="ray">
                <plugin name="laserscan" filename="libgazebo_ros_ray_sensor.so">
                    <ros>
                        <namespace>/</namespace>
                        <remapping>~/out:=scan</remapping> <!-- 发布到 /scan 话题 -->
                    </ros>
                    <output_type>sensor_msgs/LaserScan</output_type>
                    <frame_name>laser_link</frame_name>
                </plugin>
                <always_on>true</always_on>
                <visualize>true</visualize> <!-- 在 Gazebo 中可视化出红色射线 -->
                <update_rate>5</update_rate>
                <pose>0 0 0 0 0 0</pose>
                 <!-- 激光传感器扫描参数配置 -->
                <ray>
                    <!-- 设置扫描范围 -->
                    <scan>
                        <horizontal>
                            <samples>360</samples>        <!-- 一圈采 360 个点 -->
                            <resolution>1.000000</resolution>
                            <min_angle>0.000000</min_angle>
                            <max_angle>6.280000</max_angle> <!-- 0 到 2π -->
                        </horizontal>
                    </scan>
                    <!-- 设置扫描距离 -->
                    <range>
                        <min>0.120000</min>               <!-- 最近探测 12cm -->
                        <max>8.0</max>                    <!-- 最远探测 8m -->
                        <resolution>0.015000</resolution>
                    </range>
                    <!-- 高斯噪声注入,模拟真实的传感器测距误差 -->
                    <noise>
                        <type>gaussian</type>
                        <mean>0.0</mean>
                        <stddev>0.01</stddev>
                    </noise>
                </ray>
            </sensor>
        </gazebo>

        <!-- ================== 惯性测量单元 (IMU) 配置 ================== -->
        <gazebo reference="imu_link">
            <sensor name="imu_sensor" type="imu">
                <plugin name="imu_plugin" filename="libgazebo_ros_imu_sensor.so">
                    <ros>
                        <namespace>/</namespace>
                        <remapping>~/out:=imu</remapping> <!-- 发布到 /imu 话题 -->
                    </ros>
                    <initial_orientation_as_reference>false</initial_orientation_as_reference>
                </plugin>
                <update_rate>100</update_rate> <!-- IMU 通常需要高频发布 -->
                <always_on>true</always_on>
                <!-- 复杂的六轴噪声设置,模拟真实的零偏和游走误差 -->
                <imu>
                    <angular_velocity>
                        <x><noise type="gaussian"><mean>0.0</mean><stddev>2e-4</stddev><bias_mean>0.0000075</bias_mean><bias_stddev>0.0000008</bias_stddev></noise></x>
                        <y><noise type="gaussian"><mean>0.0</mean><stddev>2e-4</stddev><bias_mean>0.0000075</bias_mean><bias_stddev>0.0000008</bias_stddev></noise></y>
                        <z><noise type="gaussian"><mean>0.0</mean><stddev>2e-4</stddev><bias_mean>0.0000075</bias_mean><bias_stddev>0.0000008</bias_stddev></noise></z>
                    </angular_velocity>
                    <linear_acceleration>
                        <x><noise type="gaussian"><mean>0.0</mean><stddev>1.7e-2</stddev><bias_mean>0.1</bias_mean><bias_stddev>0.001</bias_stddev></noise></x>
                        <y><noise type="gaussian"><mean>0.0</mean><stddev>1.7e-2</stddev><bias_mean>0.1</bias_mean><bias_stddev>0.001</bias_stddev></noise></y>
                        <z><noise type="gaussian"><mean>0.0</mean><stddev>1.7e-2</stddev><bias_mean>0.1</bias_mean><bias_stddev>0.001</bias_stddev></noise></z>
                    </linear_acceleration>
                </imu>
            </sensor>
        </gazebo>

6. 深度相机传感器仿真与坐标系变换

深度相机(如 RGB-D 相机)在仿真中需要额外的光学坐标系定义。因为图像的坐标系与 ROS 标准坐标系(前方为 x)不同,图像坐标系通常以右侧为 x,下方为 y,前方为 z

(fishbot/sensor/camera.urdf.xacro)
我们需要在原有的 camera_link 基础上增加一个虚拟的 camera_optical_link 并进行旋转翻转:

        <link name='camera_optical_link'></link>
···
        <!-- 通过翻转将其转换为标准的图像坐标系 (绕 Z 转 -90 度,绕 X 转 -90 度) -->
        <joint name='camera_optical_joint' type='fixed'>
            <parent link='camera_link'/>
            <child link='camera_optical_link'/>
            <origin xyz="0 0 0" rpy="${-pi/2} 0.0 ${-pi/2}"/>
        </joint>

在刚才的 fishbot/plugins/gazebo_sensor_plugin.xacro 文件末尾继续追加相机配置:

        <!-- ================== 深度相机 (RGB-D) 配置 ================== -->
        <gazebo reference="camera_link">
            <sensor type="depth" name="camera_sensor">
                <!-- 引入相机插件 -->
                <plugin name="depth_camera" filename="libgazebo_ros_camera.so">
                    <frame_name>camera_optical_link</frame_name> <!-- 核心:绑定到翻转后的光学坐标系 -->
                </plugin>
                <always_on>true</always_on>
                <update_rate>10</update_rate>
                <camera name="camera">
                    <horizontal_fov>1.5009831567</horizontal_fov> <!-- 视场角 -->
                    <image>
                        <!-- 图像分辨率与格式 -->
                        <width>800</width>
                        <height>600</height>
                        <format>R8G8B8</format>
                    </image>
                    <!-- 畸变参数配置(目前设为 0,代表理想相机) -->
                    <distortion>
                        <k1>0.0</k1>
                        <k2>0.0</k2>
                        <k3>0.0</k3>
                        <p1>0.0</p1>
                        <p2>0.0</p2>
                        <center>0.5 0.5</center>
                    </distortion>
                </camera>
            </sensor>
        </gazebo>
    </xacro:macro>
</robot>

将插件汇总至机器人的主 Xacro 文件
最后,不要忘记在总入口文件中引入我们写好的所有插件。

修改 fishbot/fishbot.urdf.xacro

<!-- 引入插件宏定义文件 -->
<xacro:include filename="$(find fishbot_description)/urdf/fishbot/plugins/gazebo_control_plugin.xacro"/>
<xacro:include filename="$(find fishbot_description)/urdf/fishbot/plugins/gazebo_sensor_plugin.xacro"/>

·····
<!-- 实例化仿真插件 -->
<xacro:gazebo_control_plugin />
<xacro:gazebo_sensor_plugin />

7. 编译与可视化测试

至此,我们的 FishBot 已经在物理引擎中有了形体,也具备了感知世界的“眼睛”和“触觉”。

  1. 编译工作空间
    colcon build
    source install/setup.bash
    
  2. 启动 Gazebo 仿真环境
    ros2 launch fishbot_description display_robot.launch.py model:=/home/balrog-v/ros2_ws/chapt6_ws/install/fishbot_description/share/fishbot_description/urdf/fishbot/fishbot.urdf.xacro
    
  3. 使用 RQT 查看传感器图像
    新开一个终端输入 rqt。在上方菜单栏选择 Plugins -> Visualization -> Image View
    开启两个 Image View 窗口,分别订阅 /camera/image_raw(RGB 原图)和 /camera/depth/image_raw(深度图),你将看到如下令人激动的机器视觉画面!

rqt选项

RGB&深度图像

通过这些插件,你的虚拟机器人不仅能在 Gazebo 里接收 /cmd_vel 话题跑起来,还能将它看到的虚拟世界源源不断地回传给你,为后续的 SLAM 建图和视觉导航打下坚实的基础。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐