1. 什么是URDF?

URDF(Unified Robot Description Format)ROS 专用的机器人描述文件格式。本质是一个 XML 文件,用来描述机器人:

  • 连杆 (link)
    • 其实代表一个坐标系 这个文章-章节2有详细描述
    • 可以包含 机器人各部分的“实体”,比如机械臂的臂段、轮子、底盘。
    • 可以包含长方体、圆柱体、球体,或者使用 STL/DAE 模型文件。
    • 包含几何体描述
      • 惯性(mass + inertia)、视觉几何(visual)、碰撞几何(collision)等信息。
  • 关节 (joint)
    • 连接不同连杆 (link)的运动学关系。
    • 其实是描述了坐标系之间的关系
    • 描述机器人连杆 (link)之间的连接方式:哪个连杆连接到哪个连杆,以及关节的类型(旋转、平移等)。
    • 描述旋转/平移类型、父子连杆、运动限制(关节角度/速度限制)
    • 定义父子关系,这样 TF2 可以生成正确的坐标系树。
  • 传感器、末端执行器
    • 可通过 或自定义 xacro 宏嵌入 URDF,指定位置和方向。
    • 以人体做类比,将眼睛比做传感器,描述眼睛的位置, 眼睛在头部前方 10 厘米,略微向下看 5°。
  • 视觉信息
    • 用于在仿真或 RViz 中显示机器人模型。
  • 惯性与碰撞信息
    • 描述连杆的质量及质量分布, 用于动力学计算。
    • 定义连杆的碰撞形状,用于仿真中的碰撞检测。

URDF 给控制器、tf2、传感器驱动提供统一结构信息,用于确定各传感器在机器人坐标系中的位置,方便数据融合。类似于Linux 设备树描述设备的行为。
简而言之,URDF 是 ROS 里机器人的 骨架+外壳+关节关系 的描述,为仿真、可视化和传感器集成提供基础。

2. 从零开始构建一个可视化的机器人模型

在本教程中,我们将从零开始构建一个外观上有点类似 R2D2 的机器人可视化模型。在后续的教程中,你将学习如何让模型具有关节运动、如何添加物理属性,以及如何使用 xacro 来生成更整洁的代码。但目前,我们的重点是 先把机器人的可视化几何结构搭建正确。

在继续之前,请确保你已经安装了 joint_state_publisher 包。
如果你安装的是 urdf_tutorial 的二进制版本,那么它通常已经包含该包。
如果没有,请更新安装并包含这个包(你可以用 rosdep 来检查依赖)。

本教程中提到的所有机器人模型(以及源文件)都可以在 urdf_tutorial 包中找到。

2.1 一个最简单的几何体(shape)

假如说,你可以用一个 box(立方体) 或 cylinder(圆柱体) 作为机器人唯一的“零件”,这样就能写出一个几乎最简化的 URDF 文件。

创建一个01-myfirst.urdf 文件, 内容如下

<?xml version="1.0"?>
<robot name="myfirst"> <!-- 定义机器人 -->
  <!-- 定义一个坐标,通常包含一个零件。
  该机器人只有一个零件, 名字叫base_link 坐标系-->
  <link name="base_link"> 
    <!-- <visual> 是用来定义机器人某个 link 在仿真或可视化软件(如 RViz、Gazebo)里是如何显示的。 -->
    <visual>
      <geometry> <!-- 几何形状 -->
        <!-- 用来定义一个圆柱体, 半径是 0.2 米, 高度是 0.6 米 -->
        <cylinder length="0.6" radius="0.2"/>
      </geometry>
    </visual>
  </link>
</robot>

看起来确实用了很多层标签,但请相信我,后面会变得更加复杂。

要查看该模型,请启动 display.launch.py 文件:

ros2 launch urdf_tutorial display.launch.py model:=urdf/01-myfirst.urdf

urdf_tutorial 是一个ros2 urdf demo 的 github 仓库ros2/urdf_tutorial,你也可以使用apt install ros-humble-urdf-tutorial 去安装。

display.launch.py 是一个 ROS 2 launch 文件,它的作用是方便你在 RViz2 中可视化 URDF 机器人模型。 具体来说,它做了几件事:

  • 加载指定的模型并传给 robot_state_publisher节点
    • 启动 robot_state_publisher Node。
    • robot_state_publisher 的作用,这个文章-章节2有详细描述
      • robot_state_publisher 根据 JointState 自动生成每个连杆相对于父连杆的 TF 变换,并发布到 /tf 话题。
      • 这样其他节点(如 RViz 或运动规划节点)就可以知道机器人在三维空间中的每个部分的位置。
  • 启动 RViz 并加载配置
    • RViz 是 ROS 的可视化工具,用来查看:
      • 机器人模型(URDF)
      • TF 坐标系
      • 传感器数据(激光、相机等)
      • 路径、轨迹、标记等
    • display.launch.py 会指定一个 RViz 配置文件(.rviz),用来:
      • 加载 URDF 机器人模型
      • 显示关节和 TF 坐标系
      • 设置视角和显示属性
    • 这样你就可以在 RViz 中实时看到机器人的姿态变化。

2.2 多个几何体(multiple shapes)

现在我们来看如何添加多个形状/连杆。如果我们只是向 URDF 中添加更多的 元素,解析器不会知道把它们放在哪里。因此,我们必须添加关节()。关节元素可以表示灵活的关节,也可以表示非灵活的关节。我们将从非灵活(即固定)关节开始。

<?xml version="1.0"?>
<robot name="multipleshapes">
  <link name="base_link">
    <visual>
      <geometry>
        <cylinder length="0.6" radius="0.2"/>
      </geometry>
    </visual>
  </link>

  <link name="right_leg">
    <visual>
      <geometry>
        <box size="0.6 0.1 0.2"/>
      </geometry>
    </visual>
  </link>

  <!-- 表示一个固定关节(type="fixed"),名字叫 base_to_right_leg,它把两个连杆连接起来 -->
  <!-- 因为关节类型是 fixed,所以子连杆相对于父连杆的位置和方向不会改变,也就是说 right_leg 会“死死地”挂在 base_link 上,不会旋转或移动。 -->
  <joint name="base_to_right_leg" type="fixed">
    <parent link="base_link"/>  <!-- 父连杆-->
    <child link="right_leg"/>  <!-- 子连杆-->
  </joint>

</robot>

