本节摘要:SDF(Simulation Description Format)是 Gazebo 的声明式场景语言,核心层级为 world → model → link → joint/collision/visual/sensor。它比 URDF 多覆盖"整个世界"(光照、物理、地形、多模型),且带 schema 可机器校验。本节解析层级语义、给出与 URDF 的对照及工程转换命令。
ROS 生态的老牌描述格式 URDF 只描述"一台机器人":link 与 joint,完了。但一个仿真实验还需要机器人之外的一切——地形、光照、物理参数、其他机器人、传感器挂载方式。SDF 的设计目标正是描述整个可运行的世界,机器人只是世界里的一种 model。两者关系:
| 维度 | URDF | SDF |
|---|---|---|
| 覆盖范围 | 单机器人 | 整个世界(含机器人) |
| 传感器声明 | 不支持 | 一等公民(<sensor>) |
| 场景环境 | 无 | 地形、光照、大气、物理全局参数 |
| 校验工具 | 依赖解析器报错 | 官方 schema + gz sdf 工具链 |
| 典型来源 | 手写、xacro 生成 | 从 URDF 转换、手写、程序生成 |
生态实践通常是:机器人本体维护一份 URDF(ros2_control 需要它),仿真时转换成 SDF 使用;世界与环境直接写 SDF。
<?xml version="1.0" ?> <sdf version="1.9"> <world name="demo_world"> <!-- 世界级: 物理与光照(第3章的账在这里开总账) --> <physics type="ode"><max_step_size>0.001</max_step_size></physics> <light type="directional" name="sun"><cast_shadows>true</cast_shadows></light> <!-- model: 世界里的一切"东西", 机器人/桌子/墙 语法平等 --> <model name="my_robot"> <pose>0 0 0.1 0 0 0</pose> <link name="base"> <inertial><mass>5</mass>...</inertial> <collision name="col">...</collision> <!-- 物理引擎吃这个 --> <visual name="vis">...</visual> <!-- 渲染引擎吃这个 --> <sensor name="lidar" type="gpu_lidar">...</sensor> <!-- 第5章 --> </link> <joint name="wheel_joint" type="revolute">...</joint> <plugin filename="gz-sim-diff-drive-system" name="...DiffDrive"/> </model> </world> </sdf>
几个容易被低估的语义点:
<include><uri>model://my_robot</uri></include> 引用模型库里的现成模型,不必复制粘贴(4.3 讲解析规则)。
SDF 的"机器可校验"不是口号,命令行就能兑现:
gz sdf -p my_robot.sdf > pretty.sdf # 解析+规范化输出, 语法错误在这里现形 gz sdf -k my_world.sdf # 校验, 不修改 # 从 URDF 生成 SDF(机器人本体进入仿真世界的标准通道) gz sdf -p model.urdf > model.sdf
gz sdf -p 是每次交付前的例行检查:它做 schema 校验并输出规范化版本。团队协作里把"提交前必须过 gz sdf -p"写进 CI,能拦住九成的低级语法问题(标签拼错、单位制混乱、版本声明缺失)。
⚠️ 常见坑:URDF→SDF 转换会自动补全碰撞体、惯性等 URDF 缺失项,补全值是保守默认而非你的真实参数。转换后必须人工核对
<inertial>(用 3.2 的量级法)。"转换完直接用"是惯性账错的第二大来源,仅次于单位错。
层级树有个常被忽略的红利:SDF 是规则嵌套的 XML,凡是"结构重复、参数变化"的场景——传感器阵列、货架森林、随机化测试的世界批量生成——都适合写生成器而不是手工维护几百个重复块。随机化测试尤其如此:把障碍密度、光照强度、摩擦系数参数化,一份脚本产出上百个世界文件,正好喂给第 1 章说的并行批量仿真:
# gen_worlds.py:随机化仓库世界,供批量统计测试 import random, math random.seed(42) def make_world(n_shelves, mu, lux): shelves = [] for i in range(n_shelves): x, y = random.uniform(-8, 8), random.uniform(-8, 8) yaw = random.uniform(0, math.pi) shelves.append( f'<include><uri>model://shelf</uri>' f'<pose>{x:.2f} {y:.2f} 0 0 0 {yaw:.2f}</pose></include>') return f"""<sdf version="1.9"><world name="warehouse"> <physics type="ode"><max_step_size>0.001</max_step_size></physics> <light type="point" name="lamp"><diffuse>{lux} {lux} {lux}</diffuse></light> <road><surface><friction><ode><mu>{mu:.2f}</mu></ode></friction></surface></road> {''.join(shelves)} </world></sdf>""" for k in range(100): open(f"worlds/wh_{k:03d}.sdf", "w").write( make_world(random.randint(5, 30), random.uniform(0.4, 1.0), random.uniform(0.3, 0.9)))
这份生成器把"环境分布"从口头的"各种场景"变成可版本化、可复现的参数区间——团队任何人拿到同一份脚本与同一个种子,都能生成同一批世界。它与 4.3 节的模型库正交:生成器管布局与参数,model:// 引用管复用。判断该手写还是该生成,只看一条:文件里是否存在"结构相同、仅数值不同"的重复块——出现第三处重复时就该换生成器了。
gz sdf -p 进 CI;URDF 转换后必核惯性。语言会了,下一节用它建真东西——地形、建筑、大面积静态环境,以及建模粒度的取舍。