Skip to content

2.4 MuJoCo 入门教程:从零搭建四旋翼仿真环境 ​

0. 前言 ​

​ 在真实四旋翼上部署和测试控制算法,通常需要较多辅助设施,如安全网、地面缓冲垫等。但对很多个人用户来说,往往不具备这些条件,因此开展四旋翼调试的危险性和成本都相对较高。例如,参数不正确就可能导致飞行器出现大幅度震荡、突然冲天等失控现象,而由于缺乏相应的保护设施,飞行器在碰撞后很容易损坏,维修也会花费大量时间。

​ 因此,对于一种新的控制算法或一组新的参数,先在仿真环境中进行测试是更为高效合理的选择。在仿真环境中,我们已能对大部分物理特性进行模拟,飞行器在仿真中的表现与在实际部署中的特性基本能做到高度相似。仿真器让你可以在虚拟世界里随意“炸机”,炸多少次都不花钱、不伤人,也不耽误时间。当你对仿真中的控制效果满意后,再将代码部署到真机上,风险已经大大降低。

​ MuJoCo(Multi-Joint dynamics with Contact)是 Google DeepMind 开发的高性能物理引擎,以其速度、精度和 Python 友好性在机器人学界十分流行。本章将手把手带你从安装 MuJoCo 开始,一步步搭建一个完整的四旋翼悬停仿真环境。

读完本文后,你将能够:

  • 安装 MuJoCo 并运行第一个仿真程序
  • 读懂和编写 MJCF 模型文件:旋翼、传感器、执行器的 XML 定义
  • 使用 Python API 加载模型、读写传感器数据、控制执行器
  • 编写控制回调函数,在每步仿真中加入自己的控制逻辑
  • 跑通一个完整的四旋翼仿真,为后续 SO(3)、LQR、MPC、NMPC 各章的仿真实验做好准备

目录 ​

  1. MuJoCo 是什么
  2. 环境安装
  3. MJCF 模型文件:机器人的"DNA"
  4. 场景文件:搭建仿真舞台
  5. Python API:让仿真跑起来
  6. 回调函数:仿真过程中的"钩子"
  7. 驱动器与传感器:给机器人装上肌肉和感官
  8. 完整示例:四旋翼悬停仿真
  9. 常见问题与调试技巧

1. MuJoCo 是什么 ​

MuJoCo(Multi-Joint dynamics with Contact)是 Google DeepMind 开发的开源物理引擎,专门用于机器人学、生物力学和强化学习中的高精度刚体动力学仿真。

为什么选择 MuJoCo ​

特性说明
速度极快C 语言底层,支持 GPU 加速,比 PyBullet 快 3-5 倍
接触模型精确凸 Gauss 原理求解约束力,比 penalty-based 方法更物理
原生 Python 绑定pip install mujoco 即可使用,API 设计现代
MJCF 格式基于 XML 的场景描述,可读性强,可版本管理
内置可视化无头渲染 + 交互式 viewer,无需额外安装 OpenGL 工具包
开源免费Apache 2.0 协议,商业可用

MuJoCo 仿真管线的核心概念 ​

┌─────────────────────────────────────────────────────────────┐
│  仿真循环 (每步 dt)                                          │
│                                                             │
│  传感器读数 ──→ 控制器计算 ──→ 驱动器写入 ──→ 物理步进       │
│      ↑                                          │           │
│      └──────────── 下一帧 ──────────────────────┘           │
│                                                             │
│  MjModel (模型,只读)        MjData (状态,每帧更新)          │
│  · 几何体/关节/驱动器定义      · 位置/速度/加速度              │
│  · 物理参数                    · 传感器数据                   │
│  · 运动学树                    · 接触力                       │
└─────────────────────────────────────────────────────────────┘

MuJoCo 将物理仿真分为两部分:

  • MjModel:编译后的物理模型,包含所有结构信息(刚体数量、关节参数、几何体形状等),仿真过程中不修改。
  • MjData:动态变化的状态数据(位置、速度、加速度、接触力、传感器读数等),每步仿真都会更新。