这两个形状会互相重叠,因为它们共享相同的原点。如果我们不希望它们重叠,就必须为它们定义不同的原点。
在 URDF 中,默认情况下几何体(cylinder、box、sphere 等)的原点就在它的几何中心。

2.3 原点(Origins)

R2-D2 的两条腿不是插在肚子正中间,而是贴在身子侧面、靠上的位置。
所以我们得干三件事,让它在电脑里站得又稳又像:

  • 腿和身子怎么连?
    用一个 关节(joint) 把腿“焊”到身子侧面靠上的地方。
  • 不是连到腿的中间,而是连到腿的上半截!
    腿的“原点”不能在正中间,得往上挪一截(比如挪到大腿根部)。
  • 腿得站直,不能歪!
    → 给腿加个 90° 旋转,让它从“躺着”变成“站着”,脚掌朝下。

总结起来,URDF的原点有两种:

  • 1. 连杆link(坐标系)的原点
  • 2. link中几何体的原点

其分别对应不同的标签:

类型 说明 对应 URDF 位置
** link 的原点** 表示 坐标系本身在父坐标系中的位置与方向,即该 link 的坐标系相对于其父 link 的位姿。 <joint> 元素中的 <origin>
几何体的原点 表示 几何模型(可视或碰撞)相对于 link 原点的偏移与姿态,即几何体在该 link 坐标系下的局部位姿。 <visual> / <collision> 元素中的 <origin>
<?xml version="1.0"?>
<robot name="origins">
  <link name="base_link">
    <visual>
      <geometry>
        <cylinder length="0.6" radius="0.2"/>
      </geometry>
    </visual>
  </link>

  <link name="right_leg">
    <visual>
      <geometry>
        <box size="0.6 0.1 0.2"/>
      </geometry>
      <!-- <origin> 元素用于指定 link 的原点位置和方向。-->
      <!-- xyz表示 位置偏移(相对于link坐标系)。-->
	  <!-- x=0, y=0, z=-0.3 → 几何体中心在 link 原点下方 0.3m-->
	  <!--  rpy表示 旋转角度,使用 RPY(roll, pitch, yaw)欧拉角表示-->
	  <!--  roll = 0 → 绕 X 轴旋转 0-->
	  <!-- pitch = 1.57075 → 绕 Y 轴转 +90°(抬头)-->
	  <!-- yaw = 0 → 绕 Z 轴旋转 0-->
      <origin rpy="0 1.57075 0" xyz="0 0 -0.3"/>
    </visual>
  </link>

  <joint name="base_to_right_leg" type="fixed">
    <parent link="base_link"/>
    <child link="right_leg"/>
    <origin xyz="0 -0.22 0.25"/>
  </joint>

</robot>

Joint origin的作用:

  • joint origin 调整的是 父子 link(坐标系) 的姿态。
  • 决定子 link 的坐标系在父 link 坐标系中的位置和方向。
  • 例子:,表示腿子 link 在 base_link 的哪个位置“挂”上去。

以上的RUDF 描述中,我们先来看** 关节(joint)的原点** 。它是 相对于父link
(parent)的参考坐标系来定义的
。因此,我们在 y 方向移动 -0.22 米(向我们的左边,但相对于坐标轴是右边),在 z 方向移动 0.25 米(向上)。这意味着子链接(child link)的原点会位于父链接的右上方,不管子链接自身的 visual origin 是怎么定义的。由于我们没有指定 rpy(roll pitch yaw)属性,子链接的坐标系默认会与父链接的方向一致。

接下来, 看几何体的原点 , 即腿的 visual origin,它同时有 xyz 和 rpy 偏移。这定义了视觉元素的 几何中心在link 坐标系原点的相对位置 。由于我们希望腿挂在上方,我们通过设置 z 偏移为 -0.3 米,把视觉元素向下移动。并且,为了让腿的长边平行于 z 轴,我们将视觉元素绕 Y 轴旋转 π/2。
在这里插入图片描述
启动文件(launch file)会运行一些 ROS 2 节点,这些节点会根据你的 URDF 模型为每个 link 创建对应的 TF 坐标系。Rviz 利用这些 TF 坐标系来确定每个形状应该显示的位置。

如果某个 URDF link 没有对应的 TF 坐标系,那么在 Rviz 中它会被放置在原点,并显示为白色(表示未关联 TF 坐标系)。

2.3 <material> 标签

<material> 标签也可以用来改变颜色。

<?xml version="1.0"?>
<robot name="materials">
  <!-- 定义蓝色。-->
  <material name="blue">
    <color rgba="0 0 0.8 1"/>
  </material>
 <!-- 定义白色。-->
  <material name="white">
    <color rgba="1 1 1 1"/>
  </material>

  <link name="base_link">
    <visual>
      <geometry>
        <cylinder length="0.6" radius="0.2"/>
      </geometry>
      <!--引用上面定义的名为 "blue" 的material-->
      <material name="blue"/>
    </visual>
  </link>

  <link name="right_leg">
    <visual>
      <geometry>
        <box size="0.6 0.1 0.2"/>
      </geometry>
      <origin rpy="0 1.57075 0" xyz="0 0 -0.3"/>
      <!--引用上面定义的名为 "white" 的material-->
      <material name="white"/>
    </visual>
  </link>

  <joint name="base_to_right_leg" type="fixed">
    <parent link="base_link"/>
    <child link="right_leg"/>
    <origin xyz="0 -0.22 0.25"/>
  </joint>

  <link name="left_leg">
    <visual>
      <geometry>
        <box size="0.6 0.1 0.2"/>
      </geometry>
      <!--link origin定义这个几何形状在自己的 link 坐标系中的偏移和旋转
      我们希望长方体“脚”朝下、与 z 轴平行,这个旋转和偏移对于左右腿都是相同的-->
      <origin rpy="0 1.57075 0" xyz="0 0 -0.3"/>
      <material name="white"/>
    </visual>
  </link>

  <joint name="base_to_left_leg" type="fixed">
    <parent link="base_link"/>
    <child link="left_leg"/>
    <origin xyz="0 0.22 0.25"/>
  </joint>

</robot>

你也可以直接在 元素中定义材质,甚至在其他 link 中引用它。如果重新定义材质,也不会有问题。
在这里插入图片描述

2.4 完成这个模型

现在我们用几个额外的形状完成模型:脚、轮子和头部。最显著的是,我们添加了一个球体和一些网格(mesh)。我们还会添加一些后续会用到的其他部件。
创建05-visual.urdf文件。


