用 MuJoCo 与 VSCode 上手灵犀 X2 人形机器人:从 MJCF 模型结构到 Python 控制的完整教程
这篇是实操教程:从装好 MuJoCo 开始,到在查看器里打开智元灵犀 X2 人形机器人,逐层读懂它的 MJCF 模型文件,最后用二十来行 Python 驱动它的第一个关节。文中所有 XML 片段都摘自智元官方开源仓库 agibot_x2_urdf(木兰宽松许可证第 2 版),可以边读边复现。
一、准备环境:十分钟装好 MuJoCo 和 VSCode
1.1 安装 MuJoCo

MuJoCo 新版本已上架 PyPI,pip 一键安装,不再需要下载二进制包和配置环境变量:
bash
pip install mujoco
验证安装:
bash
python -c "import mujoco; print(mujoco.__version__)"
能打印出版本号(如 3.2.x)就说明装好了。Windows 桌面环境无需任何额外图形配置;如果是在无显示器的 Linux 服务器上跑,再设置 export MUJOCO_GL=egl 即可,本文不展开。
1.2 VSCode 需要的两个扩展
| 扩展 | 用途 |
|---|---|
| Python(ms-python.python) | 语法高亮、调试、运行脚本 |
| URDF Visualizer | 直接在 VSCode 里预览 .urdf 模型,官方 README 点名推荐 |
URDF Visualizer 用来"看",MuJoCo 用来"仿"。仓库同时提供了两种格式,这是官方设计的两条预览通道,后面第六节会详细对比。
1.3 获取 X2 模型仓库
bash
git clone https://github.com/AgibotTech/agibot_x2_urdf
仓库里按机型分了两个目录。怎么知道你手里的 X2 是哪个版本?官方的判据是机器人后颈铭牌最上面一行的结尾:
| 铭牌结尾 | 机型 | 用哪个目录 | 模型文件 |
|---|---|---|---|
| X2 Ultra | 旗舰版 | X2_URDF-v1.3.0/ | x2_ultra.xml、x2_fist.xml |
| X2 Ultra -N | 旗舰焕新版 | X2_URDF-v1.4.0/ | X2-Ultra.xml |
| X2 EDU | 人人造(教育机型) | X2_URDF-v1.4.0/ | X2-EDU.xml |
本教程以 X2-EDU.xml(29 个关节)为主线讲解,Ultra 版只多出头部 2 个关节,结构完全同理。
环境速查表:
| 组件 | 要求 |
|---|---|
| Python | 3.9 及以上(建议 3.10+) |
| mujoco | pip 最新版即可,无需 GPU,核显够用 |
| VSCode 扩展 | Python、URDF Visualizer |
| 操作系统 | Windows / macOS / Linux 均可 |
二、第一次打开 X2:scene.xml 是壳,机器人是芯
2.1 启动查看器
在仓库根目录执行:
bash
python -m mujoco.viewer --mjcf X2_URDF-v1.4.0/scene.xml
注意两点:
- 必须在仓库根目录运行,路径参数以根目录为基准写
- 也可以先启动 python -m mujoco.viewer 再把 XML 拖进窗口,效果一样
几秒后你会看到:棋盘格地面、渐变天空盒,以及一个站在场地中央的白色人形机器人。
2.2 场景文件拆解

scene.xml 全文只有 21 行,它不描述机器人,只负责"舞台":
xml
<mujoco model="X2-Ultra scene">
<include file="X2-Ultra.xml"/>
<statistic center="0 0 0.6" extent="1.3"/>
<visual>
<headlight diffuse="0.5 0.5 0.5" ambient="0.2 0.2 0.2" specular="0.9 0.9 0.9"/>
<rgba haze="0.15 0.25 0.35 1"/>
<global azimuth="200" elevation="-20"/>
</visual>
<asset>
<texture type="skybox" builtin="gradient" rgb1="0.3 0.5 0.7" rgb2="0 0 0" width="512" height="3072"/>
<texture type="2d" name="groundplane" builtin="checker" mark="edge" rgb1="0.3 0.52 0.63" rgb2="0.3 0.52 0.63" markrgb="1 1 1" width="300" height="300"/>
<material name="groundplane" texture="groundplane" texuniform="true" texrepeat="5 5" reflectance="0.2"/>
</asset>
<worldbody>
<geom name="floor" size="0 0 0.05" type="plane" material="groundplane"/>
</worldbody>
</mujoco>
四个关键块:
- :把 X2-Ultra.xml 整个搬进来,机器人本体在这里
- :告诉查看器"视线焦点放在 z=0.6、范围 1.3 米",否则相机默认怼到原点
- :天空盒与棋盘格贴图,纯视觉装饰
- :只放了一块无限大平面 type="plane" 当地板
切换机型的小技巧:想看 EDU 版,复制一份 scene.xml 改名为 scene_edu.xml,把 改成 X2-EDU.xml 即可,其余一字不动。
2.3 交互操作速查

