1. 环境准备与安装:避开新手第一个大坑

如果你刚接触机器人仿真或者强化学习,大概率听说过MuJoCo的大名。它是个物理引擎,专门用来做刚体动力学模拟,特点是又快又准,很多顶尖的AI实验室都用它。而mujoco_py就是它的Python接口,让你能用Python代码轻松地控制仿真世界。听起来很酷对吧?但实话实说,我第一次安装的时候,差点被劝退。网上的教程零零散散,版本对不上、依赖库缺失、GLFW报错……各种问题层出不穷。所以这部分,我打算用我踩过的所有坑,帮你铺一条最稳的安装路径。

首先,你得知道一个关键变化:MuJoCo在2021年10月发布了2.1版本,这是一个重要的分水岭。我们用的mujoco_py需要和这个版本匹配。好消息是,从2.1开始,安装过程其实简化了。坏消息是,很多老教程里的方法已经过时了。我的建议是,完全按照官方当前推荐的方式来,别去折腾那些旧的变通方法。第一步,去MuJoCo官网下载对应你操作系统(Linux或macOS)的2.1版本二进制包。注意,Windows官方已经不支持了,如果你在用Windows,最省心的办法是搞个WSL2(Windows Subsystem for Linux)的Ubuntu环境,后面的步骤就和Linux一模一样了。

下载下来的是一个压缩包,比如mujoco210-linux-x86_64.tar.gz。你需要把它解压到一个特定目录。打开你的终端,执行下面这条命令,它会创建目录并解压:

mkdir -p ~/.mujoco
tar -zxvf ~/Downloads/mujoco210-linux-x86_64.tar.gz -C ~/.mujoco

解压后,你应该在~/.mujoco/目录下看到一个mujoco210文件夹。这个路径很重要,mujoco_py默认会去这里找MuJoCo的核心库。为了确保系统能找到它,我们通常需要把它的二进制文件路径加到环境变量里。你可以把下面这行加到你的~/.bashrc或~/.zshrc文件末尾:

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$HOME/.mujoco/mujoco210/bin
export MUJOCO_PY_MUJOCO_PATH=$HOME/.mujoco/mujoco210

加完之后,别忘了执行source ~/.bashrc让配置生效。这里设置MUJOCO_PY_MUJOCO_PATH是个好习惯,相当于显式地告诉Python包:“MuJoCo库在这儿呢”,避免它自己瞎找。

接下来就是安装mujoco_py这个Python包了。听起来很简单,一句pip install mujoco-py不就完了?但这里有个版本陷阱。直接pip install可能会装上一个很老的、不兼容的版本。所以我们必须指定版本范围。我强烈建议你使用这个命令:

pip install -U 'mujoco-py<2.2,>=2.1'

这个命令确保了安装的是兼容MuJoCo 2.1的mujoco_py版本(2.1.x),同时又避免了未来可能不兼容的2.2+版本。-U参数代表升级(如果已安装旧版)。如果一切顺利,pip会开始编译并安装。这个过程可能会花几分钟,因为它需要编译一些本地代码来链接你刚下载的MuJoCo二进制库。

1.1 搞定那些烦人的依赖和报错

理想很丰满,但现实往往是,上一步的pip install命令报了一堆红色的错误。别慌,这太正常了,尤其是Linux系统。最常见的问题就是缺少系统级的开发库。mujoco_py在编译过程中需要链接OpenGL、GLFW这些图形库。我以Ubuntu系统为例,给你一个必装的依赖包列表。在终端里运行:

sudo apt update
sudo apt install libosmesa6-dev libgl1-mesa-glx libglfw3 patchelf
  • libosmesa6-dev:提供离屏渲染(Off-screen Rendering)的支持,这对于在没有显示器的服务器上运行仿真至关重要。
  • libgl1-mesa-glx:提供OpenGL的软件实现。
  • libglfw3:一个用于创建窗口和OpenGL上下文的多平台库,MuJoCo的查看器(Viewer)需要它。
  • patchelf:一个小工具,后面如果遇到库路径问题,可能会用到它来修补可执行文件。

如果你在macOS上,通常用Homebrew安装依赖会更顺畅:brew install glfw。

即便装了这些,有时还会遇到一个经典的错误:ImportError: Failed to load GLFW3 shared library.。这意思是Python的glfw包找不到底层的GLFW动态库。MuJoCo的二进制包里其实自带了一份GLFW。这时候,我们在安装mujoco_py时,可以通过环境变量临时指定库的搜索路径。你可以这样尝试:

