EG/ui/panels/animation_tools.py
2026-02-25 11:49:31 +08:00

435 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import os
from pathlib import Path
from direct.actor.Actor import Actor
from direct.task.TaskManagerGlobal import taskMgr
from panda3d.core import NodePath
class AnimationTools:
"""Animation and actor helper methods extracted from main world."""
def __init__(self, app):
self.app = app
def __getattr__(self, name):
return getattr(self.app, name)
def __setattr__(self, name, value):
if name == "app" or name in self.__dict__ or hasattr(type(self), name):
object.__setattr__(self, name, value)
else:
setattr(self.app, name, value)
def _getActor(self, origin_model):
"""
获取或创建模型的Actor用于动画控制
复用Qt版本经过验证的实现方式
"""
# 检查缓存
if origin_model in self._actor_cache:
return self._actor_cache[origin_model]
# 尝试直接从内存创建
if origin_model.hasTag("can_create_actor_from_memory"):
try:
test_actor = Actor(origin_model)
anims = test_actor.getAnimNames()
self._actor_cache[origin_model] = test_actor
print(f"[Actor加载] 内存创建检测到动画: {anims}")
if anims:
return test_actor
else:
test_actor.cleanup()
test_actor.removeNode()
except Exception as e:
print(f"从内存模型创建Actor失败: {e}")
# 如果不能直接从内存创建,再尝试通过文件路径加载
filepath = origin_model.getTag("model_path")
if not filepath:
return None
print(f"[Actor加载] 尝试加载: {filepath}")
# 处理跨平台路径问题
import os
# 检查路径是否有效,如果无效则尝试修复
if not os.path.exists(filepath):
original_filepath = filepath
# 尝试多种修复策略
fixed = False
import platform
# 策略1: 处理Linux风格路径在Windows上的问题
if filepath.startswith('/') and platform.system() == "Windows":
print("[路径转换] 尝试处理Linux风格路径:", filepath)
path_parts = filepath.split('/')
print(platform.system())
if len(path_parts) > 1:
drive_letter = path_parts[1].upper() + ':\\' # 添加反斜杠确保正确路径格式
remaining_path = '\\'.join(path_parts[2:]) if len(path_parts) > 2 else ''
potential_path = os.path.join(drive_letter, remaining_path)
print(f"[路径转换] 构造的潜在路径: {potential_path}")
if os.path.exists(potential_path):
filepath = potential_path
fixed = True
print(f"[路径转换] 成功: {original_filepath} -> {filepath}")
else:
print(f"[路径转换] 文件不存在: {potential_path}")
# 策略2: 处理路径分隔符问题
if not fixed:
# 尝试规范化路径
normalized_path = os.path.normpath(filepath)
print(f"[路径规范化] 尝试规范化路径: {filepath} -> {normalized_path}")
if os.path.exists(normalized_path):
filepath = normalized_path
fixed = True
print(f"[路径规范化] 成功: {filepath}")
else:
print(f"[路径规范化] 文件不存在: {normalized_path}")
# 策略3: 在Resources目录中查找
if not fixed:
# 尝试在Resources目录中查找文件
resources_path = str(Path(__file__).resolve().parents[2] / "Resources")
filename = os.path.basename(filepath)
potential_path = os.path.join(resources_path, filename)
print(f"[Resources查找] 尝试在Resources目录查找: {potential_path}")
if os.path.exists(potential_path):
filepath = potential_path
fixed = True
print(f"[Resources查找] 成功: {filepath}")
else:
print(f"[Resources查找] 文件不存在: {potential_path}")
if fixed:
print(f"路径修复: {original_filepath} -> {filepath}")
# 更新模型标签
origin_model.setTag("model_path", filepath)
else:
print(f"[警告] 模型文件不存在: {filepath}")
return None
# 检查是否是 FBX 文件,如果是,使用专门的 FBX 动画加载器
if filepath.lower().endswith('.fbx'):
pass
#return self._createFBXActor(origin_model, filepath)
# 其他格式使用标准 Actor 加载
try:
import gltf
from panda3d.core import Filename
# 将Panda3D路径转换为操作系统特定路径
panda_filename = Filename(filepath)
os_specific_path = panda_filename.to_os_specific()
print(f"[路径转换] {filepath} -> {os_specific_path}")
print(f"[GLTF加载] 尝试加载: {os_specific_path}")
# 使用明确的设置确保动画被加载
gltf_settings = gltf.GltfSettings(skip_animations=False)
model_root = gltf.load_model(os_specific_path, gltf_settings)
model_node = NodePath(model_root)
test_actor = Actor(model_node)
anims = test_actor.getAnimNames()
test_actor.reparentTo(self.render)
self._actor_cache[origin_model] = test_actor
print(f"[Actor加载] 标准加载检测到动画: {anims}")
if not anims:
test_actor.cleanup()
test_actor.removeNode()
return None
return test_actor
except Exception as e:
print(f"创建Actor失败: {e}")
return None
def _getModelFormat(self, origin_model):
"""获取模型格式信息"""
filepath = origin_model.getTag("model_path")
original_path = origin_model.getTag("original_path")
converted_from = origin_model.getTag("converted_from")
if filepath:
ext = filepath.lower().split('.')[-1]
format_name = ext.upper()
# 如果是转换后的文件,显示转换信息
if converted_from and original_path:
original_ext = converted_from.upper()
format_name = f"{format_name} (从{original_ext}转换)"
return format_name
return "未知"
def _processAnimationNames(self, origin_model, anim_names):
"""处理和分析动画名称,返回 [(显示名称, 原始名称), ...]"""
format_info = self._getModelFormat(origin_model)
processed = []
print(f"[动画分析] 格式: {format_info}, 原始动画名称: {anim_names}")
for name in anim_names:
display_name = name
original_name = name
if format_info == "GLB":
# GLB 格式通常有真实的动画名称
if "|" in name:
# 处理类似 'Armature|mixamo.com|Layer0' 的名称
parts = name.split("|")
if "mixamo" in name.lower():
# Mixamo 动画
display_name = f"Mixamo_{parts[-1]}" if len(parts) > 1 else name
elif len(parts) > 2:
# 其他复杂命名
display_name = f"{parts[0]}_{parts[-1]}"
else:
display_name = parts[-1]
elif format_info == "FBX":
# FBX 格式可能需要特殊处理
if self._isLikelyBoneGroup(name):
# 检查是否是骨骼组而非动画
print(f"[警告] '{name}' 可能不是真正的动画序列,而是骨骼组")
display_name = f"⚠️ {name} (可能非动画)"
else:
display_name = name
elif format_info in ["EGG", "BAM"]:
# 原生格式通常命名规范
display_name = name
processed.append((display_name, original_name))
print(f"[动画分析] {original_name}{display_name}")
return processed
def _isLikelyBoneGroup(self, name):
"""判断动画名称是否更像骨骼组而不是动画序列"""
bone_indicators = ['joints', 'bones', 'skeleton', 'surface', 'mesh', 'beta', 'rig']
name_lower = name.lower()
# 如果包含这些关键词,可能是骨骼组
for indicator in bone_indicators:
if indicator in name_lower:
return True
# 如果名称太简单少于3个字符可能不是动画
if len(name) < 3:
return True
return False
def _analyzeAnimationQuality(self, actor, anim_names, format_info):
"""分析动画质量和类型(优化版本,减少详细分析以提高性能)"""
try:
valid_anims = 0
# 简化分析:只检查动画是否存在,不详细分析帧数
for anim_name in anim_names:
try:
control = actor.getAnimControl(anim_name)
if control and control.getNumFrames() > 1:
valid_anims += 1
except Exception:
# 忽略单个动画的分析错误,继续处理其他动画
continue
if valid_anims == 0:
return "⚠️ 无有效动画"
elif valid_anims < len(anim_names):
return f"⚠️ {valid_anims}/{len(anim_names)} 个有效"
else:
return f"{valid_anims} 个动画"
except Exception as e:
# 简化错误处理
return "分析异常"
def _playAnimation(self, origin_model):
"""播放动画"""
actor = self._getActor(origin_model)
if not actor:
return
# 保存原始世界坐标
original_world_pos = origin_model.getPos(self.render)
original_world_hpr = origin_model.getHpr(self.render)
original_world_scale = origin_model.getScale(self.render)
# 设置Actor位置和姿态
actor.setPos(origin_model.getPos())
actor.setHpr(origin_model.getHpr())
actor.setScale(origin_model.getScale())
# 隐藏原始模型显示Actor
origin_model.hide()
actor.show()
# 创建任务来维持世界坐标不变
def maintainWorldPosition(task):
try:
if not actor.isEmpty():
actor.setPos(self.render, original_world_pos)
actor.setHpr(self.render, original_world_hpr)
actor.setScale(self.render, original_world_scale)
return task.cont
else:
return task.done
except:
return task.done
# 添加维持位置的任务
taskMgr.add(maintainWorldPosition, f"maintain_anim_pos_{id(actor)}")
# 获取当前选中的动画
current_anim = origin_model.getPythonTag("selected_animation")
if current_anim:
actor.play(current_anim)
print(f"『动画播放』:{current_anim}")
else:
# 兜底:使用第一个可用动画
anim_names = actor.getAnimNames()
if anim_names:
actor.play(anim_names[0])
print(f"『动画播放』:{anim_names[0]}")
def _pauseAnimation(self, origin_model):
"""暂停动画"""
actor = self._getActor(origin_model)
if not actor:
return
# 设置Actor位置和姿态
actor.setPos(origin_model.getPos())
actor.setHpr(origin_model.getHpr())
actor.setScale(origin_model.getScale())
# 隐藏原始模型显示Actor
origin_model.hide()
actor.show()
# 停止动画(保持当前姿势)
actor.stop()
print("『动画』暂停")
def _stopAnimation(self, origin_model):
"""停止动画"""
actor = self._getActor(origin_model)
if not actor:
return
# 停止动画
actor.stop()
# 获取当前选中的动画
current_anim = origin_model.getPythonTag("selected_animation")
if current_anim and actor.getAnimControl(current_anim):
actor.getAnimControl(current_anim).pose(0)
# 隐藏Actor显示原始模型
actor.hide()
origin_model.show()
# 移除维持位置的任务
taskMgr.remove(f"maintain_anim_pos_{id(actor)}")
print("『动画』停止切换至原始模型")
def _loopAnimation(self, origin_model):
"""循环播放动画"""
actor = self._getActor(origin_model)
if not actor:
return
# 保存原始世界坐标
original_world_pos = origin_model.getPos(self.render)
original_world_hpr = origin_model.getHpr(self.render)
original_world_scale = origin_model.getScale(self.render)
# 设置Actor位置和姿态
actor.setPos(origin_model.getPos())
actor.setHpr(origin_model.getHpr())
actor.setScale(origin_model.getScale())
# 隐藏原始模型显示Actor
origin_model.hide()
actor.show()
# 创建任务来维持世界坐标不变
def maintainWorldPosition(task):
try:
if not actor.isEmpty():
actor.setPos(self.render, original_world_pos)
actor.setHpr(self.render, original_world_hpr)
actor.setScale(self.render, original_world_scale)
return task.cont
else:
return task.done
except:
return task.done
# 添加维持位置的任务
taskMgr.add(maintainWorldPosition, f"maintain_anim_pos_{id(actor)}")
# 获取当前选中的动画
current_anim = origin_model.getPythonTag("selected_animation")
if current_anim:
actor.loop(current_anim)
print(f"[动画] 循环: {current_anim}")
else:
# 兜底:使用第一个可用动画
anim_names = actor.getAnimNames()
if anim_names:
actor.loop(anim_names[0])
print(f"[动画] 循环: {anim_names[0]}")
def _setAnimationSpeed(self, origin_model, speed):
"""设置动画播放速度"""
actor = self._getActor(origin_model)
if not actor:
return
# 获取当前选中的动画
current_anim = origin_model.getPythonTag("selected_animation")
if current_anim:
actor.setPlayRate(speed, current_anim)
print(f"[动画] 速度设为: {speed} ({current_anim})")
else:
# 兜底:尝试所有动画
anim_names = actor.getAnimNames()
for anim_name in anim_names:
actor.setPlayRate(speed, anim_name)
print(f"[动画] 速度设为: {speed} (所有动画)")
def _clear_animation_cache(self, node):
"""清除节点的动画缓存,当模型发生变化时调用"""
node.setPythonTag("cached_anim_info", None)
node.setPythonTag("cached_processed_names", None)
node.setPythonTag("animation", None) # 同时清除动画检测结果
# 如果Actor在缓存中也需要清理
if node in self._actor_cache:
actor = self._actor_cache[node]
try:
# 清理相关任务
taskMgr.remove(f"maintain_anim_pos_{id(actor)}")
# 清理Actor
if not actor.isEmpty():
actor.cleanup()
actor.removeNode()
except Exception as e:
print(f"清理Actor缓存失败: {e}")
finally:
del self._actor_cache[node]
print(f"[缓存清理] 清除节点 {node.getName()} 的动画缓存")