| 操作 | 效果 |
|---|---|
| 鼠标左键拖拽 | 旋转视角 |
| 鼠标右键拖拽 | 平移视角 |
| 滚轮 | 缩放 |
| 双击物体 | 选中 body(高亮) |
| 选中后 Ctrl + 右键拖拽 | 对该 body 施加外力扰动 |
| 选中后 Ctrl + 左键拖拽 | 对该 body 施加力矩 |
| Space | 暂停 / 继续仿真 |
| Backspace | 复位到初始状态 |
| F1 | 呼出快捷键帮助面板 |
2.4 第一个反直觉现象:它会倒
什么也不做,等一两秒------机器人会像失去肌肉一样缓缓瘫倒在地。
这不是模型坏了,而是机器人学的第一课:模型 ≠ 会站。MJCF 文件里只有质量和惯量,没有"站立"这个概念。真人站立是全身每个电机的力矩在实时抵消重力,同理,仿真里的 X2 想站稳,也必须有一个控制器持续输出力矩。官方仓库提供的是"身体",不包含"小脑",控制器需要你自己接------第四节先弄清身体上有哪些可以驱动的点,第五节再亲手驱动第一个关节。
三、读懂 MJCF:X2 模型的六层结构
打开 X2_URDF-v1.4.0/X2-EDU.xml(365 行),从上到下其实是清晰的六层:全局设置 → 默认样式 → 资产 → 连杆树 → 执行器 → 传感器。逐层拆解。
3.1 全局设置:compiler 与 option
xml
<compiler angle="radian" eulerseq="XYZ" meshdir="meshes/" autolimits="true" />
<option timestep="0.001" />
- angle="radian":全文角度单位都是弧度,看 range 数值时要记得换算
- eulerseq="XYZ":欧拉角旋转顺序
- meshdir="meshes/":网格文件相对本 XML 所在目录查找,所以不能把 XML 单独拷走,否则加载报错找不到 STL
- autolimits="true":写了 range 就自动启用限制,不用再写 limited="true"
- timestep="0.001":仿真步长 1 ms,这是关节型机器人仿真的常见选择,兼顾数值稳定与速度
3.2 default 类:一套"样式表"
xml
<default>
<default class="x2">
<site rgba="1 0 0 1" size="0.01" group="5" />
<joint damping="0.0" armature="0.03" frictionloss="0.3" />
<default class="visual">
<geom group="2" type="mesh" contype="0" conaffinity="0" density="0" />
</default>
<default class="collision">
<geom group="3" type="mesh" />
</default>
<default class="foot">
<geom group="3" type="sphere" size="0.005" />
</default>
</default>
</default>
MuJoCo 的 default 是可继承的属性模板,相当于 CSS。x2 类是全模型默认,下面三个子类负责几何体的分工。关节属性值得单独说:
- armature="0.03":给每个关节额外叠加 0.03 kg·m² 的转子惯量。电机转子在减速器之前高速旋转,折算到关节侧的惯量不可忽略,加上它数值积分更稳定,这是关节机器人仿真的标准做法
- frictionloss="0.3":0.3 N·m 的库仑静摩擦,模拟减速器摩擦。没有它,任何微小力矩都会让关节滑动,仿真会显得"油"
- damping="0.0":粘性阻尼为零------真实关节的阻尼主要来自控制回路,这部分留给你写的控制器去做
3.3 世界体与浮动基座:freejoint
机器人树的根在 worldbody 里:
xml
<body name="pelvis" pos="0 0 0.68" euler="0 0 0" childclass="x2">
<light name="tracking" mode="trackcom" pos="0 0 3" />
<freejoint name="floating_base_joint" />
<inertial pos="-0.0010227 -2.909e-05 -0.0017101" quat="..." mass="3.59010086" diaginertia="0.0128699 0.0125644 0.00801244" />
<site name="imu_0" pos="0.0239465 -0.000228682 0.04171" group="3" />
<geom class="visual" ... mesh="pelvis" />
<geom class="collision" size="0.06 0.08" ... type="cylinder" ... />
<geom class="collision" size="0.025 0.055 0.06" ... type="box" ... />
...
三个关键点:
- pos="0 0 0.68":骨盆初始高度 0.68 米。往下数腿链长度(0.137 + 0.183 + 0.282 ≈ 0.60 米)再加足底接触段,双脚恰好落在地面上方几毫米------初始姿态是精心算过的,不是随手填的
- :浮动基座,给骨盆完整的 6 个自由度(3 平移 + 3 转动)。所有人形机器人的 MJCF 都这样写:悬空的躯干靠 freejoint 与世界相连,"站立"从此变成一个控制问题而不是模型问题。一个 freejoint 占 7 个位置坐标(xyz + 四元数)和 6 个速度坐标,所以 qpos 维数 = 关节数 + 7
- 显式:每个连杆都写死了质量与惯量张量(来自 CAD 导出),因此后面 collision 几何体只管碰撞、不参与质量计算
是一个红点标记,定义在骨盆内,是传感器一节所有 IMU 数据的锚点。
3.4 连杆树:29 个关节怎么挂在骨盆上
MJCF 的 body 是纯树形嵌套,父 body 移动则子 body 跟随。X2-EDU 的树形结构:
bash
pelvis(浮动基座,freejoint)
├── left_hip_pitch_link
│ └── left_hip_roll_link
│ └── left_hip_yaw_link
│ └── left_knee_link
│ └── left_ankle_pitch_link
│ └── left_ankle_roll_link ← 足底 12 个接触球挂在这里
├── right_hip_pitch_link
│ └── ...(与左腿完全对称)
└── waist_yaw_link
└── waist_pitch_link
└── torso_link ← 躯干,外观用 torso_lite_link 网格
├── left_shoulder_pitch_link
│ └── left_shoulder_roll_link
│ └── left_shoulder_yaw_link
│ └── left_elbow_link
│ └── left_wrist_yaw_link
│ └── left_wrist_pitch_link
│ └── left_wrist_roll_link
└── right_shoulder_pitch_link
└── ...(与左臂完全对称)
对应到源码,左腿就是这样一层层嵌下去的(省略了几何细节):
xml
<body name="left_hip_pitch_link" pos="0 0.07135 0">
<joint name="left_hip_pitch_joint" pos="0 0 0" axis="0 1 0" range="-2.705260 2.556907" actuatorfrcrange="-120 120" />
<body name="left_hip_roll_link" pos="-0.00057492 0.0657 0">
<joint name="left_hip_roll_joint" pos="0 0 0" axis="1 0 0" range="-0.235619 2.905973" actuatorfrcrange="-120 120" />
<body name="left_hip_yaw_link" pos="0.000575 0 -0.13685">
<joint name="left_hip_yaw_joint" pos="0 0 0" axis="0 0 1" range="-1.858775925 3.604104925" actuatorfrcrange="-120 120" />
<body name="left_knee_link" pos="-0.00766 0.000225 -0.1831">
<joint name="left_knee_joint" pos="0 0 0" axis="0 1 0" range="0.000000 2.408554" actuatorfrcrange="-120 120" />
<body name="left_ankle_pitch_link" pos="-0.01 -0.000124911 -0.282">
<joint name="left_ankle_pitch_joint" pos="0 0 0" axis="0 1 0" range="-0.802851 0.453786" actuatorfrcrange="-60 60" />
<body name="left_ankle_roll_link" pos="-0.00339993 0 0">
<joint name="left_ankle_roll_joint" pos="0 0 0" axis="1 0 0" range="-0.2967059728 0.2967059728" actuatorfrcrange="-36 36" />
...
读法:每个 相对父 body 定位, 描述该 body 绕哪个轴相对父体转动(axis="0 1 0" 即绕 Y 轴)。单腿 6 关节(髋 3 + 膝 1 + 踝 2)、腰 3 关节、单臂 7 关节,合计 6×2 + 3 + 7×2 = 29 个关节,Ultra 版再加头部 yaw / pitch 共 31 个。
关节总表(角度列由弧度换算,右侧关节的 roll / yaw 范围与左侧关于 0 镜像,可在 XML 中逐一查证):
| 关节(左/通用名) | 轴 | 范围(弧度) | 范围(角度) | 力矩上限 |
|---|---|---|---|---|
| *_hip_pitch_joint | Y | -2.705 ~ 2.557 | -155.0° ~ 146.5° | ±120 N·m |
| *_hip_roll_joint | X | -0.236 ~ 2.906(左) | -13.5° ~ 166.5° | ±120 N·m |
| *_hip_yaw_joint | Z | -1.859 ~ 3.604(左) | -106.5° ~ 206.5° | ±120 N·m |
| *_knee_joint | Y | 0 ~ 2.409 | 0° ~ 138.0° | ±120 N·m |
| *_ankle_pitch_joint | Y | -0.803 ~ 0.454 | -46.0° ~ 26.0° | ±60 N·m |
| *_ankle_roll_joint | X | ±0.297 | ±17.0° | ±36 N·m |
| waist_yaw_joint | Z | -3.430 ~ 2.121 | -196.5° ~ 121.5° | ±120 N·m |
| waist_pitch_joint | Y | ±0.314 | ±18.0° | ±36 N·m |
| waist_roll_joint | X | ±0.489 | ±28.0° | ±36 N·m |
| *_shoulder_pitch_joint | Y | -3.081 ~ 2.033 | -176.5° ~ 116.5° | ±60 N·m |
| *_shoulder_roll_joint | X | -0.061 ~ 3.046(左) | -3.5° ~ 174.5° | ±60 N·m |
| *_shoulder_yaw_joint | Z | ±2.557 | ±146.5° | ±36 N·m |
| *_elbow_joint | Y | -2.356 ~ 0 | -135.0° ~ 0° | ±36 N·m |
| *_wrist_yaw_joint | Z | ±2.557 | ±146.5° | ±36 N·m |
| *_wrist_pitch_joint | Y | ±0.524 | ±30.0° | ±6 N·m |
| *_wrist_roll_joint | X | -1.510 ~ 0.724(左) | -86.5° ~ 41.5° | ±6 N·m |
| head_yaw_joint(仅 Ultra) | Z | ±0.489 | ±28.0° | ±3.131 N·m |
| head_pitch_joint(仅 Ultra) | Y | -0.401 ~ 0.489 | -23.0° ~ 28.0° | ±2 N·m |
顺带说一下命名规律,写代码时非常有用:关节名 = 部位_自由度_joint;执行器名 = motor_ + 关节名;传感器名 = jointpos_ / jointvel_ + 关节名。全模型一套规则贯穿到底。
对比 v1.3.0 与 v1.4.0 的官方变更说明,还能从力矩列读出硬件迭代:踝 pitch / 肩 pitch / 肩 roll 从 36 N·m 升到 60 N·m,踝 roll / 肩 yaw / 肘 / 腕 yaw 从 24 N·m 升到 36 N·m,腰 pitch/roll 由 48 N·m 调整为 36 N·m,头部 yaw / pitch 由 2.6 / 0.6 N·m 升到 3.131 / 2 N·m,整机质量约 42 kg → 约 45 kg。模型文件本身就是硬件规格书。
3.5 两套几何体:visual 与 collision
每个连杆都挂了两类几何体。以 left_hip_roll_link 为例:
xml
<geom class="visual" type="mesh" contype="0" conaffinity="0" group="1" density="0" rgba="1 1 1 1" mesh="left_hip_roll_link" />
<geom class="collision" size="0.0615 0.02" pos="0 0 -0.11" type="cylinder" rgba="1 1 1 1" />
- visual 类:STL 网格原样展示,但 contype="0" conaffinity="0" 关闭碰撞、density="0" 不计质量------它只负责"好看"
- collision 类:用圆柱、盒、球等原始几何近似外形,负责"碰撞"。一个精细网格有上万个面,做碰撞检测太慢;一个圆柱 32 个面就够用,物理引擎最喜欢这种
再注意骨盆和躯干的碰撞体不止一个------复杂连杆由多个简单几何拼合近似。脚则更进一步:足底不铺任何面片,而是摆了 12 个半径 5 mm 的小球作为接触点(foot 类):
xml
<geom class="foot" size="0.005" pos="-0.05 0.05 -0.068" rgba="1 1 1 1" />
<geom class="foot" size="0.005" pos="-0.065 0.025 -0.068" rgba="1 1 1 1" />
...(共 12 个)
小球接触是足式仿真的经典技巧:接触求解快、不易抖动,且"点接触"天然贴合脚掌边缘。另外仓库还提供 *_simple_collision.urdf,是碰撞体进一步简化的 URDF 版本,供对性能有要求的场景使用。
3.6 执行器与传感器:模型与控制器的接口
文件末尾的 定义了 29 个电机(Ultra 为 31 个),每行绑定一个关节:
xml
<actuator>
<motor class="x2" name="motor_left_hip_pitch_joint" joint="left_hip_pitch_joint" ctrlrange="-120 120" />
<motor class="x2" name="motor_left_hip_roll_joint" joint="left_hip_roll_joint" ctrlrange="-120 120" />
<motor class="x2" name="motor_left_hip_yaw_joint" joint="left_hip_yaw_joint" ctrlrange="-120 120" />
<motor class="x2" name="motor_left_knee_joint" joint="left_knee_joint" ctrlrange="-120 120" />
<motor class="x2" name="motor_left_ankle_pitch_joint" joint="left_ankle_pitch_joint" ctrlrange="-60 60" />
<motor class="x2" name="motor_left_ankle_roll_joint" joint="left_ankle_roll_joint" ctrlrange="-36 36" />
...
注意:ctrlrange 与对应关节的 actuatorfrcrange 一一相等,超出范围的控制量会被裁剪。 是纯力矩执行器------你写进 data.ctrl 的数就是 N·m,不是目标角度。这是初学者最容易踩的坑,第五节会再次强调。
段则预置了"状态采集器",前 5 个是整机 IMU:
xml
<sensor>
<framequat name="body-orientation" objtype="site" objname="imu_0" noise="0" />
<gyro name="body-angular-velocity" site="imu_0" noise="0.001" />
<framepos name="body-linear-pos" objtype="site" objname="imu_0" />
<velocimeter name="body-linear-vel" site="imu_0" />
<accelerometer name="body-linear-acceleration" site="imu_0" noise="0.001" />
其余是每个关节一组:29 个 jointpos + 29 个 jointvel。也就是说,真实机器人控制回路需要的观测量(IMU 姿态、角速度、加速度 + 全关节位置速度),模型里已经全部配好,noise 参数还能给 IMU 数据加噪声,方便测试状态估计算法的鲁棒性。
四、在 VSCode 里用 Python 透视模型