<?xml version="1.0"?>
<robot name="visual">

  <material name="blue">
    <color rgba="0 0 0.8 1"/>
  </material>
  <material name="black">
    <color rgba="0 0 0 1"/>
  </material>
  <material name="white">
    <color rgba="1 1 1 1"/>
  </material>

  <link name="base_link">
    <visual>
      <geometry>
        <cylinder length="0.6" radius="0.2"/>
      </geometry>
      <material name="blue"/>
    </visual>
  </link>

  <link name="right_leg">
    <visual>
      <geometry>
        <box size="0.6 0.1 0.2"/>
      </geometry>
      <origin rpy="0 1.57075 0" xyz="0 0 -0.3"/>
      <material name="white"/>
    </visual>
  </link>

  <joint name="base_to_right_leg" type="fixed">
    <parent link="base_link"/>
    <child link="right_leg"/>
    <origin xyz="0 -0.22 0.25"/>
  </joint>

  <link name="right_base">
    <visual>
      <geometry>
        <box size="0.4 0.1 0.1"/>
      </geometry>
      <material name="white"/>
    </visual>
  </link>

  <joint name="right_base_joint" type="fixed">
    <parent link="right_leg"/>
    <child link="right_base"/>
     <!--关节点在right_leg 源点的向下0.6m处,也就是右腿的下面-->
    <origin xyz="0 0 -0.6"/>
  </joint>

  <link name="right_front_wheel">
    <visual>
      <origin rpy="1.57075 0 0" xyz="0 0 0"/>
      <geometry>
        <cylinder length="0.1" radius="0.035"/>
      </geometry>
      <material name="black"/>
    </visual>
  </link>
  <joint name="right_front_wheel_joint" type="fixed">
    <parent link="right_base"/>
    <child link="right_front_wheel"/>
    <origin rpy="0 0 0" xyz="0.133333333333 0 -0.085"/>
  </joint>

  <link name="right_back_wheel">
    <visual>
      <origin rpy="1.57075 0 0" xyz="0 0 0"/>
      <geometry>
        <cylinder length="0.1" radius="0.035"/>
      </geometry>
      <material name="black"/>
    </visual>
  </link>
  <joint name="right_back_wheel_joint" type="fixed">
    <parent link="right_base"/>
    <child link="right_back_wheel"/>
    <origin rpy="0 0 0" xyz="-0.133333333333 0 -0.085"/>
  </joint>

  <link name="left_leg">
    <visual>
      <geometry>
        <box size="0.6 0.1 0.2"/>
      </geometry>
      <origin rpy="0 1.57075 0" xyz="0 0 -0.3"/>
      <material name="white"/>
    </visual>
  </link>

  <joint name="base_to_left_leg" type="fixed">
    <parent link="base_link"/>
    <child link="left_leg"/>
    <origin xyz="0 0.22 0.25"/>
  </joint>

  <link name="left_base">
    <visual>
      <geometry>
        <box size="0.4 0.1 0.1"/>
      </geometry>
      <material name="white"/>
    </visual>
  </link>

  <joint name="left_base_joint" type="fixed">
    <parent link="left_leg"/>
    <child link="left_base"/>
    <origin xyz="0 0 -0.6"/>
  </joint>

  <link name="left_front_wheel">
    <visual>
      <origin rpy="1.57075 0 0" xyz="0 0 0"/>
      <geometry>
        <cylinder length="0.1" radius="0.035"/>
      </geometry>
      <material name="black"/>
    </visual>
  </link>
  <joint name="left_front_wheel_joint" type="fixed">
    <parent link="left_base"/>
    <child link="left_front_wheel"/>
    <origin rpy="0 0 0" xyz="0.133333333333 0 -0.085"/>
  </joint>

  <link name="left_back_wheel">
    <visual>
      <origin rpy="1.57075 0 0" xyz="0 0 0"/>
      <geometry>
        <cylinder length="0.1" radius="0.035"/>
      </geometry>
      <material name="black"/>
    </visual>
  </link>
  <joint name="left_back_wheel_joint" type="fixed">
    <parent link="left_base"/>
    <child link="left_back_wheel"/>
    <origin rpy="0 0 0" xyz="-0.133333333333 0 -0.085"/>
  </joint>

  <joint name="gripper_extension" type="fixed">
    <parent link="base_link"/>
    <child link="gripper_pole"/>
    <origin rpy="0 0 0" xyz="0.19 0 0.2"/>
  </joint>

  <link name="gripper_pole">
    <visual>
      <geometry>
        <cylinder length="0.2" radius="0.01"/>
      </geometry>
      <origin rpy="0 1.57075 0 " xyz="0.1 0 0"/>
    </visual>
  </link>

  <joint name="left_gripper_joint" type="fixed">
    <origin rpy="0 0 0" xyz="0.2 0.01 0"/>
    <parent link="gripper_pole"/>
    <child link="left_gripper"/>
  </joint>

  <link name="left_gripper">
    <visual>
      <origin rpy="0.0 0 0" xyz="0 0 0"/>
      <geometry>
        <!--指定了要加载的几何文件(l_finger.dae),这是一个 Collada 格式的三维模型。-->
        <mesh filename="package://urdf_tutorial/meshes/l_finger.dae"/>
      </geometry>
    </visual>
  </link>

  <joint name="left_tip_joint" type="fixed">
    <parent link="left_gripper"/>
    <child link="left_tip"/>
  </joint>

  <link name="left_tip">
    <visual>
      <origin rpy="0.0 0 0" xyz="0.09137 0.00495 0"/>
      <geometry>
        <!-- 左夹爪的指尖部分-->
        <mesh filename="package://urdf_tutorial/meshes/l_finger_tip.dae"/>
      </geometry>
    </visual>
  </link>
  <joint name="right_gripper_joint" type="fixed">
    <origin rpy="0 0 0" xyz="0.2 -0.01 0"/>
    <parent link="gripper_pole"/>
    <child link="right_gripper"/>
  </joint>

  <link name="right_gripper">
    <visual>
      <origin rpy="-3.1415 0 0" xyz="0 0 0"/>
      <geometry>
        <mesh filename="package://urdf_tutorial/meshes/l_finger.dae"/>
      </geometry>
    </visual>
  </link>

  <joint name="right_tip_joint" type="fixed">
    <parent link="right_gripper"/>
    <child link="right_tip"/>
  </joint>

  <link name="right_tip">
    <visual>
      <origin rpy="-3.1415 0 0" xyz="0.09137 0.00495 0"/>
      <geometry>
        <mesh filename="package://urdf_tutorial/meshes/l_finger_tip.dae"/>
      </geometry>
    </visual>
  </link>

  <link name="head">
    <visual>
      <geometry>
        <sphere radius="0.2"/>
      </geometry>
      <material name="white"/>
    </visual>
  </link>
  <joint name="head_swivel" type="fixed">
    <parent link="base_link"/>
    <child link="head"/>
    <origin xyz="0 0 0.3"/>
  </joint>

  <link name="box">
    <visual>
      <geometry>
        <box size="0.08 0.08 0.08"/>
      </geometry>
      <material name="blue"/>
    </visual>
  </link>

  <joint name="tobox" type="fixed">
    <parent link="head"/>
    <child link="box"/>
    <origin xyz="0.1814 0 0.1414"/>
  </joint>
