模型编辑#

可以通过 mjSpec 结构体及相关 API 创建和修改模型。该数据结构与 MJCF 一一对应,事实上,MuJoCo 自身的 XML 解析器(MJCF 和 URDF)在加载模型时正是使用此 API。

概述#

该 API 扩展了使用 XML 文件创建和编辑模型的传统工作流程,将“解析(parse)”和“编译(compile)”步骤分离开来。正如概览章节中所总结的,传统工作流程为:

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

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

使用 mjSpec 的工作流程为:

  1. 使用 mj_makeSpec 创建一个空的 mjSpec,或者使用 mj_parseXML 解析现有的 XML 文件。

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

  3. 使用 mj_compilemjSpec 编译为 mjModel 实例。

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

用法#

这里我们介绍用于程序化模型编辑的 C API,它也提供在 Python 绑定中。高级用户可以参考 user_api_test.cc 以及 xml_native_reader.cc 中的 MJCF 解析器以获取更多使用示例。在创建新的 mjSpec 或将现有 XML 文件解析为 mjSpec 后,程序化编辑对应于设置属性。例如,要更改时间步长(timestep),可以执行:

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

变长属性为 C++ 向量和字符串,在 C 中表现为不透明类型。在 C 中,需要使用提供的 获取器 (getters)设置器 (setters)

mjs_setString(spec->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

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

元素不能直接创建;它们由相应的构造函数返回给用户,例如 mjs_addGeom。例如,要向世界体(world 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 中查看。

内存管理#

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

附加#

此框架引入了一项强大的新功能:附加(attaching)和删除模型子树。该功能已用于支持 MJCF 中的 attachreplicate 元元素。附加允许用户将子树从一个模型移动或复制到另一个模型,同时还会复制或移动相关引用的资产以及来自运动树外部的引用元素(例如,执行器和传感器)。同样,删除子树将从模型中移除所有关联元素。默认行为(“浅拷贝”)是在附加时将子对象移动到父对象中,因此对子对象的后续更改也会更改父对象。或者,用户可以选择在附加时使用 mjs_setDeepCopy 进行全新的复制。在解析 XML 时,此标志会临时设置为 true。可以将 一个主体或 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"));

或者 将主体或 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"));

或者 将框架或 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,则无论是否引用,子模型中的所有资产都将被复制进来。

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

  • 在附加带有 关键帧 (keyframes) 的模型时,需要进行模型编译以完成重新索引。如果未进行编译就执行第二次附加,第一次附加的关键帧将会丢失。

默认类#

新 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 保存#

Spec 可以分别使用 mj_saveXMLmj_saveXMLString 保存到 XML 文件或字符串中。保存操作要求 spec 必须先经过编译。重要的是,保存的 XML 将考虑所有已定义的默认值。当模型有许多重复值时(例如从不支持默认值的 URDF 加载时),这非常有用。在这种情况下,可以添加默认类,设置相关元素的类并保存;生成的 XML 将使用默认值,并具有更好的可读性。

就地重新编译#

可以在任何时候调用 mj_compile 来获取新的 mjModel 实例。相比之下,mj_recompile 会原地更新现有的 mjModel 和 mjData 对,同时保持仿真状态。这使得在仿真过程中进行模型编辑(例如添加或移除主体)成为可能。