跳转至

第 1 章 引擎解剖与仿真生态

在建模、仿真与渲染之前,需要先明确 pip install mujoco 安装的究竟是什么。文档把代码库描述为六个功能组件,而安装产物中只有一个动态库。将两者对应起来,后续各章的现象——单位换算为何发生在加载期、控制器为何要自行实现、"看不见"为何常与渲染无关——就有了统一的解释。

1. overview 的模型流水线

overview 描述的是一条流水线,而非六个并列模块。其表述只有一句:用户在 MJCF 这一 XML 格式中定义模型,由内建编译器编译成底层数据结构 mjModel;仿真期间引擎更新 mjData 中的数据数组。展开为数据流即

flowchart LR
    ed["文本编辑器"] --> file["MJCF / URDF 文件"]
    file --> pc["解析 → mjSpec → 编译"]
    pc --> model[("mjModel<br/>编译产物")]
    model --> sim["仿真器 mj_step"]
    sim --> data[("mjData<br/>状态与输出")]
    model -. "mj_saveModel" .-> mjb[("MJB 二进制")]

MJCF 与 URDF 均可直接加载,但两条路径的终点是同一个 mjModel——这正是「MJCF 为原生格式、URDF 为外部格式」的技术含义。URDF 由内建解析器读入后按 MJCF 语义编译,能表达的只是 MuJoCo 元素的一个子集:没有执行器,也没有接触、默认值这类语义(「URDF 与 MJCF 的语义差」)。编译完成的模型也可存为二进制 MJB,载入时跳过解析与编译。

图中运行期只列一个入口:mjData 由 mj_makeData 按模型尺寸一次分配,mj_step 每拍原地更新其中的状态;mj_forward 走同一条前向管线但止于 qacc,不推进状态与时钟,两者的分工在第 5 章「mj_forward 与 mj_step」。

该流水线有三点在后续各章反复使用,需先明确:

  • 解析与编译的中间对象在 3.11 叫 mjSpec(旧文档、旧代码里叫 mjCModel)。
  • 编译器把 mjSpec 编成运行时的 mjModel(C 结构)。
  • 抽象可视化器(mjv)与渲染器(mjr)是两件事——第 7 章的"两阶段渲染"就是这个区分。

2. 各组件的结构与用法

六个组件的语言与职责先给一页索引:

组件 语言 主要职责
模拟器 / 物理引擎 C 每个时间步推进动力学——碰撞、约束求解、积分,结果写回 mjData
抽象可视化器 C 把模型与状态整理成与 OpenGL 无关的绘图原语,装进 mjvScene
OpenGL 渲染器 C 把 mjvScene 画进帧缓冲,三种后端可换
UI 框架 C 控件的绘制与事件回路,供交互式仿真器拼装面板
解析器(XML/URDF) C++ 文本与 mjSpec 之间的双向翻译,MJZ 归档的打包与解包
编译器(mjSpec → mjModel) C++ 落默认值、换算单位、推断惯量,把规格编成只读模型

内部构成与用法按功能分三组展开:每步都在算的物理引擎;把模型变成画面的可视化器、渲染器与 UI;加载期把文本变成模型的解析器与编译器。

六个组件按运行时机串成一条链:加载期各跑一次,运行期每步都在跑,出图每帧按需。组件用矩形,组件之间传递的对象用圆柱,最左端是输入文本:

flowchart LR
    subgraph L["加载期:各只跑一次"]
        direction TB
        par["解析器"] --> spec[("mjSpec")]
        spec --> comp["编译器"]
    end
    subgraph R["运行期:每个时间步"]
        direction TB
        eng["物理引擎 mj_"] --> data[("mjData")]
    end
    subgraph V["每帧按需"]
        direction TB
        mjv["可视化器 mjv_"] --> scn[("mjvScene")]
        scn --> mjr["渲染器 mjr_"]
    end
    txt["MJCF / URDF 文本"] --> par
    comp --> model[("mjModel")]
    model --> eng
    data --> mjv
    mjr --> fb[("帧缓冲")]
    ui["UI 框架 mjui_"] -.->|"控件叠在窗口上"| fb

2.1 物理引擎的四个子块

物理引擎是六个组件里唯一每步仿真都在运行的:读入编译产物 mjModel,更新运行时状态 mjData,一条 mj_step 把四个子块串成完整管线(第 5 章逐阶段拆解,求解器在第 6 章展开)。

碰撞检测

