Skip to content

Commit e312ce8

Browse files
yuvaltassacopybara-github
authored andcommitted
Preserve frames when saving and recompiling.
Frames were half transient on save: the writer emitted <frame> with only its name and childclass, while the frame's transformation had already been accumulated into the children. A saved model reloaded with the same frame names but identity transforms and moved children. The writer now emits every frame with its pos and quat, relative to its parent frame, and writes the frame's contents in frame-relative coordinates: bodies, geoms, sites, cameras, lights, joint anchors and axes, and nested frames. Fixes that this required or exposed: - mjCFrame cached its accumulated pose behind a `compiled` flag which was never reset, so editing the pos or quat of a frame after a first compilation was silently ignored. The flag is now cleared for all frames at the start of every compilation. - Free-joint alignment moved a body and counter-transformed its children, but not its frames. It now carries the frames along, so a saved frame stays in place and its contents keep their authored poses. This mutates the compiled pose of the frames, hence the reset above. - A body's <plugin> was written again inside each of the body's frames. - childclass was written on every frame whose class is not "main". It is now written exactly when it differs from the class active around the frame. Tests: frames holding every kind of element round-trip through save and reload with their authored poses and the same compiled model, also inside a free-aligned body; a body plugin is written once; frame pose edits take effect on recompilation. PiperOrigin-RevId: 985340056 Change-Id: Ic73f2e63e56e22915de2dfd32bd2a73438815167
1 parent 6107eac commit e312ce8

10 files changed

Lines changed: 432 additions & 122 deletions

‎doc/XMLreference.rst‎

Lines changed: 8 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,8 @@ Meta elements
169169

170170
These elements are not strictly part of the low-level MJCF format definition, but rather instruct the compiler to
171171
perform some operation on the model. A general property of meta-elements is that they disappear from the model upon
172-
saving the XML. There are currently six meta-elements in MJCF:
172+
saving the XML; the exception is :ref:`frame<frame>`, which is preserved. There are currently six meta-elements in
173+
MJCF:
173174

174175
- :ref:`include<include>`, :ref:`frame<frame>`, and :ref:`replicate<replicate>` which are outside of the schema.
175176
- :ref:`composite<body-composite>`, :ref:`flexcomp<body-flexcomp>` and :ref:`attach<body-attach>` which are part of the
@@ -181,46 +182,10 @@ saving the XML. There are currently six meta-elements in MJCF:
181182
^^^^^^^^^^^^^
182183

183184
The frame meta-element is a pure coordinate transformation that can wrap any group of elements in the kinematic tree
184-
(under :ref:`worldbody<body>`). After compilation, frame elements disappear and their transformation is accumulated
185-
in their direct children. The attributes of the frame meta-element are documented :ref:`below<body-frame>`.
186-
187-
.. collapse:: Usage example of frame
188-
189-
Loading this model and saving it:
190-
191-
.. code-block:: xml
192-
193-
<mujoco>
194-
<worldbody>
195-
<frame quat="0 0 1 0">
196-
<geom name="Alice" quat="0 1 0 0" size="1"/>
197-
</frame>
198-
199-
<frame pos="0 1 0">
200-
<geom name="Bob" pos="0 1 0" size="1"/>
201-
<body name="Carl" pos="1 0 0">
202-
...
203-
</body>
204-
</frame>
205-
</worldbody>
206-
</mujoco>
207-
208-
Results in this model:
209-
210-
.. code-block:: xml
211-
212-
<mujoco>
213-
<worldbody>
214-
<geom name="Alice" quat="0 0 0 1" size="1"/>
215-
<geom name="Bob" pos="0 2 0" size="1"/>
216-
<body name="Carl" pos="1 1 0">
217-
...
218-
</body>
219-
</worldbody>
220-
</mujoco>
221-
222-
Note that in the saved model, the frame elements have disappeared but their transformation was accumulated with those
223-
of their child elements.
185+
(under :ref:`worldbody<body>`). At compile time the transformation is accumulated into the frame's direct children;
186+
frames have no counterpart in :ref:`mjModel`. Unlike the other meta-elements, frames are preserved when the model is
187+
saved: the frame is written with its pose and its contents in frame-relative coordinates, so a saved model reloads
188+
with the same frames. The attributes of the frame meta-element are documented :ref:`below<body-frame>`.
224189

225190
.. _replicate:
226191

