ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MuJoCo vs Gazebo vs PyBullet:机器人仿真选型与UR5e实战

MuJoCo vs Gazebo vs PyBullet:机器人仿真选型与UR5e实战 1. 为什么我最终选择了 MuJoCo 而不是 Gazebo 或 PyBullet1.1 从一次机械臂抓取实验的崩溃说起去年做 UR5e 的抓取策略训练时我一开始用的是 Gazebo。场景搭好、控制器调通、抓取成功率在仿真里跑到 85%结果一上真机直接掉到 30% 以下。排查了两周才发现问题出在接触力模型上——Gazebo 默认的 ODE 求解器在处理刚性接触时穿透深度和恢复系数的组合跟真实橡胶夹爪差异太大仿真里捏住的物体在真机上要么滑脱要么被弹飞。那次之后我开始认真对比几个主流物理引擎。PyBullet 上手最快但它的接触求解精度在高速碰撞场景下衰减明显Gazebo 生态最全但物理精度和实时性的平衡一直是个痛点。最后转向 MuJoCo核心原因是它的软接触模型soft contact model和凸优化求解器在处理机械臂抓取、足式机器人落地这类接触密集场景时稳定性和物理一致性明显更好。MuJoCo 最早是华盛顿大学 Emo Todorov 团队开发的2021 年被 DeepMind 收购后开源现在是 Apache 2.0 协议商用也没问题。它的定位很明确面向机器人控制与强化学习的高精度物理仿真。不是游戏引擎不追求画面追求的是动力学计算的准确性和速度。1.2 MuJoCo、Gazebo、PyBullet 的选型对照选仿真平台这件事没有最好只有最合适。我把三个平台在几个关键维度上的实际表现整理了一下供你参考维度MuJoCoGazeboPyBullet接触求解精度高软接触凸优化中ODE默认参数需调中高取决于求解器配置仿真速度UR5e场景约 8000-12000 步/秒约 1000-3000 步/秒约 3000-6000 步/秒建模格式MJCFXMLSDF/URDFURDF/MJCFROS 集成通过 mujoco_ros 桥接原生支持通过 pybullet_ros强化学习生态极好dm_control、Gymnasium一般好Gymnasium学习曲线中等MJCF 需熟悉较陡依赖多平缓渲染质量中但够用高中如果你的核心需求是控制算法验证或强化学习训练MuJoCo 的性价比最高。如果是要做多机器人系统集成或传感器仿真激光雷达、深度相机噪声模型Gazebo 的生态更成熟。PyBullet 适合快速原型验证但别指望它在接触密集场景下给你高保真结果。1.3 MJCF 建模格式看起来麻烦用起来真香MuJoCo 用自己的 MJCF 格式描述模型本质上是 XML。很多人第一反应是为什么不用 URDF我一开始也这么想。但用久了发现 MJCF 有几个设计确实更合理关节和几何体可以内联定义不需要像 URDF 那样把 link 和 joint 拆成平行结构可读性好很多。支持默认类default class同一类物体的摩擦、阻尼、接触参数可以统一设置改一处全生效。执行器actuator和传感器sensor直接写在模型里不需要额外的配置文件。支持程序化生成可以用 Python 脚本批量构建复杂场景。当然URDF 转 MJCF 也有现成工具后面会讲。但如果你是从零搭一个实验我建议直接用 MJCF 写省得转来转去丢信息。2. 环境搭建那些安装教程不会告诉你的细节2.1 pip 安装 mujoco 之后为什么 import 报错先说最省事的路径。MuJoCo 从 2.1 版本开始就不需要单独下载二进制包了直接 pip 装pip install mujoco但这里有个坑Python 版本和 mujoco 版本的兼容性。我实测下来mujoco 3.x 需要 Python 3.8 以上3.10 和 3.11 最稳。如果你用的是 Python 3.12某些旧版本的 mujoco 会报ImportError: DLL load failed或者undefined symbol。另一个常见问题是numpy 版本冲突。mujoco 依赖 numpy但如果你环境里已经装了某个特定版本的 numpy比如 TensorFlow 拉进来的可能会出现 ABI 不兼容。解决办法是先建一个干净的虚拟环境python -m venv mujoco_env source mujoco_env/bin/activate # Windows 用 mujoco_env\Scripts\activate pip install mujoco numpy提示Windows 用户如果遇到mujoco.dll找不到的问题大概率是 Visual C Redistributable 没装。去微软官网下载最新的 VC 运行库装上就行这个在官方文档里提得很少但踩坑率极高。2.2 验证安装跑通第一个仿真之前先做这三件事装完之后别急着写代码先做三个验证第一检查版本和基本导入import mujoco print(mujoco.__version__)第二跑一个内置模型import mujoco import mujoco.viewer model mujoco.MjModel.from_xml_string( mujoco worldbody body geom typesphere size0.1/ joint typefree/ /body /worldbody /mujoco ) data mujoco.MjData(model) mujoco.viewer.launch(model, data)如果弹出一个窗口里面有个球在重力作用下掉落说明安装没问题。第三检查渲染后端。MuJoCo 3.x 默认用 GLFW 做可视化如果你在无头服务器上跑需要设置export MUJOCO_GLegl # 或者 osmesa这个环境变量不设的话在服务器上调用mujoco.viewer.launch会直接报错。我当初在 Ubuntu Server 上折腾了半天才想起来是这个问题。2.3 目录结构别把模型文件到处乱放项目做大了之后模型文件、脚本、日志混在一起是灾难。我现在的习惯是固定这样的结构project/ ├── models/ │ ├── ur5e/ │ │ ├── ur5e.xml │ │ └── assets/ │ └── scene.xml ├── scripts/ │ ├── run_sim.py │ └── controllers/ ├── logs/ └── requirements.txtMJCF 文件里引用 mesh 或纹理时路径是相对于 XML 文件所在目录的。所以assets文件夹放在模型同级目录下最省心。如果你用include标签引入其他 XML路径规则也一样。3. 用 MJCF 从零描述一个 UR5e 机械臂3.1 UR5e 的运动学链拆解UR5e 是六自由度串联机械臂从基座到末端依次是基座base→ 肩部shoulder→ 上臂upper arm→ 前臂forearm→ 腕部1wrist 1→ 腕部2wrist 2→ 腕部3wrist 3。六个关节都是旋转关节其中肩部和腕部的轴线方向需要特别注意。在 MJCF 里每个 link 对应一个body每个关节对应一个joint。关键在于关节轴方向和body 原点的相对位置。UR5e 的 DH 参数网上能查到但直接套 DH 参数到 MJCF 里容易出错因为 MJCF 用的是相对父 body 的坐标系不是 DH 的全局坐标系。我的做法是先确定每个关节的旋转轴在父 body 坐标系下的方向再确定子 body 原点相对父 body 原点的偏移。以 UR5e 为例mujoco modelur5e compiler angleradian meshdirassets/ default joint damping0.5 armature0.01/ geom friction1.0 0.005 0.0001 rgba0.7 0.7 0.7 1/ /default worldbody body namebase pos0 0 0 geom typecylinder size0.075 0.05 pos0 0 0.025/ body nameshoulder_link pos0 0 0.089 joint nameshoulder_pan_joint typehinge axis0 0 1 range-6.28 6.28/ geom typecapsule fromto0 0 0 0 0 0.1 size0.06/ body nameupper_arm_link pos0 0.138 0 joint nameshoulder_lift_joint typehinge axis0 1 0 range-6.28 6.28/ geom typecapsule fromto0 0 0 0 0 0.425 size0.05/ body nameforearm_link pos0 0 0.425 joint nameelbow_joint typehinge axis0 1 0 range-3.14 3.14/ geom typecapsule fromto0 0 0 0 0 0.392 size0.04/ body namewrist_1_link pos0 0 0.392 joint namewrist_1_joint typehinge axis0 1 0 range-6.28 6.28/ geom typecapsule fromto0 0 0 0 0 0.095 size0.035/ body namewrist_2_link pos0 0 0.095 joint namewrist_2_joint typehinge axis0 0 1 range-6.28 6.28/ geom typecapsule fromto0 0 0 0 0 0.082 size0.035/ body namewrist_3_link pos0 0 0.082 joint namewrist_3_joint typehinge axis0 1 0 range-6.28 6.28/ geom typecapsule fromto0 0 0 0 0 0.082 size0.03/ site nameee_site pos0 0 0.082 size0.01/ /body /body /body /body /body /body /body /worldbody /mujoco这段代码里几个关键点axis指定旋转轴UR5e 的肩部 pan 关节绕 Z 轴lift 和 elbow 绕 Y 轴腕部交替绕 Y 和 Z 轴。pos是子 body 原点相对父 body 原点的偏移不是 DH 参数里的a和d直接照搬。range是关节限位UR5e 的实际限位是 ±2π 或 ±π这里按实际值设置。site用来标记末端执行器的位置后面做 IK 和抓取时会用到。3.2 用 URDF 转换省事但要注意丢信息如果你手头已经有 UR5e 的 URDF可以用urdf2mjcf工具转pip install urdf2mjcf urdf2mjcf ur5e.urdf -o ur5e.xml但转换之后一定要检查几件事mesh 路径转换工具可能把路径写成绝对路径换台机器就跑不了。关节阻尼和摩擦URDF 里通常没有这些参数转换后默认值是 0仿真会抖得厉害。碰撞几何体URDF 的 collision 和 visual 是分开的转换后要确认碰撞体没有用高精度 mesh否则仿真速度会暴跌。我一般转换完之后手动加default里的阻尼和摩擦参数再把碰撞体换成简化几何capsule 或 box速度能提升 3-5 倍。3.3 执行器配置位置控制、速度控制还是力矩控制MuJoCo 的 actuator 配置直接决定了你能用什么方式控制关节。UR5e 实验里我常用两种位置控制适合轨迹跟踪actuator position nameshoulder_pan_act jointshoulder_pan_joint kp2000 kv100/ position nameshoulder_lift_act jointshoulder_lift_joint kp2000 kv100/ !-- 其余关节类似 -- /actuatorkp是位置增益kv是速度阻尼。这两个值的选取有个经验公式kv ≈ 2 * sqrt(kp * m)其中 m 是关节等效惯量。UR5e 的关节惯量大概在 0.5-2 kg·m² 之间所以 kp 取 2000、kv 取 100 左右比较稳。kp 太小跟踪不上太大直接发散。力矩控制适合强化学习actuator motor nameshoulder_pan_act jointshoulder_pan_joint gear1 ctrlrange-150 150/ !-- 其余关节类似 -- /actuatorctrlrange是力矩限幅UR5e 各关节的最大力矩不同肩部大概 150 N·m腕部只有 28 N·m。这个值设错了要么仿真里机械臂软绵绵要么直接飞出去。注意位置控制和力矩控制不能混用在同一个关节上。如果你需要切换得在模型里定义两套 actuator然后通过mujoco.mj_setActuator动态启用/禁用。4. 正向运动学、逆向运动学与 IKPy 的配合4.1 MuJoCo 自带的 FK一行代码拿到末端位姿正向运动学在 MuJoCo 里简单到离谱。仿真步进之后直接读 site 或 body 的位置import mujoco import numpy as np model mujoco.MjModel.from_xml_path(models/ur5e/ur5e.xml) data mujoco.MjData(model) # 设置关节角度 data.qpos[:6] [0.1, -1.2, 1.5, -0.3, 0.8, 0.0] mujoco.mj_forward(model, data) # 读取末端 site 的位姿 ee_pos data.site(ee_site).xpos.copy() ee_mat data.site(ee_site).xmat.reshape(3, 3).copy() ee_quat np.zeros(4) mujoco.mju_mat2Quat(ee_quat, data.site(ee_site).xmat)mj_forward会计算所有 body 和 site 的全局位姿不需要手动做矩阵连乘。这是 MuJoCo 相比自己写 FK 最大的便利。4.2 为什么 IK 不直接用 MuJoCo 的求解器MuJoCo 本身没有内置的解析 IK 求解器。它有个mj_inverse可以做逆动力学但不是逆运动学。所以做 IK 一般有两条路数值 IK用 Jacobian 迭代自己写或者用mink这类库。解析 IK用 IKPy 这类专门做 IK 的库。IKPy 的优势是支持任意运动学链你给它一个 URDF 或者自定义的链结构它就能做数值 IK。缺点是它不感知 MuJoCo 的关节限位和碰撞所以解出来的角度可能超出 UR5e 的实际范围。我的做法是用 IKPy 求初值用 MuJoCo 做验证和限位裁剪。4.3 IKPy MuJoCo 的完整闭环先装 IKPypip install ikpy然后构建 IK 链。IKPy 可以直接读 URDFfrom ikpy.chain import Chain import numpy as np # 用 URDF 构建 IK 链 ik_chain Chain.from_urdf_file( models/ur5e/ur5e.urdf, base_elements[base_link], last_link_vector[0, 0, 0.082], active_links_mask[False, True, True, True, True, True, True] )active_links_mask里第一个 False 是因为 base_link 是固定的后面六个 True 对应六个关节。求解 IKtarget_position [0.3, 0.2, 0.5] target_orientation [0, 0, 1, 0] # 四元数 ik_solution ik_chain.inverse_kinematics( target_position, target_orientationtarget_orientation, orientation_modeall ) joint_angles ik_solution[1:7] # 去掉 base_link 对应的那个拿到角度之后别直接塞给 MuJoCo先做限位检查def clamp_joints(angles, model, joint_names): clamped [] for i, name in enumerate(joint_names): jnt_id mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_JOINT, name) lo, hi model.jnt_range[jnt_id] clamped.append(np.clip(angles[i], lo, hi)) return np.array(clamped)然后再用 MuJoCo 做 FK 验证data.qpos[:6] clamped_angles mujoco.mj_forward(model, data) actual_pos data.site(ee_site).xpos error np.linalg.norm(actual_pos - target_position) print(fIK 误差: {error:.4f} m)如果误差大于 1cm说明 IKPy 的解在 MuJoCo 的模型里不准确可能是 URDF 和 MJCF 的关节原点定义有差异。这时候要么调整 MJCF 的 body pos要么用数值 IK 在 MuJoCo 里再迭代几步。4.4 数值 IK 的补充方案用 mink 做约束优化IKPy 解不出来的情况比如目标点接近奇异位形我会用mink做补充。mink 是基于 MuJoCo 的微分 IK 库支持关节限位、速度约束等import mink configuration mink.Configuration(model) tasks [ mink.FrameTask(ee_site, site, position_cost1.0, orientation_cost1.0) ] tasks[0].set_target(mink.SE3.from_translation(target_position)) limits [mink.ConfigurationLimit(model)] solver daqp for _ in range(100): vel mink.solve_ik(configuration, tasks, 0.01, solver, limitslimits) configuration.integrate_inplace(vel, 0.01) if np.linalg.norm(configuration.data.site(ee_site).xpos - target_position) 1e-3: breakmink 的好处是它直接在 MuJoCo 的模型上做优化解出来的角度天然满足限位和碰撞约束。缺点是速度比 IKPy 慢适合做精细调整而不是实时控制。5. 仿真发散的排查链路从现象到根因5.1 发散的第一表现NaN 和爆炸的数值仿真发散是 MuJoCo 新手最常遇到的问题。典型表现是跑了几百步之后data.qpos里出现nan或者关节角度突然跳到 10^6 量级。这时候别慌按下面的链路一步步排查。第一步确认是不是时间步太大。MuJoCo 默认时间步是 0.002 秒500Hz对于大多数场景够用。但如果你的模型里有很硬的接触或者很小的质量需要降到 0.0005 甚至更小option timestep0.0005 integratorimplicitfast/integrator选implicitfast比默认的Euler稳定得多代价是每步计算量稍大。我现在的习惯是只要模型里有接触就用 implicitfast。第二步检查质量和惯量。MJCF 里如果没显式指定mass和diaginertiaMuJoCo 会根据 geom 的体积和默认密度自动算。但如果 geom 是 capsule 且 size 很小算出来的惯量可能接近 0导致数值不稳定。手动指定body nameforearm_link pos0 0 0.425 inertial pos0 0 0.2 mass1.5 diaginertia0.01 0.01 0.001/ ... /body第三步检查执行器增益。位置控制的kp太大是发散的常见原因。如果你看到关节在目标位置附近高频振荡然后飞出去先把kp降到原来的 1/10 试试。5.2 接触参数solref 和 solimp 到底怎么调MuJoCo 的接触模型由两个参数控制solref和solimp。solref0.02 1第一个数是时间常数越小接触越硬第二个数是阻尼比1 表示临界阻尼。solimp0.9 0.95 0.001分别是阻尼、刚度、最小值的插值参数。默认值对大多数场景够用但如果你做的是刚性抓取比如金属夹爪夹金属块默认参数会导致明显的穿透。我的调参经验场景solrefsolimp软物体抓取橡胶、布料0.05 10.9 0.95 0.001刚性抓取金属、塑料0.005 10.95 0.99 0.0001足式机器人落地0.01 10.9 0.95 0.001高速碰撞0.002 10.99 0.999 0.00001调这些参数的判断标准是看接触力曲线有没有高频振荡。如果力曲线像锯齿一样抖说明接触太硬把solref第一个数调大如果物体明显陷进去说明接触太软调小。5.3 一个真实的发散案例UR5e 抓取时突然飞出去上个月做 UR5e 抓取实验时遇到一个诡异问题机械臂在接近物体时正常一旦夹爪接触物体整个系统在 0.1 秒内发散。排查过程如下先看日志data.qpos在接触后第 3 步就出现 10^4 量级的值说明是接触力爆炸。检查夹爪质量夹爪的 mesh 是从 CAD 导出的质量自动算出来只有 0.001 kg但实际应该是 0.5 kg。惯量太小导致接触力计算不稳定。修复手动指定夹爪的mass和diaginertia同时把夹爪的solref从默认改成0.01 1。验证重新跑接触力曲线平滑抓取成功率从 0% 恢复到 90%。这个案例的教训是从 CAD 导入的 mesh一定要手动检查质量和惯量。MuJoCo 的自动计算对复杂几何体经常不准。6. 可视化与调试viewer 的正确打开方式6.1 被动 viewer 和主动 viewer 的区别MuJoCo 有两种 viewer 模式被动模式mujoco.viewer.launch(model, data)viewer 在单独的线程里跑你的主循环继续执行。适合调试和演示。主动模式mujoco.viewer.launch_passive(model, data)返回一个 handle你需要在主循环里调用handle.sync()。适合需要精确控制仿真步进的场景。我调试控制器时用被动模式做数据采集时用主动模式。主动模式的一个坑是如果你不在每步调用sync()viewer 会卡死。6.2 在 viewer 里实时画力和轨迹MuJoCo 的 viewer 支持自定义可视化元素。比如你想看末端执行器的轨迹import mujoco.viewer import numpy as np trajectory [] with mujoco.viewer.launch_passive(model, data) as viewer: while viewer.is_running(): mujoco.mj_step(model, data) trajectory.append(data.site(ee_site).xpos.copy()) # 每 10 步更新一次轨迹可视化 if len(trajectory) % 10 0 and len(trajectory) 1: viewer.user_scn.ngeom 0 for i in range(len(trajectory) - 1): mujoco.mjv_connector( viewer.user_scn, mujoco.mjtGeom.mjGEOM_LINE, 2, trajectory[i], trajectory[i1], np.array([1, 0, 0, 1]) # 红色 ) viewer.sync()user_scn是 viewer 的用户场景可以用来画线、画点、画箭头。mjv_connector是画连接线的函数参数依次是场景、几何类型、宽度、起点、终点、颜色。6.3 录制视频离屏渲染的正确配置做实验记录需要录视频。MuJoCo 支持离屏渲染import mujoco import imageio model mujoco.MjModel.from_xml_path(models/ur5e/scene.xml) data mujoco.MjData(model) renderer mujoco.Renderer(model, height480, width640) frames [] for _ in range(1000): mujoco.mj_step(model, data) if _ % 10 0: renderer.update_scene(data, cameratrack) frames.append(renderer.render()) imageio.mimsave(simulation.mp4, frames, fps30)几个注意点camera参数指定用哪个相机不指定的话用默认的自由相机。离屏渲染需要设置MUJOCO_GL环境变量Linux 服务器上用egl本地开发机用glfw。渲染很吃性能如果仿真步进速度重要把渲染频率降到每 10 步或每 20 步一次。7. 从仿真到实验几个让结果更可信的习惯7.1 随机化别让策略过拟合到仿真参数如果你的目的是训练强化学习策略然后迁移到真机域随机化是必须的。MuJoCo 里可以在每次 reset 时随机化这些参数def randomize(model): # 随机化质量 ±10% for i in range(model.nbody): model.body_mass[i] * np.random.uniform(0.9, 1.1) # 随机化摩擦系数 ±20% for i in range(model.ngeom): model.geom_friction[i, 0] * np.random.uniform(0.8, 1.2) # 随机化执行器增益 ±5% for i in range(model.nu): model.actuator_gainprm[i, 0] * np.random.uniform(0.95, 1.05)随机化的范围要基于真机的实际不确定性来定。比如 UR5e 的关节摩擦在出厂时就有 ±15% 的差异那仿真里就按这个范围随机。7.2 用真实数据校准仿真参数仿真和真机的差距最终要靠数据来补。我的做法是在真机上让 UR5e 做一组标准轨迹比如正弦扫频记录关节角度和力矩。在仿真里跑同样的轨迹记录力矩。用最小二乘法拟合仿真的阻尼和摩擦参数使力矩误差最小。这个过程叫系统辨识MuJoCo 本身不提供工具但可以用scipy.optimize自己写。校准之后仿真的力矩预测误差能从 30% 降到 5% 以内。7.3 仿真速度的优化从 1000 步/秒到 10000 步/秒仿真速度直接影响实验迭代效率。几个实测有效的优化手段简化碰撞几何体把 mesh 碰撞体换成 capsule 或 box速度提升 3-5 倍。关闭不必要的接触用contactexclude排除永远不会接触的 body 对。降低渲染频率渲染是最大的性能瓶颈训练时关掉 viewer只在评估时开。用mj_step而不是mj_forward 手动积分mj_step内部做了优化。多环境并行用mujoco.mj_step的批量版本或者用 Gymnasium 的向量化环境。我现在的 UR5e 抓取实验单环境能跑到 12000 步/秒16 个并行环境总共 80000 步/秒训练一个抓取策略大概 2 小时收敛。7.4 常见问题速查表最后整理一个我踩过的坑和对应解决方案的速查表现象可能原因解决方案import mujoco 报 DLL 错误VC 运行库缺失安装最新 VC Redistributableviewer 打不开MUJOCO_GL 未设置设置 MUJOCO_GLglfw 或 egl仿真发散时间步太大/惯量太小/kp太大降 timestep手动指定 inertial降 kp接触穿透明显solref 太大减小 solref 第一个参数接触力振荡solref 太小增大 solref 第一个参数IK 解误差大URDF 和 MJCF 关节原点不一致对齐 body pos 或用 mink 数值 IK仿真速度慢碰撞体太复杂/渲染太频繁简化碰撞体降低渲染频率关节抖动阻尼太小增大 joint damping 和 armature这些经验都是我在实际项目里一步步试出来的希望能帮你少走点弯路。MuJoCo 的学习曲线不算平缓但一旦跑通第一个完整实验后面的迭代会越来越顺。
返回列表