按"生成候选、过滤、细算"三步走。候选 geom 对来自两处:宽阶段(broad-phase)用改进的 sweep-and-prune 找出可能重叠的 body 对,加上 MJCF 里显式 <pair> 声明的对;body 对内部再用静态 AABB 层级树做 mid-phase 过滤。幸存的对再过四道滤网:该类型组合须有对应的碰撞函数(plane-plane 就没有)、包围球(含 margin)粗测、同体与父子体默认不建接触(weld 成一体的按一个体算)、contype/conaffinity 位掩码须兼容。最后按 geom 类型分派近阶段(near-phase)算法:plane/sphere/capsule/cylinder/ellipsoid/box 有解析碰撞器;没有解析器的(如网格)走通用凸碰撞管线——默认是基于 GJK/EPA 的 native 管线,旧管线是 libccd 的 MPR;非凸网格按凸包参与碰撞,高度场按三棱柱集合处理(computation 章)。

平面上一只自由盒,接触读数全部落在 mjData 上:

mujoco.mj_forward(m, d)                    # 接触在正向管线的碰撞阶段生成
d.ncon                                     # 本帧接触数
c = d.contact[0]
c.geom1, c.geom2, c.dist                   # geom 对;dist 为有符号距离,负值即穿透
force = np.zeros(6)
mujoco.mj_contactForce(m, d, 0, force)     # 接触系下的六维力
force[0]                                   # 法向分量

dist 的符号就是穿透方向;法向力不能直接读,要经 mj_contactForce 在接触系里取。

平滑动力学

「平滑」指不含约束:正运动学算出各体位姿,随后是质心与惯量、速度相关量、被动力(弹簧、阻尼等)与执行器输出。它回答的是"若无任何约束,系统此刻会怎么动"。一次 mj_forward 之后,这些量都在 mjData 上就地可读:

mujoco.mj_forward(m, d)      # 跑到 qacc 为止,qpos/qvel 与 time 都不动
d.xpos[body_id]              # 正向运动学:body 在世界系的位姿
d.subtree_com[body_id]       # 子树质心
d.qfrc_passive               # 被动力:弹簧、阻尼、流体
d.qfrc_bias                  # 重力与速度相关项
d.actuator_force             # 执行器本步输出

这一层没有返回值:结果全部写回字段,读 mjData 就是读计算结果。

约束动力学

把关节限位、接触、weld/connect 等式、摩擦收进一个统一的约束优化问题,求解器解出修正力,再由积分器推进状态。约束按耦合关系分岛:相互独立的约束岛(constraint islands)可并行求解,静止的岛可休眠省算力(overview)。求解器在 <option> 里选,解出的力落在 mjData 的约束数组里:

<mujoco>
  <option solver="Newton" iterations="100"/>       <!-- 约束求解器与迭代上限 -->
  <worldbody>
    <body name="a"><freejoint/><geom size=".05"/></body>
    <body name="b" pos=".2 0 0"><freejoint/><geom size=".05"/></body>
  </worldbody>
  <equality><weld body1="a" body2="b"/></equality>  <!-- 刚体焊接:三条位置 + 三条旋转 -->
</mujoco>

焊接写进去之后,d.nefc 比不写时多六条,d.efc_force 是求解器为这六条分配的力;接触、限位与等式共用同一批数组,来源由 d.efc_type 标出(第 6 章「约束模型与求解器默认值」)。

配套计算

主链之外按需调用的能力:逆动力学(由给定运动反求所需力,控制与系统辨识用)、数值导数(有限差分求转移函数雅可比,状态估计与强化学习用)、传感器采样(每步把各传感器读数写入 sensordata,第 8 章)、射线查询(沿射线求最近交点,测距类传感器的基础)。管线另留扩展点:用户回调可接管自定义力场、执行器、碰撞例程乃至默认的碰撞过滤(overview)。这类计算不进 mj_step 的主链,需要时单独调一次——逆动力学以 qacc 为输入,回来的就是所需广义力:

d.qpos[:] = q; d.qvel[:] = v; d.qacc[:] = a   # 逆动力学把 qacc 也当输入
mujoco.mj_inverse(m, d)
tau = d.qfrc_inverse                           # 复现该运动所需的广义力

雅可比(mj_jacSite)、有限差分导数(mj_differentiatePos)与射线查询(mj_ray)同属这一类,调用前同样要求 mjData 处于有效状态(第 6 章「雅可比对照与验证方法论」)。

2.2 可视化器、渲染器与 UI

三个组件接力把"模型+状态"变成画面:可视化器整理出抽象场景,渲染器画成像素,UI 把控件叠在窗口上。三者彼此解耦——场景与 OpenGL 无关,后端可换,控件不挑后端。

抽象可视化器

第 7 章「两阶段渲染」的阶段一:把 mjModel+mjData 翻译成一组与 OpenGL 无关的绘图原语,装进 mjvScene。包含四块:

  • 场景装配:哪些 body/geom/site 进入画面、以何种比例呈现;
  • 相机:自由、跟踪、目标跟随等类型;
  • 扰动交互:选中物体拖拽或旋转,施加外部力与力矩并实时看到物理响应;
  • 调试辅助几何:接触点与法向、按法向/切向分解的接触力、等价惯量盒、局部坐标系、关节与执行器轴线、自动生成的骨架与文字标签(overview)。

