From 4e16c3f5bd352d779ce8319a9305f504dacb48d3 Mon Sep 17 00:00:00 2001 From: tinqs-limited Date: Mon, 18 May 2026 20:01:15 +0100 Subject: [PATCH] fix(import): skeleton rest fixer animation conversion in headless mode The %GeneralSkeleton unique node resolution via get_node() fails during headless import (--import flag), causing ALL animation tracks to be silently skipped by ERR_CONTINUE. Rest poses get overwritten but animation keyframe values stay in the old coordinate system, producing incorrect bone orientations (e.g., arms crossed behind back). Fix: fall back to find_child() when unique name resolution fails. Added extensive logging for retarget debugging. Tinqs fork v1.0.1 Co-Authored-By: Claude Opus 4.6 (1M context) --- ...post_import_plugin_skeleton_rest_fixer.cpp | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/editor/import/3d/post_import_plugin_skeleton_rest_fixer.cpp b/editor/import/3d/post_import_plugin_skeleton_rest_fixer.cpp index 1e15b16d21..2e3e226c34 100644 --- a/editor/import/3d/post_import_plugin_skeleton_rest_fixer.cpp +++ b/editor/import/3d/post_import_plugin_skeleton_rest_fixer.cpp @@ -730,13 +730,34 @@ void PostImportPluginSkeletonRestFixer::internal_process(InternalImportCategory } String track_path = String(anim->track_get_path(i).get_concatenated_names()); - Node *node = (ap->get_node(ap->get_root_node()))->get_node(NodePath(track_path)); - ERR_CONTINUE(!node); + + // [TINQS] Fix: %UniqueNode resolution fails in headless/import context. + // Fall back to find_child when get_node fails (e.g., headless --import). + Node *node = nullptr; + Node *anim_root = ap->get_node_or_null(ap->get_root_node()); + if (anim_root) { + node = anim_root->get_node_or_null(NodePath(track_path)); + } + if (!node && track_path.begins_with(UNIQUE_NODE_PREFIX)) { + // Unique name resolution failed — try direct child search. + String skel_name = track_path.substr(1); // Strip % prefix. + node = p_base_scene->find_child(skel_name, true, false); + if (node) { + print_verbose(vformat("[RetargetRestFixer] Resolved '%s' via find_child fallback -> '%s'", track_path, node->get_name())); + } else { + WARN_PRINT(vformat("[RetargetRestFixer] Failed to resolve skeleton node '%s' — animation keyframes will NOT be converted to new rest pose. This may cause incorrect bone orientations.", track_path)); + } + } + if (!node) { + WARN_PRINT(vformat("[RetargetRestFixer] Skipping track %d: cannot resolve node path '%s' from AnimationPlayer root.", i, track_path)); + continue; + } Skeleton3D *track_skeleton = Object::cast_to(node); if (!track_skeleton || (is_using_modifier && track_skeleton != profile_skeleton && track_skeleton != orig_skeleton) || (!is_using_modifier && track_skeleton != src_skeleton)) { + print_verbose(vformat("[RetargetRestFixer] Skipping track %d: skeleton mismatch (found '%s', expected src_skeleton).", i, node->get_name())); continue; } @@ -746,6 +767,7 @@ void PostImportPluginSkeletonRestFixer::internal_process(InternalImportCategory } int bone_idx = src_skeleton->find_bone(bn); + print_verbose(vformat("[RetargetRestFixer] Converting track %d bone '%s' (idx=%d) — %d keys", i, bn, bone_idx, anim->track_get_key_count(i))); if (is_using_modifier) { int prof_idx = profile->find_bone(bn);