#3604·mujoco

Negative mesh scale produces inward convex polygon normals

Author: stevenayaCreated Sep 17, 2026Updated Sep 17, 2026

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:

python
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:

cpp
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.