mjv_* 有 28 个函数被绑定,MjvScene、MjvOption、MjvCamera 类型齐全,唯一没绑定的是 C 侧的 mjv_makeScene——Python 侧改用构造器分配容量:

scn = mujoco.MjvScene(m, maxgeom=1000)      # 绘图原语容器,容量须预分配
cam = mujoco.MjvCamera(); mujoco.mjv_defaultCamera(cam)
opt = mujoco.MjvOption()                     # geomgroup / sitegroup 等显隐开关
mujoco.mjv_updateScene(m, d, opt, None, cam, mujoco.mjtCatBit.mjCAT_ALL, scn)
scn.ngeom                                    # 本帧进入场景的原语条数

把一只球写成 group="4",同一模型 scn.ngeom 少一条——过滤发生在阶段一,与有没有渲染器无关(第 7 章「可视化分组 group」用像素差复现)。

OpenGL 渲染器

阶段二,把抽象场景画进帧缓冲。能力面覆盖网格与基元、纹理与反射、阴影、雾、透明、线框、天空盒,直到需要四缓冲 OpenGL 支持的立体渲染(overview)。实现分三个可切换后端:classic(经典 OpenGL 实现)、filament(Google Filament 物理渲染后端)、noop(空实现)——输入只是 mjvScene,换后端不必动场景代码。

with mujoco.Renderer(m, 480, 640) as r:   # 建上下文:C 侧 mjr_makeContext 的封装
    r.update_scene(d, camera="cam0")      # 先走阶段一,把模型与状态装进场景
    img = r.render()                      # 阶段二:场景 → (480, 640, 3) uint8

颜色、深度与分割走同一入口,靠开关切换;后端在 import mujoco 时按 MUJOCO_GL 定下,进程内不能换(第 7 章「离屏渲染与 headless 出图」)。

UI 框架

通用控件库:滑杆、按钮、复选、下拉、多页布局,供交互式仿真器拼装控制面板。需要说明的是,mjui_* 在 Python 绑定里一个都没有(0 个函数,MjUI、MjuiSection、MjuiItem、MjuiTheme 这些类型也不存在),渲染原语 mjr_text/mjr_rectangle 反倒是在的——所以控件树的构造与事件回路只能在 C++ 侧写,或用 mujoco.viewer 的高层封装,没有「在 Python 里手搭 mjui」这条路。控件只要求一个 OpenGL 窗口,不关心底层跑哪个渲染后端;sample 里 simulate 的面板即由它绘制,鼠标键盘事件回路不在讨论范围。Python 侧拿到的是查看器句柄:

with mujoco.viewer.launch_passive(m, d) as viewer:
    viewer.sync()          # 把最新状态推给窗口;面板改动在下一帧生效

lock() 在改模型或状态时与查看器互斥(第 7 章「查看器与 Web 嵌入通路」)。

2.3 解析器与编译器

两个组件在加载期把"文本"变成"模型",各只跑一次。

解析器

文本与 mjSpec 之间的入口和出口:读入 MJCF(原生格式)与 URDF(外部格式)生成 mjSpec;把规格写回 MJCF 的出口也在这一层,「URDF → MJCF 往返实操」存出的 MJCF 即出自这里;MJZ 归档的编解码——把 XML 与网格等资产打包成单文件——同样归它(第 10 章)。两个方向进出这一层,中间对象是 mjSpec:

spec = mujoco.MjSpec.from_file("robot.xml")   # 解析:文本 → mjSpec(URDF 走同一解析器)
m = spec.compile()                            # 交给编译器,加载期语义一次定型
mujoco.mj_saveLastXML("out.xml", m)           # 出口:把内存里的规格写回 MJCF

解析只负责翻译与展开(include 就地展开、MJZ 解包、URDF 转换),不落默认值也不做单位换算——那些是编译器的事(第 10 章「序列化与文件形态」)。

编译器

mjSpec → mjModel 全部语义的落点,第 2、3 章那些规则的执行者就是它,逐项来看:

  • 解析 <default> 继承链,给未写的属性落默认值;
  • 由 body 树构建运动学结构与自由度——自由度不由用户声明,由编译器按 joint 生成(overview);
  • 按 inertiafromgeom 从 geom 推断质量与惯量,或采用显式 <inertial>(第 3 章);
  • 处理网格资产:凸包、朝向重定向、重居中;
  • 把 composite/flexcomp 宏展开成绳、布、可变形体所需的成组常规元素(overview);
  • 计算质心、包围范围等统计量(mjStatistic),供可视化器自动定标(overview)。

这些语义在加载返回的那一刻全部定型:默认值、单位换算、惯量推断此后不可变,「加载期与运行期」一节讲的就是这条时间轴。第 10 章程序化建模修改的 mjSpec,最终仍经它编译成 mjModel。这些规则的落地结果都在 mjModel 上可读,单位换算与继承链各占一例:

