4. URDF 核心元素详解:link 与 joint 的底层逻辑
各位同学,欢迎来到第四章。前几章我们把 URDF 的骨架搭起来了,也看到了一个能动的机器人模型。但说实话,那个模型还很“虚”——它没有质量,关节也转得随心所欲。这一章,我们来给机器人注入“物理灵魂”。
URDF 里最核心的两个元素,就是 <link> 和 <joint>。一个负责定义“有什么”,一个负责定义“怎么动”。我见过不少新手,把 link 当成纯视觉模型,把 joint 的坐标系搞得乱七八糟,结果模型在 Gazebo 里要么塌成一团,要么抖得像筛糠。咱们今天就把这些坑一个个填平。
4.1 link 的几何与惯性属性
link 在 URDF 里代表一个刚体。什么叫刚体?就是不会变形的物体。在 Gazebo 的物理引擎里,每个 link 都要有形状、有质量、有碰撞边界。缺了任何一样,仿真都会出问题。
一个完整的 link 定义,通常包含三个子标签:<visual>、<collision> 和 <inertial>。我习惯把它们叫做“三件套”。
4.1.1 visual:看得见的部分
<visual> 定义的是机器人的外观。它只影响显示,不影响物理。你可以把 link 画成一个圆柱体,但碰撞体用长方体——这在视觉上可能有点怪,但物理上完全可行。
<link name="base_link">
<visual>
<geometry>
<box size="0.4 0.3 0.1"/>
</geometry>
<origin xyz="0 0 0" rpy="0 0 0"/>
<material name="blue">
<color rgba="0 0 1 1"/>
</material>
</visual>
</link>
这里要注意 <origin> 标签。它定义的是几何体相对于 link 自身坐标系的偏移。默认是 (0,0,0),但如果你想让视觉模型偏离 link 的原点,就在这里调。我曾经犯过一个错误:把视觉原点设偏了,结果机器人在 Rviz 里看着是正的,一进 Gazebo 就乱转——因为碰撞体和视觉体没对齐。
4.1.2 collision:物理碰撞的边界
<collision> 的结构和 <visual> 几乎一样,但它决定了物理引擎如何计算接触和碰撞。这里有个关键点:碰撞几何体越简单,仿真越快。
<collision>
<geometry>
<cylinder radius="0.15" length="0.3"/>
</geometry>
<origin xyz="0 0 0.15" rpy="0 0 0"/>
</collision>
4.1.3 inertial:物理仿真的灵魂
这是最容易被人忽略的部分。没有 <inertial> 的 link,在 Gazebo 里会被当作质量为零的物体——物理引擎会直接忽略它,或者报错。
惯性属性包含三个要素:
| 参数 | 含义 | 单位 | 注意事项 |
|---|---|---|---|
mass |
质量 | kg | 不能为 0,建议根据实际零件估算 |
origin |
质心位置 | m | 相对于 link 坐标系,默认 (0,0,0) |
inertia |
惯性张量 | kg·m² | 6 个独立分量:ixx, ixy, ixz, iyy, iyz, izz |
<inertial>
<mass value="2.5"/>
<origin xyz="0 0 0.05" rpy="0 0 0"/>
<inertia ixx="0.01" ixy="0" ixz="0"
iyy="0.01" iyz="0" izz="0.02"/>
</inertial>
4.2 joint 的类型与父子关系
joint 是 URDF 里最灵活的部分。它定义了 link 之间的运动关系。一个 joint 连接一个 parent link 和一个 child link,构成运动链。
joint 有六种类型,但常用的只有四种:
| 类型 | 自由度 | 典型用途 | 运动范围 |
|---|---|---|---|
fixed |
0 | 固定连接,如相机支架 | 无 |
revolute |
1(旋转) | 关节电机,如机械臂肘部 | 有限角度,需设 limit |
continuous |
1(旋转) | 车轮、无限旋转关节 | 无限制 |
prismatic |
1(平移) | 线性滑轨、升降台 | 有限位移,需设 limit |
你可能会问:revolute 和 continuous 有什么区别?说白了,一个有限位,一个没限位。机械臂的关节不能转 360 度,所以用 revolute;车轮可以一直转,所以用 continuous。
4.2.1 父子关系的设计原则
URDF 的关节是单向的。parent 是靠近基座的 link,child 是远离基座的 link。这个关系决定了运动链的传递方向。
我建议你画一个树状图再写代码。比如一个简单的两轮小车:
- base_link(父)→ left_wheel_joint(子:left_wheel_link)
- base_link(父)→ right_wheel_joint(子:right_wheel_link)
- base_link(父)→ caster_joint(子:caster_link)
这个结构很清晰。但如果你把轮子挂在另一个轮子上,那运动学就会乱套。记住:每个 link 只能有一个父 joint,但可以有多个子 joint。
4.3 <origin> 与 <axis> 标签
这两个标签是 joint 的“定位器”和“方向器”。很多仿真异常,根源都在这里。
4.3.1 <origin>:关节的位置和姿态
<origin> 定义的是 child link 相对于 parent link 的变换。它包含 xyz(平移)和 rpy(旋转,单位弧度)。
<joint name="arm_joint" type="revolute">
<parent link="base_link"/>
<child link="arm_link"/>
<origin xyz="0.2 0 0.1" rpy="0 0 0"/>
<axis xyz="0 0 1"/>
<limit effort="100" velocity="1.0" lower="-1.57" upper="1.57"/>
</joint>
这里的 xyz="0.2 0 0.1" 表示:arm_link 的原点相对于 base_link 的原点,在 X 方向偏移 0.2 米,Z 方向偏移 0.1 米。rpy="0 0 0" 表示姿态不变。
<origin> 不是关节的旋转中心,而是 child link 坐标系的起点。关节的旋转轴穿过 child link 的原点,方向由 <axis> 指定。
4.3.2 <axis>:旋转或平移的方向
<axis> 定义的是关节的运动方向。对于旋转关节,它是旋转轴;对于平移关节,它是移动方向。
轴向量是相对于 child link 的坐标系。默认值是 0 0 1(Z 轴)。
| 轴向量 | 运动方向 | 常见场景 |
|---|---|---|
0 0 1 |
绕 Z 轴旋转 / 沿 Z 平移 | 腰部旋转、升降台 |
0 1 0 |
绕 Y 轴旋转 / 沿 Y 平移 | 肘部俯仰、横向滑轨 |
1 0 0 |
绕 X 轴旋转 / 沿 X 平移 | 肩部侧摆、纵向滑轨 |
我记得有一次调试一个六轴机械臂,第五关节怎么转都不对。查了半天,发现 <axis> 写成了 0 1 1——这是个非法向量,因为轴必须是单位向量。Gazebo 不会报错,但运动完全乱套。从那以后,我每次写 <axis> 都会在心里默念:归一化,归一化,归一化。
visual 设成细长的圆柱体,沿着轴方向拉伸。这样一眼就能看出关节朝哪转。
4.4 一个完整的 joint 示例
把上面所有知识点串起来,写一个完整的关节定义:
<joint name="shoulder_pan_joint" type="revolute">
<parent link="base_link"/>
<child link="shoulder_link"/>
<origin xyz="0 0 0.2" rpy="0 0 0"/>
<axis xyz="0 0 1"/>
<limit effort="50" velocity="2.0" lower="-3.14" upper="3.14"/>
<dynamics damping="0.7" friction="0.1"/>
</joint>
这个关节模拟了一个腰部旋转轴。parent 是基座,child 是肩部连杆。原点在基座上方 0.2 米处,绕 Z 轴旋转,限位 ±180 度。我还加了阻尼和摩擦参数——这在 Gazebo 里能让运动更真实。