2. 环境安装 ​

bash
pip install mujoco

MuJoCo 自带一个基本渲染器,但如果你需要离屏渲染(如在服务器上运行),还需要 EGL 或 OSMesa:

bash
# Linux 服务器(无显示器)
pip install mujoco[egl]

# macOS
pip install mujoco[osmesa]

验证安装:

python
import mujoco
print(mujoco.__version__)  # 应该输出 3.x.x

3. MJCF 模型文件:机器人的"DNA" ​

MJCF(MuJoCo XML Format)使用 XML 描述机器人结构。本章通过一个四旋翼模型的真实 MJCF 文件 humprotor160.xml,逐段讲解。

3.1 根元素与仿真选项 ​

xml
<mujoco model="humprotor160">
  <option integrator="RK4" timestep="0.002" density="1.225" viscosity="1.8e-5"/>
属性含义本工程取值典型范围
integrator数值积分方法RK4Euler / RK4 / implicit
timestep仿真步长 (s)0.0020.001 ~ 0.005
density空气密度 (kg/m³)1.225海平面标准值
viscosity空气动力粘度1.8e-5常温常压空气

关键理解:timestep 是仿真的心脏。太大会导致仿真发散,太小则浪费计算量。对于四旋翼这种高频动态系统,0.002s 是一个较为均衡的经验值(Nyquist-Shannon 采样定理要求步长至少是系统最高频率周期的一半)。

3.2 编译器设置 ​

xml
<compiler inertiafromgeom="false" meshdir="assets" autolimits="true"/>
属性含义
inertiafromgeomfalse = 手动指定惯量(精确),true = 从网格自动计算(近似)
meshdirSTL/OBJ 网格文件的搜索目录
autolimits自动计算关节/驱动器的力矩范围

为什么惯量手动指定:对于四旋翼这种质量分布明确的结构,手动填写转动惯量矩阵比让 MuJoCo 从 STL 估算精确得多。网格自动计算假设密度均匀,而真实结构有空腔和不同材料。

3.3 默认类:DRY 原则 ​

xml
<default>
  <default class="humprotor160">
    <default class="visual">
      <geom group="2" type="mesh" contype="0" conaffinity="0"/>
    </default>
    <default class="collision">
      <geom group="3" type="mesh"/>
    </default>
    <site group="5"/>
  </default>
</default>

MuJoCo 使用 级联默认值(cascading defaults)机制来减少重复配置:

  • group 控制可视化图层(viewer 中可切换显示/隐藏)
  • contype=0, conaffinity=0 表示 visual 类的几何体 不参与碰撞检测,仅用于渲染
  • collision 类的几何体用于物理碰撞

最佳实践:将可视化网格和碰撞网格分离。碰撞检测使用简化的凸包(几百个三角面),渲染使用精细网格(几万面)。这可以大幅提升仿真性能。

3.4 资源定义 ​

xml
<asset>
  <material name="color_black" rgba="0.4 0.4 0.4 1.0"/>
  <material name="color_gray" rgba="0.918 0.918 0.918 1.0"/>
  <material name="color_transparent" rgba="0.918 0.918 0.918 0.25"/>
  <material name="color_green" rgba="0.537 0.855 0.827 1.0"/>

  <mesh file="main_collision.stl"/>
  <mesh file="3DPrint.stl"/>
  <mesh file="battery.stl"/>
  <mesh file="motor.stl"/>
  <mesh file="propeller.stl"/>
  <mesh file="nylon.stl"/>
</asset>

<asset> 块定义可复用资源。rgba 格式为 (R, G, B, A),每个分量范围 [0, 1]。

3.5 刚体定义:Worldbody ​