<mujoco>
  <compiler angle="degree" inertiafromgeom="true"/>       <!-- 角度按度读、惯量从 geom 推断 -->
  <default class="link">
    <geom rgba="0 .6 .2 1" contype="1" conaffinity="1"/>  <!-- 该类 geom 的缺省外观与碰撞位 -->
  </default>
  <worldbody>
    <body pos="0 0 1">
      <joint type="hinge" axis="0 1 0" range="-90 90"/>     <!-- 存进 mjModel 时是 ±1.5708 -->
      <geom class="link" type="box" size=".05 .05 .2" mass="1"/>
    </body>
  </worldbody>
</mujoco>

编译之后 m.jnt_range[0] 是 [-1.5708, 1.5708]、m.geom_rgba[0] 取的是 default 里那组颜色——单位换算与继承链都在这一刻落定(第 2 章「compiler 的 angle」与「compiler 属性清单与归属校验」)。

⇒ 可归纳为:解析器与编译器把"文本"变成"模型"(加载期),引擎把"模型"变成"运动"(运行期),可视化器与渲染器把"模型+状态"变成"像素"(每帧按需),UI 是这几段的控件外壳。 API 前缀即分工:mj_ 算、mjv_ 搭抽象场景、mjr_ 出图、mjs_ 改规格、mjui_ 画控件。

3. 安装产物与公开头文件

装完之后没有六个库,只有一个 libmujoco.so.3.11.0(约 6 MB),对外是一层纯 C ABI:mj_* 导出符号全部是 C 符号,C++ mangled 符号只有 mujoco::user 命名空间下少数工具函数(FilePath、StringToVector 一类),动态依赖只有 libdl/libm/libpthread/libc/librt,不含 libstdc++(nm -D 与 ldd 可查)。即:解析器与编译器确由 C++ 实现,但 C++ 运行时被静态链入这一个 so,对外只暴露 C 接口。这正是"一个库、多语言绑定"的底层原因——绑定层只需包 C 符号。因此"引擎是 C"不等于"整个库是 C":它是 C 为主、内联 C++ 的混合体。

include/mujoco/ 是这六个组件对使用者的全部可见接口,按头文件即可分类:mjmodel.h/mjdata.h 是引擎的数据结构,mjvisualize.h 是抽象可视化器,mjrender.h(含 mjrfilament.h)是渲染器,mjui.h 是 UI,mjspec.h 就是模型规格 mjSpec。

编译期还有一个内部对象 mjCModel——解析器到编译器之间的临时结构,不暴露给用户;用户拿到的是输入与输出两件:可编辑的 mjSpec 与编译产物 mjModel。供使用者读取和修改模型的入口正是 mjSpec(mjspec.h 即专属于它,Python 侧为 mujoco.MjSpec,第 10 章)。

3.1 版本编号与发布节奏

PyPI 包 mujoco 的发布遵循 major.minor.patch 编号,3.x 线上 minor 次版本承载全部新特性、patch 修订版本只做缺陷修复。changelog 的发布间隔约三到四周一个次版本(3.12.0 至 3.17.0 落在 2026 年 8 月 20 日至 10 月 22 日两个月内),每个次版本顶部以 Breaking API changes 框集中列出破坏性变更并附迁移方法;3.x 之内无 ABI 承诺,跨次版本不保证二进制兼容。

安装产物按平台与 CPython 版本分发预编译 wheel,macOS(Intel 与 Apple Silicon)、Linux、Windows 均为受支持平台。查询可用版本与升级:

pip index versions mujoco        # 首行为 PyPI 最新版,INSTALLED 行为当前环境已装版本
pip install -U mujoco            # 升级到最新版;mujoco==X.Y.Z 可锁定指定版本
python -c "import mujoco; print(mujoco.__version__)"   # 确认解释器实际导入的版本

3.2 次版本特性与破坏性变更

以 3.11.0 之后的次版本为例(changelog,2026),特性到达版本、破坏性变更与依赖侧条目如下;依赖列的“—”表示该版 changelog 未列第三方库或运行时的更新:

