Make flex vert0 rotation-invariant for interpolated flexes.

The `vert0` array, used for parametric vertex coordinates in flexes, is now computed in the flex's local, unrotated frame when using interpolation. This ensures that the parametric coordinates are independent of the flex's initial orientation. A new test confirms that `flex_vert0` is identical for an unrotated and a rotated flex grid.

PiperOrigin-RevId: 908196464
Change-Id: I47d6bcc2bc5df581479d485480e0e947ec6d3ffd
This commit is contained in:
Alessio Quaglino
2026-04-30 08:40:20 -07:00
committed by Copybara-Service
parent 22ca5fe0df
commit 7d54123303
4 changed files with 126 additions and 17 deletions
+67 -12
View File
@@ -4363,7 +4363,8 @@ void mjCFlex::Compile(const mjVFS* vfs) {
}
// compute unrotated node positions for stiffness computation
std::vector<double> nodexpos_local = ComputeUnrotatedNodePositions(nodexpos);
double R0[9] = {1, 0, 0, 0, 1, 0, 0, 0, 1}; // identity by default
std::vector<double> nodexpos_local = ComputeUnrotatedNodePositions(nodexpos, R0);
// reorder tetrahedra so right-handed face orientation is outside
// faces are (0,1,2); (0,2,3); (0,3,1); (1,3,2)
@@ -4623,16 +4624,54 @@ void mjCFlex::Compile(const mjVFS* vfs) {
// compute bounding box coordinates
vert0_.assign(3*nvert, 0);
const mjtNum* bvh = tree.Bvh().data();
size[0] = bvh[3] - radius;
size[1] = bvh[4] - radius;
size[2] = bvh[5] - radius;
for (int j=0; j < nvert; j++) {
for (int k=0; k < 3; k++) {
if (size[k] > mjMINVAL) {
vert0_[3*j+k] = (vertxpos[3*j+k] - bvh[k]) / (2*size[k]) + 0.5;
} else {
vert0_[3*j+k] = 0.5;
if (interpolated && nnode > 0) {
// for interpolated flex, compute vert0_ in the unrotated local frame
// to make parametric coordinates rotation-invariant
std::vector<double> vertxpos_local(3*nvert);
for (int j = 0; j < nvert; j++) {
mjuu_mulvecmat(vertxpos_local.data()+3*j, vertxpos.data()+3*j, R0);
}
// compute local-frame bounding box from unrotated node positions
double lo[3] = {1e30, 1e30, 1e30};
double hi[3] = {-1e30, -1e30, -1e30};
for (int i = 0; i < nnode; i++) {
for (int k = 0; k < 3; k++) {
lo[k] = std::min(lo[k], nodexpos_local[3*i+k]);
hi[k] = std::max(hi[k], nodexpos_local[3*i+k]);
}
}
// set size from local bounding box
for (int k = 0; k < 3; k++) {
size[k] = (hi[k] - lo[k]) / 2;
}
// normalize vertex positions within local bounding box
for (int j = 0; j < nvert; j++) {
for (int k = 0; k < 3; k++) {
double extent = hi[k] - lo[k];
if (extent > mjMINVAL) {
vert0_[3*j+k] = (vertxpos_local[3*j+k] - lo[k]) / extent;
} else {
vert0_[3*j+k] = 0.5;
}
}
}
} else {
// non-interpolated: use BVH bounding box (original behavior)
const mjtNum* bvh = tree.Bvh().data();
size[0] = bvh[3] - radius;
size[1] = bvh[4] - radius;
size[2] = bvh[5] - radius;
for (int j=0; j < nvert; j++) {
for (int k=0; k < 3; k++) {
if (size[k] > mjMINVAL) {
vert0_[3*j+k] = (vertxpos[3*j+k] - bvh[k]) / (2*size[k]) + 0.5;
} else {
vert0_[3*j+k] = 0.5;
}
}
}
}
@@ -4655,7 +4694,7 @@ void mjCFlex::Compile(const mjVFS* vfs) {
// be computed from axis-aligned positions to preserve the diagonal Jacobian
// assumption in ComputeLinearStiffness.
std::vector<double> mjCFlex::ComputeUnrotatedNodePositions(
const std::vector<double>& nodexpos) const {
const std::vector<double>& nodexpos, double* R0_out) const {
std::vector<double> nodexpos_local(3*nnode);
if (interpolated && nnode > 0) {
int ny_global = spec.cellcount[1] * spec.order + 1;
@@ -4707,6 +4746,22 @@ std::vector<double> mjCFlex::ComputeUnrotatedNodePositions(
double lk = mjuu_normvec(R0+6, 3);
(void)li; (void)lj; (void)lk;
// assert R0 is orthonormal (rows are the normalized edge vectors)
for (int a = 0; a < 3; a++) {
for (int b = a; b < 3; b++) {
double dot = mjuu_dot3(R0 + 3*a, R0 + 3*b);
double expected = (a == b) ? 1.0 : 0.0;
if (std::abs(dot - expected) > 1e-8) {
throw mjCError(this, "flex grid rotation R0 is not orthonormal");
}
}
}
// output R0 if requested
if (R0_out) {
mjuu_copyvec(R0_out, R0, 9);
}
// apply inverse rotation to each nodexpos to get local-frame positions
for (int i = 0; i < nnode; i++) {
const double* p = nodexpos.data() + 3*i;
+3 -1
View File
@@ -1055,7 +1055,9 @@ class mjCFlex: public mjCFlex_, private mjsFlex {
std::vector<double> node0_; // node Cartesian positions
// compute unrotated node positions for stiffness computation
std::vector<double> ComputeUnrotatedNodePositions(const std::vector<double>& nodexpos) const;
// optionally outputs the grid rotation matrix R0 (stored as rows)
std::vector<double> ComputeUnrotatedNodePositions(
const std::vector<double>& nodexpos, double* R0_out = nullptr) const;
// stiffness caching
std::string ComputeStiffnessCacheKey() const;
+52 -3
View File
@@ -392,9 +392,9 @@ TEST_F(UserFlexTest, TrilinearInterpolation) {
EXPECT_EQ(m1->nflexvert, m2->nflexvert);
for (int i = 0; i < 3*m1->nflexvert; ++i) {
EXPECT_EQ(m1->flex_vert[i], d2->flexvert_xpos[i]);
EXPECT_EQ(m1->flex_vert0[i], m2->flex_vert0[i]);
EXPECT_EQ(d1->flexvert_xpos[i], d2->flexvert_xpos[i]);
EXPECT_NEAR(m1->flex_vert[i], d2->flexvert_xpos[i], 1e-7);
EXPECT_NEAR(m1->flex_vert0[i], m2->flex_vert0[i], 1e-7);
EXPECT_NEAR(d1->flexvert_xpos[i], d2->flexvert_xpos[i], 1e-7);
}
EXPECT_EQ(m1->nM, m2->nM);
@@ -1350,6 +1350,55 @@ TEST_F(UserFlexTest, Dof2d) {
mj_deleteData(d_full);
}
TEST_F(UserFlexTest, Vert0RotationInvariant) {
// unrotated trilinear grid
static constexpr char xml_unrotated[] = R"(
<mujoco>
<worldbody>
<body name="parent">
<flexcomp name="test" type="grid" count="2 2 2" spacing="1 1 1"
dim="3" dof="trilinear">
<contact selfcollide="none" internal="false"/>
</flexcomp>
</body>
</worldbody>
</mujoco>
)";
// same grid rotated 45 degrees around Z via parent body quaternion
static constexpr char xml_rotated[] = R"(
<mujoco>
<worldbody>
<body name="parent" quat="0.9238795 0 0 0.3826834">
<flexcomp name="test" type="grid" count="2 2 2" spacing="1 1 1"
dim="3" dof="trilinear">
<contact selfcollide="none" internal="false"/>
</flexcomp>
</body>
</worldbody>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* m1 = LoadModelFromString(xml_unrotated, error.data(), error.size());
ASSERT_THAT(m1, NotNull()) << error.data();
mjModel* m2 = LoadModelFromString(xml_rotated, error.data(), error.size());
ASSERT_THAT(m2, NotNull()) << error.data();
// same number of vertices
ASSERT_EQ(m1->nflexvert, m2->nflexvert);
// vert0 must be identical regardless of rotation
for (int i = 0; i < 3 * m1->nflexvert; ++i) {
EXPECT_NEAR(m1->flex_vert0[i], m2->flex_vert0[i], 1e-10)
<< "vert0 mismatch at index " << i;
}
mj_deleteModel(m1);
mj_deleteModel(m2);
}
} // namespace
} // namespace mujoco
+4 -1
View File
@@ -1404,7 +1404,10 @@ std::vector<std::string> GetWriteReadTestModels() {
absl::StrContains(xml, "hfield_xml") ||
absl::StrContains(xml, "fromto_convex") ||
absl::StrContains(xml, "cube_skin") ||
absl::StrContains(xml, "cube_3x3x3")) {
absl::StrContains(xml, "cube_3x3x3") ||
// exclude files that fail since we do not save pinned flex nodes
absl::StrContains(xml, "gripper_trilinear") ||
absl::StrContains(xml, "strain")) {
continue;
}
models.push_back(xml);