Skip to content

quads follow-up: bones/transforms/lighting after n-gon import - #335

Merged
fernandotonon merged 4 commits into
feat/quadsfrom
fix/quads-followup
Apr 29, 2026
Merged

quads follow-up: bones/transforms/lighting after n-gon import#335
fernandotonon merged 4 commits into
feat/quadsfrom
fix/quads-followup

Conversation

@fernandotonon

Copy link
Copy Markdown
Owner

Summary

Three follow-up fixes for the n-gon import path on feat/quads. Stacked off feat/quads; targets it (not master). Closes the three known issues we documented when chunks 4 / 4b / 5a landed.

1. Bone-handle drift on skinned meshes (Codex P1 on #332)

loadFromAssimpFile stored aiBone mesh-local indices in EditableBoneAssignment::boneIndex. The GUI import path (MeshProcessor) instead resolves aiBone->mName against the loaded Ogre::Skeleton and stores Ogre::Bone::getHandle(). After a topology op re-emitted VertexBoneAssignments via resizeEntityBuffers, those mesh-local indices were re-interpreted as Ogre handles — vertices rebound to whichever bones happened to occupy those handle slots.

Fix: new optional Ogre::Skeleton* parameter on loadFromAssimpFile. When non-null, aiBone->mName is resolved against it (matching MeshProcessor) and the resulting handle is stored. Bones that don't resolve are skipped so we never emit wild handles. EditModeController passes the live entity's skeleton when entering edit mode.

2. Z-up overlay rotation on FBX/glTF Z-up assets (Codex P1 on #332)

MeshProcessor bakes a +90°-around-X rotation into rendered buffers for assets declared Z-up (FBX UpAxis = 2). loadFromAssimpFile read raw aiMesh vertices unchanged, so the editable representation lived in pre-bake space while rendered buffers were post-bake — vertex/edge/face overlays appeared rotated 90° relative to the on-screen geometry, and a commit would write the rotated positions back, silently rotating the entity.

Fix: new isZup parameter on loadFromAssimpFile. When true, applies the same +90°-around-X bake to position, normal, and tangent before storing. MeshImporterExporter caches the source up-axis under qtme.source_up_axis alongside the existing source-path / convert-LH caches; EditModeController reads it back when re-entering edit mode.

3. Bump map + per-pixel lighting drop after topology ops (deferred from #334)

After any Edit-Mode topology op on a bump-mapped n-gon-imported asset, the model went dark and lost its normal map. Two compounding issues:
a. EditableMesh::buildSubMeshBuffers checks only vertices[0].hasTangent to decide whether to add VES_TANGENT to the rebuilt declaration — new vertices default-construct with zero-valued tangents, so RTSS's SRS_NORMALMAP TBN math collapsed.
b. Each topology op had its own inline _deinitialise/_initialise + invalidateMaterial block; the undo/redo path had a third copy that skipped the RTSS hook entirely.

Fix: centralise post-op refresh into a new static EditModeController::rewriteEntityAfterTopologyChange(Entity*) that:

  • Detects bump-map intent by scanning subentity materials for normal_map/NormalMap TUS;
  • Forces Mesh::buildTangentVectors BEFORE _deinitialise/_initialise (so the SubEntity vertex-decl cache reads the corrected layout on _initialise);
  • Saves/restores per-subentity material overrides;
  • Re-runs MeshImporterExporter::applyNormalMapsToEntity so RTSS re-attaches SRS_NORMALMAP against the fresh tangents.

All five inline copies in EditModeController.cpp now call this helper. EditMeshTopologyCommand::applyMeshState (undo/redo) calls it too — so a Subdivide-then-Ctrl-Z preserves lighting state.

Test plan

  • 234 standalone tests pass (3 new regressions: Z-up bake math, bone-skeleton no-op shape, helper-static-API).
  • Smoke: bump-mapped Mixamo asset → subdivide preserves bump map + lighting
  • Smoke: extrude preserves bump map + lighting
  • Smoke: undo / redo preserves bump map + lighting
  • Smoke: bones / animations on skinned mesh remain correctly bound after topology op (issue Mac os build #1 fix)
  • Reviewer to confirm Z-up FBX behaviour (no Z-up FBX in test fixtures — math is verified by unit test)

Known follow-ups (separate PRs)

  • Knife tool regression (broken on feat/quads)
  • Fill produces triangles where it should produce a single quad

Addresses both Codex P1 findings on PR #332 (chunk 4) before they reach
master.

Issue 1 — bone-handle drift on skinned meshes
  loadFromAssimpFile stored aiBone mesh-local indices in
  EditableBoneAssignment::boneIndex. The GUI import path
  (MeshProcessor) instead resolves aiBone->mName against the loaded
  Ogre::Skeleton and stores Ogre::Bone::getHandle(). When a topology op
  re-emitted VertexBoneAssignments via resizeEntityBuffers, those
  mesh-local indices were re-interpreted as Ogre handles, so vertices
  rebound to whichever bones happened to occupy those handle slots.

  Fix: add an optional Ogre::Skeleton* parameter to loadFromAssimpFile.
  When non-null, aiBone->mName is resolved against it (matching
  MeshProcessor) and the resulting handle is stored. Bones that don't
  resolve are skipped so we never emit wild handles. EditModeController
  passes the live entity's skeleton when entering edit mode.

Issue 2 — Z-up overlay rotation on FBX/glTF assets
  MeshProcessor bakes a +90°-around-X rotation into rendered buffers
  for assets declared Z-up (FBX UpAxis = 2), so the Ogre scene-graph
  stays Y-up without a node rotation. loadFromAssimpFile read raw
  aiMesh vertices unchanged, so on Z-up assets the editable
  representation lived in pre-bake space while rendered buffers were
  post-bake — vertex/edge/face overlays appeared rotated 90° relative
  to the on-screen geometry, and a commit would write the rotated
  positions back, silently rotating the entity.

  Fix: add an `isZup` parameter to loadFromAssimpFile. When true, apply
  the same +90°-around-X bake to position, normal, and tangent before
  storing them. MeshImporterExporter caches the source up-axis under
  "qtme.source_up_axis" alongside the existing source-path / convert-LH
  caches, and EditModeController reads it back when re-entering edit
  mode. EditableMesh::commitToEntity / resizeEntityBuffers now also
  erase this cache key when the live buffers diverge from the source.

Tests: two new standalone regression tests cover the Z-up bake math
and the unskinned-mesh shape of the new bone-skeleton parameter. The
skinned-mesh skeleton-lookup case is exercised by
EditModeController integration tests at run time.
Addresses the deferred lighting/RTSS regression flagged on PR #334.

Root cause
  After any Edit-Mode topology op (subdivide, extrude, bevel, knife,
  merge, delete/dissolve, undo/redo, …) bump-mapped meshes loaded
  through the n-gon import path went dark and lost their normal map.

  Two compounding issues:

  1. EditableMesh::buildSubMeshBuffers checks only `vertices[0]
     .hasTangent` to decide whether to add a VES_TANGENT element to the
     rebuilt declaration. New vertices created by the op default-
     construct EditableVertex (zero-valued Vector4 tangent), so:
       (a) if the first vertex retained tangents the declaration kept
           VES_TANGENT but new vertices wrote (0,0,0,0), making RTSS's
           SRS_NORMALMAP TBN math collapse to zeros;
       (b) if the asset came in via loadFromAssimpFile (which
           deliberately omits aiProcess_CalcTangentSpace because that
           flag forces triangulation), every vertex has hasTangent=false
           and the declaration drops VES_TANGENT entirely.
     Either way the bump map was effectively gone.

  2. Each topology op had its OWN inline _deinitialise/_initialise +
     invalidateMaterial block; the undo/redo path
     (EditMeshTopologyCommand::applyMeshState) had a third copy that
     skipped the RTSS hook entirely. So a Subdivide-then-Ctrl-Z lost
     lighting even when the redo would have restored it.

Fix
  Centralise post-topology-op refresh into a new static method
  EditModeController::rewriteEntityAfterTopologyChange(Entity*). It:

    a) Detects bump-map intent by scanning every subentity's material
       for a `normal_map`/`NormalMap` TUS. If any subentity is bump-
       mapped, force `Mesh::buildTangentVectors` (storeParityInW=true)
       BEFORE _deinitialise/_initialise. Order matters: calling it
       AFTER _initialise is too late — the SubEntity already linked
       against the old (no-tangent) declaration and the RTSS shaders
       compile against that stale layout.

    b) Saves per-subentity material overrides before the deinit/init
       (Ogre resets them to the SubMesh default), restores after.

    c) Re-runs MeshImporterExporter::applyNormalMapsToEntity so RTSS
       re-attaches its SRS_NORMALMAP sub-render-state against the
       fresh tangents. invalidateMaterial alone only drops cached
       shader programs; the SRS_NORMALMAP gets dropped on
       removeShaderBasedTechnique inside applyNormalMap, so we must
       call it again to re-add it.

    d) Final invalidateMaterial pass to keep behaviour identical to
       the old per-op blocks for materials that aren't bump-mapped.

  All five inline copies in EditModeController.cpp (extrude, bevel
  commit, bevel cancel, knife commit, generic post-op via
  applyTopologyMutationNoSurvivor) now call this helper, and so does
  EditMeshTopologyCommand::applyMeshState in TransformCommands.cpp —
  so undo/redo gets the same treatment.