</robot>

这里用到的网格(mesh)是从 PR2 借来的。它们是独立的文件,你必须通过路径来指定。你应该使用

package://包名/路径

这种 package://NAME_OF_PACKAGE/path 的写法。
在本教程中使用的网格文件放在 urdf_tutorial 包里面的 meshes 文件夹中。
网格(mesh)可以用多种不同的格式导入。STL 是比较常见的,但引擎也支持 DAE 格式。

  • DAE 文件可以自带颜色数据,这样你就不需要在 URDF 里再指定颜色或材质。
  • 不过很多时候颜色信息会在单独的文件里保存。比如这些网格就引用了 meshes 文件夹里的 .tif 贴图文件。
  • 网格也可以通过 相对缩放参数(scale) 或 指定包围盒大小(bounding box size) 来调整尺寸。
  • 我们甚至可以引用一个完全不同的包里的网格文件。

这就是了,一个类似 R2D2 的 URDF 模型。现在你可以继续进入下一步,让它动起来。

3. 构建一个可移动的机器人模型

在本教程中,我们将修改之前教程里制作的 R2D2 模型,使其具备可移动的关节。之前的模型中,所有的关节都是固定的。现在我们将探索另外三种重要的关节类型:连续型 (continuous)、转动型 (revolute) 和 移动型 (prismatic)

  • 1. Continuous Joint(连续型关节)
    • 特点:可以无限制旋转,没有角度范围限制。
    • 比喻:就像 车轮 或者 电风扇的扇叶,可以一直转下去。
    • URDF 中的定义:
<joint name="wheel_joint" type="continuous">
  <parent link="chassis"/>
  <child link="wheel"/>
  <origin xyz="0 0 0" rpy="0 0 0"/>
   <!-- 定义关节绕哪个轴旋转-->
   <!-- <axis xyz="X Y Z"/>,表示一个向量 (X, Y, Z),方向就是旋转轴-->
  <axis xyz="0 0 1"/>
</joint>
  • 应用:机器人小车的轮子、雷达的转台
  • 2. Revolute Joint(转动型关节)
    • 特点:可以在一个轴上旋转,但有角度限制(比如 -90° 到 +90°)。
    • 比喻:就像人的 手肘 或 膝盖,只能在一定范围内弯曲。
    • 应用:机械臂的关节、摄像头的俯仰机构。
    • URDF 中的定义:
<joint name="elbow_joint" type="revolute">
  <parent link="upper_arm"/>
  <child link="lower_arm"/>
  <origin xyz="0 0 0" rpy="0 0 0"/>
   <!-- 定义关节绕哪个轴旋转-->
  <axis xyz="0 1 0"/>
  <!-- effort="10"
  	表示关节最大允许的力矩或推力。
  	单位是牛·米(N·m),表示马达最大输出扭矩。
  	上例中:关节最多输出 10 N·m 或 10 N。-->
  <!-- velocity="1"
  	表示关节最大速度。
  	对旋转关节:单位是弧度/秒(rad/s)。
  	对滑动关节:单位是米/秒(m/s)。
  	上例中:关节不能超过 1 rad/s。-->
  <!-- lower="-1.57" 与 upper="1.57"
  	表示关节的运动范围。
  	对旋转关节:单位是弧度,-1.57 ≈ -90°,1.57 ≈ 90°。
  	对滑动关节:单位是米,表示最小/最大伸长距离。
  	上例中:关节只能旋转 -90° 到 +90°。-->
  <limit effort="10" velocity="1" lower="-1.57" upper="1.57"/>
</joint>
  • 3. Prismatic Joint(移动型关节)
    • 特点:沿某一轴直线滑动,有移动范围限制。
    • 比喻:就像 抽屉 或者 人的手臂伸缩动作(但不是旋转,而是前后滑动)。
    • 应用:机器人升降平台、线性滑轨、伸缩机构。
    • URDF 中的定义:
<joint name="slider_joint" type="prismatic">
  <parent link="base"/>
  <child link="slider"/>
  <origin xyz="0 0 0" rpy="0 0 0"/>
  <axis xyz="1 0 0"/>
  <!-- effort="10"
	关节允许的最大力矩(旋转关节)或最大推力(滑动关节)。
	单位: 滑动关节:牛(N)
	本例:最大输出 10 N。-->
  <!-- velocity="0.5"
  	关节允许的最大速度。
	单位:米/秒(m/s)
	本例:最大速度 0.5 rad/s 或 0.5 m/s。-->
  <!-- lower="0.0" 与 upper="0.5"
	关节的运动范围限制。
	单位:米
	本例:关节只能从 0 到 0.5
	长度只能从 0 到 0.5 米-->
  <limit effort="10" velocity="0.5" lower="0.0" upper="0.5"/>
</joint>

在继续之前,请确保你已经安装了所有的前置依赖。所需依赖的说明请参考上一个教程。

同样,本教程中提到的所有机器人模型,都可以在 urdf_tutorial 软件包中找到。

相比于05-visual.urdf ,我们主要修改了一下内容:

3.1 头部关节

  <joint name="head_swivel" type="continuous">
    <parent link="base_link"/>
    <child link="head"/>
    <axis xyz="0 0 1"/>
    <origin xyz="0 0 0.3"/>
  </joint>

身体和头部之间的连接是一个 连续关节(continuous joint),意味着它的角度可以从负无穷到正无穷任意变化。车轮也是这样建模的,因此它们可以无限地向前或向后滚动。

我们唯一需要额外添加的信息是 旋转轴(axis of rotation),这里通过一个 xyz 三元组指定,表示头部绕哪个向量旋转。由于我们希望它绕 z 轴旋转,所以指定向量为 “0 0 1”。

