从零学习 MuJoCo1从零跑通第一个物理仿真作者晓柒 · 从零学习 MuJoCo 系列想象把一个小球放在离地面 1 米高的地方然后松手。它会在重力作用下落到地面。在 MuJoCo 中复现这个过程需要做两件事先说明“小球和地面是什么样”再让程序一步一步计算“小球接下来会在哪里”。这篇教程就从这个例子开始。你将创建两个文件运行一个小球下落的仿真并通过修改几个数字弄清楚模型、运动计算和画面显示是怎样配合的。图 1先搭建场景再计算运动最后观察结果。1. MuJoCo 怎样让小球动起来MuJoCo 是一个物理仿真引擎可以把它理解成负责计算物体运动的程序。以小球为例程序知道这些信息小球的半径和质量小球此刻的位置和速度重力的大小与方向地面在哪里。有了这些信息MuJoCo 就能计算一小段时间之后小球的位置和速度。反复计算小球的运动过程就产生了。**Viewer 是显示仿真画面的窗口。**本例使用的launch_passive模式由 Python 程序控制仿真程序算一步再把结果同步到 Viewer。窗口负责显示循环负责推动小球运动。图 2mj_step()计算下一步viewer.sync()将结果同步到画面。先分清“设计图”和“当前状态”运行 MuJoCo 时经常会看到两个名字MjModel和MjData。可以用同一个小球来区分它们对象可以怎样理解小球例子MjModel场景的“设计图”保存模型结构和参数小球半径 0.1 米、质量 1 千克重力沿 z 轴向下MjData仿真的“状态记录表”保存当前状态和计算结果现在过去了多久、小球在哪里、正以多快的速度运动小球下落时半径和质量通常不变位置和速度却一直在变化。因此程序可以一直使用同一份model不断更新data。这两个名字暂时不用死记。后面的代码里model对应设计图data对应当前状态。“前进一步”是前进多久本例把每一步的时间设为0.01秒optiontimestep0.01/timestep就是“每次计算让仿真时间前进多久”。因此调用 1 次 mj_step() → 仿真前进 0.01 秒 调用 10 次 mj_step() → 仿真前进 0.1 秒 调用 100 次 mj_step() → 仿真前进约 1 秒这里说的是模拟世界过去的时间。电脑实际花了多久还取决于计算速度。后面的 Python 程序会用sleep适当等待让画面速度大致接近真实时间。2. 用 XML 搭一个“小球地面”的场景XML 是一种用标签组织内容的文本格式。MuJoCo 使用的这套模型描述格式叫MJCF。在这篇教程中XML 文件负责写清楚“场景里有什么”。先新建一个文件夹把下面的内容保存为其中的hello.xmlmujocomodelhello_mujoco!-- 每步前进 0.01 秒重力沿 z 轴向下 --optiontimestep0.01gravity0 0 -9.81/worldbody!-- 地面 --geomnamegroundtypeplanesize2 2 0.1/!-- 小球球心初始高度为 1 米 --bodynameballpos0 0 1freejoint/geomnameball_geomtypespheresize0.1mass1//body/worldbody/mujoco这段代码可以从外到内读mujoco包住整个模型worldbody里放地面和小球小球的body里面再写它的运动方式和形状。下面把其中几个关键部分拆开说明。worldbody物体放在哪里worldbody是整个场景的世界根节点可以把它理解成放置物体的最外层。本例中它里面有两样东西worldbody ├── 地面 ground └── 小球 ball地面直接放在worldbody下固定在世界中。小球则单独放进一个body方便定义它的位置和运动方式。body和geom物体与形状先看这两行bodynameballpos0 0 1geomtypespheresize0.1mass1//body可以把它们读成一句话建立一个名为 ball 的刚体把它放在(0, 0, 1)给它一个半径为 0.1 米、质量为 1 千克的球形几何体。“刚体”就是运动时形状不发生变化的物体。本例把小球看作刚体不考虑它被压扁或拉长。这里的分工是body表示这个刚体用于组织物体的位置和层级关系。geom描述它的形状还可以参与碰撞与质量计算。一个body也可以有多个geom。例如用几个简单形状拼成一件复杂物体它们仍然属于同一个刚体。pos0 0 1中的三个数字依次对应 x、y、z。本例把 z 轴作为高度方向所以最后一个数字1表示球心在地面上方 1 米。freejoint小球为什么可以下落完整模型比上面的两行示例多了一行freejoint/它为小球添加一个自由关节让小球可以在空间中自由移动和转动。这里的“自由度”指独立的运动方式。小球可以沿 x、y、z 三个方向移动也可以绕三个方向转动共6 个运动自由度。如果删掉freejoint/这个小球就固定在世界中的初始位置。即使重力还在它也不会自由下落。所以想让物体动起来需要同时考虑它受到什么力以及模型允许它怎样运动。gravity重力朝哪边gravity0 0 -9.81这三个数依次表示 x、y、z 方向的重力加速度。前两个为 0最后一个为负数表示重力沿 z 轴负方向也就是本例的“向下”。-9.81的单位是米每二次方秒常写成m/s²。先记住它控制重力大小与方向后面可以直接改数字观察效果。3. 用 Python 让场景运行起来XML 已经说明了场景内容。接下来需要一个 Python 程序完成三件事加载场景、计算运动、显示结果。在hello.xml旁边新建hello.py文件结构如下你的项目文件夹/ ├── hello.xml └── hello.py把下面的代码保存到hello.pyimporttimefrompathlibimportPathimportmujocoimportmujoco.viewer# 在当前 Python 文件旁边寻找 hello.xmlxml_pathPath(__file__).parent/hello.xml# 加载模型“设计图”再创建一份“状态记录表”modelmujoco.MjModel.from_xml_path(str(xml_path))datamujoco.MjData(model)# 打开窗口只要窗口没关闭就不断计算和显示withmujoco.viewer.launch_passive(model,data)asviewer:whileviewer.is_running():step_starttime.perf_counter()# 计算下一个仿真时刻的状态mujoco.mj_step(model,data)# 把最新状态同步到窗口viewer.sync()# 如果本次计算用时不足 0.01 秒就等待剩余时间remainingmodel.opt.timestep-(time.perf_counter()-step_start)ifremaining0:time.sleep(remaining)加载模型读入“设计图”modelmujoco.MjModel.from_xml_path(str(xml_path))这一行读取hello.xml把里面的小球、地面、重力等描述转换成 MuJoCo 可以使用的模型。xml_path是 XML 文件的位置。前面的Path(__file__).parent表示当前 Python 文件所在的文件夹因此代码会到hello.py旁边寻找hello.xml。创建数据记录“现在是什么样”datamujoco.MjData(model)模型加载好后需要一份data来保存当前状态。仿真开始时它包含初始位置和速度随着程序运行它会不断更新。循环计算一步接着一步循环里最关键的是这两行mujoco.mj_step(model,data)viewer.sync()第一行让 MuJoCo 根据当前状态计算下一步并更新data第二行让 Viewer 显示更新后的结果。本例的timestep是0.01秒所以每调用一次mj_step()仿真就向前走0.01秒。while viewer.is_running()会反复执行它们直到窗口关闭。最后的sleep用于控制观看速度如果电脑很快算完了一步就稍等一下再继续。它只能让速度大致接近真实时间不能保证精确计时。4. 安装 MuJoCo 并运行程序以下操作使用Windows PowerShell并假设电脑已经安装 Python。先在保存hello.xml和hello.py的文件夹中打开终端。第一步创建项目环境python-m venv.venv.\.venv\Scripts\Activate.ps1第一行创建一个名为.venv的虚拟环境第二行进入这个环境。可以把它理解成项目自己的“工具箱”MuJoCo 等依赖包安装在这里便于与其他项目分开管理。第二步安装并检查python-m pip install mujoco python-cimport mujoco; print(mujoco.__version__)如果第二条命令能输出版本号就说明当前环境能够找到 MuJoCo。第三步运行python hello.pyViewer 打开后小球应从空中下落并与地面发生碰撞。具体的弹跳幅度与停稳过程会受到接触参数等因素影响先观察下落和接触地面的过程。图 3本例程序的运行画面。小球的位置由程序持续计算再同步到 Viewer。除了看画面也可以看数字想知道程序里记录的小球高度可以在循环中的mj_step()后面加一行打印mujoco.mj_step(model,data)print(ftime{data.time:.2f}s, ball_z{data.qpos[2]:.3f}m)viewer.sync()其中data.time是仿真已经过去的时间。在这个只有一个自由关节的小球模型中data.qpos的前 3 个数是小球的位置data.qpos[2]就是球心高度。运行后应看到时间不断增加球心高度从约 1 米开始下降。这里打印的是球心高度所以球接触地面时数值也不会直接变成 0。确认后可删掉打印语句避免终端一直刷屏。5. 改几个数字看看结果是否符合预期每次只改一个地方保存 XML 后重新运行程序。完成一项实验后恢复原值再试下一项这样更容易判断是哪项修改影响了运动。实验一让小球从更高处落下把pos0 0 1改成pos0 0 2最后一个数字是高度所以小球的起点从 1 米变成了 2 米。它需要下落更长的距离碰到地面的时间也会更晚。实验二让重力变小把gravity0 0 -9.81改成gravity0 0 -2重力仍然向下但数值的绝对值变小。小球下落时速度增加得更慢看起来也会落得更慢。实验三让重力朝另一个方向把重力改成gravity9.81 0 0现在重力沿 x 轴正方向小球会朝这个方向运动。这个实验说明场景中的“向下”由重力向量决定MuJoCo 会按照你写下的三个数字计算。实验四把小球固定住恢复原来的重力再删除freejoint/重新运行后小球会固定在初始位置。这个实验把“受到重力”和“允许移动”区分开来自由关节提供了移动的可能重力才会让小球自由下落。6. 遇到报错时先检查什么No module named mujoco这表示运行程序的 Python 环境找不到 MuJoCo。电脑上可能有多个 Python或者编辑器选用了另一个环境。在运行hello.py的同一个终端执行python-m pip install mujoco python-cimport mujoco; print(mujoco.__version__)如果使用 VS Code还需要打开命令面板选择Python: Select Interpreter再选中项目里的.venv\Scripts\python.exe。安装包和运行脚本要使用同一个 Python 环境。找不到hello.xml检查hello.xml是否放在hello.py旁边以及文件名是否与程序中的写法一致。还要留意文件是否误存成了hello.xml.txt。窗口打开了小球却不动先检查三个地方循环里有没有mj_step(model, data)后面有没有viewer.sync()小球的 XML 中有没有freejoint/。这三处分别关系到计算运动、更新显示和允许运动。7. 记住这几个对应关系名称本例中的作用XML / MJCF写下场景规则小球、地面、重力等MjModel加载后的模型“设计图”MjData小球当前的状态记录worldbody整个场景的最外层body定义一个刚体geom描述刚体的形状并参与碰撞等计算freejoint让小球可以自由移动和转动timestep每次计算让仿真时间前进多久mj_step()计算下一步的状态viewer.sync()把最新状态同步到画面回到最初的小球XML 说明它是什么样、从哪里出发Python 加载模型并创建状态mj_step()计算它接下来会在哪里Viewer 把结果显示出来。这条流程同样适用于后续的机械臂和机器人仿真。下一篇将用固定方块、自由方块和单摆进一步解释body、geom和joint如何配合。参考资料MuJoCo 官方概览模型、数据与仿真步进MuJoCo Python 文档安装与 ViewerMuJoCo XML 参考body、geom 与 freejoint