这篇文章是 Mujoco 学习系列第二篇,主要介绍一些基础功能与 xmI 使用,重点在于如何编写与读懂 xml 文件。
运行这篇博客前请先确保正确安装 Mujoco 并通过了基本功能与GUI的验证,即至少完整下面这个博客的 第二章节 内容:
1. 启动仿真器
在第一篇博客中已经介绍了如何通过命令启动仿真器,但实际上mujoco提供了很多种方式启动,不同启动方式在后面的工作中会有不同的作用。
1.1 python 命令行启动
- 只启动仿真器,不加载模型
(mojoco) $ python -m mujoco.viewer
- 启动仿真器,同时加载模型(这里加载自带的小车模型)
(mojoco) $ python -m mujoco.viewer --mjcf=./model/car/car.xml
1.2 python 脚本启动
- 启动仿真器,不加载模型(阻塞)
import mujoco
mojoco.viewer.launch()
- 启动仿真器,同时加载模型(阻塞)
import mujoco
model_xml_path = "./model/car/car.xml"
mujoco.viewer.launch_from_path(model_xml_path)
上面两种阻塞方式启动后 terminal 会一直等待你在仿真器中操作完并关闭。mujoco 也提供了非阻塞方式启动仿真器 launch_passive(model, data)
,但在启动时必须将模型加载进来,同时需要手动管理 mj_step()
函数,而以阻塞方式启动的仿真器不需要显示调用该函数。因为是非阻塞方式启动,需要将仿真器放在一个循环中,否则一启动就会立刻关闭。
- 启动仿真器,同时加载模型(非阻塞)
import time
import mujoco
import mujoco.viewer
model_xml_path = "./model/car/car.xml"
model = mujoco.MjModel.from_xml_path(model_xml_path)
data = mujoco.MjData(model)
with mujoco.viewer.launch_passive(model=model, data=data) as viewer:
while viewer.is_running():
step_start = time.time() # 每一帧仿真的开始时间,用于控制仿真的时间步长
mujoco.mj_step(model, data) # [核心] 手动推进一次仿真
# 给 simulate GUI 加锁,防止数据修改线程与渲染线程出现冲突
with viewer.lock():
# 这行主要是提升交互体验,你在运行后可以发现环境中每个接触点都会有黄色的圆柱在闪烁
viewer.opt.flags[mujoco.mjtVisFlag.mjVIS_CONTACTPOINT] = int(data.time % 2)
viewer.sync() # 将最新的数据同步给GUI中并显示
# 到达此处说明当前帧的运算和渲染已经结束了,计算一下到下一帧的时间间隔,用于控制仿真节奏
# 如果仿真在这一步消耗了很长时间,那么该值是有可能为负
time_until_next_step = model.opt.timestep - (time.time() - step_start)
if time_until_next_step > 0:
time.sleep(time_until_next_step) # 休眠一下后准备计算下一帧
运行之后就可以看到上图中的效果,可以发现在小车三个轮子的位置处有黄色的矮圆柱体在闪烁,这就是代码中 viewer.opt.flags
部分起的作用。
2. 编写 xml 文件
在上一章节中其实遗留了一个问题:多个模型的加载在机器人仿真中是必要的,但 mujoco 本身是不支持同时加载多个 xml
文件的,因为 mujoco 是 面向单物理场景 设计的,只不过有方法来实现这点,上面例子中的小车本质上就是在一个 xml 文件中创建了不同元素并将其组合,多个模型加载问题可以被转化成不同元素但不进行组合。
在编写之前需要先理解 mujoco 如何解析 xml 文件的,特别是哪些标签是核心的、哪些是可以以类形式定义等。
- 官方解释链接:XMLreference.html
我将 xml 的标签分为两类:
- 环境标签:这类标签定义了全局仿真配置,包括重力、第三人称相机视角、密度、时间步长等;
- 对象标签:这类标签是可以继承、包含、相互作用,有点类似代码中的 class;
这种分类方式实际上不严谨,但对于初学者而言可以先这样简化地去理解。
2.1 立方体与平面
首先是最简单的一个例子,在一个空间中有一个立方体和一个平面,期望立方体自由落体后掉在平面上:
<mujoco>
<!-- 场景 -->
<worldbody>
<!-- 光源 -->
<light diffuse="0.5 0.5 0.5" pos="0 0 3" dir="0 0 -1"/>
<!-- 平面 -->
<geom name="ground" type="plane" size="1 1 0.1" rgba="0.5 0.5 0.5 1"/>
<!-- 对象 -->
<body name="cube" pos="0 0 1">
<joint type="free"/> <!-- 该对象与外界的链接方式 -->
<geom type="box" size="0.1 0.1 0.1" rgba="0.0 0.0 0.5 1"/>
</body>
</worldbody>
</mujoco>
上面的例子中顶级标签为 <mujoco>
,其他所有对象都在 <worldbody>
标签下,在这个标签中有两个对象 平面 和 立方体。
【Note】:虽然mujoco官方文档和很多教程都告诉你可以省略 name
字段,没有显示声明的情况下会自动分配一个匿名值,但我会将 name
字段一直写上去,因为 mujoco 不允许出现同名对象,同时有 name
字段可以帮你更快定位到问题。
直接运行就可以看到一个立方体自由落体到平面上:
(mujoco) $ python -m mujoco.viewer --mjcf=./merge.xml
实际上在xml中的 平面 也是一个对象,但我个人习惯地面对象不用 body
标签包裹以区分运动对象和地面,所以下面的写法也是正确的:
<mujoco>
<!-- 场景 -->
<worldbody>
<!-- 光源 -->
<light diffuse="0.5 0.5 0.5" pos="0 0 3" dir="0 0 -1"/>
<!-- 平面 -->
<body name="ground" pos="0 0 0" >
<geom type="plane" size="1 1 0.1" rgba="0.5 0.5 0.5 1"/>
</body>
<!-- 对象 -->
<body name="cube" pos="0 0 1">
<joint type="free"/>
<geom type="box" size="0.1 0.1 0.1" rgba="0.0 0.0 0.5 1"/> <!-- 对象的几何形状 -->
</body>
</worldbody>
</mujoco>
同理,如果想要在不同位置添加一个新的立方体则如下所示,新的立方体中多了一个 euler
属性表示其初始角度信息,更多属性以及其默认值可以在官网文档中找到:
<mujoco>
<worldbody>
<light diffuse="0.5 0.5 0.5" pos="0 0 3" dir="0 0 -1"/>
<geom name="ground" type="plane" size="1 1 0.1" rgba="0.5 0.5 0.5 1"/>
<!-- 立方体1 -->
<body name="cube1" pos="0 0 1">
<joint type="free"/>
<geom type="box" size="0.1 0.1 0.1" rgba="0.0 0.0 0.5 1"/>
</body>
<!-- 立方体2 -->
<body name="cube2" pos="0.5 0.5 0.5" euler="0 20 30">
<joint type="free"/>
<geom type="box" size="0.1 0.1 0.1" rgba="0.5 0.0 0.0 1"/>
</body>
</worldbody>
</mujoco>
运行后也是两个立方体自由落体,只不过初试高度和角度不同,因此最终落地姿势也不同。
(mujoco) $ python -m mujoco.viewer --mjcf=./merge.xml
2.2 环境标签
通常情况下环境标签是在第一步就需要做的,为了避免不同平台中存在差异,虽然 mujoco 允许在运行过程中修改环境标签的值,例如更改重力方向,但在没有特殊需求的情况下这些值应该被定义为一个静态值。
在 mujoco 中通过 xml 里的 <option>
标签定义环境,可以修改的属性值有以下几个:
我最常用的是下面几个:
- timestep:仿真时间步长,默认 0.002 0.002 0.002s,影响计算速度与精度的最重要参数;
- gravity:重力方向,默认 ( 0 , 0 , − 9.81 ) (0,0,-9.81) (0,0,−9.81);
- density:环境介质密度,默认 0 0 0,可以修改你的仿真环境是在水下还是空气中,默认在空气中;
xml 文件示例如下:
<mujoco>
<!-- 环境标签 -->
<option gravity="0 0 -1" />
<worldbody>
<light diffuse="0.5 0.5 0.5" pos="0 0 3" dir="0 0 -1"/>
<geom name="ground" type="plane" size="1 1 0.1" rgba="0.5 0.5 0.5 1"/>
<body name="cube" pos="0 0 1">
<joint type="free"/>
<geom type="box" size="0.1 0.1 0.1" rgba="0.0 0.0 0.5 1"/>
</body>
</worldbody>
</mujoco>
【Note】:注意环境标签 <option>
的位置,由于环境标签是全局作用的,因此需要将其放在顶级域名之下。
2.3 单位与轴
因为存在 万向节死锁 的问题,有些算法会调整 rpy
的旋转顺序,mujoco 提供了标签 <compile>
来定义单位与轴旋转顺序;在没有明确定义的情况下度数单位为 弧度,但也可以通过修改来确定度的单位为 角度;
示例如下:
<mujoco>
<compiler angle="degree" eulerseq="yzx"/>
<worldbody>
<light diffuse="0.5 0.5 0.5" pos="0 0 3" dir="0 0 -1"/>
<geom name="ground" type="plane" size="1 1 0.1" rgba="0.5 0.5 0.5 1"/>
<body name="cube" pos="0 0 1">
<joint type="free"/>
<geom type="box" size="0.1 0.1 0.1" rgba="0.0 0.0 0.5 1"/>
</body>
</worldbody>
</mujoco>
这些本质上和环境标签是同一类型,都是确定好后不会频繁变化的,因此在曾经结构上也是顶级位置。
2.4 通用资产定义
有些属性或变量可能会被多个对象使用,如果每个对象都重新写一遍会非常冗余,mujoco 提供了 <asset>
标签用来定义通用资产,并且允许对象直接使用。
<mujoco>
<asset>
<!-- 定义材质 -->
<material name="blue" rgba="0 0 0.5 1"/>
<!-- 定义凸包 -->
<mesh name="tetrahedron" vertex="0 0 0 1 0 0 0 1 0 0 0 1"/>
</asset>
<worldbody>
<light diffuse="0.5 0.5 0.5" pos="0 0 3" dir="0 0 -1"/>
<geom name="ground" type="plane" size="1 1 0.1" rgba="0.5 0.5 0.5 1"/>
<body name="cube" pos="0 0 1">
<joint type="free"/>
<!-- 使用材质和凸包 -->
<geom type="box" size="0.1 0.1 0.1" material="blue" mesh="tetrahedron"/>
</body>
</worldbody>
</mujoco>
上面的示例中使用了 mesh
这个标签,本质是表面网格,但 网格也可以定义成不带面的网格(本质上是点云)。在这种情况下,即使编译器属性 convexhull
为 false,凸包也会自动构建。这使得直接在 XML 中构建简单形状变得非常简单。例如,可以如下创建金字塔:
如果你想要使用 mesh
原本的含义,即物体表面渲染方式,那么这样写即可:
<mujoco>
<asset>
<material name="blue" rgba="0 0 0.5 1"/>
<!-- 前提是同级目录下有这个文件 -->
<mesh name="forearm" file="forearm.stl"/>
</asset>
<worldbody>
<light diffuse="0.5 0.5 0.5" pos="0 0 3" dir="0 0 -1"/>
<geom name="ground" type="plane" size="1 1 0.1" rgba="0.5 0.5 0.5 1"/>
<body name="cube" pos="0 0 1">
<joint type="free"/>
<geom type="box" size="0.1 0.1 0.1" material="blue" mesh="forearm"/>
</body>
</worldbody>
</mujoco>
【Note】:由于 mujoco 不允许相同的 name
属性,因此定义 <asset>
标签时可以用一些带有前缀的变量,如 name="asset_blue"
,这样可以避免在复杂工程中存在名称冲突的情况,特别是有些厂商提供的模型文件中也定义了颜色和材质等对象。
2.5 文件包含
如果把所有的配置都写在一个文件中会非常难以梳理,mujoco 提供了 <include>
标签实现文件包含,我通常会将环境、单位、通用资产的定义写在一个 common.xml
文件中,在主文件中只关注对象的运动关系:
【Note】:所有包含与被包含文件中的元素都必须在 <mujoco>
这个根标签下,这是mujoco识别的依据。
- common.xml 文件
<mujoco>
<asset>
<material name="blue" rgba="0 0 0.5 1"/>
</asset>
</mujoco>
- merge.xml 文件
<mujoco>
<!-- 包含公共变量与资产 -->
<include file="./common.xml"/>
<worldbody>
<light diffuse="0.5 0.5 0.5" pos="0 0 3" dir="0 0 -1"/>
<geom name="ground" type="plane" size="1 1 0.1" rgba="0.5 0.5 0.5 1"/>
<body name="cube" pos="0 0 1">
<joint type="free"/>
<geom type="box" size="0.1 0.1 0.1" material="blue"/>
</body>
</worldbody>
</mujoco>
2.6 关节约束
重头戏来了 <joint>
关节约束。mujoco 对关节约束的定义和 urdf 文件基本一致,允许一下几种形式的约束:
- free:三个平移自由度 + 三个旋转自由度;
- ball:三个旋转自由度的球形关节,四元数 (1,0,0,0) 对应于定义初始状态;
- slide:一个平移自由度的滑动或平移关节,需要明确平移方向;
- hinge:铰链类型创建具有一个旋转自由度的铰链关节,需要明确旋转轴;
【Note】:为了更好的交互效果,这里提前引入了<actuator>
标签,否则无法在仿真器中拖拽。
示例如下:
<mujoco>
<worldbody>
<light diffuse="0.5 0.5 0.5" pos="0 0 3" dir="0 0 -1"/>
<geom name="ground" type="plane" size="5 5 0.1" rgba="0.5 0.5 0.5 1" friction="0.1 0.05 0.05"/>
<!-- 可拖拽立方体 -->
<body name="draggable_cube" pos="0 0 1">
<joint name="x_slide" type="slide" axis="1 0 0" damping="5" stiffness="50" range="-3 3"/>
<joint name="y_slide" type="slide" axis="0 1 0" damping="5" stiffness="50" range="-3 3"/>
<geom name="cube" type="box" size="0.1 0.1 0.1" rgba="0 0.5 0.8 1" mass="5"/>
</body>
</worldbody>
<actuator>
<!-- 位置伺服控制器 -->
<position name="x_pos" joint="x_slide" kp="500" kv="20"/>
<position name="y_pos" joint="y_slide" kp="500" kv="20"/>
</actuator>
</mujoco>
启动仿真器后可以在右侧的 Control
面板中拖动滑块以观察立方体运动。
2.7 定义执行器
在上面的小节中提前用到了执行器 <actuator>
,这一小节将更细致介绍如何定义执行器标签来控制环境中的对象,你可以将执行器理解为 电机;
mujoco 提供了很多执行器工具,在其官网文档中点击左侧 XML Reference
连接后往下拉或者搜索 actuator
即可找到可用的执行器标签。为关节或对象添加执行器后就可以在控制对象的时候添加合适的参数,否则所有 joint 都是 free 形式。
我这里只列举我最常用的几个执行器,感兴趣的可以查看其官网并进行实验。
2.7.1 通用执行器 general
通用执行器允许独立设置所有执行器组件,包括传输类型、激活动态、增益类型等。是一个非常灵活的执行器类型,允许用户自定义执行器的动态特性、增益、偏置,可以实现各种类型的执行器,如直接驱动、位置伺服、速度伺服等。
这个例子实现了一个力执行器,为了让滑块能够在力消失时停下来,在地面和滑块中都添加了friction
属性以体现摩擦力:
<mujoco>
<worldbody>
<light diffuse="0.5 0.5 0.5" pos="0 0 5" dir="0 0 -1"/>
<!-- 地面:高滑动摩擦 -->
<geom name="ground" type="plane" size="5 5 0.1" pos="0 0 0"
rgba="0.5 0.5 0.5 1" friction="1.5 0.3 0.05"
solimp="0.9 0.95 0.001" solref="0.02 1"/>
<!-- 滑块:中等摩擦 -->
<body name="slider" pos="0 0 0.1">
<joint name="slide_joint" type="slide" axis="1 0 0" damping="0.2"/>
<geom type="box" size="0.2 0.1 0.1" rgba="0 0.5 0.8 1" mass="0.5" friction="1.0 0.2 0.02"/>
</body>
</worldbody>
<actuator>
<general name="force" joint="slide_joint" ctrlrange="-1 1"/>
</actuator>
</mujoco>
启动仿真器后在右侧控制面板中拖拽 force
滑块就可以给其传入一个力,点击 Clear all
就可以将力清空然后观察滑块逐渐停下来。
由于 joint 的活动范围可以通过其属性 range
定义,如果不想给物体添加摩擦力也可以通过限制活动范围让物体停下来,停下后即便有力物体也不会继续运动
<mujoco>
<worldbody>
<light diffuse="0.5 0.5 0.5" pos="0 0 5" dir="0 0 -1"/>
<!-- 地面-->
<geom name="ground" type="plane" size="5 5 0.1" pos="0 0 0"
rgba="0.5 0.5 0.5 1"
solimp="0.9 0.95 0.001" solref="0.02 1"/>
<!-- 滑块 添加了range属性限制活动范围 -->
<body name="slider" pos="0 0 0.1">
<joint name="slide_joint" type="slide" axis="1 0 0" damping="0.2" range="-2 2"/>
<geom type="box" size="0.2 0.1 0.1" rgba="0 0.5 0.8 1" mass="0.5" />
</body>
</worldbody>
<actuator>
<general name="force" joint="slide_joint" ctrlrange="-1 1"/>
</actuator>
</mujoco>
2.7.2 位置伺服 position
位置伺服在上面的小节中已经用了一次,这个控制器就是一个位置环伺服,有点类似与 PID 一样的控制器,所以你在滑动的时候会发现有一个明显的回调过程,通过修改 kp
和 kv
可以体验阻尼大小。
<mujoco>
<worldbody>
<light diffuse="0.5 0.5 0.5" pos="0 0 5" dir="0 0 -1"/>
<geom name="ground" type="plane" size="5 5 0.1" pos="0 0 0"
rgba="0.5 0.5 0.5 1"
solimp="0.9 0.95 0.001" solref="0.02 1"/>
<body name="slider" pos="0 0 0.1">
<joint name="slide_joint" type="slide" axis="1 0 0" damping="0.2" range="-2 2"/>
<geom type="box" size="0.2 0.1 0.1" rgba="0 0.5 0.8 1" mass="0.5" />
</body>
</worldbody>
<actuator>
<position name="position" joint="slide_joint" kp="100" kv="10"/>
</actuator>
</mujoco>
2.7.3 速度伺服 velocity
既然有位置伺服那当然还有速度伺服,和位置伺服同理,速度伺服也用来调控速度到目标值
<mujoco>
<worldbody>
<light diffuse="0.5 0.5 0.5" pos="0 0 5" dir="0 0 -1"/>
<geom name="ground" type="plane" size="5 5 0.1" pos="0 0 0"
rgba="0.5 0.5 0.5 1"
solimp="0.9 0.95 0.001" solref="0.02 1"/>
<body name="slider" pos="0 0 0.1">
<joint name="slide_joint" type="slide" axis="1 0 0" damping="0.2" range="-2 2"/>
<geom type="box" size="0.2 0.1 0.1" rgba="0 0.5 0.8 1" mass="0.5" />
</body>
</worldbody>
<actuator>
<velocity name="velocity" joint="slide_joint" kv="50"/>
</actuator>
</mujoco>
2.7.4 电机执行器 motor
电机只能输出与关节自由度相同的力和力矩,如果你很明确这个 joint 需要输出的是力,那么建议用这个,虽然通用执行器也可以实现相同的效果,但设置起来没有电机执行器简洁。
<mujoco>
<worldbody>
<light diffuse="0.5 0.5 0.5" pos="0 0 5" dir="0 0 -1"/>
<geom name="ground" type="plane" size="5 5 0.1" pos="0 0 0"
rgba="0.5 0.5 0.5 1"
solimp="0.9 0.95 0.001" solref="0.02 1"/>
<body name="slider" pos="0 0 0.1">
<joint name="slide_joint" type="slide" axis="1 0 0" damping="0.2" range="-2 2"/>
<geom type="box" size="0.2 0.1 0.1" rgba="0 0.5 0.8 1" mass="0.5" />
</body>
</worldbody>
<actuator>
<motor name="motor" joint="slide_joint" gear="1"/>
</actuator>
</mujoco>
2.8 显示坐标轴
坐标轴的显示在仿真中非常重要,打开仿真器后展开左侧工具栏中的 Rendering
标签,通过选择 Frame
即可选择想要显示的坐标轴。