From 2822dfee7114f005097729c5738313333b46d0c2 Mon Sep 17 00:00:00 2001 From: Hector <145347438+hudomn@users.noreply.github.com> Date: Thu, 19 Mar 2026 15:17:30 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=92=AD=E6=94=BE=E5=8A=A8?= =?UTF-8?q?=E7=94=BB=E5=9C=BA=E6=99=AF=E5=8D=A1=E9=A1=BF=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui/panels/animation_tools.py | 256 +++++++++++++++++-------------- ui/panels/editor_panels_right.py | 32 +++- 2 files changed, 163 insertions(+), 125 deletions(-) diff --git a/ui/panels/animation_tools.py b/ui/panels/animation_tools.py index 5c3cda7c..586c6377 100644 --- a/ui/panels/animation_tools.py +++ b/ui/panels/animation_tools.py @@ -118,6 +118,102 @@ class AnimationTools: else: setattr(self.app, name, value) + def _ensure_animation_runtime_state(self): + if not hasattr(self, "_animation_sync_tasks") or self._animation_sync_tasks is None: + self._animation_sync_tasks = {} + if not hasattr(self, "_animation_debug_enabled"): + self._animation_debug_enabled = False + + def _anim_log(self, message): + self._ensure_animation_runtime_state() + if self._animation_debug_enabled: + print(message) + + def _acquire_playback_actor(self, origin_model, owner_model, force_rebuild=False): + """ + 获取用于播放的 Actor。 + 默认优先复用缓存,只有缓存失效或明确要求时才重建,避免点击播放时阻塞。 + """ + self._ensure_animation_runtime_state() + + if force_rebuild: + self._cleanup_conflicting_animation_actors(owner_model) + self._discard_cached_actor(owner_model, restore_owner=True) + + actor = self._getActor(owner_model) + if not actor: + return None + + # 若仍在“无路径的内存代理”,仅在确实恢复出路径后重建一次真实 Actor。 + if isinstance(actor, _BoundAnimationProxy): + try: + owner_has_path = owner_model.hasTag("model_path") and bool(owner_model.getTag("model_path")) + except Exception: + owner_has_path = False + + if not owner_has_path: + recovered_path = self._recover_model_path_from_tags(owner_model) or self._recover_model_path_from_tags(origin_model) + if recovered_path: + try: + owner_model.setTag("model_path", recovered_path) + if (not owner_model.hasTag("original_path")) or (not owner_model.getTag("original_path")): + owner_model.setTag("original_path", recovered_path) + except Exception: + pass + self._clear_animation_cache(owner_model) + actor = self._getActor(owner_model) + return actor + + def _sync_actor_transform_for_playback(self, owner_model, actor): + is_scene_bound_proxy = isinstance(actor, _BoundAnimationProxy) and (not getattr(actor, "_owns_node", False)) + if is_scene_bound_proxy: + return True + + actor.setPos(self.render, owner_model.getPos(self.render)) + actor.setHpr(self.render, owner_model.getHpr(self.render)) + actor.setScale(self.render, owner_model.getScale(self.render)) + + actor.setPos(owner_model.getPos()) + actor.setHpr(owner_model.getHpr()) + actor.setScale(owner_model.getScale()) + return False + + def _ensure_actor_sync_task(self, owner_model, actor, enabled): + self._ensure_animation_runtime_state() + + old_task_name = self._animation_sync_tasks.get(owner_model) + if old_task_name and old_task_name != f"maintain_anim_pos_{id(actor)}": + try: + taskMgr.remove(old_task_name) + except Exception: + pass + + if not enabled: + self._animation_sync_tasks.pop(owner_model, None) + return + + task_name = f"maintain_anim_pos_{id(actor)}" + self._animation_sync_tasks[owner_model] = task_name + + def maintainWorldPosition(task): + try: + if not actor.isEmpty() and not owner_model.isEmpty(): + owner_transform = owner_model.getTransform(self.render) + last_transform = owner_model.getPythonTag("_last_anim_sync_transform") + if last_transform == owner_transform: + return task.cont + owner_model.setPythonTag("_last_anim_sync_transform", owner_transform) + actor.setTransform(self.render, owner_transform) + return task.cont + self._animation_sync_tasks.pop(owner_model, None) + return task.done + except Exception: + self._animation_sync_tasks.pop(owner_model, None) + return task.done + + taskMgr.remove(task_name) + taskMgr.add(maintainWorldPosition, task_name) + def _is_scene_root_node(self, node): try: if not node or node.isEmpty(): @@ -931,7 +1027,7 @@ class AnimationTools: pass anims = actor.getAnimNames() - print(f"[Actor加载] {source_desc} 检测到动画: {anims}") + self._anim_log(f"[Actor加载] {source_desc} 检测到动画: {anims}") if not anims: _cleanup_actor(actor) return None @@ -948,7 +1044,7 @@ class AnimationTools: continue if not playable: - print(f"[Actor加载] {source_desc} 动画控制无效,尝试其他加载路径") + self._anim_log(f"[Actor加载] {source_desc} 动画控制无效,尝试其他加载路径") _cleanup_actor(actor) return None @@ -959,7 +1055,7 @@ class AnimationTools: except Exception: has_geom = False if not has_geom: - print(f"[Actor加载] {source_desc} 无可见几何体,尝试其他加载路径") + self._anim_log(f"[Actor加载] {source_desc} 无可见几何体,尝试其他加载路径") _cleanup_actor(actor) return None @@ -967,7 +1063,7 @@ class AnimationTools: actor.hide() return actor except Exception as e: - print(f"[Actor加载] {source_desc} 失败: {e}") + self._anim_log(f"[Actor加载] {source_desc} 失败: {e}") return None def _try_create_autobind_proxy(model_np, source_desc, owns_node=False): @@ -1056,13 +1152,13 @@ class AnimationTools: except Exception: has_geom = False if not has_geom: - print(f"[Actor加载] {source_desc} autoBind 检测到动画但无可见几何体,尝试其他节点") + self._anim_log(f"[Actor加载] {source_desc} autoBind 检测到动画但无可见几何体,尝试其他节点") return None - print(f"[Actor加载] {source_desc} autoBind 检测到动画: {list(controls.keys())}") + self._anim_log(f"[Actor加载] {source_desc} autoBind 检测到动画: {list(controls.keys())}") return _BoundAnimationProxy(model_np, controls, owns_node=owns_node) except Exception as e: - print(f"[Actor加载] {source_desc} autoBind 失败: {e}") + self._anim_log(f"[Actor加载] {source_desc} autoBind 失败: {e}") return None def _try_create_actor_via_gltf_path(path): @@ -1097,7 +1193,7 @@ class AnimationTools: succeeded = True return proxy except Exception as e: - print(f"[Actor加载] GLTF专用加载失败 ({path}): {e}") + self._anim_log(f"[Actor加载] GLTF专用加载失败 ({path}): {e}") finally: # 仅在失败且本地临时节点仍存在时清理,避免泄漏 try: @@ -1109,16 +1205,6 @@ class AnimationTools: # 检查缓存(无效缓存自动清理,避免“无动画”结果被永久缓存) cached_actor = self._actor_cache.get(owner_model) - if cached_actor: - # 若已获得模型路径,优先重建真实 Actor,避免长期停留在 autoBind 代理导致“能触发但不可见” - try: - owner_has_path = owner_model.hasTag("model_path") and bool(owner_model.getTag("model_path")) - except Exception: - owner_has_path = False - if owner_has_path and isinstance(cached_actor, _BoundAnimationProxy): - _cleanup_actor(cached_actor) - self._actor_cache.pop(owner_model, None) - cached_actor = None if cached_actor: try: @@ -1249,7 +1335,7 @@ class AnimationTools: owner_model.setTag("has_animations", "true") return mem_actor except Exception as e: - print(f"[Actor加载] 创建内存模型副本失败: {e}") + self._anim_log(f"[Actor加载] 创建内存模型副本失败: {e}") finally: # _try_create_actor_from_source 失败时,清理临时副本 try: @@ -1269,27 +1355,27 @@ class AnimationTools: self._actor_cache[owner_model] = mem_proxy owner_model.setTag("has_animations", "true") if source_node != owner_model: - print(f"[Actor加载] 使用可见节点作为动画源: {owner_model.getName()} -> {source_node.getName()}") + self._anim_log(f"[Actor加载] 使用可见节点作为动画源: {owner_model.getName()} -> {source_node.getName()}") return mem_proxy return None # 始终优先尝试从文件路径加载,因为底层 gltf 插件只有在加载文件时才能抽取完整的动画名称。 - print(f"[Actor加载调试] 传入的 origin_model: {origin_model.getName() if origin_model else 'None'}") - print(f"[Actor加载调试] origin_model tags: {origin_model.getTags() if origin_model else 'None'}") - print(f"[Actor加载调试] 解析出的 owner_model: {owner_model.getName() if owner_model else 'None'}") + 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'}") + self._anim_log(f"[Actor加载调试] 解析出的 owner_model: {owner_model.getName() if owner_model else 'None'}") try: - print(f"[Actor加载调试] owner_model tags: {owner_model.getTags() if owner_model else 'None'}") - print(f"[Actor加载调试] owner_model path tag: {owner_model.getTag('model_path') if owner_model.hasTag('model_path') else 'MISSING'}") + self._anim_log(f"[Actor加载调试] owner_model tags: {owner_model.getTags() if owner_model else 'None'}") + self._anim_log(f"[Actor加载调试] owner_model path tag: {owner_model.getTag('model_path') if owner_model.hasTag('model_path') else 'MISSING'}") except Exception as e: - print(f"[Actor加载调试] 获取 tags 异常: {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") - print(f"[Actor加载调试] 获取到的 filepath: '{filepath}'") + self._anim_log(f"[Actor加载调试] 获取到的 filepath: '{filepath}'") if not filepath: - print(f"[Actor加载调试] filepath为空,触发 _try_memory_fallback()") + self._anim_log(f"[Actor加载调试] filepath为空,触发 _try_memory_fallback()") return _try_memory_fallback() # 针对Actor加载,必须使用Panda3D规范的Unix风格路径,否则Windows绝对路径会导致加载彻底崩溃并返回空节点 @@ -1340,7 +1426,7 @@ class AnimationTools: load_paths = existing_paths + [p for p in unique_paths if p not in existing_paths] for p in load_paths: - print(f"[Actor加载验证] 正在尝试通过路径读取骨骼和动画文件: {p}") + self._anim_log(f"[Actor加载验证] 正在尝试通过路径读取骨骼和动画文件: {p}") actor = _try_create_actor_from_source(p, f"文件路径({p})") if actor: owner_model.setTag("model_path", p) @@ -1408,7 +1494,7 @@ class AnimationTools: format_info = self._getModelFormat(origin_model) processed = [] - print(f"[动画分析] 格式: {format_info}, 原始动画名称: {anim_names}") + self._anim_log(f"[动画分析] 格式: {format_info}, 原始动画名称: {anim_names}") for name in anim_names: display_name = name @@ -1439,7 +1525,7 @@ class AnimationTools: # FBX 格式可能需要特殊处理 if self._isLikelyBoneGroup(name): # 检查是否是骨骼组而非动画 - print(f"[警告] '{name}' 可能不是真正的动画序列,而是骨骼组") + self._anim_log(f"[警告] '{name}' 可能不是真正的动画序列,而是骨骼组") display_name = f"⚠️ {name} (可能非动画)" else: display_name = name @@ -1449,7 +1535,7 @@ class AnimationTools: display_name = name processed.append((display_name, original_name)) - print(f"[动画分析] {original_name} → {display_name}") + self._anim_log(f"[动画分析] {original_name} → {display_name}") return processed @@ -1630,6 +1716,12 @@ class AnimationTools: except Exception: pass + try: + self._ensure_animation_runtime_state() + self._animation_sync_tasks.pop(owner_model, None) + except Exception: + pass + try: actor.hide() except Exception: @@ -1694,70 +1786,19 @@ class AnimationTools: pass owner_model = self._resolve_animation_owner_model(origin_model) - self._stop_all_active_animations() - self._cleanup_conflicting_animation_actors(owner_model) - self._discard_cached_actor(owner_model, restore_owner=True) - actor = self._getActor(owner_model) + self._stop_all_active_animations(except_owner=owner_model) + actor = self._acquire_playback_actor(origin_model, owner_model, force_rebuild=False) if not actor: return - # 若仍在“无路径的内存代理”,播放前强制再尝试一次路径恢复并重建真实 Actor - if isinstance(actor, _BoundAnimationProxy): - owner_has_path = owner_model.hasTag("model_path") and bool(owner_model.getTag("model_path")) - if not owner_has_path: - recovered_path = self._recover_model_path_from_tags(owner_model) or self._recover_model_path_from_tags(origin_model) - if recovered_path: - try: - owner_model.setTag("model_path", recovered_path) - if (not owner_model.hasTag("original_path")) or (not owner_model.getTag("original_path")): - owner_model.setTag("original_path", recovered_path) - except Exception: - pass - self._clear_animation_cache(owner_model) - actor = self._getActor(owner_model) - if not actor: - return - - is_scene_bound_proxy = isinstance(actor, _BoundAnimationProxy) and (not getattr(actor, "_owns_node", False)) - - if not is_scene_bound_proxy: - actor.setPos(self.render, owner_model.getPos(self.render)) - actor.setHpr(self.render, owner_model.getHpr(self.render)) - actor.setScale(self.render, owner_model.getScale(self.render)) - - # original_world_pos = owner_model.getPos(self.render) - # original_world_hpr = owner_model.getHpr(self.render) - # original_world_scale = owner_model.getScale(self.render) - - if not is_scene_bound_proxy: - actor.setPos(owner_model.getPos()) - actor.setHpr(owner_model.getHpr()) - actor.setScale(owner_model.getScale()) + is_scene_bound_proxy = self._sync_actor_transform_for_playback(owner_model, actor) if self._should_swap_visibility_for_actor(owner_model, actor): owner_model.hide() else: owner_model.show() actor.show() - - task_name = f"maintain_anim_pos_{id(actor)}" - - # 维持 Actor 的世界变换,避免父节点层级差异导致位置抖动/漂移 - def maintainWorldPosition(task): - try: - if not actor.isEmpty() and not owner_model.isEmpty(): - actor.setPos(self.render, owner_model.getPos(self.render)) - actor.setHpr(self.render, owner_model.getHpr(self.render)) - actor.setScale(self.render, owner_model.getScale(self.render)) - return task.cont - else: - return task.done - except: - return task.done - - taskMgr.remove(task_name) - if not is_scene_bound_proxy: - taskMgr.add(maintainWorldPosition, task_name) + self._ensure_actor_sync_task(owner_model, actor, enabled=(not is_scene_bound_proxy)) # 获取当前选中的动画 current_anim = self._resolve_selected_animation_name(origin_model, actor, owner_model) @@ -1849,47 +1890,19 @@ class AnimationTools: pass owner_model = self._resolve_animation_owner_model(origin_model) - self._stop_all_active_animations() - self._cleanup_conflicting_animation_actors(owner_model) - self._discard_cached_actor(owner_model, restore_owner=True) - actor = self._getActor(owner_model) + self._stop_all_active_animations(except_owner=owner_model) + actor = self._acquire_playback_actor(origin_model, owner_model, force_rebuild=False) if not actor: return - is_scene_bound_proxy = isinstance(actor, _BoundAnimationProxy) and (not getattr(actor, "_owns_node", False)) - - original_world_pos = owner_model.getPos(self.render) - original_world_hpr = owner_model.getHpr(self.render) - original_world_scale = owner_model.getScale(self.render) - - if not is_scene_bound_proxy: - actor.setPos(self.render, owner_model.getPos(self.render)) - actor.setHpr(self.render, owner_model.getHpr(self.render)) - actor.setScale(self.render, owner_model.getScale(self.render)) + is_scene_bound_proxy = self._sync_actor_transform_for_playback(owner_model, actor) if self._should_swap_visibility_for_actor(owner_model, actor): owner_model.hide() else: owner_model.show() actor.show() - - task_name = f"maintain_anim_pos_{id(actor)}" - - def maintainWorldPosition(task): - try: - if not actor.isEmpty() and not owner_model.isEmpty(): - actor.setPos(self.render, owner_model.getPos(self.render)) - actor.setHpr(self.render, owner_model.getHpr(self.render)) - actor.setScale(self.render, owner_model.getScale(self.render)) - return task.cont - else: - return task.done - except: - return task.done - - taskMgr.remove(task_name) - if not is_scene_bound_proxy: - taskMgr.add(maintainWorldPosition, task_name) + self._ensure_actor_sync_task(owner_model, actor, enabled=(not is_scene_bound_proxy)) # 获取当前选中的动画 current_anim = self._resolve_selected_animation_name(origin_model, actor, owner_model) @@ -1928,6 +1941,11 @@ class AnimationTools: node.setPythonTag("cached_processed_names", None) node.setPythonTag("cached_has_animation_nodes", None) node.setPythonTag("animation", None) # 同时清除动画检测结果 + if owner_node and owner_node != node: + owner_node.setPythonTag("cached_anim_info", None) + owner_node.setPythonTag("cached_processed_names", None) + owner_node.setPythonTag("cached_has_animation_nodes", None) + owner_node.setPythonTag("animation", None) # 如果Actor在缓存中,也需要清理 if owner_node in self._actor_cache: diff --git a/ui/panels/editor_panels_right.py b/ui/panels/editor_panels_right.py index 41d9d97c..61508831 100644 --- a/ui/panels/editor_panels_right.py +++ b/ui/panels/editor_panels_right.py @@ -658,7 +658,9 @@ class EditorPanelsRightMixin( pass has_animation_tag = anim_node.hasTag("has_animations") and anim_node.getTag("has_animations").lower() == "true" - has_cached_animation = anim_node.getPythonTag("animation") is True + cached_animation_state = anim_node.getPythonTag("animation") + has_cached_animation = cached_animation_state is True + cached_no_animation = cached_animation_state is False has_animation_nodes = ( has_animation_tag or has_cached_animation or @@ -691,13 +693,15 @@ class EditorPanelsRightMixin( anim_node.setPythonTag("animation", None) should_force_probe = False - if not (has_animation_tag or has_animation_nodes or has_cached_animation): + if not (has_animation_tag or has_animation_nodes or has_cached_animation or cached_no_animation): should_force_probe = True - anim_node.setPythonTag("cached_anim_info", None) - anim_node.setPythonTag("cached_processed_names", None) - anim_node.setPythonTag("cached_has_animation_nodes", None) has_animation_nodes = self._get_cached_animation_structure_state(anim_node, force_refresh=True) - + if not has_animation_nodes: + cached_no_animation = True + anim_node.setPythonTag("animation", False) + anim_node.setPythonTag("cached_anim_info", "无动画") + anim_node.setPythonTag("cached_processed_names", []) + # 只有在没有缓存时才进行完整的动画检测和处理 if cached_anim_info is None or cached_processed_names is None: # 获取Actor @@ -710,8 +714,18 @@ class EditorPanelsRightMixin( anim_node.setPythonTag("animation", False) anim_node.setPythonTag("cached_processed_names", []) anim_node.setPythonTag("cached_anim_info", "无动画") + try: + anim_node.setTag("has_animations", "false") + anim_node.setTag("has_animations_checked", "true") + except Exception: + pass else: imgui.text_colored((0.7, 0.7, 0.7, 1.0), "此模型不包含动画") + anim_node.setPythonTag("animation", False) + if cached_processed_names is None: + anim_node.setPythonTag("cached_processed_names", []) + if cached_anim_info is None: + anim_node.setPythonTag("cached_anim_info", "无动画") return # 获取和分析动画名称 @@ -724,6 +738,12 @@ class EditorPanelsRightMixin( if not (has_animation_tag or has_animation_nodes): anim_node.setPythonTag("cached_processed_names", []) anim_node.setPythonTag("cached_anim_info", "无动画") + anim_node.setPythonTag("animation", False) + try: + anim_node.setTag("has_animations", "false") + anim_node.setTag("has_animations_checked", "true") + except Exception: + pass return anim_node.setTag("has_animations", "true")