XML 模型基础:MJCF 格式解剖
说实话,刚接触 MuJoCo 的时候,我也被那一堆 XML 标签搞得有点懵。但用久了你会发现,MJCF 的设计其实很优雅——它把机器人模型拆成了几个核心零件,你只要搞懂 body、joint、geom 这三个东西,基本就能搭出任何模型了。
这一章,我们就从零开始,手搓一个简单的机械臂。我会把我踩过的坑、总结的习惯,都揉进去讲。
MJCF 文件长什么样?
一个标准的 MJCF 文件,结构大概是这样的:
<mujoco model="simple_arm">
<compiler angle="radian" />
<option gravity="0 0 -9.81" />
<worldbody>
<body name="base" pos="0 0 0">
<geom type="box" size="0.1 0.1 0.05" rgba="0.5 0.5 0.5 1" />
<joint name="shoulder" type="hinge" axis="0 0 1" pos="0 0 0" />
<body name="upper_arm" pos="0 0 0.05">
<geom type="cylinder" size="0.03 0.2" rgba="0.2 0.6 0.8 1" />
<joint name="elbow" type="hinge" axis="0 1 0" pos="0 0 0.2" />
<body name="forearm" pos="0 0 0.2">
<geom type="cylinder" size="0.025 0.15" rgba="0.8 0.3 0.2 1" />
</body>
</body>
</body>
</worldbody>
</mujoco>
嗯,看着有点嵌套,但别怕。我们一层层拆开看。
三大核心元素:body、joint、geom
我个人习惯把这三个元素比作「骨架、关节、皮肉」:
| 元素 | 类比 | 作用 | 关键属性 |
|---|---|---|---|
| body | 骨架段 | 定义刚体段,是空间中的参考系 | name, pos, quat, euler |
| joint | 关节 | 定义 body 之间的运动自由度 | name, type, axis, pos, range |
| geom | 皮肉 | 定义碰撞体和视觉形状 | type, size, rgba, friction |
body:一切的基础
每个 <body> 代表一个刚体段。它有自己的坐标系,子 body 会继承父 body 的位置和朝向。说白了,这就是个「空间容器」。
我在项目中遇到过一个问题:新手经常把 body 的 pos 设成相对于世界坐标的值,结果模型一跑就飞了。记住,body 的 pos 是相对于父 body 的,不是世界坐标。
<worldbody> 下加一个网格 geom,方便观察位置关系:<geom type="plane" size="1 1 0.01" rgba="0.9 0.9 0.9 1" />
joint:让机械臂动起来
没有 joint,你的模型就是一坨死物。joint 定义了 body 之间怎么相对运动。
常用的 joint 类型就三种:
- hinge:旋转关节,绕一个轴转。机械臂的肩、肘基本都是这个。
- slide:滑动关节,沿一个轴平移。比如直线导轨。
- free:自由关节,6 个自由度全放开。一般只给基座用。
这里有个坑——axis 属性。我曾经把肘关节的轴设成 0 0 1(绕 Z 轴转),结果机械臂像扭麻花一样。后来才意识到,axis 是相对于父 body 坐标系的,不是世界坐标系。
pos 属性也很容易搞错。它定义的是关节在父 body 坐标系中的位置。如果你不写,默认是父 body 的原点。但机械臂的关节通常不在 body 中心,所以一定要显式指定。
geom:让模型看得见、碰得到
geom 是 MuJoCo 里最直观的元素——它就是形状。但它的作用不止于「好看」:
- 碰撞检测:MuJoCo 用 geom 算接触力。
- 视觉渲染:你看到的模型就是 geom 拼出来的。
- 质量属性:geom 可以自动计算惯性,省得你手动填。
常用的 geom 类型:
| 类型 | 适用场景 | size 参数 |
|---|---|---|
| box | 基座、方形结构 | x y z(半边长) |
| cylinder | 连杆、轴 | 半径 半高 |
| sphere | 球关节、末端 | 半径 |
| mesh | 复杂形状(STL/OBJ) | 缩放因子 |
我个人习惯:调试阶段先用 box 和 cylinder 搭骨架,确认运动学正确后,再换成精细的 mesh。这样迭代快,不容易被模型细节干扰。
从零搭建:一个 2 自由度机械臂
好,理论说完了,我们动手。下面这个例子,我一步一步拆给你看。
先画个结构图,帮你理解 body 的层级关系:
看到没?每个 body 嵌套在父 body 里,joint 定义在父 body 中,geom 附在所属 body 上。这个层级关系,就是机械臂运动学树的核心。
下面是完整的 XML:
<mujoco model="arm_2dof">
<compiler angle="degree" />
<option gravity="0 0 -9.81" />
<worldbody>
<!-- 地面 -->
<geom type="plane" size="1 1 0.01" rgba="0.9 0.9 0.9 1" />
<!-- 基座 -->
<body name="base" pos="0 0 0">
<geom type="box" size="0.08 0.08 0.04" rgba="0.4 0.4 0.4 1" />
<joint name="shoulder" type="hinge" axis="0 0 1" pos="0 0 0.04" />
<!-- 上臂 -->
<body name="upper_arm" pos="0 0 0.04">
<geom type="cylinder" size="0.03 0.2" rgba="0.2 0.6 0.8 1" pos="0 0 0.2" />
<joint name="elbow" type="hinge" axis="0 1 0" pos="0 0 0.4" />
<!-- 前臂 -->
<body name="forearm" pos="0 0 0.4">
<geom type="cylinder" size="0.025 0.15" rgba="0.8 0.3 0.2 1" pos="0 0 0.15" />
</body>
</body>
</body>
</worldbody>
</mujoco>
- 基座的 joint(shoulder)绕 Z 轴转,实现水平旋转。
- 肘关节(elbow)绕 Y 轴转,实现上下摆动。
- 每个 geom 的 pos 是相对于所属 body 的,不是世界坐标。
- joint 的 pos 定义在父 body 坐标系中,通常设在旋转中心。
常见坑与避坑指南
做这个模型的时候,我踩过几个坑,分享给你:
- geom 位置不对:我曾经把 cylinder 的 pos 忘了写,结果它从 body 原点长出来,整个机械臂看起来像断了一样。后来我养成了习惯——每个 geom 都显式写 pos,哪怕就是
0 0 0。 - joint 轴方向搞反:肘关节的 axis 写成
0 -1 0,结果机械臂往反方向弯。我的经验是:先用0 1 0或0 0 1这种标准轴,调试通了再改。 - body 嵌套层级混乱:有人把 forearm 直接挂在 worldbody 下,结果 elbow 关节控制不了它。记住,body 的父子关系就是运动学树,乱不得。
快速验证:用 MuJoCo 跑起来
写完 XML,怎么验证对不对?我一般用 MuJoCo 自带的 simulate 工具:
# 命令行直接加载
./simulate arm_2dof.xml
窗口打开后,按 Ctrl + 右键 拖拽机械臂末端,看看运动学对不对。如果关节转的方向不对,回去改 axis;如果 body 位置不对,回去改 pos。
嗯,这个过程可能会反复几次。别急,我当年调第一个模型也花了半小时。但一旦你搞懂了 body、joint、geom 的关系,后面就快了。
这一章的内容就到这里。记住三个词:body 是骨架,joint 是关节,geom 是皮肉。把这三样玩明白,MuJoCo 建模你就入门了。