xml
<worldbody>
  <body name="humprotor160" pos="0 0 0.3" childclass="humprotor160">

    <!-- 6自由度自由运动 -->
    <freejoint/>

    <!-- 惯量:质心位置 | 质量(kg) | 绕三轴的转动惯量(kg·m²) -->
    <inertial pos="0 0 0" mass="0.2436" diaginertia="3.58e-4 3.65e-4 6.62e-4"/>

这是模型文件最核心的部分:

元素含义
<body>定义刚体,"嵌套"关系表示运动学父子链
pos="0 0 0.3"初始位置:世界系 x=0, y=0, z=0.3m(离开地面一定高度)
<freejoint/>自由关节 = 6 DOF(平动 3 + 转动 3),不约束任何方向
<inertial>惯性属性:pos 是相对父 body 的质心偏移,mass 是总质量,diaginertia 是主转动惯量对角线

如果没有 <freejoint/>,body 将被固定在世界中,像一尊雕塑。<freejoint/> 使它可以自由飞行。

xml
    <!-- 可视化几何体(不参与碰撞) -->
    <geom mesh="3DPrint" material="color_green" class="visual"/>
    <geom mesh="battery" material="color_gray" class="visual"/>
    <geom mesh="motor" material="color_gray" class="visual"/>
    <geom mesh="propeller" material="color_transparent" class="visual"/>
    <geom mesh="nylon" material="color_black" class="visual"/>

    <!-- 碰撞几何体(不渲染,只做碰撞检测) -->
    <geom mesh="main_collision" class="collision"/>

<geom> 是 MuJoCo 中最常见的元素。它有一个 class 属性回指之前定义的 default,用于自动继承属性。

xml
    <!-- 传感器和电机的安装点(sensors、actuators 的锚点) -->
    <site name="imu"/>
    <site name="motor1_site" pos=" 0.05657  0.05657 0.0"/>
    <site name="motor2_site" pos=" 0.05657 -0.05657 0.0"/>
    <site name="motor3_site" pos="-0.05657 -0.05657 0.0"/>
    <site name="motor4_site" pos="-0.05657  0.05657 0.0"/>
    <site name="center" pos="0.0 0.0 0.0"/>
  </body>
</worldbody>

<site> 是模型上的标记点:它本身没有物理影响,但可以:

  • 作为 驱动器 的施力点(推力作用在电机位置)
  • 作为 传感器 的安装位置(IMU 装在机体中心)
  • 在 viewer 中可视化为小球体,方便调试

pos 坐标是 相对于其父 body 原点的偏移。四个电机布置在 ±56.57mm 的 X 形对角线上(轴距 ≈ 80mm × √2)。

3.6 驱动器:给机器人装上"肌肉" ​

xml
<actuator>
  <!-- 四个升力电机 -->
  <!-- ctrlrange: 推力范围(N) | gear: 6维齿轮比(力X,力Y,力Z,力矩X,力矩Y,力矩Z) -->
  <motor ctrlrange="0 1.5" gear="0 0 1 0 0 -0.00627" site="motor1_site" name="motor1"/>
  <motor ctrlrange="0 1.5" gear="0 0 1 0 0  0.00627" site="motor2_site" name="motor2"/>
  <motor ctrlrange="0 1.5" gear="0 0 1 0 0 -0.00627" site="motor3_site" name="motor3"/>
  <motor ctrlrange="0 1.5" gear="0 0 1 0 0  0.00627" site="motor4_site" name="motor4"/>

  <!-- 外部六轴扰动(用于注入风扰等外部力和力矩) -->
  <motor ctrlrange="-1.0 1.0" gear="1 0 0 0 0 0" site="center" name="forceTx"/>
  <motor ctrlrange="-1.0 1.0" gear="0 1 0 0 0 0" site="center" name="forceTy"/>
  <motor ctrlrange="-1.0 1.0" gear="0 0 1 0 0 0" site="center" name="forceTz"/>
  <motor ctrlrange="-1.0 1.0" gear="0 0 0 1 0 0" site="center" name="torqueMx"/>
  <motor ctrlrange="-1.0 1.0" gear="0 0 0 0 1 0" site="center" name="torqueMy"/>
  <motor ctrlrange="-1.0 1.0" gear="0 0 0 0 0 1" site="center" name="torqueMz"/>
