Mujoco 学习系列(二)基础功能与xml使用
这篇文章是 Mujoco 学习系列第二篇,主要介绍一些基础功能与 xmI 使用,重点在于如何编写与读懂 xml 文件。
运行这篇博客前请先确保正确安装 Mujoco 并通过了基本功能与GUI的验证,即至少完整下面这个博客的 第二章节 内容:
- Mujoco 学习系列(一)安装与部署
1. 启动仿真器
在第一篇博客中已经介绍了如何通过命令启动仿真器,但实际上mujoco提供了很多种方式启动,不同启动方式在后面的工作中会有不同的作用。
1.1 python 命令行启动
- 只启动仿真器,不加载模型
(mojoco) $ python -m mujoco.viewer
- 启动仿真器,同时加载模型(这里加载自带的小车模型)
(mojoco) $ python -m mujoco.viewer --mjcf=./model/car/car.xml
1.2 python 脚本启动
- 启动仿真器,不加载模型(阻塞)
import mujocomojoco.viewer.launch()
- 启动仿真器,同时加载模型(阻塞)
import mujocomodel_xml_path = "./model/car/car.xml"
mujoco.viewer.launch_from_path(model_xml_path)
上面两种阻塞方式启动后 terminal 会一直等待你在仿真器中操作完并关闭。mujoco 也提供了非阻塞方式启动仿真器 launch_passive(model, data)
,但在启动时必须将模型加载进来,同时需要手动管理 mj_step()
函数,而以阻塞方式启动的仿真器不需要显示调用该函数。因为是非阻塞方式启动,需要将仿真器放在一个循环中,否则一启动就会立刻关闭。
- 启动仿真器,同时加载模型(非阻塞)
import timeimport mujoco
import mujoco.viewermodel_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>
,这一小节将更细致介绍如何定义执行器标签来控制环境中的对象。
【未完待续】