前言
这是一个系列小文章,主要介绍在ROS-Gazebo中如何更好地使用SDF格式建模与仿真。众所周知,URDF是ROS的原生支持格式,但在某些情况下(尤其是Gazebo仿真时),使用SDF格式会更加合理。鉴于中文网络上几乎没有成体系的SDF介绍博文,因此我将自己在硕士期间关于SDF模型的使用经验稍作总结。如有谬误,还望友好地指出。
本系列规划如下内容,建议按顺序阅读。尤其是第一篇,以确认你是否有必要使用SDF建模:
- 为什么选择SDF?
- SDF规范文档与模型资源
- SDF的建模与使用
- ERB:模块化生成SDF模型
- SDF与URDF的相互转换
- 基于SDF的多机器人仿真
- 使用与编写自定义插件——以PID控制插件为例
1 ERB是什么?为什么要使用ERB?
熟悉URDF格式的小伙伴们都知道,我们在编写大型URDF文件时,一般不直接在URDF中操作,而是编写对应的.xacro。xacro可以定义宏变量,并将代码模块化,既减少了代码量,又节省了建模、调试时间。
那么,SDF模型可不可以使用xacro来编写呢?很遗憾,答案是否定的。然而好消息是,SDF也有可以使用的替代方法——嵌入式Ruby(Embedded Ruby),简称ERB。ERB常用来生成Web页面,但也可以用来生成诸如XML、RSS这样的结构化文件。
什么时候需要使用ERB呢?当我们的模型十分庞大复杂,尤其是存在许多相似关节,例如多个车轮的移动机器人、模块化机械臂、对称结构的机器人等,都可以采用ERB方法,既减少了代码量,又可参数化地调整相关变量,十分方便。
下面,我们就来看看怎样使用ERB来编写和生成SDF文件。
2 ERB基本语法
2.1 表达式
形如下面的代码块称为表达式,是由标签和等号组成的封闭代码。表达式用来将代码直接嵌入模板,或将某个变量替换为相应的值。下面的代码将会在渲染时被替换为name
变量所代表的内容。
<%= @name %>
2.2 脚本
形如下面的代码块称为脚本,是由标签(而没有等号)组成的不封闭代码。脚本的运行结果将会被插入最终的代码。下面的代码将会执行一个循环,将shopping_list
中的所有元素依次插入代码中。
<% for @item in @shopping_list %>
<%= @item %>
<% end %>
默认情况下,每个标签后面都会添加一个换行符。如果不想要换行符,可以通过如下方法抑制换行符:
<% for @item in @items -%>
<%= @item %>
<% end -%>
2.3 注释
用如下方法添加注释:
<%# This is just a comment %>
3 使用ERB生成SDF模型
Gazebo官方模型库中,有一部分模型就是用ERB生成的。这里直接借用一个汽车模型来说明ERB的使用方法。模型的链接和图片如下,这是一个四轮小车:官方模型:cart_front_steer
首先看看模型文件夹中有哪些内容,其中model.config
和model.sdf
分别是配置信息和模型文件,是SDF模型中必须存在的两个文件。model.rsdf 是为了模块化生成SDF而建立的ERB代码文件,也就是说,我们的操作对象是model.rsdf,而不是model.sdf。
图1 常用模型文件夹中的内容
对照.rsdf和.sdf文件,可以看到ERB语法在建模中主要有两种用法:表达式嵌入变量、脚本嵌入代码块。嵌入变量可以减少我们后期参数调试的工作量,嵌入代码块可以减少模型的代码量。
3.1 用表达式嵌入变量
在.rsdf中,首先用一个不封闭的标签<% %>
定义需要用到的值。可以看到,数值既可以定义为常量,也可以定义为变量表达式。在示例代码中,位置坐标定义为常量,惯量定义为变量表达式:
<%
...
# Geometry
chassis_dx = 1.0
...
# inertia
chassis_ixx = chassis_mass/12.0 * (chassis_dy**2 + chassis_dz**2)
...
%>
在接下来开始写模型的代码,用表达式<%= name %>
的形式嵌入了先前定义的变量。如下代码嵌入了惯性矩阵在xx方向的元素:
...
<inertia>
<ixx><%= chassis_ixx %></ixx>
...
3.2 用脚本嵌入代码块
除了直接嵌入变量外,还可以用脚本嵌入代码块。尤其是使用循环脚本,可以生成大量相似的link和joint。
首先还是在文件开头的不封闭的标签<% %>
中定义了前轮和后轮的位置:
<%
...
wheel_x0 = chassis_dx*0.5
wheel_y0 = chassis_dy*0.5 + wheel_width*0.6
...
front_wheel_locations = {
"front_left" => {:x0 => wheel_x0, :y0 => wheel_y0 },
"front_right" => {:x0 => wheel_x0, :y0 => -wheel_y0 },
}
rear_wheel_locations = {
"rear_left" => {:x0 => -wheel_x0, :y0 => wheel_y0 },
"rear_right" => {:x0 => -wheel_x0, :y0 => -wheel_y0 },
}
...
%>
接下来在模型代码中使用了2个each..do..循环,分别创建了两个前轮和后轮的link和joint。下面展示了创建前轮的循环:
<%
front_wheel_locations.keys.each do |k|
x0 = front_wheel_locations[k][:x0]
y0 = front_wheel_locations[k][:y0]
%>
<%= "<link name=" + '"wheel_' + k + '">' %>
<pose><%= x0 %> <%= y0 %>
...
<%= "<joint name=" + '"wheel_' + k + '_steer_spin" type="universal">' %>
...
<% end %>
从model.sdf文件中对照,可以看到上述代码分别创建了wheel_front_left、wheel_front_right两个link,以及wheel_front_left_steer_spin和wheel_front_left_steer_spin两个joint。
3.3 从.rsdf生成.sdf
当rsdf编写完毕后,在终端中运行如下命令即可生成对应的.sdf文件。注意:这一步是必须的,因为我们最终使用的是sdf文件,而不是rsdf。
erb model.rsdf > model.sdf
采用这种方法所生成的SDF文件,和我们直接编写的SDF没有任何区别。如果后续想要批量修改某个数值,只需要在.rsdf中修改相应的变量,再重新生成SDF即可。
参考资料
[1] Gazebo:模型结构教程
[2] erb简介
[3] Gazebo官方模型