Correct polygon winding order for all meshes not only when loaded from a file.

PiperOrigin-RevId: 882053060
Change-Id: I94ee81838fa4074a1a890de4f282eca45ee9e820
This commit is contained in:
Sam Haves
2026-03-11 09:24:44 -07:00
committed by Copybara-Service
parent db9ab94555
commit ecc22667fa
2 changed files with 79 additions and 13 deletions
+14 -13
View File
@@ -647,19 +647,6 @@ void mjCMesh::LoadFromDecoder(mjResource* resource, bool remove_repeated) {
facenormal_.assign(src_mesh->userfacenormal->begin(), src_mesh->userfacenormal->end());
facetexcoord_.assign(src_mesh->userfacetexcoord->begin(), src_mesh->userfacetexcoord->end());
// correct winding order for left-handed coordinate systems
bool righthand = scale[0] * scale[1] * scale[2] > 0;
if (!righthand) {
for (size_t i = 0; i < face_.size(); i += 3) {
std::swap(face_[i + 1], face_[i + 2]);
}
for (size_t i = 0; i < facenormal_.size(); i += 3) {
std::swap(facenormal_[i + 1], facenormal_[i + 2]);
}
for (size_t i = 0; i < facetexcoord_.size(); i += 3) {
std::swap(facetexcoord_[i + 1], facetexcoord_[i + 2]);
}
}
std::vector<float> vert(src_mesh->uservert->begin(), src_mesh->uservert->end());
mj_deleteSpec(mesh_spec);
@@ -1434,6 +1421,20 @@ void mjCMesh::Process() {
polygon_map_.resize(nvert());
}
// correct winding order for left-handed coordinate systems
bool righthand = scale[0] * scale[1] * scale[2] > 0;
if (!righthand) {
for (size_t i = 0; i < face_.size(); i += 3) {
std::swap(face_[i + 1], face_[i + 2]);
}
for (size_t i = 0; i < facenormal_.size(); i += 3) {
std::swap(facenormal_[i + 1], facenormal_[i + 2]);
}
for (size_t i = 0; i < facetexcoord_.size(); i += 3) {
std::swap(facetexcoord_[i + 1], facetexcoord_[i + 2]);
}
}
// user offset, rotation, scaling
ApplyTransformations();
+65
View File
@@ -1086,6 +1086,71 @@ TEST_F(MjCMeshTest, MeshScale) {
mj_deleteModel(model);
}
TEST_F(MjCMeshTest, NegativeScaleUserMeshCompiles) {
static constexpr char xml[] = R"(
<mujoco>
<asset>
<mesh name="example_mesh" scale="-1 1 1" inertia="exact"
vertex="0 0 0 1 0 0 0 1 0 0 0 1"
face="0 2 1 0 3 2 1 3 0 1 2 3" />
</asset>
<worldbody>
<geom type="mesh" mesh="example_mesh"/>
</worldbody>
</mujoco>
)";
char error[1024];
mjModel* model = LoadModelFromString(xml, error, sizeof(error));
ASSERT_THAT(model, NotNull()) << error;
mj_deleteModel(model);
}
TEST_F(MjCMeshTest, NegativeScaleUserMeshMatchesPositiveScale) {
static constexpr char pos_xml[] = R"(
<mujoco>
<asset>
<mesh name="pos_mesh"
vertex="0 0 0 1 0 0 0 1 0 0 0 1"
face="0 2 1 0 3 2 1 3 0 1 2 3" />
</asset>
<worldbody>
<geom type="mesh" mesh="pos_mesh"/>
</worldbody>
</mujoco>
)";
static constexpr char neg_xml[] = R"(
<mujoco>
<asset>
<mesh name="neg_mesh" scale="-1 1 1"
vertex="0 0 0 1 0 0 0 1 0 0 0 1"
face="0 2 1 0 3 2 1 3 0 1 2 3" />
</asset>
<worldbody>
<geom type="mesh" mesh="neg_mesh"/>
</worldbody>
</mujoco>
)";
char error[1024];
mjModel* pos_model = LoadModelFromString(pos_xml, error, sizeof(error));
ASSERT_THAT(pos_model, NotNull()) << error;
mjModel* neg_model = LoadModelFromString(neg_xml, error, sizeof(error));
ASSERT_THAT(neg_model, NotNull()) << error;
ASSERT_EQ(pos_model->nmeshface, neg_model->nmeshface);
for (int i = 0; i < pos_model->nmeshface; i++) {
EXPECT_EQ(pos_model->mesh_face[3*i + 0], neg_model->mesh_face[3*i + 0]);
EXPECT_EQ(pos_model->mesh_face[3*i + 1], neg_model->mesh_face[3*i + 2]);
EXPECT_EQ(pos_model->mesh_face[3*i + 2], neg_model->mesh_face[3*i + 1]);
}
mj_deleteModel(pos_model);
mj_deleteModel(neg_model);
}
TEST_F(MjCMeshTest, ShellInertiaTest) {
static constexpr char xml[] = R"(
<mujoco>