Skip to content

Support Unreal Engine FBX imports: Z-up baking, clean animation names, preserve numeric suffixes#239

Merged
fernandotonon merged 3 commits into
masterfrom
fix/ue-animation-direction
Apr 2, 2026
Merged

Support Unreal Engine FBX imports: Z-up baking, clean animation names, preserve numeric suffixes#239
fernandotonon merged 3 commits into
masterfrom
fix/ue-animation-direction

Conversation

@fernandotonon
Copy link
Copy Markdown
Owner

@fernandotonon fernandotonon commented Apr 2, 2026

Summary

  • Z-up mesh baking: Vertex positions, normals, tangents/bitangents, and root skeleton bone transforms are now baked with R_x(+90°) at import time for Z-up FBX files (Unreal Engine exports). No more scene-node rotation hack — exported meshes are upright by default.
  • Animation-only FBX detection: UE retarget FBX files (no geometry, animations only) are auto-detected and offered for instant merge when a compatible skeletal mesh is already in the scene. Z-up→Y-up coordinate conversion is handled automatically during merge.
  • Clean animation names: cleanAnimNoise now strips "Unreal Take" (space variant) in addition to "unreal_take" (underscore). Animation names fall back to the source file name (e.g. mm_attack_03).
  • Preserve intentional numeric suffixes: deduplicateName no longer preemptively strips _N suffixes. mm_attack_03 stays mm_attack_03, not mm_attack. Stripping only happens when the name actually collides.
  • Model name from file path: modelName is always derived from the file path rather than scene->mName, which UE sets to generic values like "unreal_take".
  • Inspector auto-refresh: The inspector panel refreshes automatically after a successful animation merge.
  • Two new unit tests: MergeAnimationsUnrealTakeCleanup and MergeAnimationsNumericSuffixPreserved.
  • Version bump: 2.18.1 → 2.19.0
  • Docs updated: docs/index.html now documents the Unreal Engine FBX workflow alongside the Mixamo workflow.

Test plan

  • Import a UE skeletal mesh FBX → displays upright (no rotation applied to scene node)
  • Import a UE retarget animation-only FBX with a mesh selected → merge dialog appears → after merge, animation appears in inspector named after the source file (e.g. mm_attack_03)
  • Merging multiple UE animations preserves numeric suffixes (mm_attack_01, mm_attack_02, etc.)
  • Mixamo workflow unchanged: "mixamo.com" noise still cleaned, animations named correctly
  • CI unit tests pass: MergeAnimationsUnrealTakeCleanup, MergeAnimationsNumericSuffixPreserved

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Import and merge Unreal Engine FBX animations alongside Mixamo; auto-detect animation-only retarget FBX and handle coordinate system conversion.
    • Added a way to register a skeleton’s native up-axis so merges apply correct transforms.
  • Bug Fixes

    • Preserve intended numeric suffixes and avoid empty/unhelpful merged animation names.
    • Show merge failure warnings and refresh selection after merges.
  • Documentation

    • Updated docs and UI guidance to include Unreal Engine workflows.
  • Tests

    • Added tests for Unreal-name cleanup and numeric-suffix preservation.
  • Chores

    • Project version bumped to 2.19.0.

…meric suffixes

- Bake Z-up→Y-up into mesh vertex data and root skeleton bones instead of
  applying a scene-node rotation, so exported meshes are upright by default
- Animation-only FBX files (UE retargets with no geometry) are detected and
  offered for instant merge when a compatible mesh is already loaded
- Model name always derived from file path rather than scene->mName, which
  UE sets to generic values like "unreal_take"
- cleanAnimNoise now matches "Unreal Take" (space variant) in addition to
  "unreal_take" (underscore), so UE take names are stripped and the animation
  gets the source file name (e.g. "mm_attack_03")
- deduplicateName preserves intentional numeric suffixes (_03, _04, …) by
  only stripping trailing _N when the desired name already collides
