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 坐标系的,不是世界坐标系。

注意:joint 的 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 的层级关系:

2 自由度机械臂 body 层级结构 worldbody body: base geom: box | joint: shoulder (hinge) body: upper_arm geom: cylinder | joint: elbow (hinge) body: forearm geom: cylinder joint 定义在父 body 内 子 body 的 pos 相对于父 body geom 只负责形状和碰撞 不参与运动学计算

看到没?每个 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 的父子关系就是运动学树,乱不得。
重要提醒:如果你在仿真里看到机械臂「散架」或者「乱抖」,99% 是 body 的 pos 或者 joint 的 axis 写错了。先检查这两个,别急着调参数。

快速验证:用 MuJoCo 跑起来

写完 XML,怎么验证对不对?我一般用 MuJoCo 自带的 simulate 工具:

# 命令行直接加载
./simulate arm_2dof.xml

窗口打开后,按 Ctrl + 右键 拖拽机械臂末端,看看运动学对不对。如果关节转的方向不对,回去改 axis;如果 body 位置不对,回去改 pos。

嗯,这个过程可能会反复几次。别急,我当年调第一个模型也花了半小时。但一旦你搞懂了 body、joint、geom 的关系,后面就快了。

这一章的内容就到这里。记住三个词:body 是骨架,joint 是关节,geom 是皮肉。把这三样玩明白,MuJoCo 建模你就入门了。