模型编辑#
新 API
下面描述的 API 是新的,但功能齐全。推荐用于一般用途,但仍可能存在潜在错误。请在 GitHub 上报告任何问题。
从 MuJoCo 3.2.0 版本开始,可以使用 mjSpec 结构体及相关 API 创建和修改模型。此数据结构与 MJCF 一一对应,实际上,MuJoCo 自己的 XML 解析器(包括 MJCF 和 URDF)在加载模型时都使用了此 API。
概述#
新 API 增强了使用 XML 文件创建和编辑模型的传统工作流程,将解析和编译步骤分解。正如概述章节中所述,传统工作流程是:
创建 XML 模型描述文件(MJCF 或 URDF)及相关资源。
调用 mj_loadXML,获取 mjModel 实例。
使用 mjSpec 的新工作流程是:
用法#
这里我们描述了用于过程化模型编辑的 C API,但它也通过 Python 绑定暴露。高级用户可以参考 user_api_test.cc 和 xml_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 中,使用提供的 getter 和 setter:
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。例如,要将一个盒子 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
mjs_addGeom 的第二个参数 NULL
是可选的默认类指针。当以过程方式使用默认值时,默认类会显式传递给元素构造函数。所有元素的全局默认值(在没有传递默认类时使用)可以在 user_init.c 中查看。
附加#
这个框架引入了一个强大的新功能:附加和分离模型子树。此功能已用于驱动 MJCF 中的 attach 和 replicate 元元素。附加允许用户将一个子树从一个模型移动或复制到另一个模型,同时也会复制或移动相关的引用资源以及运动学树外部的引用元素(例如,执行器和传感器)。类似地,分离一个子树将从模型中删除所有相关元素。默认行为是在附加时将子元素移动到父元素中,因此对子元素的后续更改也会更改父元素。或者,用户可以选择在附加时使用 mjs_setDeepCopy 创建一个全新的副本。在解析 XML 时,此标志会临时设置为 true。可以将体附加到框架:
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* 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* 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 属性,该属性指定角度解释的单位。编译器标志在附加期间会继承,因此子模型将使用子模型的标志进行编译,而父模型将使用父模型的标志进行编译。
另请注意,一旦子元素通过引用附加到父元素,子元素就无法单独编译。
默认类#
新 API 完全支持默认类,但使用它们需要理解默认类的实现方式。如默认设置部分所述,默认类首先作为虚拟元素的树加载,然后用于初始化引用它们的元素。编辑带有默认值的模型时,此初始化是显式的:
mjSpec* spec = mj_makeSpec();
mjsDefault* main = mjs_getSpecDefault(spec);
main->geom.type = mjGEOM_BOX;
mjsGeom* geom = mjs_addGeom(mjs_findBody(spec, "world"), main);
重要提示:在默认类用于初始化元素后更改它不会更改已初始化元素的属性。
未来可能的更改
上述描述的默认值仅在初始化时应用的L行为是旧的仅限 XML 加载管道的遗留。未来的 API 更改可能会允许在初始化后更改和应用默认值。如果您认为此功能对您很重要,请在 GitHub 上告知我们。
XML 保存#
可以分别使用 mj_saveXML 或 mj_saveXMLString 将 Spec 保存到 XML 文件或字符串。保存需要先编译 Spec。重要的是,保存的 XML 将考虑任何已定义的默认值。这对于模型具有许多重复值的情况非常有用,例如从不支持默认值的 URDF 加载时。在这种情况下,可以添加默认类,设置相关元素的类,然后保存;生成的 XML 将使用默认值并且更易于人类阅读。
原位重新编译#
可以在任何时候调用使用 mj_compile 进行的编译,以获取新的 mjModel 实例。相比之下,mj_recompile 会在原位更新现有的 mjModel 和 mjData 对,同时保留模拟状态。这使得模型编辑可以在模拟期间发生,例如添加或删除体。