4.1 三个数字:nq / nv / nu
MuJoCo 把模型加载成纯数值结构,所有维度问 model 就行:
| 字段 | 含义 | X2-EDU | X2-Ultra |
|---|---|---|---|
| nq | 位置坐标维数 | 7 + 29 = 36 | 7 + 31 = 38 |
| nv | 速度坐标维数 | 6 + 29 = 35 | 6 + 31 = 37 |
| nu | 执行器数 | 29 | 31 |
nq 比 nv 多 1 是因为 freejoint 的姿态用四元数(4 个数)表示但角速度只有 3 个数。
4.2 检查模型脚本
新建 inspect_model.py(放在仓库根目录):
python
import mujoco
model = mujoco.MjModel.from_xml_path("X2_URDF-v1.4.0/X2-EDU.xml")
print("nq(位置维数):", model.nq)
print("nv(速度维数):", model.nv)
print("nu(执行器数):", model.nu)
print("body 数量:", model.nbody, "(含世界体)")
print("整机质量:", round(model.body_mass.sum(), 2), "kg")
print("仿真步长:", model.opt.timestep, "s")
预期输出:
bash
nq(位置维数): 36
nv(速度维数): 35
nu(执行器数): 29
body 数量: 31(含世界体)
整机质量: 41.2 kg
仿真步长: 0.001 s
整机质量 41.2 kg 是全连杆 之和,与官方 README"旗舰焕新版约 45 kg、上代约 42 kg"的口径对得上(EDU 精简了头部与部分结构,更轻)。
再顺手把关节名遍历一遍,建立"索引 → 关节"的映射直觉:
python
for i in range(model.njnt):
name = mujoco.mj_id2name(model, mujoco.mjtObj.mjOBJ_JOINT, i)
jtype = model.jnt_type[i] # 0=free, 3=hinge
print(i, name, "FREE" if jtype == 0 else "HINGE")
输出第一个是 0 floating_base_joint FREE,其余 29 个全是 HINGE,顺序与第三节那棵树的前序遍历完全一致。
4.3 qpos 布局速查表
data.qpos 是 36 维向量,布局固定,调试 RL 策略时天天要查:
| qpos 索引 | 内容 |
|---|---|
| 0--2 | 骨盆位置 x y z(米),初始 z = 0.68 |
| 3--6 | 骨盆姿态四元数 w x y z,初始 1 0 0 0 |
| 7--12 | 左腿:hip_pitch → hip_roll → hip_yaw → knee → ankle_pitch → ankle_roll |
| 13--18 | 右腿(同顺序) |
| 19--21 | 腰:yaw → pitch → roll |
| 22--28 | 左臂:shoulder_pitch → shoulder_roll → shoulder_yaw → elbow → wrist_yaw → wrist_pitch → wrist_roll |
| 29--35 | 右臂(同顺序) |
| 36--37(仅 Ultra) | 头部:yaw → pitch |
data.qvel 同理:前 6 位是骨盆线速度 + 角速度,其余 29 位是关节角速度。执行器控制量 data.ctrl 的布局与 段书写顺序一致:0--5 左腿、6--11 右腿、12--14 腰、15--21 左臂、22--28 右臂(EDU)。Ultra 版的 段把头部两个执行器插在腰部之后:15--16 是头,左臂顺延为 17--23、右臂 24--30。注意这与 qpos 的排法不同------qpos 严格按 树的出现顺序,头部排在最后(36--37);ctrl 按 段的书写顺序,头部排在中间。两套索引各有依据,查表时别混用。
五、动手控制:从"看它倒下"到"驱动左肘"