LD_LIBRARY_PATH=$HOME/.mujoco/mujoco210/bin:$LD_LIBRARY_PATH pip install 'mujoco-py<2.2,>=2.1'

这条命令在安装时,临时将MuJoCo的bin目录加到了库搜索路径的最前面,安装程序就能找到正确的GLFW库了。

还有一个我遇到过的诡异问题,系统明明有libGL.so,但编译器就是说找不到。错误信息里包含cannot find -lGL。这通常是符号链接的问题。可以检查一下/usr/lib/x86_64-linux-gnu/目录下有没有libGL.so这个文件(通常是个指向libGL.so.1的软链接)。如果没有,可以手动创建一个:

sudo ln -s /usr/lib/x86_64-linux-gnu/libGL.so.1 /usr/lib/x86_64-linux-gnu/libGL.so

做完这些,再重新运行安装命令,成功率会高很多。安装成功后,我们来进行一个简单的“Hello World”测试,验证一切是否正常。

2. 第一个仿真程序:让世界动起来

安装成功只是万里长征第一步,接下来我们要真正用代码创造一个仿真世界。别担心,我们从最简单的开始。打开你的Python编辑器或者Jupyter Notebook,跟着我一步步来。首先,我们需要导入关键的模块,并加载一个模型。模型是MuJoCo世界的蓝图,它用XML格式定义了所有的物体、关节、执行器、传感器等等。

mujoco_py贴心地为我们准备了一些示例模型。我们可以用utils.discover_mujoco()函数自动找到mujoco_py的安装路径,进而找到它自带的模型文件。我们来加载一个经典的人形机器人(humanoid)模型:

import mujoco_py
import os

# 自动发现MuJoCo模型目录
mj_path = mujoco_py.utils.discover_mujoco()
# 拼接出人形机器人模型的完整路径
xml_path = os.path.join(mj_path, 'model', 'humanoid.xml')
print(f"模型文件路径:{xml_path}")

# 从文件路径加载模型
model = mujoco_py.load_model_from_path(xml_path)
# 创建仿真实例,这是整个仿真世界的核心控制器
sim = mujoco_py.MjSim(model)

# 看看初始的关节位置(qpos)
print("初始关节位置:", sim.data.qpos)

运行这段代码,你应该能看到一长串数字(大多是0),这就是人形机器人所有关节的初始位置。现在,这个世界是静止的。sim对象(MjSim类实例)掌管着这个仿真的所有状态,包括时间、位置、速度等等。sim.data是一个庞大的数据结构,存储着仿真每一刻的瞬时数据,比如所有刚体的位置body_xpos、速度body_xvel,接触力cfrc_ext等等。我们刚打印的sim.data.qpos就是广义坐标位置。

让世界动起来,只需要一步——调用sim.step()。这个函数会推进仿真一个时间步长(timestep),时间步长在模型XML文件里定义(比如<option timestep="0.005"/>就是0.005秒)。物理引擎会根据当前的受力、约束等,计算出下一个瞬间所有物体的新状态。

# 向前推进一个仿真步
sim.step()
# 再看一下关节位置,它们应该因为重力等原因发生了微小的变化
print("一步后的关节位置:", sim.data.qpos)

你会发现,输出数字不再全是零了,有些关节因为重力产生了微小的位移。恭喜你,你已经成功运行了一个物理仿真!但是,我们目前还“看不见”这个世界。为了可视化,我们需要请出查看器(Viewer)。

2.1 可视化与基础交互:用眼睛“调试”

仿真不能只靠打印数字,我们需要亲眼看到那个小人动起来。mujoco_py提供了MjViewer类,它能打开一个图形窗口,实时渲染仿真状态。代码非常简单:

# 创建查看器,传入我们的仿真实例
viewer = mujoco_py.MjViewer(sim)

# 现在,让我们运行一个简单的循环,让人形机器人“站”一会儿
for i in range(1000):
    sim.step()          # 推进物理仿真
    viewer.render()     # 更新查看器画面

运行这段代码,会弹出一个窗口,你会看到一个人形机器人模型瘫倒在地面上。这是因为我们还没有给任何关节施加控制力,它只是在重力作用下自由落体然后与地面碰撞。viewer.render()函数负责从当前视角(默认是自由摄像头)捕获画面并显示到窗口上。

