"""Scene manager load/save and GUI rebuild operations.""" import os import shutil import time import json import aiohttp import asyncio import inspect from pathlib import Path from panda3d.core import ( ModelPool, ModelRoot, Filename, NodePath, GeomNode, Material, Vec4, Vec3, MaterialAttrib, ColorAttrib, Point3, CollisionNode, CollisionSphere, CollisionBox, BitMask32, TransparencyAttrib, LColor, TransformState, RenderModeAttrib, ShaderAttrib ) from panda3d.egg import EggData, EggVertexPool from direct.actor.Actor import Actor from RenderPipelineFile.rpplugins.smaa.jitters import halton_seq from scene import util class SceneManagerIOMixin: def _cleanup_untracked_render_children(self): """Remove direct render children left behind by previous scene/runtime state.""" render = getattr(self.world, "render", None) if not render or render.isEmpty(): return keep_exact = { "camera", "ForwardShadingCam", "EnvmapCamRig", "PSSMCameraRig", "PSSMDistShadowsESM", "PSSMSceneSunShadowCam", "SkyAOCaptureCam", "SceneRoot", "alight", "dlight", "ground", } keep_prefixes = ( "ShadowCam-", ) stale_children = [] for child in render.getChildren(): try: name = child.getName() except Exception: continue if name in keep_exact or any(name.startswith(prefix) for prefix in keep_prefixes): continue stale_children.append(child) for child in stale_children: try: child_name = child.getName() if not child.isEmpty(): child.removeNode() print(f"清理残留场景节点: {child_name}") except Exception as e: print(f"清理残留场景节点失败: {e}") def _collectGUIElementInfo(self, gui_node): """收集GUI元素的信息用于保存""" try: # 获取GUI元素类型 gui_type = "unknown" if hasattr(gui_node, 'hasTag') and gui_node.hasTag("gui_type"): gui_type = gui_node.getTag("gui_type") elif hasattr(gui_node, 'hasTag') and gui_node.hasTag("saved_gui_type"): gui_type = gui_node.getTag("saved_gui_type") else: # 尝试从节点名称推断类型 name_lower = gui_node.getName().lower() if "button" in name_lower: gui_type = "button" elif "label" in name_lower: gui_type = "label" elif "entry" in name_lower: gui_type = "entry" elif "image" in name_lower: gui_type = "2d_image" elif "videoscreen" in name_lower: if "2d" in name_lower: gui_type = "2d_video_screen" else: gui_type = "video_screen" elif "info_panel" in name_lower: if "3d" in name_lower: gui_type = "info_panel_3d" else: gui_type = "info_panel" else: # 如果无法识别类型,跳过该元素 print(f"跳过无法识别类型的GUI元素: {gui_node.getName()}") return None gui_info = { "name": gui_node.getName(), "type": gui_type, "position": list(gui_node.getPos()), "rotation": list(gui_node.getHpr()), "scale": list(gui_node.getScale()), "tags": {}, "parent_name":None, "video_path":gui_node.getTag("video_path") if gui_node.hasTag("video_path") else None, "panel_id":gui_node.getTag("panel_id") if gui_node.hasTag("panel_id") else None, } parent = gui_node.getParent() if parent and not parent.isEmpty(): parent_name = parent.getName() if parent_name not in ["render","aspect2d","render2d"]: gui_info["parent_name"] = parent_name # 收集所有标签(仅对NodePath类型的对象) if hasattr(gui_node, 'getTagKeys'): for tag in gui_node.getTagKeys(): gui_info["tags"][tag] = gui_node.getTag(tag) elif hasattr(gui_node, 'getTags'): # 对于DirectGUI对象 # DirectGUI对象使用不同的方法存储标签 if hasattr(gui_node, '_tags'): gui_info["tags"] = gui_node._tags.copy() # 根据类型收集特定信息 if gui_type == "button": if hasattr(gui_node, 'get'): # DirectButton gui_info["text"] = gui_node.get() elif hasattr(gui_node, 'getText'): # 其他类型 gui_info["text"] = gui_node.getText() elif hasattr(gui_node, 'hasTag') and gui_node.hasTag("gui_text"): gui_info["text"] = gui_node.getTag("gui_text") elif gui_type == "label": if hasattr(gui_node, 'getText'): gui_info["text"] = gui_node.getText() elif hasattr(gui_node, 'hasTag') and gui_node.hasTag("gui_text"): gui_info["text"] = gui_node.getTag("gui_text") elif gui_type == "entry": if hasattr(gui_node, 'get'): gui_info["text"] = gui_node.get() elif hasattr(gui_node, 'hasTag') and gui_node.hasTag("gui_text"): gui_info["text"] = gui_node.getTag("gui_text") elif gui_type == "2d_image": if hasattr(gui_node, 'hasTag') and gui_node.hasTag("image_path"): gui_info["image_path"] = gui_node.getTag("image_path") elif hasattr(gui_node, 'hasTag') and gui_node.hasTag("gui_image_path"): gui_info["image_path"] = gui_node.getTag("gui_image_path") elif gui_type == "3d_text": if hasattr(gui_node,'hasTag') and gui_node.hasTag("gui_text"): gui_info["text"] = gui_node.getTag("gui_text") elif hasattr(gui_node,'node') and hasattr(gui_node.node(),'getText'): gui_info["text"] = gui_node.node().getText() elif gui_type == "3d_image": if hasattr(gui_node,'hasTag') and gui_node.hasTag("gui_image_path"): gui_info["image_path"] = gui_node.getTag("gui_image_path") elif gui_type == "video_screen": if hasattr(gui_node, 'hasTag') and gui_node.hasTag("video_path"): gui_info["video_path"] = gui_node.getTag("video_path") elif gui_type == "2d_video_screen": if hasattr(gui_node, 'hasTag') and gui_node.hasTag("video_path"): gui_info["video_path"] = gui_node.getTag("video_path") elif gui_type == "virtual_screen": if hasattr(gui_node, 'hasTag') and gui_node.hasTag("gui_text"): gui_info["text"] = gui_node.getTag("gui_text") elif gui_type in ["info_panel", "info_panel_3d"]: # 收集信息面板的特定信息 if hasattr(gui_node, 'hasTag') and gui_node.hasTag("panel_id"): gui_info["panel_id"] = gui_node.getTag("panel_id") # 收集背景图片信息 if hasattr(gui_node, 'hasTag') and gui_node.hasTag("image_path"): gui_info["image_path"] = gui_node.getTag("image_path") # 收集GUI元素的可见性信息 user_visible = gui_node.getPythonTag("user_visible") if user_visible is not None: gui_info["user_visible"] = user_visible else: # 默认为可见 gui_info["user_visible"] = True # 收集挂载的脚本信息 if hasattr(self.world, 'script_manager') and self.world.script_manager: try: script_manager = self.world.script_manager scripts = script_manager.get_scripts_on_object(gui_node) # 修复:使用 gui_node 而不是 node if scripts: gui_info["scripts"] = [] for script_component in scripts: try: script_name = script_component.script_name # 获取脚本路径 script_class = script_component.script_instance.__class__ script_file = self._get_script_file_path(script_class, script_name) # 只有当脚本文件存在时才保存 if script_file and os.path.exists(script_file): gui_info["scripts"].append({ "name": script_name, "file": script_file }) print(f"收集脚本信息: {script_name} from {script_file}") else: print(f"警告: 脚本文件不存在: {script_file}") except Exception as e: print(f"收集单个脚本信息失败 {script_name}, 错误: {e}") continue except Exception as e: print(f"收集脚本信息失败: {e}") print(f"成功收集GUI元素信息: {gui_info}") return gui_info except Exception as e: print(f"收集GUI元素信息失败: {e}") import traceback traceback.print_exc() return None def saveScene(self, filename,project_path): """保存场景到BAM文件 - 完整版,支持GUI元素,地形""" try: print(f"\n=== 开始保存场景到: {filename} ===") # 确保文件路径是规范化的 filename = os.path.normpath(filename) # 确保目录存在 directory = os.path.dirname(filename) if directory and not os.path.exists(directory): os.makedirs(directory) resources_dir = os.path.join(directory,"resources") if not os.path.exists(resources_dir): os.makedirs(resources_dir) # 存储需要临时隐藏的节点,以便保存后恢复 nodes_to_restore = [] # 查找并隐藏所有坐标轴和选择框节点 gizmo_nodes = self.world.render.findAllMatches("**/gizmo*") selection_box_nodes = self.world.render.findAllMatches("**/selectionBox*") # 隐藏坐标轴节点 for node in gizmo_nodes: if not node.isHidden(): nodes_to_restore.append((node, True)) # (节点, 原先是否可见) node.hide() print(f"临时隐藏坐标轴节点: {node.getName()}") # 隐藏选择框节点 for node in selection_box_nodes: if not node.isHidden(): nodes_to_restore.append((node, True)) node.hide() print(f"临时隐藏选择框节点: {node.getName()}") # 收集所有需要保存的节点 all_nodes = [] all_nodes.extend(self.models) all_nodes.extend(self.Spotlight) all_nodes.extend(self.Pointlight) def expand_scene_package_wrappers(nodes): expanded_nodes = [] ssbo_editor = getattr(self.world, "ssbo_editor", None) source_scene_model = None source_scene_root = None if ssbo_editor: source_scene_model = getattr(ssbo_editor, "source_model", None) source_scene_root = getattr(ssbo_editor, "source_model_root", None) for node in nodes: if not node or node.isEmpty(): continue is_ssbo_runtime_root = ( ssbo_editor and node == getattr(ssbo_editor, "model", None) and source_scene_root and not source_scene_root.isEmpty() ) is_scene_wrapper = ( node.hasTag("scene_import_source") and node.getTag("scene_import_source") == "project_scene_bam" ) if not is_scene_wrapper and not is_ssbo_runtime_root: expanded_nodes.append(node) continue source_children = [] source_snapshot = source_scene_root if is_ssbo_runtime_root else source_scene_model if source_snapshot and not source_snapshot.isEmpty(): for child in source_snapshot.getChildren(): try: if not child or child.isEmpty(): continue if child.getName() in {"render", "render2d", "aspect2d"}: continue source_children.append(child) except Exception: continue if source_children: print(f"展开SSBO场景节点 {node.getName()} -> 使用原始场景树的 {len(source_children)} 个顶层子节点") expanded_nodes.extend(source_children) continue print(f"SSBO场景节点 {node.getName()} 缺少原始场景树,回退为包装根节点保存") expanded_nodes.append(node) return expanded_nodes all_nodes = expand_scene_package_wrappers(all_nodes) # 创建用于保存GUI信息的JSON文件路径 gui_info_file = filename.replace('.bam', '_gui.json') # 保存所有节点的信息 for node in all_nodes: if node.isEmpty(): continue # 保存变换信息 node.setTag("transform_pos", str(node.getPos())) node.setTag("transform_hpr", str(node.getHpr())) node.setTag("transform_scale", str(node.getScale())) print(f"保存节点 {node.getName()} 的变换信息") # 保存可见性信息 user_visible = node.getPythonTag("user_visible") if user_visible is not None: node.setTag("user_visible", str(user_visible).lower()) print(f"保存节点 {node.getName()} 的可见性信息: {user_visible}") # 保存父子关系信息 - 关键修改 parent = node.getParent() if parent and not parent.isEmpty() and parent != self.world.render: # 只有当父节点不是根节点且父节点是场景中的模型时才保存父子关系 if parent.getName() not in ["render", "aspect2d", "render2d"]: # 检查父节点是否也是场景中的模型 is_parent_model = False for model in self.models: if model == parent: is_parent_model = True break if is_parent_model: node.setTag("parent_name", parent.getName()) print(f"保存节点 {node.getName()} 的父节点信息: {parent.getName()}") # 获取当前状态 state = node.getState() # 如果有材质属性,保存为标签 if state.hasAttrib(MaterialAttrib.getClassType()): mat_attrib = state.getAttrib(MaterialAttrib.getClassType()) material = mat_attrib.getMaterial() if material: # 保存材质属性到标签 node.setTag("material_ambient", str(material.getAmbient())) node.setTag("material_diffuse", str(material.getDiffuse())) node.setTag("material_specular", str(material.getSpecular())) node.setTag("material_emission", str(material.getEmission())) node.setTag("material_shininess", str(material.getShininess())) if material.hasBaseColor(): node.setTag("material_basecolor", str(material.getBaseColor())) # 保存特定类型节点的额外信息 if node.hasTag("light_type"): # 保存光源特定信息 light_obj = node.getPythonTag("rp_light_object") if light_obj: node.setTag("light_energy", str(light_obj.energy)) if node.hasTag("stored_energy"): node.setTag("stored_energy", node.getTag("stored_energy")) node.setTag("light_radius", str(getattr(light_obj, 'radius', 0))) if hasattr(light_obj, 'fov'): node.setTag("light_fov", str(light_obj.fov)) elif node.hasTag("element_type"): element_type = node.getTag("element_type") if element_type == "cesium_tileset": # 保存tileset特定信息 if node.hasTag("tileset_url"): node.setTag("saved_tileset_url", node.getTag("tileset_url")) elif node.hasTag("gui_type") or node.hasTag("is_gui_element"): # 保存GUI元素特定信息 gui_type = node.getTag("gui_type") if node.hasTag("gui_type") else \ node.getTag("saved_gui_type") if node.hasTag("saved_gui_type") else "unknown" node.setTag("saved_gui_type", gui_type) # 保存GUI元素的通用属性 if hasattr(node, 'getPythonTag'): # 保存任何Python标签数据 for tag_name in node.getPythonTagKeys(): try: tag_value = node.getPythonTag(tag_name) node.setTag(f"python_tag_{tag_name}", str(tag_value)) except: pass # 保存模型动画信息 elif node.hasTag("is_model_root"): # 保存模型动画相关信息 if node.hasTag("has_animations"): node.setTag("saved_has_animations", node.getTag("has_animations")) if node.hasTag("model_path"): node.setTag("saved_model_path", node.getTag("model_path")) if node.hasTag("can_create_actor_from_memory"): node.setTag("saved_can_create_actor_from_memory", node.getTag("can_create_actor_from_memory")) if hasattr(self.world,'script_manager') and self.world.script_manager: script_manager = self.world.script_manager scripts = script_manager.get_scripts_on_object(node) if scripts: node.setTag("has_scripts", "true") script_info_list = [] for script_component in scripts: script_name = script_component.script_name print(f"保存脚本信息: {script_name}") # 获取脚本类的文件路径 script_class = script_component.script_instance.__class__ script_file = self._get_script_file_path(script_class, script_name) script_info_list.append({ "name": script_name, "file": script_file }) # 将脚本信息保存为JSON字符串 import json node.setTag("scripts_info", json.dumps(script_info_list, ensure_ascii=False)) print(f"为节点 {node.getName()} 保存了 {len(script_info_list)} 个脚本") try: unique_nodes = [] for node in all_nodes: if not node or node.isEmpty(): continue if any(existing == node for existing in unique_nodes): continue unique_nodes.append(node) save_root = NodePath(ModelRoot("SavedSceneRoot")) def has_saved_parent(node): parent = node.getParent() if not parent or parent.isEmpty() or parent == self.world.render: return False return any(candidate == parent for candidate in unique_nodes) def strip_runtime_render_state(root_np): for current in [root_np] + list(root_np.findAllMatches("**")): try: current.clearShader() except Exception: pass try: current.clearAttrib(ShaderAttrib.getClassType()) except Exception: pass for node in unique_nodes: if has_saved_parent(node): continue clone = node.copyTo(save_root) strip_runtime_render_state(clone) self.take_screenshot(project_path) # 保存场景 success = save_root.writeBamFile(Filename.fromOsSpecific(filename)) if success: print(f"✓ 场景保存成功: {filename}") else: print("✗ 场景保存失败") return success finally: try: if 'save_root' in locals() and save_root and not save_root.isEmpty(): save_root.removeNode() except Exception: pass # 恢复之前隐藏的节点 for item in nodes_to_restore: node, was_visible = item if was_visible and not node.isEmpty(): node.show() print(f"恢复显示节点: {node.getName()}") if nodes_to_restore: print(f"已恢复 {len(nodes_to_restore)} 个辅助节点的显示") except Exception as e: print(f"保存场景时发生错误: {str(e)}") import traceback traceback.print_exc() return False def take_screenshot(self, projectpath): """ 截图并保存到指定的完整路径 Args: full_path (str): 完整的文件保存路径,包括文件名和扩展名 Returns: bool: 截图是否成功 """ try: from panda3d.core import Filename import os print(f"\n=== 截图保存: {projectpath} ===") # 确保目录存在 directory = os.path.dirname(projectpath) if directory and not os.path.exists(directory): os.makedirs(directory) print(f"创建目录: {directory}") # 规范化路径 filename = os.path.basename(os.path.normpath(projectpath)) filename = f'{filename}.png' print(f'project_path: {projectpath}') print(f'project_name: {filename}') full_path = os.path.normpath(os.path.join(projectpath, filename)) p3d_filename = Filename.from_os_specific(full_path) # 使用 Panda3D 的截图功能 success = self.world.win.saveScreenshot(p3d_filename) if success: print(f"✅ 成功截图并保存到: {full_path}") return True else: print(f"❌ 截图保存失败: {full_path}") return False except Exception as e: print(f"保存截图时发生错误: {str(e)}") import traceback traceback.print_exc() return False def loadScene(self, filename, retry_count=0): """从BAM文件加载场景""" max_retries = 3 try: print(f"\n=== 开始加载场景: {filename} (尝试 {retry_count + 1}/{max_retries + 1}) ===") # 确保文件路径是规范化的 filename = os.path.normpath(filename) # print(f"[DEBUG] 规范化后的文件路径: {filename}") # 检查文件是否存在 if not os.path.exists(filename): print(f"场景文件不存在: {filename}") return False # print(f"[DEBUG] 文件大小: {os.path.getsize(filename)} bytes") # 检查文件是否损坏 if os.path.getsize(filename) == 0: # print(f"[DEBUG] 错误: 场景文件为空") return False if retry_count == 0: try: selection = getattr(self.world, "selection", None) if selection: selection.clearSelection() except Exception as e: print(f"清理选择状态失败: {e}") try: script_manager = getattr(self.world, "script_manager", None) if script_manager and hasattr(script_manager, "reset_scene_state"): script_manager.reset_scene_state() except Exception as e: print(f"清理脚本场景状态失败: {e}") try: ssbo_editor = getattr(self.world, "ssbo_editor", None) if ssbo_editor and hasattr(ssbo_editor, "reset_scene_state"): ssbo_editor.reset_scene_state() except Exception as e: print(f"清理 SSBO 场景状态失败: {e}") # 预防性清理:确保Panda3D处于稳定状态 if retry_count > 0: # print(f"[DEBUG] 执行预防性清理...") try: # 清理所有可能的残留状态 from panda3d.core import ModelPool ModelPool.releaseAllModels() # 强制垃圾回收 import gc gc.collect() # 清理渲染状态 if hasattr(self.world, 'render') and self.world.render: # 清理渲染节点的子节点(保留基本节点) children_to_remove = [] for i in range(self.world.render.getNumChildren()): child = self.world.render.getChild(i) if child.getName() not in ['camera', 'alight', 'dlight', 'ambient', 'render']: children_to_remove.append(child) for child in children_to_remove: if not child.isEmpty(): child.removeNode() # print(f"[DEBUG] 清理了 {len(children_to_remove)} 个渲染子节点") except Exception as cleanup_error: # print(f"[DEBUG] 预防性清理时发生异常: {cleanup_error}") pass # print(f"[DEBUG] 预加载检查完成,开始正式加载...") tree_widget = self._get_tree_widget() # print(f"[DEBUG] 获取树形控件: {tree_widget is not None}") # 清除当前场景 print("\n清除当前场景...") # print(f"[DEBUG] 需要清理的模型数量: {len(self.models)}") for i, model in enumerate(self.models): # print(f"[DEBUG] 清理模型 {i+1}/{len(self.models)}: {model.getName() if model else 'None'}") if tree_widget: tree_widget.delete_item(model) else: # 直接移除节点 if not model.isEmpty(): model.removeNode() self.models.clear() # print(f"[DEBUG] 模型清理完成") # 清除灯光 for light_node in self.Spotlight: if tree_widget: tree_widget.delete_item(light_node) else: if not light_node.isEmpty(): light_node.removeNode() for light_node in self.Pointlight: if tree_widget: tree_widget.delete_item(light_node) else: if not light_node.isEmpty(): light_node.removeNode() if hasattr(self.world, 'terrain_manager') and self.world.terrain_manager and hasattr(self.world.terrain_manager, 'terrains'): for terrain in self.world.terrain_manager.terrains: if tree_widget: tree_widget.delete_item(terrain) else: if terrain and not terrain.isEmpty(): terrain.removeNode() for light in self.Spotlight: if not light.isEmpty(): light.removeNode() self.Spotlight.clear() for light in self.Pointlight: if not light.isEmpty(): light.removeNode() self.Pointlight.clear() # 清理可能存在的辅助节点 self._cleanupAuxiliaryNodes() self._cleanup_untracked_render_children() # 加载场景 # print(f"[DEBUG] 开始加载BAM文件...") # 安全清理措施 try: # 1. 清理Panda3D模型缓存 from panda3d.core import ModelPool ModelPool.releaseAllModels() # print(f"[DEBUG] 模型缓存已清理") # 2. 强制垃圾回收 import gc gc.collect() # print(f"[DEBUG] 垃圾回收完成") # 3. 检查文件状态 if not os.access(filename, os.R_OK): # print(f"[DEBUG] 文件不可读: {filename}") return False # 4. 使用更安全的加载方式 # print(f"[DEBUG] 尝试加载BAM文件...") # 设置加载选项 from panda3d.core import LoaderOptions loader_options = LoaderOptions() loader_options.setFlags(loader_options.LFNoCache) # 禁用缓存 # 兼容中文路径:优先使用宽字符接口,再回退常规接口。 candidate_files = [] for ctor_name in ("fromOsSpecificW", "from_os_specific_w", "fromOsSpecific", "from_os_specific"): ctor = getattr(Filename, ctor_name, None) if not ctor: continue try: fn = ctor(filename) key = fn.get_fullpath() if key not in [c.get_fullpath() for c in candidate_files]: candidate_files.append(fn) except Exception: continue if not candidate_files: candidate_files = [Filename(filename)] scene = None last_error = None for panda_filename in candidate_files: try: scene = self.world.loader.loadModel(panda_filename, loader_options) if scene and not scene.isEmpty(): break except Exception as e: last_error = e scene = None # 极端回退:把场景复制到ASCII路径再加载(规避少数编码问题) if (not scene or scene.isEmpty()) and os.path.exists(filename): try: fallback_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "_scene_load_cache") os.makedirs(fallback_dir, exist_ok=True) fallback_file = os.path.join(fallback_dir, "scene_fallback.bam") shutil.copy2(filename, fallback_file) scene = self.world.loader.loadModel(Filename.fromOsSpecific(fallback_file), loader_options) if scene and not scene.isEmpty(): print("[SceneLoad] Fallback loaded from ASCII cache path.") except Exception as e: last_error = e if not scene or scene.isEmpty(): print(f"场景加载失败: {filename}") if last_error: print(f"[SceneLoad] 最后错误: {last_error}") return False # print(f"[DEBUG] BAM文件加载成功") except Exception as e: # print(f"[DEBUG] BAM加载过程中发生异常: {e}") import traceback traceback.print_exc() return False # print(f"[DEBUG] BAM文件加载成功,场景根节点: {scene.getName()}") # print(f"[DEBUG] 场景子节点数量: {scene.getNumChildren()}") # 验证场景节点的有效性 if scene.isEmpty(): # print(f"[DEBUG] 警告: 加载的场景节点为空") return False if not scene.hasParent(): # print(f"[DEBUG] 将场景节点临时挂载到render") scene.reparentTo(self.world.render) if tree_widget: # print(f"[DEBUG] 创建模型项...") try: tree_widget.create_model_items(scene) # print(f"[DEBUG] 模型项创建完成") except Exception as e: # print(f"[DEBUG] 创建模型项失败: {e}") # 继续执行,不影响场景加载 pass else: # print(f"[DEBUG] 树形控件为空,跳过创建模型项") pass # 遍历场景中的所有模型节点 # 用于存储处理后的灯光节点,避免重复处理 processed_lights = [] # 用于存储处理后的GUI元素,避免重复处理 #存储所有加载的节点,用于后续处理父子关系 loaded_nodes = {} #name->nodePath映射 use_ssbo_scene_import = bool( getattr(self.world, "use_ssbo_mouse_picking", False) and getattr(self.world, "use_ssbo_scene_import", False) and getattr(self.world, "ssbo_editor", None) and callable(getattr(self.world, "_import_model_for_runtime", None)) ) ssbo_scene_model = None if use_ssbo_scene_import: try: print(f"[SSBO] 打开项目使用统一导入链路: {filename}") ssbo_scene_model = self.world._import_model_for_runtime( filename, scene_package_import=True, ) if ssbo_scene_model and not ssbo_scene_model.isEmpty(): if ssbo_scene_model not in self.models: self.models.append(ssbo_scene_model) else: print("[SSBO] 统一导入未返回有效模型,回退旧流程。") use_ssbo_scene_import = False except Exception as e: print(f"[SSBO] 统一导入失败,回退旧流程: {e}") use_ssbo_scene_import = False # 遍历场景中的所有节点 def processNode(nodePath, depth=0): indent = " " * depth # print(f"{indent}处理节点: {nodePath.getName()} (类型: {type(nodePath.node()).__name__})") # 检查节点是否有效 if nodePath.isEmpty(): # print(f"{indent}[DEBUG] 节点为空,跳过处理") return #存储节点以便后续处理父子关系 loaded_nodes[nodePath.getName()] = nodePath if nodePath.getName().startswith('ground'): print(f"{indent}跳过ground节点: {nodePath.getName()}") return # 跳过render节点的递归 if nodePath.getName() == "render" and depth > 0: print(f"{indent}跳过重复的render节点") return # 跳过光源节点 if nodePath.getName() in ["alight", "dlight"]: print(f"{indent}跳过光源节点: {nodePath.getName()}") return # 跳过相机节点 if nodePath.getName() in ["camera", "cam"]: print(f"{indent}跳过相机节点: {nodePath.getName()}") return # 跳过辅助节点 if nodePath.getName().startswith(("gizmo", "selectionBox")): print(f"{indent}跳过辅助节点: {nodePath.getName()}") return if nodePath.getName() in ['SceneRoot'] or \ any(keyword in nodePath.getName() for keyword in ["Skybox", "skybox"]): print(f"{indent}跳过环境节点:{nodePath.getName()}") return # 检查是否是用户创建的场景元素 is_scene_element = ( nodePath.hasTag("is_scene_element") or nodePath.hasTag("is_model_root") or nodePath.hasTag("light_type") or nodePath.hasTag("gui_type") or # 检查gui_type标签 nodePath.hasTag("is_gui_element") or nodePath.hasTag("saved_gui_type") or (nodePath.hasTag("element_type") and nodePath.getTag("element_type") == "info_panel") ) # 特殊处理:检查节点名称是否包含GUI相关关键词 is_potential_gui = any(keyword in nodePath.getName().lower() for keyword in ["gui", "button", "label", "entry", "image", "video", "screen", "text"]) if is_scene_element or is_potential_gui: print(f"{indent}找到场景元素节点: {nodePath.getName()}") is_model_root = nodePath.hasTag("is_model_root") # 如果是潜在的GUI元素但没有标签,添加基本标签 if is_potential_gui and not (nodePath.hasTag("gui_type") or nodePath.hasTag("is_gui_element")): print(f"{indent}为潜在GUI元素添加标签: {nodePath.getName()}") nodePath.setTag("is_gui_element", "1") nodePath.setTag("is_scene_element", "1") # 尝试从名称推断类型 name_lower = nodePath.getName().lower() if "button" in name_lower: nodePath.setTag("gui_type", "button") elif "label" in name_lower: nodePath.setTag("gui_type", "label") elif "entry" in name_lower: nodePath.setTag("gui_type", "entry") elif "image" in name_lower: nodePath.setTag("gui_type", "image") elif "video" in name_lower or "screen" in name_lower: nodePath.setTag("gui_type", "video_screen") else: nodePath.setTag("gui_type", "unknown") # 保留原始材质/颜色状态,避免在场景恢复阶段造成黑模。 # 恢复变换信息 def parseVec3(vec_str): """解析向量字符串为Vec3""" try: vec_str = vec_str.replace('LVecBase3f', '').replace('LPoint3f', '').strip('()') x, y, z = map(float, vec_str.split(',')) return Vec3(x, y, z) except Exception as e: print(f"解析向量失败: {vec_str}, 错误: {e}") return Vec3(0, 0, 0) if nodePath.hasTag("transform_pos"): pos = parseVec3(nodePath.getTag("transform_pos")) nodePath.setPos(pos) print(f"{indent}恢复位置: {pos}") if nodePath.hasTag("transform_hpr"): hpr = parseVec3(nodePath.getTag("transform_hpr")) nodePath.setHpr(hpr) print(f"{indent}恢复旋转: {hpr}") if nodePath.hasTag("transform_scale"): scale = parseVec3(nodePath.getTag("transform_scale")) nodePath.setScale(scale) print(f"{indent}恢复缩放: {scale}") # 恢复可见性状态 user_visible = True if nodePath.hasTag("user_visible"): user_visible = nodePath.getTag("user_visible").lower() == "true" # 设置用户可见性标记 nodePath.setPythonTag("user_visible", user_visible) # 应用可见性状态 if hasattr(self.world, 'property_panel'): self.world.property_panel._syncEffectiveVisibility(nodePath) else: # 如果没有属性面板,直接应用可见性 if user_visible: nodePath.show() else: nodePath.hide() if ( nodePath.hasTag("has_scripts") and nodePath.getTag("has_scripts") == "true" and not (use_ssbo_scene_import and is_model_root) ): if hasattr(self.world,'script_manager') and self.world.script_manager: try: import json scripts_info = json.loads(nodePath.getTag("scripts_info")) print(f"节点 {nodePath.getName()} 需要重新挂载 {len(scripts_info)} 个脚本") script_manager = self.world.script_manager for script_info in scripts_info: script_name = script_info["name"] script_file = script_info.get("file","") print(f"尝试重新挂载脚本{script_name}from {script_file}") if script_name not in script_manager.loader.script_classes: if script_file and os.path.exists(script_file): print(f"从文件加载脚本:{script_file}") loaded_class = script_manager.load_script_from_file(script_file) if loaded_class is None: print(f"从文件加载脚本失败{script_file}") script_path = self._find_script_in_directory(script_name) if script_path: print(f"从目录找到脚本并加载{script_path}") script_manager.load_script_from_file(script_path) else: script_path = self._find_script_in_directory(script_name) if script_path: print(f"从目录找到脚本并加载: {script_path}") script_manager.load_script_from_file(script_path) else: print(f"找不到脚本文件: {script_name}") if script_name in script_manager.loader.script_classes: script_component = script_manager.add_script_to_object(nodePath,script_name) if script_component: print(f"成功为 {nodePath.getName()} 添加脚本: {script_name}") else: print(f"为 {nodePath.getName()} 添加脚本失败: {script_name}") else: print(f"脚本 {script_name} 不可用,跳过挂载") except Exception as e: print(f"重新挂载脚本失败: {e}") import traceback traceback.print_exc() # 恢复材质属性 def parseColor(color_str): """解析颜色字符串为Vec4""" try: color_str = color_str.replace('LVecBase4f', '').strip('()') r, g, b, a = map(float, color_str.split(',')) return Vec4(r, g, b, a) except: return Vec4(1, 1, 1, 1) if not is_model_root: # 创建并恢复材质 material = Material() material_changed = False if nodePath.hasTag("material_ambient"): material.setAmbient(parseColor(nodePath.getTag("material_ambient"))) material_changed = True if nodePath.hasTag("material_diffuse"): material.setDiffuse(parseColor(nodePath.getTag("material_diffuse"))) material_changed = True if nodePath.hasTag("material_specular"): material.setSpecular(parseColor(nodePath.getTag("material_specular"))) material_changed = True if nodePath.hasTag("material_emission"): material.setEmission(parseColor(nodePath.getTag("material_emission"))) material_changed = True if nodePath.hasTag("material_shininess"): material.setShininess(float(nodePath.getTag("material_shininess"))) material_changed = True if nodePath.hasTag("material_basecolor"): material.setBaseColor(parseColor(nodePath.getTag("material_basecolor"))) material_changed = True if material_changed: nodePath.setMaterial(material) # 恢复颜色属性 if nodePath.hasTag("color"): nodePath.setColor(parseColor(nodePath.getTag("color"))) # 处理特定类型的节点 if nodePath.hasTag("light_type"): light_type = nodePath.getTag("light_type") print(f"{indent}检测到光源类型: {light_type}") # 检查是否已经处理过这个灯光 if nodePath not in processed_lights: # 重新创建RP光源对象 if light_type == "spot_light": self._recreateSpotLight(nodePath) elif light_type == "point_light": self._recreatePointLight(nodePath) # 标记为已处理 processed_lights.append(nodePath) elif nodePath.hasTag("element_type"): element_type = nodePath.getTag("element_type") if element_type == "cesium_tileset": tileset_url = nodePath.getTag("saved_tileset_url") if nodePath.hasTag( "saved_tileset_url") else "" tileset_info = { 'url': tileset_url, 'node': nodePath, 'position': nodePath.getPos(), 'tiles': {} } # 将节点重新挂载到render下(如果需要) # 注意:GUI元素可能需要挂载到特定的父节点上 if nodePath.hasTag("gui_type") or nodePath.hasTag("is_gui_element"): # GUI元素通常应该挂载到aspect2d或特定的GUI父节点上 # 这里我们先保持原挂载关系 pass else: if use_ssbo_scene_import and is_model_root: # SSBO 模式下模型节点由新导入链路接管,这里不直接挂载。 pass # 其他节点确保挂载到render下 elif nodePath.getParent() != self.world.render and not nodePath.getName() in ["render", "aspect2d", "render2d"]: nodePath.wrtReparentTo(self.world.render) # 为模型节点设置碰撞检测 if is_model_root: print(f"J{indent}处理模型节点{nodePath.getName()}") if use_ssbo_scene_import: # SSBO 模式下整个 scene.bam 已通过统一导入链路载入, # 这里跳过逐模型旧导入逻辑,避免与菜单导入路径不一致。 pass else: #self._validateAndFixAllTransforms(nodePath) self._fixModelStructure(nodePath) self._restoreModelAnimationInfo(nodePath) self._processModelAnimations(nodePath) try: ssbo_editor = getattr(self.world, "ssbo_editor", None) repair_fn = getattr(ssbo_editor, "_repair_missing_textures", None) if ssbo_editor else None if callable(repair_fn): model_path = "" for tag_name in ("model_path", "saved_model_path", "original_path", "file"): if nodePath.hasTag(tag_name): candidate = nodePath.getTag(tag_name).strip() if candidate: model_path = candidate break repair_fn(nodePath, model_path or filename) except Exception as e: print(f"[SceneLoad] 贴图修复失败: {e}") # if self.world.property_panel._hasCollision(nodePath): # print(f"{indent}模型{nodePath.getName()}已有碰撞体,跳过碰撞体设置") # else: # print(f"{indent}为模型{nodePath.getName()}设置碰撞检测") # self.setupCollision(nodePath) self.models.append(nodePath) # 递归处理子节点 for child in nodePath.getChildren(): processNode(child, depth + 1) print("\n开始处理场景节点...") processNode(scene) # SSBO 模式下模型已在前面统一导入;若失败已自动回退旧流程。 #处理父子关系 - 在所有节点加载完成后设置正确的父子关系 print("\n开始重建父子关系...") self._rebuildParentChildRelationships(loaded_nodes) # 加载GUI信息并重新创建非3D的GUI元素 gui_info_file = filename.replace('.bam', '_gui.json') if os.path.exists(gui_info_file): try: with open(gui_info_file, 'r', encoding='utf-8') as f: content = f.read().strip() if content: # 检查文件是否为空 import json gui_data = json.loads(content) print(f"✓ 成功加载GUI信息文件: {gui_info_file}") print(f" 发现 {len(gui_data)} 个GUI元素需要重建") # 使用gui_manager重新创建GUI元素 #self._recreateGUIElementsFromData(gui_data) else: print("ℹ️ GUI信息文件为空") except json.JSONDecodeError as e: print(f"✗ GUI信息文件格式错误: {e}") except Exception as e: print(f"✗ 加载GUI信息失败: {e}") import traceback traceback.print_exc() else: print("ℹ️ 未找到GUI信息文件") # 移除临时场景节点 if not scene.isEmpty(): scene.removeNode() # 更新场景树 #self.updateSceneTree() #self._get_tree_widget().create_model_items(scene) print("=== 场景加载完成 ===\n") return True except Exception as e: print(f"=== 场景加载失败 ===") print(f"加载场景时发生错误: {str(e)}") import traceback traceback.print_exc() # 重试机制 if retry_count < 3: print(f"[DEBUG] 准备重试... ({retry_count + 1}/{3})") try: # 清理状态 import gc gc.collect() from panda3d.core import ModelPool ModelPool.releaseAllModels() # 等待一小段时间后重试 import time time.sleep(0.5) return self.loadScene(filename, retry_count + 1) except Exception as retry_error: print(f"[DEBUG] 重试失败: {retry_error}") return False def _rebuildParentChildRelationships(self, loaded_nodes): try: parent_child_relations = [] for node_name, node in loaded_nodes.items(): if node.hasTag("parent_name"): parent_name = node.getTag("parent_name") if parent_name in loaded_nodes: parent_child_relations.append((node, loaded_nodes[parent_name])) # 修复:应该是元组 print(f"发现父子关系:{parent_name}->{node_name}") else: print(f"警告:节点{node_name}的父节点{parent_name}不存在") for child_node, parent_node in parent_child_relations: try: child_node.wrtReparentTo(parent_node) print(f"成功设置父子关系:{parent_node.getName()}->{child_node.getName()}") except Exception as e: print(f"设置父子关系失败{parent_node.getName()}->{child_node.getName()}:{e}") if not parent_child_relations: print("尝试从场景结构推断父子关系") self._inferParentChildRelationships(loaded_nodes) print("父子关系重建完成") except Exception as e: print(f"重建父子关系时出错: {e}") import traceback traceback.print_exc() except Exception as e: print(f"重建父子关系时出错: {e}") import traceback traceback.print_exc() def _inferParentChildRelationships(self, loaded_nodes): """从场景结构推断父子关系""" try: # 这里可以添加更复杂的父子关系推断逻辑 # 例如,根据节点名称、位置关系等进行推断 # 目前保持简单,后续可以扩展 print("父子关系推断完成(当前为空实现)") except Exception as e: print(f"推断父子关系时出错: {e}") def _shouldSkipNodeInTree(self, nodePath): """判断节点是否应该在场景树中跳过显示""" if nodePath.getName().startswith('ground'): return True # 跳过render节点的递归 if nodePath.getName() == "render": return True # 跳过光源节点 if nodePath.getName() in ["alight", "dlight"]: return True # 跳过相机节点 if nodePath.getName() in ["camera", "cam"]: return True # 跳过3D文本和3D图像节点 if (hasattr(nodePath.node(), "hasTag") and nodePath.node().hasTag("gui_type") and nodePath.node().getTag("gui_type") in ["3d_text", "3d_image"]): return True # 跳过辅助节点 if nodePath.getName().startswith(("gizmo", "selectionBox")): return True return False def _find_script_in_directory(self, script_name): """在脚本目录中查找脚本文件""" try: if hasattr(self.world, 'script_manager') and self.world.script_manager: script_manager = self.world.script_manager scripts_dir = script_manager.scripts_directory if os.path.exists(scripts_dir): # 首先精确匹配 for file_name in os.listdir(scripts_dir): if file_name.endswith('.py'): base_name = os.path.splitext(file_name)[0] if base_name == script_name: return os.path.join(scripts_dir, file_name) # 如果没有精确匹配,尝试模糊匹配 for file_name in os.listdir(scripts_dir): if file_name.endswith('.py'): base_name = os.path.splitext(file_name)[0] if script_name.lower() in base_name.lower() or base_name.lower() in script_name.lower(): return os.path.join(scripts_dir, file_name) except Exception as e: print(f"查找脚本文件时出错: {e}") return None