2. 仿真环境搭建:Gazebo与Stage仿真器选型,世界模型与机器人模型加载

说到仿真环境搭建,我见过不少新手一上来就纠结「到底用哪个仿真器」。其实这事没那么玄乎。Gazebo 和 Stage,说白了就是两个不同量级的工具。你想想看,一个是要做高保真物理模拟,一个是要快速验证算法逻辑,它们压根就不是替代关系。

我个人习惯是这么分的:如果你的导航算法需要跟真实传感器特性、动力学模型死磕,那就上 Gazebo;如果你只是想快速调通路径规划、行为状态机这些逻辑层,Stage 反而更顺手。我在项目中遇到过好几次,团队一上来就上 Gazebo,结果光调模型参数就花了两周,最后发现核心算法逻辑还没跑通。嗯,这里要注意,选仿真器之前,先想清楚你要验证什么。

2.1 Gazebo vs Stage:核心差异对比

先看一张对比表,这样你心里就有数了。

对比维度 Gazebo Stage
物理引擎 ODE / Bullet / DART 等,支持碰撞、摩擦、惯性 无物理引擎,仅做位姿更新
传感器仿真 激光雷达、IMU、相机、深度相机等,带噪声模型 仅支持简化的激光测距和声纳
渲染能力 OGRE 3D 渲染,支持光照、纹理、阴影 2D 平面渲染,像素风格
计算开销 高,需要 GPU 加速效果更好 极低,单核 CPU 就能跑
适用场景 传感器选型、动力学验证、SLAM 闭环测试 路径规划、行为决策、多机器人调度
启动速度 30秒~数分钟(取决于模型复杂度) 1~3秒

看到这里你可能会问:「那我到底该选哪个?」我的建议是——两个都要用。我在做导航全链路联调时,通常先用 Stage 把规划、控制、行为树这些逻辑层跑通,确认没有逻辑 bug 之后,再切到 Gazebo 做传感器噪声、动力学响应层面的验证。这样既快又稳。

核心原则:逻辑验证用 Stage,物理验证用 Gazebo。不要试图用一个仿真器解决所有问题。

2.2 世界模型加载:从空白地图到结构化环境

世界模型,说白了就是机器人要面对的那个「世界」。在仿真里,它通常是一个描述环境几何、障碍物、地面属性的文件。

Gazebo 的世界模型是 .world 文件,里面用 SDF 或 URDF 格式描述环境。Stage 的世界模型是 .world 文件(没错,同名但格式不同),用 Stage 自己的 .inc 文件描述。

我刚开始用 Gazebo 时,踩过一个坑:直接加载官方提供的 empty.world,结果机器人一启动就往下掉。后来才发现,empty.world 默认没有地面。嗯,这里要注意,一定要显式添加地面模型。

下面是一个典型的 Gazebo 世界模型文件结构:

<?xml version="1.0" ?>
<sdf version="1.6">
  <world name="navigation_world">
    <include>
      <uri>model://sun</uri>
    </include>
    <include>
      <uri>model://ground_plane</uri>
    </include>
    <include>
      <uri>model://obstacle_1</uri>
      <name>wall_01</name>
      <pose>2.0 0.0 0.0 0 0 0</pose>
    </include>
    <include>
      <uri>model://obstacle_2</uri>
      <name>pillar_01</name>
      <pose>-1.5 2.0 0.0 0 0 0</pose>
    </include>
  </world>
</sdf>

Stage 的世界模型更轻量,一个简单的 .world 文件长这样:

define floorplan model
(
  color "gray"
  boundary 1
  gui_nose 0
  gui_grid 0
  gui_outline 0
  gripper_return 0
  fiducial_return 0
  laser_return 1
)

define robot position
(
  size [0.4 0.4 0.3]
  origin [0 0 0 0]
  gui_nose 1
  drive "diff"
)

# 加载地图
floorplan
(
  name "office_floor"
  bitmap "office_map.png"
  size [20.0 20.0 1.0]
  pose [0 0 0 0]
)

# 加载机器人
robot
(
  name "robot1"
  pose [0.0 0.0 0.0]
)

小技巧:Stage 的 bitmap 支持 PNG 格式的栅格地图。你可以用任何图像编辑软件画一张黑白图,黑色代表障碍物,白色代表可通行区域。我经常用这个方式快速搭建测试场景,比 Gazebo 一个个摆模型快多了。

2.3 机器人模型加载:URDF 与 SDF 的抉择

机器人模型怎么加载?这又是一个让新手头疼的问题。Gazebo 支持两种格式:URDF 和 SDF。Stage 则用自己定义的 position 或 differential 模型描述。

我个人更推荐在 Gazebo 里用 SDF 格式。为什么?因为 SDF 是 Gazebo 的原生格式,支持更丰富的传感器标签、物理参数和插件系统。URDF 虽然也能用,但经常需要额外写 .gazebo 扩展文件来补充 Gazebo 特有的配置。

