模型描述的低级数据结构 mjModel
模型描述的低级数据结构 mjModel 可以说是 MuJoCo 用来跟机器打交道的工具。基本上所有的仿真算法都是基于该数据结构进行的。 既然是低级的描述结构,它就没有那么直观,我们想直接从 C/C++ 代码构造一个 mjModel 的对象会比较费劲。实际上我们也不应该直接构造它。 我们可以用高级 MJCF 格式描述仿真场景,然后加载成高级数据结构 mjCModel,最后通过 Compile 接口得到低级的数据结构 mjModel 的对象。
1. struct mjModel
struct mjModel_
// ------------------------------- size
int nq; // 广义坐标维度(位置变量的数量)
int nv; // 自由度(速度变量的数量)
int nbody; // 刚体的数量
int njnt; // 关节的数量
size_t nbuffer; // 缓存的字节数量
// ------------------------------- options and statistics
mjOption opt; // 物理引擎配置
mjVisual vis; // 可视化配置
mjStatistic stat; // 模型统计信息
// ------------------------------- buffers
void* buffer; // 模型缓存
int* body_parentid; // 各刚体父节点id (nbody x 1)
mjModel 被定义在头文件 <mujoco/mjmodel.h>
中,
它是一个很大的数据结构,描述了整个仿真系统的系统模型。
其中每个字段的含义我们会逐渐在fork仓库中添加中文注释。
如左侧代码片段所示,从前到后 mjModel 的字段大体上可以分为尺寸(size)、配置(options and statistics)、缓存(buffer)三类。
尺寸类的字段有 70 多个,大体上是描述整个模型有多少个刚体、多少个关节之类、缓存大小等等各类与数量相关的信息。mjModel 的构造器会用这些信息申请内存, 分配各个对象的偏移地址。
配置类的三个字段有专门的结构体定义。其中物理引擎配置 mjOption 中定义了仿真时间步长、解析器配置、重力加速度、碰撞设定等信息。 可视化配置 mjVisual 中定义的是环境光、渲染质量等信息。统计信息 mjStatistic 中记录的是关于模型的一些综合信息,比如平均质量、平均尺寸等。
缓存类的字段记录了模型的所有数据和内存分配情况。其中void* buffer;
是整个模型的主缓存,
在构造的时候会为它申请一个 nbuffer 个字节的内存空间,用于存放模型的所有数据。其余缓存类字段都是各种数据类型的指针,它们都指向 buffer 内部,
描述着模型的内存分配情况。如果说字段 buffer 中记录的数据(data),那么其余字段则是模型的元数据(metadata)。
2. mjCModel::Compile
在初识仿真系统模型的时候,我们看到 MuJoCo 加载描述文件得到高级 mjCModel 对象之后,
还会通过 mjModel* m = model->Compile(vfs);
将其转换成计算时用的低级 mjModel 对象。
为了兼容 C 语言的异常捕获,这里作者用了一些技巧。下面是该函数的代码片段,它有一个输入参数 vfs 是 MuJoCo 的虚拟文件系统。
暂时不用理会他。函数的一开始声明了两个指针,其中 m 将用于指示低级模型对象,data 只是用来验证模型的。
mjModel* mjCModel::Compile(const mjVFS* vfs) {
// 源码里写了一段注释用来解释为啥这里需要 volatile 的修饰
// 大体意思是,用来解决技巧中用到的 longjmp 和 setjmp 因为编译优化带来的内存泄漏的问题
mjModel* volatile m = nullptr;
mjData* volatile data = nullptr;
然后针对 C 语言的接口,注册错误和警告处理函数。大体套路就是先用局部的函数指针 save_error 和 save_warning 保存当前的处理函数。 再注册编译器的函数 errorhandler 和 warninghandler。在退出该函数的时候,还会把 save_error 和 save_warning 保存的函数还原回去。
// save error and warning handlers
void (*save_error)(const char*) = _mjPRIVATE__get_tls_error_fn();
void (*save_warning)(const char*) = _mjPRIVATE__get_tls_warning_fn();
// install error and warning handlers, clear error and warning
_mjPRIVATE__set_tls_error_fn(errorhandler);
_mjPRIVATE__set_tls_warning_fn(warninghandler);
// 错误消息
errInfo = mjCError();
warningtext[0] = 0;
为了让生成的纹理总是一样的,这里将随机种子设置成了一个固定值。在应用程序中有时需要生成一些随机数,这在机器学习的应用中十分常见。 为了能够生成指定分布的随机数,通常都是用一个确定性的算法来生成一个在统计意义上满足分布的序列,然后从序列的某一项开始取用数据,得到一个看似比较随机的随机数。这个某一项就是随机种子。 只要随机种子是固定的,那么每次重新运行程序产生的随机数就都是一样的。
srand(123); // init random number generator, to make textures reproducible
接下来在一个 try 语句块里面调用 TryCompile 完成具体的编译工作。如果出现了任何异常都会在 catch 语句块中释放申请的资源、恢复异常处理函数、返回 nullptr。 这里第 17 行配合上刚才注册的 errorhandler 里面的 longjump 语句可以用来捕获 C 语言接口中的一些异常。 关于这个技巧可以参考C语言接口与实现(CII)一书的第4章。
try {
if (setjmp(error_jmp_buf) != 0) {
// TryCompile resulted in an mju_error which was converted to a longjmp.
throw mjCError(0, "engine error: %s", errortext);
}
TryCompile(*const_cast(&m), *const_cast(&data), vfs);
} catch (mjCError err) {
// 省略异常处理的代码
}
最后恢复异常处理函数,并返回编译后的对象。
_mjPRIVATE__set_tls_error_fn(save_error);
_mjPRIVATE__set_tls_warning_fn(save_warning);
compiled = true;
return m;
}