4. 仿真世界构建:SDF世界文件编写、静态物体与动态物体、光照与物理引擎参数、场景保存与加载
好,我们进入第四章。这一章要聊的是仿真世界的构建,说白了就是怎么用代码搭一个虚拟的物理舞台。你想想看,机器人要在仿真里跑,总得有个场地、有障碍物、有光照、有物理规则吧?这些全得靠 SDF 文件来定义。
我个人习惯把 SDF 文件看作是仿真世界的「宪法」。它规定了这个世界里有什么、长什么样、怎么动。Gazebo 启动时读的就是这个文件。所以,写不好 SDF,后面导航调参全是白搭。
4.1 SDF 文件结构:从根节点到模型
SDF 格式是 XML 的一种。根节点是 <sdf>,里面包着 <world>,<world> 里再放模型、光照、物理参数。层级非常清晰。
<?xml version="1.0" ?>
<sdf version="1.7">
<world name="my_warehouse">
<include>
<uri>model://ground_plane</uri>
</include>
<include>
<uri>model://sun</uri>
</include>
<model name="shelf">
<pose>2.0 1.5 0.0 0 0 0</pose>
<link name="body">
<collision name="collision">
<geometry>
<box>
<size>1.0 0.5 1.8</size>
</box>
</geometry>
</collision>
<visual name="visual">
<geometry>
<box>
<size>1.0 0.5 1.8</size>
</box>
</geometry>
<material>
<ambient>0.8 0.6 0.2 1.0</ambient>
</material>
</visual>
</link>
</model>
</world>
</sdf>
这段代码定义了一个叫 my_warehouse 的世界。里面有一块地面、一个太阳、一个货架。货架的位置在 (2.0, 1.5),高度 1.8 米。注意 <pose> 的六个参数:x, y, z, roll, pitch, yaw。我刚开始写的时候经常把 yaw 和 pitch 搞反,结果货架斜着插进地里,找了半天 bug。
gz sdf --check your_file.sdf 检查语法。这能省下你至少一半的调试时间。
4.2 静态物体与动态物体
仿真世界里,物体分两种:静态的和动态的。静态物体不会动,比如墙壁、货架、地面。动态物体受重力影响,会碰撞、会翻滚,比如机器人、箱子、球。
怎么区分?看 <static> 标签。
<!-- 静态物体:墙壁 -->
<model name="wall">
<static>true</static>
<link name="body">
<collision name="collision">
<geometry>
<box>
<size>0.2 3.0 2.0</size>
</box>
</geometry>
</collision>
<visual name="visual">
<geometry>
<box>
<size>0.2 3.0 2.0</size>
</box>
</geometry>
</visual>
</link>
</model>
<!-- 动态物体:箱子 -->
<model name="box">
<static>false</static>
<link name="body">
<inertial>
<mass>5.0</mass>
<inertia>
<ixx>0.1</ixx>
<iyy>0.1</iyy>
<izz>0.1</izz>
</inertia>
</inertial>
<collision name="collision">
<geometry>
<box>
<size>0.5 0.5 0.5</size>
</box>
</geometry>
</collision>
<visual name="visual">
<geometry>
<box>
<size>0.5 0.5 0.5</size>
</box>
</geometry>
</visual>
</link>
</model>
动态物体必须定义 <inertial> 属性,包括质量和惯性张量。我曾经偷懒没写惯性,结果箱子一碰就飞到了天边,物理引擎直接崩了。嗯,这里要注意:惯性张量的值不能太离谱,否则物理仿真会变得很不稳定。
| 属性 | 静态物体 | 动态物体 |
|---|---|---|
<static> |
true | false |
| 受重力影响 | 否 | 是 |
| 可碰撞 | 是 | 是 |
| 需要惯性参数 | 否 | 是 |
| 典型用途 | 墙壁、地面、货架 | 机器人、箱子、球 |
4.3 光照与物理引擎参数
光照影响的是视觉,物理引擎参数影响的是「手感」。两者都很关键。
4.3.1 光照配置
Gazebo 支持多种光源:方向光、点光源、聚光灯。最常用的是方向光,模拟太阳。
<light type="directional" name="sun">
<cast_shadows>true</cast_shadows>
<pose>0 0 10 0 0 0</pose>
<diffuse>0.8 0.8 0.8 1.0</diffuse>
<specular>0.2 0.2 0.2 1.0</specular>
<direction>-0.5 0.1 -0.9</direction>
<attenuation>
<range>1000</range>
<constant>0.9</constant>
<linear>0.01</linear>
<quadratic>0.001</quadratic>
</attenuation>
</light>
我个人习惯把 <cast_shadows> 打开。没有阴影的仿真场景看起来特别假,机器人导航时也容易误判障碍物距离。但注意,阴影会消耗 GPU 资源,场景复杂时酌情关闭。
4.3.2 物理引擎参数
物理引擎参数写在 <physics> 标签里。Gazebo 默认用 ODE,也可以换成 Bullet、DART 或 Simbody。
<physics type="ode">
<max_step_size>0.001</max_step_size>
<real_time_factor>1.0</real_time_factor>
<real_time_update_rate>1000</real_time_update_rate>
<ode>
<solver>
<type>quick</type>
<iters>50</iters>
<sor>1.3</sor>
</solver>
<constraints>
<cfm>0.0</cfm>
<erp>0.2</erp>
<contact_max_correcting_vel>100.0</contact_max_correcting_vel>
<contact_surface_layer>0.001</contact_surface_layer>
</constraints>
</ode>
</physics>
几个关键参数:
- max_step_size:仿真步长。0.001 秒是 1 kHz,精度高但 CPU 开销大。导航调参时我常用 0.005 秒,够用且流畅。
- real_time_factor:实时因子。1.0 表示仿真时间等于真实时间。调试时我经常设成 0.5,让仿真慢一点,方便观察。
- iters:求解器迭代次数。50 次是安全值,太低会导致物体穿透地面。我曾经设成 10 次,箱子直接掉到地心去了。
- cfm 和 erp:约束力混合参数和误差降低参数。默认值 0.0 和 0.2 一般够用,如果物体抖动可以微调。
contact_surface_layer 设成了 0.1。这个参数控制接触表面层厚度,太大就像在冰面上走。建议保持 0.001 以下。
4.4 场景保存与加载
场景搭好了,怎么保存?怎么加载?Gazebo 提供了两种方式。
4.4.1 从 SDF 文件加载
最直接的方式:启动时指定世界文件。
gazebo --verbose worlds/my_warehouse.sdf
或者用 roslaunch 加载:
<launch>
<include file="$(find gazebo_ros)/launch/empty_world.launch">
<arg name="world_name" value="$(find my_pkg)/worlds/my_warehouse.sdf"/>
<arg name="paused" value="false"/>
<arg name="use_sim_time" value="true"/>
<arg name="gui" value="true"/>
<arg name="headless" value="false"/>
<arg name="debug" value="false"/>
</include>
</launch>
4.4.2 运行时保存场景
在 Gazebo GUI 里手动添加了物体后,可以保存当前场景为 SDF 文件。点击菜单栏的 File → Save World As...,或者用命令行:
gz service --service /gazebo/save_world --request "filename: '/path/to/your_world.sdf'"
我个人更推荐用命令行保存,因为 GUI 保存有时会漏掉一些运行时参数。保存后检查一下文件,确保 <physics> 和 <light> 配置都在。
4.4.3 场景快照与恢复
Gazebo 支持通过服务保存和恢复场景状态,这对调试特别有用。
# 保存当前状态
rosservice call /gazebo/get_world_properties "{}"
# 恢复状态(需要先保存模型状态)
rosservice call /gazebo/set_model_state "model_state: ..."
我经常在导航调参时用这个功能:先让机器人跑到一个位置,保存状态,然后反复修改参数从那个位置重新跑,不用每次都从头开始。
4.5 本章知识体系
下面这张图总结了本章的核心内容,从 SDF 文件结构到场景的保存与加载,一条线串起来。
4.6 本章小结
写 SDF 文件就像搭积木。先搭地面和墙壁(静态),再放机器人和箱子(动态),调好光照和物理参数,最后保存成文件。下次启动直接加载,整个世界就复原了。
我个人建议你从最简单的场景开始:一个地面、一个机器人、一堵墙。跑通了再慢慢加东西。别一上来就搭一个几百兆的工厂模型,到时候物理引擎卡死,你连 bug 都找不到在哪。
核心要点回顾:
- SDF 文件是仿真世界的定义文件,根节点为
<sdf>,内嵌<world>。 - 静态物体用
<static>true</static>,动态物体必须定义惯性参数。 - 光照影响视觉,物理参数影响运动真实性。
- 场景可通过 SDF 文件加载,也可在运行时保存和恢复。