- Inspector panel auto-refreshes after animation merge
- Add two new unit tests: UnrealTakeCleanup and NumericSuffixPreserved
- Bump version 2.18.1 → 2.19.0
- Update docs/index.html to document Unreal Engine FBX workflow

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 2, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ac908d46-0b99-42ce-af6e-a86121130f47

📥 Commits

Reviewing files that changed from the base of the PR and between 7221873 and ccad9fe.

📒 Files selected for processing (3)
  • src/Assimp/BoneProcessor.cpp
  • src/Assimp/BoneProcessor.h
  • src/Assimp/Importer.cpp
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/Assimp/BoneProcessor.h

📝 Walkthrough

Walkthrough

This PR bumps the project version, adds Unreal Engine references in docs, and implements skeleton up‑axis handling across import, mesh/bone processing, and animation merging (registration, Z-up→Y-up baking/rotation, translation conversion, and animation-name cleanup).

Changes

Cohort / File(s) Summary
Version Update
CMakeLists.txt
Project version changed from 2.18.12.19.0.
Documentation
docs/index.html
Hero, meta, and feature text updated to include Unreal Engine; workflows and GUI notes expanded for Unreal retargeted FBX and Z-up/Y-up handling.
Animation Merger Core
src/AnimationMerger.h, src/AnimationMerger.cpp
Added registerSkeletonUpAxis() and internal up‑axis registry; renamed cleanMixamoNoise()cleanAnimNoise() to also strip UE FBX tokens; deduplicateName() preserves unique numeric suffixes; mergeAnimationsByName(...) now accepts srcUpAxis/baseUpAxis, applies per‑bone binding‑pose orientation correction, and converts Z-up translations to Y-up.
Animation Merger Tests
src/AnimationMerger_test.cpp
Added tests MergeAnimationsUnrealTakeCleanup and MergeAnimationsNumericSuffixPreserved validating Unreal take cleanup and numeric-suffix preservation.
Assimp Bone/Animation Processing
src/Assimp/BoneProcessor.h, src/Assimp/BoneProcessor.cpp, src/Assimp/AnimationProcessor.cpp
Added BoneProcessor::bakeZupToYup() to rotate root bones +90° X; AnimationProcessor applies bone T‑pose inverse rotation to translation keys. Note: new stray brace reported near processBones insertion point.
Assimp Mesh Processing
src/Assimp/MeshProcessor.h, src/Assimp/MeshProcessor.cpp
MeshProcessor gains isZup flag (constructor changed) and rotates vertex positions, normals, and tangent-space vectors by +90° X when isZup.
Assimp Importer
src/Assimp/Importer.cpp
Model name now derived from filename fallback; detects isZup from scene up-axis; conditionally bakes Z-up into skeletons when isZup && !animationOnly; passes isZup to MeshProcessor and uses updated bake timing.
Mesh Import/Export Integration
src/MeshImporterExporter.cpp
Registers each loaded skeleton’s up-axis with AnimationMerger::registerSkeletonUpAxis() (hardcoded Y-up for .mesh/.xml, detected for Assimp imports); removed prior display-only scene-node rotation.
UI
src/mainwindow.cpp
importMeshs captures mergeAnimations() return, warns on null merge, and re-selects baseEntity on success to refresh the inspector.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant MainWindow
    participant MeshImporterExporter
    participant AssimpImporter
    participant BoneProcessor
    participant MeshProcessor
    participant AnimationMerger

    User->>MainWindow: Import FBX (animation-only or mesh)
    MainWindow->>MeshImporterExporter: loadModel(path)
    MeshImporterExporter->>AssimpImporter: parse file
    AssimpImporter->>BoneProcessor: create skeleton (isZup = sceneUpAxis==2)
    Note right of BoneProcessor: if isZup → rotate root rest poses +90° X
    AssimpImporter->>MeshProcessor: process meshes (isZup)
    Note right of MeshProcessor: if isZup → rotate vertices/normals/tangents +90° X
    AssimpImporter-->>MeshImporterExporter: skeleton/mesh created (upAxis)
    MeshImporterExporter->>AnimationMerger: registerSkeletonUpAxis(name, upAxis)
    AnimationMerger-->>MeshImporterExporter: stored mapping

    User->>MainWindow: Merge animations into base entity
    MainWindow->>AnimationMerger: mergeAnimations(base, src, srcUpAxis, baseUpAxis)
    AnimationMerger->>AnimationMerger: lookup upAxis for skeletons
    AnimationMerger->>AnimationMerger: apply binding-pose orientation correction
    AnimationMerger->>AnimationMerger: if srcUp==Z and baseUp==Y → convert translations (x,z,-y)
    AnimationMerger-->>MainWindow: merged animations (or null + err)
    MainWindow->>MainWindow: re-select baseEntity to refresh inspector
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I hopped through code, a tiny sleuth,

