4. 机器人建模与URDF:URDF语法详解、link与joint配置、在Gazebo中显示自定义机器人
好,咱们今天来聊聊机器人建模。说白了,就是给你的机器人画一张「骨架图」。在ROS2和Gazebo的世界里,这张骨架图就是URDF文件。我刚开始接触URDF时,觉得不就是个XML嘛,写起来应该不难。结果第一次在Gazebo里加载,机器人直接散架了——轮子飞了,身体塌了,像个喝醉的积木。嗯,从那以后,我再也不敢小看这个文件了。
核心认知:URDF不是代码,是描述。它告诉系统:你的机器人长什么样,关节在哪,能怎么动。
4.1 URDF语法——其实就两个核心标签
URDF的语法结构非常清晰。你想想看,一个机器人无非就是「身体部件」和「连接方式」。对应到URDF里,就是link和joint。
- link:描述一个刚体部件。比如底盘、轮子、摄像头支架。
- joint:描述两个link之间的连接关系。比如轮子怎么装在底盘上,能转还是不能转。
我个人习惯把link想象成「骨头」,joint想象成「关节」。骨头不会动,关节才能动。这个比喻很直观,你记一下。
4.1.1 link的配置
一个典型的link长这样:
<link name="base_link">
<visual>
<geometry>
<box size="0.4 0.3 0.1"/>
</geometry>
<material name="blue">
<color rgba="0 0 1 1"/>
</material>
</visual>
<collision>
<geometry>
<box size="0.4 0.3 0.1"/>
</geometry>
</collision>
<inertial>
<mass value="2.0"/>
<inertia ixx="0.01" ixy="0" ixz="0"
iyy="0.01" iyz="0"
izz="0.01"/>
</inertial>
</link>
这里有三块内容:
- visual:视觉显示,Gazebo里你看到的样子。说白了就是「好看」用的。
- collision:碰撞检测,物理引擎用它算碰撞。我踩过坑:有一次visual和collision尺寸不一致,机器人看着没撞到,但物理引擎认为撞了,直接弹飞。所以,collision尽量和visual保持一致。
- inertial:惯性参数,质量、转动惯量。Gazebo仿真物理效果全靠它。如果你不写,Gazebo会报warning,而且物理行为会非常诡异。
我的经验:inertial里的转动惯量ixx、iyy、izz,对于简单形状可以用公式估算。长方体就是 (1/12)*m*(h²+w²) 这种。别偷懒,否则机器人会像在冰面上走路。
4.1.2 joint的配置
joint是URDF的灵魂。没有joint,你的机器人就是一坨散件。来看一个轮子的joint:
<joint name="left_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="left_wheel_link"/>
<origin xyz="0.2 -0.25 0" rpy="0 0 0"/>
<axis xyz="0 1 0"/>
</joint>
关键点:
- type:关节类型。continuous是连续旋转(轮子),revolute是有限角度旋转(关节臂),fixed是固定连接(摄像头支架)。
- parent/child:父子关系。父link是底盘,子link是轮子。这个关系决定了运动学树的结构。
- origin:子link相对于父link的位置和姿态。xyz是平移,rpy是旋转(roll, pitch, yaw)。
- axis:旋转轴。对于轮子,绕Y轴转就是向前滚动。
注意:我曾经犯过一个低级错误——把parent和child写反了。结果轮子成了底盘的父亲,整个运动学树乱套,Gazebo直接崩溃。记住:父link是靠近机器人基座的,子link是末端。
4.2 从零搭建一个自定义机器人
好,理论说完了,咱们动手。我带你搭一个两轮差速底盘机器人。结构很简单:一个底盘 + 两个轮子 + 一个万向轮。
先画个图,理清思路:
这个图很清晰吧?base_link是根节点,两个驱动轮通过continuous joint连接,万向轮通过fixed joint连接。这就是一个完整的运动学树。
4.2.1 完整URDF代码
把上面的结构写成URDF,大概长这样:
<?xml version="1.0"?>
<robot name="my_robot">
<!-- 底盘 -->
<link name="base_link">
<visual>
<geometry>
<box size="0.4 0.3 0.1"/>
</geometry>
<material name="blue"/>
</visual>
<collision>
<geometry>
<box size="0.4 0.3 0.1"/>
</geometry>
</collision>
<inertial>
<mass value="2.0"/>
<inertia ixx="0.01" ixy="0" ixz="0"
iyy="0.01" iyz="0"
izz="0.01"/>
</inertial>
</link>
<!-- 左轮 -->
<link name="left_wheel_link">
<visual>
<geometry>
<cylinder radius="0.05" length="0.04"/>
</geometry>
<material name="black"/>
</visual>
<collision>
<geometry>
<cylinder radius="0.05" length="0.04"/>
</geometry>
</collision>
<inertial>
<mass value="0.2"/>
<inertia ixx="0.0001" ixy="0" ixz="0"
iyy="0.0001" iyz="0"
izz="0.0001"/>
</inertial>
</link>
<!-- 左轮关节 -->
<joint name="left_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="left_wheel_link"/>
<origin xyz="-0.15 -0.2 0" rpy="0 0 0"/>
<axis xyz="0 1 0"/>
</joint>
<!-- 右轮 -->
<link name="right_wheel_link">
<visual>
<geometry>
<cylinder radius="0.05" length="0.04"/>
</geometry>
<material name="black"/>
</visual>
<collision>
<geometry>
<cylinder radius="0.05" length="0.04"/>
</geometry>
</collision>
<inertial>
<mass value="0.2"/>
<inertia ixx="0.0001" ixy="0" ixz="0"
iyy="0.0001" iyz="0"
izz="0.0001"/>
</inertial>
</link>
<!-- 右轮关节 -->
<joint name="right_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="right_wheel_link"/>
<origin xyz="-0.15 0.2 0" rpy="0 0 0"/>
<axis xyz="0 1 0"/>
</joint>
<!-- 万向轮 -->
<link name="caster_wheel_link">
<visual>
<geometry>
<sphere radius="0.03"/>
</geometry>
<material name="gray"/>
</visual>
<collision>
<geometry>
<sphere radius="0.03"/>
</geometry>
</collision>
<inertial>
<mass value="0.1"/>
<inertia ixx="0.00001" ixy="0" ixz="0"
iyy="0.00001" iyz="0"
izz="0.00001"/>
</inertial>
</link>
<!-- 万向轮关节(固定) -->
<joint name="caster_wheel_joint" type="fixed">
<parent link="base_link"/>
<child link="caster_wheel_link"/>
<origin xyz="0.15 0 -0.05" rpy="0 0 0"/>
</joint>
<!-- 颜色定义 -->
<material name="blue">
<color rgba="0.2 0.4 0.8 1.0"/>
</material>
<material name="black">
<color rgba="0.1 0.1 0.1 1.0"/>
</material>
<material name="gray">
<color rgba="0.5 0.5 0.5 1.0"/>
</material>
</robot>
避坑指南:我曾经把万向轮的joint类型写成continuous,结果Gazebo里万向轮疯狂自转,机器人原地打转。万向轮应该是fixed,它只是被动滚动,不需要主动驱动。
4.3 在Gazebo中显示自定义机器人
URDF写好了,怎么让Gazebo看到它?这里有个关键步骤:你需要一个launch文件来加载URDF并启动Gazebo。
我个人习惯用robot_state_publisher来发布机器人状态,然后用spawn_entity.py把机器人放到Gazebo里。来看一个典型的launch文件:
import os
from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.actions import IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch_ros.actions import Node
def generate_launch_description():
# URDF文件路径
urdf_path = os.path.join(
get_package_share_directory('my_robot_description'),
'urdf',
'my_robot.urdf'
)
# 读取URDF内容
with open(urdf_path, 'r') as f:
robot_desc = f.read()
return LaunchDescription([
# 启动Gazebo
IncludeLaunchDescription(
PythonLaunchDescriptionSource([
os.path.join(
get_package_share_directory('gazebo_ros'),
'launch',
'gazebo.launch.py'
)
]),
),
# 发布机器人状态
Node(
package='robot_state_publisher',
executable='robot_state_publisher',
output='screen',
parameters=[{'robot_description': robot_desc}]
),
# 将机器人放入Gazebo
Node(
package='gazebo_ros',
executable='spawn_entity.py',
arguments=['-topic', 'robot_description', '-entity', 'my_robot'],
output='screen'
)
])
运行这个launch文件,Gazebo就会启动,你的机器人就会出现在仿真世界里。如果一切顺利,你会看到:
- 一个蓝色底盘
- 两个黑色轮子
- 一个灰色万向轮
- 机器人稳稳地站在地面上
常见问题:如果Gazebo里机器人直接掉到地面以下,或者悬浮在半空,检查两个地方:
- 万向轮的origin坐标是否正确?它应该接触地面。
- 碰撞模型是否和视觉模型一致?不一致会导致物理引擎计算错误。
我遇到过最离谱的一次——机器人直接穿模到地心里,后来发现是collision的尺寸写成了0。嗯,0尺寸的碰撞体,物理引擎直接忽略它。
4.4 本章小结
URDF是机器人建模的基石。说白了,它就是一张「零件清单 + 装配图」。你只要把link和joint配置好,Gazebo就能帮你把机器人「组装」出来。
我个人觉得,URDF最难的不是语法,而是空间想象。你要在脑子里把xyz坐标、rpy旋转想清楚。刚开始可以画个草图,就像我上面那张SVG图一样。等熟练了,直接写代码也不怕。
下一章,我们会给这个机器人加上传感器,让它能「看」到世界。但那是后话了,先把今天的URDF练熟。
核心要点回顾:
- link = 骨头(刚体部件),joint = 关节(连接关系)
- 每个link必须有visual、collision、inertial三部分
- joint的parent/child关系决定运动学树结构
- Gazebo显示需要robot_state_publisher + spawn_entity
可可精品资料库