版本(发布) 关键新增 破坏性变更要点 依赖与工具链
3.12.0(2026-08-20) 内置 pid 执行器(带积分抗饱和与 setpoint 限斜率,取代 mujoco.pid 插件);dcmotor 改为输入签名 pos/vel/ff/voltage;MJCF 语法收敛到单一 schema 文件;纹理仅支持 PNG/KTX 自定义二进制纹理格式移除;dcmotor 增益从电压空间改为扭矩空间 —
3.13.0(2026-09-08) discrete 积分器:约束求解与隐式速度更新合并进有效度量 \(\widehat{M}=M+hD+h^2K\),大刚度位置增益可用远超显式稳定极限的步长;site 支持 mesh 几何 3.11 在 implicit/implicitfast+CG 下的 flex 有效度量特例移除,改由 discrete 承担 wheel 新增 Python 3.15(含 Free-Threading)
3.14.0(2026-09-22) 实验性 ipc 接触模式(discrete 积分器下穿透自由的 flex 接触);spec 程序化编辑从元素数平方降为增量,8000 geom 建模约快 10 倍 mj_readCtrl 签名与语义变更,支持多输入执行器 —
3.15.0(2026-10-05) dampratio 改按操作空间惯性计算,多关节传动不再过阻尼;insidesite 传感器新增 enclosed 属性 用 dampratio 的多连杆模型阻尼取值随之变化 —
3.16.0(2026-10-14) frame 编译时缩放(scale/scaling/massscale,等比缩放保持动力学);savecompiled/savecanonical 保留原始书写;flexcomp 可直接引用 mesh 资产 Filament C/Python API 批量改名;settotalmass 弃用 —
3.17.0(2026-10-22) batch API(mujoco.batch,线程池上对同一模型并行多次仿真,结果与单线程逐位一致);1D flex 弯曲弹性(离散弹性杆);纹理可按引用存储(embed="false")降低加载内存 mjNEQDATA 11→12;meaninertia 弃用;fuse_static/discard_visual 返回值改为操作计数 Python 绑定支持 Bazel 构建(先覆盖不含渲染/viewer 的 core)

跨到 major 的 4.0.0(2026-11-06)是目前最大的一次依赖形态变更:Python 发行拆成 mujoco-core(仅依 numpy 的最小绑定与 C 库)、mujoco-studio(Studio 与 Filament 渲染绑定)与默认包 mujoco(两者同装);pip 跨拆分升级要先 pip uninstall -y mujoco 再装,或升级后跑 pip install --force-reinstall --no-deps mujoco mujoco-core mujoco-studio,否则 pip 会删掉已移到 mujoco-core 的文件;同版移除 Python 3.10 支持。本章其余各章立论仍基于 3.11.0。

由此可得版本选型判据:

  • 3.11:主线各章的立论基线,按上表各版本的破坏性变更核对前保持行为一致。
  • 3.12 起:需要内置 pid 执行器。
  • 3.13 起:需要 discrete 积分器的大步长隐式阻尼与位置增益。
  • 3.16 起:需要 frame 缩放与保书写原样的模型保存。
  • 3.17 起:需要 mujoco.batch 线程池批量仿真;mujoco-mjx 亦要求 mujoco>=3.17,MJX 用户随此下限。
  • 跨版本升级前先读该版本 changelog 的 Breaking API changes 框:dampratio、dcmotor、被动 flex 碰撞三处是行为变化而非接口报错,旧模型在新版本下静默改变数值表现。
  • wheel 支持的 CPython 窗口随时间前移,过旧的 CPython 在最新版取不到预编译包——跟进最新次版本前应确认解释器版本仍在支持窗内。

4. 加载期与运行期

六个组件不仅是空间上的分层,更是时间上的分期。一个 hinge(range 以度书写)套一个 slide(range 以米书写)的两关节模型,最能体现这种分期:

<mujoco>
  <worldbody>
    <body name="h">                                    <!-- 一级连杆:世界下第一个可动 body -->
      <joint name="hj" type="hinge" axis="0 1 0" range="-90 90"/>   <!-- 绕全局 y,限位 ±90°(单位是度) -->
      <inertial mass="1" pos="0 0 0" diaginertia="0.001 0.001 0.001"/> <!-- 显式惯性 → 不走 geom 推断 -->
      <geom name="hg" type="box" size="0.05 0.05 0.2"/>       <!-- 无 mass:只做几何,不贡献动力学 -->
      <body name="s">                                   <!-- 二级连杆:pos 相对父 body,不是世界坐标 -->
        <joint name="sj" type="slide" axis="0 0 1" range="-1 1" limited="true"/> <!-- 沿自身 z 平移 ±1 m -->
        <geom name="sg" type="box" size="0.05 0.05 0.2" mass="1" pos="0 0 0.3"/> <!-- 带 mass:这一路才有惯量 -->
      </body>
    </body>
  </worldbody>
  <!-- 驱动写在树外:执行器不进入树层,也不产生 body —— 想靠它"带出"一个 body 是徒劳的 -->
  <actuator><position name="p" joint="sj" kp="100" kv="5" ctrlrange="-1 1"/></actuator>
</mujoco>
  • 加载期(一次):解析器读 XML 成 mjSpec,编译器产出 mjModel。hinge 的 -90 90 存下来是 ±1.5708(弧度),slide 的 -1 1 原样保留——只有角度类属性受 compiler/@angle 支配,米制的 slide range 不变(第 2 章)。这些取值在 from_xml_path 返回的那一刻即已确定。
  • 运行期(每帧):引擎(C)算出 qfrc_bias=[0, 9.81],即广义重力项只落在 slide 上(hinge 的质心恰在铰点,不产生力矩);施加 ctrl=1 并步进 500 步后,slide 停在 qpos≈0.902——这是位置伺服在重力场里的平衡点,由 kp(1−q)=mg 解出;它距 [-1, 1] 的限位还有余量,限位并未参与。

