From 90239b7051e164e9b8a5f0c08cd3512cbfe4db62 Mon Sep 17 00:00:00 2001 From: Hector <145347438+hudomn@users.noreply.github.com> Date: Wed, 25 Mar 2026 08:56:20 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=A8=E7=94=BB=E5=AD=98=E5=9C=A8=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- imgui.ini | 26 ++--- ui/panels/animation_tools.py | 200 ++++++++++++++++++++++++++++++++--- ui/panels/app_actions.py | 7 ++ 3 files changed, 207 insertions(+), 26 deletions(-) diff --git a/imgui.ini b/imgui.ini index 1159ccb0..15b27504 100644 --- a/imgui.ini +++ b/imgui.ini @@ -31,21 +31,21 @@ DockId=0x0000000D,0 [Window][场景树] Pos=0,20 -Size=339,1084 +Size=339,611 Collapsed=0 -DockId=0x00000007,0 +DockId=0x00000003,0 [Window][属性面板] -Pos=1694,20 -Size=346,1084 +Pos=1574,20 +Size=346,1012 Collapsed=0 DockId=0x00000002,0 [Window][控制台] -Pos=341,705 -Size=1351,399 +Pos=0,633 +Size=339,399 Collapsed=0 -DockId=0x00000006,1 +DockId=0x00000004,0 [Window][脚本管理] Pos=1950,20 @@ -59,7 +59,7 @@ Collapsed=0 [Window][WindowOverViewport_11111111] Pos=0,20 -Size=2040,1084 +Size=1920,1012 Collapsed=0 [Window][测试窗口1] @@ -98,8 +98,8 @@ Size=600,500 Collapsed=0 [Window][资源管理器] -Pos=341,705 -Size=1351,399 +Pos=341,633 +Size=1231,399 Collapsed=0 DockId=0x00000006,0 @@ -226,9 +226,11 @@ Size=460,260 Collapsed=0 [Docking][Data] -DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=2040,1084 Split=X +DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=1920,1012 Split=X DockNode ID=0x00000001 Parent=0x08BD597D SizeRef=2212,1012 Split=X - DockNode ID=0x00000007 Parent=0x00000001 SizeRef=339,1084 Selected=0xE0015051 + DockNode ID=0x00000007 Parent=0x00000001 SizeRef=339,1084 Split=Y Selected=0xE0015051 + DockNode ID=0x00000003 Parent=0x00000007 SizeRef=339,611 Selected=0xE0015051 + DockNode ID=0x00000004 Parent=0x00000007 SizeRef=339,399 Selected=0x5428E753 DockNode ID=0x00000008 Parent=0x00000001 SizeRef=1871,1084 Split=Y DockNode ID=0x00000005 Parent=0x00000008 SizeRef=2048,683 Split=Y DockNode ID=0x0000000D Parent=0x00000005 SizeRef=1318,383 HiddenTabBar=1 Selected=0x43A39006 diff --git a/ui/panels/animation_tools.py b/ui/panels/animation_tools.py index 60ac5f16..6429748d 100644 --- a/ui/panels/animation_tools.py +++ b/ui/panels/animation_tools.py @@ -686,6 +686,101 @@ class AnimationTools: except Exception: return None + def _find_animation_driver_node(self, node): + """Find the concrete node that should drive animation playback.""" + try: + if not node or node.isEmpty(): + return None + + cached_driver = node.getPythonTag("animation_driver_node") + if cached_driver and (not cached_driver.isEmpty()): + return cached_driver + except Exception: + pass + + try: + candidates = [] + + def add_candidate(candidate): + try: + if not candidate or candidate.isEmpty(): + return + for existing in candidates: + if existing == candidate: + return + candidates.append(candidate) + except Exception: + return + + add_candidate(node) + + current = node + for _ in range(32): + if not current or current.isEmpty() or self._is_scene_root_node(current): + break + add_candidate(current) + try: + for child in current.getChildren(): + add_candidate(child) + except Exception: + pass + parent = current.getParent() + if not parent or parent.isEmpty() or parent == current: + break + current = parent + + best = None + best_score = -1 + for candidate in candidates: + try: + has_anim = self._node_has_animation_nodes(candidate) + if not has_anim: + continue + has_geom = self._node_has_geom(candidate) + character_count = candidate.findAllMatches("**/+Character").getNumPaths() + bundle_count = candidate.findAllMatches("**/+AnimBundleNode").getNumPaths() + is_character = character_count > 0 + is_bundle = bundle_count > 0 + is_same_node = candidate == node + is_descendant = False + is_ancestor = False + try: + if candidate != node: + is_descendant = node.isAncestorOf(candidate) + is_ancestor = candidate.isAncestorOf(node) + except Exception: + is_descendant = False + is_ancestor = False + score = 0 + score += 140 if has_anim and has_geom else 0 + score += 90 if has_geom else 0 + score += 80 if is_character else 0 + score += 40 if is_bundle else 0 + score += 35 if is_descendant else 0 + score += 15 if is_same_node else 0 + score -= 20 if is_ancestor else 0 + score += character_count * 5 + score += bundle_count * 3 + if score > best_score: + best_score = score + best = candidate + except Exception: + continue + + if best: + try: + node.setPythonTag("animation_driver_node", best) + except Exception: + pass + try: + best.setPythonTag("animation_driver_node", best) + except Exception: + pass + return best + except Exception: + pass + return None + def _recover_model_path_from_tags(self, node): """从常见标签恢复模型路径,尽量避免退化到纯内存 autoBind。""" try: @@ -905,6 +1000,16 @@ class AnimationTools: def _resolve_animation_owner_model(self, node): """向上查找动画所属的模型根节点,避免选中子节点时绑定失败。""" + try: + if node and (not node.isEmpty()): + # If the user explicitly selected the animated skeleton node, + # respect that directly. More aggressive driver remapping made + # playback regress to "cannot play at all". + if self._node_has_animation_nodes(node): + return node + except Exception: + pass + ssbo_owner = self._resolve_ssbo_source_owner(node) if ssbo_owner and not ssbo_owner.isEmpty(): try: @@ -1431,6 +1536,13 @@ class AnimationTools: except Exception: has_animation_nodes = False + filepath = owner_model.getTag("model_path") if owner_model.hasTag("model_path") else "" + if not filepath and owner_model.hasTag("original_path"): + filepath = owner_model.getTag("original_path") + + lower_filepath = str(filepath).lower() if filepath else "" + is_gltf_family = lower_filepath.endswith(".glb") or lower_filepath.endswith(".gltf") + def _try_memory_fallback(): def _collect_autobind_source_candidates(): candidates = [] @@ -1554,6 +1666,34 @@ class AnimationTools: return mem_proxy return None + if is_gltf_family and has_animation_nodes: + # For imported GLTF/GLB, prefer binding controls directly onto the + # visible scene model. A separate runtime Armature proxy can report + # playing=True while not driving the visible mesh the user selected. + direct_proxy = _try_create_autobind_proxy( + owner_model, + f"当前模型({owner_model.getName()})", + owns_node=False, + ) + if direct_proxy: + self._mark_runtime_animation_node(direct_proxy, owner_model) + self._actor_cache[owner_model] = direct_proxy + owner_model.setTag("has_animations", "true") + return direct_proxy + + if filepath: + actor = _try_create_actor_via_gltf_path(filepath) + if actor: + owner_model.setTag("model_path", filepath) + owner_model.setTag("has_animations", "true") + self._mark_runtime_animation_node(actor, owner_model) + self._actor_cache[owner_model] = actor + return actor + + actor = _try_memory_fallback() + if actor: + return actor + # 始终优先尝试从文件路径加载,因为底层 gltf 插件只有在加载文件时才能抽取完整的动画名称。 self._anim_log(f"[Actor加载调试] 传入的 origin_model: {origin_model.getName() if origin_model else 'None'}") self._anim_log(f"[Actor加载调试] origin_model tags: {origin_model.getTags() if origin_model else 'None'}") @@ -1564,10 +1704,6 @@ class AnimationTools: except Exception as e: self._anim_log(f"[Actor加载调试] 获取 tags 异常: {e}") - filepath = owner_model.getTag("model_path") if owner_model.hasTag("model_path") else "" - if not filepath and owner_model.hasTag("original_path"): - filepath = owner_model.getTag("original_path") - self._anim_log(f"[Actor加载调试] 获取到的 filepath: '{filepath}'") if not filepath: self._anim_log(f"[Actor加载调试] filepath为空,触发 _try_memory_fallback()") @@ -1622,14 +1758,24 @@ class AnimationTools: for p in load_paths: self._anim_log(f"[Actor加载验证] 正在尝试通过路径读取骨骼和动画文件: {p}") - actor = _try_create_actor_from_source(p, f"文件路径({p})") - if actor: - owner_model.setTag("model_path", p) - owner_model.setTag("has_animations", "true") - self._actor_cache[owner_model] = actor - return actor + lower_p = str(p).lower() + + if lower_p.endswith(".glb") or lower_p.endswith(".gltf"): + actor = _try_create_actor_via_gltf_path(p) + if actor: + owner_model.setTag("model_path", p) + owner_model.setTag("has_animations", "true") + self._mark_runtime_animation_node(actor, owner_model) + self._actor_cache[owner_model] = actor + return actor + else: + actor = _try_create_actor_from_source(p, f"文件路径({p})") + if actor: + owner_model.setTag("model_path", p) + owner_model.setTag("has_animations", "true") + self._actor_cache[owner_model] = actor + return actor - # 标准 Actor 路径失败时,针对 GLTF/GLB 走插件加载兜底 actor = _try_create_actor_via_gltf_path(p) if actor: owner_model.setTag("model_path", p) @@ -1638,7 +1784,9 @@ class AnimationTools: self._actor_cache[owner_model] = actor return actor - # 路径 Actor 失败后,再尝试把文件作为普通模型加载并 autoBind + if is_gltf_family: + continue + try: model_source = p if isinstance(p, (str, os.PathLike)): @@ -1831,6 +1979,8 @@ class AnimationTools: pass valid_names = [] + best_name = None + best_frames = -1 for anim_name in anim_names: try: control = actor.getAnimControl(anim_name) @@ -1838,12 +1988,17 @@ class AnimationTools: control = None if not control: continue + frame_count = -1 try: - if control.getNumFrames() <= 1: + frame_count = control.getNumFrames() + if frame_count <= 1: continue except Exception: pass valid_names.append(anim_name) + if frame_count > best_frames: + best_frames = frame_count + best_name = anim_name if not valid_names: valid_names = list(anim_names) @@ -1855,7 +2010,7 @@ class AnimationTools: break if current_anim not in valid_names: - current_anim = valid_names[0] + current_anim = best_name or valid_names[0] try: origin_model.setPythonTag("selected_animation", current_anim) @@ -2078,6 +2233,18 @@ class AnimationTools: is_scene_bound_proxy = self._sync_actor_transform_for_playback(owner_model, actor) + debug_actor_name = getattr(actor, "getName", None) + try: + debug_actor_name = debug_actor_name() if callable(debug_actor_name) else getattr(getattr(actor, "_node", None), "getName", lambda: "Unknown")() + except Exception: + debug_actor_name = "Unknown" + + debug_frames = -1 + try: + debug_frames = control.getNumFrames() + except Exception: + pass + if self._can_swap_actor_owner_visibility(owner_model, actor): self._ensure_owner_hidden_lock_task(owner_model, enabled=True) self._apply_actor_owner_visibility(owner_model, actor, prefer_actor_visible=True) @@ -2093,6 +2260,11 @@ class AnimationTools: except Exception: pass actor.play(current_anim) + try: + is_playing = control.isPlaying() + except Exception: + is_playing = "unknown" + print(f"[动画调试] owner={owner_model.getName()} actor={debug_actor_name} anim={current_anim} frames={debug_frames} playing={is_playing} scene_proxy={is_scene_bound_proxy}") print(f"『动画播放』:{current_anim}") def _pauseAnimation(self, origin_model): diff --git a/ui/panels/app_actions.py b/ui/panels/app_actions.py index fecf149e..3debc1ba 100644 --- a/ui/panels/app_actions.py +++ b/ui/panels/app_actions.py @@ -1541,6 +1541,8 @@ class AppActions: Legacy mode: load via SceneManager. """ animated_model = bool(file_path and self._model_file_has_animation(file_path)) + lower_path = str(file_path).lower() if file_path else "" + is_gltf_family = lower_path.endswith((".glb", ".gltf")) if animated_model: prefer_scene_manager = True ssbo_editor = getattr(self, 'ssbo_editor', None) @@ -1550,6 +1552,11 @@ class AppActions: except Exception: pass + if is_gltf_family: + fallback_model = self._import_model_via_gltf_fallback(file_path) + if fallback_model: + return fallback_model + if self.use_ssbo_mouse_picking and getattr(self, 'ssbo_editor', None): if animated_model: print(f"[AnimationImport] 检测到动画模型,跳过SSBO导入: {file_path}")