3.2 抓手

<joint name="left_gripper_joint" type="revolute">
  <axis xyz="0 0 1"/>
  <limit effort="1000.0" lower="0.0" upper="0.548" velocity="0.5"/>
  <origin rpy="0 0 0" xyz="0.2 0.01 0"/>
  <parent link="gripper_pole"/>
  <child link="left_gripper"/>
</joint>

左右抓手的关节都被建模为 转动关节(revolute joint)。这意味着它们的旋转方式与连续关节类似,但有严格的角度限制。因此,我们必须在 URDF 中使用 标签来指定关节的最大和最小旋转角度(以弧度为单位)。同时,还需要为该关节指定最大速度和最大力/扭矩,但对于本教程的目的,这些具体数值并不重要。

3.3 抓手机械臂

<joint name="gripper_extension" type="prismatic">
  <parent link="base_link"/>
  <child link="gripper_pole"/>
  <limit effort="1000.0" lower="-0.38" upper="0" velocity="0.5"/>
  <origin rpy="0 0 0" xyz="0.19 0 0.2"/>
</joint>

抓手机械臂属于另一种类型的关节,即滑动关节(prismatic joint)。这意味着它沿着一个轴线移动,而不是围绕轴线旋转。这种平移运动使我们的机器人模型能够伸出和收回抓手机械臂。

如果滑动关节(prismatic joint)没有 标签,URDF 会默认使用 父坐标系的 X 轴 作为平移方向,也就是等同于 。

滑动关节的运动范围限制与转动关节类似,只是单位是米(meters),而不是弧度(radians)。

3.4 其他类型的关节

还有另外两种可以在空间中运动的关节类型。与滑动关节只能沿一维移动不同,平面关节(planar joint) 可以在一个平面内移动,也就是二维移动。此外,浮动关节(floating joint) 没有约束,可以在三维空间中自由移动。这些关节不能仅用一个数值来描述,因此在本教程中没有包含。

3.5 指定位姿

在 Rviz 里你看到的模型动态移动,其实是靠 关节状态(JointState)消息 控制的。流程大概是这样的:

  • 1. 解析 URDF:
    • 程序先读取你的机器人模型(URDF),知道哪些关节可以动,以及它们的限制范围(角度/位移)。
  • 2. GUI 滑块控制
    • 启动 joint_state_publisher_gui 后,会弹出一个带有 滑块(slider) 的小窗口。
    • 每个非固定关节对应一个滑块。
    • 当你拖动滑块时,就会生成对应的关节状态值(例如:手臂抬高多少弧度,滑块移动多少米)。
  • 3. 发布 JointState 消息:
    • joint_state_publisher会把这些值被封装成 sensor_msgs/msg/JointState 消息。
    • 通过 ROS2 发布机制,把消息发到 /joint_states 话题。
  • 4. robot_state_publisher 计算变换
    • robot_state_publisher 订阅 /joint_states,并读取 URDF 文件里的关节与连杆描述。
    • 它根据关节值和模型参数,计算每个连杆(link)相对于父连杆的位置和姿态。
    • robot_state_publisher 根据这些信息,把每个连杆(link)在世界坐标系下的位置和姿态计算出来。
    • 最终生成 TF 树(Transform Tree),描述机器人所有部件的空间关系。
  • 5. Rviz 显示模型
    • Rviz 订阅 TF 树和机器人模型。
    • 根据这些变换,Rviz 将机器人以正确的姿态显示出来。
    • 当滑块变化时,Rviz 中的模型会实时更新(手臂抬起、轮子旋转等)。

4. 添加物理属性和碰撞属性

在本教程中,我们将探讨如何为您的 URDF 模型添加一些基本的物理属性,以及如何指定其碰撞属性。
到目前为止,我们只在链接(link)中指定了一个子元素——visual,它定义了机器人外观(不出意外)。然而,为了使碰撞检测能够工作或对机器人进行仿真,我们还需要定义一个 collision 元素。下面是包含碰撞和物理属性的新 URDF。

4.1 碰撞

4.1.1 什么是碰撞体

想象一个真实场景, 你有一个纸箱机器人玩具,放在桌子上。

  • 你看到的外壳(玩具的颜色、造型、按钮、灯光) → 对应 URDF 中的 <visual>。
  • 碰撞体 → 想象你在纸箱外面套了一个透明塑料盒,这个盒子稍微大一点,不管你看到的手臂有多复杂,塑料盒就是“碰撞边界”。
  • 作用:当这个透明塑料盒碰到桌子、墙或者其他物体时,你知道“机器人碰到了东西”,仿真就会响应。
    碰撞体 = 仿真里“看不见的外壳”,只负责告诉电脑哪里会碰到东西。

你看不到这个透明盒,但它决定了碰撞计算。
collision 元素描述了机器人的几何形状,用于:

  • 碰撞检测:机器人与环境或自身各部分是否发生碰撞。
  • 物理仿真:与物理引擎(如 Gazebo、PyBullet)结合,计算接触力、摩擦、质量分布等。

注意:collision 与 visual 元素类似,但两者用途不同:
visual:只关心外观渲染。
collision:只关心物理和碰撞检测,不一定需要渲染。

我们给出一个例子:

<link name="base_link">
	<!-- 可视化部分-->
    <visual>
      <geometry>
        <cylinder length="0.6" radius="0.2"/>
      </geometry>
      <material name="blue">
        <color rgba="0 0 .8 1"/>
      </material>
    </visual>
    <collision>
      <geometry>
      	<!-- 用于仿真和碰撞检测,告诉仿真环境这个 link 占据的空间。-->
        <cylinder length="0.6" radius="0.2"/>
      </geometry>
    </collision>
  </link>

collision 元素是 link 对象的直接子元素,与 visual 标签处于同一层级。

collision 元素通过 标签定义其形状,方式与 visual 元素完全相同。这里 标签的格式与 visual 中的用法完全一致。

很多情况下,**你会希望碰撞几何体和 与可视几何体和 完全相同。**然而,有两种主要情况不适合这样做:

  • 加快处理速度
    对两个 mesh 进行碰撞检测比对两个简单几何体的计算复杂得多。因此,你可能希望在 元素中用更简单的几何体替代 mesh,以提高仿真效率。
  • 安全区域
    你可能希望限制机器人靠近敏感设备的动作。例如,如果不希望任何物体碰到 R2D2 的头部,可以将碰撞几何体定义为一个包裹其头部的圆柱体,从而防止其他物体靠得太近。