我曾经在一个项目里用 URDF 加载机器人,结果激光雷达的噪声参数怎么都调不生效。折腾了两天,最后换成 SDF 格式,十分钟搞定。说白了,原生格式永远是最省心的。

下面是一个简单的 SDF 机器人模型示例:

<?xml version="1.0" ?>
<sdf version="1.6">
  <model name="my_robot">
    <link name="base_link">
      <pose>0 0 0.1 0 0 0</pose>
      <inertial>
        <mass>10.0</mass>
        <inertia>
          <ixx>0.1</ixx>
          <iyy>0.1</iyy>
          <izz>0.1</izz>
        </inertia>
      </inertial>
      <collision name="collision">
        <geometry>
          <cylinder>
            <radius>0.2</radius>
            <length>0.2</length>
          </cylinder>
        </geometry>
      </collision>
      <visual name="visual">
        <geometry>
          <cylinder>
            <radius>0.2</radius>
            <length>0.2</length>
          </cylinder>
        </geometry>
      </visual>
    </link>
    <plugin name="diff_drive" filename="libDiffDrivePlugin.so">
      <left_joint>left_wheel_joint</left_joint>
      <right_joint>right_wheel_joint</right_joint>
      <wheel_separation>0.4</wheel_separation>
      <wheel_diameter>0.1</wheel_diameter>
    </plugin>
  </model>
</sdf>

注意:Stage 不支持 SDF/URDF 格式。如果你要在 Stage 里用自定义机器人,必须用 Stage 自己的模型描述语法。不过 Stage 的模型描述极其简单,通常十几行就能搞定一个差速驱动机器人。

2.4 仿真器选型决策流程

说了这么多,我画了一张决策流程图,帮你快速判断该用哪个仿真器。

仿真器选型决策流程 仿真器选型决策流程 开始选型 主要验证什么? 逻辑验证(规划/行为) 物理验证(传感器/动力学) 选用 Stage 选用 Gazebo 建议:先用 Stage 验证逻辑,再用 Gazebo 验证物理 提示:如果项目周期紧,优先保证逻辑验证;物理验证可以后续补上

2.5 加载实战:一条命令启动仿真

理论说完了,咱们来点实际的。假设你已经装好了 ROS 和 Gazebo,加载一个带机器人的世界模型只需要一条命令:

# 启动 Gazebo 并加载世界模型和机器人
roslaunch my_navigation_package navigation_world.launch

这个 launch 文件的内容大致如下:

<launch>
  <!-- 启动 Gazebo 世界 -->
  <include file="$(find gazebo_ros)/launch/empty_world.launch">
    <arg name="world_name" value="$(find my_navigation_package)/worlds/navigation_world.world"/>
    <arg name="paused" value="false"/>
    <arg name="use_sim_time" value="true"/>
    <arg name="gui" value="true"/>
  </include>

  <!-- 加载机器人模型到 Gazebo -->
  <node name="spawn_robot" pkg="gazebo_ros" type="spawn_model" 
        args="-file $(find my_navigation_package)/models/my_robot.sdf 
              -sdf -model my_robot -x 0 -y 0 -z 0.1"/>
</launch>

Stage 的启动更简单:

# 启动 Stage 仿真
rosrun stage_ros stageros $(find my_navigation_package)/worlds/office.world

个人经验:我习惯在 launch 文件里加一个 paused 参数。调试阶段设为 true,这样 Gazebo 启动后不会立刻开始物理模拟,我可以先检查模型位置、传感器配置是否正确。确认无误后,再手动点击 Play 按钮开始仿真。这个小习惯帮我避免了很多「模型飞出去」的尴尬。

2.6 避坑指南:我踩过的三个坑

最后分享几个我实际踩过的坑,希望能帮你省点时间。

  • 坑一:模型坐标对不上 —— 我曾经在 Gazebo 里加载机器人,结果它直接穿模到地板下面。后来发现是 .world 文件里地面的 pose 设成了 0 0 0,而机器人的 z 坐标也是 0。解决办法:地面 z 保持 0,机器人 z 设为 底盘高度/2。
  • 坑二:Stage 地图像素和实际尺寸不匹配 —— Stage 的 bitmap 加载地图时,size 参数的单位是米。如果你画了一张 100x100 像素的 PNG,但 size 设成了 [10 10 1],那每个像素就代表 0.1 米。我一开始没注意这个比例,结果机器人在地图里走两步就撞墙。
  • 坑三:Gazebo 插件不生效 —— 差速驱动插件加载后,机器人就是不动。排查了半天,发现是 left_joint 和 right_joint 的名字写错了,跟 URDF/SDF 里定义的关节名不一致。嗯,这种低级错误最容易让人抓狂。