一、MuJoCo 是什么?为什么灵巧手仿真都用它?
MuJoCo 全称 Multi-Joint dynamics with Contact,是 Roboti LLC 开发、后被 DeepMind 收购并开源的物理引擎。它专门为 机器人、多体系统、接触丰富 的场景设计。
1.1 和其他引擎的对比
| 引擎 |
特点 |
适合什么 |
| MuJoCo |
快、稳、接触处理优秀 |
机器人、灵巧手、抓取 |
| PyBullet |
Python 接口友好,功能全面 |
教学、入门 |
| Isaac Sim |
高保真、GPU 加速、光线追踪 |
Sim2Real、视觉仿真 |
| Gazebo |
传感器模型丰富,ROS 生态好 |
工业级移动操作平台 |
灵巧手仿真为什么选 MuJoCo?
- 快:单环境 1000+ FPS,RL 训练的基础
- 接触稳定:灵巧手每天和物体接触几百次,其他引擎容易炸
- 开源、免费:改代码无限制
- DeepMind 背书:OpenAI Dactyl 用它训练的 Shadow Hand 策略直接迁移到真机
1.2 版本说明
2022 年 DeepMind 收购后,MuJoCo 进入 2.2+ 时代:
- 完全开源(Apache 2.0 许可证)
pip install mujoco 直接安装
- 每天全球数以万计的科研/工业用户
二、安装
2.1 最简单的安装方式
等待 30 秒到 1 分钟即可完成。
2.2 验证安装
打开 Python,运行:
1 2 3
| import mujoco print(mujoco.__version__)
|
如果没报错,说明装好了。
2.3 GPU 与显示需求
MuJoCo 的物理仿真跑在 CPU 上——不需要 GPU 也能跑 1000+ FPS。GPU 只在可视化渲染时用到。如果没有显示器(如 SSH 服务器),可以关闭渲染只跑物理。
2.4 无显示器(SSH 服务器)也能用
MuJoCo 可以纯 Python 运行,无需任何 GUI:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import mujoco
xml = """ <mujoco> <worldbody> <light name="top" pos="0 0 1"/> <body name="box" pos="0 0 0.5"> <freejoint/> <geom type="box" size="0.2 0.2 0.2" rgba="1 0 0 1"/> </body> <geom name="floor" type="plane" size="1 1 0.1"/> </worldbody> </mujoco> """
m = mujoco.MjModel.from_xml_string(xml) d = mujoco.MjData(m)
for _ in range(1000): mujoco.mj_step(m, d)
print("最终高度:", d.geom_xpos[1][2])
|
这段代码不需要显示器,在服务器上也能跑。MuJoCo 的 MjModel.from_xml_string() 是一个极其有用的函数,允许你在不创建文件的情况下直接在内存中构建场景。
三、核心概念:4 个你必须搞清楚的构件
3.1 MjModel(模型)—— 机器人的「骨架和肌肉」
MjModel 是 MuJoCo 的核心数据对象,包含:
- 运动学:每个关节的类型、限位、父-子关系
- 动力学:连杆的质量、惯量
- 碰撞:几何体的形状、尺寸、摩擦系数
- 控制:电机增益、力矩限幅
MjModel 是不可变的——加载后你一般不改它。它描述了机器人长什么样,能怎么动。可以把 MjModel 理解为硬件规格书。
3.2 MjData(数据)—— 机器人的「当前状态」
MjData 包含:
qpos:所有关节的当前位置(角度/位置)
qvel:所有关节的当前速度
ctrl:你发给关节的控制信号
contact:当前的接触信息(位置、力、法向)
sensor:传感器读数(如果有定义)
MjData 每步都在变,代表了机器人现在的姿势和受力。可以把 MjData 理解为仪表盘读数。
二者关系:
1 2
| MjModel = 你的灵巧手的「设计图纸」——不变的,加载一次用全程 MjData = 你的灵巧手的「当前状态」——每步在变,reset 就重置
|
这个分离设计有一个重要优势: 想要多个并行仿真环境(RL 训练用),只需要 1 个 MjModel + N 个 MjData:
1 2 3 4 5 6 7
| model = mujoco.MjModel.from_xml_path('hand.xml') datas = [mujoco.MjData(model) for _ in range(1024)]
for d in datas: d.ctrl[:] = random_action() mujoco.mj_step(model, d)
|
1024 个环境并行跑,这就是 RL 训练的基础。
3.3 mj_step —— 仿真的「心跳」
这一行做了 6 件事:
- 读取
d.ctrl 里的控制信号
- 解动力学方程(牛顿第二定律 + 欧拉方程)
- 检测所有碰撞对
- 解接触力(摩擦锥约束、法向力)
- 更新
d.qpos、d.qvel
- 更新所有传感器
每调一次 mj_step,仿真就前进一个时间步。默认时间步长 m.opt.timestep = 0.002 秒(2ms)。1000 步 = 2 秒的真实时间。
3.4 XML 模型文件 —— 你想要仿真的「一切定义」
XML 文件描述了整个仿真世界:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <mujoco> <compiler angle="radian"/> <option timestep="0.002"/> <worldbody> <geom name="floor" type="plane" size="0 0 0.05"/> <include file="hand.xml"/> <body name="object" pos="0.3 0 0.1"> <freejoint/> <geom type="cylinder" size="0.02 0.05"/> </body> </worldbody> <actuator> <motor name="finger_motor" joint="index_joint1" gear="1"/> </actuator> </mujoco>
|
其中几个关键元素:
<body>:刚体(连杆),有质量和惯量
<joint>:关节,连接父 body 和子 body
<geom>:几何体,用于碰撞检测和可视化
<actuator>:驱动器(电机/肌肉)
<sensor>:虚拟传感器(力/力矩/触觉)
四、第一个完整示例:用 MuJoCo 跑灵巧手
4.1 加载官方 Shadow Hand
MuJoCo 自带 Shadow Dexterous Hand 的模型文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import mujoco
xml_path = mujoco.models.get_path('hand.xml') print("模型路径:", xml_path)
model = mujoco.MjModel.from_xml_path(xml_path) data = mujoco.MjData(model)
print("关节数量 (nq):", model.nq) print("控制数量 (nu):", model.nu) print("身体数量:", model.nbody) print("几何体数量:", model.ngeom)
|
输出示例:
1 2 3 4 5
| 模型路径: /usr/lib/python3/dist-packages/mujoco/models/hand.xml 关节数量 (nq): 26 控制数量 (nu): 20 身体数量: 49 几何体数量: 54
|
26 个自由度、20 个控制通道——这就是灵巧手比普通机械臂复杂的地方。
4.2 查看并控制关节
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import mujoco import numpy as np
model = mujoco.MjModel.from_xml_path(mujoco.models.get_path('hand.xml')) data = mujoco.MjData(model)
for i in range(model.nu): name = mujoco.mj_id2name(model, mujoco.mjtObj.mjOBJ_ACTUATOR, i) print(f"控制通道 {i}: {name}")
data.ctrl[0] = 1.0
for step in range(1000): mujoco.mj_step(model, data) if step % 100 == 0: print(f"步 {step}: 拇指关节角度 = {data.qpos[0]:.3f} rad")
|
输出示例:
1 2 3 4 5 6 7
| 控制通道 0: TH 控制通道 1: FF ... 步 0: 拇指关节角度 = 0.000 rad 步 100: 拇指关节角度 = 0.037 rad 步 200: 拇指关节角度 = 0.068 rad 步 300: 拇指关节角度 = 0.094 rad
|
手指在你的控制下开始弯曲了!
4.3 用仿真频率和控制频率解耦
1 2
| 仿真频率:1000 Hz(mj_step 每步 1ms) 控制频率:50 Hz(策略每 20ms 发一次指令)
|
你的策略不需要每步都发指令:
1 2 3 4
| for step in range(1000): if step % 20 == 0: data.ctrl[:] = policy(data) mujoco.mj_step(model, data)
|
这模拟了真实机器人的控制频率限制——真机上的电机通常只能以 50-100 Hz 更新。
五、核心 API:你需要掌握的完整手册
你不需要记住 MuJoCo 的所有 API,但需要熟练掌握下面这些:
5.1 模型加载
1 2 3 4 5 6 7 8 9 10 11 12 13
| model = mujoco.MjModel.from_xml_path('path/to/hand.xml')
model = mujoco.MjModel.from_xml_string(xml_string)
model = mujoco.MjModel.from_xml_path( mujoco.models.get_path('hand.xml') )
model = mujoco.MjModel.from_xml_bytes(xml_bytes)
|
5.2 仿真步进
1 2 3 4 5 6 7 8 9 10
| data = mujoco.MjData(model)
mujoco.mj_step(model, data)
mujoco.mj_step(model, data, nstep=10)
mujoco.mj_forward(model, data)
|
mj_forward 通常用于 reset 后或手动修改位置后,快速重新计算所有几何体的世界坐标。
5.3 控制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| data.ctrl[joint_index] = force_value
data.ctrl[:] = desired_forces
data.ctrl[:] = desired_positions
for i in range(model.nu): name = mujoco.mj_id2name( model, mujoco.mjtObj.mjOBJ_ACTUATOR, i ) print(f"{i}: {name}")
|
5.4 读取状态
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| data.qpos data.qpos[0]
data.qvel
data.body_xpos[body_id] data.body_xquat[body_id] data.body_xmat[body_id]
data.geom_xpos[geom_id]
data.qfrc_actuator data.qfrc_passive data.qfrc_constraint data.qfrc_bias
|
获取末端执行器(指尖)位置的完整方法:
1 2 3 4 5 6 7
| body_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_BODY, 'fingertip') fingertip_pos = data.body_xpos[body_id].copy()
site_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_SITE, 'fingertip_site') fingertip_pos = data.site_xpos[site_id].copy()
|
5.5 接触信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| data.ncon
for i in range(data.ncon): c = data.contact[i] c.pos c.frame c.dist c.geom1 c.geom2 normal_force = c.frame[0]
|
5.6 传感器
1 2 3 4 5 6 7 8 9 10 11
| model.nsensordata
data.sensordata
sensor_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_SENSOR, 'fingertip_force') sensor_adr = model.sensor_adr[sensor_id] sensor_dim = model.sensor_dim[sensor_id] force_reading = data.sensordata[sensor_adr:sensor_adr + sensor_dim]
|
六、进阶:灵巧手仿真特有的配置
6.1 域随机化(Domain Randomization)
域随机化是 Sim-to-Real 迁移最核心的技术之一。通过在训练时随机化物理参数,策略学会适应各种条件,从而在真机上也能工作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import numpy as np
def randomize_physics(model): """随机化物理参数,提高策略对真实世界差异的鲁棒性""" for i in range(model.ngeom): factor = np.random.uniform(0.7, 1.3) model.geom_friction[i] = model.geom_friction[i] * factor for i in range(model.njnt): model.damping[i] = model.damping[i] * np.random.uniform(0.5, 1.5) for i in range(model.ngeom): model.geom_density[i] = model.geom_density[i] * np.random.uniform(0.5, 2.0) gravity_scale = np.random.uniform(0.8, 1.2) model.opt.gravity[2] = -9.81 * gravity_scale
|
需要注意的是:MjModel 在文档中被描述为「不可变」,但实际上加载后可以修改部分数值字段(如摩擦系数、阻尼、重力等)。这是因为 MuJoCo 将这些参数暴露为可写的 numpy 数组,以便域随机化等训练场景使用。
6.2 添加力传感器(近似触觉)
MuJoCo 原生支持力/力矩传感器,可以用来近似灵巧手的触觉反馈:
1 2 3 4 5 6 7 8 9 10 11
| <sensor> <force name="fingertip_force" site="fingertip_site"/> <torque name="fingertip_torque" site="fingertip_site"/> <touch name="tactile_taxel_1" site="taxel_1"/> <touch name="tactile_taxel_2" site="taxel_2"/> </sensor>
|
6.3 软指尖模型(参考 FineManip 论文方法)
在灵巧手仿真中,指尖通常是刚体——但真实的灵巧手有硅胶指套。可以通过调整接触参数来近似软接触:
1 2 3 4 5
| <geom type="sphere" size="0.01" pos="0 0 -0.02" solref="0.01 1" <!-- 接触硬度:时间常数 0.01s,阻尼 1 --> solimp="0.9 0.95 0.001" condim="4"/>
|
参数含义:
solref:参考时间常数和阻尼比,控制接触的「软硬程度」
solimp:接触力随穿透深度的增长曲线,控制「渐进接触」
condim:接触维度,影响摩擦力是否建模
调整这些参数可以让指尖接触更像真实硅胶材料。这正是 FineManip(精细操作触觉皮肤)论文实现亚像元精度的物理基础。
6.4 设置腱绳约束
MuJoCo 原生支持 tendon(腱绳)约束,这正是腱绳驱动灵巧手(如帕西尼感知科技的产品)需要的:
1 2 3 4 5 6 7 8
| <tendon> <spatial name="tendon_1" limited="true" range="0 0.05"> <geom site="joint_A_pos" site="joint_B_pos" width="0.002"/> <pulley divisor="1" multiplier="1"/> </spatial> </tendon>
|
在仿真中,腱绳可以配合 TendonForce 论文的方法,用 Transformer 模型预测真实腱绳力,替代理想力的假设。具体做法:
- 在 MuJoCo 的
mjData.ctrl 中设置电机位置
- 从 Transformer 模型读取预测的腱绳力
- 将该力应用到对应的
qfrc_actuator 上
- 执行
mj_step 时,运动学受腱绳约束影响
七、一个典型的灵巧手仿真工作流
你在灵巧手公司(如帕西尼感知科技)的日常仿真工作流大致如下:
1 2 3 4 5 6 7 8
| ① 加载灵巧手 XML ② 创建待抓取物体(球/圆柱/不规则形状) ③ 设置初始位姿(手张开、物体在掌心) ④ 运行控制策略(随机/PPO/示教) ⑤ 采集数据(接触力、关节角度、触觉读数) ⑥ 训练或验证策略 ↓ ①-⑥ 循环,每天跑几千次抓取验证
|
下面是一个完整的抓取仿真器骨架:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101
| import mujoco import numpy as np
class DexterousGraspSimulator: """灵巧手抓取仿真器骨架""" def __init__(self, xml_path): self.model = mujoco.MjModel.from_xml_path(xml_path) self.data = mujoco.MjData(self.model) self.contact_history = [] self.reward_history = [] self._cache_ids() def _cache_ids(self): """预缓存常用的 ID,避免重复查询""" self.object_geom_id = mujoco.mj_name2id( self.model, mujoco.mjtObj.mjOBJ_GEOM, 'object' ) self.fingertip_site_ids = [ mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_SITE, f'fingertip_{i}') for i in range(4) ] def reset(self, randomize=True): """重置到初始状态""" mujoco.mj_resetData(self.model, self.data) if randomize: self.data.qpos[-7:] += np.random.uniform(-0.02, 0.02, size=7) mujoco.mj_forward(self.model, self.data) def step(self, action): """执行一步控制""" self.data.ctrl[:] = np.clip(action, -1.0, 1.0) mujoco.mj_step(self.model, self.data) reward = self._compute_reward() self.reward_history.append(reward) self.contact_history.append(self.data.ncon) def _compute_reward(self): """计算抓取质量""" object_height = self.data.geom_xpos[self.object_geom_id][2] lift_reward = 1.0 if object_height > 0.1 else 0.0 contact_force = self._get_total_fingertip_force() optimal_force = 5.0 force_penalty = -0.01 * abs(contact_force - optimal_force) xy_dist = np.linalg.norm(self.data.geom_xpos[self.object_geom_id][:2]) drift_penalty = -0.1 * xy_dist return lift_reward + force_penalty + drift_penalty def _get_total_fingertip_force(self): """获取指尖总接触力""" total_force = 0.0 for i in range(self.data.ncon): c = self.data.contact[i] geom1_name = mujoco.mj_id2name( self.model, mujoco.mjtObj.mjOBJ_GEOM, c.geom1 ) geom2_name = mujoco.mj_id2name( self.model, mujoco.mjtObj.mjOBJ_GEOM, c.geom2 ) if 'fingertip' in geom1_name or 'fingertip' in geom2_name: normal_force = np.linalg.norm(c.frame[:3]) total_force += normal_force return total_force
sim = DexterousGraspSimulator('hand_with_object.xml')
for episode in range(100): sim.reset() done = False while not done: action = np.random.uniform(-1.0, 1.0, size=sim.model.nu) sim.step(action) object_pos = sim.data.geom_xpos[sim.object_geom_id][2] done = object_pos < 0.01 print(f"Episode {episode}: 平均奖励 = {np.mean(sim.reward_history[-50:]):.3f}")
|
八、常见问题与调试技巧
8.1 手指穿透物体
1 2 3 4 5 6 7
| 现象:手指穿过物体而不是推开它 原因:碰撞检测开启太少,或时间步长太大
解决: • 减小 timestep(0.001 而不是 0.002) • 增大接触检测范围(model.opt.tolerance) • 检查 geom 类型是否都设了 type
|
8.2 物体抖动/爆炸
1 2 3 4 5 6 7
| 现象:物体在接触后剧烈抖动或飞走 原因:接触刚度太高,或质量设置不合理
解决: • 减小 solref 中的刚度值 • 确保物体的质量和惯性矩是合理的 • 检查是否有两个 geom 重叠初始位置
|
8.3 控制信号没反应
1 2 3 4 5 6 7
| 现象:设置了 data.ctrl 但关节不动 原因:actuator 没连到正确的 joint 上
解决: • 检查 actuator 中的 joint/transmission 名称是否正确 • 检查 actuator 的 gear 值是否合理 • 使用 mujoco.mj_id2name 验证名称映射
|
8.4 仿真速度太慢
1 2 3 4 5 6 7 8
| 现象:FPS 低于预期
解决: • 减少 geom 的数量(尤其是复杂 mesh) • 增大 timestep(但不要超过接触稳定性极限) • 关闭不必要的传感器 • 使用 model.opt.cone = mujoco.mjtCone.mjCONE_PYRAMIDAL (金字塔摩擦锥比椭球锥更快)
|
九、我该去哪里进一步学习?
📖 中文文档(推荐优先阅读):
1 2 3 4 5 6 7
| 0. MuJoCo 中文文档翻译站(强烈推荐 ⭐) https://docs.mujoco.cn 由社区维护的完整中文翻译,与官方英文版同步 内容覆盖:概述、计算、建模、XML 参考、编程、 API 参考、Python、MuJoCo XLA/Warp、 Unity 插件、OpenUSD、模型库、更新日志 推荐作为日常查阅的首选文档
|
官方资源(英文原版):
1 2 3 4 5 6 7 8 9 10 11
| 1. MuJoCo 官方文档 https://mujoco.readthedocs.io/ 必读章节:XML 参考、Python 接口
2. MuJoCo 官方 Python 示例 https://github.com/google-deepmind/mujoco/tree/main/python/examples 推荐文件:body_interactions.py, marker.py, actuator.py
3. MuJoCo Menagerie(官方模型库) https://github.com/google-deepmind/mujoco_menagerie 包含 Shadow Hand、Allegro Hand、Franka 等多种模型
|
社区资源:
1 2 3 4 5 6 7 8
| 4. Roboti 论坛 https://roboti.us/forum/ 搜索 "dexterous hand" 有大量讨论
5. 论文源码中的 MuJoCo 使用 OpenAI Dactyl(Shadow Hand + MuJoCo 的奠基工作) T-Dex(触觉预训练 + MuJoCo) TendonForce(腱绳力建模 + MuJoCo)
|
十、速查表
| 你想做什么 |
代码 |
| 加载模型 |
m = mujoco.MjModel.from_xml_path('file.xml') |
| 从字符串加载 |
m = mujoco.MjModel.from_xml_string(xml_str) |
| 初始化数据 |
d = mujoco.MjData(m) |
| 前进一步 |
mujoco.mj_step(m, d) |
| 给关节发指令 |
d.ctrl[i] = value |
| 读关节角度 |
d.qpos[i] |
| 读接触数量 |
d.ncon |
| 读接触力 |
d.contact[i].frame[:3] |
| 重置 |
mujoco.mj_resetData(m, d) |
| 前向运动学 |
mujoco.mj_forward(m, d) |
| 获取 body 位置 |
d.body_xpos[body_id] |
| 通过名称找 ID |
mujoco.mj_name2id(m, mujoco.mjtObj.mjOBJ_BODY, 'name') |
| 通过 ID 找名称 |
mujoco.mj_id2name(m, mujoco.mjtObj.mjOBJ_BODY, id) |
| 获取传感器值 |
d.sensordata[adr:adr+dim] |
本文为 MuJoCo 零基础入门指南,结合了官方文档(mujoco.readthedocs.io)和学术界常用的最佳实践编写而成。 核心建议:不要试图学完所有 MuJoCo 知识再开始。安装→跑通示例→让手指动一下,三步就够了,遇到具体问题再查文档。