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。

小技巧:写 SDF 时,先用 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 文件结构到场景的保存与加载,一条线串起来。

仿真世界构建知识体系 SDF 世界文件 模型定义 光照配置 物理引擎 静态物体 动态物体 方向光/点光源 ODE/Bullet 步长/迭代 场景保存与加载 SDF 文件加载 运行时保存/恢复 核心原则 静态物体定义环境骨架,动态物体承载交互逻辑;物理参数决定仿真真实度,光照影响感知质量

4.6 本章小结

写 SDF 文件就像搭积木。先搭地面和墙壁(静态),再放机器人和箱子(动态),调好光照和物理参数,最后保存成文件。下次启动直接加载,整个世界就复原了。

我个人建议你从最简单的场景开始:一个地面、一个机器人、一堵墙。跑通了再慢慢加东西。别一上来就搭一个几百兆的工厂模型,到时候物理引擎卡死,你连 bug 都找不到在哪。

核心要点回顾:

  • SDF 文件是仿真世界的定义文件,根节点为 <sdf>,内嵌 <world>。
  • 静态物体用 <static>true</static>,动态物体必须定义惯性参数。
  • 光照影响视觉,物理参数影响运动真实性。
  • 场景可通过 SDF 文件加载,也可在运行时保存和恢复。