Gazebo自定义世界创建指南:从SDF到完整仿真场景

📅 发布时间:2026/9/10 4:33:52
Gazebo自定义世界创建指南:从SDF到完整仿真场景
做 Gazebo 仿真绕不开一个坎自带的 empty.world 和那些示例 demo 世界看起来能跑可一旦接到自己的算法、自己的机器人任务上就会发现哪哪都不对。场地尺寸不合适、没有参照物、光照太刺眼、地面摩擦系数跟真实场景对不上……这时候你就必须学会创建自定义世界。这是 Gazebo 仿真环境系列教程的第四篇我会用一篇完整可复现的流程带你从零写一个属于自己的 .world 文件并在里面放入静态障碍物、灯光、物理参数最后把机器人也塞进去跑起来。不管你是做导航避障、机械臂抓取还是无人机视觉仿真这篇文章都能让你少走不少弯路。1. 动手前先想清楚自定义世界到底要做什么1.1 为什么不能一直用 empty.world 凑合我在带学生和帮朋友调项目的时候见过太多人用 empty.world 一路跑到中期答辩。这个文件什么都没有就是一个平坦的灰色地面加一个太阳光机器人放上去之后能走、能转、能发 TF看起来好像没问题。但问题会在一瞬间集中爆发你做导航测试机器人前方没有墙所谓的“绕障”根本没有验证场景你做机械臂抓取工作台上没有目标物视觉算法连识别什么都不知道你做无人机高度保持地面上没有任何纹理光流和视觉里程计全部失效。到这一步再回来补场景等于把整个仿真流程重新推倒一遍。所以我的建议是在项目启动的第一天就花半天时间把自定义世界搭好。这听起来像额外工作实际上是在省时间。一个合格的自定义世界是在你写控制代码之前先把“测试环境”这件事固定下来后面所有算法跑出来的数据才是可对比、可复现的。1.2 场景设计清单从需求到布局动手写 world 文件之前我习惯先画一张物理草图不需要多精细但必须包含这些内容任务类型导航、抓取、视觉识别、多机编队还是单纯做控制算法验证不同任务对场景要求完全不同。场地尺寸长宽高各是多少。导航测试一般至少 10m x 10m机械臂工位 2m x 2m 就够无人机室内视觉建议 6m x 6m 以上。障碍物布局墙体、柱子、桌子的位置和尺寸要保证机器人路径上有“必须绕开”的东西。光源类型室内用 point 光或 spot 光室外用 sun视觉仿真还要考虑阴影。物理参数重力方向、仿真步长、地面摩擦系数这些跟机器人运动控制直接相关。机器人初始位姿机器人出生在哪、朝向哪个方向、周围有没有足够的活动空间。我通常会把这些整理成一个表格作为世界文件的设计文档设计项我的常用值说明场地尺寸20m x 20m足够大多数移动机器人测试地面摩擦系数mu11.0, mu21.0默认值模拟普通室内地面光照1 个 sun 1-2 个 point 补充有阴影视觉算法能测深度障碍物3-5 个 box 模型 2-3 个模型库物件形成 U 形或绕行路线物理步长max_step_size0.001s精度高机器人不容易抖机器人出生点场地一角朝向场地中央避免出生在模型内部这些数值不是死的但提前想清楚比写到一半再改要省事得多。1.3 我的参考环境与版本组合写这篇文章前我特意确认了一下自己当前的稳定环境Ubuntu 22.04 ROS2 Humble Gazebo Classic 11。这个组合是我用下来最稳的网上资料也最多。如果你用的是 ROS2 Jazzy创建自定义世界的思路完全一样Gazebo Classic 照样可以用 apt 装到只是部分 launch 文件里的包名要留意一下。这里必须先说清楚一个非常容易混淆的点现在“Gazebo”这个名字底下有两个系列。一个是经典的 Gazebo Classic启动命令是gazeboworld 文件是.worldROS2 下对应的包是gazebo_ros另一个是新一代的 Gazebo SimIgnition 改名而来启动命令是gz sim文件格式和插件机制都不一样。这篇文章讲的完全是 Gazebo Classic。你如果用的是后者千万不要把.world文件直接拿去gz sim里加载格式虽然有点类似但细节差异很大。验证版本很简单gazebo --version printenv | grep GAZEBO正常能看到 Gazebo multi-version 11.x.x 的输出以及 GAZEBO_MODEL_PATH、GAZEBO_RESOURCE_PATH 等环境变量。2. 认识 SDF自定义世界的“源文件”长什么样2.1 SDF 文件的层级关系.world 文件的格式叫 SDF全称 Simulation Description Format本质上是 XML。所有场景元素——地面、墙体、光照、物理参数、传感器、机器人——都以节点形式组织在这个 XML 树里。顶层是sdf version1.6里面包着一个worldworld 就是整个仿真世界的根节点。world 下面可以挂这些关键子节点include引用模型库里的现成模型比如 sun、ground_plane、咖啡桌model直接用图元创建模型或者写死一个完整模型light定义灯光physics物理引擎参数scene天空、环境光、阴影等视觉属性gui仿真界面配置用代码块展示一个世界的骨架看着就非常直观sdf version1.6 world namemy_world include.../include model.../model light.../light physics.../physics scene.../scene /world /sdfSDF 的层级结构是严格的父子关系元素不能放错位置。比如physics直接放在sdf下面而不进worldGazebo 虽然偶尔能容忍但加载时大概率会报解析警告严重的时候直接加载失败。2.2 从一个最小世界开始不要一上来就堆一大堆模型我建议先搞一个最小可用的世界文件跑通了再逐步加内容。最小世界文件长这样?xml version1.0 ? sdf version1.6 world namemy_first_world include urimodel://sun/uri /include include urimodel://ground_plane/uri /include /world /sdfurimodel://sun/uri的意思是“从 Gazebo 的模型库里找 sun 这个模型”。sun 是一个特殊模型本质是一个无限远方向光ground_plane 是一个带碰撞属性的无限大平面。这两个 include 我建议永远保留它们相当于世界的基本盘。没有 sun场景就是黑的没有 ground_plane机器人放进去会直接往下掉。除非你有特殊需求否则别删。如果你装了完整的 Gazebo 软件包这俩模型在本地就能找到不需要联网。如果你的环境里模型库缺失底栏的模型面板会一片空白这种情况我们放到第六节的常见问题里专门说。2.3 启动你的第一个自定义世界把上面的内容存成my_first_world.world然后启动gazebo --verbose my_first_world.world--verbose这个参数强烈建议加上它能输出日志一旦世界文件里有问题你能在终端里直接看到而不是对着黑屏发呆。启动成功后你会看到灰色地面和一个方向光。这时候可以做一个实验打开左侧面板的模型库Model Database在“Edit”菜单里选“Model Database”或者直接按 CtrlB 呼出插入面板。随便拖一个模型到场景里比如一个cafe_table它会稳稳落在你点击的位置。接着你可以用顶部工具栏的移动图标拖动它。这一步看起来没什么用但它是在验证一件事这个自定义世界的“接受新模型”能力是正常的。有些世界文件因为物理参数或嵌套层级配置问题会导致拖入的模型无法固定或直接消失。确认能拖入模型后你可以直接把当前场景保存成一个新的 world 文件菜单栏 File → Save World As。保存出来的文件里Gazebo 会把刚才拖入的模型以include或完整model的形式写进去连坐标都算好了。我的习惯是先手动摆一个大概布局保存成 world再拿去文本编辑器里精修坐标和参数。这比纯手写坐标高效得多。3. 搭建场地地面、墙体、障碍物3.1 用图元和材质创造静态物体最小世界解决了“能跑”的问题接下来要解决“像样”的问题。自定义世界最常见的需求是自定义一块地面、加几面墙、摆几个障碍物。我们先说地面。ground_plane模型虽然好用但它是无限大的而且外观是默认的灰色。如果你想要一个 20m x 20m 的限定场地可以用plane图元自己画一块model nameground_20x20 statictrue/static link namelink collision namecollision geometry plane normal0 0 1/normal size20 20/size /plane /geometry /collision visual namevisual geometry plane normal0 0 1/normal size20 20/size /plane /geometry material script urifile://media/materials/scripts/gazebo.material/uri nameGazebo/Grey/name /script /material /visual /link /model这里必须强调statictrue/static的作用。static 模型的意思是不会因为碰撞、重力产生任何物理响应相当于“焊死”在场景里。场地、墙体、装饰性障碍物都应该设成 static这样机器人撞上去只会被挡住不会把整面墙撞飞。如果你对“static”理解有偏差后面仿真大概率会出现“炸机”名场面——机器人轻轻一碰墙体模型就飞出去场景瞬间一片混乱。再举个例子三面墙体围出一个开口区域这是导航仿真最常用的测试布局。墙体用box图元model namewall_1 statictrue/static pose0 0 0.5 0 0 0/pose link namelink collision namecollision geometry box size2 0.2 1/size /box /geometry /collision visual namevisual geometry box size2 0.2 1/size /box /geometry material script urifile://media/materials/scripts/gazebo.material/uri nameGazebo/Red/name /script /material /visual /link /modelpose0 0 0.5 0 0 0/pose这六个数字分别是 x、y、z、roll、pitch、yaw。墙高 1m所以 z 设成 0.5让墙体中心悬在半空底部正好贴地。这是新手最容易犯的错box 的size是完整高度但 pose 的中心默认在几何体中心如果 z 设成 0墙体会有一半陷进地面以下。3.2 从模型库拉取现成模型所有障碍物都用手搓图元太累了而且不真实。Gazebo 模型库里有一堆现成模型从桌子、椅子到厂房、车库都有直接 include 进来就行include urimodel://cafe_table/uri namemy_cafe_table/name pose-2 1 0 0 0 0.5/pose /includeurimodel://cafe_table/uri是模型库路径name可以给这个模型实例起个别名防止同一个模型被多次引用时名字冲突pose指定摆放位置。这里有一个非常关键的问题为什么有时候 include 了模型启动后却找不到因为你本地的模型库里没有这个模型。Gazebo 默认允许从在线模型库下载但国内网络环境访问在线模型库经常超时。我建议你提前把常用模型下载到本地放到~/.gazebo/models/目录下同时设置环境变量export GAZEBO_MODEL_PATH$HOME/.gazebo/models这个变量最好写进~/.bashrc否则每次开新终端都要重新 export。模型库模型的目录结构有严格要求必须这样~/.gazebo/models/cafe_table/ ├── model.config ├── model.sdf └── materials/ └── scripts/ └── ...model.config是模型的元信息文件里面声明了模型名称、SDF 文件名、作者等信息。如果目录里缺了model.configGazebo 就不会识别这个模型。3.3 摆放与堆叠不用一个坐标一个坐标去试手工写pose坐标尤其是要摆放七八个障碍物的时候非常折磨人。我试过反复启动 Gazebo、拖动模型、手动读坐标效率极低。后来我总结了一套比较省事的工作流先在 GUI 里手动摆放好所有模型。我刚才说过用模型库面板把模型拖进场景然后用移动工具把每个模型放到大致位置。注意打开“Snap”吸附功能在编辑菜单里可以设置吸附步长默认 0.1m这样摆放出来的模型间距很整齐。摆好后 File → Save World As把场景保存成 world 文件。用文本编辑器打开你会看到每个模型都被写成了带坐标的include或model块直接复制里面生成的pose值用就行。然后手工微调。GUI 保存出来的文件里往往有很多冗余参数比如plugin、self_collide等我会清理掉自己不需要的部分。坐标已经是你手动摆出来的位置主要修改的是尺寸、材质、static 属性。如果你需要重复摆放一系列障碍物比如一排路障、一圈柱子推荐写一个简单的 Python 脚本生成 world 文件片段我写过类似的效果很好import math obstacles [] for i in range(8): x 5.0 i * 1.5 y 3.0 * math.sin(i * 0.5) model f model namepole_{i} statictrue/static pose{x} {y} 0.5 0 0 0/pose link namelink collision namecollision geometrycylinderradius0.15/radiuslength1/length/cylinder/geometry /collision visual namevisual geometrycylinderradius0.15/radiuslength1/length/cylinder/geometry materialscripturifile://media/materials/scripts/gazebo.material/urinameGazebo/Yellow/name/script/material /visual /link /model obstacles.append(model) print(\n.join(obstacles))运行脚本把输出的 XML 片段粘进 world 文件比手动复制粘贴几遍靠谱得多。这里也要顺带说一句非 static 的模型必须有inertial块质量、惯性矩阵否则物理引擎计算不了运动学关系。之前遇到过一个案例朋友把一个桌子模型从模型库下载后删掉了 inertial 块结果每次启动 Gazebo 都报错模型疯狂抖动后消失。原因就是碰撞几何体计算需要惯性参数。4. 控制环境表现光照、天空与物理参数4.1 光源的选择与参数光照配置直接影响视觉仿真效果。Gazebo 支持四类光源常用的是 sun、directional、point、spot。我列一张表方便对比类型特点适用场景sun无限远方向光会投射平行阴影室外场景最常用directional固定方向的光线类似于 sun 但可自定义位置需要控制光照方向时point点光源向四周均匀发光室内补光、模拟灯泡spot聚光灯有锥形照射范围模拟射灯、车灯、探照灯自定义一个 point 光源的示例light typepoint namepoint_light_1 pose0 0 3 0 0 0/pose diffuse1 1 1 1/diffuse specular0.1 0.1 0.1 1/specular attenuation range20/range constant0.9/constant linear0.01/linear quadratic0.001/quadratic /attenuation /lightdiffuse是漫反射颜色四个数分别是 R、G、B、透明度数值范围 0 到 1。attenuation控制光的衰减range 是有效距离constant、linear、quadratic 分别对应光强的恒定量、线性衰减和二次衰减。实际调的时候把 linear 和 quadratic 调大一点光源照射范围就会明显收窄。光源数量的把控是一个实战经验问题。我之前在某个视觉抓取项目里放了 8 个 point 光源启动后 Gazebo 界面卡到只有 2 FPS。原因是每个动态光源都要参与每帧的渲染计算对 CPU 和 GPU 都是不小的负担。建议 point 和 spot 光源总数控制在 3 个以内室内场景优先用 1 个 sun 加 1 个 point 的组合。4.2 场景环境与视觉氛围光照是光源场景是环境。世界文件里的scene节点控制天空颜色、环境光、阴影等全局视觉属性scene ambient0.4 0.4 0.4 1/ambient background0.7 0.8 0.9 1/background shadowstrue/shadows /sceneambient是环境光它决定整个场景的基础亮度。环境光太低模型会显得很黑太高阴影效果会变得不明显。.4 0.4 0.4是我个人比较常用的中性值。background是天空背景色模拟白天就是浅蓝色室内场景可以调成偏灰的白色。shadows设置为 true 会开启阴影渲染物体看起来更真实但计算量也更大。视觉仿真要讲真实感的地方颜色、阴影、反射缺一不可。不过如果你是做纯控制算法验证我反而建议把阴影关掉、环境光调亮一点因为视觉仿真里阴影增加的时间成本远比它带来的真实感要高。4.3 物理引擎参数与仿真稳定性world 文件里放上一段physics节点是很多人忽略但极其关键的一步。默认参数虽然能跑但对精度要求高的机器人仿真默认参数往往是“炸机”的根源。physics typeode max_step_size0.001/max_step_size real_time_update_rate1000/real_time_update_rate gravity0 0 -9.8/gravity /physicsmax_step_size物理引擎一次更新计算的步长单位秒。步长越小力学计算越精确但 CPU 消耗越大。0.001 秒1000Hz是我的默认选择对大多数移动机器人和机械臂来说足够稳定。real_time_update_rate希望仿真以多少倍的实时频率运行单位是每秒更新次数。1000 表示尽量让仿真进度和真实时间 1:1 对齐。如果 CPU 跟不上仿真会变慢。gravity重力加速度矢量默认 0 0 -9.8。如果你做的是某个特殊姿态的仿真比如倒挂机械臂可以修改重力方向这是个很灵活的参数。这里我说一个经验移动机器人出现剧烈抖动首先排查max_step_size如果当前是 0.001 还抖就把地面摩擦系数调低一点看看。物理引擎里摩擦接触的数值如果设置过大配合步长过大模型底部会产生非常强的震动反馈看起来就像“炸机”前的征兆。5. 把机器人放进自定义世界5.1 两种插入方式的取舍自定义世界搭好了最后一步是把机器人放进去。有两种常见方式一是直接在 world 文件里 include 机器人的 SDF 描述例如include urimodel://my_robot/uri namemy_robot_instance/name pose0 0 0.1 0 0 0/pose /include这种方式简单直接机器人跟着世界文件启动就出现适合你只需要“世界里有这么个机器人”的场景。缺点是你无法在 ROS2 里动态控制它的生成过程也不方便随时更换机器人型号。二是用 gazebo_ros 的 spawn_entity 服务在仿真环境启动后通过 ROS2 服务调用把机器人生成到场景里。这种方式更贴合实际开发流程因为你通常已经通过robot_state_publisher在发布robot_description话题了spawn_entity 可以直接从话题里读模型描述然后把机器人“丢”进世界。我用一张表对比这两种方式对比项world 文件内 includespawn_entity 服务启动顺序随世界启动需要等待 Gazebo 就绪模型来源本地模型库URDF/SDF 话题或文件动态重生不灵活可以多次调用与 ROS2 集成一般强我的建议临时测试用 include正式项目全部走 spawn_entity。5.2 用 ROS2 launch 一次启动世界与机器人如果你用的是 ROS2最舒服的姿势就是写一个 Python launch 文件把启动世界、启动机器人描述、生成机器人三个动作打包在一起。下面是我在项目里常用的模板直接做成my_robot_gazebo/launch/spawn_world.launch.pyimport os from launch import LaunchDescription from launch.actions import ExecuteProcess, DeclareLaunchArgument, SetEnvironmentVariable from launch.substitutions import LaunchConfiguration from launch_ros.actions import Node def generate_launch_description(): pkg_share /path/to/your/package world_path os.path.join(pkg_share, worlds, my_world.world) return LaunchDescription([ SetEnvironmentVariable( GAZEBO_MODEL_PATH, os.path.join(pkg_share, models) ), ExecuteProcess( cmd[gazebo, --verbose, world_path, -s, libgazebo_ros_factory.so, -s, libgazebo_ros_init.so], outputscreen ), Node( packagerobot_state_publisher, executablerobot_state_publisher, parameters[{robot_description: open(/path/to/robot.urdf).read()}], outputscreen ), Node( packagegazebo_ros, executablespawn_entity.py, arguments[ -topic, robot_description, -entity, my_robot, -x, 0.0, -y, 0.0, -z, 0.1 ], outputscreen ) ])这里有几个关键点-s libgazebo_ros_factory.so和-s libgazebo_ros_init.so是 Gazebo 的 ROS2 插件不加这两个插件spawn_entity 服务根本不存在你后面所有“生成机器人”的 ROS2 调用都会报 service not available。spawn_entity.py的-x -y -z参数是机器人出生的位置z 设成 0.1 而不是 0是为了让机器人底部和地面之间留一点缝隙避免模型一出生就与地面发生穿透导致物理引擎强制修正坐标而产生初速度“弹跳”。除了在包目录里找路径之外更规范的做法是用ament_index_python来定位包路径from ament_index_python.packages import get_package_share_directory pkg_share get_package_share_directory(my_robot_gazebo)5.3 验证自定义世界是否工作正常启动完成后不要急着关终端先做一轮快速验证ros2 topic list ros2 node list正常情况你应该能看到/spawn_entity服务、/robot_description话题、机器人状态相关话题。如果你插入了带摄像头的机器人还会看到/camera/image_raw这类话题。打开 rviz2添加 TF 和 RobotModel 显示确认机器人的 TF 树已经发布、模型显示在预期位置。这一步能过滤掉一大半“机器人没生成成功”的问题。还有一个冷门但有效的验证方法直接查看 Gazebo 场景里的模型状态ros2 service call /get_model_list gazebo_msgs/srv/GetModelList返回结果里应该包含你 world 里的所有模型和机器人实体如果只有地面和墙体没有机器人那就是 spawn_entity 没生效去查日志。6. 调车也是一门课常见问题与避坑记录6.1 高频报错速查表这几年代码写得多踩过的坑也攒了不少。我把自定义世界相关的高频问题整理成一张速查表你遇到问题直接对着查现象可能原因解决方式启动后场景全黑缺少 sun 模型或场景里没有光源检查 model://sun 是否成功 include模型库面板空白GAZEBO_MODEL_PATH 未设置或本地无模型export GAZEBO_MODEL_PATH重新安装 gazebo-data 包拖入模型后模型消失模型没有 static且缺少 inertial 参数检查模型目录的 model.sdf 文件机器人出生后疯狂抖动max_step_size 过大或接触参数设置不合理调小步长到 0.001检查地面摩擦系数机器人从地面往下掉地面模型没有碰撞体或机器人初始 z 太低确认 ground_plane 的 collision 存在提高 spawn 高度world 文件加载报 XML 解析错误标签嵌套错误或漏了闭合标签用 xml 解析器校验文件格式Gazebo 卡得动不了动态光源太多或阴影开启减少 point/spot 光源数量关闭 shadowsspawn_entity 报 service not available忘了加载 gazebo_ros_factory 插件启动命令加-s libgazebo_ros_factory.so修改世界文件后启动还是旧场景Gazebo 配置文件缓存删除~/.gazebo/server-*下的临时文件再启动特别注意最后一条Gazebo 有时候会把场景缓存到临时目录你改了 world 文件却看不到变化十有八九是缓存问题。清理后再启动世界就正常了。6.2 仿真性能与稳定性提升技巧自定义世界越大、细节越多性能越差。我见过有人把整个工厂的 CAD 模型转成 SDF 丢进 Gazebo最后仿真跑起来的频率不到 0.5 倍实时。做工程要懂得做取舍模型细节要按需分配。远处的装饰性模型用简单的 box 代替只有机器人视觉能看到的区域才放高精度模型。一个 2000 面的 Mesh 模型和一个 6 个面的 box 相比渲染开销差距很大但对于导航算法来说效果可能完全一样。该 static 的必须 static。我前面反复强调过 static这里再补一句static 模型不仅不会飞还能大幅降低物理引擎的计算量。物理引擎每帧都要计算碰撞响应static 模型可以跳过很多动力学求解性能收益非常明显。光源数量是视觉仿真性能的第一杀手。我建议把点光源控制在 2 个以内把阴影设为 false。等你的视觉算法调试通过后再按需开启阴影测试算法鲁棒性。传感器更新频率也要控制。激光雷达、摄像头如果跑到 30Hz 以上每帧都要渲染和计算CPU 直接拉满。我一般把传感器发布频率设在 10Hz 到 15Hz既保证控制算法数据足够又不会把仿真拖垮。最后再分享一个小技巧如果发现自定义世界加载后地面位置不对或者模型整体偏移优先检查你的pose写法尤其是 z 轴。图元模型在 Gazebo 里的坐标原点默认在几何体中心不是底部这个和真实世界的“物体放在地面上”的直觉不一样。每次写完 world 文件先启动看一眼再微调坐标比一次性把所有坐标都精确算出来要省心得多。按这套流程走下来你已经能从零搭出一个带地面、墙体、障碍物、光源和机器人的完整自定义仿真世界。后面无论是接控制算法、跑视觉识别还是做多机编队这个世界的骨架都能稳定给你托底。