第 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支配,米制的 sliderange不变(第 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+ PythonMjSpec):读改模型的入口是mjSpec。- 次版本承载新特性、约三到四周一个,破坏性变更集中记在 changelog;选型按"需要哪个特性就锁哪个下限",升级前先查行为变化三处:
dampratio、dcmotor、被动 flex 碰撞。 - 分期比分层实用:解析与编译只在加载期跑一次,引擎每帧都在跑。改单位是加载期的事,改控制是运行期的事。
- "编译器"一名两义:C++ 的代码组件 ≠ MJCF 的
<compiler>属性节,后者是前者的输入。 - 仿真器各占研发管线一段,没有全能冠军;描述格式各有"母语",URDF 是最大公约数枢纽。
- URDF→MJCF 不是标签替换:树 vs 可含复杂拓扑、显式惯量 vs 可推断、
rpyvsquat、无执行器 vs 内置<actuator>。 - 引擎侧 URDF→MJCF 三行代码即可且保真;反向 MJCF→URDF 既无内置导出器也无现成的第三方工具,按语义清点是 15 类构造里 10 类彻底无槽位、3 类只能部分表达。