CNA-GAP-038: Mirrored glTF placements keep the shared winding, so under the default cull mode they render back-facing
Evidence basis: source-verified at the pinned commit; tests exist (not executed for this page). Claims on this page were checked by reading the CNA source at commit 009d40f5; unless a sentence says otherwise, nothing here was built or executed. Nothing on this page was executed unless the Evidence section says so.
A placement whose world transform has a negative determinant is detected and reported (mirrored-winding-unapplied) but CNA neither reverses its winding nor changes cull state, and exposes no per-mesh flag for the application to do it.
- Identifier
CNA-GAP-038- Category
- Functional gap
- Subsystem
- Models & glTF
- Status
- Open
- Verified against
- CNA
009d40f5(009d40f5dd085c4e674d3479675fac84b12b3e0a) - Evidence basis
- Source-verified: read at TARGET, not executed
- Tests touching this area
- Yes: see Current tests
- Affected contract
- ContentManager::Load<Model> on glTF files with negatively scaled or mirroring node transforms; Model::Draw
Expected behaviour
glTF §3.7.4 requires the front-face winding to be reversed when a node's global transform has a negative determinant, so mirrored geometry stays front-facing.
Actual behaviour at TARGET
ContentManager.cpp logs, per mirrored placement, that CNA "does not reverse the triangle winding for it, so this placement's front faces are back-facing under the default cull mode", and GltfImportCore.cpp (AppendGltfInstanceReportEXT) records mirrored-winding-unapplied: "the application must reverse culling for this draw". The reason is architectural: the index and vertex buffers are shared with unmirrored placements of the same mesh, and cull state is per-draw device state. No public Model or ModelMesh property carries a per-mesh mirroring flag. The only public route is the Model's import report (Model::getGltfImportReportEXTProperty), which lists each mirrored placement by node name in a diagnostic whose Code is mirrored-winding-unapplied, so an application has to map that node name to the ModelMesh it draws, or recompute the determinant from the bone transforms.
Source locations
modules/content/src/Xna/ContentManager.cpp— ReadGltfModel: GLTF-116 warning per mirrored placementmodules/content/src/GltfImport/GltfImportCore.cpp— AppendGltfInstanceReportEXT: mirrored-winding-unappliedmodules/content/tests/CNA/Internal/GltfImport/GltfMirroringTests.cpp— detection and untouched shared buffers
Evidence
Checked by reading at 009d40f5; not executed. GltfMirroringTests.cpp pins the detection and that the buffers stay untouched; nothing renders a mirrored placement with a reversed cull mode.
Focused reproduction
No focused reproduction is known. Nothing has been invented here; the evidence above is what exists.
Current tests
GltfMirroringTests.cpp covers detection through hierarchies, 180° rotations, shared buffers and tangent handedness.
Regression test
If a per-draw reversal is added, an L7 fixture (xf-negative-scale exists in the corpus) expecting the mirrored placement to be visible under default culling.
Blast radius
glTF assets with negative scale or mirroring hierarchies drawn with Model::Draw under CullCounterClockwise.
Workaround
Find the mirrored placements in the Model's import report (getGltfImportReportEXTProperty(), diagnostic mirrored-winding-unapplied, subject = node name), then draw such meshes with RasterizerState::CullNone, or detect a negative determinant of the absolute bone transform and switch to the opposite cull mode for that draw.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- User guide
- Model loading: runtime glTF
- Known issues
- Functional gap index