This commit is contained in:
Rowland 2026-01-23 16:05:54 +08:00
parent bde899a04f
commit fbf71fd6a2

606
demo.py
View File

@ -5,6 +5,7 @@ from direct.showbase.ShowBase import ShowBase
from direct.task import Task
from direct.actor.Actor import Actor
from direct.interval.IntervalGlobal import Sequence
from panda3d.core import NodePath
import p3dimgui
@ -155,6 +156,10 @@ class MyWorld(CoreWorld):
# 初始化资源管理器
self.resource_manager = ResourceManager(self)
# 初始化Actor缓存系统用于动画控制
self._actor_cache = {}
print("✓ Actor缓存系统初始化完成")
# 初始化自定义鼠标控制器(视角移动)
self.mouse_controller = CustomMouseController(self)
self.mouse_controller.setUp(mouse_speed=25, move_speed=20)
@ -1463,6 +1468,10 @@ class MyWorld(CoreWorld):
elif node_type == "模型":
if imgui.collapsing_header("模型属性"):
self._draw_model_properties(node)
# 动画控制组(只对模型显示)
if imgui.collapsing_header("动画控制"):
self._draw_animation_properties(node)
# 外观属性组(通用)
if imgui.collapsing_header("外观属性"):
@ -1476,6 +1485,236 @@ class MyWorld(CoreWorld):
if imgui.collapsing_header("操作"):
self._draw_property_actions(node)
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 = os.path.join(os.path.dirname(os.path.dirname(__file__)), "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 _get_node_type_from_node(self, node):
"""从节点判断其类型"""
# 检查是否为GUI元素
@ -1538,8 +1777,36 @@ class MyWorld(CoreWorld):
if has_script:
badges.append(("脚本", (0.8, 0.4, 0.8, 1.0))) # 紫色
# 动画徽章
has_animation = hasattr(node, 'getPythonTag') and node.getPythonTag('animation')
# 动画徽章优化检测逻辑避免重复创建Actor
has_animation = False
if node_type == "模型": # 只对模型类型进行动画检测
# 首先检查是否已经缓存了检测结果
cached_result = node.getPythonTag('animation')
if cached_result is not None:
has_animation = cached_result
else:
# 只有在未缓存时才进行检测
try:
# 使用轻量级检测:先检查文件扩展名
model_path = node.getTag("model_path")
if model_path and model_path.lower().endswith(('.glb', '.gltf', '.fbx')):
# 对于可能包含动画的格式才进行Actor检测
actor = self._getActor(node)
if actor and actor.getAnimNames():
has_animation = True
# 缓存检测结果
node.setPythonTag('animation', has_animation)
print(f"[动画检测] {node.getName()}: {'有动画' if has_animation else '无动画'}")
else:
# 对于不太可能有动画的格式,直接标记为无动画
node.setPythonTag('animation', False)
except Exception as e:
print(f"动画检测失败: {e}")
node.setPythonTag('animation', False)
else:
# 对于非模型类型,检查已有的动画标签
has_animation = hasattr(node, 'getPythonTag') and node.getPythonTag('animation')
if has_animation:
badges.append(("动画", (0.4, 0.8, 0.4, 1.0))) # 绿色
@ -1955,13 +2222,338 @@ class MyWorld(CoreWorld):
def _draw_model_properties(self, node):
"""绘制模型属性"""
imgui.text("模型属性")
# 获取模型信息
model_path = node.getTag("model_path") if node.hasTag("model_path") else "未知"
# 模型路径
imgui.text("模型路径: (暂不支持显示)")
imgui.text("模型路径:")
imgui.same_line()
imgui.text_colored((0.7, 0.7, 0.7, 1.0), model_path)
# 材质数量
imgui.text("材质数量: (暂不支持显示)")
# 模型基本信息
imgui.text("模型名称:")
imgui.same_line()
model_name = node.getName() or "未命名模型"
imgui.text_colored((0.7, 0.7, 0.7, 1.0), model_name)
# 模型位置信息
imgui.text("位置:")
imgui.same_line()
pos = node.getPos()
imgui.text_colored((0.7, 0.7, 0.7, 1.0), f"X:{pos.x:.2f} Y:{pos.y:.2f} Z:{pos.z:.2f}")
# 模型缩放信息
imgui.text("缩放:")
imgui.same_line()
scale = node.getScale()
imgui.text_colored((0.7, 0.7, 0.7, 1.0), f"X:{scale.x:.2f} Y:{scale.y:.2f} Z:{scale.z:.2f}")
# 模型旋转信息
imgui.text("旋转:")
imgui.same_line()
hpr = node.getHpr()
imgui.text_colored((0.7, 0.7, 0.7, 1.0), f"H:{hpr.x:.1f}° P:{hpr.y:.1f}° R:{hpr.z:.1f}°")
def _draw_animation_properties(self, node):
"""绘制动画控制属性面板(优化版本,使用缓存避免重复计算)"""
# 检查是否已经缓存了动画信息
cached_anim_info = node.getPythonTag("cached_anim_info")
cached_processed_names = node.getPythonTag("cached_processed_names")
# 只有在没有缓存时才进行完整的动画检测和处理
if cached_anim_info is None or cached_processed_names is None:
# 获取Actor
actor = self._getActor(node)
if not actor:
imgui.text_colored((0.7, 0.7, 0.7, 1.0), "此模型不包含动画")
return
# 获取和分析动画名称
anim_names = actor.getAnimNames()
processed_names = self._processAnimationNames(node, anim_names)
if not processed_names:
imgui.text_colored((0.7, 0.7, 0.7, 1.0), "未检测到动画序列")
# 缓存空结果
node.setPythonTag("cached_processed_names", [])
node.setPythonTag("cached_anim_info", "无动画")
return
# 计算并缓存动画信息
format_info = self._getModelFormat(node)
animation_info = self._analyzeAnimationQuality(actor, anim_names, format_info)
info_text = f"格式: {format_info} | 动画数量: {len(processed_names)}"
if animation_info:
info_text += f" | {animation_info}"
# 缓存结果
node.setPythonTag("cached_anim_info", info_text)
node.setPythonTag("cached_processed_names", processed_names)
else:
# 使用缓存的数据
info_text = cached_anim_info
processed_names = cached_processed_names
# 如果缓存的空结果,直接返回
if not processed_names:
imgui.text_colored((0.7, 0.7, 0.7, 1.0), "未检测到动画序列")
return
# 显示动画信息(使用缓存的数据)
imgui.text("信息:")
imgui.same_line()
imgui.text_colored((0.7, 0.7, 0.7, 1.0), info_text)
imgui.spacing()
# 动画选择下拉框
imgui.text("动画名称:")
imgui.same_line()
# 获取当前选中的动画
current_anim = node.getPythonTag("selected_animation")
if current_anim is None:
current_anim = processed_names[0][1] if processed_names else ""
node.setPythonTag("selected_animation", current_anim)
# 查找当前动画的索引
current_index = 0
for i, (display_name, original_name) in enumerate(processed_names):
if original_name == current_anim:
current_index = i
break
# 创建下拉框选项
animation_options = [display_name for display_name, _ in processed_names]
changed, new_index = imgui.combo("##animation_combo", current_index, animation_options)
if changed and new_index < len(processed_names):
selected_display, selected_original = processed_names[new_index]
node.setPythonTag("selected_animation", selected_original)
print(f"选择动画: {selected_display} (原始名称: {selected_original})")
imgui.spacing()
# 控制按钮组
imgui.text("控制:")
# 播放按钮
if imgui.button("播放##play_animation"):
self._playAnimation(node)
imgui.same_line()
# 暂停按钮
if imgui.button("暂停##pause_animation"):
self._pauseAnimation(node)
imgui.same_line()
# 停止按钮
if imgui.button("停止##stop_animation"):
self._stopAnimation(node)
imgui.same_line()
# 循环按钮
if imgui.button("循环##loop_animation"):
self._loopAnimation(node)
imgui.spacing()
# 播放速度控制
imgui.text("播放速度:")
imgui.same_line()
# 获取当前速度
current_speed = node.getPythonTag("anim_speed")
if current_speed is None:
current_speed = 1.0
node.setPythonTag("anim_speed", current_speed)
# 速度滑块
changed, new_speed = imgui.slider_float("##anim_speed", current_speed, 0.1, 5.0, "%.1f")
if changed:
node.setPythonTag("anim_speed", new_speed)
self._setAnimationSpeed(node, new_speed)
imgui.same_line()
imgui.text_colored((0.7, 0.7, 0.7, 1.0), "倍速")
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()} 的动画缓存")
def _draw_collision_properties(self, node):
"""绘制碰撞检测属性"""