4.2 物理属性

为了让你的模型能够正确进行仿真,你需要定义机器人的若干物理属性,即像 Gazebo 这样的物理引擎所需要的属性。

4.2.1 惯性(Inertia)

每个要进行仿真的 link 元素都需要一个 标签。下面是一个简单示例。

<link name="base_link">
  <visual>
    <geometry>
      <cylinder length="0.6" radius="0.2"/>
    </geometry>
    <material name="blue">
      <color rgba="0 0 .8 1"/>
    </material>
  </visual>
  <collision>
    <geometry>
      <cylinder length="0.6" radius="0.2"/>
    </geometry>
  </collision>
  <inertial>
    <mass value="10"/>
    <inertia ixx="1e-3" ixy="0.0" ixz="0.0" iyy="1e-3" iyz="0.0" izz="1e-3"/>
  </inertial>
</link>

该元素同样是 link 对象的子元素。每个要进行物理仿真的 link 都必须有 ,告诉仿真器这个 link 的质量和惯性信息。

  • 质量<mass value=“…”/>

    • 定义 link 的质量。
    • 质量以 千克(kg) 为单位定义。
    • 质量决定了机器人在物理仿真中的惯性、重力作用以及动力学行为。
  • 3×3 的旋转惯量矩阵

    • 通过 元素指定。
    • 惯性矩阵描述了物体绕三个轴旋转时的惯性,是 3×3 对称矩阵。
    • 对称矩阵只需要 6 个元素就能完整描述. 每个元素意义:
      • ixx, iyy, izz → 绕 x、y、z 轴的主惯性
      • ixy, ixz, iyz → 旋转交叉项(惯性耦合)
        在这里插入图片描述
        这些信息可以通过像 MeshLab 这样的建模软件获得。几何基本体(圆柱、长方体、球体)的惯量可以根据维基百科上提供的惯量张量列表计算(并在上面的示例中使用)。

惯量张量取决于物体的质量以及质量的分布。一个较好的初步估算方法是假设物体的质量在其体积中均匀分布,然后根据物体的形状计算惯量张量,如上所述。

如果不确定该填什么,对于中等大小的 link,使用 ixx/iyy/izz = 1e-3 或更小的矩阵通常是一个合理的默认值(相当于边长 0.1 米、质量 0.6 千克的长方体)。而使用单位矩阵则特别不合适,因为其值通常太大(相当于边长 0.1 米、质量 600 千克的长方体!)

你还可以指定 标签来设置重心位置以及惯性参考系(相对于 link 的参考系)。

在使用实时控制器时,如果惯性元素为零(或接近零),可能会导致机器人模型突然塌陷,所有 link 的原点都会重合到世界坐标系原点。

4.2.2 接触系数

你还可以定义 link 之间接触时的行为。这是通过 标签下的一个子元素 <contact_coefficients> 来实现的。需要指定三个属性:

  • mu — 摩擦系数
    • 控制物体表面间的摩擦力。
    • 范围一般 0~1:
      • 0 → 光滑,无摩擦
      • 1 → 很粗糙,摩擦力大
    • 0.5 → 中等摩擦,物体接触时既不会滑动得很快,也不会完全卡住。
  • kp — 刚度系数
    • 决定接触时的“硬度”,即碰撞时的弹性程度。
    • 数值越大 → 碰撞越硬、物体几乎不压缩
    • 数值越小 → 碰撞较软,物体可以“轻微变形”
    • 1000 → 中等刚度,碰撞时既不会太硬,也不会像海绵一样软。
  • kd — 阻尼系数
    • 决定碰撞后物体震动衰减速度。
    • 数值越大 → 碰撞后快速停止.
    • 数值越小 → 碰撞后震动持续更久
    • 10 → 轻微阻尼,物体会在碰撞后缓慢停下来。
<collision>
  <geometry>
    <box size="0.5 0.5 0.1"/>
  </geometry>
  <!-- mu = 0.5:摩擦适中,类似橡胶在木板上的摩擦
	kp = 1000:接触刚度中等,碰撞不太“软”
	kd = 10:轻微阻尼,碰撞后震动会快速衰减-->
  <contact_coefficients mu="0.5" kp="1000" kd="10"/>
</collision>

4.2.3 关节动力学(Joint Dynamics)

关节的运动方式由关节的 标签定义。该标签有两个属性:

  • friction — 物理静摩擦。
    • 对平移关节(prismatic joint),单位为牛顿(N)。
    • 对旋转关节(revolute joint),单位为牛·米(N·m)。
  • damping — 物理阻尼值。
    • 对平移关节,单位为牛·秒每米(N·s/m)。
    • 对旋转关节,单位为牛·米·秒每弧度(N·m·s/rad)。
      如果没有指定,这些系数默认值为 0。
<joint name="elbow_joint" type="revolute">
  <parent link="upper_arm"/>
  <child link="forearm"/>
  <axis xyz="0 0 1"/>
  	<!-- friction = 0.2 N·m → 静摩擦力,让手臂不容易自己滑动
	damping = 0.05 N·m·s/rad → 阻尼,关节运动后会逐渐减速停止
	效果:手臂旋转时既有摩擦,又不会无限摆动-->
  <dynamics friction="0.2" damping="0.05"/>
</joint>

4.2.4 其他标签(Other Tags)

在纯 URDF 范畴内(即不包括 Gazebo 特有的标签),还有两个用于定义关节的标签: 和 <safety_controller>。

5. 使用 Xacro 优化你的代码

到目前为止,如果你在家里按照这些步骤为自己的机器人设计进行操作,你可能已经厌倦了为让一些非常简单的机器人描述能够正确解析而做各种数学计算。幸运的是,你可以使用 xacro 包来简化工作。

5.1 Xacro 是什么?

Xacro = XML Macros,即 XML 的宏语言扩展。它本质上是URDF 的增强工具,用来生成复杂的 URDF 文件。
通过 Xacro,你可以在 URDF 中使用:

  • 宏(Macros):类似函数,避免重复代码
  • 变量(Constants/Properties):可以统一管理长度、质量等参数
  • 简单数学计算(Expressions):减少手动计算错误

简单理解:Xacro 就像 C 语言里的宏和变量,让你写 URDF 更高效、可维护。

