From fbf71fd6a219c6e81407b55263d6eb92624fdb86 Mon Sep 17 00:00:00 2001 From: Rowland <975945824@qq.com> Date: Fri, 23 Jan 2026 16:05:54 +0800 Subject: [PATCH] =?UTF-8?q?ui=E6=9B=BF=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- demo.py | 606 +++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 599 insertions(+), 7 deletions(-) diff --git a/demo.py b/demo.py index c2c6f951..2fd825ca 100644 --- a/demo.py +++ b/demo.py @@ -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): """绘制碰撞检测属性"""