</actuator>

MuJoCo 的 <motor> 元素充当广义力发生器。它的工作原理:

ctrl 值 × gear = 施加在 site 上的广义力 (6维: [fx, fy, fz, tx, ty, tz])

以 motor1 为例:

  • ctrlrange="0 1.5" — 控制输入范围 0~1.5N
  • gear="0 0 1 0 0 -0.00627" — 只在 Z 方向产生推力,同时在 Z 轴产生一个与推力成比例的反扭矩(-0.00627 的正负号区分正反桨)
  • site="motor1_site" — 力施加在电机的安装位置

ctrlrange 有两个关键作用:限幅:控制器写入的值会被自动 clamp,归一化:可视化滑块中作为显示范围。

3.7 传感器:机器人的"感官" ​

xml
<sensor>
  <gyro name="sensorGyro" site="imu"/>          <!-- 机体角速度 (rad/s) -->
  <accelerometer name="sensorAccelerometer" site="imu"/>  <!-- 机体加速度 (m/s²) -->
  <framequat name="sensorFrameQuat" objtype="site" objname="imu"/>  <!-- 姿态四元数 [w,x,y,z] -->
  <framepos name="sensorFramePos" objtype="site" objname="imu"/>    <!-- 世界位置 (m) -->
  <framelinvel name="sensorFrameLinVel" objtype="site" objname="imu"/> <!-- 世界速度 (m/s) -->
</sensor>

所有传感器的读数按定义顺序存储在 d.sensordata 这一个 NumPy 数组中。在代码中获取:

python
sensorData = d.sensordata
gyro  = sensorData[0:3]   # 前3个是 gyro
acc   = sensorData[3:6]   # 接下来3个是 accelerometer
quat  = sensorData[6:10]  # 4个四元数分量
pos   = sensorData[10:13] # 3个位置分量
vel   = sensorData[13:16] # 3个速度分量

重要:sensordata 的顺序由 <sensor> 块中传感器的定义顺序决定,与 <sensor> 块中各子元素的属性名无关。

传感器类型速查表:

标签测量量维度参考系
<gyro>角速度3局部(body)
<accelerometer>加速度3局部(body)
<framequat>姿态四元数4body→world 旋转
<framepos>位置3世界(world)
<framelinvel>线速度3世界(world)

4. 场景文件:搭建仿真舞台 ​

scene.xml 使用 <include> 引入模型定义,实现模型与场景解耦:

xml
<mujoco model="HumpRotor160 Scene">
  <include file="humprotor160.xml"/>
  ...

这样的好处是同一个机器人模型可以被多个场景复用(室内/室外/避障场等等)。

4.1 摄像机 ​

xml
<camera name="world_camera" pos="0.560 -0.687 0.744"
        xyaxes="0.872 0.489 -0.000 -0.260 0.465 0.846"/>

pos 定义相机位置,xyaxes 是一个 3×2 旋转矩阵(去掉第三列,由前两列叉积得到),用于定义相机朝向。简单理解为相机 CS 在 world CS 下的 x 轴向量和 y 轴向量。

4.2 光照 ​

xml
<light pos="0 0 1.5" dir="0 0 -1" directional="true"/>
  • directional="true" = 平行光(模拟太阳),光源位置不影响光照方向
  • directional="false" = 点光源,从 pos 向四周辐射

4.3 天空盒与地面纹理 ​

xml
<texture type="skybox" builtin="gradient" rgb1="0.3 0.5 0.7" rgb2="0 0 0"
         width="512" height="3072"/>

