4、Gazebo仿真环境配置:world文件创建、添加光源与地面、导入静态物体、设置物理引擎参数
好,咱们接着往下走。上一章我们把机械臂模型和控制器都搞定了,模型能在Gazebo里动起来了。但说实话,光秃秃一个机械臂悬在半空中,看着挺别扭的,也没法做正经的抓取任务。这一章,我们就来搭一个像样的仿真环境。
我个人习惯,每次开始一个新项目,第一件事就是先把“舞台”搭好。你想想看,一个真实的机器人工作台,得有地面、有光照、有桌子、有被抓的物体,对吧?Gazebo里也一样,这些全得靠 .world 文件来定义。
4.1 什么是world文件?
说白了,.world 文件就是Gazebo的“场景剧本”。它告诉Gazebo:
- 天空是什么颜色?
- 地面在哪里?
- 有没有桌子、箱子?
- 重力加速度设多少?
- 物理引擎用ODE还是Bullet?
嗯,你可以把它理解成一个超级详细的“装修图纸”。Gazebo启动时,会先读这个文件,然后按照图纸把场景搭起来。
核心要点: 一个完整的仿真环境 = 模型 + 控制器 + world文件。缺一不可。
4.2 创建你的第一个world文件
我们先从最简单的开始。创建一个空文件,命名为 pick_place.world,放在你的功能包 worlds 目录下。
<?xml version="1.0" ?>
<sdf version="1.6">
<world name="pick_place_world">
<!-- 这里放光源 -->
<!-- 这里放地面 -->
<!-- 这里放静态物体 -->
<!-- 这里设置物理参数 -->
</world>
</sdf>
这个骨架文件,就是我们的起点。接下来,我们往里面填东西。
4.3 添加光源与地面
没有光,你啥也看不见。Gazebo默认会带一个环境光,但效果很差。我建议你至少加一个主光源,让场景有立体感。
4.3.1 添加光源
我个人习惯用 <light> 标签来定义光源。下面是一个典型的太阳光配置:
<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</diffuse>
<specular>0.2 0.2 0.2 1</specular>
<attenuation>
<range>1000</range>
<constant>0.9</constant>
<linear>0.01</linear>
<quadratic>0.001</quadratic>
</attenuation>
<direction>-0.5 0.1 -0.9</direction>
</light>
这里有几个参数我解释一下:
- type:
directional是平行光,模拟太阳。还有point(点光源)和spot(聚光灯)。 - cast_shadows:一定要设为
true,否则物体没有影子,看着很假。 - diffuse:漫反射颜色。RGB值,范围0-1。0.8 0.8 0.8 是暖白色。
- direction:光线方向。这个向量指向光源照射的方向。我一般调成斜45度,效果最自然。
我的小技巧: 如果你发现场景太暗或太亮,优先调 diffuse 的亮度值,别去动 attenuation(衰减)。衰减参数主要是给点光源用的,平行光基本用不上。
4.3.2 添加地面
地面其实就是一个巨大的平面模型。Gazebo提供了一个内置的 ground_plane 模型,直接用就行。
<include>
<uri>model://ground_plane</uri>
</include>
就这么简单?嗯,就这么简单。但如果你想要一个带纹理的地面,比如木地板或者水泥地,那就得自己定义 <visual> 了。不过对于抓取任务,默认的灰色地面完全够用。
注意: 地面模型一定要放在 <world> 标签的最前面。因为Gazebo加载模型是有顺序的,地面作为“基底”,应该最先被加载。我曾经因为把地面放在最后,导致其他模型加载时悬空报错,排查了半天。
4.4 导入静态物体
光有地面和机械臂还不够。我们要抓东西,总得有个桌子、几个箱子吧?这些物体在Gazebo里被称为“静态物体”(static objects)。
静态物体和动态物体的区别:
- 静态物体:不受重力影响,不会移动。比如桌子、墙壁、传送带。
- 动态物体:受物理引擎影响,可以被推动、抓取。比如被抓的零件。
4.4.1 添加一个桌子
我习惯用SDF格式直接定义桌子的几何体。下面是一个简单的桌子模型:
<model name="table">
<static>true</static>
<link name="table_link">
<collision name="collision">
<geometry>
<box>
<size>1.0 0.8 0.05</size>
</box>
</geometry>
</collision>
<visual name="visual">
<geometry>
<box>
<size>1.0 0.8 0.05</size>
</box>
</geometry>
<material>
<ambient>0.5 0.3 0.1 1</ambient>
<diffuse>0.8 0.5 0.2 1</diffuse>
</material>
</visual>
<pose>0 0 0.75 0 0 0</pose>
</link>
</model>
这里的关键点:
- <static>true</static>:告诉Gazebo,这个物体是固定的,别用物理引擎去算它。
- pose:桌面的高度是0.75米。为什么?因为机械臂的基座高度通常是0,桌面设在0.75米,机械臂刚好能轻松够到。
- collision 和 visual:碰撞体和视觉体。我一般让它们保持一致,避免出现“看得见摸不着”或者“摸得着看不见”的诡异情况。
4.4.2 添加被抓物体
抓取任务,总得有个东西抓吧?我放一个红色的小方块在桌面上:
<model name="target_box">
<static>false</static>
<link name="box_link">
<inertial>
<mass>0.1</mass>
<inertia>
<ixx>0.0001</ixx>
<ixy>0</ixy>
<ixz>0</ixz>
<iyy>0.0001</iyy>
<iyz>0</iyz>
<izz>0.0001</izz>
</inertia>
</inertial>
<collision name="collision">
<geometry>
<box>
<size>0.05 0.05 0.05</size>
</box>
</geometry>
</collision>
<visual name="visual">
<geometry>
<box>
<size>0.05 0.05 0.05</size>
</box>
</geometry>
<material>
<ambient>0.8 0.1 0.1 1</ambient>
<diffuse>0.9 0.2 0.2 1</diffuse>
</material>
</visual>
<pose>0.3 0 0.78 0 0 0</pose>
</link>
</model>
注意看,这个物体的 <static> 是 false,而且有 <inertial>(惯性参数)。这意味着它会被物理引擎影响,可以被机械臂抓起来、推走。
避坑指南: 惯性参数 <inertia> 一定要算对。对于一个小方块,质量0.1kg,边长0.05m,惯性矩大概就是 0.1 * (0.05^2) / 6 ≈ 0.0000417。我上面写的0.0001是个近似值。如果你设得太小,物体会像羽毛一样飘;设得太大,又像铅块一样推不动。我曾经因为惯性矩设错,导致夹爪一碰物体,物体就飞出去了,场面一度非常尴尬。
4.5 设置物理引擎参数
物理引擎是Gazebo的“心脏”。它负责计算碰撞、摩擦力、重力等等。Gazebo默认使用ODE引擎,但你也换成Bullet或DART。
我个人推荐用ODE,因为它最稳定,社区支持也最好。下面是一组我常用的物理参数:
<physics type="ode">
<real_time_update_rate>1000</real_time_update_rate>
<max_step_size>0.001</max_step_size>
<real_time_factor>1</real_time_factor>
<ode>
<solver>
<type>quick</type>
<iters>50</iters>
<sor>1.3</sor>
<use_dynamic_moi_rescaling>false</use_dynamic_moi_rescaling>
</solver>
<constraints>
<cfm>0.0</cfm>
<erp>0.2</erp>
<contact_max_correcting_vel>100</contact_max_correcting_vel>
<contact_surface_layer>0.001</contact_surface_layer>
</constraints>
</ode>
</physics>
这些参数什么意思?我挑几个重点讲:
| 参数 | 作用 | 我的建议值 |
|---|---|---|
real_time_update_rate |
物理引擎每秒更新次数 | 1000(越高越精确,但CPU压力大) |
max_step_size |
每一步的时间步长(秒) | 0.001(与1000Hz对应) |
real_time_factor |
仿真速度倍率(1=实时) | 1(除非你要加速仿真) |
iters |
求解器迭代次数 | 50(越大越稳定,但越慢) |
sor |
超松弛因子 | 1.3(1.0-1.5之间) |
cfm |
约束力混合参数 | 0.0(设大了物体会“软”) |
erp |
误差修正参数 | 0.2(设大了物体会“抖”) |
我的经验: 如果你发现机械臂抓取时物体抖动得很厉害,先别急着调PID。试试把 erp 降到0.1,或者把 iters 提高到100。很多时候,物理参数比控制参数更能解决问题。
4.6 完整的world文件示例
好了,我们把上面所有的东西拼在一起,就是一个完整的 pick_place.world 文件了。结构如下:
<?xml version="1.0" ?>
<sdf version="1.6">
<world name="pick_place_world">
<!-- 光源 -->
<light type="directional" name="sun">
...
</light>
<!-- 地面 -->
<include>
<uri>model://ground_plane</uri>
</include>
<!-- 桌子(静态) -->
<model name="table">
<static>true</static>
...
</model>
<!-- 被抓物体(动态) -->
<model name="target_box">
<static>false</static>
...
</model>
<!-- 物理引擎参数 -->
<physics type="ode">
...
</physics>
</world>
</sdf>
把这个文件保存好,然后在启动文件里引用它:
roslaunch gazebo_ros empty_world.launch world_name:=/path/to/your/pick_place.world
启动后,你应该能看到一个带光照、有地面、有桌子、桌子上有个红色小方块的仿真环境。机械臂就位后,就可以开始抓取了。
4.7 本章知识体系
为了让你更直观地理解这一章的内容,我画了一张图:
嗯,这一章的内容就到这里。环境搭好了,下一章我们就可以真正开始写抓取逻辑了。不过在那之前,我建议你先把world文件跑起来,亲手调一调光源角度、物体位置,感受一下参数变化对仿真效果的影响。纸上得来终觉浅,绝知此事要躬行嘛。