优化播放动画场景卡顿问题

This commit is contained in:
Hector 2026-03-19 15:17:30 +08:00
parent 9bbcc89daa
commit 2822dfee71
2 changed files with 163 additions and 125 deletions

View File

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

View File

@ -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")