#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 测试动画模型 - Panda3D应用程序 使用Panda3D引擎编辑器创建 """ from __future__ import print_function import json from direct.actor.Actor import Actor from direct.showbase.ShowBaseGlobal import globalClock from panda3d.core import TextNode, CardMaker, TextureStage, NodePath, Texture, TransparencyAttrib, CollisionTraverser, \ Point3 from core.InfoPanelManager import InfoPanelManager # 获取渲染管线路径 # 在文件开头添加sys导入(如果还没有的话) import sys import os # 修改工作目录设置部分 if getattr(sys, 'frozen', False): # 打包后的环境 project_root = os.path.dirname(sys.executable) else: # 开发环境 try: project_root = os.path.dirname(os.path.abspath(__file__)) except NameError: project_root = os.getcwd() os.chdir(project_root) render_pipeline_path = 'RenderPipelineFile' sys.path.insert(0, render_pipeline_path) # 改进路径设置逻辑 pipeline_path = os.path.join(project_root, "RenderPipelineFile") if os.path.exists(pipeline_path) and pipeline_path not in sys.path: sys.path.insert(0, pipeline_path) # 同时添加子目录以确保所有模块都能正确导入 for root, dirs, files in os.walk(pipeline_path): if root not in sys.path: sys.path.insert(0, root) import math from random import random, randint, seed from panda3d.core import Vec3, load_prc_file_data, Filename from direct.showbase.ShowBase import ShowBase from direct.task.TaskManagerGlobal import taskMgr # os.chdir(os.path.dirname(os.path.realpath(__file__))) from core.script_system import ScriptManager from core.CustomMouseController import CustomMouseController from panda3d.core import CollisionTraverser class MainApp(ShowBase): def __init__(self): # 在调用父类构造函数前确保必要的属性存在 if not hasattr(self, 'appRunner'): self.appRunner = None load_prc_file_data("", """ win-size 1380 750 window-title support-threads #t """) # 简化 sys.path 设置逻辑 pipeline_path = os.path.join(project_root, "RenderPipelineFile") if os.path.exists(pipeline_path): if pipeline_path not in sys.path: sys.path.insert(0, pipeline_path) else: print(f"错误: 找不到渲染管线目录: {pipeline_path}") return try: from rpcore import RenderPipeline self.render_pipeline = RenderPipeline() self.render_pipeline.create(self) except ImportError as e: print(f"导入RenderPipeline模块失败: {e}") import traceback traceback.print_exc() ShowBase.__init__(self) self.render_pipeline = None return self.script_manager = ScriptManager(self) self.script_manager.start_system() # 加载所有脚本e self.script_manager.load_all_scripts_from_directory() self.info_panel_manager = InfoPanelManager(self) try: # 再导入controller模块 from rpcore.util.movement_controller import MovementController self.render_pipeline._showbase.camera = self.render_pipeline._showbase.cam self.controller = MovementController(self) self.camLens.set_fov(80) self.controller.set_initial_position( Vec3(0, -50, 20), Vec3(0, 0, 0)) self.controller.setup() except ImportError as e: print(f"导入MovementController失败: {e}") self.controller = None self._last_click_time = 0 self._last_clicked_node = None self._double_click_threshold = 0.3 self.cameraSpeed = 20.0 self.cameraRotateSpeed=10.0 self.lastMouseX=0 self.lastMouseY=0 self.mouseRightPressed=False self._loadFont() self.loadFullScene() self.loadGUIFromJSON() self.setupMouseClickHandler() self.cTrav = CollisionTraverser() self.createFrameRateDisplay() self.women_actor = None self.current_actor = None if hasattr(self, 'accept'): base.accept("l", self.tour) def _loadFont(self): """加载中文字体""" self.chinese_font = None try: self.chinese_font = self.loader.loadFont('/usr/share/fonts/truetype/wqy/wqy-microhei.ttc') if not self.chinese_font: print("警告: 无法加载中文字体,将使用默认字体") else: print("✓ 中文字体加载成功") except: print("警告: 无法加载中文字体,将使用默认字体") self.chinese_font = None def getChineseFont(self): """获取中文字体""" return self.chinese_font def getResourcePath(self,relative_path): if getattr(sys,'frozen',False): base_path = os.path.dirname(sys.executable) else: base_path = os.path.dirname(os.path.abspath(__file__)) return os.path.join(base_path,relative_path) def loadFullScene(self): if not hasattr(self, 'loader') or not hasattr(self, 'render'): print("错误: Panda3D核心组件未正确初始化") return try: scene_file = self.getResourcePath("scene.bam") if os.path.exists(scene_file): # 使用readBamFile加载完整场景 from panda3d.core import BamCache BamCache.getGlobalPtr().setActive(False) # 禁用缓存以避免问题 scene = self.loader.loadModel(Filename.fromOsSpecific(scene_file)) if scene: scene.reparentTo(self.render) self.render_pipeline.prepare_scene(scene) print("✓ 完整场景加载成功") # 检测并播放模型动画 #self._processModelAnimations(scene) # 处理场景中的各种元素 self.processSceneElements(scene) else: print("⚠️ 场景文件加载失败") else: print("⚠️ 未找到场景文件") except Exception as e: print(f"加载完整场景时出错: {str(e)}") import traceback traceback.print_exc() def checkAnimationStatus(self): """检查场景中所有Actor的动画状态""" try: all_actors = self.render.findAllMatches("**/+ActorNode") print(f"=== 动画状态检查 ===") print(f"场景中总共有 {len(all_actors)} 个Actor") for i, actor_np in enumerate(all_actors): actor_node = actor_np.node() if isinstance(actor_node, Actor): # 确保Actor可见 actor_np.show() current_anim = actor_node.getCurrentAnim() anim_names = actor_node.getAnimNames() print(f"Actor {i + 1} ({actor_np.getName()}):") print(f" 位置: {actor_np.getPos()}") print(f" 可见性: {actor_np.isHidden()}") print(f" 可用动画: {anim_names}") print(f" 当前播放: {current_anim}") if current_anim: frame = actor_node.getCurrentFrame(current_anim) num_frames = actor_node.getNumFrames(current_anim) play_rate = actor_node.getPlayRate(current_anim) print(f" 播放进度: {frame}/{num_frames} 帧") print(f" 播放速度: {play_rate}") else: print(f"节点 {actor_np.getName()} 不是Actor类型") print("==================") except Exception as e: print(f"检查动画状态时出错: {str(e)}") def _processModelAnimations(self, node_path): """处理节点中的动画模型""" try: # 查找场景中所有可能的动画模型 char_nodes = node_path.findAllMatches("**/+Character") for char_node in char_nodes: try: # 获取父节点(通常是模型根节点) model_root = char_node.getParent() model_name = model_root.getName() print(f"检测到可能的动画模型: {model_name}") # 尝试创建Actor actor = Actor(model_root) actor.reparentTo(self.render) actor.setPos(node_path.getPos()) actor.setHpr(node_path.getHpr()) actor.setScale(node_path.getScale()) # 获取动画名称 anim_names = actor.getAnimNames() if anim_names: print(f"✓ 成功创建动画模型: {model_name}") print(f" 可用动画: {anim_names}") # 循环播放所有动画 for anim_name in anim_names: print(f" 循环播放动画: {anim_name}") actor.loop(anim_name) break # 只播放第一个动画,避免同时播放多个动画 # 替换原始模型 model_root.detachNode() else: # 没有动画,使用原始模型 print(f"模型 {model_name} 不包含动画") actor.detachNode() # 移除创建的Actor except Exception as e: print(f"处理动画模型 {char_node.getName()} 时出错: {str(e)}") except Exception as e: print(f"处理模型动画时出错: {str(e)}") import traceback traceback.print_exc() def processSceneElements(self, scene): """处理场景中的各种元素""" try: processed_lights = [] loaded_nodes = {} def processNode(nodePath, depth=0): loaded_nodes[nodePath.getName()] = nodePath # 为模型添加碰撞体 - 修复版本 if nodePath.hasTag("is_scene_element") and not nodePath.hasTag("is_gizmo"): if nodePath.hasTag("gui_type"): gui_type = nodePath.getTag("gui_type") if gui_type in ["video_screen"]: print(f"移除GUI视频节点: {nodePath.getName()}") nodePath.removeNode() # 移除视频屏幕节点 return # 使用更精确的包围盒 from panda3d.core import CollisionNode, CollisionBox bounds = nodePath.getBounds() if not bounds.isEmpty(): min_point = bounds.getMin() max_point = bounds.getMax() # 创建碰撞节点 collision_node = CollisionNode(f'collision_{nodePath.getName()}') collision_node.addSolid(CollisionBox(min_point, max_point)) collision_np = nodePath.attachNewNode(collision_node) # 隐藏碰撞体 collision_np.hide() if nodePath.hasTag("scripts_info"): try: import json scripts_info = json.loads(nodePath.getTag("scripts_info")) self.processScripts(nodePath, scripts_info) except Exception as e: print(f"处理节点 {nodePath.getName()} 的脚本时出错: {str(e)}") if nodePath.hasTag("light_type"): light_type = nodePath.getTag("light_type") if nodePath.hasTag("is_auxiliary_light") and nodePath.getTag("is_auxiliary_light").lower() == "true": print(f"跳过辅助灯光节点:{nodePath.getName()}") return if nodePath not in processed_lights: if light_type == "spot_light": self._recreateSpotLight(nodePath) elif light_type == "point_light": self._recreatePointLight(nodePath) processed_lights.append(nodePath) for child in nodePath.getChildren(): processNode(child, depth + 1) processNode(scene) except Exception as e: print(f"处理场景元素时出错: {str(e)}") def _recreateSpotLight(self,light_node): try: from RenderPipelineFile.rpcore import SpotLight from panda3d.core import Vec3 light = SpotLight() light.direction = Vec3(0,0,-1) light.fov = 70 light.set_color_from_temperature(5*1000.0) if light_node.hasTag("light_energy"): light.energy = float(light_node.getTag("light_energy")) else: light.energy = 5000 light.radius = 1000 light.casts_shadows = True light.shadow_map_resolution = 256 light_pos = light_node.getPos() light.setPos(light_pos) self.render_pipeline.add_light(light) except Exception as e: print(f"创建点光源 {light_node.getName()} 失败: {str(e)}") import traceback traceback.print_exc() def _recreatePointLight(self,light_node): try: from RenderPipelineFile.rpcore import PointLight light = PointLight() if light_node.hasTag("light_energy"): light.energy = float(light_node.getTag("light_energy")) else: light.energy = 5000 light.radius = 1000 light.inner_radius = 0.4 light.set_color_from_temperature(5*1000.0) light.casts_shadows = True light.shadow_map_resolution = 256 light.setPos(light_node.getPos()) self.render_pipeline.add_light(light) except Exception as e: print(f"创建点光源 {light_node.getName()} 失败: {str(e)}") import traceback traceback.print_exc() def processGUIElements(self, scene): """处理场景中的GUI元素""" try: # 查找并处理2D图像 images_2d = scene.findAllMatches("**/=gui_type=image_2d") for img_node in images_2d: try: # GUI元素通常在场景加载时自动处理 print(f"✓ 2D图像 {img_node.getName()} 已加载") except Exception as e: print(f"处理2D图像 {img_node.getName()} 失败: {str(e)}") except Exception as e: print(f"处理GUI元素时出错: {str(e)}") def tour(self): mopath = ( (Vec3(-10.8645000458, 9.76458263397, 2.13306283951), Vec3(-133.556228638, -4.23447799683, 0.0)), (Vec3(-10.6538448334, -5.98406457901, 1.68028640747), Vec3(-59.3999938965, -3.32706642151, 0.0)), (Vec3(9.58458328247, -5.63625621796, 2.63269257545), Vec3(58.7906494141, -9.40668964386, 0.0)), (Vec3(6.8135137558, 11.0153560638, 2.25509500504), Vec3(148.762527466, -6.41223621368, 0.0)), (Vec3(-9.07093334198, 3.65908527374, 1.42396306992), Vec3(245.362503052, -3.59927511215, 0.0)), (Vec3(-8.75390911102, -3.82727789879, 0.990055501461), Vec3(296.090484619, -0.604830980301, 0.0)), ) self.controller.play_motion_path(mopath, 3.0) def loadGUIFromJSON(self): try: gui_json_path = self.getResourcePath("gui/gui_elements.json") if os.path.exists(gui_json_path): with open(gui_json_path, 'r', encoding='utf-8') as f: content = f.read().strip() if content: gui_data = json.loads(content) self.createGUIElement(gui_data) else: print("GUI配置文件为空 ") except Exception as e: print(f"加载GUI元素时出错: {str(e)}") import traceback traceback.print_exc() def createGUIElement(self, element_data): try: processed_names = set() element_original_data = {} for i, gui_info in enumerate(element_data): name = gui_info.get("name", f"gui_element_{i}") element_original_data[name] = { "scale": gui_info.get("scale", [1, 1, 1]), "position": gui_info.get("position", [0, 0, 0]), "parent_name": gui_info.get("parent_name") } valid_parents = set() for gui_info in element_data: name = gui_info.get("name", f"gui_element_{gui_info.get('index', 0)}") valid_parents.add(name) for i, gui_info in enumerate(element_data): try: gui_type = gui_info.get("type", "unknown") name = gui_info.get("name", f"gui_element_{i}") position = gui_info.get("position", [0, 0, 0]) scale = gui_info.get("scale", [1, 1, 1]) tags = gui_info.get("tags", {}) text = gui_info.get("text", "") image_path = gui_info.get("image_path", "") video_path = gui_info.get("video_path", "") bg_image_path = gui_info.get("bg_image_path", "") parent_name = gui_info.get("parent_name") if name in processed_names: continue processed_names.add(name) absolute_position = list(position) absolute_scale = list(scale) if parent_name and parent_name in element_original_data: parent_data = element_original_data[parent_name] parent_scale = parent_data["scale"] if gui_type in ["3d_text", "3d_image", "button", "label", "entry", "2d_image", "2d_video_screen"]: # 位置需要乘以父级缩放来得到绝对位置 for j in range(min(len(absolute_position), len(parent_scale))): absolute_position[j] *= parent_scale[j] if len(parent_scale) > j else parent_scale[0] # 缩放需要乘以父级缩放来得到绝对缩放 for j in range(min(len(absolute_scale), len(parent_scale))): absolute_scale[j] *= parent_scale[j] if len(parent_scale) > j else parent_scale[0] new_element = None if gui_type == "3d_text": size = absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 0.5 new_element = self.createGUI3DText( pos=tuple(absolute_position), text=text, size=size ) elif gui_type == "button": new_element = self.createGUIButton( pos=tuple(absolute_position), text=text, size=absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 1.0, command=self.playModelAnimation ) elif gui_type == "label": new_element = self.createGUILabel( pos=tuple(absolute_position), text=text, size=absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 1.0 ) elif gui_type == "entry": new_element = self.createGUIEntry( pos=tuple(absolute_position), placeholder=text, size=absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 1.0, command=self.onGUIEntrySubmit ) elif gui_type == "2d_image": scale_value = absolute_scale[0] print(f"2d_image{scale_value}") new_element = self.createGUI2DImage( pos=tuple(absolute_position), image_path=image_path, size=absolute_scale ) elif gui_type == "2d_video_screen": new_element = self.createGUI2DVideoScreen( pos=tuple(absolute_position), video_path=video_path, size=absolute_scale ) elif gui_type == "video_screen": new_element = self.createGUIVideoScreen( pos=tuple(absolute_position), video_path=video_path, size=absolute_scale ) elif gui_type == "info_panel": new_element = self.info_panel_manager.onCreateSampleInfoPanel() if "scripts" in gui_info and new_element: self.processScripts(new_element,gui_info["scripts"]) except Exception as e: print(f"重建GUI元素失败 {name}: {e}") import traceback traceback.print_exc() continue except Exception as e: print(f"创建GUI元素时出错: {str(e)}") def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=0.1, command=None): from direct.gui.DirectGui import DirectButton button = DirectButton( text=text, pos=(pos[0], pos[1], pos[2]), # 保持正确的坐标格式 scale=size, # size 应该是数值而不是元组 frameColor=(0.2, 0.6, 0.8, 1), text_font=self.getChineseFont() if self.getChineseFont() else None, rolloverSound=None, clickSound=None, parent=None, command=command ) def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=0.5): """创建3D文本GUI元素""" try: # 创建文本节点 text_node = TextNode("gui_3d_text") text_node.setText(text) text_node.setAlign(TextNode.ACenter) # 设置字体(如果可用) if self.getChineseFont(): text_node.setFont(self.getChineseFont()) # 创建节点路径并添加到场景 text_np = self.render.attachNewNode(text_node) # 设置位置和大小 text_np.setPos(Vec3(pos[0], pos[1], pos[2])) text_np.setScale(size) # 设置面向摄像机 # text_np.setBillboardPointEye() # 设置渲染属性 text_np.setBin("fixed", 40) text_np.setDepthWrite(False) return text_np except Exception as e: print(f"❌ 创建3D文本失败: {str(e)}") import traceback traceback.print_exc() return None def createGUILabel(self, pos=(0, 0, 0), text="标签", size=0.08): from direct.gui.DirectGui import DirectLabel label = DirectLabel( text=text, pos=(pos[0], pos[1], pos[2]), scale=size, frameColor=(0, 0, 0, 0), text_fg=(1, 1, 1, 1), text_font=self.getChineseFont() if self.getChineseFont() else None, text_align=TextNode.ACenter, text_wordwrap=None, text_mayChange=True, parent=None ) def createGUIEntry(self, pos=(0, 0, 0), placeholder="输入文本...", size=0.08, command=None): from direct.gui.DirectGui import DirectEntry entry = DirectEntry( text="", pos=(pos[0], pos[1], pos[2]), scale=size, command=command, initialText=placeholder, numLines=1, width=12, focus=0, frameColor=(0, 0, 0, 0), text_fg=(1, 1, 1, 1), text_font=self.getChineseFont() if self.getChineseFont() else None, text_align=TextNode.ACenter, text_wordwrap=None, text_mayChange=True, parent=None, rolloverSound=None, clickSound=None, # 添加焦点管理命令 focusInCommand=self.disableCameraControl, focusOutCommand=self.enableCameraControl, # 确保输入框能正确捕获所有键盘事件 suppressKeys=True, # 这个参数很重要,它会阻止按键事件传播到其他处理器 suppressMouse=True ) return entry def disableCameraControl(self): """禁用相机控制""" try: if hasattr(self, 'controller'): # 如果控制器有内置的禁用方法 if hasattr(self.controller, 'disable'): self.controller.disable() else: # 否则手动禁用事件监听 self.controller.ignoreAll() # 忽略所有已注册的事件 print("相机控制已禁用") except Exception as e: print(f"禁用相机控制时出错: {e}") def enableCameraControl(self): """启用相机控制""" try: if hasattr(self, 'controller'): # 如果控制器有内置的启用方法 if hasattr(self.controller, 'enable'): self.controller.enable() else: # 重新设置控制器 self.controller.setup() print("相机控制已启用") except Exception as e: print(f"启用相机控制时出错: {e}") def onGUIEntrySubmit(self, text, entry_id=None): """GUI输入框提交事件处理""" try: print(f"GUI输入框提交: {entry_id} = {text}") # 重新启用相机控制 self.enableCameraControl() # 清除输入框焦点(如果需要) # base.win.focus() # 在这里添加您需要的文本处理逻辑 # 例如保存文本、更新UI等 except Exception as e: print(f"处理输入框提交时出错: {e}") import traceback traceback.print_exc() def createGUI2DImage(self, pos=(0, 0, 0), image_path=None, size=0.2): # 添加属性检查 if not hasattr(self, 'aspect2d'): print("错误: aspect2d未初始化") return None if image_path and not os.path.isabs(image_path): image_path = self.getResourcePath(image_path) # 处理非均匀缩放 if isinstance(size, (list, tuple)) and len(size) >= 2: # 分别处理宽度和高度的缩放 width_scale = size[0] * 0.2 height_scale = size[2] * 0.2 else: # 如果只提供了一个缩放值,则使用相同值 width_scale = size * 0.1 if isinstance(size, (int, float)) else 0.2 height_scale = width_scale cm = CardMaker("gui-2d-image") cm.setFrame(-width_scale, width_scale, -height_scale, height_scale) #cm.setFrame(-size, size, -size, size) image_node = self.aspect2d.attachNewNode(cm.generate()) image_node.setPos(pos) image_node.setBin("fixed", 0) image_node.setDepthWrite(False) image_node.setDepthTest(False) image_node.setColor(1, 1, 1, 1) # 设置透明度支持 image_node.setTransparency(TransparencyAttrib.MAlpha) if image_path: try: texture = self.loader.loadTexture(image_path) if texture: image_node.setTexture(texture, 1) texture.setWrapU(Texture.WM_clamp) texture.setWrapV(Texture.WM_clamp) texture.setMinfilter(Texture.FT_linear) texture.setMagfilter(Texture.FT_linear) image_node.setColor(1, 1, 1, 1) else: print(f"无法加载图片: {image_path}") except Exception as e: print(f"加载图片时出错: {e}") return image_node def createGUIVideoScreen(self,pos=(0,0,0),size=1,video_path=None): import os from panda3d.core import TransparencyAttrib,Texture,TextureStage if isinstance(size,(list,tuple)) and len(size) >= 2: width_scale = size[0] height_scale = size[2] else: width_scale = size * 0.1 if isinstance(size, (int, float)) else 0.2 height_scale = width_scale cm = CardMaker("gui-video-screen") cm.setFrame(-width_scale,width_scale,-height_scale,height_scale) video_screen = self.render.attachNewNode(cm.generate()) video_screen.setPos(pos) video_screen.setBin("fixed", 0) #video_screen.setTransparency(TransparencyAttrib.MAlpha) video_screen.setColor(1, 1, 1, 1) self._ensureVideoScreenMaterial(video_screen) if video_path: if video_path.startswith(("http://", "https://")): try: success = self._loadVideoFromURLWithOpenCV3D(video_screen, video_path) if success: print("视频已从URL加载成功") else: print("从URL加载视频时出错") except Exception as e: print(f"从URL加载视频时出错: {e}") elif os.path.exists(video_path): movie_texture = self._loadMovieTexture(video_path) if movie_texture: video_screen.setTexture(movie_texture, 1) else: print(f"视频文件不存在: {video_path}") else: # 设置占位符纹理 placeholder_texture = Texture() placeholder_texture.setup2dTexture(1, 1, Texture.TUnsignedByte, Texture.FRgb) placeholder_data = b'\x19\x19\x4c' # 深蓝色占位符 placeholder_texture.setRamImage(placeholder_data) video_screen.setTexture(placeholder_texture, 1) return video_screen def createGUI2DVideoScreen(self, pos=(0, 0, 0), size=0.2, video_path=None): import os from direct.gui.DirectGui import DirectFrame from panda3d.core import TransparencyAttrib, Texture, TextureStage if isinstance(size,(list,tuple)) and len(size) >= 2: width_scale = size[0]*0.2 height_scale = size[2]*0.2 else: width_scale = size*0.1 if isinstance(size,(int,float)) else 0.2 height_scale = width_scale video_screen = DirectFrame( frameSize=(-width_scale, width_scale, -height_scale, height_scale), frameColor=(1, 1, 1, 1), pos=pos, parent=None, suppressMouse=True ) video_screen.setTransparency(TransparencyAttrib.MAlpha) placeholder_texture = Texture() placeholder_texture.setup2dTexture(1, 1, Texture.TUnsignedByte, Texture.FRgb) placeholder_data = b'\x19\x19\x4c' placeholder_texture.setRamImage(placeholder_data) if video_path: if video_path.startswith(("http://","https://")): try: success = self._loadVideoFromURLWithOpenCV(video_screen,video_path) if success: print("视频已从URL加载成功") else: print("从URL加载视频时出错") except Exception as e: print(f"从URL加载视频时出错: {e}") elif os.path.exists(video_path): movie_texture = self._loadMovieTexture(video_path) if movie_texture: video_screen["frameTexture"] = movie_texture return video_screen def _loadMovieTexture(self, video_path): from panda3d.core import Texture, MovieTexture, Filename import os # Convert Windows path to Panda3D compatible path format panda_path = Filename.fromOsSpecific(video_path) converted_path = str(panda_path) movie_texture = MovieTexture(converted_path) if movie_texture.read(converted_path): self._configureVideoTexture(movie_texture) return movie_texture def _configureVideoTexture(self, texture): from panda3d.core import Texture texture.setWrapU(Texture.WM_clamp) texture.setWrapV(Texture.WM_clamp) texture.setMinfilter(Texture.FT_linear) texture.setMagfilter(Texture.FT_linear) texture.setFormat(Texture.FRgb8) if hasattr(texture, 'set_loop') and hasattr(texture, 'set_play_rate'): texture.set_loop(True) texture.set_play_rate(1.0) def _loadVideoFromURLWithOpenCV(self, video_screen, url): try: import cv2 import threading from panda3d.core import Texture,PNMImage import numpy as np import time def video_stream_thread(): cap = cv2.VideoCapture(url) if not cap.isOpened(): print(f"无法打开视频流: {url}") return False cap.set(cv2.CAP_PROP_BUFFERSIZE,1) fps = cap.get(cv2.CAP_PROP_FPS) frame_delay = 1.0 / fps if fps > 0 else 0.033 # 默认30fps while hasattr(self, 'video_stream_active') and self.video_stream_active: ret, frame = cap.read() if not ret: print("视频流读取失败,尝试重新连接...") cap.release() time.sleep(1) cap = cv2.VideoCapture(url) continue frame_height,frame_width = frame.shape[:2] if frame_width > 256: scale = 256.0/frame_width new_width = int(frame_width * scale) new_height = int(frame_height * scale) frame = cv2.resize(frame,(new_width,new_height)) frame_rgb = cv2.cvtColor(frame,cv2.COLOR_BGR2RGB) height,width = frame_rgb.shape[:2] img = PNMImage(width,height,3) img.set_maxval(255) for y in range(height): for x in range(width): r,g,b = frame_rgb[y,x] img.setXelVal(x,y,r,g,b) texture = Texture("video_texture") texture.setMagfilter(Texture.FTLinear) texture.setMinfilter(Texture.FTLinear) texture.setWrapU(Texture.WMClamp) texture.setWrapV(Texture.WMClamp) texture.load(img) # 在主线程中更新GUI纹理 def update_texture(): if hasattr(self, 'aspect2d') and not video_screen.isEmpty(): video_screen["frameTexture"] = texture # 使用taskMgr在主线程中更新纹理 if hasattr(self, 'taskMgr'): self.taskMgr.doMethodLater(0, lambda task: update_texture() or task.done, f"updateVideoTexture_{time.time()}") # 控制帧率 time.sleep(frame_delay) cap.release() print("视频流线程结束") return True # 启动视频流线程 self.video_stream_active = True self.video_thread = threading.Thread(target=video_stream_thread, daemon=True) self.video_thread.start() return True except Exception as e: print(f"加载视频流失败: {e}") import traceback traceback.print_exc() return False def _loadVideoFromURLWithOpenCV3D(self,video_screen,url): try: import cv2 import threading from panda3d.core import Texture,PNMImage import time def video_stream_thread(): cap = cv2.VideoCapture(url) if not cap.isOpened(): return False cap.set(cv2.CAP_PROP_BUFFERSIZE,1) fps = cap.get(cv2.CAP_PROP_FPS) frame_delay = 1.0 / fps if fps > 0 else 0.033 while hasattr(self,'video_stream_active') and self.video_stream_active: ret,frame = cap.read() if not ret: cap.release() time.sleep(1) cap = cv2.VideoCapture(url) continue frame_height,frame_width = frame.shape[:2] if frame_width > 256: scale = 256.0/frame_width new_width = int(frame_width*scale) new_height = int(frame_height*scale) frame = cv2.resize(frame,(new_width,new_height)) frame_rgb = cv2.cvtColor(frame,cv2.COLOR_BGR2RGB) height,width = frame_rgb.shape[:2] img = PNMImage(width,height,3) img.set_maxval(255) for y in range(height): for x in range(width): r,g,b = frame_rgb[y,x] img.setXelVal(x,y,r,g,b) texture = Texture("video_texture_3d") texture.setMagfilter(Texture.FTLinear) texture.setMinfilter(Texture.FTLinear) texture.setWrapU(Texture.WMClamp) texture.setWrapV(Texture.WMClamp) texture.load(img) def update_texture(): if hasattr(self,'render') and not video_screen.isEmpty(): video_screen.setTexture(texture,1) if hasattr(self,'taskMgr'): self.taskMgr.doMethodLater(0,lambda task:update_texture() or task.done,f"updateVideoTexture_{time.time()}") time.sleep(frame_delay) cap.release() return True self.video_stream_active = True self.video_thread_3d = threading.Thread(target=video_stream_thread,daemon=True) self.video_thread_3d.start() return True except Exception as e: print(f"加载3D视频流失败: {e}") import traceback traceback.print_exc() return False def _ensureVideoScreenMaterial(self, video_screen): """确保视频屏幕有正确的材质设置""" try: from panda3d.core import Material, LColor if not video_screen.hasMaterial(): material = Material(f"video-material-{video_screen.getName()}") material.setBaseColor(LColor(1, 1, 1, 1)) material.setDiffuse(LColor(1, 1, 1, 1)) material.setAmbient(LColor(1, 1, 1, 1)) # 确保环境光为白色 material.setEmission(LColor(0, 0, 0, 1)) material.setSpecular(LColor(0, 0, 0, 1)) material.setShininess(0) video_screen.setMaterial(material, 1) else: # 更新现有材质确保正确设置 material = video_screen.getMaterial() material.setBaseColor(LColor(1, 1, 1, 1)) material.setAmbient(LColor(1, 1, 1, 1)) # 确保环境光为白色 video_screen.setMaterial(material, 1) except Exception as e: print(f"⚠️ 设置视频屏幕材质时出错: {e}") def playModelAnimation(self): """播放场景中所有 .glb 模型的动画(仅转换有动画的模型为 Actor)""" try: glb_models = self.render.findAllMatches("**/*.glb*") # 修复过滤逻辑,确保正确排除碰撞体节点 filtered_models = [] for model in glb_models: model_name = model.getName().lower() # 排除碰撞体节点 if ("collision_" not in model_name and "modelcollision_" not in model_name and not model_name.endswith("_bounds")): filtered_models.append(model) if not filtered_models: print("⚠️ 场景中没有找到 .glb 模型") return print(f"找到 {len(filtered_models)} 个 glb 模型") self.actors = [] # 存储所有 Actor,避免被垃圾回收 for model in filtered_models: print(f"正在处理模型: {model.getName()}") actor = None # 尝试把模型当作Actor加载,判断是否有动画 try: # 在创建Actor之前先检查节点是否可能包含动画 if not self._isValidAnimationNode(model): print(f"⚠️ {model.getName()} 不是有效的动画节点,跳过") continue test_actor = Actor(model) anim_names = test_actor.getAnimNames() except Exception as e: print(f"⚠️ {model.getName()} 无法作为Actor加载: {str(e)}") anim_names = [] if anim_names: # ✅ 只有有动画才转为Actor if not isinstance(model, Actor): model_parent = model.getParent() model_pos = model.getPos() model_hpr = model.getHpr() model_scale = model.getScale() actor = Actor(model) actor.reparentTo(model_parent) actor.setPos(model_pos) actor.setHpr(model_hpr) actor.setScale(model_scale) model.detachNode() else: actor = model # 播放动画 actor.show() self.actors.append(actor) print(f"✓ {actor.getName()} 可用动画: {anim_names}") first_anim = anim_names[0] actor.stop() actor.setPlayRate(1.0, first_anim) actor.loop(first_anim) print(f"✓ {actor.getName()} 正在播放动画: {first_anim}") actor.update() else: # 没有动画 print(f"⚠️ {model.getName()} 没有动画,不转为Actor") except Exception as e: print(f"播放模型动画时出错: {str(e)}") import traceback traceback.print_exc() def _isValidAnimationNode(self, nodePath): """检查节点是否可能是有效的动画节点""" # 排除明显不是动画节点的节点 name = nodePath.getName().lower() if ("collision_" in name or "modelcollision_" in name or "bound" in name or name.endswith("_bounds")): return False # 检查节点是否有网格数据(简单判断) from panda3d.core import GeomNode geom_nodes = nodePath.findAllMatches("**/+GeomNode") if not geom_nodes: return False return True def focusOnWomenModel(self): """定位并聚焦到Women模型""" try: women_models = self.render.findAllMatches("**/Women_1.glb*") if women_models: model = women_models[0] # 确保模型可见 model.show() # 将模型放置在原点附近 model.setPos(0, 0, 0) model.setScale(1.0) # 确保是Actor的话设置动画 if isinstance(model, Actor): anim_names = model.getAnimNames() if anim_names: model.loop(anim_names[0]) model.setPlayRate(1.0, anim_names[0]) print(f"为模型设置动画: {anim_names[0]}") print(f"已定位模型: {model.getName()}") else: print("未找到Women模型") except Exception as e: print(f"定位模型时出错: {str(e)}") def processScripts(self, element, script_info_list): """处理元素上挂载的脚本 - 使用新的脚本系统""" try: print(f"正在为元素 {element.getName()} 挂载脚本") print(f"可用脚本列表: {self.script_manager.get_available_scripts()}") if not hasattr(self,'script_manager'): print("脚本管理器未初始化") return for script_info in script_info_list: script_name = script_info["name"] script_file = script_info.get("file", "") if script_name: script_component = self.script_manager.add_script_to_object(element,script_name) if script_component: print(f"✓ 脚本 {script_name} 已挂载到元素 {element.getName()}") else: print(f"⚠️ 脚本 {script_name} 挂载失败") # 列出可用脚本帮助调试 available_scripts = self.script_manager.get_available_scripts() print(f"当前可用脚本: {available_scripts}") else: print(f"⚠️ 脚本信息不完整: {script_info}") # # 从文件路径中提取脚本类名 # if script_file: # # 获取脚本文件名(不含路径和扩展名) # script_filename = os.path.basename(script_file) # script_class_name = os.path.splitext(script_filename)[0] # print(f"尝试挂载脚本: {script_class_name} (来自文件: {script_file})") # # # 使用脚本管理器为元素添加脚本 # script_component = self.script_manager.add_script_to_object(element, script_class_name) # # if script_component: # print(f"✓ 脚本 {script_class_name} 已挂载到元素 {element.getName()}") # else: # print(f"⚠️ 脚本 {script_class_name} 挂载失败") # # 列出可用脚本帮助调试 # available_scripts = self.script_manager.get_available_scripts() # print(f"当前可用脚本: {available_scripts}") # else: # print(f"⚠️ 脚本信息不完整: {script_name}") except Exception as e: print(f"挂载脚本到元素 {element.getName()} 失败: {str(e)}") import traceback traceback.print_exc() def checkDoubleClick(self, nodePath): """检查是否为双击,返回布尔值 - 改进版本""" try: import time current_time = time.time() # 必须是同一节点且在时间阈值内 is_double_click = (self._last_clicked_node is not None and self._last_clicked_node == nodePath and nodePath is not None and current_time - self._last_click_time < self._double_click_threshold) if is_double_click: # 双击成功,重置状态 self._last_click_time = 0 self._last_clicked_node = None print(f"✓ 检测到双击: {nodePath.getName()}") return True else: # 单击,更新状态 self._last_click_time = current_time self._last_clicked_node = nodePath return False except Exception as e: print(f"双击检测失败: {e}") return False def focusCameraOnNode(self, nodePath): """带动画效果的聚焦到节点方法""" try: if not nodePath or nodePath.isEmpty(): print("无效的节点") return minPoint = Point3() maxPoint = Point3() if not nodePath.calcTightBounds(minPoint, maxPoint,self.render): print("无法计算选中节点的边界框,使用节点为位置作为替代方案") node_pos = nodePath.getPos(self.render) optimal_distance = 10.0 current_cam_pos = self.cam.getPos() view_direction = node_pos - current_cam_pos if view_direction.length()<0.001: view_direction = Vec3(5,-5,2) view_direction.normalize() target_cam_pos = node_pos-(view_direction * optimal_distance) temp_node = self.render.attachNewNode("temp_lookat_target") temp_node.setPos(node_pos) dummy_cam = self.render.attachNewNode("dummy_camera") dummy_cam.setPos(target_cam_pos) dummy_cam.lookAt(temp_node) target_cam_hpr = Vec3(dummy_cam.getHpr()) temp_node.removeNode() dummy_cam.removeNode() current_cam_pos = Point3(self.cam.getPos()) current_cam_hpr = Vec3(self.cam.getHpr()) self._startCameraFocusAnimation(current_cam_pos,target_cam_pos,current_cam_hpr,target_cam_hpr) print(f"开始聚焦到节点(使用位置): {nodePath.getName()}") return center = Point3( (minPoint.x + maxPoint.x)*0.5, (minPoint.y+maxPoint.y)*0.5, (minPoint.z+maxPoint.z)*0.5 ) size = (maxPoint - minPoint).length() if size < 0.01: size = 1.0 current_cam_pos = Point3(self.cam.getPos()) current_cam_hpr = Vec3(self.cam.getHpr()) view_direction = current_cam_pos - center if view_direction.length() < 0.001: view_direction = Vec3(5,-5,2) view_direction.normalize() optimal_distance = max(size * 2.5,1.0) target_cam_pos = center + (view_direction * optimal_distance) temp_node = self.render.attachNewNode("temp_lookat_target") temp_node.setPos(center) dummy_cam = self.render.attachNewNode("dummy_camera") dummy_cam.setPos(target_cam_pos) dummy_cam.lookAt(temp_node) target_cam_hpr = Vec3(dummy_cam.getHpr()) temp_node.removeNode() dummy_cam.removeNode() self._startCameraFocusAnimation(current_cam_pos,target_cam_pos, current_cam_hpr,target_cam_hpr) print(f"开始聚焦到节点: {nodePath.getName()}") return True except Exception as e: print(f"聚焦相机失败: {str(e)}") import traceback traceback.print_exc() def _startCameraFocusAnimation(self, start_pos, end_pos, start_hpr, end_hpr): """启动相机聚焦动画 - 改进版本""" try: class CameraFocusData: def __init__(self, start_pos, end_pos, start_hpr, end_hpr): self.start_pos = Point3(start_pos) self.end_pos = Point3(end_pos) self.start_hpr = Vec3(start_hpr) self.end_hpr = Vec3(end_hpr) self.elapsed_time = 0.0 self.duration = 0.5 # 增加动画时间使移动更平滑 self._camera_focus_data = CameraFocusData(start_pos, end_pos, start_hpr, end_hpr) from direct.task.TaskManagerGlobal import taskMgr taskMgr.remove("cameraFocusTask") taskMgr.add(self._cameraFocusTask, "cameraFocusTask") print(f"开始相机聚焦动画: {start_pos} -> {end_pos}") except Exception as e: print(f"相机聚焦动画启动失败: {str(e)}") def _normalizeAngle(self, angle): """规范化角度到-180到180度之间""" while angle > 180: angle -= 360 while angle < -180: angle += 360 return angle def _cameraFocusTask(self, task): """摄像机聚焦动画任务""" try: if not hasattr(self, '_camera_focus_data'): return task.done data = self._camera_focus_data from direct.showbase.ShowBaseGlobal import globalClock dt = globalClock.getDt() if dt <= 0: dt = 0.016 data.elapsed_time += dt t = min(1.0, data.elapsed_time / data.duration) # 使用平滑插值 smooth_t = t * t * (3 - 2 * t) # 插值位置和朝向 current_pos = data.start_pos + (data.end_pos - data.start_pos) * smooth_t current_hpr = data.start_hpr + (data.end_hpr - data.start_hpr) * smooth_t self.cam.setPos(current_pos) self.cam.setHpr(current_hpr) if t >= 1.0: self.cam.setPos(data.end_pos) self.cam.setHpr(data.end_hpr) print(f"聚焦完成: 位置 {data.end_pos}, 朝向 {data.end_hpr}") if hasattr(self, '_camera_focus_data'): delattr(self, '_camera_focus_data') return task.done return task.cont except Exception as e: print(f"摄像机聚焦动画任务失败: {e}") import traceback traceback.print_exc() return task.done def setupMouseClickHandler(self): """设置鼠标点击处理器""" try: # 设置鼠标监听 self.accept("mouse1", self.onMouseClick) # 启用鼠标观察器 if not hasattr(self, 'mouseWatcherNode'): from panda3d.core import MouseWatcher self.mouseWatcherNode = MouseWatcher() except Exception as e: print(f"设置鼠标点击处理器失败: {e}") def onMouseClick(self): """处理鼠标左键点击""" try: if not hasattr(self, 'mouseWatcherNode') or not self.mouseWatcherNode.hasMouse(): return # 获取鼠标位置 mouse_pos = self.mouseWatcherNode.getMouse() # 创建射线进行点击检测 from panda3d.core import CollisionRay, CollisionNode, CollisionHandlerQueue, BitMask32 # 创建射线 ray = CollisionRay() ray.setFromLens(self.camNode, mouse_pos.x, mouse_pos.y) # 创建碰撞节点 ray_node = CollisionNode('mouseRay') ray_node.addSolid(ray) ray_node.setFromCollideMask(BitMask32.allOn()) # 附加到相机 ray_np = self.camera.attachNewNode(ray_node) # 创建碰撞队列 handler = CollisionHandlerQueue() # 确保碰撞遍历器存在 if not hasattr(self, 'cTrav'): self.cTrav = CollisionTraverser() self.cTrav.addCollider(ray_np, handler) self.cTrav.traverse(self.render) # 检查碰撞结果 target_node = None if handler.getNumEntries() > 0: # 按距离排序 handler.sortEntries() # 遍历所有碰撞结果,找到最近的有效节点 for i in range(handler.getNumEntries()): entry = handler.getEntry(i) clicked_node = entry.getIntoNodePath() # 向上遍历找到可选择的父节点 current_node = clicked_node found_valid_node = False while current_node and not current_node.isEmpty(): # 检查节点是否应该被选择 if (current_node.hasTag("is_scene_element") and not current_node.hasTag("is_gizmo") and current_node.getName() not in ["render", "camera", "ambient_light", "directional_light", "mouseRay"] and not current_node.getName().startswith("collision_") and "ground" not in current_node.getName().lower()): target_node = current_node found_valid_node = True break # 如果当前节点是碰撞体,获取其父节点作为目标 if current_node.getName().startswith("collision_"): parent = current_node.getParent() if (parent.hasTag("is_scene_element") and not parent.hasTag("is_gizmo") and parent.getName() not in ["render", "camera", "ambient_light", "directional_light"] and "ground" not in parent.getName().lower()): target_node = parent found_valid_node = True break current_node = current_node.getParent() if current_node.getName() == "render": break if found_valid_node: break # 如果找到了目标节点,则进行双击检测 if target_node: print(f"找到可选择节点: {target_node.getName()}") print(f"节点位置: {target_node.getPos()}") print(f"节点边界: {target_node.getBounds()}") # 检查是否为双击 if self.checkDoubleClick(target_node): print(f"双击节点: {target_node.getName()}") self.focusCameraOnNode(target_node) else: print(f"单击节点: {target_node.getName()}") else: print("未找到有效的目标节点") # 清理碰撞器 ray_np.removeNode() except Exception as e: print(f"处理鼠标点击失败: {e}") import traceback traceback.print_exc()\ def createFrameRateDisplay(self): from direct.gui.DirectGui import DirectLabel from panda3d.core import TextNode self.fps_label = DirectLabel( text="FPS:0", pos=(-1.8,0,0.9), scale=0.05, text_fg=(1,1,1,1), text_align=TextNode.ALeft, frameSize=(-0.1,0.3,-0.05,0.05), frameColor=(0,0,0,0.5), text_font=self.getChineseFont() if self.getChineseFont() else None, #parent=self.a2dTopLeft ) taskMgr.add(self.updateFrameRate,"updateFrameRateTask") def updateFrameRate(self,task): try: fps = globalClock.getAverageFrameRate() if self.fps_label: self.fps_label.setText(f"FPS:{fps:.1f}") except Exception as e: print(f"更新帧率失败: {e}") return task.cont # 在 main.py 的最后部分,修改为: if __name__ == "__main__": try: app = MainApp() if hasattr(app, 'run'): app.run() else: print("应用程序初始化失败") except Exception as e: print(f"应用程序启动失败: {str(e)}") import traceback traceback.print_exc()