<texture type="2d" name="groundplane" builtin="checker" mark="edge"
         rgb1="0.2 0.3 0.4" rgb2="0.1 0.2 0.3"
         markrgb="0.8 0.8 0.8" width="300" height="300"/>

<material name="groundplane" texture="groundplane"
          texuniform="true" texrepeat="5 5" reflectance="0.2"/>

MuJoCo 内置了 gradient(渐变)和 checker(棋盘格)纹理,不需要 external 图片文件。

4.4 地面 ​

xml
<geom name="floor" size="0 0 0.05" type="plane" material="groundplane"/>

type="plane" 创建无限大平面。其法线方向始终为 z 轴正向,即平面方程为 z = 0(未偏移时)。size 的第三个分量(0.05)将平面上移 5cm,避免与模型视觉网格发生 Z-fighting。


5. Python API:让仿真跑起来 ​

5.1 最简示例:加载模型与步进 ​

python
import mujoco
import numpy as np

# 1. 从 XML 文件加载模型
m = mujoco.MjModel.from_xml_path('humprotor160/scene.xml')

# 2. 创建数据容器
d = mujoco.MjData(m)

# 3. 仿真步进 1000 步(2秒 @ 0.002s 步长)
for i in range(1000):
    mujoco.mj_step(m, d)
    print(f"t={d.time:.3f}s  pos=({d.qpos[0]:.3f}, {d.qpos[1]:.3f}, {d.qpos[2]:.3f})")

mujoco.mj_step(m, d) 执行一步完整的物理仿真:

  1. 计算主动力(来自 actuator 的 ctrl)
  2. 求解接触/约束
  3. 数值积分更新位置和速度
  4. 更新传感器数据

5.2 核心数据结构的访问 ​

python
# --- 模型结构信息(M*Model,带 m 前缀,编译后只读)---
m.nq       # 广义坐标维度(本工程 = 7: [x, y, z, qw, qx, qy, qz])
m.nv       # 广义速度维度(本工程 = 6: [vx, vy, vz, wx, wy, wz])
m.nu       # 控制输入维度(本工程 = 10: 4个电机 + 6个扰动)
m.nsensor  # 传感器总数
m.opt.timestep  # 仿真步长
m.body('humprotor160').id  # 通过名字获取 body ID

# --- 运行时状态(M*Data,带 d 前缀,每步更新)---
d.time     # 当前仿真时间
d.qpos     # 广义位置 (nq,)
d.qvel     # 广义速度 (nv,)
d.qacc     # 广义加速度 (nv,)
d.xpos     # 每个 body 的世界系位置 (nbody, 3)
d.xmat     # 每个 body 的世界系旋转矩阵 (nbody, 3, 3)
d.sensordata  # 所有传感器读数串联 (nsensor,)
d.actuator_force  # 各 actuator 当前施加的力

5.3 直接操纵状态 ​

python
# 重置姿态(零速度,初始位置)
d.qpos[:] = [0, 0, 0.3, 1, 0, 0, 0]  # [x, y, z, qw, qx, qy, qz]
d.qvel[:] = 0

# 设置电机推力
d.actuator('motor1').ctrl[0] = 0.5   # motor1 推力 0.5N
d.actuator('motor2').ctrl[0] = 0.5
d.actuator('motor3').ctrl[0] = 0.5
d.actuator('motor4').ctrl[0] = 0.5

注意:d.actuator('name') 返回 actuator 的引用,修改它的 .ctrl 数组会直接影响下一步仿真。

5.4 使用 viewer 可视化 ​

python
import mujoco.viewer as viewer

# 方法一:手动循环
with viewer.launch(m, d) as v:
    while v.is_running():
        d.actuator('motor1').ctrl[0] = 0.5
        mujoco.mj_step(m, d)
        v.sync()  # 将 viewer 同步到当前状态

# 方法二:使用 loader 回调(像本工程一样)
viewer.launch(loader=loadCallback)

