模型编辑#

新 API

下面描述的 API 是新的,但功能完整。推荐用于一般用途,但仍可能存在潜在的错误。请在 GitHub 上报告任何问题。

自 MuJoCo 3.2.0 起,可以使用 mjSpec 结构体和相关 API 创建和修改模型。此数据结构与 MJCF 一一对应,实际上,MuJoCo 自身的 XML 解析器(包括 MJCF 和 URDF)在加载模型时也使用此 API。

概述#

新 API 增强了使用 XML 文件创建和编辑模型的传统工作流程,将*解析*和*编译*步骤分开。如概述章节中所总结的,传统工作流程是:

  1. 创建一个 XML 模型描述文件(MJCF 或 URDF)及相关资源。

  2. 调用 mj_loadXML,获取一个 mjModel 实例。

使用 mjSpec 的新工作流程是:

  1. 创建一个空的 mjSpec 或解析一个现有的 XML 文件。

  2. 通过添加、修改和移除元素,以编程方式编辑 mjSpec 数据结构。

  3. 将 mjSpec 编译成一个 mjModel 实例。

编译后,mjSpec 仍然可以编辑,因此第 2 步和第 3 步可以互换。

用法#

这里我们描述了用于程序化模型编辑的 C API,但它也暴露在Python 绑定中。高级用户可以参考 user_api_test.ccxml_native_reader.cc 中的 MJCF 解析器以获取更多用法示例。在创建一个新的 mjSpec 或将现有 XML 文件解析为 mjSpec 后,程序化编辑对应于设置属性。例如,要更改时间步长,可以这样做:

mjSpec* spec = mj_makeSpec();
spec->opt.timestep = 0.01;
...
mjModel* model = mj_compile(spec, NULL);

长度可变的属性是 C++ 向量和字符串,作为不透明类型暴露给 C。在 C 语言中,使用提供的获取器设置器

mjs_setString(model->modelname, "my_model");

在 C++ 中,可以直接使用向量和字符串:

std::string modelname = "my_model";
*spec->modelname = modelname;

从 XML 加载 spec 可以按如下方式完成:

std::array<char, 1000> error;
mjSpec* s = mj_parseXML(filename, vfs, error.data(), error.size());

模型元素#

与 MJCF 对应的模型元素以带有 mjs 前缀的 C 结构体形式暴露给用户,其定义列在结构体参考的模型编辑部分下。例如,一个 MJCF geom 对应一个 mjsGeom

所有元素的全局默认值由初始化器设置,如 mjs_defaultGeom。这些函数在 user_init.c 中定义,是所有默认值的真实来源。

元素不能直接创建;它们由相应的构造函数返回给用户,例如 mjs_addGeom。例如,要向世界 body 添加一个盒子 geom,可以这样做:

mjSpec* spec = mj_makeSpec();                                  // make an empty spec
mjsBody* world = mjs_findBody(spec, "world");                  // find the world body
mjsGeom* my_geom = mjs_addGeom(world, NULL);                   // add a geom to the world
my_geom->type = mjGEOM_BOX;                                    // set geom type
my_geom->size[0] = my_geom->size[1] = my_geom->size[2] = 0.5;  // set box size
mjModel* model = mj_compile(spec, NULL);                       // compile to mjModel
...
mj_deleteModel(model);                                         // free model
mj_deleteSpec(spec);                                           // free spec

mjs_addGeom 的第二个参数 NULL 是可选的默认类指针。当以程序化方式使用默认值时,默认类会显式传递给元素构造函数。所有元素的全局默认值(在没有传入默认类时使用)可以在 user_init.c 中查看。

内存管理#

如上例所示,模型元素从不由用户直接分配,而是由构造函数返回。库拥有所有元素的所有权,并在父 mjSpec 使用 mj_deleteSpec 删除时释放它们。用户只负责释放 mjSpec 结构体。

附加#