Z flipped to Y with careful tooth.
Skeletons now know which way is up,
Names cleaned tidy, no rogue “unreal_take” cup.
Merge complete—animations spring and hop!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.92% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main changes: Z-up baking for Unreal Engine FBX imports, animation name cleanup, and numeric suffix preservation are all core features introduced in this PR.
Description check ✅ Passed The description provides comprehensive coverage with clear Summary and Technical Details sections. It documents all major features, bugfixes, test plan, version bump, and documentation updates with sufficient detail.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 fix/ue-animation-direction

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.

Copy link
Copy Markdown

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

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: 206cd58f4c

ℹ️ 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/Assimp/Importer.cpp Outdated
// Only bake Z-up into mesh skeletons. Animation-only files stay in their native
// space so AnimationMerger's needsZupToYup path can apply the single correct
// coordinate conversion when merging into the already-baked mesh skeleton.
boneProcessor.processBones(skeleton, scene, isZup && !animationOnly);
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 Avoid baking bind pose before importing embedded Z-up keys

AssimpToOgreImporter::loadModel now bakes Z-up root rest transforms via processBones(..., isZup && !animationOnly) and then immediately imports animation channels, but AnimationProcessor::processAnimationChannel still consumes keyframe positions in the file’s original coordinate basis. For Z-up files that contain embedded animations, this creates a bind/key basis mismatch, so playback gets axis-swapped offsets (notably root translation/jump motion) even though the mesh is upright.

Useful? React with 👍 / 👎.

Comment thread src/AnimationMerger.cpp
// corrected = (q_dst⁻¹ * q_src) * stored
const Ogre::Quaternion q_src = srcSkel->getBone(srcHandle)->getOrientation();
const Ogre::Quaternion q_dst = baseSkel->getBone(baseHandle)->getOrientation();
const Ogre::Quaternion boneCorrection = q_src.Inverse() * q_dst;
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 Use destination-to-source quaternion order for bone correction

The translation-space remap in mergeAnimationsByName computes boneCorrection as q_src.Inverse() * q_dst, but converting a vector from source bone-local space into destination bone-local space requires q_dst.Inverse() * q_src (which also matches the nearby comment). With the current order, merges where bind orientations differ rotate translation deltas in the wrong direction, producing incorrect root/track motion for orientation-mismatched skeleton pairs.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
src/AnimationMerger.cpp (1)

10-20: Thread-safety consideration for the global registry.