@@ -4229,8 +4194,8 @@ the saved XML file. Note that this element is a subset of the functionality of t
42294194
:el-prefix:`body/` |-| **frame** |*|
42304195
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
42314196

4232-
Frames specify a coordinate transformation which is applied to all child elements. They disappear during compilation
4233-
and the transformation they encode is accumulated in their direct children. See :ref:`frame<frame>` for examples.
4197+
Frames specify a coordinate transformation which is applied to all child elements. During compilation the
4198+
transformation they encode is accumulated in their direct children; frames are preserved when the model is saved.
42344199

42354200
.. _frame-name:
42364201

‎doc/changelog.rst‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ General
2121
:ref:`mjpResourceProvider` interface to mount and read archive containers (such as
2222
``.mjz``/``.zip``), decoupling container handling from format decoders and enabling
2323
on-demand asset extraction without requiring a pre-allocated :ref:`VFS<Virtualfilesystem>`.
24+
- :ref:`Frames<frame>` are now preserved when saving MJCF: ``<frame>`` elements are written with their ``pos`` and
25+
``quat`` and their contents in frame-relative coordinates, so a saved model reloads with the same frames. Previously
26+
the frame transformation was accumulated into the children on save and only the frame's name was kept. Changes to the
27+
``pos`` or ``quat`` of a frame in an :ref:`mjSpec` now also take effect when the spec is recompiled.
2428

2529

2630
Engine

‎src/user/user_model.cc‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5324,6 +5324,9 @@ void mjCModel::TryCompile(mjModel*& m, mjData*& d, const mjVFS* vfs) {
53245324
timer[mjCTIMER_ASSETS] = Seconds(Clock::now() - t0).count();
53255325
}
53265326

5327+
// frames cache their accumulated pose, recompute it in every compile
5328+
for (mjCFrame* frame : frames_) { frame->compiled = false; }
5329+
53275330
// compile objects in kinematic tree
53285331
for (int i = 0; i < bodies_.size(); i++) {
53295332
bodies_[i]->Compile(); // also compiles joints, geoms, sites, cameras, lights, frames

‎src/user/user_objects.cc‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2784,6 +2784,11 @@ void mjCBody::Compile(void) {
27842784
mjuu_frameaccumChild(ipos_inverse, iquat_inverse, lights[i]->pos, qunit);
27852785
mjuu_rotVecQuat(lights[i]->dir, lights[i]->dir, iquat_inverse);
27862786
}
2787+
2788+
// frames: keep them in place relative to their contents, for the writer
2789+
for (int i = 0; i < frames.size(); i++) {
2790+
mjuu_frameaccumChild(ipos_inverse, iquat_inverse, frames[i]->pos, frames[i]->quat);
2791+
}
27872792
}
27882793
}
27892794