viewer.launch() 会打开一个交互窗口。在窗口中你可以:

  • 鼠标左键拖动:旋转视角
  • 鼠标右键/滚轮:缩放
  • 鼠标中键拖动:平移视角
  • 双击 body:跟踪该刚体
  • Ctrl+鼠标拖动:施加虚拟力扰动

6. 回调函数:仿真过程中的"钩子" ​

MuJoCo 支持注册控制回调函数,在每步仿真前自动调用。这是注入控制逻辑的标准方式。

6.1 注册回调 ​

python
def controlCallback(m, d):
    # 读取传感器数据
    gyro = d.sensordata[0:3]
    # 计算控制量
    thrust = compute_thrust(gyro)
    # 写入 actuator
    d.actuator('motor1').ctrl[0] = thrust[0]
    ...

# 注册全局回调
mujoco.set_mjcb_control(lambda m, d: controlCallback(m, d))

6.2 使用 loader 回调管理生命周期 ​

python
def loadCallback(m=None, d=None):
    mujoco.set_mjcb_control(None)  # 先清除旧回调

    m = mujoco.MjModel.from_xml_path('./humprotor160/scene.xml')
    d = mujoco.MjData(m)

    if m is not None:
        mujoco.set_mjcb_control(lambda m, d: controlCallback(m, d))

    return m, d

viewer.launch(loader=loadCallback)

loader 回调的设计解决了一个关键问题:viewer 重置时如何重新创建模型。用户每次点击 viewer 的 Reset 按钮,loader 都会被调用一次,返回全新的 (m, d),保证重置是干净的。

6.3 定频控制 ​

MuJoCo 的仿真步长可能很小(0.002s),但控制器不需要跑那么快。通过时间比较实现降频:

python
UPDATE_DT = 0.002          # 控制频率 = 500Hz
lastUpdateTime = 0.0

def controlCallback(m, d):
    global lastUpdateTime
    currentTime = d.time

    # 重置检测
    if currentTime < lastUpdateTime:
        lastUpdateTime = currentTime

    # 不到时间就不执行
    if currentTime - lastUpdateTime < UPDATE_DT:
        return

    dt = currentTime - lastUpdateTime
    lastUpdateTime = currentTime

    # 控制逻辑...

7. 驱动器与传感器:给机器人装上肌肉和感官 ​

7.1 驱动器(Actuator)详解 ​

MuJoCo 的 actuator 是广义力发生器,不仅仅局限于电机。常用类型:

标签作用用途
<motor>施加 6 维广义力 (fx, fy, fz, tx, ty, tz)旋翼推力、扰动力
<position>位置伺服(PD 控制)舵机、关节角度控制
<velocity>速度伺服轮式机器人驱动
<cylinder>液压/气动缸液压机器人
<muscle>肌肉模型生物力学

gear 属性是一个 6 维向量,在 actuator 的 site 坐标系中解释:

[力X, 力Y, 力Z, 力矩X, 力矩Y, 力矩Z]

通过将某些分量设为 0,可以将 actuator 的作用限制在特定方向。

7.2 传感器(Sensor)详解 ​

标签数据维度说明
<gyro>3局部系(body)角速度
<accelerometer>3局部系加速度(含重力)
<framequat>4body→world 旋转四元数 [w,x,y,z]
<framepos>3world 位置
<framelinvel>3world 线速度
<force>3关节/actuator 力
<torque>3关节/actuator 力矩
<touch>1接触检测

关键:<accelerometer> 测量的加速度包含重力项。如果飞行器悬停在空中,加速度计输出 [0, 0, -9.81](ENU 坐标系,z 轴向上时),而不是零。这是因为加速度计实际记录的是 比力(specific force):作用在机体上所有非引力外力的合力除以质量。


8. 完整示例:四旋翼悬停仿真 ​

将以上所有知识串联起来,下面是一个简化版悬停仿真(完整代码见 main.py):

python
import mujoco
import mujoco.viewer as viewer
import numpy as np
from simplemath import quat2euler, quat2mat

