第一次接触机器人仿真时,我被URDF文件里那些密密麻麻的XML标签搞得头晕眼花。直到亲手搭建了第一个小车模型,才发现这就像搭积木一样有趣。URDF(Unified Robot Description Format)本质上就是用XML语法描述机器人结构的标准格式,你可以把它想象成乐高说明书 - 告诉系统每个零件长什么样、怎么连接。
先来看个最简单的例子:一个长方体车身加两个圆柱形轮子的小车。URDF的核心是<link>和joint>这对黄金搭档。<link>定义实体部件,比如车身和轮子;<joint>则像关节一样把这些部件连接起来。下面这段代码定义了一个蓝色车身:
xml复制<link name="base_link">
<visual>
<geometry>
<box size="0.5 0.5 0.25"/>
</geometry>
<material name="blue">
<color rgba="0 0.5 1 1"/>
</material>
</visual>
</link>
新手最常踩的坑就是忘记写碰撞属性。在RViz里看着好好的模型,一到Gazebo就穿模,八成是因为<collision>标签没写或者和<visual>不匹配。我的经验法则是:每个<visual>块都要配一个相同的<collision>块,就像这样:
xml复制<collision>
<geometry>
<box size="0.5 0.5 0.25"/>
</geometry>
</collision>
轮子的定义稍微复杂些,因为需要设置转动轴。这里有个实用技巧:用<origin rpy="1.570795 0 0"/>把圆柱体旋转90度,让它像真实轮子一样横置。joint的<axis>参数更要特别注意 - 设置xyz="0 1 0"表示绕Y轴旋转,这是差速小车的标准配置。
好不容易在RViz里看到漂亮的小车模型,拖进Gazebo却变成一坨灰色几何体?这是因为Gazebo需要的是SDF格式。别担心,不需要重头再来,URDF转SDF就像翻译文档一样简单。
最关键的秘诀是在URDF里添加<gazebo>标签。这些标签专门告诉Gazebo如何处理材质和物理特性。比如要让车身显示蓝色,需要在</robot>结束前添加:
xml复制<gazebo reference="base_link">
<material>Gazebo/Blue</material>
</gazebo>
转换命令简单得惊人:
bash复制gz sdf -p my_robot.urdf > my_robot.sdf
但这里有个隐藏陷阱:Gazebo对单位制非常敏感。我有次模型在空中乱飞,查了半天发现是质量单位设成了克而不是千克。建议在<inertial>块里先用保守值:
xml复制<inertial>
<mass value="5"/>
<inertia ixx="0.13" ixy="0" ixz="0" iyy="0.21" iyz="0" izz="0.13"/>
</inertial>
为了让模型能被Gazebo正常调用,还需要创建模型包。标准的目录结构应该是:
code复制my_robot/
├── model.config
├── my_robot.sdf
└── meshes/ (可选)
把整个文件夹放到~/.gazebo/models/下,你的小车就会出现在Gazebo的插入模型列表里了。我建议先用Gazebo自带的模型编辑器测试一下,能正常拖拽再继续开发插件。
模型能显示只是第一步,要让小车动起来还得靠插件。Gazebo插件就像机器人的大脑,我用C++写的控制插件大概200行代码,核心是处理ROS消息和更新模型状态。
先看插件的基本骨架:
cpp复制#include <gazebo/gazebo.hh>
#include <gazebo/physics/physics.hh>
#include <ros/ros.h>
namespace gazebo {
class MyRobotPlugin : public ModelPlugin {
public:
void Load(physics::ModelPtr _model, sdf::ElementPtr _sdf) {
this->model = _model;
// ROS初始化代码...
}
private:
physics::ModelPtr model;
};
GZ_REGISTER_MODEL_PLUGIN(MyRobotPlugin)
}
最关键的Load函数里要做三件事:
我强烈建议用多线程处理ROS消息,否则Gazebo会卡顿。下面这段代码创建了一个独立的消息处理线程:
cpp复制this->rosQueueThread = std::thread(
std::bind(&MyRobotPlugin::QueueThread, this));
对应的线程函数要这样写:
cpp复制void QueueThread() {
while (this->rosNode->ok()) {
this->rosQueue.callAvailable(ros::WallDuration(0.01));
}
}
速度控制逻辑要放在OnUpdate函数里,这个函数会在每次物理引擎更新时自动调用。设置速度的代码出乎意料的简单:
cpp复制void OnUpdate() {
this->model->SetLinearVel(ignition::math::Vector3d(speed, 0, 0));
}
编译插件时需要特别注意CMakeLists.txt的配置。必须链接Gazebo和ROS的库:
cmake复制find_package(gazebo REQUIRED)
find_package(roscpp REQUIRED)
add_library(my_plugin SHARED my_plugin.cpp)
target_link_libraries(my_plugin ${GAZEBO_LIBRARIES} ${roscpp_LIBRARIES})
插件写好之后,就可以通过ROS话题来控制小车了。我设计了一个简单的控制方案:订阅/cmd_vel话题,接收速度指令。
首先在插件里创建订阅者:
cpp复制ros::SubscribeOptions so =
ros::SubscribeOptions::create<std_msgs::Float32>(
"/cmd_vel", 1,
boost::bind(&MyRobotPlugin::OnRosMsg, this, _1),
ros::VoidPtr(), &this->rosQueue);
this->rosSub = this->rosNode->subscribe(so);
对应的回调函数将速度值存储到成员变量:
cpp复制void OnRosMsg(const std_msgs::Float32ConstPtr &_msg) {
this->target_speed = _msg->data;
}
测试时可以手动发布指令:
bash复制rostopic pub /cmd_vel std_msgs/Float32 "data: 0.5"
为了让整个系统跑起来,需要准备一个世界文件:
xml复制<sdf version="1.6">
<world name="test_world">
<include>
<uri>model://sun</uri>
</include>
<include>
<uri>model://ground_plane</uri>
</include>
<model name="my_robot">
<include>
<uri>model://my_robot</uri>
</include>
<plugin name="controller" filename="libmy_plugin.so"/>
</model>
</world>
</sdf>
启动时建议开三个终端:
roscoregazebo my_world.worldrostopic pub /cmd_vel ...调试时如果遇到模型乱飞,先检查这些参数:
在Gazebo中调试物理模型就像在冰面上走路 - 一不小心就会滑进坑里。这里分享几个血泪教训:
材质问题:Gazebo不会自动转换URDF的颜色定义。必须为每个<material>添加对应的<gazebo>标签,而且只能用Gazebo内置的材质名。我常用的有:
单位制混乱:URDF默认单位是米和千克,但Gazebo有时会抽风。建议所有数值都明确单位,比如:
xml复制<inertial>
<mass value="5"/> <!-- 千克 -->
<inertia ixx="0.1" ixy="0" ixz="0" iyy="0.1" iyz="0" izz="0.1"/> <!-- kg·m² -->
</inertial>
插件加载失败:最常见的原因是库路径问题。确保:
模型抖动问题:如果小车在地面上疯狂抖动:
xml复制<physics type="ode">
<max_step_size>0.001</max_step_size>
<real_time_factor>1</real_time_factor>
</physics>
ROS通信延迟:如果控制指令响应慢:
记得经常用gz topic -l查看Gazebo内部话题,用rostopic list检查ROS通信。当模型行为异常时,先简化问题 - 去掉所有插件,只用基础模型测试。