简介:这份资源是面向ROS2机器人开发初学者与仿真研究者的Gazebo多线激光雷达仿真示例工程,基于ROS2 Humble与Ubuntu 22.04搭建,重点提供带多线激光雷达传感器的机器人模型及配套仿真配置,可用于地图构建、定位导航等感知算法的验证与学习。压缩包共30个文件,约62KB,以12个xacro机器人描述文件、4个Python脚本、2个yaml参数配置为主,另含world仿真场景、sdf与urdf模型、rviz可视化配置、launch启动文件及说明文档,结构紧凑、模块划分清晰。其中fishbot_description完整给出机器人本体描述,方便快速理解模型组成并在此基础上二次修改。资源附有安装运行说明与附赠文档,对参数和模块均有解释,即便背景不深的用户也能顺利跑通仿真。目前已有50人学习,适合想入门ROS2与Gazebo联合仿真的开发者参考实践。
1. 从 fishbot_description 说起:一套能跑起来的多线激光雷达仿真到底长什么样
很多人第一次在 ROS2 Humble 上折腾 Gazebo 多线激光雷达,卡住的地方往往不是算法,而是环境本身:Ubuntu 22.04 装完,Gazebo 能打开,机器人模型也能显示,但一加雷达插件要么话题没数据,要么点云稀稀拉拉像坏了一样。这个标题讲的工程,本质就是把「fishbot_description 机器人描述 + Gazebo 仿真配置 + 多线激光雷达插件」这三件事串成一条能复现的链路,让你在本地把一台带多线雷达的差速小车跑起来,并且能在 RViz2 里看到可用的点云。
它适合两类人:一类是刚学完 ROS2 话题、服务、动作,想找个真实项目练手的;另一类是要做 SLAM、建图、导航验证,但手头没有实体雷达,想先用仿真把算法跑通的。核心不是把模型画得多好看,而是让雷达数据在 Gazebo 里真实产生、在 RViz2 里正确显示、能被后续节点订阅。下面按「模型怎么描述 → 仿真怎么配 → 雷达怎么出数据 → 坑在哪」的顺序讲透。
2. fishbot_description 的 URDF/Xacro 结构:机器人描述文件怎么组织才不返工
2.1 为什么用 Xacro 而不是裸 URDF
裸 URDF 写多线雷达机器人会非常痛苦:底盘、左右轮、万向轮、雷达支架、雷达本体,每个 link 都要写 visual、collision、inertial,重复度极高。Xacro 提供宏、参数、数学运算和文件包含,能把「一个轮子」抽象成宏,左右轮只改参数。常见做法是把描述文件拆成几个 xacro 文件,用xacro:include组合,主文件只负责拼装和调用。
我一般会这样分:fishbot_base.xacro放底盘和轮子,fishbot_lidar.xacro放雷达和支架,fishbot.gazebo.xacro放 Gazebo 专用标签(插件、材质、摩擦系数),最后fishbot.urdf.xacro把它们 include 进来。这样改雷达型号时只动一个文件,不会把底盘参数带崩。
2.2 一个可复用的雷达 link 与 joint 写法
下面这段是雷达本体加支架的最小写法,注意inertial不能省,Gazebo 里没有惯量会导致模型抖动甚至飞出去。
<!-- fishbot_lidar.xacro --> <robot xmlns:xacro="http://www.ros.org/wiki/xacro"> <xacro:property name="lidar_radius" value="0.04"/> <xacro:property name="lidar_height" value="0.05"/> <xacro:property name="lidar_mass" value="0.1"/> <link name="lidar_link"> <visual> <geometry> <cylinder radius="${lidar_radius}" length="${lidar_height}"/> </geometry> <material name="black"/> </visual> <collision> <geometry> <cylinder radius="${lidar_radius}" length="${lidar_height}"/> </geometry> </collision> <inertial> <mass value="${lidar_mass}"/> <inertia ixx="${lidar_mass*0.001}" ixy="0" ixz="0" iyy="${lidar_mass*0.001}" iyz="0" izz="${lidar_mass*0.0005}"/> </inertial> </link> <joint name="lidar_joint" type="fixed"> <parent link="base_link"/> <child link="lidar_link"/> <origin xyz="0 0 0.12" rpy="0 0 0"/> </joint> </robot>逻辑说明:lidar_link用圆柱近似雷达外形,inertial里的惯量按圆柱近似公式给一个量级正确的值即可,仿真不要求精确。joint用fixed类型,因为雷达相对底盘不动。参数说明:lidar_radius和lidar_height决定雷达在 RViz2 里的显示大小,也影响 Gazebo 碰撞体;origin的 z 值要和底盘高度匹配,否则雷达会陷进底盘或悬空。
2.3 在主 xacro 里拼装并验证
主文件负责 include 和调用,写完先用xacro命令转一次 URDF,确认没有语法错误再进 Gazebo。
# 把 xacro 转成 urdf,检查语法 xacro fishbot.urdf.xacro > /tmp/fishbot.urdf # 用 check_urdf 验证结构 check_urdf /tmp/fishbot.urdf逻辑说明:xacro命令会展开所有宏和 include,任何拼写错误、参数缺失都会在这一步报出来,比在 Gazebo 里看黑屏快得多。check_urdf会检查 link 和 joint 的树结构是否合法。参数说明:如果输出里出现No such file,检查 include 路径;如果check_urdf报root link问题,检查是否有多个根 link。
提示:每次改完 xacro 都先跑一遍这两条命令,能省掉大量在 Gazebo 里反复重启的时间。
3. Gazebo 仿真配置:把模型、世界和插件接起来
3.1 世界文件与模型加载方式
Gazebo 启动时需要知道两件事:世界长什么样,机器人怎么进来。常见做法是写一个.world文件,里面放地面、光源,然后通过 launch 文件把机器人模型 spawn 进去。世界文件不需要很复杂,一个ground_plane加一个sun就够跑雷达。
<!-- worlds/fishbot.world --> <sdf version="1.6"> <world name="default"> <include> <uri>model://ground_plane</uri> </include> <include> <uri>model://sun</uri> </include> </world> </sdf>逻辑说明:include直接引用 Gazebo 自带模型库,不用自己建地面。参数说明:world name可以自定义,但 launch 文件里要对应;如果后续要加障碍物,在world里继续 include 或直接写 model。
3.2 launch 文件里 spawn 模型与加载插件
launch 文件是整条链路的粘合剂:启动 Gazebo、加载世界、把机器人描述转成参数、spawn 模型、启动 robot_state_publisher。
# launch/fishbot_gazebo.launch.py import os from launch import LaunchDescription from launch.actions import IncludeLaunchDescription from launch.launch_description_sources import PythonLaunchDescriptionSource from launch_ros.actions import Node from ament_index_python.packages import get_package_share_directory def generate_launch_description(): pkg_share = get_package_share_directory('fishbot_description') gazebo_ros_share = get_package_share_directory('gazebo_ros') # 读取 xacro 并转成 urdf 字符串 urdf_file = os.path.join(pkg_share, 'urdf', 'fishbot.urdf.xacro') robot_desc = os.popen(f'xacro {urdf_file}').read() return LaunchDescription([ # 启动 gazebo,加载自定义世界 IncludeLaunchDescription( PythonLaunchDescriptionSource( os.path.join(gazebo_ros_share, 'launch', 'gazebo.launch.py') ), launch_arguments={'world': os.path.join(pkg_share, 'worlds', 'fishbot.world')}.items() ), # 发布机器人描述 Node( package='robot_state_publisher', executable='robot_state_publisher', parameters=[{'robot_description': robot_desc}] ), # spawn 模型到 gazebo Node( package='gazebo_ros', executable='spawn_entity.py', arguments=['-topic', 'robot_description', '-entity', 'fishbot'] ), ])逻辑说明:gazebo.launch.py是 gazebo_ros 包自带的启动文件,传world参数就能加载自定义世界。robot_state_publisher订阅robot_description并发布 TF。spawn_entity.py从robot_description话题读取模型并生成到 Gazebo。参数说明:-entity是模型在 Gazebo 里的名字,重复 spawn 同名模型会报错;-topic必须和 robot_state_publisher 发布的话题一致。
3.3 多线雷达插件怎么配:以 ray 传感器为例
Gazebo 里多线雷达靠ray传感器实现,关键参数是samples(每线采样数)、min_angle/max_angle(水平范围)、vertical相关参数(线数和垂直范围)。下面是一个 16 线雷达的插件配置片段,放在fishbot.gazebo.xacro里。
<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> <vertical> <samples>16</samples> <resolution>1</resolution> <min_angle>-0.2618</min_angle> <max_angle>0.2618</max_angle> </vertical> </scan> <range> <min>0.1</min> <max>30.0</max> <resolution>0.01</resolution> </range> </ray> <plugin name="lidar_controller" filename="libgazebo_ros_ray_sensor.so"> <ros> <remapping>~/out:=scan</remapping> </ros> <output_type>sensor_msgs/PointCloud2</output_type> <frame_name>lidar_link</frame_name> </plugin> </sensor> </gazebo>逻辑说明:horizontal的samples决定一圈多少个点,360 表示 1 度一个点;vertical的samples就是线数,16 表示 16 线。min_angle/max_angle用弧度,垂直范围 ±0.2618 约等于 ±15 度。output_type选PointCloud2才能出三维点云,选LaserScan只能出单线。参数说明:update_rate是雷达频率,10 表示 10Hz;range的max决定最远探测距离,设太大仿真会变慢;frame_name必须和 URDF 里的 link 名一致,否则 RViz2 里点云位置会错。
注意:
visualize设为 true 会在 Gazebo 里画出射线,调试时有用,但线数多时会明显拖慢仿真,正式跑建议关掉。
4. 从 Gazebo 到 RViz2:点云话题、TF 与显示排查
4.1 确认话题和消息类型
模型跑起来后,第一件事是确认雷达话题真的在发数据。
# 列出所有话题 ros2 topic list # 查看 scan 话题的消息类型和频率 ros2 topic info /scan ros2 topic hz /scan # 看一帧数据 ros2 topic echo /scan --once逻辑说明:topic list里应该能看到/scan,topic info会显示类型是sensor_msgs/msg/PointCloud2。topic hz如果一直没输出,说明插件没加载成功或话题名不对。参数说明:如果话题名不是/scan,检查插件里的remapping;如果类型是LaserScan,说明output_type配错了。
4.2 RViz2 里正确显示点云的三步
RViz2 显示点云翻车,九成是 Fixed Frame 和 TF 的问题。按下面三步走:
第一步,启动 RViz2,把 Fixed Frame 改成base_link或lidar_link,不要用map,因为仿真里没有 map 到 base 的 TF。
第二步,Add 一个 PointCloud2 显示,Topic 选/scan,Style 选 Points,Size 调小一点比如 0.01。
第三步,如果点云不显示,先看 RViz2 左下角有没有 TF 报错,再用命令确认 TF 树。
# 查看 TF 树 ros2 run tf2_tools view_frames # 手动查两个 frame 之间的变换 ros2 run tf2_ros tf2_echo base_link lidar_link逻辑说明:view_frames会生成一个 pdf,能看到所有 frame 的连接关系。tf2_echo如果一直输出Translation和Rotation,说明 TF 正常;如果报Invalid frame ID,说明某个 frame 没发布。参数说明:base_link和lidar_link必须都在 TF 树里,且lidar_link是base_link的子节点。
4.3 点云稀疏或缺失的常见参数调整
如果点云看起来只有几条线,或者范围明显不对,优先查三个参数:samples、min_angle/max_angle、range的max。samples太小会导致一圈点数不够,看起来像断线;角度范围写错会导致只扫一半;max太小会导致远处点直接消失。改完参数后不需要重新编译,但要重启 Gazebo 让插件重新加载。
5. 避坑与排查:多线雷达仿真里最容易翻车的 5 个点
现象一:Gazebo 里模型加载了,但/scan话题不存在。原因:插件没被 Gazebo 识别,通常是filename写错或gazebo_ros包没装全。 解决:确认libgazebo_ros_ray_sensor.so存在,用ros2 pkg prefix gazebo_ros找到路径,检查gazebo_ros是否通过apt正确安装。
现象二:点云在 RViz2 里位置偏移,或者跟着机器人乱飞。原因:frame_name和 URDF 里的 link 名不一致,或者 TF 没发布。 解决:把插件里的frame_name改成和 URDF 完全一致的 link 名,再用tf2_echo确认该 link 在 TF 树里。
现象三:Gazebo 启动后机器人直接掉下去或抖动。原因:inertial缺失或质量、惯量设成 0。 解决:每个 link 都要有inertial,质量给一个合理值,惯量按几何体近似给,不要留空。
现象四:点云频率很低,仿真卡顿。原因:samples和线数太大,或者visualize开着。 解决:先把visualize关掉,再把samples从 360 降到 180 试,线数按实际需要设,不要盲目上 32 线、64 线。
现象五:ros2 topic hz /scan有频率,但 RViz2 里就是看不到点。原因:RViz2 的 Fixed Frame 设成了map,而仿真里没有map到base_link的 TF。 解决:把 Fixed Frame 改成base_link或lidar_link,再检查 PointCloud2 的 Topic 和 Style 设置。
6. 进阶技巧:用参数化 xacro 快速切换雷达线数并验证数据质量
真正做项目时,你不会只跑一种雷达。今天验证 16 线,明天可能要对比 32 线,如果每次都手改插件参数,很容易改漏。我的习惯是把线数、采样数、角度范围全部抽成 xacro 参数,用命令行传参生成不同配置。
<!-- 在 fishbot_lidar.xacro 顶部定义可覆盖参数 --> <xacro:arg name="lidar_lines" default="16"/> <xacro:arg name="lidar_samples" default="360"/> <xacro:property name="lidar_lines" value="$(arg lidar_lines)"/> <xacro:property name="lidar_samples" value="$(arg lidar_samples)"/>然后在插件里引用这些属性:
<vertical> <samples>${lidar_lines}</samples> <min_angle>-0.2618</min_angle> <max_angle>0.2618</max_angle> </vertical> <horizontal> <samples>${lidar_samples}</samples> <min_angle>-3.14159</min_angle> <max_angle>3.14159</max_angle> </horizontal>生成时直接传参:
xacro fishbot.urdf.xacro lidar_lines:=32 lidar_samples:=720 > /tmp/fishbot_32.urdf逻辑说明:xacro:arg定义外部可传参数,xacro:property把它变成内部属性,插件里用${}引用。这样一套描述文件能生成多种雷达配置。参数说明:lidar_lines控制垂直采样数即线数,lidar_samples控制水平一圈点数;线数和采样数越大,仿真越吃 CPU,建议先小后大逐步验证。
验证数据质量时,我一般会订阅/scan并统计一帧的点数,和理论值对比。16 线 × 360 采样理论上是 5760 个点,实际会因为遮挡和距离限制少一些。如果差太多,就回去查range的max和角度范围。这个习惯帮我省过很多次「以为雷达坏了其实只是参数写错」的后悔药。
最后说个血泪经验:每次改完 xacro 和插件,先跑xacro转 URDF 确认语法,再启动 Gazebo,最后用ros2 topic hz和 RViz2 双重确认。不要一上来就开 RViz2 盯着看,那样出了问题你根本不知道是模型、插件还是显示配置的锅。希望帮到你。
本文还有配套的精品资源,点击获取