#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 测试动画模型 - Panda3D应用程序 使用Panda3D引擎编辑器创建 """ from __future__ import print_function import json from direct.actor.Actor import Actor from panda3d.core import TextNode, CardMaker, TextureStage, NodePath, Texture, TransparencyAttrib, CollisionTraverser 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 Render """) # 简化 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) #self.render_pipeline.pre_show_base_init() #ShowBase.__init__(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() # 加载所有脚本 self.script_manager.load_all_scripts_from_directory() self.info_panel_manager = InfoPanelManager(self) try: # 再导入controller模块 from rpcore.util.movement_controller import MovementController self.controller = MovementController(self) 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._loadFont() self.loadFullScene() self.loadGUIFromJSON() self.setupMouseClickHandler() self.cTrav = CollisionTraverser() 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 _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("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 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) # 处理GUI元素 #self.processGUIElements(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.resetWomenModel ) 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": scale_value = absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 0.2 new_element = self.createGUI2DVideoScreen( 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 createGUI2DVideoScreen(self, pos=(0, 0, 0), size=0.2, video_path=None): import os if video_path and not os.path.isabs(video_path): image_path = self.getResourcePath(video_path) from direct.gui.DirectGui import DirectFrame from panda3d.core import TransparencyAttrib, Texture, TextureStage import os 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 and 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 import os movie_texture = MovieTexture(video_path) if movie_texture.read(video_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) if hasattr(texture, 'set_loop') and hasattr(texture, 'set_play_rate'): texture.set_loop(True) texture.set_play_rate(1.0) def resetWomenModel(self): """调整 Women_1.glb 模型大小,实现从小到大再到小的完整循环效果""" try: # 查找 Women_1.glb 模型 women_models = self.render.findAllMatches("**/Women_1.glb*") if women_models: for model in women_models: # 定义完整的缩放级别序列(从0.5到3.0再回到0.5) scale_levels = [0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 2.5, 2.0, 1.5, 1.0, 0.5] # 获取当前缩放值 current_scale = model.getScale() # 查找当前最接近的缩放级别索引 current_index = 0 min_diff = float('inf') for i, scale in enumerate(scale_levels): diff = abs(current_scale.x - scale) if diff < min_diff: min_diff = diff current_index = i # 计算下一个缩放级别(循环) next_index = (current_index + 1) % len(scale_levels) next_scale = scale_levels[next_index] # 应用新的缩放 model.setScale(next_scale) print(f"✓ 调整模型 {model.getName()} 大小: {current_scale.x:.1f} -> {next_scale:.1f}") print(f" 当前索引: {current_index}, 下一个索引: {next_index}") else: print("⚠️ 未找到 Women_1.glb 模型") except Exception as e: print(f"调整模型大小时出错: {str(e)}") import traceback traceback.print_exc() 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 == nodePath and nodePath is not None and current_time - self._last_click_time180: 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 data.elapsed_time += globalClock.getDt() 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 self.cam.setPos(current_pos) start_h = self._normalizeAngle(data.start_hpr.x) end_h = self._normalizeAngle(data.end_hpr.x) start_p = self._normalizeAngle(data.start_hpr.y) end_p = self._normalizeAngle(data.end_hpr.y) start_r = self._normalizeAngle(data.start_hpr.z) end_r = self._normalizeAngle(data.end_hpr.z) if abs(end_h - start_h)>180: if end_h > start_h: start_h += 360 else: end_h += 360 if abs(end_p - start_p) > 180: if end_p > start_p: start_p += 360 else: end_p += 360 if abs(end_r - start_r)>180: if end_r > start_r: start_r += 360 else: end_r += 360 current_hpr = Vec3( start_h + (end_h - start_h)*smooth_t, start_p + (end_p - start_p)*smooth_t, start_r + (end_r - start_r)*smooth_t ) current_hpr.x = self._normalizeAngle(current_hpr.x) current_hpr.y = self._normalizeAngle(current_hpr.y) current_hpr.z = self._normalizeAngle(current_hpr.z) self.cam.setHpr(current_hpr) if t>=1.0: return task.done return task.cont except Exception as e: print(f"摄像机聚焦动画失败{e}") 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 from panda3d.core import GeomNode # 创建射线 ray = CollisionRay() ray.setFromLens(self.camNode, mouse_pos.x, mouse_pos.y) # 创建碰撞节点 ray_node = CollisionNode('mouseRay') ray_node.addSolid(ray) ray_node.setFromCollideMask(BitMask32.bit(0)) # 附加到相机 ray_np = self.camera.attachNewNode(ray_node) # 创建碰撞队列 handler = CollisionHandlerQueue() # 修复语法错误:正确初始化碰撞遍历器 if not hasattr(self, 'cTrav') or self.cTrav is None: self.cTrav = CollisionTraverser() self.cTrav.addCollider(ray_np, handler) self.cTrav.traverse(self.render) # 检查碰撞结果 if handler.getNumEntries() > 0: # 按距离排序 handler.sortEntries() # 获取最近的碰撞节点 entry = handler.getEntry(0) clicked_node = entry.getIntoNodePath() # 向上遍历找到可选择的父节点 current_node = clicked_node while current_node and not current_node.isEmpty(): # 检查节点是否应该被选择 if (current_node.hasTag("is_scene_element") or current_node.hasTag("element_type") or current_node.getName() not in ["render", "camera", "ambient_light", "directional_light"]): # 检查是否为双击 if self.checkDoubleClick(current_node): print(f"双击节点: {current_node.getName()}") self.focusCameraOnNode(current_node) break else: print(f"单击节点: {current_node.getName()}") current_node = current_node.getParent() # 避免无限循环 if current_node.getName() == "render": break # 清理碰撞器 ray_np.removeNode() except Exception as e: print(f"处理鼠标点击失败: {e}") import traceback traceback.print_exc() # 在 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()