Xacro 的优势:

  • 减少重复代码
    • 可以把重复的 link、joint 定义写成宏,每次调用即可。
    • 例如,一个轮子模型可能有多个重复实例,可以用宏定义一次。
  • 统一参数管理
    • 定义变量,比如轮子半径、臂长、质量等。
    • 修改变量即可全局更新 URDF,避免手动逐个修改。
  • 支持简单数学运算
    • 可以在 URDF 中直接做加减乘除、平方根、三角函数等计算。
    • 避免手动计算错误,提高灵活性。
  • 生成标准 URDF
    • Xacro 文件本身不是 URDF,需要通过 xacro 命令生成 URDF。
    • 生成的 URDF 与普通 URDF 完全兼容,ROS 系统可以直接使用。

在本教程中,我们将介绍这些快捷方式,帮助减少 URDF 文件的整体规模,同时使文件更易于阅读和维护。

5.2 使用 Xacro(Using Xacro)

顾名思义,xacro 是一种用于 XML 的宏语言。
xacro 程序会执行所有宏并输出最终结果。典型的使用方式如下:

xacro model.xacro > model.urdf

你也可以在 launch 文件 中自动生成 URDF。这样做的好处是始终保持最新,并且不会占用硬盘空间。不过需要注意,生成过程会花费一些时间,因此你的 launch 文件启动时可能会变得更慢。
要在 launch 文件 中运行 xacro,你需要将 Command 替换指令 作为参数传递给 robot_state_publisher。

# 返回指定 ROS 2 包的 share 目录绝对路径
# 拼接路径得到包含 xacro的 URDF 文件的完整绝对路径
path_to_urdf = get_package_share_path('turtlebot3_description') / 'urdf' / 'turtlebot3_burger.urdf'
robot_state_publisher_node = launch_ros.actions.Node(
    package='robot_state_publisher',
    executable='robot_state_publisher',
    parameters=[{
        'robot_description': ParameterValue(
        	## 执行转换,将返回内容作为参数
            Command(['xacro ', str(path_to_urdf)]), value_type=str
        )
    }]
)

Command([…])

在 ROS 2 Launch 系统中,Command 是一个 Launch Substitution,即“启动时替换值”的机制。
它的主要功能是:

  • 执行 shell 命令。
  • 获取该命令的标准输出(stdout)内容,并把这个内容作为字符串返回。

5.2.1 使用 urdf_launch 包来自动加载 Xacro/URDF 文件

一种更简单的加载机器人模型的方法是使用 urdf_launch 包来自动加载 Xacro/URDF 文件。

在 ROS 2 中,要让机器人模型显示在 RViz 或被其他节点使用,通常需要以下步骤:

  • 解析 Xacro 文件(生成 URDF)
  • 将 URDF 作为参数 robot_description 传给 robot_state_publisher
  • 启动 joint_state_publisher 或 joint_state_publisher_gui
  • 在 RViz 中显示机器人模型

如果手动写 Launch 文件,需要写很多节点配置和命令调用。urdf_launch 就是 把这些步骤封装成可复用的 Launch 文件,让你更容易快速加载机器人模型。

urdf_launch的display.launch.py提供的通用 Launch 文件,专门用来加载和显示任意机器人 URDF/Xacro 模型。它并不依赖特定机器人,只要你提供:

  • URDF 包名(urdf_package)
  • URDF 文件路径(urdf_package_path)
    就可以解析 Xacro、启动 robot_state_publisher 并显示模型。
display.launch.py 的核心功能
  • 自动解析 Xacro/URDF
    • 支持直接指定 Xacro 文件路径,启动时自动调用 xacro 命令生成 URDF。
  • 启动 Robot State Publisher
    • 自动将解析后的 URDF 传给 robot_state_publisher,发布机器人关节坐标变换(TF)。
  • 启动 Joint State Publisher
    • 可选启动 GUI 版本,让你手动调整关节状态,实时更新机器人模型。
  • 集成 RViz(可选)
    • 提供显示机器人的可视化启动文件,便于调试和验证 URDF 模型。
from launch import LaunchDescription
from launch.actions import IncludeLaunchDescription
from launch.substitutions import PathJoinSubstitution
from launch_ros.substitutions import FindPackageShare


def generate_launch_description():
    return LaunchDescription([
    	# 用于在一个 Launch 文件中 包含另一个 Launch 文件
    	# 参数1:指定要包含的 Launch 文件路径。
    	# 参数2:启动Launch 文件传递的参数。 
        IncludeLaunchDescription(
        	# FindPackageShare 返回指定 ROS 2 包的 share 目录绝对路径
        	# PathJoinSubstitution 把多个路径或 Substitution 拼接成一个完整路径字符串
        	# 类似 Python 的 os.path.join
            PathJoinSubstitution([FindPackageShare('urdf_launch'), 'launch', 'display.launch.py']),
            launch_arguments={
            	# 告诉Launch 文件,URDF/Xacro 所在的 ROS2 包的包名
                'urdf_package': 'turtlebot3_description',
                # URDF/Xacro 文件在包内的相对路径
                'urdf_package_path': PathJoinSubstitution(['urdf', 'turtlebot3_burger.urdf'])
            }.items()
        )
    ])

在lanuch文件中,加载urdf_launch的display.launch.py, 并提供你自己URDF/Xacro 所在的 ROS2 包的包名 & 路径,就可以快速加载机器人模型。

5.3 xacro 基本语法

5.3.1 定义命名空间

在 URDF 文件的开头,必须指定一个命名空间,以便文件能够正确解析。例如,下面是一个有效 Xacro 文件的前两行:

<?xml version="1.0"?>
<!-- robot 定义一个机器人
xmlns:xacro 表示定义一个名为 xacro 的命名空间
http://www.ros.org/wiki/xacro 是命名空间的 URI(不必真的访问,只是标识)
name 属性就是机器人树根节点名称-->
<robot xmlns:xacro="http://www.ros.org/wiki/xacro" name="firefighter">

5.3.2 常量

让我们快速看一下 R2D2 中的 base_link。

<link name="base_link">
  <visual>
    <geometry>
      <cylinder length="0.6" radius="0.2"/>
    </geometry>
    <material name="blue"/>
  </visual>
  <collision>
    <geometry>
      <cylinder length="0.6" radius="0.2"/>
    </geometry>
  </collision>
</link>

这里的信息有点冗余。我们把圆柱体的长度和半径写了两次。更糟的是,如果我们想修改它,就必须在两个不同的地方进行修改。