⇒ 可归纳为:解析器与编译器 = 一次性、加载期、C++;引擎 = 每一帧、运行期、C。 第 5 章那张 26 阶段管线里找不到"解析"和"编译",因为它们全在 mj_step 开始之前结束了。

本章最需防范的混淆是"编译器"一词在 MuJoCo 中有两个所指:一个是 C++ 写的代码组件(mjSpec → mjModel),一个是 MJCF 里的 <compiler> 属性节(angle、inertiafromgeom、meshdir……),后者是喂给前者的输入参数。写 <compiler angle="radian"/> 改的是那个隐藏组件的行为——一个是你写的配置,一个是执行配置的组件,理解时务必分开。

5. 仿真器与描述格式一览

5.1 主流机器人仿真器一览

将视角放宽:机器人仿真并不只有 MuJoCo,描述文件也不只有 MJCF。下表只收录变动较小的信息(定位、底层引擎、原生格式、许可)——加速比、显存、并行规模等数值依赖具体版本,应以各家文档为准。

仿真器 底层物理引擎 原生描述格式 生态定位 许可
MuJoCo 自研(广义坐标 + 优化式接触) MJCF(也直读 URDF) 控制 / 接触丰富操作 / RL 基线 Apache-2.0
Gazebo 可插拔(DART / Bullet / ODE…) SDF(也吃 URDF) ROS 2 系统集成、传感器与导航回归 Apache-2.0
Isaac Sim / Lab PhysX 5 / Flex USD(导入 URDF/MJCF) GPU 大规模并行 RL、合成数据、高保真渲染 免费但绑定 NVIDIA/Omniverse 条款
PyBullet Bullet 3 URDF 轻量、入门门槛低、教学与研究原型 zlib(现多被视为维护模式)
Webots ODE PROTO/VRML 教育与快速原型,内置大量模型 Apache-2.0
SAPIEN PhysX URDF + 部件级碰撞 操作向、配套 ManiSkill 基准 MIT
Drake 自研(多体 + 规划/控制) URDF / SDF / MJCF 模型驱动的设计、规划与控制 BSD-3
Genesis 多求解器统一、可微 URDF / MJCF / USD 2024 起的生成式数据与通用物理 见各家文档
Newton 基于 NVIDIA Warp、可微 USD 系 NVIDIA + DeepMind + 迪士尼联合 见各家文档

"谁更好"并不存在普适答案,只有各自适配研发管线的哪一段:MuJoCo 长于接触动力学与控制,Gazebo 长于 ROS 集成,Isaac 长于 GPU 并行与照片级渲染。后续各章以 MuJoCo 为对象,正是因为"接触 + 控制 + RL"这一链条在其表达体系中最为集中。

5.2 各家描述格式

机器人描述文件至少有五种,各自绑一个生态:

  • URDF——ROS 的主力,XML,强制树状 <link>/<joint>。运动学友好,robot_state_publisher/RViz/MoveIt 都靠它;物理表达弱:没有执行器语义(要靠 <transmission> 插件)、没有默认值、没有接触声明。
  • SDF / SDFormat——Gazebo 原生。比 URDF 多了 world/场景、多机器人、物理材质与传感器建模。
  • MJCF——MuJoCo 原生(第 2 章起全书在用),面向"算得准、稳、可复现":<default> 继承、<compiler>、<actuator>、细粒度 geom 与接触对、软约束参数 solref/solimp(第 6 章)。
  • USD / OpenUSD——Isaac/Omniverse 原生,皮克斯出身,是场景描述(DAG,可含闭环、材质、光照、动画、分层引用),比"机器人模型"大一圈。MuJoCo 自己也支持 USD 双向(导入、导出,还把 MJCF 做成 USD 文件格式插件),USD 不是 Isaac 的专利。
  • PROTO/VRML——Webots 的模型格式;另有 SRDF(MoveIt 的语义/碰撞成对描述,是 URDF 的旁挂补充)。

一个有用的心智模型:URDF 如同"机器人简历"(只描述机器人自身),USD 如同"电影剧本"(可描述整个仿真世界)。URDF 的信息量是 USD 的子集,因此 USD→URDF 的逆向转换必然有损——OpenUSD 侧亦不建议将其作为常规工作流,从 CAD 直接生成 URDF 更为可靠。

6. URDF 与 MJCF 的语义差与转换损耗

6.1 URDF 与 MJCF 的语义差