5.1 被动仿真:记录骨盆高度
新建 passive_sim.py,什么都不控制,只观察自由落体式的坍塌:
python
import mujoco
model = mujoco.MjModel.from_xml_path("X2_URDF-v1.4.0/X2-EDU.xml")
data = mujoco.MjData(model)
for i in range(2000):
mujoco.mj_step(model, data)
if i % 400 == 0:
print(f"t={data.time:.2f}s 骨盆高度={data.qpos[2]:.3f}m")
预期输出(数值随 MuJoCo 版本可能略有出入,趋势一定一致):
t=0.00s 骨盆高度=0.680m
t=0.40s 骨盆高度=0.653m
t=0.80s 骨盆高度=0.512m
t=1.20s 骨盆高度=0.274m
t=1.60s 骨盆高度=0.198m
0.68 米一路跌到 0.2 米左右------这就是 2.4 节"会倒"的数值版。data.qpos2 正是速查表里骨盆的 z 坐标。将来你接上任何步态控制器,判断"站没站稳"最直接的指标就是这条曲线。
5.2 打开同步查看器,给左肘上 -3 N·m
真正动手驱动第一个关节。新建 drive_elbow.py:
python
import mujoco
import mujoco.viewer
model = mujoco.MjModel.from_xml_path("X2_URDF-v1.4.0/X2-EDU.xml")
data = mujoco.MjData(model)
actuator_id = mujoco.mj_name2id(
model, mujoco.mjtObj.mjOBJ_ACTUATOR, "motor_left_elbow_joint"
)
with mujoco.viewer.launch_passive(model, data) as viewer:
while viewer.is_running():
data.ctrl[actuator_id] = -3.0 # 3 N·m,负方向即弯肘方向
mujoco.mj_step(model, data)
viewer.sync()
运行后你会看到左前臂以肘关节为轴缓缓弯起(左肘关节范围是 -2.356 ~ 0 rad,负方向对应弯肘;motor_left_elbow_joint 的 ctrlrange 是 ±36 N·m,3 N·m 在合法范围内),同时全身照旧瘫倒------其余 28 个电机没有输出,这完全正常。
核心认知: 是力矩执行器,data.ctrl 的单位是 N·m。想要"肘部弯到 90°",你需要自己写力矩控制器(比如最简单的 PD 控制:torque = kp * (目标角 - 当前角) - kd * 当前角速),这就是机器人控制与强化学习入门的第一道练习题。
两个可选的小实验:
- 把 -3.0 改成 +3.0(伸肘方向),左臂会被关节限位挡在 0° 附近伸直;加大到 -36.0(满力矩),感受力矩大小与运动速度的关系;设成 -40 试试,观察 MuJoCo 如何把超限控制量裁剪回 -36
- 在 while 前加一行 model.opt.gravity: = 0,整机进入失重模式飘在半空,可以不受"倒下"干扰、专注观察肘关节动作------顺便理解了为什么空间站要做机械臂实验
5.3 读传感器
控制器不仅输出力矩,还要读状态。MJCF 里定义好的传感器用名字直接访问:
python
import mujoco
model = mujoco.MjModel.from_xml_path("X2_URDF-v1.4.0/X2-EDU.xml")
data = mujoco.MjData(model)
mujoco.mj_forward(model, data) # 只算一步运动学/动力学,不推进时间
print("IMU 姿态(四元数 wxyz):", data.sensor("body-orientation").data)
print("IMU 角速度:", data.sensor("body-angular-velocity").data)
print("左肘关节角:", data.sensor("jointpos_left_elbow_joint").data)
print("左肘关节角速:", data.sensor("jointvel_left_elbow_joint").data)
初始时刻输出:四元数 1 0 0 0(竖直站立)、角速度全零、左肘 0 rad。把 mj_forward 换进 5.2 的主循环,每步读一次,就是一个最小可用的"闭环"骨架------剩下的只是把力矩计算换成你的控制律。
六、常见问题 FAQ
Q1:URDF 和 MJCF 该看哪个?
分工明确:VSCode 里预览结构用 URDF(装 URDF Visualizer 扩展直接打开),跑仿真用官方调好的 MJCF:
bash
# 通道一(看结构):VSCode 打开 .urdf 文件,URDF Visualizer 扩展自动渲染
# 通道二(做仿真):MuJoCo 加载官方调好的 .xml
python -m mujoco.viewer --mjcf X2_URDF-v1.4.0/scene.xml
两种格式在仓库里一一对应,注意末端变体只有 URDF 版:
| 机型 / 变体 | URDF | MJCF |
|---|---|---|
| 旗舰版 | x2_ultra.urdf | x2_ultra.xml |
| 假拳末端变体 | x2_fist.urdf | x2_fist.xml |
| 旗舰焕新版 | X2-Ultra.urdf | X2-Ultra.xml |
| 灵巧手末端 | X2-Ultra_omnihand.urdf | 无 |
| 夹爪末端 | X2-Ultra_omnipicker.urdf | 无 |
| 人人造 | X2-EDU.urdf | X2-EDU.xml |
Q2:加载报错找不到 STL?
meshdir="meshes/" 是相对 XML 所在目录解析的,把 XML 单独拷走必然报错。复现与修复:
python
import mujoco
# 反例:把 X2-EDU.xml 单独拷到桌面再加载,meshdir 随之失效
model = mujoco.MjModel.from_xml_path(r"C:\Users\you\Desktop\X2-EDU.xml")
# 报错关键词类似:Error opening file: meshes/pelvis.stl
# 正确做法一:从仓库根目录按相对路径加载
model = mujoco.MjModel.from_xml_path("X2_URDF-v1.4.0/X2-EDU.xml")
# 正确做法二:脚本放任何目录都行,用绝对路径指向仓库内的 XML
model = mujoco.MjModel.from_xml_path(r"D:\code\agibot_x2_urdf\X2_URDF-v1.4.0\X2-EDU.xml")
记住一个原则:XML 和 meshes/ 目录必须待在一起,要么整目录拷贝,要么原地引用。
Q3:直接打开 X2-EDU.xml 没有地面?
机器人本体文件里没有地板,加载后会一直下坠。命令对比:
bash
# 能打开,但脚下无地板,机器人无限下坠
python -m mujoco.viewer --mjcf X2_URDF-v1.4.0/X2-EDU.xml
# 正确姿势:通过场景壳启动
python -m mujoco.viewer --mjcf X2_URDF-v1.4.0/scene.xml
想看 EDU 版,复制一份 scene.xml 改名 scene_edu.xml:功能上只改 include 一行即可(model 名只是元数据标签,顺手改成 X2-EDU scene 更清晰):
xml
<mujoco model="X2-EDU scene">
<include file="X2-EDU.xml"/> <!-- 唯一改动:X2-Ultra.xml 改成 X2-EDU.xml -->
<statistic center="0 0 0.6" extent="1.3"/>
<visual>
<headlight diffuse="0.5 0.5 0.5" ambient="0.2 0.2 0.2" specular="0.9 0.9 0.9"/>
<rgba haze="0.15 0.25 0.35 1"/>
<global azimuth="200" elevation="-20"/>
</visual>
<asset>
<texture type="skybox" builtin="gradient" rgb1="0.3 0.5 0.7" rgb2="0 0 0" width="512" height="3072"/>
<texture type="2d" name="groundplane" builtin="checker" mark="edge" rgb1="0.3 0.52 0.63" rgb2="0.3 0.52 0.63" markrgb="1 1 1" width="300" height="300"/>
<material name="groundplane" texture="groundplane" texuniform="true" texrepeat="5 5" reflectance="0.2"/>
</asset>
<worldbody>
<geom name="floor" size="0 0 0.05" type="plane" material="groundplane"/>
</worldbody>
</mujoco>
Q4:我的 X2 该用哪个文件?
看后颈铭牌最上面一行的结尾,三种机型对应三条启动命令:
bash
# 旗舰版(铭牌结尾 X2 Ultra)
python -m mujoco.viewer --mjcf X2_URDF-v1.3.0/scene.xml
# 旗舰焕新版(铭牌结尾 X2 Ultra -N)
python -m mujoco.viewer --mjcf X2_URDF-v1.4.0/scene.xml
# 人人造(铭牌结尾 X2 EDU):用 Q3 里的 scene_edu.xml
python -m mujoco.viewer --mjcf X2_URDF-v1.4.0/scene_edu.xml
拿不准就跑个一键体检脚本,三个模型全部加载一遍,用执行器数区分(EDU 29,其余 31):
python
import mujoco
for f in [
"X2_URDF-v1.3.0/x2_ultra.xml",
"X2_URDF-v1.4.0/X2-Ultra.xml",
"X2_URDF-v1.4.0/X2-EDU.xml",
]:
m = mujoco.MjModel.from_xml_path(f)
print(f, "-> nu =", m.nu)
预期输出:
bash
X2_URDF-v1.3.0/x2_ultra.xml -> nu = 31
X2_URDF-v1.4.0/X2-Ultra.xml -> nu = 31
X2_URDF-v1.4.0/X2-EDU.xml -> nu = 29
顺带一提,v1.3.0 的 x2_fist.xml 是带"假拳"末端的变体,拳面可当操作末端用。
Q5:想要更轻量的碰撞模型?
MJCF 的 collision 类本身就是简化几何,仓库里实际存在两种碰撞风格(都是真实源码行):
xml
<!-- 风格一:原始几何近似,绝大多数连杆都是这种,默认已经轻量 -->
<geom class="collision" size="0.0615 0.02" pos="0 0 -0.11" type="cylinder" rgba="1 1 1 1" />
<!-- 风格二:网格碰撞,仅膝盖、躯干、肘部、手腕 roll 共 7 处连杆使用 -->
<geom class="collision" type="mesh" rgba="1 1 1 1" mesh="left_knee_link" />
性能敏感的场景可以仿照风格一,把网格碰撞体换成圆柱/盒。如果用的是 URDF,官方还有进一步削减过面数的版本:
bash
X2_URDF-v1.3.0/x2_ultra_simple_collision.urdf
X2_URDF-v1.4.0/X2-Ultra_simple_collision.urdf
X2_URDF-v1.4.0/X2-EDU_simple_collision.urdf
Q6:质量、惯量从哪来?能信吗?
每个 都写死了数值(CAD 导出),碰撞体不参与质量计算。用脚本把各连杆质量列出来,最重的零件一目了然:
python
import mujoco
model = mujoco.MjModel.from_xml_path("X2_URDF-v1.4.0/X2-EDU.xml")
masses = []
for i in range(1, model.nbody): # 从 1 开始,跳过 0 号世界体
name = mujoco.mj_id2name(model, mujoco.mjtObj.mjOBJ_BODY, i)
masses.append((model.body_mass[i], name))
for m, name in sorted(masses, reverse=True)[:5]:
print(f"{m:8.4f} kg {name}")
print("整机合计:", round(model.body_mass.sum(), 2), "kg")
预期输出(数值直接来自 XML 里的 inertial 声明):
bash
10.5942 kg torso_link
3.5901 kg pelvis
1.8877 kg left_hip_yaw_link
1.8728 kg right_hip_yaw_link
1.7533 kg waist_yaw_link
整机合计: 41.2 kg
躯干独占四分之一的质量,这就是为什么腰部 yaw 关节配了 ±120 N·m 的大扭矩电机。改模型时不要动这些数,除非你知道自己在做什么。
写在最后
回头看这篇文章做的事:装好 MuJoCo → 用 21 行的 scene.xml 打开机器人 → 把 365 行的 X2-EDU.xml 拆成六层结构 → 用三个数字和两张速查表掌握模型维度 → 写了三个脚本,分别完成观察、驱动、读反馈。全程没有一行"魔法代码",每个 XML 片段都能在 agibot_x2_urdf 仓库里找到原文。
人形机器人开发的门槛,一半在算法,一半在"能不能把模型打开、看懂、驱动起来"。今天你已经完成了后者。下一步,该轮到你给这具身体写小脑了。
访问启元机器人开发者社区,获取 SDK、仿真工具、开发文档,完成从代码仿真到真机落地的全流程开发:https://dev.primebot.com/