‎src/user/user_objects.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -655,6 +655,7 @@ class mjCFrame : public mjCFrame_, private mjsFrame {
655655
friend class mjCCamera;
656656
friend class mjCLight;
657657
friend class mjCModel;
658+
friend class mjXWriter;
658659

659660
public:
660661
mjCFrame(mjCModel* = 0, mjCFrame* = 0);

‎src/xml/xml_native_writer.cc‎

Lines changed: 83 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -382,9 +382,15 @@ void mjXWriter::OneJoint(XMLElement* elem,
382382
&def->Joint().spec,
383383
kJointAttrs,
384384
kJointAttrsN);
385-
if (joint->type != mjJNT_FREE) { WriteAttr(elem, "pos", 3, joint->pos, def->Joint().spec.pos); }
385+
// pos and axis relative to the joint's frame
386+
double pos[3], axis[3], iquat[4] = {1, 0, 0, 0};
387+
mjuu_copyvec(pos, joint->pos, 3);
388+
mjuu_copyvec(axis, joint->axis, 3);
389+
FrameLocal(joint->frame, pos, iquat);
390+
mjuu_rotVecQuat(axis, axis, iquat);
391+
if (joint->type != mjJNT_FREE) { WriteAttr(elem, "pos", 3, pos, def->Joint().spec.pos); }
386392
if (joint->type != mjJNT_FREE && joint->type != mjJNT_BALL) {
387-
WriteAttr(elem, "axis", 3, joint->axis, def->Joint().spec.axis);
393+
WriteAttr(elem, "axis", 3, axis, def->Joint().spec.axis);
388394
}
389395
if (joint->type != mjJNT_FREE) {
390396
WriteAttrKey(elem, "limited", FalseTrueAuto_map, 3, joint->limited, def->Joint().limited);
@@ -422,30 +428,16 @@ void mjXWriter::OneGeom(XMLElement* elem, const mjCGeom* geom, mjCDef* def, stri
422428
}
423429
if (mjuu_defined(geom->mass)) { mass = geom->GetVolume() * def->Geom().density; }
424430

425-
// mesh geom
426-
if (geom->type == mjGEOM_MESH || geom->type == mjGEOM_SDF) {
427-
mjCMesh* mesh = geom->mesh;
428-
429-
// write pos/quat if there is a difference
430-
if (!SameVector(geom->pos, mesh->GetPosPtr(), 3) ||
431-
!SameVector(geom->quat, mesh->GetQuatPtr(), 4)) {
432-
// recover geom pos/quat before mesh frame transformation
433-
double p[3], q[4];
434-
mjuu_copyvec(p, geom->pos, 3);
435-
mjuu_copyvec(q, geom->quat, 4);
436-
mjuu_frameaccuminv(p, q, mesh->GetPosPtr(), mesh->GetQuatPtr());
437-
438-
// write
439-
WriteAttr(elem, "pos", 3, p, unitq + 1);
440-
WriteAttr(elem, "quat", 4, q, unitq);
441-
}
442-
}
443-
444-
// non-mesh geom
445-
else {
446-
WriteAttr(elem, "pos", 3, geom->pos, unitq + 1);
447-
WriteAttr(elem, "quat", 4, geom->quat, unitq);
431+
// pose: undo the mesh transformation, then the frame
432+
double pos[3], quat[4];
433+
mjuu_copyvec(pos, geom->pos, 3);
434+
mjuu_copyvec(quat, geom->quat, 4);
435+
if ((geom->type == mjGEOM_MESH || geom->type == mjGEOM_SDF) && geom->mesh) {
436+
mjuu_frameaccuminv(pos, quat, geom->mesh->GetPosPtr(), geom->mesh->GetQuatPtr());
448437
}
438+
FrameLocal(geom->frame, pos, quat);
439+
WriteAttr(elem, "pos", 3, pos, unitq + 1);
440+
WriteAttr(elem, "quat", 4, quat, unitq);
449441
} else {
450442
WriteAttr(elem, "size", 3, geom->size, def->Geom().size);
451443
}
@@ -505,30 +497,16 @@ void mjXWriter::OneSite(XMLElement* elem, const mjCSite* site, mjCDef* def, stri
505497
WriteAttr(elem, "size", mjGEOMINFO[site->type], site->size, def->Site().size);
506498
}
507499

508-
// mesh site
509-
if (site->type == mjGEOM_MESH) {
510-
mjCMesh* mesh = site->mesh;
511-
512-
// write pos/quat if there is a difference
513-
if (!SameVector(site->pos, mesh->GetPosPtr(), 3) ||
514-
!SameVector(site->quat, mesh->GetQuatPtr(), 4)) {
515-
// recover site pos/quat before mesh frame transformation
516-
double p[3], q[4];
517-
mjuu_copyvec(p, site->pos, 3);
518-
mjuu_copyvec(q, site->quat, 4);
519-
mjuu_frameaccuminv(p, q, mesh->GetPosPtr(), mesh->GetQuatPtr());
520-
521-
// write
522-
WriteAttr(elem, "pos", 3, p, unitq + 1);
523-
WriteAttr(elem, "quat", 4, q, unitq);
524-
}
525-
}
526-
527-
// non-mesh site
528-
else {
529-
WriteAttr(elem, "pos", 3, site->pos, unitq + 1);
530-
WriteAttr(elem, "quat", 4, site->quat, unitq);
500+
// pose: undo the mesh transformation, then the frame
501+
double pos[3], quat[4];
502+
mjuu_copyvec(pos, site->pos, 3);
503+
mjuu_copyvec(quat, site->quat, 4);
504+
if (site->type == mjGEOM_MESH && site->mesh) {
505+
mjuu_frameaccuminv(pos, quat, site->mesh->GetPosPtr(), site->mesh->GetQuatPtr());
531506
}
507+
FrameLocal(site->frame, pos, quat);
508+
WriteAttr(elem, "pos", 3, pos, unitq + 1);
509+
WriteAttr(elem, "quat", 4, quat, unitq);
532510
} else {
533511
WriteAttr(elem, "size", 3, site->size, def->Site().size);
534512
}
@@ -562,22 +540,22 @@ void mjXWriter::OneCamera(XMLElement* elem,
562540
string_view classname) {
563541
double unitq[4] = {1, 0, 0, 0};
564542

543+
// pose relative to the camera's frame
544+
mjsCamera local = *static_cast<const mjsCamera*>(camera);
545+
FrameLocal(camera->frame, local.pos, local.quat);
546+
565547
// regular
566548
if (!writingdefaults) {
567549
WriteAttrTxt(elem, "name", camera->name);
568550
if (classname != camera->classname && camera->classname != "main") {
569551
WriteAttrTxt(elem, "class", camera->classname);
570552
}
571553
WriteAttrTxt(elem, "target", camera->get_targetbody());
572-
WriteAttr(elem, "quat", 4, camera->quat, unitq);
554+
WriteAttr(elem, "quat", 4, local.quat, unitq);
573555
}
574556

575557
// defaults and regular
576-
WriteAttrTable(elem,
577-
static_cast<const mjsCamera*>(camera),
578-
&def->Camera().spec,
579-
kCameraAttrs,
580-
kCameraAttrsN);
558+
WriteAttrTable(elem, &local, &def->Camera().spec, kCameraAttrs, kCameraAttrsN);
581559

582560
// camera intrinsics if specified
583561
if (camera->sensor_size[0] > 0 && camera->sensor_size[1] > 0) {
@@ -612,12 +590,14 @@ void mjXWriter::OneLight(XMLElement* elem,
612590
WriteAttrTxt(elem, "target", light->get_targetbody());
613591
}
614592

593+
// pos and dir relative to the light's frame
594+
mjsLight local = *static_cast<const mjsLight*>(light);
595+
double iquat[4] = {1, 0, 0, 0};
596+
FrameLocal(light->frame, local.pos, iquat);
597+
mjuu_rotVecQuat(local.dir, local.dir, iquat);
598+
615599
// defaults and regular
616-
WriteAttrTable(elem,
617-
static_cast<const mjsLight*>(light),
618-
&def->Light().spec,
619-
kLightAttrs,
620-
kLightAttrsN);
600+
WriteAttrTable(elem, &local, &def->Light().spec, kLightAttrs, kLightAttrsN);
621601
WriteAttrKey(elem, "type", lighttype_map, lighttype_sz, light->type, def->Light().type);
622602
WriteAttrTxt(elem, "texture", light->get_texture());
623603
}
@@ -1544,17 +1524,42 @@ void mjXWriter::Asset(XMLElement* root) {
15441524
}
15451525

15461526

1547-
XMLElement* mjXWriter::OneFrame(XMLElement* elem, mjCFrame* frame) {
1527+
// strip a frame from a compiled pose: pos/quat become relative to the frame
1528+
void mjXWriter::FrameLocal(const mjCFrame* frame, double pos[3], double quat[4]) {
1529+
if (!frame) { return; }
1530+
double ipos[3], iquat[4];
1531+
mjuu_frameinvert(ipos, iquat, frame->pos, frame->quat);
1532+
mjuu_frameaccumChild(ipos, iquat, pos, quat);
1533+
}
1534+
1535+
1536+
XMLElement* mjXWriter::OneFrame(XMLElement* elem, mjCFrame* frame, string_view childclass) {
15481537
if (!frame) { return elem; }
1538+
double unitq[4] = {1, 0, 0, 0};
15491539

1550-
// TODO: empty classname should not occur (but does)
1551-
if (frame->name.empty() && (frame->classname.empty() || frame->classname == "main")) {
1540+
// pose relative to the parent frame
1541+
double pos[3], quat[4];
1542+
mjuu_copyvec(pos, frame->pos, 3);
1543+
mjuu_copyvec(quat, frame->quat, 4);
1544+
FrameLocal(frame->frame, pos, quat);
1545+
1546+
// omit unnamed identity frame with no childclass
1547+
bool has_class = !frame->classname.empty() && frame->classname != childclass;
1548+
if (frame->name.empty() && !has_class &&
1549+
SameVector(pos, unitq + 1, 3) && SameVector(quat, unitq, 4)) {
15521550
return elem;
15531551
}
15541552

15551553
XMLElement* frame_elem = InsertEnd(elem, "frame");
15561554
WriteAttrTxt(frame_elem, "name", frame->name);
1557-
if (frame->classname != "main") { WriteAttrTxt(frame_elem, "childclass", frame->classname); }
1555+
1556+
// childclass, unless inherited from the enclosing frame or body
1557+
if (has_class) {
1558+
WriteAttrTxt(frame_elem, "childclass", frame->classname);
1559+
}
1560+
1561+
WriteAttr(frame_elem, "pos", 3, pos, unitq + 1);
1562+
WriteAttr(frame_elem, "quat", 4, quat, unitq);
15581563
return frame_elem;
15591564
}
15601565

@@ -1574,9 +1579,13 @@ void mjXWriter::Body(XMLElement* elem, mjCBody* body, mjCFrame* frame, string_vi
15741579
WriteAttrTxt(elem, "childclass", body->classname);
15751580
}
15761581

1577-
// write pos if it's not {0, 0, 0}
1578-
if (body->pos[0] || body->pos[1] || body->pos[2]) { WriteAttr(elem, "pos", 3, body->pos); }
1579-
WriteAttr(elem, "quat", 4, body->quat, unitq);
1582+
// pose relative to the body's frame
1583+
double pos[3], quat[4];
1584+
mjuu_copyvec(pos, body->pos, 3);
1585+
mjuu_copyvec(quat, body->quat, 4);
1586+
FrameLocal(body->frame, pos, quat);
1587+
WriteAttr(elem, "pos", 3, pos, unitq + 1);
1588+
WriteAttr(elem, "quat", 4, quat, unitq);
15801589
if (body->mocap) { WriteAttrKey(elem, "mocap", bool_map, 2, 1); }
15811590

15821591
// gravity compensation
@@ -1666,8 +1675,8 @@ void mjXWriter::Body(XMLElement* elem, mjCBody* body, mjCFrame* frame, string_vi
16661675
classname.empty() ? childclass : classname);
16671676
}
16681677

1669-
// write plugin
1670-
if (body->plugin.active) { OnePlugin(InsertEnd(elem, "plugin"), &body->plugin); }
1678+
// write plugin, once: not again inside the body's frames
1679+
if (!frame && body->plugin.active) { OnePlugin(InsertEnd(elem, "plugin"), &body->plugin); }
16711680

16721681
// write children recursively
16731682
int i = 0, j = 0;
@@ -1694,9 +1703,12 @@ void mjXWriter::Body(XMLElement* elem, mjCBody* body, mjCFrame* frame, string_vi
16941703

16951704
// write frame if its frame matches the current frame
16961705
if (fframe->frame == frame) {
1697-
string classname =
1698-
fframe && !fframe->classname.empty() ? fframe->classname : body->classname;
1699-
Body(OneFrame(elem, fframe), body, fframe, childclass);
1706+
// class active around the new frame: the current frame's, else the body's
1707+
string classname = frame && !frame->classname.empty() ? frame->classname : body->classname;
1708+
Body(OneFrame(elem, fframe, classname.empty() ? childclass : classname),
1709+
body,
1710+
fframe,
1711+
childclass);
17001712
}
17011713
}
17021714

‎src/xml/xml_native_writer.h‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,12 @@ class mjXWriter : public mjXBase {
105105
void OneTendon(tinyxml2::XMLElement* elem, const mjCTendon* ptendon, mjCDef* def);
106106
void OneActuator(tinyxml2::XMLElement* elem, const mjCActuator* pactuator, mjCDef* def);
107107
void OnePlugin(tinyxml2::XMLElement* elem, const mjsPlugin* plugin);
108-
tinyxml2::XMLElement* OneFrame(tinyxml2::XMLElement* elem, mjCFrame* frame);
108+
tinyxml2::XMLElement* OneFrame(tinyxml2::XMLElement* elem,
109+
mjCFrame* frame,
110+
std::string_view childclass);
111+
112+
// strip a frame from a compiled pose: pos/quat become relative to the frame
113+
static void FrameLocal(const mjCFrame* frame, double pos[3], double quat[4]);
109114

110115
bool writingdefaults; // true during defaults write
111116
};

0 commit comments

Comments
 (0)
⌂ 换网址