Fix bug in mesh normal scaling for nonuniform mesh scales.

PiperOrigin-RevId: 946937791
Change-Id: I1b8a0ad4d573842ef9f76572dea25668dd319b52
This commit is contained in:
Yuval Tassa
2026-07-13 03:56:14 -07:00
committed by Copybara-Service
parent fbf031a7d5
commit f5f9d9efb7
3 changed files with 66 additions and 3 deletions
+4
View File
@@ -36,6 +36,10 @@ General
- Changed the default value of :ref:`sleep_tolerance<option-sleep_tolerance>` from 1e-4 to 1e-3 (1mm/sec in SI
units).
Bug fixes
^^^^^^^^^
- Fixed a bug in the mesh compiler where normals were scaled as vectors rather than covectors.
Version 3.10.0 (June 22, 2026)
------------------------------
+4 -3
View File
@@ -1292,10 +1292,11 @@ void mjCMesh::ApplyTransformations(double* dvert) {
dvert[3*i + 2] *= scale[2];
}
// normals are covectors: transform by the inverse scale
for (int i = 0; i < nnormal(); i++) {
normal_[3*i + 0] *= scale[0];
normal_[3*i + 1] *= scale[1];
normal_[3*i + 2] *= scale[2];
normal_[3*i + 0] /= scale[0];
normal_[3*i + 1] /= scale[1];
normal_[3*i + 2] /= scale[2];
}
}
+58
View File
@@ -1120,6 +1120,64 @@ TEST_F(MjCMeshTest, MeshScale) {
EXPECT_THAT(AsVector(model->mesh_scale + 3, 3), ElementsAre(0.9, 1, -1));
}
TEST_F(MjCMeshTest, UserNormalsAnisotropicScale) {
// normals are covectors: under mesh scale s they must transform by 1/s.
// cube with per-vertex normals along the vertex directions, anisotropic
// scale; expected compiled normals follow (+-1/sx, +-1/sy, +-1/sz)
static constexpr char xml[] = R"(
<mujoco>
<asset>
<mesh name="cube" scale=".2 .1 .1"
vertex="-1 -1 -1 1 -1 -1 1 1 -1 -1 1 -1 -1 -1 1 1 -1 1 1 1 1 -1 1 1"
normal="-1 -1 -1 1 -1 -1 1 1 -1 -1 1 -1 -1 -1 1 1 -1 1 1 1 1 -1 1 1"
face="0 2 1 0 3 2 4 5 6 4 6 7 0 1 5 0 5 4 1 2 6 1 6 5
2 3 7 2 7 6 3 0 4 3 4 7"/>
</asset>
<worldbody>
<body>
<freejoint/>
<geom type="mesh" mesh="cube"/>
</body>
</worldbody>
</mujoco>
)";
char error[1024];
MjModelPtr model = LoadModelFromString(xml, error, sizeof(error));
ASSERT_THAT(model, NotNull()) << error;
const mjModel* m = model.get();
int mid = 0;
const mjtNum* mq = m->mesh_quat + 4*mid;
const mjtNum* mp = m->mesh_pos + 3*mid;
double scale[3] = {.2, .1, .1};
int va = m->mesh_vertadr[mid];
int na = m->mesh_normaladr[mid];
int fa = m->mesh_faceadr[mid];
for (int i = 0; i < 3*m->mesh_facenum[mid]; i++) {
int vid = m->mesh_face[3*fa + i];
int nid = m->mesh_facenormal[3*fa + i];
// rotate vertex and normal back to the authored frame
mjtNum vm[3] = {m->mesh_vert[3*(va+vid)+0],
m->mesh_vert[3*(va+vid)+1],
m->mesh_vert[3*(va+vid)+2]};
mjtNum nm[3] = {m->mesh_normal[3*(na+nid)+0],
m->mesh_normal[3*(na+nid)+1],
m->mesh_normal[3*(na+nid)+2]};
mjtNum v[3], n[3];
mju_rotVecQuat(v, vm, mq);
mju_addTo3(v, mp);
mju_rotVecQuat(n, nm, mq);
// expected: sign pattern of the vertex, divided by scale, normalized
mjtNum expected[3];
for (int k = 0; k < 3; k++) {
expected[k] = (v[k] > 0 ? 1.0 : -1.0) / scale[k];
}
mju_normalize3(expected);
EXPECT_GT(mju_dot3(n, expected), 0.999);
}
}
TEST_F(MjCMeshTest, NegativeScaleUserMeshCompiles) {
static constexpr char xml[] = R"(
<mujoco>