格式转换的首要误区,是认为只需将 <link>→<body>、<joint>→<joint> 逐一对应替换。两套语言的设计哲学不同:

维度 URDF MJCF 对应章节
拓扑 强制树,一父一子 允许更复杂结构(tendon / equality / flex) 第 4 章 weld/equality
默认值 几乎没有 <default> 类继承体系 本章 include/继承(第 3 章)
单位 无强制约定,常弧度/米混用 不指定单位制,自洽即可(默认值 MKS 风格) 第 2 章 angle
惯量 <inertial> 必须显式 缺省可从 geom 质量/形状推断 第 3 章惯量
视觉/碰撞 <visual> 与 <collision> 分开 统一 <geom>,靠 type/group 区分 第 3、7 章 group
驱动 <transmission> 插件(Gazebo 侧) 内置 <actuator>(position/motor…) 第 6 章执行器
朝向 origin rpy(欧拉) pos + quat,四元数 (w,x,y,z) 第 4 章四元数

⇒ URDF 将机器人"画"出来,MJCF 将机器人"算"出来。 迁移到 MJCF 的价值,在于获得 default、执行器、接触这些 URDF 中并不存在的语义;反向迁移到 URDF,这些语义会被削减。

6.2 转换方向与信息损耗

各家格式不是两两互转,而是以 URDF 为中心辐射:

flowchart LR
    cad["CAD / SolidWorks"] --> urdf["URDF<br/>最大公约数"]
    urdf --> mjcf["MJCF<br/>加载 URDF 再存,或第三方脚本"]
    urdf --> sdf["SDF<br/>Gazebo:gz sdf -p robot.urdf"]
    urdf --> usd["USD<br/>Isaac:asset.importer.urdf"]
    mjcf -. "无内置逆向:第三方,丢执行器 / default / 接触" .-> urdf

三条要点:URDF 是最大公约数,多平台分发常以它为源;转换具有方向性,转向语义更强的 MJCF/USD 是语义增补,退回 URDF 是语义缩减且有损;MJCF↔USD 已有 MuJoCo 侧实验性双向通路(导入、导出、MJCF 文件格式插件,第 2 章「MJCF 与 URDF、USD 的分工」),但 MJCF↔SDF 仍靠 URDF 中转。

7. URDF → MJCF 往返实操

实现只需三行代码。MuJoCo 可直接读取 URDF,读入后即得到编译完成的 MjModel;再用 mj_saveLastXML 存出即为 MJCF——相当于由引擎自身完成转换:

m = mujoco.MjModel.from_xml_string(urdf)   # urdf 是同目录下 3 link、1 revolute + 1 fixed 的最小机器人
mujoco.mj_saveLastXML("out.xml", m)        # 存回来的已经是合法 MJCF,可以直接改造成生产模型

## 计数即证据:3 个 link 只剩 2 个 body(世界 + 合并后的可动件),
## 1 个关节(fixed 被折叠掉了),0 个执行器(URDF 没有执行器概念)
m.nbody, m.njnt, m.ngeom, m.nu            # → (2, 1, 3, 0)

四个数字各说一件事:revolute→hinge 所以 njnt=1;fixed 关节不建关节,3 个 link 被 fusestatic 融成 2 个 body;ngeom=3 说明只有 collision 进来、<visual> 被默认的 discardvisual=true 丢掉;nu=0 说明 URDF 不会自动生成执行器(第 9 章)。

存出的 out.xml 中有三处改写最能说明"这不是标签替换":

<mujoco model="convert_arm">
  <compiler angle="radian"/>                        <!-- ① 单位显式写为弧度 -->
  <worldbody>
    <geom size="0.05 0.05 0.05" type="box"/>       <!-- base_link 融进了 worldbody -->
    <body name="link1" pos="0 0 0.05">
      <inertial pos="0 0 0.2" mass="1.5" .../>      <!-- ② link1+link2 的惯量已合并 -->
      <joint name="joint1" axis="0 0 1" range="-1.5 1.5"/>
      <geom size="0.03 0.03" pos="0 0 0.3" quat="0.968912 0 0.247404 0" type="cylinder"/>
    </body>                                         <!-- ③ 源的 rpy="0 0.5 0" 变成这个 quat -->
  </worldbody>
</mujoco>

那个 quat="0.968912 0 0.247404 0" 是绕 y 轴 0.5 rad 的半角表示(w=cos0.25、y=sin0.25)——rpy→quat、分量顺序 (w,x,y,z),正是第 4 章换算的体现;mass=1.5(1.0+0.5)是固定关节融合后重算的并体惯量(第 3 章)。再以 from_xml_path("out.xml") 载入一次,nbody/njnt/ngeom/nu 与直读 URDF 完全一致:URDF→MJCF 方向是保真的。