这个查看器窗口是交互式的!你可以用鼠标拖拽来旋转视角,滚动滚轮缩放。还有一些快捷键非常有用:

  • TAB键:在模型文件中定义的不同摄像机视角间切换。
  • 空格键:暂停/继续仿真。
  • 右键(在窗口上点击右键):单步执行,每点击一次,仿真只前进一步,方便逐帧调试。
  • C键:切换是否显示接触力(Contact Forces)。开启后,你会看到物体接触点出现红色的小箭头,表示力的方向和大小,这对于调试碰撞行为极其有用。
  • R键:切换几何体(Geom)的透明度,方便你看清被遮挡的内部结构。

试着在循环里加一点控制信号。虽然我们还没深入讲解执行器,但可以先简单操作一下。对于人形机器人,我们可以直接给它的执行器(在模型中已定义)一个很小的随机力,看看效果:

import numpy as np
for _ in range(500):
    # 生成与执行器数量相同的随机控制信号,范围在-0.01到0.01之间
    sim.data.ctrl[:] = np.random.uniform(low=-0.01, high=0.01, size=sim.model.nu)
    sim.step()
    viewer.render()

你会看到机器人开始抽搐、抖动。虽然这离优雅的行走还差得远,但它证明了我们已经可以通过sim.data.ctrl这个数组来向仿真世界施加控制输入了。sim.model.nu就是模型中执行器的数量。通过这个简单的“创建-加载-仿真-可视化”流程,你已经掌握了mujoco_py最核心的工作流。接下来,我们要深入看看这个世界的内部数据是如何组织的。

3. 深入数据核心:理解PyMjData与状态控制

当你看到sim.data后面那一长串属性名(qpos, qvel, body_xpos, cfrc_ext...)时,可能会有点懵。这些直接对应着MuJoCo底层C库的mjData结构体。mujoco_py通过自动生成的包装器(PyMjData)将其暴露给Python。理解这些数据是进行高级操作和算法设计(比如强化学习)的基础。我们可以把它们分成几大类来理解。

第一类是广义状态。这是描述系统整体位形的核心数据。

  • qpos:广义坐标位置。对于旋转关节,这是角度(弧度);对于滑动关节,这是位移(米);对于自由关节(free joint),这是位置和四元数姿态。它的长度等于模型的总自由度(nq)。
  • qvel:广义坐标速度。是qpos对应的时间导数。长度等于速度自由度(nv)。
  • act:执行器激活状态(如果执行器模型有内部动力学,比如肌肉模型)。
  • time:当前的仿真时间(秒)。

第二类是空间状态。这些数据描述了每个物体(body)在三维世界中的具体状态,更直观。

  • body_xpos:一个(nbody, 3)的数组,表示每个物体在世界坐标系下的位置(x, y, z)。
  • body_xquat:一个(nbody, 4)的数组,表示每个物体的姿态,用四元数[w, x, y, z]表示。
  • body_xvelp:物体的线速度。
  • body_xvelr:物体的角速度。

第三类是接触与力。这对于机器人抓取、操控等任务至关重要。

  • ncon:当前发生的接触对的数量。
  • contact:一个包含所有接触对信息的结构数组,比如接触位置、法向量、摩擦力等。
  • cfrc_ext:一个(nbody, 6)的数组,表示施加在每个物体上的外部空间力( wrench,包含力和扭矩)。这是你施加外力(比如模拟风、推力)的接口。

如何高效地读写这些数据呢?mujoco_py提供了非常方便的按名称访问的方法。你不需要去记晦涩的数组索引,直接用物体、关节、几何体的名字就能获取数据。例如,我想获取名叫“torso”的身体的当前位置和速度:

# 获取名为‘torso’的身体的位置和线速度
torso_pos = sim.data.get_body_xpos('torso')
torso_lin_vel = sim.data.get_body_xvelp('torso')
print(f"躯干位置:{torso_pos}, 线速度:{torso_lin_vel}")

# 获取名为‘knee’的关节的位置和速度
knee_pos = sim.data.get_joint_qpos('knee')
knee_vel = sim.data.get_joint_qvel('knee')
print(f"膝关节位置:{knee_pos}, 速度:{knee_vel}")

这些get_方法内部帮你处理了索引映射,用起来非常顺手。反过来,设置状态也很重要,比如你想把机器人摆成一个特定的初始姿势。但这里有一个非常重要的坑:直接修改sim.data.qpos后,必须调用sim.forward()! 这个函数会根据新的位置重新计算所有依赖的量,如空间姿态、接触、惯性等。如果不调用,物理状态会不一致,导致仿真出错。

# 错误做法:只改qpos,不调用forward
# sim.data.qpos[some_index] = new_value
# sim.step() # 这样可能会得到奇怪的结果或报错

