1. 为什么我最终选择了 MuJoCo 而不是 Gazebo 或 PyBullet
1.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 的选型对照
选仿真平台这件事,没有"最好",只有"最合适"。我把三个平台在几个关键维度上的实际表现整理了一下,供你参考:
| 维度 | MuJoCo | Gazebo | PyBullet |
|---|---|---|---|
| 接触求解精度 | 高(软接触+凸优化) | 中(ODE默认参数需调) | 中高(取决于求解器配置) |
| 仿真速度(UR5e场景) | 约 8000-12000 步/秒 | 约 1000-3000 步/秒 | 约 3000-6000 步/秒 |
| 建模格式 | MJCF(XML) | SDF/URDF | URDF/MJCF |
| ROS 集成 | 通过 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 type="sphere" size="0.1"/> <joint type="free"/> </body> </worldbody> </mujoco> """) data = mujoco.MjData(model) mujoco.viewer.launch(model, data)如果弹出一个窗口,里面有个球在重力作用下掉落,说明安装没问题。
第三,检查渲染后端。MuJoCo 3.x 默认用 GLFW 做可视化,如果你在无头服务器上跑,需要设置:
export MUJOCO_GL=egl # 或者 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)→ 腕部1(wrist 1)→ 腕部2(wrist 2)→ 腕部3(wrist 3)。六个关节都是旋转关节,其中肩部和腕部的轴线方向需要特别注意。
在 MJCF 里,每个 link 对应一个<body>,每个关节对应一个<joint>。关键在于关节轴方向和body 原点的相对位置。UR5e 的 DH 参数网上能查到,但直接套 DH 参数到 MJCF 里容易出错,因为 MJCF 用的是相对父 body 的坐标系,不是 DH 的全局坐标系。
我的做法是:先确定每个关节的旋转轴在父 body 坐标系下的方向,再确定子 body 原点相对父 body 原点的偏移。以 UR5e 为例:
<mujoco model="ur5e"> <compiler angle="radian" meshdir="assets"/> <default> <joint damping="0.5" armature="0.01"/> <geom friction="1.0 0.005 0.0001" rgba="0.7 0.7 0.7 1"/> </default> <worldbody> <body name="base" pos="0 0 0"> <geom type="cylinder" size="0.075 0.05" pos="0 0 0.025"/> <body name="shoulder_link" pos="0 0 0.089"> <joint name="shoulder_pan_joint" type="hinge" axis="0 0 1" range="-6.28 6.28"/> <geom type="capsule" fromto="0 0 0 0 0 0.1" size="0.06"/> <body name="upper_arm_link" pos="0 0.138 0"> <joint name="shoulder_lift_joint" type="hinge" axis="0 1 0" range="-6.28 6.28"/> <geom type="capsule" fromto="0 0 0 0 0 0.425" size="0.05"/> <body name="forearm_link" pos="0 0 0.425"> <joint name="elbow_joint" type="hinge" axis="0 1 0" range="-3.14 3.14"/> <geom type="capsule" fromto="0 0 0 0 0 0.392" size="0.04"/> <body name="wrist_1_link" pos="0 0 0.392"> <joint name="wrist_1_joint" type="hinge" axis="0 1 0" range="-6.28 6.28"/> <geom type="capsule" fromto="0 0 0 0 0 0.095" size="0.035"/> <body name="wrist_2_link" pos="0 0 0.095"> <joint name="wrist_2_joint" type="hinge" axis="0 0 1" range="-6.28 6.28"/> <geom type="capsule" fromto="0 0 0 0 0 0.082" size="0.035"/> <body name="wrist_3_link" pos="0 0 0.082"> <joint name="wrist_3_joint" type="hinge" axis="0 1 0" range="-6.28 6.28"/> <geom type="capsule" fromto="0 0 0 0 0 0.082" size="0.03"/> <site name="ee_site" pos="0 0 0.082" size="0.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 name="shoulder_pan_act" joint="shoulder_pan_joint" kp="2000" kv="100"/> <position name="shoulder_lift_act" joint="shoulder_lift_joint" kp="2000" kv="100"/> <!-- 其余关节类似 --> </actuator>kp是位置增益,kv是速度阻尼。这两个值的选取有个经验公式:kv ≈ 2 * sqrt(kp * m),其中 m 是关节等效惯量。UR5e 的关节惯量大概在 0.5-2 kg·m² 之间,所以 kp 取 2000、kv 取 100 左右比较稳。kp 太小跟踪不上,太大直接发散。
力矩控制(适合强化学习):
<actuator> <motor name="shoulder_pan_act" joint="shoulder_pan_joint" gear="1" ctrlrange="-150 150"/> <!-- 其余关节类似 --> </actuator>ctrlrange是力矩限幅,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 的完整闭环
先装 IKPy:
pip install ikpy然后构建 IK 链。IKPy 可以直接读 URDF:
from 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 对应六个关节。
求解 IK:
target_position = [0.3, 0.2, 0.5] target_orientation = [0, 0, 1, 0] # 四元数 ik_solution = ik_chain.inverse_kinematics( target_position, target_orientation=target_orientation, orientation_mode="all" ) 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(f"IK 误差: {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_cost=1.0, orientation_cost=1.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, limits=limits) 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 timestep="0.0005" integrator="implicitfast"/>integrator选implicitfast比默认的Euler稳定得多,代价是每步计算量稍大。我现在的习惯是:只要模型里有接触,就用 implicitfast。
第二步,检查质量和惯量。MJCF 里如果没显式指定mass和diaginertia,MuJoCo 会根据 geom 的体积和默认密度自动算。但如果 geom 是 capsule 且 size 很小,算出来的惯量可能接近 0,导致数值不稳定。手动指定:
<body name="forearm_link" pos="0 0 0.425"> <inertial pos="0 0 0.2" mass="1.5" diaginertia="0.01 0.01 0.001"/> ... </body>第三步,检查执行器增益。位置控制的kp太大是发散的常见原因。如果你看到关节在目标位置附近高频振荡然后飞出去,先把kp降到原来的 1/10 试试。
5.2 接触参数:solref 和 solimp 到底怎么调
MuJoCo 的接触模型由两个参数控制:solref和solimp。
solref="0.02 1":第一个数是时间常数,越小接触越硬;第二个数是阻尼比,1 表示临界阻尼。solimp="0.9 0.95 0.001":分别是阻尼、刚度、最小值的插值参数。
默认值对大多数场景够用,但如果你做的是刚性抓取(比如金属夹爪夹金属块),默认参数会导致明显的穿透。我的调参经验:
| 场景 | solref | solimp |
|---|---|---|
| 软物体抓取(橡胶、布料) | 0.05 1 | 0.9 0.95 0.001 |
| 刚性抓取(金属、塑料) | 0.005 1 | 0.95 0.99 0.0001 |
| 足式机器人落地 | 0.01 1 | 0.9 0.95 0.001 |
| 高速碰撞 | 0.002 1 | 0.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[i+1], 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, height=480, width=640) frames = [] for _ in range(1000): mujoco.mj_step(model, data) if _ % 10 == 0: renderer.update_scene(data, camera="track") frames.append(renderer.render()) imageio.mimsave("simulation.mp4", frames, fps=30)几个注意点:
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 倍。
- 关闭不必要的接触:用
<contact><exclude>排除永远不会接触的 body 对。 - 降低渲染频率:渲染是最大的性能瓶颈,训练时关掉 viewer,只在评估时开。
- 用
mj_step而不是mj_forward+ 手动积分:mj_step内部做了优化。 - 多环境并行:用
mujoco.mj_step的批量版本,或者用 Gymnasium 的向量化环境。
我现在的 UR5e 抓取实验,单环境能跑到 12000 步/秒,16 个并行环境总共 80000 步/秒,训练一个抓取策略大概 2 小时收敛。
7.4 常见问题速查表
最后整理一个我踩过的坑和对应解决方案的速查表:
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| import mujoco 报 DLL 错误 | VC++ 运行库缺失 | 安装最新 VC++ Redistributable |
| viewer 打不开 | MUJOCO_GL 未设置 | 设置 MUJOCO_GL=glfw 或 egl |
| 仿真发散 | 时间步太大/惯量太小/kp太大 | 降 timestep,手动指定 inertial,降 kp |
| 接触穿透明显 | solref 太大 | 减小 solref 第一个参数 |
| 接触力振荡 | solref 太小 | 增大 solref 第一个参数 |
| IK 解误差大 | URDF 和 MJCF 关节原点不一致 | 对齐 body pos 或用 mink 数值 IK |
| 仿真速度慢 | 碰撞体太复杂/渲染太频繁 | 简化碰撞体,降低渲染频率 |
| 关节抖动 | 阻尼太小 | 增大 joint damping 和 armature |
这些经验都是我在实际项目里一步步试出来的,希望能帮你少走点弯路。MuJoCo 的学习曲线不算平缓,但一旦跑通第一个完整实验,后面的迭代会越来越顺。