反向不通:MuJoCo 只提供 mj_saveLastXML(写 MJCF)与 mj_saveModel(写 MJB),并无 URDF 导出器;也没有可用的反向转换器——第三方 urdf2mjcf 只做 URDF→MJCF 单一方向(见「URDF → MJCF 往返实操」),urdf_parser_py 与 gz 工具链均无 MJCF→URDF 导出器。退回只能按语义清点,以 assets/models/01_pendulum.xml(4 关节、5 geom、2 site、3 执行器、1 传感器、1 个 default 节)为样本,15 类 MJCF 构造在 URDF 里的去处是这样的:

MJCF 构造 URDF 槽位 说明
body/joint/geom 骨架 有 一一对应,但 URDF 强制树、一父一子
<inertial> 有 <inertial>,六元组 ixx..izz 必填缺一不可
joint/@axis、@limit 部分 URDF 的 <limit> 没有 accel,摩擦在 <dynamics>
joint/@armature 无 URDF 无电机臂惯量项
contype/conaffinity/margin/gap/solref/solimp/friction(5) 无 URDF 无接触对与软约束参数
<site> 无 URDF 无参考点;MoveIt 靠旁挂 SRDF 补
<camera>/<light> 部分 需 Gazebo 扩展 <sensor>,命名不同
<actuator>(kp/kv/ctrlrange/forcerange) 无 <transmission> 只表达机械接口,不含控制律;本样本 3 路全部无处安放
<default> 类继承 无 只能靠外部模板/宏
<equality>/<tendon>/<contact> 无 URDF 不能表达闭环、腱与接触对
<compiler> 无 无编译期配置节
<option> 无 <dynamics> 只能给 damping/friction
<keyframe> 无 无命名初始态
<statistic>/<visual>/<size> 无 属仿真器侧配置
<mesh> + scale/refpos/refquat 部分 URI 可带,无编译期归一化与凸包语义

15 类里无槽位 10 类、部分可表达 3 类、完整可表达 2 类。所以退回 URDF 之后:执行器、传感器、site、default 四项直接归零,关节只剩几何与限位,阻尼与摩擦要挪进 <dynamics>。正向仍然保真(同一份 URDF 直读、存回 MJCF 再载入,nbody/njnt/ngeom/nu/nsensor 完全一致,且存出的文件里带着引擎补写的 <compiler angle="radian"> 与 quat)。第三方正向转换工具 urdf2mjcf 0.2.39(pip)对同一份文件给出保结构产物——fixed 不融合(nbody=4 vs 引擎 2)、<visual> 保留(ngeom=5 vs 3)、自动补 freejoint 与一路 motor、外加 5 个 default 类与 5 只传感器;引擎直读则是物理融合路线(fusestatic、丢 visual、nu=0)。两条路线都合法:要"结构完整可编辑"选第三方,要"物理量即用"选引擎。

8. 要点

  • overview 是一条流水线(MJCF/URDF 或 mjSpec → 编译 → mjModel),六个组件是功能划分,不是目录名——目录随版本搬家过。
  • 六组件各司其职:引擎每帧算(mj_)、可视化器搭抽象场景(mjv_)、渲染器出图(mjr_)、UI 画控件、解析器与编译器加载期把文本变模型(mjs_ 改规格)。
  • 装完之后只有一个 libmujoco.so(3.11 是 6.14 MB):202 个 C 导出 + 10 个 C++ 导出,ldd 不含 libstdc++ ⇒ C++ 静态链入,对外只露 C ABI。
  • mjCModel 在 3.11 的头文件里 0 命中、mjSpec 是公开 API(mjspec.h + Python MjSpec):读改模型的入口是 mjSpec。
  • 次版本承载新特性、约三到四周一个,破坏性变更集中记在 changelog;选型按"需要哪个特性就锁哪个下限",升级前先查行为变化三处:dampratio、dcmotor、被动 flex 碰撞。
  • 分期比分层实用:解析与编译只在加载期跑一次,引擎每帧都在跑。改单位是加载期的事,改控制是运行期的事。
  • "编译器"一名两义:C++ 的代码组件 ≠ MJCF 的 <compiler> 属性节,后者是前者的输入。
  • 仿真器各占研发管线一段,没有全能冠军;描述格式各有"母语",URDF 是最大公约数枢纽。
  • URDF→MJCF 不是标签替换:树 vs 可含复杂拓扑、显式惯量 vs 可推断、rpy vs quat、无执行器 vs 内置 <actuator>。
  • 引擎侧 URDF→MJCF 三行代码即可且保真;反向 MJCF→URDF 既无内置导出器也无现成的第三方工具,按语义清点是 15 类构造里 10 类彻底无槽位、3 类只能部分表达。

参考文献与延伸阅读

  • Todorov, E., Erez, T., & Tassa, Y. MuJoCo: A Physics Engine for Model-Based Control. IROS, 2012. DOI
  • MuJoCo. 文档入口. 文档
  • MuJoCo. 源码仓库. 仓库