Tests
  Standalone regression test confirms the helper is reachable as a
  static method (so TransformCommands.cpp's qualified call survives a
  refactor that might shove it back into an anonymous namespace) and
  null-tolerant. Full bump-map / RTSS exercise needs a GL context
  which the test infra doesn't provide on macOS; coverage there is via
  hand smoke tests on the bump-mapped Mixamo asset (subdivide /
  extrude / undo / redo all confirmed visually preserving the bump
  map and per-pixel lighting).
@coderabbitai

coderabbitai Bot commented Apr 29, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: acfed797-410c-41ee-9bdb-4d5816475f57

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/quads-followup

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 49f5a2b589

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// doesn't survive a buffer-format change), so we must run it again
// to guarantee the SRS_NORMALMAP is re-bound against the new
// tangents. Materials without a normal-map TUS are no-ops here.
MeshImporterExporter::applyNormalMapsToEntity(ent);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Guard normal-map refresh against material load failures

rewriteEntityAfterTopologyChange now runs on every topology mutation and undo/redo, but it unconditionally calls MeshImporterExporter::applyNormalMapsToEntity, which performs mat->load() without exception handling. If any sub-entity uses an unloaded or broken material resource, that load can throw and abort the edit operation; this is a regression from the old path that only invalidated RTSS materials and did not force-load them during topology edits.

