Negative mesh scale produces inward convex polygon normals
Intro
Hi! I found a mesh compilation issue where negative mesh scale produces inward-facing convex polygon normals.
My setup
- Ubuntu 24.04.4 LTS, x86_64, Python API.
- Reproduced with PyPI MuJoCo 3.11.0, 3.12.0, and 3.13.0.
- Self-contained reproduction using model compilation only.
What's happening? What did you expect?
Two identical cube meshes differ only in scale="1 -1 1". After compilation, the unscaled cube has six outward-facing mesh_polynormal vectors, while the reflected cube has six inward-facing vectors.
I expected both meshes to have outward convex polygon normals. Negative mesh scaling is documented as supported.
The test below computes dot(normal, polygon_center - mesh_center) in compiled mesh coordinates. For these cubes, all six values should be positive.
Steps for reproduction
Run this standalone script in an environment containing mujoco:
import mujoco
xml = """
<mujoco>
<asset>
<mesh name="normal"
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"/>
<mesh name="mirrored" scale="1 -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"/>
</asset>
<worldbody>
<geom type="mesh" mesh="normal"/>
<geom type="mesh" mesh="mirrored" pos="4 0 0"/>
</worldbody>
</mujoco>
"""
model = mujoco.MjModel.from_xml_string(xml)
print("MuJoCo", mujoco.__version__)
for name in ("normal", "mirrored"):
mesh_id = model.mesh(name).id
start = model.mesh_vertadr[mesh_id]
vertices = model.mesh_vert[start:start + model.mesh_vertnum[mesh_id]]
center = vertices.mean(axis=0)
start = model.mesh_polyadr[mesh_id]
scores = []
for p in range(start, start + model.mesh_polynum[mesh_id]):
a = model.mesh_polyvertadr[p]
ids = model.mesh_polyvert[a:a + model.mesh_polyvertnum[p]]
outward = vertices[ids].mean(axis=0) - center
scores.append(round(float(model.mesh_polynormal[p] @ outward), 6))
print(name, scores)Actual output (the same signs on all three tested versions):
MuJoCo 3.13.0
normal [1.0, 1.0, 1.0, 1.0, 1.0, 1.0]
mirrored [-1.0, -1.0, -1.0, -1.0, -1.0, -1.0]Cause and local verification
In mjCMesh::Process, polygons_ retains its original vertex ordering through negative-determinant scaling. Later, MakePolygonNormals recomputes normals from the reflected vertices using that ordering, producing inward-facing normals. The existing winding correction for face_, facenormal_, and facetexcoord_ needs to cover polygons_ as well.
In an isolated 3.12 source build, also reversing each polygon's winding in that branch made all six scores positive for both cubes:
for (auto& polygon : polygons_) {
std::reverse(polygon.begin() + 1, polygon.end());
}Prior checks
Checked the mesh documentation and searched existing issues/discussions for negative scale and polygon winding.
Source: google-deepmind/mujoco