# 正确做法:修改后调用forward
sim_state = sim.get_state()  # 获取当前完整状态快照
sim_state.qpos[7] = 0.5      # 假设修改第7个关节位置
sim.set_state(sim_state)      # 将状态设置回去
sim.forward()                 # **关键步骤**:前向动力学计算
sim.step()                    # 现在可以安全地步进了

sim.get_state()和sim.set_state()是处理状态重置、做实验复现的利器。它们操作的是一个MjSimState对象,里面打包了time, qpos, qvel, act等状态。在强化学习中,我们经常需要把环境重置到某个特定状态,用这对方法就非常方便。

3.1 状态序列化:保存和加载你的仿真世界

有时候,你需要把整个仿真世界(包括模型和当前状态)保存下来,可能是为了分享,也可能是为了以后复现某个实验场景。mujoco_py提供了两种主要的序列化方式:XML和MJB。

XML格式是可读的文本,你可以用任何文本编辑器打开查看和修改。它的优点是透明、易于版本控制。保存为XML:

# 获取模型的XML字符串
xml_string = sim.model.get_xml()
# 你可以把它保存到文件
with open('my_saved_model.xml', 'w') as f:
    f.write(xml_string)
# 之后可以从这个文件重新加载
model_reloaded = mujoco_py.load_model_from_path('my_saved_model.xml')

注意,get_xml()保存的是模型定义,以及当前状态作为初始关键帧。但它不包含纹理、网格等外部资源文件。如果你的模型使用了复杂的视觉外观,这些资源需要单独管理。

MJB格式是MuJoCo自定义的二进制格式。它的最大优点是自包含,会把纹理、网格等资源一起打包进一个二进制字符串里,非常适合分发和部署。

# 获取模型的MJB二进制字节串
mjb_bytes = sim.model.get_mjb()
# 可以直接从字节串加载模型,无需外部文件
model_from_mjb = mujoco_py.load_model_from_mjb(mjb_bytes)
# 验证加载是否成功
assert sim.model.nbody == model_from_mjb.nbody
print("MJB模型加载成功,刚体数量一致。")

在实际项目中,如果模型视觉复杂且需要移植,我推荐使用MJB。如果模型简单,或者你需要频繁地手动调整XML参数,那么用XML会更灵活。理解并熟练运用状态数据的获取、设置和保存,是你从“能用”到“用好”mujoco_py的关键一步。

4. 解锁高级功能:碰撞、渲染与批处理

掌握了基础仿真和数据处理后,我们可以玩点更高级的了。mujoco_py提供了一些强大的工具,能让你实现更逼真的视觉效果和更高效的运算。这些功能在机器人学、计算机视觉的交叉研究中特别有用。

首先来看看碰撞检测与交互。MuJoCo的物理引擎核心就是处理刚体接触。我们如何直观地观察和理解碰撞呢?body_interaction.py这个例子展示了不同物体(有关节的和没关节的)在碰撞时的行为差异。核心在于模型XML中几何体(<geom>)的contype(接触类型)和conaffinity(接触亲和性)属性。简单来说,contype定义了这个几何体属于哪个“接触组”,conaffinity定义了这个几何体会和哪些其他“接触组”发生碰撞。默认情况下,所有几何体都会相互碰撞。但你可以通过设置这些属性,来实现“幽灵物体”(只渲染不碰撞)或者选择性碰撞。

更酷的是动态纹理随机化(Domain Randomization)。这个技术在强化学习中用于提升模型的泛化能力,让AI智能体在视觉多变的环境中也能学会任务。disco_fetch.py示例展示了如何使用TextureModder。这个工具允许你在仿真运行时,动态地改变几何体的纹理、颜色、材质光泽度等。想象一下,你在训练一个机械臂抓取积木,通过纹理随机化,每个回合积木的颜色、花纹、反光程度都不同,这样训练出来的策略就不会过度依赖特定的视觉特征。

from mujoco_py.modder import TextureModder
# ... 创建sim和viewer之后 ...
modder = TextureModder(sim)

while True:
    for geom_name in sim.model.geom_names:
        # 随机化这个几何体的所有纹理属性
        modder.rand_all(geom_name)
    sim.step()
    viewer.render()

modder.rand_all()会随机生成纹理的色调、饱和度、明度,甚至纹理图像的偏移和缩放。你可以针对特定的几何体进行更精细的控制,比如只随机化颜色modder.rand_rgb(geom_name),或者只随机化纹理modder.rand_texture(geom_name)。这为创建无限多样的视觉训练环境打开了大门。