Useful? React with 👍 / 👎.

Codex P2 on PR #335: applyNormalMapsToEntity is now called from
rewriteEntityAfterTopologyChange on every topology op AND every
undo/redo, so any single broken/unresolvable material would now
abort the entire edit op via an unhandled `mat->load()` throw —
a regression from the old path that only invalidated RTSS without
forcing a load.

Wrap the load call in a try/catch and skip the offending sub-entity
on failure. Logs the material name + Ogre exception description so
we don't lose the diagnostic.
PR #335's SonarCloud quality gate failed on cognitive complexity:
  - rewriteEntityAfterTopologyChange: 47 (limit 25)
  - enterEditMode: 32 (limit 25)
plus a deprecation warning on the old buildTangentVectors overload.

Refactor (no behaviour change):
  - File-scope `entityWantsTangents`, `rebuildMeshTangents`,
    `invalidateEntityRtssMaterials` factor out the three loops inside
    `rewriteEntityAfterTopologyChange`. Switches to the non-deprecated
    buildTangentVectors signature.
  - File-scope `tryLoadEditableMeshNGonPath` factors out the four-
    nested-try-block n-gon-import attempt from `enterEditMode`.

`rewriteEntityAfterTopologyChange` and `enterEditMode` now read top-
to-bottom as plain sequences of named steps. All 234 standalone tests
still pass.
@sonarqubecloud

Copy link
Copy Markdown

@fernandotonon
fernandotonon merged commit fcc9c65 into feat/quads Apr 29, 2026
19 checks passed
@fernandotonon
fernandotonon deleted the fix/quads-followup branch April 29, 2026 03:11
fernandotonon added a commit that referenced this pull request Apr 29, 2026
PR #335's SonarCloud quality gate failed on cognitive complexity:
  - rewriteEntityAfterTopologyChange: 47 (limit 25)
  - enterEditMode: 32 (limit 25)
plus a deprecation warning on the old buildTangentVectors overload.

Refactor (no behaviour change):
  - File-scope `entityWantsTangents`, `rebuildMeshTangents`,
    `invalidateEntityRtssMaterials` factor out the three loops inside
    `rewriteEntityAfterTopologyChange`. Switches to the non-deprecated
    buildTangentVectors signature.
  - File-scope `tryLoadEditableMeshNGonPath` factors out the four-
    nested-try-block n-gon-import attempt from `enterEditMode`.

`rewriteEntityAfterTopologyChange` and `enterEditMode` now read top-
to-bottom as plain sequences of named steps. All 234 standalone tests
still pass.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant