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 是什么

MuJoCoMulti-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

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