s_skeletonUpAxis is a global QMap without synchronization. This is safe as long as registration (via registerSkeletonUpAxis) and lookup (via lookupUpAxis) are only called from the main/UI thread. If there's any possibility of concurrent imports or merges from background threads in the future, consider using QReadWriteLock or std::shared_mutex.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/AnimationMerger.cpp` around lines 10 - 20, s_skeletonUpAxis is an
unsynchronized global QMap accessed by registerSkeletonUpAxis and lookupUpAxis;
protect concurrent access by adding a shared/read-write lock (e.g.,
QReadWriteLock or std::shared_mutex) around all reads and writes: acquire write
lock in AnimationMerger::registerSkeletonUpAxis when modifying s_skeletonUpAxis
and acquire read lock in lookupUpAxis when reading from s_skeletonUpAxis
(preserve the default value behavior). Ensure the lock object is also a static
global alongside s_skeletonUpAxis so both functions use the same lock.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/AnimationMerger.cpp`:
- Around line 152-189: The bone correction quaternion is computed with the
operands reversed; update the computation of boneCorrection (currently using
q_src.Inverse() * q_dst) to use the correct order q_dst.Inverse() * q_src so
that stored source-local vectors are re-expressed into the destination
bone-local space; change the assignment where boneCorrection is created (refer
to q_src, q_dst, boneCorrection, and needsBoneCorrection) accordingly and
add/adjust a unit test that uses non-identity binding-pose orientations to catch
regressions.

In `@src/Assimp/Importer.cpp`:
- Around line 114-122: The root-bake (Z-up) is being applied too early in
BoneProcessor::processBones which mutates the skeleton before
AnimationProcessor::processAnimations reads T-pose; either move the Z-up bake to
run after AnimationProcessor::processAnimations() but before capturing the final
binding pose, or instead apply the inverse rotation to raw root keys inside
AnimationProcessor so their deltas are computed against the original FBX basis;
specifically, stop calling BoneProcessor::processBones(...) with isZup &&
!animationOnly before animations are processed and re-invoke the bake afterward
(or rotate root keyframes by R_x(+90°) prior to converting them to Ogre deltas)
so AnimationProcessor uses the correct T-pose basis.

In `@src/mainwindow.cpp`:
- Around line 720-725: The current branch only inspects errMsg but must treat
AnimationMerger::mergeAnimations()'s return value as the real success indicator:
capture the returned merged entity (result of mergeAnimations()), and if that
pointer is null treat it as a failure and show the QMessageBox::warning (using
errMsg or a fallback message); only when the returned merged entity is non-null
proceed to re-select (SelectionSet::getSingleton()->append(baseEntity)) so the
inspector refreshes correctly.

---

Nitpick comments:
In `@src/AnimationMerger.cpp`:
- Around line 10-20: s_skeletonUpAxis is an unsynchronized global QMap accessed
by registerSkeletonUpAxis and lookupUpAxis; protect concurrent access by adding
a shared/read-write lock (e.g., QReadWriteLock or std::shared_mutex) around all
reads and writes: acquire write lock in AnimationMerger::registerSkeletonUpAxis
when modifying s_skeletonUpAxis and acquire read lock in lookupUpAxis when
reading from s_skeletonUpAxis (preserve the default value behavior). Ensure the
lock object is also a static global alongside s_skeletonUpAxis so both functions
use the same lock.
🪄 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: 759410d6-bec5-4443-b826-5a6dfe6cd624

📥 Commits

Reviewing files that changed from the base of the PR and between ddd050e and 206cd58.

📒 Files selected for processing (13)
  • CMakeLists.txt
  • docs/index.html
  • src/AnimationMerger.cpp
  • src/AnimationMerger.h
  • src/AnimationMerger_test.cpp
  • src/Assimp/AnimationProcessor.cpp
  • src/Assimp/BoneProcessor.cpp
  • src/Assimp/BoneProcessor.h
  • src/Assimp/Importer.cpp
  • src/Assimp/MeshProcessor.cpp
  • src/Assimp/MeshProcessor.h
  • src/MeshImporterExporter.cpp
  • src/mainwindow.cpp

Comment thread src/AnimationMerger.cpp
Comment on lines +152 to +189
// Per-bone binding-pose orientation correction.
// AnimationProcessor stores translate/rotate keyframes in the source bone's
// local space (divided by the source bone's binding-pose orientation).
// If source and target have different binding poses for this bone, re-express
// the stored values in the target bone's local space:
// corrected = (q_dst⁻¹ * q_src) * stored
const Ogre::Quaternion q_src = srcSkel->getBone(srcHandle)->getOrientation();
const Ogre::Quaternion q_dst = baseSkel->getBone(baseHandle)->getOrientation();
const Ogre::Quaternion boneCorrection = q_src.Inverse() * q_dst;
const bool needsBoneCorrection = !boneCorrection.equals(Ogre::Quaternion::IDENTITY, Ogre::Radian(1e-4f));

for (unsigned short k = 0; k < srcTrack->getNumKeyFrames(); ++k)
{
const auto* kf = srcTrack->getNodeKeyFrame(k);
auto* dstKf = dstTrack->createNodeKeyFrame(kf->getTime());
dstKf->setTranslate(kf->getTranslate());
dstKf->setRotation(kf->getRotation());

Ogre::Vector3 t = kf->getTranslate();
Ogre::Quaternion r = kf->getRotation();

// Step 1: correct translation for binding-pose orientation mismatch.
// Translations are stored in bone-local space (AnimationProcessor divides by
// the source bone's binding-pose orientation). If the target bone has a
// different binding-pose orientation, re-express the vector in that space.
// Rotations do NOT need this correction — the target binding pose already
// provides the equivalent compensation via the scene-node/skeleton setup.
if (needsBoneCorrection) {
t = boneCorrection * t;
}
// Step 2: convert coordinate system (Z-up source → Y-up base).
// R_x(-90°) maps (x,y,z) → (x, z, -y): Z-up axis becomes Y-up.
// Rotations pass through unchanged — bone-local storage makes them
// self-consistent once the dest binding pose is baked to Y-up.
if (needsZupToYup) {
t = Ogre::Vector3(t.x, t.z, -t.y);
}

dstKf->setTranslate(t);
dstKf->setRotation(r);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for any existing tests or usages that might validate this formula
rg -n "boneCorrection|q_src|q_dst" --type cpp -C3

Repository: fernandotonon/QtMeshEditor

Length of output: 3230


🏁 Script executed:

#!/bin/bash
# Search for test files related to AnimationMerger
fd -e cpp -e h -path "*test*" | head -20

Repository: fernandotonon/QtMeshEditor

Length of output: 238


🏁 Script executed:

#!/bin/bash
# Search for all AnimationMerger-related files and their sizes
fd -i animationmerger | xargs wc -l

Repository: fernandotonon/QtMeshEditor

Length of output: 174


🏁 Script executed:

#!/bin/bash
# Look at the full context around line 114 to understand the function-level comment
sed -n '100,130p' src/AnimationMerger.cpp

Repository: fernandotonon/QtMeshEditor

Length of output: 1926


🏁 Script executed:

#!/bin/bash
# Check if there's any validation or actual usage of the boneCorrection in the merge
sed -n '140,200p' src/AnimationMerger.cpp

Repository: fernandotonon/QtMeshEditor

Length of output: 3340


🏁 Script executed:

#!/bin/bash
# Examine the test file for bone correction validation
head -100 src/AnimationMerger_test.cpp

Repository: fernandotonon/QtMeshEditor

Length of output: 3751


🏁 Script executed:

#!/bin/bash
# Search for test cases related to bone correction or quaternion validation
rg -n "boneCorrection|mergeAnimationsByName|TEST|test" src/AnimationMerger_test.cpp | head -40

Repository: fernandotonon/QtMeshEditor

Length of output: 1019


🏁 Script executed:

#!/bin/bash
# Read the MergeAnimationsBasic test and related tests to understand validation
sed -n '136,220p' src/AnimationMerger_test.cpp

Repository: fernandotonon/QtMeshEditor

Length of output: 3640


🏁 Script executed:

#!/bin/bash
# Also check the MergeAnimationsMixamoCleanup test which mentions "Mixamo" (likely Y-up with binding pose differences)
sed -n '219,270p' src/AnimationMerger_test.cpp

Repository: fernandotonon/QtMeshEditor

Length of output: 2431


🏁 Script executed:

#!/bin/bash
# Check if there are any comments in the test file explaining bone correction validation
rg -n "binding.pose|correction|q_src|q_dst|orientation" src/AnimationMerger_test.cpp

Repository: fernandotonon/QtMeshEditor

Length of output: 52


🏁 Script executed:

#!/bin/bash
# Check if createTestSkeleton ever sets non-identity bone orientations
sed -n '70,100p' src/AnimationMerger_test.cpp

Repository: fernandotonon/QtMeshEditor

Length of output: 1278


🏁 Script executed:

#!/bin/bash
# Search for any test that might set quaternion or orientation values
rg -n "setOrientation|Quaternion|R_x" src/AnimationMerger_test.cpp

Repository: fernandotonon/QtMeshEditor

Length of output: 52


🏁 Script executed:

#!/bin/bash
# Check if there are any integration tests or test FBX files mentioned
rg -n "\.fbx|test.*FBX|integration" src/AnimationMerger_test.cpp

Repository: fernandotonon/QtMeshEditor

Length of output: 52


🏁 Script executed:

#!/bin/bash
# Double-check by reading the full function to see if there's any other context
sed -n '107,165p' src/AnimationMerger.cpp

Repository: fernandotonon/QtMeshEditor

Length of output: 3481


Fix the quaternion order in the bone correction formula.

Line 157's comment states the correct formula: corrected = (q_dst⁻¹ * q_src) * stored. Line 114's docstring also correctly states: correction = q_dst.Inverse() * q_src. However, line 160 implements the inverse: boneCorrection = q_src.Inverse() * q_dst.

The stored values are in the source bone's local space (pre-multiplied by q_src⁻¹). Converting to the destination bone's local space requires: v_dest_local = q_dst⁻¹ * (q_src * v_stored) = (q_dst⁻¹ * q_src) * v_stored.

Change line 160 to:

const Ogre::Quaternion boneCorrection = q_dst.Inverse() * q_src;

The current test suite does not catch this because all test skeletons use identity binding poses. This bug manifests only when merging animations between skeletons with different binding-pose orientations (e.g., Unreal Engine FBX with R_x(-90°) root merged into a Mixamo character). Recommend adding a test case with non-identity binding poses.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/AnimationMerger.cpp` around lines 152 - 189, The bone correction
quaternion is computed with the operands reversed; update the computation of
boneCorrection (currently using q_src.Inverse() * q_dst) to use the correct
order q_dst.Inverse() * q_src so that stored source-local vectors are
re-expressed into the destination bone-local space; change the assignment where
boneCorrection is created (refer to q_src, q_dst, boneCorrection, and
needsBoneCorrection) accordingly and add/adjust a unit test that uses
non-identity binding-pose orientations to catch regressions.

Comment thread src/Assimp/Importer.cpp Outdated
Comment thread src/mainwindow.cpp Outdated
fernandotonon and others added 2 commits April 2, 2026 10:20
- mainwindow.cpp: check mergeAnimations() return value (not just errMsg)
  as the authoritative success signal, per Codex review suggestion
- AnimationMerger.cpp: update boneCorrection comment to match actual
  formula (q_src⁻¹ * q_dst) used in code

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
BoneProcessor::processBones no longer takes an isZup parameter.
Instead, a new BoneProcessor::bakeZupToYup() static method is called
from Importer.cpp *after* AnimationProcessor::processAnimations() so
that animation deltas are computed against the original (pre-bake)
T-pose. Baking before animation processing caused a basis mismatch for
Z-up FBX files with embedded animations: AnimationProcessor would read
the already-rotated bone orientations as its T-pose reference while
keyframe data was still in Z-up space, leading to axis-swapped root
motion on playback.

Addresses PR #239 review comment (Codex P1 / CodeRabbit major).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented Apr 2, 2026

@fernandotonon fernandotonon merged commit ca8c679 into master Apr 2, 2026
18 checks passed
@fernandotonon fernandotonon deleted the fix/ue-animation-direction branch April 2, 2026 20:28
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