该框架引入了一个强大的新功能:附加和删除模型子树。此功能已被用于支持 MJCF 中的 attachreplicate 元元素。附加功能允许用户将一个子树从一个模型移动或复制到另一个模型,同时也会复制或移动相关的被引用资源以及运动树外部的引用元素(例如,执行器和传感器)。类似地,删除一个子树将从模型中移除所有关联的元素。默认行为(“浅拷贝”)是在附加时将子模型移动到父模型中,因此对子模型的后续更改也会改变父模型。或者,用户可以选择在附加时使用 mjs_setDeepCopy 创建一个全新的副本。在解析 XML 时,此标志会暂时设置为 true。可以将一个 body 或一个 mjSpec 附加到一个 frame

mjSpec* parent = mj_makeSpec();
mjSpec* child = mj_makeSpec();
parent->compiler.degree = 0;
child->compiler.degree = 1;
mjsElement* frame = mjs_addFrame(mjs_findBody(parent, "world"), NULL)->element;
mjsElement* body = mjs_addBody(mjs_findBody(child, "world"), NULL)->element;
mjsBody* attached_body_1 = mjs_asBody(mjs_attach(frame, body, "attached-", "-1"));

将一个 body 或一个 mjSpec 附加到一个 site

mjSpec* parent = mj_makeSpec();
mjSpec* child = mj_makeSpec();
mjsElement* site = mjs_addSite(mjs_findBody(parent, "world"), NULL)->element;
mjsElement* body = mjs_addBody(mjs_findBody(child, "world"), NULL)->element;
mjsBody* attached_body_2 = mjs_asBody(mjs_attach(site, body, "attached-", "-2"));

将一个 frame 或一个 mjSpec 附加到一个 body

mjSpec* parent = mj_makeSpec();
mjSpec* child = mj_makeSpec();
mjsElement* body = mjs_addBody(mjs_findBody(parent, "world"), NULL)->element;
mjsElement* frame = mjs_addFrame(mjs_findBody(child, "world"), NULL)->element;
mjsFrame* attached_frame = mjs_asFrame(mjs_attach(body, frame, "attached-", "-1"));

请注意,在上述示例中,父模型和子模型的 compiler.degree 值不同,对应于 compiler/angle 属性,指定了角度解释的单位。编译器标志在附加过程中会一并传递,因此子模型将使用子模型的标志进行编译,而父模型将使用父模型的标志进行编译。

另请注意,一旦子模型通过引用附加到父模型,子模型就不能单独编译。

已知问题

存在以下已知限制:

  • 如果父模型和子模型不是同一个 mjSpec,则子模型的所有资源都将被复制进来,无论它们是否被引用。

  • 不检查循环引用,这会导致无限循环。

  • 当附加带有关键帧的模型时,需要进行模型编译才能完成重新索引。如果在没有编译的情况下执行第二次附加,第一次附加的关键帧将会丢失。

默认类#

新 API 完全支持默认类,但使用它们需要理解默认值的实现方式。如默认设置部分所述,默认类首先作为一棵虚拟元素树被加载,然后用于初始化引用它们的元素。当编辑带有默认值的模型时,这种初始化是显式的:

mjSpec* spec = mj_makeSpec();
mjsDefault* main = mjs_getSpecDefault(spec);
main->geom.type = mjGEOM_BOX;
mjsGeom* geom = mjs_addGeom(mjs_findBody(spec, "world"), main);

重要的是,在默认类被用于初始化元素后更改它,不会改变已初始化元素的属性。

未来可能的更改

上述行为,即默认值仅在初始化时应用,是旧的、仅限 XML 的加载流程的遗留物。未来的 API 更改可能会允许在初始化后更改和应用默认值。如果您认为此功能对您很重要,请在 GitHub 上告知我们。

XML 保存#

可以使用 mj_saveXMLmj_saveXMLString 分别将 spec 保存到 XML 文件或字符串中。保存要求 spec 首先被编译。重要的是,保存的 XML 会考虑任何已定义的默认值。当一个模型有许多重复的值时,这很有用,例如从不支持默认值的 URDF 加载时。在这种情况下,可以添加默认类,设置相关元素的类,然后保存;生成的 XML 将使用默认值,更具可读性。

原地重新编译#

可以随时调用 mj_compile 进行编译以获取新的 mjModel 实例。相比之下,mj_recompile 会原地更新现有的 mjModel 和 mjData 对,同时保留仿真状态。这允许模型编辑在**仿真期间**进行,例如添加或删除 body。