另一个高级特性是批处理模拟(MjSimPool)。在强化学习或大规模参数扫描中,我们经常需要并行运行成百上千个独立但相同的仿真。如果用一个for循环串行跑,会慢得无法忍受。MjSimPool就是为解决这个问题而生的。它内部管理着一组MjSim实例,并利用底层MuJoCo库的线程安全特性,允许你并行地向前步进(step)所有这些仿真。

from mujoco_py import MjSimPool
# 假设我们已经有一个原型仿真 sim_prototype
sim_prototype = MjSim(model)

# 用这个原型创建包含10个仿真的池子
pool = MjSimPool.create_from_sim(sim_prototype, nsims=10)

# 现在可以并行步进所有10个仿真!
pool.step()
# 也可以并行重置
pool.reset()

使用池子时,控制输入和读取数据需要一点技巧。因为池子里的每个仿真都是独立的,你需要分别访问它们。pool.sims是一个包含所有MjSim实例的列表。你可以这样批量设置控制信号:

import numpy as np
# 为池子里的每个仿真生成不同的随机控制信号
for i, sim in enumerate(pool.sims):
    sim.data.ctrl[:] = np.random.randn(sim.model.nu) * 0.01
# 然后一次性并行步进所有仿真
pool.step()
# 再批量读取结果,比如所有仿真的躯干高度
torso_heights = [s.data.get_body_xpos('torso')[2] for s in pool.sims]

批处理模拟能极大提升数据收集效率,尤其是在使用现代多核CPU的情况下。对于需要大量环境交互的算法,这几乎是一个必备的优化手段。

4.1 调用底层函数与自定义渲染

mujoco_py虽然封装得很好,但有时你可能需要直接调用MuJoCo的底层C函数,以实现一些非常定制化的功能。internal_functions.py示例展示了如何通过mujoco_py.functions模块来做到这一点。这个模块直接暴露了MuJoCo C库的许多内部函数。比如,mjv_room2model函数用于在“房间”坐标系和“模型”坐标系之间转换点的位置和姿态,这在处理复杂的摄像机视图时有用。

from mujoco_py import functions
import numpy as np

# 假设我们有一个渲染上下文 scn (来自 sim.render_contexts[0].scn)
model_pos = np.zeros(3)
model_quat = np.zeros(4)
room_pos = np.array([1.0, 0.5, 2.0])
room_quat = np.array([1.0, 0.0, 0.0, 0.0])  # 单位四元数

# 调用底层函数进行坐标转换
functions.mjv_room2model(model_pos, model_quat, room_pos, room_quat, scn)
print(f"转换后的模型坐标:{model_pos}")

直接使用底层函数需要你对MuJoCo的C API有一定了解,并且要非常小心内存管理和数据类型匹配(比如确保numpy数组的dtype是np.float64)。这属于高级用法,但当你需要压榨出最后一点性能或实现某个特殊效果时,它会非常强大。

最后,别忘了标记(Markers) 这个实用的可视化工具。标记是纯粹的视觉几何体,不会参与物理计算。你可以用它来画轨迹、指示目标点、显示向量(如力、速度)等。markers_demo.py展示了如何使用viewer.add_marker()。

# 在位置 (0.5, 0.5, 1.0) 添加一个红色的球体标记
viewer.add_marker(
    pos=[0.5, 0.5, 1.0],
    size=[0.05, 0.05, 0.05],  # 对于球体,size[0]是半径
    rgba=[1, 0, 0, 1],        # 红色,不透明
    type=mujoco_py.const.GEOM_SPHERE,
    label="目标点"
)
# 添加一个从原点指向(1,0,0)的箭头标记(用圆柱体模拟)
viewer.add_marker(
    pos=[0.5, 0, 0.5],        # 箭头中点
    size=[0.01, 0.5],         # 半径,半长度
    rgba=[0, 1, 0, 0.8],      # 绿色,半透明
    type=mujoco_py.const.GEOM_CYLINDER,
    euler=[0, np.pi/2, 0]     # 旋转使其指向X轴方向
)

标记只在当前渲染帧有效,如果你想让它持续显示,需要在每次viewer.render()之前重新添加。这个功能在调试算法、展示内部状态时极其有用,能让你的仿真可视化信息量提升一个档次。从碰撞交互到视觉随机化,再到并行计算和深度定制,这些高级功能将mujoco_py从一个简单的物理仿真器,变成了一个强大的机器人学与AI研究平台。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