UPDATE_DT = 0.002
lastUpdateTime = 0.0

# 物理参数
MASS = 0.2436           # kg
HOVER_THRUST = MASS * 9.81 / 4.0  # 每电机悬停推力 ≈ 0.597N

def motorUpdate(d, dt, thrust):
    """将推力写入四个电机"""
    d.actuator('motor1').ctrl[0] = thrust[0]
    d.actuator('motor2').ctrl[0] = thrust[1]
    d.actuator('motor3').ctrl[0] = thrust[2]
    d.actuator('motor4').ctrl[0] = thrust[3]

def controlCallback(m, d):
    global lastUpdateTime

    currentTime = d.time
    if currentTime < lastUpdateTime:
        lastUpdateTime = currentTime
    if currentTime - lastUpdateTime < UPDATE_DT:
        return
    dt = currentTime - lastUpdateTime
    lastUpdateTime = currentTime

    # 读取传感器数据
    sd = d.sensordata
    gyro  = sd[0:3]    # 机体角速度
    acc   = sd[3:6]    # 机体加速度
    quat  = sd[6:10]   # 姿态四元数 [w,x,y,z]
    pos   = sd[10:13]  # 世界位置
    vel   = sd[13:16]  # 世界速度

    euler = quat2euler(quat)
    Rwb = quat2mat(quat)       # body → world 旋转
    velBody = Rwb.T @ vel      # world → body 线速度

    # 简单的 PID 悬停控制(示意)
    # 实际控制算法参考本工程后续章节
    thrust = np.full(4, HOVER_THRUST)
    motorUpdate(d, dt, thrust)

def loadCallback(m=None, d=None):
    mujoco.set_mjcb_control(None)
    m = mujoco.MjModel.from_xml_path('./humprotor160/scene.xml')
    d = mujoco.MjData(m)
    if m is not None:
        mujoco.set_mjcb_control(lambda m, d: controlCallback(m, d))
    return m, d

if __name__ == '__main__':
    try:
        viewer.launch(loader=loadCallback)
    finally:
        print("程序已退出")

9. 常见问题与调试技巧 ​

9.1 仿真发散("爆炸") ​

症状:几秒后位置/速度变成 NaN 或极大值。

原因与解决:

可能原因解决方案
步长过大减小 timestep,或用 RK4 替换 Euler
控制输入过大检查 ctrlrange,确保推力不超限
惯量设置不合理用平行轴定理手动计算 diaginertia
碰撞体穿透增大 solref 刚度或 solimp 阻尼

9.2 传感器读数为零 ​

检查传感器的 site 属性是否正确引用了模型中的 site 名称。site 必须在对应的 body 下定义。

9.3 可视化模型不显示 ​

  • group 值:viewer 默认显示 group 0-3,检查你的 <geom> 的 group 属性
  • contype / conaffinity:这些属性不影响可见性,只影响碰撞
  • mesh 文件路径:compiler meshdir 指定的是相对 XML 文件所在目录的路径

9.4 调试技巧 ​

python
# 打印所有 actuator 名称
for i in range(m.nu):
    print(f"actuator[{i}]: {mujoco.mj_id2name(m, mujoco.mjtObj.mjOBJ_ACTUATOR, i)}")

# 打印所有 sensor 名称和维度
for i in range(m.nsensor):
    name = mujoco.mj_id2name(m, mujoco.mjtObj.mjOBJ_SENSOR, i)
    dim = m.sensor_dim[i]
    print(f"sensor[{i}]: {name} (dim={dim})")

# 打印 sensordata 的分段索引
offset = 0
for i in range(m.nsensor):
    dim = m.sensor_dim[i]
    name = mujoco.mj_id2name(m, mujoco.mjtObj.mjOBJ_SENSOR, i)
    print(f"  {name}: sensordata[{offset}:{offset+dim}]")
    offset += dim

本章参考资料与引申阅读已汇总至 参考资料。