第 9 章 Gazebo 仿真¶
Gazebo(现名 gz sim)是 ROS 生态中集成度最高的物理仿真器——原生支持 SDF 模型描述、插件化传感器模拟、以及与 ROS 2 的深度桥接。在仿真中检验控制算法与感知链路,再迁移到真机,是机器人开发的标准工作流。本章从 gz sim 的版本配对与安装讲起,经 SDF 建模、ros_gz 桥接、传感器仿真与 gz_ros2_control 通路,到与 MuJoCo 的选型对比,给出在 Humble 环境下把仿真回路搭起来的实操路径。
1. gz sim 基线与版本配对¶
1.1 谱系与命名¶
Gazebo 项目经历了分裂与更名:
| 名称 | 标识 | 状态 |
|---|---|---|
Gazebo Classic(gazebo) |
11.x 及以前 | 已 EOL(2025-01),不再维护 |
| gz sim(新架构) | 作为 gz-<codename> 发行 |
活跃开发 |
新版采用代号命名:gz-fortress(LTS,与 Humble 配对)、gz-garden(与 Iron 配对)、gz-harmonic(与 Jazzy 配对)。Humble 仓库提供 gz-fortress 元包,其 libgz-transport11/libgz-msgs8 即 Fortress 代的 gz 库(早期包名沿用 libignition-transport11/libignition-msgs8,两者同代)。具体小版本配对以 Gazebo 配对表为准。
1.2 安装与启动¶
## Humble 下的 gz sim 本体 + ros_gz 桥接(ros-humble-ros-gz 0.244.26,含 sim/bridge/image 子包)
sudo apt install -y gz-fortress ros-humble-ros-gz
gz-fortress 是过渡元包,实体会拉进 libgz-sim6/libignition-gazebo6(Fortress 对应 gz-sim 6.x)。启动命令有个同名陷阱:若系统同时装了已 EOL 的 Gazebo Classic 11(gazebo 包),/usr/bin/gz 会被其占成 gz-11.x 软链,gz sim 会报“Invalid arguments”;Fortress 的命令行入口是 ign gazebo(ros_gz_sim 的 launch 对 version<7 也自动选 ign gazebo)。无头启动示例:
ign gazebo -s empty.sdf # -s 仅服务端(无 GUI);-v4 输出详细日志
gz sim -r # 未装 Classic 时,Fortress 的 `gz sim` 亦可
无头 ign gazebo -s -v2 <ignition-gazebo6>/worlds/empty.sdf 会加载世界、启用 DART 物理,发布 gz transport 话题 /clock、/stats、/world/empty/*(物理默认 1ms profile)。
1.3 物理引擎¶
gz sim 支持可插拔物理引擎:默认 DART(面向关节体),可选 Bullet、TPE(轻量测试引擎)。切换在 SDF 的 <physics> 标签或环境变量 GZ_SIM_PHYSICS_ENGINE_LIBRARY 完成。DART 对关节体(机械臂、底盘)仿真质量稳定;TPE 适合简单场景但不保证接触精度。
坑:gz 命令可能指向 Classic
Classic 11 与现代 gz sim 共存时 /usr/bin/gz 指向 Classic,gz sim 无效;Fortress 的入口是 ign gazebo,ros_gz 的 launch 对旧版本会自动选它。
2. SDF 与建模链¶
2.1 模型与世界文件¶
SDF(Simulation Description Format)是 gz sim 的原生模型格式,描述比 URDF 更完整——包含物理属性、传感器、插件与多模型场景:
<?xml version="1.0" ?>
<sdf version="1.8">
<model name="diff_drive_bot">
<pose>0 0 0.1 0 0 0</pose> <!-- 初始位姿 x y z roll pitch yaw -->
<link name="base_link">
<inertial>
<mass>2.0</mass>
<inertia><ixx>0.01</ixx><iyy>0.02</iyy><izz>0.03</izz></inertia>
</inertial>
<collision name="base_collision">
<geometry><box><size>0.4 0.3 0.1</size></box></geometry>
</collision>
<visual name="base_visual">
<geometry><box><size>0.4 0.3 0.1</size></box></geometry>
</visual>
</link>
<link name="wheel_left">
<inertial>
<mass>0.1</mass>
<inertia><ixx>0.0001</ixx><iyy>0.0001</iyy><izz>0.0001</izz></inertia>
</inertial>
<collision name="wheel_collision">
<geometry><cylinder><radius>0.05</radius><length>0.03</length></cylinder></geometry>
<surface><friction><ode><mu>1.0</mu><mu2>1.0</mu2></ode></friction></surface>
</collision>
</link>
<joint name="wheel_left_joint" type="revolute">
<parent>base_link</parent>
<child>wheel_left</child>
<pose relative_to="base_link">0 0.18 -0.05 0 0 0</pose>
<axis>
<xyz>0 1 0</xyz>
<limit><effort>10</effort><velocity>5</velocity></limit>
</axis>
</joint>
<!-- 右轮:与左轮对称(y 取反),DiffDrive 插件需两个关节都在 -->
<link name="wheel_right">
<inertial>
<mass>0.1</mass>
<inertia><ixx>0.0001</ixx><iyy>0.0001</iyy><izz>0.0001</izz></inertia>
</inertial>
<collision name="wheel_right_collision">
<geometry><cylinder><radius>0.05</radius><length>0.03</length></cylinder></geometry>
<surface><friction><ode><mu>1.0</mu><mu2>1.0</mu2></ode></friction></surface>
</collision>
</link>
<joint name="wheel_right_joint" type="revolute">
<parent>base_link</parent>
<child>wheel_right</child>
<pose relative_to="base_link">0 -0.18 -0.05 0 0 0</pose>
<axis>
<xyz>0 1 0</xyz>
<limit><effort>10</effort><velocity>5</velocity></limit>
</axis>
</joint>
<!-- 差速驱动插件 -->
<plugin filename="gz-sim-diff-drive-system" name="gz::sim::systems::DiffDrive">
<left_joint>wheel_left_joint</left_joint>
<right_joint>wheel_right_joint</right_joint>
<wheel_separation>0.36</wheel_separation>
<wheel_radius>0.05</wheel_radius>
<odom_publish_frequency>50</odom_publish_frequency>
</plugin>
<!-- 激光雷达传感器 -->
<link name="laser_link">
<sensor name="laser" type="gpu_lidar">
<pose>0.1 0 0.15 0 0 0</pose>
<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.12</min><max>12.0</max></range>
</lidar>
</sensor>
</link>
</model>
</sdf>
world 是最外层的容器,可包含多个 model、光照、地形与全局物理参数。步长与实时倍速写在 <physics> 里,被包含的模型按 uri 引用,同一份模型可被多个世界复用:
<sdf version="1.8">
<world name="my_world">
<physics name="1ms" type="ode">
<max_step_size>0.001</max_step_size> <!-- 仿真步长 1ms -->
<real_time_factor>1.0</real_time_factor> <!-- 实时倍速 -->
</physics>
<!-- 引用模型库中的模型;name 可覆盖模型自身名字,避免多份实例重名 -->
<include>
<uri>model://diff_drive_bot</uri>
<name>diff_drive_bot</name>
</include>
</world>
</sdf>
2.2 URDF/xacro → SDF 转换¶
已有 URDF 模型时通过 sdformat 工具转换:
## gz 命令行转换(形态以 gz 文档为准)
gz model --convert my_car.urdf -o my_car.sdf
gz sdf -k my_car.sdf # 检查 SDF 文件语法
URDF → SDF 的已知限制:URDF 中的 <transmission> 和 <gazebo> 扩展标签不被直接转换——ros2_control 的 <ros2_control> 标签需要通过 gz_ros2_control 插件在 SDF 中手动添加。
3. ros_gz 桥¶
ros_gz 包组是 gz sim 与 ROS 2 之间的消息桥接层,由三个子包组成:
| 子包 | 职责 |
|---|---|
ros_gz_sim |
启动 gz sim、spawn 模型到世界、提供 /clock 发布 |
ros_gz_bridge |
通用双向消息桥(gz transport ↔ ROS 2 topic) |
ros_gz_image |
图像专用桥(高效处理 sensor_msgs/Image ↔ gz::msgs::Image) |
桥接的拓扑关系:仿真侧与 ROS 侧各跑各的图,ros_gz 在中间做双向消息翻译,图像与时钟各有专用通路:
flowchart LR
subgraph gz["gz sim(Fortress)"]
world["世界 / 物理 / 传感器"]
end
world -->|"gz transport"| br["ros_gz_bridge<br/>gz ↔ ROS 2 双向"]
world -->|"gz 图像"| img["ros_gz_image<br/>图像专用桥"]
world -.->|"gz.msgs.Clock"| br
br -->|"/clock、/cmd_vel、/scan…"| ros["ROS 2 节点<br/>(use_sim_time)"]
img --> ros
3.1 启动 gz sim 并接入 ROS 2¶
## gz_sim.launch.py 关键片段
from launch_ros.actions import Node
gazebo = Node(
package='ros_gz_sim',
executable='create', # 向运行中的 gz sim 注入模型
arguments=['-name', 'diff_drive_bot', '-topic', 'robot_description',
'-x', '0', '-y', '0', '-z', '0.1'],
output='screen',
)
bridge = Node(
package='ros_gz_bridge',
executable='parameter_bridge',
parameters=[{
'config_file': os.path.join(pkg_share, 'gz_bridge.yaml'),
}],
)
3.2 消息映射配置¶
gz_bridge.yaml 定义 gz topic ↔ ROS topic 的双向映射:
完整 gz_bridge.yaml(点击展开)
## gz_bridge.yaml
- ros_topic_name: "/clock"
gz_topic_name: "/clock"
ros_type_name: "rosgraph_msgs/msg/Clock"
gz_type_name: "gz.msgs.Clock"
direction: GZ_TO_ROS
- ros_topic_name: "/scan"
gz_topic_name: "/world/my_world/model/diff_drive_bot/link/laser_link/sensor/laser/scan"
ros_type_name: "sensor_msgs/msg/LaserScan"
gz_type_name: "gz.msgs.LaserScan"
direction: GZ_TO_ROS
- ros_topic_name: "/cmd_vel"
gz_topic_name: "/model/diff_drive_bot/cmd_vel"
ros_type_name: "geometry_msgs/msg/Twist"
gz_type_name: "gz.msgs.Twist"
direction: ROS_TO_GZ
- ros_topic_name: "/odom"
gz_topic_name: "/model/diff_drive_bot/odometry"
ros_type_name: "nav_msgs/msg/Odometry"
gz_type_name: "gz.msgs.Odometry"
direction: GZ_TO_ROS
3.3 差速小车最小世界¶
完整步骤——从零到 /cmd_vel 控制仿真小车运动:
# 1. 启动 gz sim(ros_gz_sim 自带 gz_sim.launch.py)
ros2 launch ros_gz_sim gz_sim.launch.py gz_args:="-r my_world.sdf"
# 2. 模型注入 + 桥接(my_bringup 为自写 launch,内容见 3.1)
ros2 launch my_bringup gz_sim.launch.py
# 3. 检查桥接
ros2 topic list # 应出现 /scan /odom /cmd_vel /clock
ros2 topic hz /scan # 10 Hz 雷达数据
ros2 topic hz /odom # 50 Hz 里程计
# 4. 发布速度命令
ros2 topic pub -r 10 /cmd_vel geometry_msgs/msg/Twist \
"{linear: {x: 0.5}, angular: {z: 0.3}}"
# 5. RViz2 观察(需 TF:robot_state_publisher 发布 base_link → laser_link)
rviz2 --fixed-frame odom
4. 仿真时间与传感器¶
4.1 /clock 与 use_sim_time¶
gz sim 在每个仿真步发布 gz.msgs.Clock → ros_gz_bridge 转为 rosgraph_msgs/msg/Clock 发布到 ROS 话题 /clock。所有设置 use_sim_time := true 的节点从 /clock 获取时间戳,而非系统时钟。
一行桥接即可把 gz 的 /clock 转到 ROS 侧(无头 ign gazebo -s empty.sdf 起世界后):
ros2 run ros_gz_bridge parameter_bridge "/clock@rosgraph_msgs/msg/Clock[gz.msgs.Clock"
## ROS 侧 `ros2 topic hz /clock` 得到百赫兹量级(无头服务端快于实时);`[` 表 gz→ROS。Humble 的 ros_gz 对 `gz.msgs.*` 与 `ignition.msgs.*` 两名都接受。
需注意:纯 -s 无头服务端下,自带 diff_drive.sdf 的 odometry 发布器不逐帧产出数据(需 GUI 或外部步进);要跑 cmd_vel→odom 的驱动闭环,需在带 GUI 或 --run 的交互环境下进行。
## 全链路使用仿真时间(launch 注入)
ros2 launch my_bringup gz_sim.launch.py use_sim_time:=true
时间冻结问题:gz sim 暂停时 /clock 停止推进,依赖时间的 TF 查询超时、SLAM 卡住。解法:gz sim 必须处于 running 状态(GUI 中点播放或命令行 -r)。
4.2 real_time_factor 与步长¶
<real_time_factor> 控制仿真倍速:1.0 为实时,0.5 为半速(仿真时间比墙钟慢),>1.0 为加速(但受 CPU 算力限制达不到标称倍速)。<max_step_size> 决定数值积分精度——越小越精确但计算越慢。差速底盘建议步长 1ms、倍速 1.0;高频力控场景可能需要 0.5ms 步长。
4.3 传感器仿真对照真机¶
| 传感器 | gz sim 插件/模型 | 输出话题 | 与真机驱动的差异 |
|---|---|---|---|
| 相机 | gz-sim-camera-system |
/camera/image_raw + /camera_info |
真机内参由标定得出;仿真内参写在 SDF 中 |
| 2D 激光雷达 | gz-sim-lidar-system (ray) |
/scan |
仿真无杂点与反射率缺失;真机有盲区与光强衰减 |
| 3D LiDAR | gpu_lidar 多线模型 |
/points2 |
仿真点云无运动畸变;真机有(扫描期间底盘移动) |
| IMU | gz-sim-imu-system |
/imu/data |
仿真无零偏漂移(或需手动配噪声模型);真机有 |
| GPS | gz-sim-navsat-system |
/fix (sensor_msgs/msg/NavSatFix) |
仿真精度远高于真机多路径误差(真机侧接入与融合见第 7 章「GNSS 接收机接入」、第 10 章「GNSS/RTK 观测与质量分档」) |
仿真传感器话题名与真机驱动话题名不一致时,通过 remap 统一接口——下游算法不需要改动。
判据:全链路统一 use_sim_time
仿真里凡是对时间敏感的节点(TF、规划、状态估计)都要吃 /clock;漏一个节点就会出现“只差一拍”的诡异失配,且只在那一个节点上体现。
5. gz_ros2_control 与 MuJoCo 桥接选型¶
5.1 gz_ros2_control¶
gz_ros2_control 让 gz sim 中的模型使用 ros2_control 框架的控制器——与真机共用同一套 controller_manager + hardware_interface + controller 配置(Humble 包 ros-humble-gz-ros2-control):
<!-- SDF 中注入 gz_ros2_control 插件 -->
<plugin filename="gz_ros2_control-system"
name="gz_ros2_control::GazeboSimROS2ControlPlugin">
<parameters>$(find my_bringup)/config/controllers.yaml</parameters>
</plugin>
SDF 的 <ros2_control> 标签格式与 URDF 中完全一致(第 6 章)。仿真 hardware_interface 的 read() 从物理引擎获取关节状态,write() 将力矩/速度命令传给物理引擎——替换了真机的串口/CAN 通信层。
确认 gz_ros2_control 控制器已加载:
ros2 control list_controllers
## 期望输出:diff_drive_controller [diff_drive_controller/DiffDriveController] active
5.2 MuJoCo 桥接¶
MuJoCo 的 ROS 2 集成有两条路径。mujoco_ros2_control(Humble apt 包 ros-humble-mujoco-ros2-control)按 ros2_control 硬件接口接入,与 gz_ros2_control 同构,适合标准控制器栈(见 mujoco-textbook 第 9 章「外部集成与训练」中的 ros2_control 通路)。另一条是单进程 rclpy 桥接:在 MuJoCo Python 循环内直接调 rclpy publisher/subscriber,仿真与控制共享同一进程,适合快速试错与轻量仿真。
与 mujoco-textbook 第 9 章「外部集成与训练」中的 rclpy 桥接方案对照:
| 维度 | gz sim + ros_gz | MuJoCo + rclpy 单进程 |
|---|---|---|
| 进程模型 | gz sim 独立进程 + bridge 进程 + ROS 节点 | MuJoCo 与 ROS 节点在同一 Python 进程 |
| 通信路径 | ROS topic → bridge → gz transport | 直接 Python 调用(零序列化) |
| 物理引擎 | DART/Bullet(关节体优化) | MuJoCo(接触丰富操作优化) |
| 传感器仿真 | 内置(ray/LiDAR/Camera/IMU 插件) | 需手动调 mjData.sensordata 并填充 ROS 消息 |
| 适用 | 导航、SLAM 全链路、多机器人 | 操作、力控、RL(需高频接触仿真) |
5.3 选型判据¶
- 需要多传感器 + 全链路仿真(导航、SLAM):gz sim——内置传感器仿真完善、ros_gz 生态成熟、社区模型丰富。
- 需要高精度接触动力学 + 强化学习训练:MuJoCo——求解器精度高、并行仿真效率高、Python API 简洁。
- 需要 GPU 大规模并行 + 照片级渲染:Isaac Sim/Lab——PhysX GPU 加速、USD 生态、合成数据管线。
三者不是互斥关系——gz sim 做导航测试、MuJoCo 做操作训练、Isaac 做 sim-to-real 合成数据是当前业界常见分工。
6. 要点¶
- gz sim(Fortress)是 Humble 的配套仿真器;Classic 已 EOL,新项目应直接用 gz sim。SDF 是其原生格式,可从 URDF 转换但
<ros2_control>等扩展标签需手动迁移。 - ros_gz 三件套(sim/bridge/image)将 gz 内部话题映射为 ROS 话题;
gz_bridge.yaml是唯一的配置入口,每条映射需精确指定 gz 侧的层级话题名。 use_sim_time := true+/clock发布是仿真全链路时间戳一致的前提;gz sim 暂停时 clock 冻结,所有依赖时间的模块同步停滞。gz_ros2_control让仿真与真机共用同一套 ros2_control 配置——换 hardware_interface 插件即可在仿真与真机之间切换,控制代码零改动。- 仿真传感器与真机的差异(噪声模型、运动畸变、盲区)决定了下游算法的仿真结论需在真机上重新确认。
- 选型按"全链路集成度 → 接触精度 → GPU 并行规模"三条轴线在 gz sim、MuJoCo、Isaac 之间决策;三工具可分工并存。