4. 构建你的第一个机器人

好,咱们正式开始动手了。

前面几章我们把Gazebo的界面、传感器、世界文件都过了一遍。说实话,光看不练容易犯困。今天咱们来点硬核的——用SDF搭一个两轮小车底盘。

你可能会问:为什么非要从底盘开始?

嗯,我个人习惯是,学任何仿真工具,先搞定一个能动的、看得见的东西。底盘就是机器人的骨架,你把它搭明白了,后面加传感器、加控制器,都是往上添砖加瓦的事。

4.1 机器人的基本构成:link和joint

在Gazebo里,一个机器人模型由两样东西组成:link(连杆)和joint(关节)。

link 就是机器人的一个刚体部件。比如一个轮子、一块底盘、一个机械臂的臂杆。每个link都有自己的物理属性:质量、惯性、碰撞形状、视觉外观。

joint 是连接两个link的约束。它决定了两个link之间怎么相对运动。可以固定不动,也可以旋转、平移。

说白了,link是骨头,joint是关节。骨头不会动,关节让骨头动起来。

核心概念:一个机器人模型必须至少有一个link(叫"base_link"),否则Gazebo会报错。joint可以没有,但link必须有。

我在项目中遇到过新手把link和joint搞混。有个同事把轮子直接写成了joint,结果Gazebo里显示一个旋转的圆环,根本没法用。记住:能摸到的是link,能转动的是joint。

4.2 SDF模型的结构骨架

一个完整的SDF机器人模型,长这样:

<model name="my_robot">
  <link name="base_link">
    <!-- 视觉、碰撞、惯性 -->
  </link>
  <link name="left_wheel">
    <!-- 视觉、碰撞、惯性 -->
  </link>
  <link name="right_wheel">
    <!-- 视觉、碰撞、惯性 -->
  </link>
  <joint name="left_wheel_joint" type="revolute">
    <parent>base_link</parent>
    <child>left_wheel</child>
    <!-- 关节位置、轴方向 -->
  </joint>
  <joint name="right_wheel_joint" type="revolute">
    <parent>base_link</parent>
    <child>right_wheel</child>
    <!-- 关节位置、轴方向 -->
  </joint>
</model>

你看,结构非常清晰。一个model下面,先定义所有link,再定义所有joint。joint里用<parent>和<child>指明谁连谁。

这里有个坑:parent和child的顺序不能反。parent是靠近机器人基座的link,child是远离基座的link。我曾经把轮子当parent、底盘当child,结果轮子拖着底盘跑,画面相当诡异。

4.3 两轮小车底盘的SDF实现

好,咱们直接写代码。下面是一个完整的两轮小车底盘SDF文件:

<?xml version="1.0" ?>
<sdf version="1.6">
  <model name="two_wheel_robot">
    <!-- 底盘 -->
    <link name="base_link">
      <pose>0 0 0.05 0 0 0</pose>
      <inertial>
        <mass>1.0</mass>
        <inertia>
          <ixx>0.01</ixx>
          <ixy>0</ixy>
          <ixz>0</ixz>
          <iyy>0.01</iyy>
          <iyz>0</iyz>
          <izz>0.01</izz>
        </inertia>
      </inertial>
      <collision name="base_collision">
        <geometry>
          <box>
            <size>0.4 0.3 0.1</size>
          </box>
        </geometry>
      </collision>
      <visual name="base_visual">
        <geometry>
          <box>
            <size>0.4 0.3 0.1</size>
          </box>
        </geometry>
        <material>
          <ambient>0.2 0.5 0.8 1</ambient>
        </material>
      </visual>
    </link>

    <!-- 左轮 -->
    <link name="left_wheel">
      <pose>-0.15 0.2 0.05 0 1.5708 0</pose>
      <inertial>
        <mass>0.2</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="left_wheel_collision">
        <geometry>
          <cylinder>
            <radius>0.05</radius>
            <length>0.04</length>
          </cylinder>
        </geometry>
      </collision>
      <visual name="left_wheel_visual">
        <geometry>
          <cylinder>
            <radius>0.05</radius>
            <length>0.04</length>
          </cylinder>
        </geometry>
        <material>
          <ambient>0.8 0.2 0.2 1</ambient>
        </material>
      </visual>
    </link>

    <!-- 右轮 -->
    <link name="right_wheel">
      <pose>-0.15 -0.2 0.05 0 1.5708 0</pose>
      <inertial>
        <mass>0.2</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="right_wheel_collision">
        <geometry>
          <cylinder>
            <radius>0.05</radius>
            <length>0.04</length>
          </cylinder>
        </geometry>
      </collision>
      <visual name="right_wheel_visual">
        <geometry>
          <cylinder>
            <radius>0.05</radius>
            <length>0.04</length>
          </cylinder>
        </geometry>
        <material>
          <ambient>0.2 0.8 0.2 1</ambient>
        </material>
      </visual>
    </link>

    <!-- 左轮关节 -->
    <joint name="left_wheel_joint" type="revolute">
      <parent>base_link</parent>
      <child>left_wheel</child>
      <pose>-0.15 0.2 0.05 0 0 0</pose>
      <axis>
        <xyz>0 0 1</xyz>
      </axis>
    </joint>

    <!-- 右轮关节 -->
    <joint name="right_wheel_joint" type="revolute">
      <parent>base_link</parent>
      <child>right_wheel</child>
      <pose>-0.15 -0.2 0.05 0 0 0</pose>
      <axis>
        <xyz>0 0 1</xyz>
      </axis>
    </joint>
  </model>
