Skip to content

feat(skinning): Skinning v2 — DQS display toggle, evaluation suite, Slice-C decision (#819 Slices C/D/E)#830

Merged
fernandotonon merged 5 commits into
masterfrom
feat/skinning-v2-cde-819
Jul 10, 2026
Merged

feat(skinning): Skinning v2 — DQS display toggle, evaluation suite, Slice-C decision (#819 Slices C/D/E)#830
fernandotonon merged 5 commits into
masterfrom
feat/skinning-v2-cde-819

Conversation

@fernandotonon

@fernandotonon fernandotonon commented Jul 10, 2026

Copy link
Copy Markdown
Owner

Completes the remaining slices of #819 on top of the merged Slices A+B (PR #829).

Slice D — Dual-quaternion skinning display toggle

No weight map fixes linear-blend volume collapse on twists — that needs a different blend at render time.

  • SkinningDisplay (src/SkinningDisplay.{h,cpp}): per-entity toggle via Ogre's RTSS HardwareSkinningFactory. Dual Quaternion = prepareEntityForSkinning(ST_DUAL_QUATERNION, antipodality-corrected) + technique invalidation; Linear = erase the material's HS_SRS_DATA imprint (mirrors Ogre's file-local constant, stable since 1.8) and regenerate. Mode is tracked on the entity's UserObjectBindings.
  • The factory + a dormant template SRS are registered in RTShaderHelper::initialize (Ogre ShaderSystem-sample pattern — plain materials are unaffected) and unregistered + deleted on shutdown. Bone cap 96: entities above it stay safely on the default path (verified: 119-bone bandit renders unchanged).
  • Surfaces: "Display: Linear | Dual Quaternion" row in the Animation-mode Skinning section, MCP set_skinning_display (mode enum), SkinWeightsController::skinningDisplayMode/setSkinningDisplayMode, Sentry render.skinning.
  • Display only — exporters consume the unchanged vertex weights (documented in tooltips + MCP description + docs).
  • Live-verified through the running app (load → toggle DQS → toggle Linear → renders correctly both ways). One pre-existing finding while verifying: quitting via AppleScript aborts (exit 134) — reproduces with the new factory disabled, i.e. not introduced here.

Slice E — Metrics, acceptance suite, Mixamo protocol

  • SkinMetrics (pure-data, unit-tested): influence histogram, Laplacian weight-smoothness energy, LBS deform (rotationAbout + deformLBS), signed mesh volume.
  • SkinEvaluate: extracts an entity's existing weights (whoever authored them) and reports metrics — including the geodesic bleed of existing weights against a fresh GeodesicVoxelBind field — plus a reference comparison with position-matched vertices and name-matched bones. Equidistant duplicate vertices (seams, hand-on-prop contact points) tie-break on minimum weight difference, so self-compare is exactly 0 (verified on the 90k-vert bandit; the naive matcher reported spurious 1.45 max-diffs at contact points).
  • CLI: qtmesh skin <file> --evaluate [--voxel-res N] [--json] and qtmesh skin <file> --compare <reference> [--json].
  • Acceptance fixtures (hermetic, procedural — no binary assets): 90° elbow-bend capsule volume ratio ≥ 0.9 (measured 0.911 with geodesic weights + Slice-B smoothing), two-limb proximity bleed = 0 for geodesic vs > 0.1 for the inverse-distance contrast, smoothing strictly reduces Laplacian energy, plus metric primitive tests.
  • docs/SKINNING_QUALITY.md: metric definitions, thresholds, the manual Mixamo comparison protocol (no Adobe assets in-repo), and the env-gated regression test (QTMESH_SKIN_OURS_FBX / QTMESH_SKIN_REF_FBX — skipped in CI, suite has unconditional tests so it never goes skip-only).

Slice C — UniRig skinning head: decision record

Per the issue's own decision gate ("also evaluate SkinTokens … if it exports cleaner, prefer it"):

  • UniRig's skin head is not ONNX-exportable as-is: verified against upstream src/model/unirig_skin.py — the mesh geometry path runs through a PTv3 backbone on spconv sparse convolutions (no ONNX operator lowering) plus flash-attn MHA. The AI: RigNet auto-rigging (ONNX, ML) #408 skeleton export only worked because that stage never executes the PTv3 path. A faithful skin.onnx means re-implementing PTv3 densely — a research task.
  • SkinTokens/TokenRig (VAST-AI, MIT code, FSQ-CVAE weight tokenizer + Qwen3-0.6B AR transformer, no spconv) is the preferred ML-skinning follow-up — exportable with the AI: RigNet auto-rigging (ONNX, ML) #408 KV-cache decoder pattern, but a full new integration rather than a drop-in head. Weights-license confirmation is part of that follow-up.
  • Recorded in THIRD_PARTY_AI_MODELS.md; Algorithm::UniRigML keeps its clean, tested fallback to GeodesicVoxel on every surface meanwhile, so the ML path can light up later without changing any app surface.

Verification

  • All pure-data fixtures verified via the standalone harness (macOS can't run Ogre-backed UnitTests locally; Linux CI runs the full suite): elbow ratio 0.911, bleed contrasts, smoothing energy reduction.
  • --evaluate on the 90k-vert bandit: avg 3.44 influences, bleed 0.028, smoothness 0.0124. --compare self-test: 90,573/90,573 matched, mean/max L1 diff 0.0000.
  • DQS toggle exercised live via MCP on two real assets, screenshots inspected.
  • App + UnitTests build clean.

Tracks #819 (Slices C/D/E; A+B merged in #829).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added Linear and Dual Quaternion skinning display modes in the Animation Mode tools (visual-only).
    • Added MCP tool support to switch the selected entity’s skinning display mode at runtime.
    • Added qtmesh skin --evaluate (quality metrics) and qtmesh skin --compare <reference> (per-vertex weight diffs) with JSON or text output.
  • Documentation
    • Expanded skinning-quality guidance, acceptance workflow, and dual-quaternion display behavior.
    • Updated CLI and skinning decision records.
  • Tests
    • Added end-to-end and unit coverage for display mode toggling, skin metrics, deformation/smoothing/bleed, and reference comparisons.

…ion (#819 Slices C/D/E)

Slice D — dual-quaternion skinning display:
- SkinningDisplay toggles RTSS hardware skinning per entity: Dual
  Quaternion (prepareEntityForSkinning ST_DUAL_QUATERNION + technique
  invalidation, kills candy-wrapper collapse on twists) / Linear
  (erases the HS_SRS_DATA imprint, back to the default path)
- HardwareSkinningFactory + dormant template SRS registered in
  RTShaderHelper (bone cap 96; above-cap entities keep the default
  path), factory unregistered+deleted on shutdown
- Surfaces: "Display: Linear | Dual Quaternion" row in the
  Animation-mode Skinning section, MCP set_skinning_display,
  SkinWeightsController get/set, Sentry render.skinning
- Display only: exported weights unchanged (documented in tooltips)
- Live-verified: toggle DQS/linear on real assets renders correctly;
  known pre-existing AppleScript-quit abort is unrelated (reproduces
  with the factory disabled)

Slice E — skin-quality metrics + acceptance suite:
- SkinMetrics (pure-data): influence histogram, Laplacian weight-
  smoothness energy, LBS deform + rotation transforms, signed mesh
  volume
- SkinEvaluate: extract EXISTING weights from an entity; --evaluate
  report (histogram, smoothness, geodesic bleed of existing weights);
  --compare vs a reference-skinned copy with position-matched
  vertices (equidistant duplicates tie-break on min weight diff —
  self-compare is exactly 0) and name-matched bones
- CLI: qtmesh skin --evaluate [--json], --compare <ref> [--json]
- Acceptance fixtures (hermetic, procedural): 90° elbow capsule
  volume >= 0.9 (measured 0.911), proximity bleed 0 for GVB vs >0.1
  for inverse-distance, smoothing strictly reduces energy
- docs/SKINNING_QUALITY.md: metric definitions, thresholds, Mixamo
  comparison protocol; env-gated reference test
  (QTMESH_SKIN_OURS_FBX / QTMESH_SKIN_REF_FBX)

Slice C — decision record (THIRD_PARTY_AI_MODELS.md):
- UniRig's skin head is blocked on spconv/PTv3 (no ONNX lowering;
  verified upstream) — a faithful export is a research task, not an
  export chore
- SkinTokens (MIT, Qwen3-0.6B AR, no spconv) evaluated per the
  issue's decision gate: the preferred ML-skinning follow-up
- Algorithm::UniRigML keeps its clean GeodesicVoxel fallback on all
  surfaces meanwhile

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4914f698-f8fc-401e-8660-02e7809dc247

📥 Commits

Reviewing files that changed from the base of the PR and between 8dc4b2e and 2418ed0.

📒 Files selected for processing (4)
  • src/RTShaderHelper.cpp
  • src/SkinEvaluate_test.cpp
  • src/SkinningDisplay.cpp
  • src/SkinningDisplay_test.cpp
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/SkinningDisplay.cpp
  • src/SkinEvaluate_test.cpp

📝 Walkthrough

Walkthrough

Adds skin-quality metrics, asset evaluation and comparison commands, runtime linear/dual-quaternion display controls, MCP and QML integrations, RTSS lifecycle handling, tests, and documentation for skinning workflows.

Changes

Skinning quality tooling

Layer / File(s) Summary
Skin metrics primitives
src/SkinMetrics.*, src/SkinMetrics_test.cpp, src/CMakeLists.txt, tests/CMakeLists.txt
Adds influence histograms, Laplacian energy, transforms, LBS deformation, mesh volume calculations, and unit/acceptance tests.
Evaluation and comparison workflow
src/SkinEvaluate.*, src/SkinEvaluate_test.cpp, src/CLIPipeline.cpp
Extracts Ogre skin data, reports quality metrics, compares vertex weights against reference assets, and adds text/JSON CLI analysis modes.
Runtime skinning display controls
src/SkinningDisplay.*, src/SkinWeightsController.*, qml/PropertiesPanel.qml, src/MCPServer.*, src/RTShaderHelper.cpp, src/SkinningDisplay_test.cpp
Adds per-entity linear and dual-quaternion display switching through RTSS, QML, and MCP interfaces, with lifecycle and behavior tests.
Skinning workflow documentation
.gitignore, CLAUDE.md, THIRD_PARTY_AI_MODELS.md, docs/SKINNING_QUALITY.md
Documents evaluation commands, quality criteria, display controls, and the UniRig skinning decision record while preserving the quality document from ignore rules.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant CLIPipeline
  participant SkinEvaluate
  participant OgreEntity
  User->>CLIPipeline: Run skin evaluation or comparison
  CLIPipeline->>OgreEntity: Import asset
  CLIPipeline->>SkinEvaluate: Evaluate or compare
  SkinEvaluate->>OgreEntity: Extract skin data
  SkinEvaluate-->>CLIPipeline: Return report
  CLIPipeline-->>User: Print text or JSON
Loading
sequenceDiagram
  participant PropertiesPanel
  participant SkinWeightsController
  participant SkinningDisplay
  participant OgreRTSS
  PropertiesPanel->>SkinWeightsController: Select display mode
  SkinWeightsController->>SkinningDisplay: Apply mode to selected entity
  SkinningDisplay->>OgreRTSS: Prepare or clear hardware skinning
  OgreRTSS-->>SkinningDisplay: Regenerate material techniques
  SkinningDisplay-->>PropertiesPanel: Report mode state
Loading

Possibly related issues

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.98% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is specific and matches the main changes: DQS display toggle, skinning evaluation, and Slice C decision record.
Description check ✅ Passed The description is detailed and covers the PR's main slices, technical changes, and verification, though it doesn't follow the template headings exactly.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/skinning-v2-cde-819

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.

@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: 78f87b6aa7

ℹ️ 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".

Comment thread src/SkinningDisplay.cpp Outdated
Comment on lines +70 to +71
mat->getTechnique(0)->getPass(0)
->getUserObjectBindings().eraseUserAny(kOgreHsDataKey);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Remove HS_SRS_DATA from the technique

When the user toggles Dual Quaternion back to Linear, this erases the key from the pass user bindings, but Ogre's prepareEntityForSkinning stores HS_SRS_DATA on Technique(0) and the RTSS hardware-skinning SRS reads it from the technique. The DQS imprint therefore survives invalidation, so the regenerated material can keep rendering in DQS while the UI/MCP report linear; erase mat->getTechnique(0)->getUserObjectBindings() instead.

Useful? React with 👍 / 👎.

Comment thread src/SkinningDisplay.cpp
Comment on lines +60 to +62
Ogre::RTShader::HardwareSkinningFactory::prepareEntityForSkinning(
entity, Ogre::RTShader::ST_DUAL_QUATERNION,
/*correctAntidpodalityHandling*/ true, /*shearScale*/ false);

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 Clone shared materials before imprinting DQS

When two skinned entities share the same Ogre material, this call imprints the shared material resource, while the selected mode is tracked on only one entity. Toggling one character to DQS (or later back to Linear after the erase bug is fixed) changes the generated shader for every other entity using that material and leaves their current() mode stale, so this is not actually per-entity unless the material is made unique before applying the imprint.

Useful? React with 👍 / 👎.

fernandotonon and others added 2 commits July 10, 2026 01:05
The test harness never runs Manager::loadResources() (the GUI/CLI
path that brings RTSS up), so ShaderGenerator::getSingletonPtr() was
null in the isolated CI run. Initialize it in SetUp like
MaterialPresetLibrary_test does.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-material entities

Codex review on PR #830:
- P1: Ogre stores/reads HS_SRS_DATA on getTechnique(0)'s user
  bindings (verified v14.5.2 imprintSkeletonData/preAddToRenderState),
  not on a pass — Linear mode was erasing the wrong object, so the
  DQS imprint survived and the material kept rendering DQS while
  reporting linear. Test's imprint probe fixed to match.
- P2: the imprint is material-level, so entities sharing a material
  switch together — apply() now stamps the tracked mode on every
  entity sharing one of the affected materials, keeping current()
  truthful (new SharedMaterialEntitiesTrackTheSameMode test).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@fernandotonon

Copy link
Copy Markdown
Owner Author

Both Codex findings were valid — fixed in the latest commit:

  • P1 (imprint on the technique): confirmed against Ogre v14.5.2 source (imprintSkeletonData stores on getTechnique(0)->getUserObjectBindings(), preAddToRenderState reads the same) — Linear mode now erases the technique binding, and the test's imprint probe checks the same place.
  • P2 (shared materials): apply() now stamps the tracked mode on every entity sharing one of the affected materials, so current() stays truthful for all of them; covered by the new SharedMaterialEntitiesTrackTheSameMode test. (Material uniquification was deliberately avoided — silently cloning materials would leak clone names into exports and the Material editor.)

Also fixed the CI failure: the test harness never runs Manager::loadResources(), so SkinningDisplayTest now initializes RTSS explicitly in SetUp (the MaterialPresetLibrary_test pattern).

🤖 Generated with Claude Code

@coderabbitai coderabbitai 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.

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/CLIPipeline.cpp (1)

8921-8942: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add validation for --evaluate/--compare mutual exclusivity and missing reference path.

Two edge cases are silently mishandled:

  1. If both --evaluate and --compare are passed, --compare silently takes priority (line 8992 checks !comparePath.isEmpty() first), which is confusing.
  2. If --compare is the last argument (no reference path), the i + 1 < argc guard at line 8922 fails silently, comparePath stays empty, and the user gets a misleading "-o required" error or unintended weight computation.
🔧 Proposed validation
     if (inputPath.isEmpty()) {
         err() << "Error: No input file specified." << Qt::endl;
         err() << "Usage: qtmesh skin <file> "
                  "[--algo geodesic-voxel|inverse-distance|unirig] "
                  "[--max-influences N] [--falloff F] [--max-distance D] "
                  "[--voxel-res N] [--smooth-iterations N] "
                  "[--skip-unweighted] [--merge] -o <out> [--json]\n"
                  "       qtmesh skin <file> --evaluate [--voxel-res N] [--json]\n"
                  "       qtmesh skin <file> --compare <reference> [--json]"
               << Qt::endl;
         return 2;
     }
+    if (evaluateMode && !comparePath.isEmpty()) {
+        err() << "Error: --evaluate and --compare are mutually exclusive." << Qt::endl;
+        return 2;
+    }
+    if (evaluateMode && comparePath.isEmpty()) {
+        // --evaluate mode: no reference needed
+    } else if (!evaluateMode && !comparePath.isEmpty() && false) {
+        // handled below
+    }
+    // Detect --compare without a following path argument
     const bool analysisMode = evaluateMode || !comparePath.isEmpty();

A simpler approach: add a compareRequested bool set when --compare is seen (regardless of whether a path follows), then validate after the loop:

+    bool compareRequested = false;
     for (int i = 1; i < argc; ++i) {
         ...
         if (arg == "--compare") {
+            compareRequested = true;
             if (i + 1 < argc) {
                 comparePath = QString::fromLocal8Bit(argv[++i]);
             }
             continue;
         }
         ...
     }
     ...
+    if (compareRequested && comparePath.isEmpty()) {
+        err() << "Error: --compare requires a reference file path." << Qt::endl;
+        return 2;
+    }
+    if (evaluateMode && !comparePath.isEmpty()) {
+        err() << "Error: --evaluate and --compare are mutually exclusive." << Qt::endl;
+        return 2;
+    }
     const bool analysisMode = evaluateMode || !comparePath.isEmpty();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/CLIPipeline.cpp` around lines 8921 - 8942, Add a compareRequested flag in
the argument-parsing logic and set it whenever --compare is encountered, even
without a following value. After parsing, reject simultaneous evaluateMode and
compareRequested with a clear error, and reject compareRequested when
comparePath is empty with a missing-reference error; return before analysis mode
or output validation so these cases cannot fall through.
🧹 Nitpick comments (1)
src/SkinningDisplay_test.cpp (1)

91-122: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Test entity not destroyed at the end.

ent created via createAnimatedTestEntity isn't destroyed here, unlike GuardsRejectInvalidEntities which calls sceneMgr->destroyEntity(ent). Minor test-hygiene inconsistency; low practical impact given uniqueName() avoids collisions, but leaves entities accumulating in the shared SceneManager across the test binary's lifetime.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/SkinningDisplay_test.cpp` around lines 91 - 122, Add explicit cleanup for
the entity created by createAnimatedTestEntity in DqsAppliesAndLinearReverts,
ensuring sceneMgr->destroyEntity(ent) is called after the assertions (including
on assertion failures if the test fixture supports a scoped cleanup mechanism),
matching the cleanup pattern used by GuardsRejectInvalidEntities.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/SKINNING_QUALITY.md`:
- Around line 52-61: Add the text language identifier to the fenced
sample-output block in the Skin Evaluation section by changing the opening fence
to ```text, while leaving the report contents unchanged.
- Around line 16-18: The documented bleed metric contradicts the GeodesicVoxel
sample output. Update the “Bleed fraction” definition and related acceptance
threshold to state whether post-processing or smoothing can create non-local
weights, then revise the GeodesicVoxel sample value or explanation so the
definition, threshold, and sample output are consistent.

In `@qml/PropertiesPanel.qml`:
- Around line 2345-2382: Make the skinning display options in the dispBtn
delegate keyboard- and screen-reader-accessible by replacing the raw
Rectangle/MouseArea interaction with the project’s existing accessible
segmented-control pattern, or add equivalent activeFocusOnTab, Accessible
role/name/checked state, visible focus styling, and keyboard activation for the
Space/Enter keys. Preserve the existing setSkinningDisplayMode and activeMode
behavior, and ensure the controls expose mutually exclusive radio-style
selection.
- Around line 2321-2328: The active mode state in skinDispRow is refreshed only
by onSelectionChanged, so runtime changes from MCPServer.cpp can leave the UI
stale. Add an observable SkinWeightsController mode-changed signal and connect
it alongside onSelectionChanged, or otherwise trigger the same
skinningDisplayMode() refresh after shared runtime mode updates; ensure both
user and MCP changes update skinDispRow.activeMode.

In `@src/CLIPipeline.cpp`:
- Around line 9002-9008: Add a multi-entity validation after the reference
import and entity-discovery loop associated with refEntity: count all newly
imported Ogre::Entity objects not present in meshEntities, reject the reference
file when more than one is found, and report the same error/cleanup outcome used
by the existing input-file multi-entity check. Ensure refEntity is assigned only
for the single valid entity and prevent comparison from continuing with silently
ignored entities.

In `@src/SkinEvaluate.cpp`:
- Around line 196-198: Document in the public static evaluate() API that calling
it resets the entity’s shared skeleton to bind pose as part of geodesic voxel
field generation, or save and restore all bone transforms around
skel->reset(true) to prevent the caller’s pose from being mutated.

In `@src/SkinMetrics.cpp`:
- Around line 81-106: Guard against a zero-length rotation axis in
SkinMetrics::rotationAbout before normalizing it. Define the API’s intended
behavior for this invalid input—preferably reject it explicitly using the
project’s established error-handling convention, or return an identity transform
if that is the documented contract—instead of dividing by zero; preserve
existing behavior for nonzero axes.

In `@src/SkinWeightsController.cpp`:
- Line 231: Update the mode handling in SkinWeightsController using
SkinningDisplay::modeFromString so unknown or unsupported display-mode strings
are detected instead of defaulting to Linear; validate the input against the
accepted mode names and return an appropriate error without changing the entity
when validation fails.
- Around line 232-238: Move the SentryReporter::addBreadcrumb call to after
SkinningDisplay::apply succeeds, or update it to include the operation outcome;
ensure failed applications do not emit a breadcrumb claiming the mode was set.

In `@src/SkinWeightsController.h`:
- Around line 53-60: Clarify the API documentation near skinningDisplayMode()
and setSkinningDisplayMode(): state that skinningDisplayMode() returns the
current mode for the first selected entity, while setSkinningDisplayMode()
returns a bool indicating whether the requested mode was successfully applied.

---

Outside diff comments:
In `@src/CLIPipeline.cpp`:
- Around line 8921-8942: Add a compareRequested flag in the argument-parsing
logic and set it whenever --compare is encountered, even without a following
value. After parsing, reject simultaneous evaluateMode and compareRequested with
a clear error, and reject compareRequested when comparePath is empty with a
missing-reference error; return before analysis mode or output validation so
these cases cannot fall through.

---

Nitpick comments:
In `@src/SkinningDisplay_test.cpp`:
- Around line 91-122: Add explicit cleanup for the entity created by
createAnimatedTestEntity in DqsAppliesAndLinearReverts, ensuring
sceneMgr->destroyEntity(ent) is called after the assertions (including on
assertion failures if the test fixture supports a scoped cleanup mechanism),
matching the cleanup pattern used by GuardsRejectInvalidEntities.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c8001557-e739-4497-8dcb-2132c67aacd5

📥 Commits

Reviewing files that changed from the base of the PR and between 923cd93 and 8dc4b2e.

📒 Files selected for processing (22)
  • .gitignore
  • CLAUDE.md
  • THIRD_PARTY_AI_MODELS.md
  • docs/SKINNING_QUALITY.md
  • qml/PropertiesPanel.qml
  • src/CLIPipeline.cpp
  • src/CMakeLists.txt
  • src/MCPServer.cpp
  • src/MCPServer.h
  • src/RTShaderHelper.cpp
  • src/SkinEvaluate.cpp
  • src/SkinEvaluate.h
  • src/SkinEvaluate_test.cpp
  • src/SkinMetrics.cpp
  • src/SkinMetrics.h
  • src/SkinMetrics_test.cpp
  • src/SkinWeightsController.cpp
  • src/SkinWeightsController.h
  • src/SkinningDisplay.cpp
  • src/SkinningDisplay.h
  • src/SkinningDisplay_test.cpp
  • tests/CMakeLists.txt

Comment thread docs/SKINNING_QUALITY.md
Comment on lines +16 to +18
| Influence histogram | vertices per influence count (0–8), average, max | max ≤ 4 (hardware-skinning convention) |
| Weight smoothness | Laplacian energy: mean over mesh edges of ‖w_u − w_v‖² | lower = smoother falloffs; hard 0/1 borders score ~2 per edge |
| Bleed fraction | share of committed (vertex, bone) weights whose bone is **not geodesically local** to the vertex (GeodesicVoxelBind field) | 0 — geodesic-voxel weights are 0 by construction |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reconcile the bleed metric definition with the sample output.

The table says GeodesicVoxel bleed is 0 “by construction,” but the documented GeodesicVoxel sample reports 0.0055. Clarify whether post-processing/smoothing can introduce non-local weights, then align the definition, acceptance threshold, and sample output.

Also applies to: 59-60

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/SKINNING_QUALITY.md` around lines 16 - 18, The documented bleed metric
contradicts the GeodesicVoxel sample output. Update the “Bleed fraction”
definition and related acceptance threshold to state whether post-processing or
smoothing can create non-local weights, then revise the GeodesicVoxel sample
value or explanation so the definition, threshold, and sample output are
consistent.

Comment thread docs/SKINNING_QUALITY.md
Comment on lines +52 to +61
```
Skin Evaluation
===============
Vertices: 90573
Bones: 119
Avg influences: 3.47
Max influences: 4
Smoothness energy: 0.004... (lower = smoother falloffs)
Bleed fraction: 0.0055 (weights not geodesically local)
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add a language identifier to the sample-output fence.

The unlabelled fence triggers MD040. Use ```text for this report block.

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 52-52: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/SKINNING_QUALITY.md` around lines 52 - 61, Add the text language
identifier to the fenced sample-output block in the Skin Evaluation section by
changing the opening fence to ```text, while leaving the report contents
unchanged.

Source: Linters/SAST tools

Comment thread qml/PropertiesPanel.qml
Comment on lines +2321 to +2328
// Re-read the active mode when the selection changes.
property string activeMode: SkinWeightsController.skinningDisplayMode()
Connections {
target: SkinWeightsController
function onSelectionChanged() {
skinDispRow.activeMode =
SkinWeightsController.skinningDisplayMode()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Refresh the active mode when runtime/MCP changes it.

This handler only reacts to selection changes, but MCPServer.cpp applies the mode directly without changing selection. The viewport can therefore be in Dual Quaternion mode while the Linear button remains highlighted until the user reselects the entity. Add an observable mode-changed notification or refresh this state after shared runtime changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@qml/PropertiesPanel.qml` around lines 2321 - 2328, The active mode state in
skinDispRow is refreshed only by onSelectionChanged, so runtime changes from
MCPServer.cpp can leave the UI stale. Add an observable SkinWeightsController
mode-changed signal and connect it alongside onSelectionChanged, or otherwise
trigger the same skinningDisplayMode() refresh after shared runtime mode
updates; ensure both user and MCP changes update skinDispRow.activeMode.

Comment thread qml/PropertiesPanel.qml
Comment on lines +2345 to +2382
Rectangle {
id: dispBtn
required property var modelData
readonly property bool active:
skinDispRow.activeMode === modelData.mode
width: dispLabel.implicitWidth + 14
height: 22
radius: 3
color: active ? PropertiesPanelController.highlightColor
: (dispMa.containsMouse
? PropertiesPanelController.headerColor
: PropertiesPanelController.inputColor)
border.color: PropertiesPanelController.borderColor
border.width: 1

Text {
id: dispLabel
anchors.centerIn: parent
text: dispBtn.modelData.label
color: PropertiesPanelController.textColor
font.pixelSize: 10
}
MouseArea {
id: dispMa
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (SkinWeightsController.setSkinningDisplayMode(
dispBtn.modelData.mode))
skinDispRow.activeMode = dispBtn.modelData.mode
}
ToolTip.visible: containsMouse
ToolTip.delay: 500
ToolTip.text: dispBtn.modelData.mode === "dual-quaternion"
? "Render with dual-quaternion hardware skinning — preserves volume on twists (no candy-wrapper). Display only: exported weights are unchanged; engines re-skin with their own blend."
: "Render with the default linear-blend skinning path."
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Make the display controls keyboard- and screen-reader-accessible.

These new controls are raw Rectangle/MouseArea elements with no activeFocusOnTab, Accessible role/name/checked state, or keyboard activation. Users who cannot use a mouse cannot change the skinning mode. Use accessible radio/button controls or add the same focus and key handling used by the existing segmented controls.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@qml/PropertiesPanel.qml` around lines 2345 - 2382, Make the skinning display
options in the dispBtn delegate keyboard- and screen-reader-accessible by
replacing the raw Rectangle/MouseArea interaction with the project’s existing
accessible segmented-control pattern, or add equivalent activeFocusOnTab,
Accessible role/name/checked state, visible focus styling, and keyboard
activation for the Space/Enter keys. Preserve the existing
setSkinningDisplayMode and activeMode behavior, and ensure the controls expose
mutually exclusive radio-style selection.

Comment thread src/CLIPipeline.cpp
Comment on lines +9002 to +9008
for (Ogre::Entity* e : Manager::getSingleton()->getEntities()) {
if (e && e->getMovableType() == "Entity"
&& !meshEntities.contains(e)) {
refEntity = e;
break;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Add multi-entity guard for the reference file.

The reference entity is found by picking the first new Ogre::Entity* not in meshEntities. If the reference file imports multiple entities, only the first is used and the rest are silently ignored, potentially producing incorrect comparison results. The input file already has a multi-entity check (lines 8978-8984), but the reference does not.

🛡️ Proposed multi-entity guard
             Ogre::Entity* refEntity = nullptr;
+            int newCount = 0;
             for (Ogre::Entity* e : Manager::getSingleton()->getEntities()) {
                 if (e && e->getMovableType() == "Entity"
                     && !meshEntities.contains(e)) {
-                    refEntity = e;
-                    break;
+                    if (!refEntity) refEntity = e;
+                    ++newCount;
                 }
             }
+            if (newCount > 1) {
+                err() << "Error: reference " << comparePath
+                      << " contains multiple mesh entities. `qtmesh skin --compare` "
+                         "currently supports one entity per file."
+                      << Qt::endl;
+                return 1;
+            }
             if (!refEntity) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (Ogre::Entity* e : Manager::getSingleton()->getEntities()) {
if (e && e->getMovableType() == "Entity"
&& !meshEntities.contains(e)) {
refEntity = e;
break;
}
}
Ogre::Entity* refEntity = nullptr;
int newCount = 0;
for (Ogre::Entity* e : Manager::getSingleton()->getEntities()) {
if (e && e->getMovableType() == "Entity"
&& !meshEntities.contains(e)) {
if (!refEntity) refEntity = e;
+newCount;
}
}
if (newCount > 1) {
err() << "Error: reference " << comparePath
<< " contains multiple mesh entities. `qtmesh skin --compare` "
"currently supports one entity per file."
<< Qt::endl;
return 1;
}
if (!refEntity) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/CLIPipeline.cpp` around lines 9002 - 9008, Add a multi-entity validation
after the reference import and entity-discovery loop associated with refEntity:
count all newly imported Ogre::Entity objects not present in meshEntities,
reject the reference file when more than one is found, and report the same
error/cleanup outcome used by the existing input-file multi-entity check. Ensure
refEntity is assigned only for the single valid entity and prevent comparison
from continuing with silently ignored entities.

Comment thread src/SkinEvaluate.cpp
Comment on lines +196 to +198
{
Ogre::Skeleton* skel = entity->getMesh()->getSkeleton().get();
skel->reset(true);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Document the skeleton reset side effect in evaluate().

skel->reset(true) mutates the skeleton's shared state to obtain bind-pose bone positions for the geodesic voxel field. This is correct for CLI usage, but evaluate() is a public static API — a future GUI or MCP caller would see the entity's pose silently reset. Consider adding a brief note to the header doc or saving/restoring bone transforms.

📝 Suggested doc addition in SkinEvaluate.h
     //   when the mesh encloses volume — the geodesic bleed fraction
     //   (GeodesicVoxelBind field at `voxelResolution`).
+    // Note: resets the skeleton to bind pose to compute bone
+    // positions for the voxel field; callers displaying the entity
+    // should restore the pose afterwards if needed.
     static QJsonObject evaluate(Ogre::Entity* entity,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/SkinEvaluate.cpp` around lines 196 - 198, Document in the public static
evaluate() API that calling it resets the entity’s shared skeleton to bind pose
as part of geodesic voxel field generation, or save and restore all bone
transforms around skel->reset(true) to prevent the caller’s pose from being
mutated.

Comment thread src/SkinMetrics.cpp
Comment on lines +81 to +106
SkinMetrics::Transform SkinMetrics::rotationAbout(const double axis[3],
const double pivot[3],
double angleRad)
{
// Normalize the axis.
const double len = std::sqrt(axis[0] * axis[0] + axis[1] * axis[1]
+ axis[2] * axis[2]);
const double x = axis[0] / len, y = axis[1] / len, z = axis[2] / len;
const double c = std::cos(angleRad), s = std::sin(angleRad);
const double t = 1.0 - c;

// Rodrigues rotation matrix.
const double r[9] = {
t * x * x + c, t * x * y - s * z, t * x * z + s * y,
t * x * y + s * z, t * y * y + c, t * y * z - s * x,
t * x * z - s * y, t * y * z + s * x, t * z * z + c,
};
// Translation = pivot − R·pivot so the pivot is a fixed point.
const double tx = pivot[0] - (r[0] * pivot[0] + r[1] * pivot[1] + r[2] * pivot[2]);
const double ty = pivot[1] - (r[3] * pivot[0] + r[4] * pivot[1] + r[5] * pivot[2]);
const double tz = pivot[2] - (r[6] * pivot[0] + r[7] * pivot[1] + r[8] * pivot[2]);

return { r[0], r[1], r[2], tx,
r[3], r[4], r[5], ty,
r[6], r[7], r[8], tz };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Guard against zero-length axis in rotationAbout.

Line 88 divides by len without checking for zero. A zero-length axis produces NaN/Inf values that silently propagate through the transform. Callers currently pass valid axes, but this is a public API.

🛡️ Proposed fix
     const double len = std::sqrt(axis[0] * axis[0] + axis[1] * axis[1]
                                  + axis[2] * axis[2]);
-    const double x = axis[0] / len, y = axis[1] / len, z = axis[2] / len;
+    if (len < 1e-12)
+        return identityTransform();
+    const double x = axis[0] / len, y = axis[1] / len, z = axis[2] / len;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
SkinMetrics::Transform SkinMetrics::rotationAbout(const double axis[3],
const double pivot[3],
double angleRad)
{
// Normalize the axis.
const double len = std::sqrt(axis[0] * axis[0] + axis[1] * axis[1]
+ axis[2] * axis[2]);
const double x = axis[0] / len, y = axis[1] / len, z = axis[2] / len;
const double c = std::cos(angleRad), s = std::sin(angleRad);
const double t = 1.0 - c;
// Rodrigues rotation matrix.
const double r[9] = {
t * x * x + c, t * x * y - s * z, t * x * z + s * y,
t * x * y + s * z, t * y * y + c, t * y * z - s * x,
t * x * z - s * y, t * y * z + s * x, t * z * z + c,
};
// Translation = pivot − R·pivot so the pivot is a fixed point.
const double tx = pivot[0] - (r[0] * pivot[0] + r[1] * pivot[1] + r[2] * pivot[2]);
const double ty = pivot[1] - (r[3] * pivot[0] + r[4] * pivot[1] + r[5] * pivot[2]);
const double tz = pivot[2] - (r[6] * pivot[0] + r[7] * pivot[1] + r[8] * pivot[2]);
return { r[0], r[1], r[2], tx,
r[3], r[4], r[5], ty,
r[6], r[7], r[8], tz };
}
SkinMetrics::Transform SkinMetrics::rotationAbout(const double axis[3],
const double pivot[3],
double angleRad)
{
// Normalize the axis.
const double len = std::sqrt(axis[0] * axis[0] + axis[1] * axis[1]
axis[2] * axis[2]);
if (len < 1e-12)
return identityTransform();
const double x = axis[0] / len, y = axis[1] / len, z = axis[2] / len;
const double c = std::cos(angleRad), s = std::sin(angleRad);
const double t = 1.0 - c;
// Rodrigues rotation matrix.
const double r[9] = {
t * x * x + c, t * x * y - s * z, t * x * z + s * y,
t * x * y + s * z, t * y * y + c, t * y * z - s * x,
t * x * z - s * y, t * y * z + s * x, t * z * z + c,
};
// Translation = pivot − R·pivot so the pivot is a fixed point.
const double tx = pivot[0] - (r[0] * pivot[0] + r[1] * pivot[1] + r[2] * pivot[2]);
const double ty = pivot[1] - (r[3] * pivot[0] + r[4] * pivot[1] + r[5] * pivot[2]);
const double tz = pivot[2] - (r[6] * pivot[0] + r[7] * pivot[1] + r[8] * pivot[2]);
return { r[0], r[1], r[2], tx,
r[3], r[4], r[5], ty,
r[6], r[7], r[8], tz };
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/SkinMetrics.cpp` around lines 81 - 106, Guard against a zero-length
rotation axis in SkinMetrics::rotationAbout before normalizing it. Define the
API’s intended behavior for this invalid input—preferably reject it explicitly
using the project’s established error-handling convention, or return an identity
transform if that is the documented contract—instead of dividing by zero;
preserve existing behavior for nonzero axes.

return false;
}

const auto m = SkinningDisplay::modeFromString(mode);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject invalid display-mode strings instead of silently selecting Linear.

SkinningDisplay::modeFromString() maps every unknown value to Linear, so a typo or unsupported MCP/QML value reports success while changing the entity’s mode. Validate accepted values and return an error for unknown modes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/SkinWeightsController.cpp` at line 231, Update the mode handling in
SkinWeightsController using SkinningDisplay::modeFromString so unknown or
unsupported display-mode strings are detected instead of defaulting to Linear;
validate the input against the accepted mode names and return an appropriate
error without changing the entity when validation fails.

Comment on lines +232 to +238
SentryReporter::addBreadcrumb(QStringLiteral("render.skinning"),
QStringLiteral("display mode %1 on %2")
.arg(SkinningDisplay::modeToString(m),
QString::fromStdString(entity->getName())));

QString err;
if (!SkinningDisplay::apply(entity, m, &err)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Record the breadcrumb after applying the mode, or include the outcome.

The breadcrumb currently says the mode was set before SkinningDisplay::apply() can fail, producing misleading telemetry for skeleton-less entities or uninitialized RTSS. As per coding guidelines, significant operations must be tracked with breadcrumbs that accurately represent the operation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/SkinWeightsController.cpp` around lines 232 - 238, Move the
SentryReporter::addBreadcrumb call to after SkinningDisplay::apply succeeds, or
update it to include the operation outcome; ensure failed applications do not
emit a breadcrumb claiming the mode was set.

Source: Coding guidelines

Comment on lines +53 to +60
/// #819 Slice D: dual-quaternion display toggle for the selected
/// entity. Modes: "linear" (default LBS path) or
/// "dual-quaternion" (RTSS hardware DQS — kills the candy-wrapper
/// collapse on twists). Runtime shading only; exported weights
/// are unchanged. Returns the mode applied to / read from the
/// first selected entity.
Q_INVOKABLE QString skinningDisplayMode() const;
Q_INVOKABLE bool setSkinningDisplayMode(const QString& mode);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Clarify the getter and setter return contracts.

The documentation says these methods “return the mode,” but setSkinningDisplayMode() returns bool and reports application success/failure. Document the getter as returning the mode and the setter as returning success.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/SkinWeightsController.h` around lines 53 - 60, Clarify the API
documentation near skinningDisplayMode() and setSkinningDisplayMode(): state
that skinningDisplayMode() returns the current mode for the first selected
entity, while setSkinningDisplayMode() returns a bool indicating whether the
requested mode was successfully applied.

The CI harness counts ANY skipped test as a suite failure —
CompareAgainstReference now passes as a no-op when
QTMESH_SKIN_OURS_FBX / QTMESH_SKIN_REF_FBX are unset.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ogre 14.x registers HardwareSkinningFactory as a ShaderGenerator
BUILT-IN (mBuiltinSRSFactories): our initialize() guard
(!getSingletonPtr()) therefore never fired — the bone cap and the
template SRS were never applied — while shutdown() deleted the built-in
factory that ShaderGenerator::destroy() then deleted again. The double
free segfaulted teardown of every suite that runs the full RTSS
lifecycle on CI (MainWindowTest, MCPServerTest, RTShaderHdrIblTest,
SceneSaveLoadTest, ...), failing unit-tests-linux with 7 crashed suites.

Follow the DualQuaternion sample instead: use the built-in factory,
set the bone cap, and add the template SRS to the generated scheme
(guarded by TYPE — addTemplateSubRenderState only dedupes by instance,
and tests re-run initialize()). Shutdown no longer touches the factory;
ShaderGenerator::destroy() owns it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@fernandotonon

Copy link
Copy Markdown
Owner Author

CI is green — 2418ed0 fixes the unit-tests-linux failure.

Root cause: Ogre 14.x registers HardwareSkinningFactory as a ShaderGenerator built-in (mBuiltinSRSFactories). The Slice D shutdown deleted that built-in, and ShaderGenerator::destroy() then deleted it again — the double free segfaulted teardown of every suite running the full RTSS lifecycle (7 crashed suites → executed-count gate failure). Meanwhile the matching initialize() block was guarded by !getSingletonPtr(), which was always false, so the bone cap and template SRS were silently never applied.

Now per the Ogre DualQuaternion sample: use the built-in factory, set the bone cap, add the template hardware-skinning SRS to the generated scheme (deduped by type — addTemplateSubRenderState only dedupes by instance and tests re-run initialize), and leave the factory alone at shutdown. Side effect: the DQS display toggle's SRS actually reaches generated techniques now.

🤖 Generated with Claude Code

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