幸运的是,xacro 允许我们定义一些属性,把它们当作常量来使用。这样一来,上面的代码就可以改写成下面这样。

<!-- xacro:property,在 Xacro 文件中定义一个 常量/变量,供后续引用。-->
<xacro:property name="width" value="0.2" />
<xacro:property name="bodylen" value="0.6" />
<link name="base_link">
    <visual>
        <geometry>
        	<!-- 引用常量/变量, ${变量名}-->
            <cylinder radius="${width}" length="${bodylen}"/>
        </geometry>
        <material name="blue"/>
    </visual>
    <collision>
        <geometry>
            <cylinder radius="${width}" length="${bodylen}"/>
        </geometry>
    </collision>
</link>

${} 结构中的内容值会被用来替换 ${}。这意味着你可以在属性中把它与其他文本组合使用。

<xacro:property name="robotname" value="marvin" />
<link name="${robotname}s_leg" />
<!-- 这样会生成<link name="marvins_leg" /> -->

然而,${} 中的内容并不一定只能是一个属性,这就引出了我们的下一个要点……

5.3.3 数学计算

你可以在 ${} 结构中构建任意复杂的表达式,使用四则运算(+、-、*、/)、一元负号以及括号。示例:

<!-- wheeldiam/2 → 圆柱半径 = 轮子直径除以 2-->
<cylinder radius="${wheeldiam/2}" length="0.1"/>
<!-- reflect和width都是通过 <xacro:property> 定义的常量
width+.02 = width + 0.02-->
<origin xyz="${reflect*(width+.02)} 0 0.25" />

你还可以使用比基本数学运算更多的函数,比如 sin 和 cos。

5.3.4 宏(Macros)

这是 Xacro 包中 最大也是最有用的功能组件。

简单的宏

让我们来看一个 简单但无实际用途的宏。

<xacro:macro name="default_origin">
    <origin xyz="0 0 0" rpy="0 0 0"/>
</xacro:macro>
<!-- 引用定义的default_origin-->
<xacro:default_origin />

这是无用的,因为如果不指定 origin,它的值与这里的一样。

<origin rpy="0 0 0" xyz="0 0 0"/>
  • 名称(name)在技术上并不是必须的元素,但你需要指定它才能使用宏。
  • 每个 <xacro:$NAME /> 的实例都会被 xacro:macro 标签中的内容替换。
  • 请注意,尽管生成的 XML 与原来不完全相同(两个属性的顺序可能交换了),但它们在功能上是等效的。
  • 如果找不到指定名称的 Xacro 宏,它将不会被展开,也不会产生错误。
参数化宏

你还可以给宏添加参数,这样每次生成的内容就不会完全相同。结合数学功能使用时,这种方式会更加强大。
首先,我们来看一个在 R2D2 中使用的简单宏示例。

<xacro:macro name="default_inertial" params="mass">
    <inertial>
            <mass value="${mass}" />
            <inertia ixx="1e-3" ixy="0.0" ixz="0.0"
                 iyy="1e-3" iyz="0.0"
                 izz="1e-3" />
    </inertial>
</xacro:macro>

它可以被一下方式调用:

<xacro:default_inertial mass="10"/>
块参数

Xacro 宏不仅可以接收简单数值或字符串参数,还可以传入 整个 XML 代码块。 要指定一个代码块参数,在参数名前加一个星号(*)。可以使用 xacro:insert_block 命令插入这个代码块。

<!-- *shape 表名参数shape 是一个块。-->
<xacro:macro name="blue_shape" params="name *shape">
    <link name="${name}">
        <visual>
            <geometry>
            	<!-- 插入这个代码块-->
                <xacro:insert_block name="shape" />
            </geometry>
            <material name="blue"/>
        </visual>
        <collision>
            <geometry>
            	<!-- 你可以根据需要多次插入该代码块。-->
                <xacro:insert_block name="shape" />
            </geometry>
        </collision>
    </link>
</xacro:macro>

<xacro:blue_shape name="base_link">
    <cylinder radius=".42" length=".01" />
</xacro:blue_shape>
实际使用

Xacro 语言在允许你做的事情上非常灵活。除了上面展示的默认惯性宏之外,下面列出了一些在 R2D2 模型中常用的 Xacro 用法。

要查看 Xacro 文件生成的模型,可以运行与之前教程相同的命令:

ros2 launch urdf_tutorial display.launch.py model:=urdf/08-macroed.urdf.xacro
  • 启动 ROS 2 的 urdf_tutorial 包中的 display.launch.py
  • 加载并展开 08-macroed.urdf.xacro
  • 发布机器人状态
  • 在 RViz 中显示展开后的机器人模型
腿(Leg)宏

很多时候,你需要在不同位置创建多个外观相似的对象。你可以像我们在 R2 的两条腿上所做的那样,使用宏和一些简单的数学运算来减少需要编写的代码量。

<xacro:macro name="leg" params="prefix reflect">
    <link name="${prefix}_leg">
        <visual>
            <geometry>
                <box size="${leglen} 0.1 0.2"/>
            </geometry>
            <origin xyz="0 0 -${leglen/2}" rpy="0 ${pi/2} 0"/>
            <material name="white"/>
        </visual>
        <collision>
            <geometry>
                <box size="${leglen} 0.1 0.2"/>
            </geometry>
            <origin xyz="0 0 -${leglen/2}" rpy="0 ${pi/2} 0"/>
        </collision>
        <xacro:default_inertial mass="10"/>
    </link>

    <joint name="base_to_${prefix}_leg" type="fixed">
        <parent link="base_link"/>
        <child link="${prefix}_leg"/>
        <origin xyz="0 ${reflect*(width+.02)} 0.25" />
    </joint>
    <!-- A bunch of stuff cut -->
</xacro:macro>
<xacro:leg prefix="right" reflect="1" />
<xacro:leg prefix="left" reflect="-1" />
  • 常用技巧 1:使用名称前缀来生成两个名字相似的对象。
  • 常用技巧 2:使用数学计算关节的原点位置。如果你更改了机器人尺寸,只需修改一个属性,并通过一些数学计算关节偏移量,就能省去大量麻烦。
  • 常用技巧 3:使用一个反射(reflect)参数,并将其设置为 1 或 -1。看看我们是如何利用 reflect 参数,把两条腿放在机身两侧的(在 base_to_${prefix}_leg 的原点定义中)
Logo

立足具身智能前沿赛道,致力于搭建全球化、开源化、全栈式技术交流与实践共创平台。

更多推荐