</sdf>

这段代码里,我做了几件事:

  • 底盘是一个长方体,长0.4米、宽0.3米、高0.1米,颜色是蓝色
  • 两个轮子是圆柱体,半径0.05米、厚度0.04米,左红右绿
  • 轮子通过revolute关节(旋转关节)连接到底盘
  • 关节的旋转轴是Z轴(0 0 1),这样轮子可以前后滚动

你可能会注意到,轮子的pose里有个1.5708。那是90度的弧度值。因为圆柱体默认是竖着的,我需要把它横过来当轮子用。

小技巧:SDF里的角度单位是弧度,不是度。90° = 1.5708 rad,180° = 3.1416 rad。记不住没关系,我每次都是现算的。

4.4 在Gazebo中查看模型结构

文件写好了,怎么验证?

把上面的代码保存为two_wheel_robot.sdf,然后启动Gazebo:

gazebo --verbose two_wheel_robot.sdf

如果一切正常,你会看到一个小车底盘出现在世界里。用鼠标滚轮拉近看看,底盘是蓝色的长方体,两个轮子一红一绿。

Gazebo左侧的Model Tree面板里,你能看到模型的层级结构:

  • two_wheel_robot
    • base_link
    • left_wheel
    • right_wheel
    • left_wheel_joint
    • right_wheel_joint

点开每个link,还能看到它的碰撞形状、视觉形状、惯性参数。这就是Gazebo的模型浏览器,非常直观。

注意:如果模型加载后轮子掉在地上或者位置不对,八成是pose写错了。检查一下轮子的位置是不是相对于底盘,而不是相对于世界坐标系。

4.5 本章知识结构图

下面这张图帮你理清link和joint的关系:

SDF机器人模型结构 model (机器人模型) link (连杆) joint (关节) 视觉(visual) + 碰撞(collision) + 惯性(inertial) parent + child + axis 核心关系 link是刚体部件,joint定义link之间的运动约束 一个模型至少有一个link,joint可以有0个或多个

这张图把模型、link、joint的关系画得很清楚。你写SDF的时候,脑子里要有这个结构树。

4.6 常见问题与避坑

最后分享几个我踩过的坑:

  • 惯性参数不能全零:Gazebo要求每个link的惯性矩阵必须合理。如果你偷懒全写0,仿真会直接崩掉。至少给个很小的正数。
  • 碰撞形状和视觉形状可以不同:为了性能,碰撞形状可以简化。比如视觉用精细的网格模型,碰撞用一个长方体代替。
  • 关节类型选错:轮子用revolute,机械臂关节用revolute或prismatic,固定连接用fixed。别把轮子写成fixed,那样它转不了。

嗯,今天就到这。把上面那个SDF文件跑起来,在Gazebo里转转视角,看看模型树。动手试一次,比看十遍教程都管用。

可可精品资料库