435 lines
16 KiB
Python
435 lines
16 KiB
Python
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()} 的动画缓存")
|