4. 创建第一艘智能船舶模型
在ROS2智能船舶仿真中,船模是整个系统的物理基础。URDF(Unified Robot Description Format)用一棵XML树描述船体的连杆(link)、关节(joint)、几何外形、质量与惯性张量,以及推进器、舵等执行机构的连接关系。本节以一艘小型双推进器带舵的无人艇为教学对象,从零写出一个可在Gazebo中加载的船模,并说明每个参数为什么这样取、加载后如何判断模型是否正确。
一、先确定坐标系与船体尺寸约定
建模前必须先固定坐标约定,否则后续惯性参数、关节轴方向和推进力方向都会混乱。本节采用ROS标准约定:X轴指向船艏,Y轴指向左舷,Z轴向上,原点位于船体几何中心处的设计水线面。船体主尺度取教学示例值:总长 L=2.0 m,型宽 B=0.8 m,型深 H=0.35 m,设计吃水 d=0.15 m。船体用长方体近似(后续章节可替换为更精细的网格),其几何中心位于原点,因此船体link的视觉与碰撞几何都写成 size="2.0 0.8 0.35",中心偏移为0。
需要特别说明:长方体近似只用于入门阶段验证URDF流程与Gazebo加载,其流体动力特性与真实船型差异很大,不能用于操纵性定量分析。这一点在后续引入水动力插件时必须替换。
二、质量、质心与惯性张量
URDF中每个link必须给出 <inertial>,包含质量、质心位置和3×3惯性张量(只填对角线ixx、iyy、izz及惯性积ixy、ixz、iyz,对称阵)。质量取船体空载质量 m=25 kg。质心假设位于原点略偏下,取 origin xyz="0 0 -0.02",模拟设备布置偏低带来的稳心改善。
对均质长方体,绕质心轴的惯性矩为:
Ixx = m/12 * (B² + H²) = 25/12 * (0.8² + 0.35²) ≈ 1.59 kg·m²
Iyy = m/12 * (L² + H²) = 25/12 * (2.0² + 0.35²) ≈ 8.59 kg·m²
Izz = m/12 * (L² + B²) = 25/12 * (2.0² + 0.8²) ≈ 9.67 kg·m²
由于船体近似为对称长方体,惯性积ixy、ixz、iyz均取0。注意Izz(绕竖轴,即艏摇惯量)明显大于Ixx(横摇惯量),这符合细长船体的物理直觉,也是后续观察船体转向响应快慢的依据。若把船体质量随意写成1 kg或惯性全填1,Gazebo中船会表现得像纸片,一推就翻,这是初学者最常见的错误。
三、推进器与舵的关节定义
本节采用“左右两个推进器+单舵”的布置。推进器不建模为真实螺旋桨叶片,而用一个短圆柱表示桨毂与推力方向指示,通过连续关节(continuous)绕自身轴线旋转,转速由 ros2_control 或Gazebo插件驱动;舵用转动关节(revolute),限定在±35°范围内摆动。
左推进器安装在船艉 xyz="-0.95 0.25 -0.10",右推进器镜像为 xyz="-0.95 -0.25 -0.10",关节轴均为 axis xyz="1 0 0"(绕X轴旋转,即桨盘垂直于船艏方向)。舵安装在 xyz="-1.0 0 -0.12",关节轴为 axis xyz="0 0 1"(绕Z轴摆动,改变舵角)。关节的 parent 均为 base_link,child 分别为 prop_left、prop_right、rudder。
舵关节需写限位:<limit lower="-0.61" upper="0.61" effort="20" velocity="2.0"/>,其中0.61 rad约等于35°。推进器连续关节无角度限位,但应给出 effort 与 velocity,例如 effort="5" velocity="100",表示最大驱动力矩5 N·m、最高转速约100 rad/s(约955 rpm),这些是教学示例值,实际应按所选电机与桨匹配确定。
<inertial>,哪怕质量很小(如0.3 kg)。Gazebo对缺少惯性的link会报警告,甚至导致关节行为异常。四、在Gazebo中加载与观察
URDF本身不含仿真属性,需要在文件末尾追加 <gazebo> 标签:为 base_link 指定材质颜色,为船体添加浮力与阻尼插件(入门阶段可先用 libgazebo_ros_buoyancy.so 或简化的位姿控制),并为推进器关节挂接 libgazebo_ros_joint_velocity_controller.so。随后用以下命令启动:
ros2 launch my_boat_description boat_gazebo.launch.py
# 或手动加载
ros2 run gazebo_ros spawn_entity.py -entity smart_boat \
-file install/my_boat_description/share/my_boat_description/urdf/boat.urdf
加载后按以下顺序观察判断:第一,在Gazebo左侧模型树中确认 smart_boat 下出现 base_link、prop_left、prop_right、rudder 四个link,说明URDF树解析成功;第二,用 ros2 run tf2_tools view_frames 检查关节变换是否连通,若某关节断开,通常是parent/child写错或link名拼写不一致;第三,在Gazebo中给推进器关节发速度指令,观察船体是否沿X轴前进、左右推进器差速时是否产生艏摇,舵角变化时航向是否随之改变。若船体原地打转或下沉穿地,应优先检查惯性张量是否为零、碰撞几何是否与视觉几何一致、浮力插件参数是否匹配质量。
五、一个可直接参考的URDF骨架
把上述参数落到文件里,核心结构如下(省略了visual/collision的重复细节):
<robot name="smart_boat">
<link name="base_link">
<inertial>
<origin xyz="0 0 -0.02"/>
<mass value="25.0"/>
<inertia ixx="1.59" iyy="8.59" izz="9.67"
ixy="0" ixz="0" iyz="0"/>
</inertial>
<visual><geometry><box size="2.0 0.8 0.35"/></geometry></visual>
<collision><geometry><box size="2.0 0.8 0.35"/></geometry></collision>
</link>
<link name="prop_left">
<inertial><mass value="0.3"/>
<inertia ixx="0.001" iyy="0.001" izz="0.001"
ixy="0" ixz="0" iyz="0"/></inertial>
</link>
<joint name="prop_left_joint" type="continuous">
<parent link="base_link"/><child link="prop_left"/>
<origin xyz="-0.95 0.25 -0.10"/>
<axis xyz="1 0 0"/>
<limit effort="5" velocity="100"/>
</joint>
<joint name="rudder_joint" type="revolute">
<parent link="base_link"/><child link="rudder"/>
<origin xyz="-1.0 0 -0.12"/>
<axis xyz="0 0 1"/>
<limit lower="-0.61" upper="0.61" effort="20" velocity="2.0"/>
</joint>
</robot>
右推进器与舵的link定义方式相同,只需镜像Y坐标。写完后用 check_urdf boat.urdf 做语法与树结构校验,输出中应看到 base_link 下挂三个子link,且无“link not connected”提示。
六、常见问题与判断依据
加载后若模型在Gazebo中“抖动”或“缓慢下沉”,通常不是URDF语法问题,而是质量与浮力不匹配:25 kg船体需要排开约0.025 m³水体,若浮力插件体积参数写错,船会持续下沉。若推进器转动但船不动,检查关节是否被Gazebo插件正确接管、effort 是否过小、以及碰撞几何是否与地面或水面发生穿透。若船体一转向就侧翻,多半是Ixx过小或质心过高,应复核惯性张量与质心Z偏移。把这些现象与参数对应起来,才算真正完成了“加载并观察”这一环节,而不是仅仅看到模型出现在场景里。