#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 场景管理器 - 负责场景和模型管理的核心功能 处理模型导入、场景树构建、材质系统、碰撞设置等 """ import os from PyQt5.QtCore import Qt from panda3d.core import ( ModelPool, ModelRoot, Filename, NodePath, GeomNode, Material, Vec4, Vec3, MaterialAttrib, ColorAttrib, Point3, CollisionNode, CollisionSphere, BitMask32, TransparencyAttrib,LColor ) import json import aiohttp import asyncio import inspect from pathlib import Path from panda3d.egg import EggData, EggVertexPool from direct.actor.Actor import Actor from QPanda3D.Panda3DWorld import get_render_pipeline from scene import util class CesiumIntegration: def __init__(self, scene_manager): self.scene_manager = scene_manager self.world = scene_manager.world self.tilesets = {} def add_tileset(self,name,url,position=(0,0,0)): try: tileset_node = self.scene_manager.load_cesium_tileset(url,position) if tileset_node: self.tilesets[name] = { 'node':tileset_node, 'url':url, 'position':position } print(f"✓ 添加 Cesium tileset: {name}") return tileset_node else: print(f"✗ 添加 Cesium tileset 失败: {name}") return None except Exception as e: print(f"✗ 添加 Cesium tileset 出错: {e}") return None def remove_tileset(self, name): """移除 tileset""" if name in self.tilesets: tileset_info = self.tilesets[name] tileset_info['node'].removeNode() del self.tilesets[name] print(f"✓ 移除 Cesium tileset: {name}") return True return False def get_tileset(self, name): """获取 tileset""" return self.tilesets.get(name, None) def list_tilesets(self): """列出所有 tilesets""" return list(self.tilesets.keys()) class SceneManager: """场景管理器 - 统一管理场景中的所有元素""" def __init__(self, world): """初始化场景管理器 Args: world: 主程序world对象引用 """ self.world = world self.models = [] # 模型列表 self.Spotlight = [] self.Pointlight = [] self.tilesets = [] #来存储tilesets self.cesium_integration = CesiumIntegration(self) print("✓ 场景管理系统初始化完成") # ==================== 模型导入和处理 ==================== def importModel(self, filepath, apply_unit_conversion=False, normalize_scales=True, auto_convert_to_glb=True): """导入模型到场景 Args: filepath: 模型文件路径 apply_unit_conversion: 是否应用单位转换(主要针对FBX文件) normalize_scales: 是否标准化子节点缩放(推荐开启) auto_convert_to_glb: 是否自动将非GLB格式转换为GLB以获得更好的动画支持 """ try: print(f"\n=== 开始导入模型: {filepath} ===") print(f"单位转换: {'开启' if apply_unit_conversion else '关闭'}") print(f"自动转换GLB: {'开启' if auto_convert_to_glb else '关闭'}") filepath = util.normalize_model_path(filepath) original_filepath = filepath # 检查是否需要转换为GLB以获得更好的动画支持 if auto_convert_to_glb and self._shouldConvertToGLB(filepath): print(f"🔄 检测到需要转换的格式,尝试转换为GLB...") converted_path = self._convertToGLBWithProgress(filepath) if converted_path: print(f"✅ 转换成功: {converted_path}") filepath = converted_path # 显示成功消息 try: from PyQt5.QtWidgets import QMessageBox original_ext = os.path.splitext(original_filepath)[1].upper() QMessageBox.information(None, "转换成功", f"已将 {original_ext} 格式自动转换为 GLB 格式\n以获得更好的动画支持!") except: pass else: print(f"⚠️ 转换失败,使用原始文件") # 总是重新加载模型以确保材质信息完整 # 不使用ModelPool缓存,避免材质信息丢失问题 print("直接从文件加载模型...") model = self.world.loader.loadModel(filepath) if not model: print("加载模型失败") return None # 设置模型名称 model_name = os.path.basename(filepath) model.setName(model_name) # 将模型添加到场景 model.reparentTo(self.world.render) # 保存原始路径和转换后的路径 model.setTag("model_path", filepath) model.setTag("original_path", original_filepath) if filepath != original_filepath: model.setTag("converted_from", os.path.splitext(original_filepath)[1]) model.setTag("converted_to_glb", "true") # 可选的单位转换(主要针对FBX) if apply_unit_conversion and filepath.lower().endswith('.fbx'): print("应用FBX单位转换(厘米到米)...") self._applyUnitConversion(model, 0.01) # 智能缩放标准化(处理FBX子节点的大缩放值) if normalize_scales and filepath.lower().endswith('.fbx'): print("标准化FBX模型缩放层级...") self._normalizeModelScales(model) # 调整模型位置到地面 self._adjustModelToGround(model) # 创建并设置基础材质 print("\n=== 开始设置材质 ===") self._applyMaterialsToModel(model) # 设置碰撞检测(重要!用于选择功能) print("\n=== 设置碰撞检测 ===") self.setupCollision(model) # 添加文件标签用于保存/加载 model.setTag("file", model_name) model.setTag("is_model_root", "1") model.setTag("is_scene_element", "1") model.setTag("tree_item_type", "IMPORTED_MODEL_NODE") # 记录应用的处理选项 if apply_unit_conversion: model.setTag("unit_conversion_applied", "true") if normalize_scales: model.setTag("scale_normalization_applied", "true") # 添加到模型列表 self.models.append(model) # 更新场景树 # 获取树形控件并添加到Qt树中 tree_widget = self._get_tree_widget() if tree_widget: # 找到根节点项 root_item = None for i in range(tree_widget.topLevelItemCount()): item = tree_widget.topLevelItem(i) if item.text(0) == "render" or item.data(0, Qt.UserRole) == self.world.render: root_item = item break if root_item: qt_item = tree_widget.add_node_to_tree_widget(model, root_item, "IMPORTED_MODEL_NODE") if qt_item: tree_widget.setCurrentItem(qt_item) # 更新选择和属性面板 tree_widget.update_selection_and_properties(model, qt_item) print("✅ Qt树节点添加成功") else: print("⚠️ Qt树节点添加失败,但Panda3D对象已创建") else: print("⚠️ 未找到根节点项,无法添加到Qt树") #self.updateSceneTree() print(f"=== 模型导入成功: {model_name} ===\n") return model except Exception as e: print(f"导入模型失败: {str(e)}") return None def _applyModelScale(self, model, scale_factor): """应用模型特定缩放 Args: model: 要缩放的模型 scale_factor: 缩放因子 """ try: print(f"应用模型缩放因子: {scale_factor}") # 获取当前边界用于后续位置调整 original_bounds = model.getBounds() # 应用缩放 model.setScale(scale_factor) # 重新调整位置(因为缩放会影响边界) if original_bounds and not original_bounds.isEmpty(): new_bounds = model.getBounds() min_point = new_bounds.getMin() ground_offset = -min_point.getZ() model.setZ(ground_offset) print(f"缩放后重新调整位置: Z偏移 = {ground_offset}") print(f"模型缩放完成,缩放因子: {scale_factor}") except Exception as e: print(f"应用模型缩放失败: {str(e)}") def _applyMaterialsToModel(self, model): """递归应用材质到模型的所有GeomNode""" def apply_material(node_path, depth=0): indent = " " * depth try: print(f"{indent}处理节点: {node_path.getName()}") print(f"{indent}节点类型: {node_path.node().__class__.__name__}") if isinstance(node_path.node(), GeomNode): print(f"{indent}发现GeomNode,处理材质") geom_node = node_path.node() # 检查所有几何体的状态 has_color = False color = None # 首先检查节点自身的状态 node_state = node_path.getState() if node_state.hasAttrib(MaterialAttrib.getClassType()): mat_attrib = node_state.getAttrib(MaterialAttrib.getClassType()) node_material = mat_attrib.getMaterial() if node_material and node_material.hasDiffuse(): color = node_material.getDiffuse() has_color = True print(f"{indent}从节点材质获取颜色: {color}") # 检查FBX特有的属性 for tag_key in node_path.getTagKeys(): print(f"{indent}发现标签: {tag_key}") if "color" in tag_key.lower() or "diffuse" in tag_key.lower(): tag_value = node_path.getTag(tag_key) print(f"{indent}颜色相关标签: {tag_key} = {tag_value}") # 如果还没找到颜色,检查几何体 if not has_color: for i in range(geom_node.getNumGeoms()): try: geom = geom_node.getGeom(i) state = geom_node.getGeomState(i) # 检查顶点颜色 vdata = geom.getVertexData() if vdata: format = vdata.getFormat() if format: for j in range(format.getNumColumns()): try: column = format.getColumn(j) # InternalName对象需要使用getName()转换为字符串 column_name = column.getName().getName() if "color" in column_name.lower(): print(f"{indent}发现顶点颜色数据: {column_name}") except Exception: continue # 检查材质属性 if state.hasAttrib(MaterialAttrib.getClassType()): mat_attrib = state.getAttrib(MaterialAttrib.getClassType()) orig_material = mat_attrib.getMaterial() if orig_material: if orig_material.hasBaseColor(): color = orig_material.getBaseColor() has_color = True print(f"{indent}从基础颜色获取: {color}") break elif orig_material.hasDiffuse(): color = orig_material.getDiffuse() has_color = True print(f"{indent}从漫反射颜色获取: {color}") break # 检查颜色属性 if not has_color and state.hasAttrib(ColorAttrib.getClassType()): color_attrib = state.getAttrib(ColorAttrib.getClassType()) if not color_attrib.isOff(): color = color_attrib.getColor() has_color = True print(f"{indent}从颜色属性获取: {color}") break except Exception as geom_error: print(f"{indent}处理几何体 {i} 时出错: {geom_error}") continue # 创建新材质 material = Material() if has_color: print(f"{indent}应用找到的颜色: {color}") try: material.setDiffuse(color) material.setBaseColor(color) # 同时设置基础颜色 node_path.setColor(color) except Exception as color_error: print(f"{indent}设置颜色时出错: {color_error}") material.setDiffuse((0.8, 0.8, 0.8, 1.0)) else: print(f"{indent}使用默认颜色") material.setDiffuse((0.8, 0.8, 0.8, 1.0)) # 设置其他材质属性 material.setAmbient((0.2, 0.2, 0.2, 1.0)) material.setSpecular((0.5, 0.5, 0.5, 1.0)) material.setShininess(32.0) # 应用材质 node_path.setMaterial(material) print(f"{indent}几何体数量: {geom_node.getNumGeoms()}") except Exception as node_error: print(f"{indent}处理节点 {node_path.getName()} 时出错: {node_error}") # 递归处理子节点 child_count = node_path.getNumChildren() print(f"{indent}子节点数量: {child_count}") for i in range(child_count): try: child = node_path.getChild(i) apply_material(child, depth + 1) except Exception as child_error: print(f"{indent}处理子节点 {i} 时出错: {child_error}") continue # 应用材质 print("\n开始递归应用材质...") try: apply_material(model) except Exception as e: print(f"应用材质时出错: {e}") print("=== 材质设置完成 ===\n") def _adjustModelToGround(self, model): """智能调整模型到地面,但保持原有缩放结构""" try: print("调整模型位置到地面...") # 获取模型的边界框 bounds = model.getBounds() if not bounds or bounds.isEmpty(): print("无法获取模型边界,使用默认位置") model.setPos(0, 0, 0) return # 获取边界框的最低点 min_point = bounds.getMin() center = bounds.getCenter() # 计算需要移动的距离,使模型底部贴合地面(Z=0) # 这里不涉及缩放,只是简单的位置调整 ground_offset = -min_point.getZ() # 设置模型位置:X,Y居中,Z调整到地面 model.setPos(0, 0, ground_offset) print(f"模型边界: 最小点{min_point}, 中心{center}") print(f"地面偏移: {ground_offset}") print(f"最终位置: {model.getPos()}") except Exception as e: print(f"调整模型位置失败: {str(e)}") # 失败时使用默认位置 model.setPos(0, 0, 0) def _applyUnitConversion(self, model, scale_factor): """应用单位转换缩放 Args: model: 要转换的模型 scale_factor: 缩放因子(如0.01表示从厘米转换到米) """ try: print(f"应用单位转换缩放: {scale_factor}") # 检查模型是否已经应用过单位转换 if model.hasTag("unit_conversion_applied"): print("模型已应用过单位转换,跳过") return # 获取当前边界用于后续位置调整 original_bounds = model.getBounds() # 应用缩放 model.setScale(scale_factor) # 重新调整位置(因为缩放会影响边界) if original_bounds and not original_bounds.isEmpty(): new_bounds = model.getBounds() min_point = new_bounds.getMin() ground_offset = -min_point.getZ() model.setZ(ground_offset) print(f"缩放后重新调整位置: Z偏移 = {ground_offset}") print(f"单位转换完成,缩放因子: {scale_factor}") except Exception as e: print(f"应用单位转换失败: {str(e)}") def _normalizeModelScales(self, model): """智能标准化模型缩放层级 检测并修复FBX模型中子节点的大缩放值问题 """ try: print("开始分析模型缩放结构...") # 收集所有节点的缩放信息 scale_info = [] self._collectScaleInfo(model, scale_info) if not scale_info: print("没有找到需要处理的缩放信息") return # 分析缩放模式 large_scales = [info for info in scale_info if max(abs(info['scale'].x), abs(info['scale'].y), abs(info['scale'].z)) > 10] if not large_scales: print("没有发现大缩放值,无需标准化") return print(f"发现 {len(large_scales)} 个节点有大缩放值") # 计算标准化因子(基于最常见的大缩放值) common_large_scale = self._findCommonLargeScale(large_scales) if common_large_scale: normalize_factor = 1.0 / common_large_scale print(f"检测到常见大缩放值: {common_large_scale}, 标准化因子: {normalize_factor}") # 应用标准化 self._applyScaleNormalization(model, normalize_factor) print("✓ 缩放标准化完成") else: print("无法确定合适的标准化因子,跳过标准化") except Exception as e: print(f"缩放标准化失败: {str(e)}") def _collectScaleInfo(self, node, scale_info, depth=0): """递归收集节点缩放信息""" try: scale = node.getScale() scale_info.append({ 'node': node, 'name': node.getName(), 'scale': scale, 'depth': depth }) # 递归处理子节点 for i in range(node.getNumChildren()): child = node.getChild(i) self._collectScaleInfo(child, scale_info, depth + 1) except Exception as e: print(f"收集缩放信息失败 ({node.getName()}): {str(e)}") def _findCommonLargeScale(self, large_scales): """找到最常见的大缩放值""" try: # 提取缩放值(取绝对值的最大分量) scale_values = [] for info in large_scales: scale = info['scale'] max_scale = max(abs(scale.x), abs(scale.y), abs(scale.z)) scale_values.append(round(max_scale)) # 四舍五入到整数 if not scale_values: return None # 找到最常见的值 from collections import Counter counter = Counter(scale_values) most_common = counter.most_common(1)[0] print(f"缩放值统计: {dict(counter)}") print(f"最常见的大缩放值: {most_common[0]} (出现{most_common[1]}次)") # 只有当最常见的值确实很大时才返回 if most_common[0] >= 10: return float(most_common[0]) return None except Exception as e: print(f"分析常见缩放值失败: {str(e)}") return None def _applyScaleNormalization(self, node, normalize_factor, depth=0): """递归应用缩放标准化,同时调整位置以保持视觉一致性""" try: indent = " " * depth current_scale = node.getScale() current_pos = node.getPos() # 检查是否需要标准化(只处理明显的大缩放) max_scale_component = max(abs(current_scale.x), abs(current_scale.y), abs(current_scale.z)) if max_scale_component > 10: # 只标准化明显的大缩放 # 应用新的缩放 new_scale = current_scale * normalize_factor node.setScale(new_scale) # 同时调整位置:当缩放变小时,位置也应该相应变小以保持视觉相对位置 # 这确保了子节点之间的相对距离在视觉上保持一致 new_pos = current_pos * normalize_factor node.setPos(new_pos) print(f"{indent}标准化 {node.getName()}:") print(f"{indent} 缩放: {current_scale} -> {new_scale}") print(f"{indent} 位置: {current_pos} -> {new_pos}") # 递归处理子节点 for i in range(node.getNumChildren()): child = node.getChild(i) self._applyScaleNormalization(child, normalize_factor, depth + 1) except Exception as e: print(f"应用缩放标准化失败 ({node.getName()}): {str(e)}") def importModelAsync(self, filepath): """异步导入模型""" try: # 创建异步加载请求 request = self.world.loader.makeAsyncRequest(filepath) # 添加完成回调 def modelLoaded(task): if task.isReady(): model = task.result() if model: # 处理加载完成的模型 self.processLoadedModel(model) return task.done() request.done_event = modelLoaded # 开始异步加载 self.world.loader.loadAsync(request) except Exception as e: print(f"异步加载模型失败: {str(e)}") # ==================== 材质和几何体处理 ==================== def processMaterials(self, model): """处理模型材质""" if isinstance(model.node(), GeomNode): # 创建基础材质 material = Material() material.setAmbient((0.2, 0.2, 0.2, 1.0)) material.setDiffuse((0.8, 0.8, 0.8, 1.0)) material.setSpecular((0.5, 0.5, 0.5, 1.0)) material.setShininess(32.0) # 检查FBX材质 state = model.node().getGeomState(0) if state.hasAttrib(MaterialAttrib.getClassType()): fbx_material = state.getAttrib(MaterialAttrib.getClassType()).getMaterial() if fbx_material: # 复制FBX材质属性 material.setAmbient(fbx_material.getAmbient()) material.setDiffuse(fbx_material.getDiffuse()) material.setSpecular(fbx_material.getSpecular()) material.setShininess(fbx_material.getShininess()) # 应用材质 model.setMaterial(material) def processModelGeometry(self, model): """处理模型几何体""" # 创建EggData对象 egg_data = EggData() # 处理顶点数据 vertex_pool = EggVertexPool("vpool") egg_data.addChild(vertex_pool) # 处理几何体 if isinstance(model.node(), GeomNode): for i in range(model.node().getNumGeoms()): geom = model.node().getGeom(i) # 处理几何体数据 # ... # ==================== 碰撞系统 ==================== def setupCollision(self, model): """为模型设置碰撞检测(增强版本)""" try: # 创建碰撞节点 cNode = CollisionNode(f'modelCollision_{model.getName()}') # 设置碰撞掩码 cNode.setIntoCollideMask(BitMask32.bit(2)) # 用于鼠标选择 # 如果启用了模型间碰撞检测,添加额外的掩码 if (hasattr(self.world, 'collision_manager') and self.world.collision_manager.model_collision_enabled): # 同时设置模型间碰撞掩码 current_mask = cNode.getIntoCollideMask() model_collision_mask = BitMask32.bit(6) # MODEL_COLLISION cNode.setIntoCollideMask(current_mask | model_collision_mask) print(f"为 {model.getName()} 启用模型间碰撞检测") # 获取模型的边界 bounds = model.getBounds() if bounds.isEmpty(): print(f"⚠️ 模型 {model.getName()} 边界为空,使用默认碰撞体") # 使用默认的小球体 cSphere = CollisionSphere(Point3(0, 0, 0), 1.0) else: center = bounds.getCenter() radius = bounds.getRadius() # 确保半径不为零 if radius <= 0: radius = 1.0 print(f"⚠️ 模型 {model.getName()} 半径为零,使用默认半径 1.0") # # # 添加碰撞球体 # cSphere = CollisionSphere(center, radius) cSphere = self.world.collision_manager.createCollisionShape(model, 'polygon') cNode.addSolid(cSphere) # 将碰撞节点附加到模型上 cNodePath = model.attachNewNode(cNode) # 根据调试设置决定是否显示碰撞体 if hasattr(self.world, 'debug_collision') and self.world.debug_collision: cNodePath.show() else: cNodePath.hide() # 为模型添加碰撞相关标签 model.setTag("has_collision", "true") model.setTag("collision_radius", str(radius if 'radius' in locals() else 1.0)) print(f"✅ 为模型 {model.getName()} 设置碰撞检测完成") return cNodePath except Exception as e: print(f"❌ 为模型 {model.getName()} 设置碰撞检测失败: {str(e)}") import traceback traceback.print_exc() return None # ==================== 场景树管理 ==================== def updateSceneTree(self): """更新场景树显示 - 代理到interface_manager""" if hasattr(self.world, 'interface_manager'): return self.world.interface_manager.updateSceneTree() else: print("界面管理器未初始化,无法更新场景树") # ==================== 场景保存和加载 ==================== 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" 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": {} } # 收集所有标签(仅对NodePath类型的对象) if hasattr(gui_node, 'getTagNames'): for tag in gui_node.getTagNames(): 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 in ["video_screen", "2d_video_screen"]: if hasattr(gui_node, 'hasTag') and gui_node.hasTag("video_path"): gui_info["video_path"] = gui_node.getTag("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 == "info_panel": if hasattr(gui_node, 'hasTag') and gui_node.hasTag("panel_data"): gui_info["panel_data"] = gui_node.getTag("panel_data") # 修改 _collectGUIElementInfo 方法中的脚本收集部分 if hasattr(self.world, 'script_manager') and self.world.script_manager: script_manager = self.world.script_manager # 获取挂载在此节点上的所有脚本 scripts = script_manager.get_scripts_on_object(gui_node) if scripts: gui_info["scripts"] = [] 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) gui_info["scripts"].append({ "name": script_name, "file": script_file }) print(f"脚本 {script_name} 来自文件: {script_file}") print(f"成功收集GUI元素信息: {gui_info}") return gui_info except Exception as e: print(f"收集GUI元素信息失败: {e}") import traceback traceback.print_exc() return None def _get_script_file_path(self, script_class, script_name): """ 获取脚本文件路径的可靠方法 """ script_file = "" # 方法1: 使用 inspect.getfile try: script_file = inspect.getfile(script_class) if script_file and os.path.exists(script_file): return script_file except: pass # 方法2: 使用 __file__ 属性 try: if hasattr(script_class, '__file__') and script_class.__file__: script_file = script_class.__file__ if script_file and os.path.exists(script_file): return script_file except: pass # 方法3: 使用模块的 __file__ 属性 try: module = inspect.getmodule(script_class) if module and hasattr(module, '__file__') and module.__file__: script_file = module.__file__ if script_file and os.path.exists(script_file): return script_file except: pass # 方法4: 从脚本管理器中查找 try: if hasattr(self.world, 'script_manager') and self.world.script_manager: script_manager = self.world.script_manager # 查找脚本类对应的文件路径 for file_path, file_mtime in script_manager.loader.file_mtimes.items(): # 检查文件名是否匹配脚本名 file_name = os.path.splitext(os.path.basename(file_path))[0] if file_name == script_name: if os.path.exists(file_path): return file_path except: pass # 方法5: 在脚本目录中查找 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: full_path = os.path.join(scripts_dir, file_name) if os.path.exists(full_path): return full_path except: pass print(f"警告: 无法获取脚本 {script_name} 的文件路径") return script_file 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) # 存储需要临时隐藏的节点,以便保存后恢复 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) # 添加GUI元素节点 gui_elements = [] if hasattr(self.world, 'gui_elements'): # 过滤掉空的或重复的GUI元素 unique_gui_elements = [] seen_names = set() for elem in self.world.gui_elements: if elem and not elem.isEmpty(): if not elem.isEmpty() and elem.getName() not in seen_names: unique_gui_elements.append(elem) seen_names.add(elem.getName()) gui_elements = unique_gui_elements print(f"保存时GUI元素列表=>>>>>>>>>>>>{self.world.gui_elements}") all_nodes.extend(gui_elements) # 创建用于保存GUI信息的JSON文件路径 gui_info_file = filename.replace('.bam', '_gui.json') print(self.world.gui_elements) # 收集GUI元素信息(排除3D文本和3D图像) gui_data = [] for gui_node in gui_elements: gui_info = self._collectGUIElementInfo(gui_node) if gui_info: gui_data.append(gui_info) print(f"添加GUI信息{gui_info['name']}") # 保存GUI信息到JSON文件(确保即使没有GUI元素也创建有效的空JSON数组) try: import json with open(gui_info_file, 'w', encoding='utf-8') as f: json.dump(gui_data, f, ensure_ascii=False, indent=2) print(f"✓ GUI信息已保存到: {gui_info_file}") except Exception as e: print(f"✗ 保存GUI信息失败: {e}") import traceback traceback.print_exc() # 添加tilesets节点 for tileset_info in self.tilesets: if tileset_info.get('node') and not tileset_info['node'].isEmpty(): all_nodes.append(tileset_info['node']) # 添加Cesium tilesets节点 for tileset_name, tileset_info in self.cesium_integration.tilesets.items(): if tileset_info.get('node') and not tileset_info['node'].isEmpty(): all_nodes.append(tileset_info['node']) # 保存所有节点的信息 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()} 的变换信息") # 获取当前状态 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)) 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("element_type") and node.getTag("element_type") == "info_panel": # 保存信息面板特定信息 print(f"保存信息面板信息: {node.getName()}") panel_id = node.getTag("panel_id") if node.hasTag("panel_id") else node.getName() if hasattr(self.world, 'info_panel_manager'): panel_data = self.world.info_panel_manager.serializePanelData(panel_id) if panel_data: import json node.setTag("info_panel_data", json.dumps(panel_data, ensure_ascii=False)) try: print("--- 打印当前场景图 (render) ---") self.world.render.ls() print("---------------------------------") self.take_screenshot(project_path) # 保存场景 success = self.world.render.writeBamFile(Filename.fromOsSpecific(filename)) if success: print(f"✓ 场景保存成功: {filename}") else: print("✗ 场景保存失败") return success finally: # 恢复之前隐藏的节点 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): """从BAM文件加载场景""" try: print(f"\n=== 开始加载场景: {filename} ===") # 确保文件路径是规范化的 filename = os.path.normpath(filename) # 检查文件是否存在 if not os.path.exists(filename): print(f"场景文件不存在: {filename}") return False tree_widget = self._get_tree_widget() # 清除当前场景 print("\n清除当前场景...") # for model in self.models: # tree_widget.delete_item(model) # # 清除灯光 # for light_node in self.Spotlight: # tree_widget.delete_item(light_node) # # for light_node in self.Pointlight: # tree_widget.delete_item(light_node) # # for terrain in self.world.terrain_manager.terrains: # tree_widget.delete_item(terrain) # # for gui in self.world.gui_elements: # if not gui.isEmpty(): # tree_widget.delete_item(gui) # # 清除tilesets # for tileset_info in self.tilesets: # tree_widget.delete_item(tileset_info['node']) tree_widget.clear_tree() 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() # 清理tilesets for tileset_info in self.tilesets: if tileset_info['node'] and not tileset_info['node'].isEmpty(): tileset_info['node'].removeNode() self.tilesets.clear() # 清理Cesium tilesets for tileset_name, tileset_info in list(self.cesium_integration.tilesets.items()): if tileset_info['node'] and not tileset_info['node'].isEmpty(): tileset_info['node'].removeNode() self.cesium_integration.tilesets.clear() for gui in self.world.gui_elements: if not gui.isEmpty(): gui.removeNode() self.world.gui_elements.clear() if hasattr(self.world,'info_panel_manager'): self.world.info_panel_manager.removeAllPanels() # 清理可能存在的辅助节点 self._cleanupAuxiliaryNodes() # 加载场景 scene = self.world.loader.loadModel(Filename.fromOsSpecific(filename)) if not scene: print("场景加载失败") return False tree_widget.create_model_items(scene) # 遍历场景中的所有模型节点 # 用于存储处理后的灯光节点,避免重复处理 processed_lights = [] # 用于存储处理后的GUI元素,避免重复处理 # 遍历场景中的所有节点 def processNode(nodePath, depth=0): indent = " " * depth print(f"{indent}处理节点: {nodePath.getName()} (类型: {type(nodePath.node()).__name__})") # 跳过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()}") # 如果是潜在的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") # 清除现有材质状态 nodePath.clearMaterial() nodePath.clearColor() # 恢复变换信息 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}") # 恢复材质属性 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) # 创建并恢复材质 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': {} } self.tilesets.append(tileset_info) self.cesium_integration.tilesets[nodePath.getName()] = tileset_info # 将节点重新挂载到render下(如果需要) # 注意:GUI元素可能需要挂载到特定的父节点上 if nodePath.hasTag("gui_type") or nodePath.hasTag("is_gui_element"): # GUI元素通常应该挂载到aspect2d或特定的GUI父节点上 # 这里我们先保持原挂载关系 pass else: # 其他节点确保挂载到render下 if nodePath.getParent() != self.world.render and not nodePath.getName() in ["render", "aspect2d", "render2d"]: nodePath.wrtReparentTo(self.world.render) # 为模型节点设置碰撞检测 if nodePath.hasTag("is_model_root"): self.setupCollision(nodePath) self.models.append(nodePath) # 递归处理子节点 for child in nodePath.getChildren(): processNode(child, depth + 1) print("\n开始处理场景节点...") processNode(scene) # 加载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(f"加载完成,GUI元素数量: {len(self.world.gui_elements)}") if len(self.world.gui_elements) > 0: print("GUI元素列表:") for i, elem in enumerate(self.world.gui_elements): print( f" {i + 1}. {elem.getName()} (类型: {elem.getTag('gui_type') if elem.hasTag('gui_type') else 'unknown'})") print("=== 场景加载完成 ===\n") return True except Exception as e: print(f"加载场景时发生错误: {str(e)}") import traceback traceback.print_exc() return False def _recreateGUIElementsFromData(self, gui_data): """根据保存的GUI数据重新创建GUI元素""" try: gui_manager = getattr(self.world, 'gui_manager', None) if not gui_manager: print("GUI管理器未找到,无法重建GUI元素") return print(f"开始重建 {len(gui_data)} 个GUI元素...") # 用于跟踪已处理的元素名称,防止重复创建 processed_names = set() for i, gui_info in enumerate(gui_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","") # 检查是否已经处理过同名元素 if name in processed_names: print(f"跳过重复元素: {name}") continue processed_names.add(name) print(f"重建GUI元素: {name} (类型: {gui_type})") print(f" 位置: {position}") print(f" 缩放: {scale}") print(f" 文本: {text}") print(f" 图像路径: {image_path}") print(f"视频路径:{video_path}") # 根据类型创建相应的GUI元素 new_element = None if gui_type == "button" and hasattr(gui_manager, 'createGUIButton'): new_element = gui_manager.createGUIButton( pos=tuple(position), text=text, size=scale[0] if scale and len(scale) > 0 else 1.0 ) elif gui_type == "label" and hasattr(gui_manager, 'createGUILabel'): scale_value = scale[0] if scale and len(scale) > 0 else 1.0 new_element = gui_manager.createGUILabel( pos=tuple(position), text=text, size=scale_value ) elif gui_type == "entry" and hasattr(gui_manager, 'createGUIEntry'): new_element = gui_manager.createGUIEntry( pos=tuple(position), placeholder=text, size=scale[0] if scale and len(scale) > 0 else 1.0 ) elif gui_type == "2d_image" and hasattr(gui_manager, 'createGUI2DImage'): scale_value = scale[0] if scale and len(scale) > 0 else 0.2 new_element = gui_manager.createGUI2DImage( pos=tuple(position), image_path=image_path, size=scale_value*0.2 ) elif gui_type == "3d_text" and hasattr(gui_manager,'createGUI3DText'): size = scale[0] if scale and len(scale) > 0 else 0.5 new_element = gui_manager.createGUI3DText( pos=tuple(position), text=text, size=size ) elif gui_type == "3d_image" and hasattr(gui_manager, 'createGUI3DImage'): # 处理3D图像 # 根据缩放值的数量处理尺寸 if len(scale) >= 3: size = (scale[0] * 2, scale[1] * 2) elif len(scale) >= 2: size = (scale[0] * 2, scale[1] * 2) elif len(scale) >= 1: size = (scale[0] * 2, scale[0] * 2) else: size = (1.0, 1.0) new_element = gui_manager.createGUI3DImage( pos=tuple(position), image_path=image_path, size=size ) elif gui_type == "video_screen" and hasattr(gui_manager,'createVideoScreen'): new_element = gui_manager.createVideoScreen( pos=tuple(position), size=scale, video_path=video_path ) if video_path and new_element and hasattr(gui_manager, 'loadVideoFile'): # 延迟一帧执行,确保节点完全初始化 from direct.task.TaskManagerGlobal import taskMgr def load_video_task(task): gui_manager.loadVideoFile(new_element, video_path) return task.done taskMgr.doMethodLater(0.1, load_video_task, 'loadVideoTask') elif gui_type == "2d_video_screen" and hasattr(gui_manager,'createGUI2DVideoScreen'): new_element = gui_manager.createGUI2DVideoScreen( pos=tuple(position), size=scale, video_path=video_path ) # 如果创建成功,设置属性 if new_element: # 如果返回的是列表(多选创建),取第一个 if isinstance(new_element, list): new_element = new_element[0] # 设置名称 new_element.setName(name) # 设置变换 new_element.setPos(*position) if len(scale) >= 3: new_element.setScale(scale[0], scale[1], scale[2]) elif len(scale) >= 1: new_element.setScale(scale[0]) # 设置标签 # 对于NodePath对象 if hasattr(new_element, 'setTag'): for tag_name, tag_value in tags.items(): # 跳过变换标签,因为我们已经设置了 if tag_name not in ["transform_pos", "transform_hpr", "transform_scale"]: new_element.setTag(tag_name, tag_value) # 对于DirectGUI对象,使用自定义标签存储 elif hasattr(new_element, '_tags'): new_element._tags.update(tags) # 重新挂载脚本(如果有的话) # 在 _recreateGUIElementsFromData 方法中找到重新挂载脚本的部分,替换为以下代码: # 重新挂载脚本(如果有的话) if "scripts" in gui_info and hasattr(self.world, 'script_manager') and self.world.script_manager: script_manager = self.world.script_manager for script_info in gui_info["scripts"]: 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}") # 为元素添加脚本 script_component = script_manager.add_script_to_object(new_element, script_name) if script_component: print(f"成功为 {name} 添加脚本: {script_name}") else: print(f"为 {name} 添加脚本失败: {script_name}") print(f"GUI元素重建成功: {name}") else: print(f"无法重建GUI元素: {name} (类型: {gui_type})") except Exception as e: print(f"重建GUI元素失败 {name}: {e}") import traceback traceback.print_exc() continue print("GUI元素重建完成") except Exception as e: print(f"重建GUI元素时发生错误: {e}") import traceback traceback.print_exc() 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 def _recreateSpotLight(self, light_node): """重新创建聚光灯""" try: from RenderPipelineFile.rpcore import SpotLight from QPanda3D.Panda3DWorld import get_render_pipeline # 创建聚光灯对象 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.setPos(light_node.getPos()) # 添加到渲染管线 render_pipeline = get_render_pipeline() render_pipeline.add_light(light) # 保存光源对象引用 light_node.setPythonTag("rp_light_object", light) # 添加到管理列表 self.Spotlight.append(light_node) # 确保灯光节点有正确的标签,以便在场景树更新时被识别 if not light_node.hasTag("is_scene_element"): light_node.setTag("is_scene_element", "1") print(f"重新创建聚光灯: {light_node.getName()}") except Exception as e: print(f"重新创建聚光灯失败: {str(e)}") def _recreatePointLight(self, light_node): """重新创建点光源""" try: from RenderPipelineFile.rpcore import PointLight from QPanda3D.Panda3DWorld import get_render_pipeline # 创建点光源对象 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()) # 添加到渲染管线 render_pipeline = get_render_pipeline() render_pipeline.add_light(light) # 保存光源对象引用 light_node.setPythonTag("rp_light_object", light) # 添加到管理列表 self.Pointlight.append(light_node) # 确保灯光节点有正确的标签,以便在场景树更新时被识别 if not light_node.hasTag("is_scene_element"): light_node.setTag("is_scene_element", "1") print(f"重新创建点光源: {light_node.getName()}") except Exception as e: print(f"重新创建点光源失败: {str(e)}") def _cleanupAuxiliaryNodes(self): """清理场景中可能存在的辅助节点""" try: # 查找并移除所有坐标轴节点 gizmo_nodes = self.world.render.findAllMatches("**/gizmo*") for node in gizmo_nodes: if not node.isEmpty(): node.removeNode() print(f"清理坐标轴节点: {node.getName()}") # 查找并移除所有选择框节点 selection_box_nodes = self.world.render.findAllMatches("**/selectionBox*") for node in selection_box_nodes: if not node.isEmpty(): node.removeNode() print(f"清理选择框节点: {node.getName()}") # 停止相关的更新任务 from direct.task.TaskManagerGlobal import taskMgr taskMgr.remove("updateGizmo") taskMgr.remove("updateSelectionBox") print("辅助节点清理完成") except Exception as e: print(f"清理辅助节点时出错: {e}") # ==================== 模型管理 ==================== def deleteModel(self, model): """删除模型""" try: if model in self.models: tree_widget = self._get_tree_widget() if not tree_widget: return False tree_widget.delete_items(tree_widget.selectedItems()) # model.removeNode() # self.models.remove(model) # self.updateSceneTree() print(f"删除模型: {model.getName()}") return True except Exception as e: print(f"删除模型失败: {str(e)}") return False def clearAllModels(self): """清除所有模型""" pass # try: # for model in self.models: # model.removeNode() # self.models.clear() # self.updateSceneTree() # print("清除所有模型完成") # except Exception as e: # print(f"清除所有模型失败: {str(e)}") def getModels(self): """获取模型列表""" return self.models.copy() def getModelCount(self): """获取模型数量""" return len(self.models) def findModelByName(self, name): """根据名称查找模型""" for model in self.models: if model.getName() == name: return model return None # ==================== 帮助方法 ==================== def processLoadedModel(self, model): """处理加载完成的模型(用于异步加载回调)""" if model: # 添加到模型列表 self.models.append(model) # 设置基础变换 model.setPos(0, 0, 0) model.setHpr(0, 0, 0) model.setScale(1, 1, 1) # 应用材质 self.processMaterials(model) # 设置碰撞检测 self.setupCollision(model) # 更新场景树 self.updateSceneTree() print(f"异步加载模型完成: {model.getName()}") def createSpotLight(self, pos=(0, 0, 0)): """创建聚光灯 - 支持多选创建,优化版本""" try: from RenderPipelineFile.rpcore import SpotLight from QPanda3D.Panda3DWorld import get_render_pipeline from panda3d.core import Vec3, NodePath from PyQt5.QtCore import Qt print(f"🔆 开始创建聚光灯,位置: {pos}") # 获取树形控件 tree_widget = self._get_tree_widget() if not tree_widget: print("❌ 无法访问树形控件") return None # 获取目标父节点列表 target_parents = tree_widget.get_target_parents_for_creation() if not target_parents: print("❌ 没有找到有效的父节点") return None created_lights = [] render_pipeline = get_render_pipeline() # 为每个有效的父节点创建聚光灯 for parent_item, parent_node in target_parents: try: # 生成唯一名称 light_name = f"Spotlight_{len(self.Spotlight)}" # 创建挂载节点 - 挂载到选中的父节点 light_np = NodePath(light_name) light_np.reparentTo(parent_node) # 挂载到父节点而不是render light_np.setPos(*pos) # 创建聚光灯对象 light = SpotLight() light.direction = Vec3(0, 0, -1) light.fov = 70 light.set_color_from_temperature(5 * 1000.0) light.energy = 5000 light.radius = 1000 light.casts_shadows = True light.shadow_map_resolution = 256 light.setPos(*pos) # 添加到渲染管线 render_pipeline.add_light(light) # 设置节点属性和标签 light_np.setTag("light_type", "spot_light") light_np.setTag("is_scene_element", "1") light_np.setTag("tree_item_type", "LIGHT_NODE") light_np.setTag("light_energy", str(light.energy)) light_np.setTag("created_by_user", "1") # 保存光源对象引用 light_np.setPythonTag("rp_light_object", light) # 添加到管理列表 self.Spotlight.append(light_np) print(f"✅ 为 {parent_item.text(0)} 创建聚光灯成功: {light_name}") # 在Qt树形控件中添加对应节点 qt_item = tree_widget.add_node_to_tree_widget(light_np, parent_item, "LIGHT_NODE") if qt_item: created_lights.append((light_np, qt_item)) else: created_lights.append((light_np, None)) print("⚠️ Qt树节点添加失败,但Panda3D对象已创建") except Exception as e: print(f"❌ 为 {parent_item.text(0)} 创建聚光灯失败: {str(e)}") import traceback traceback.print_exc() continue # 处理创建结果 if not created_lights: print("❌ 没有成功创建任何聚光灯") return None # 选中最后创建的光源 if created_lights: last_light_np, last_qt_item = created_lights[-1] if last_qt_item: tree_widget.setCurrentItem(last_qt_item) # 更新选择和属性面板 tree_widget.update_selection_and_properties(last_light_np, last_qt_item) print(f"🎉 总共创建了 {len(created_lights)} 个聚光灯") # 返回值处理 if len(created_lights) == 1: return created_lights[0][0] # 单个光源返回NodePath else: return [light_np for light_np, _ in created_lights] # 多个光源返回列表 except Exception as e: print(f"❌ 创建聚光灯过程失败: {str(e)}") import traceback traceback.print_exc() return None def createPointLight(self, pos=(0, 0, 0)): """创建点光源 - 支持多选创建,优化版本""" try: from RenderPipelineFile.rpcore import PointLight from QPanda3D.Panda3DWorld import get_render_pipeline from panda3d.core import Vec3, NodePath from PyQt5.QtCore import Qt print(f"💡 开始创建点光源,位置: {pos}") # 获取树形控件 tree_widget = self._get_tree_widget() if not tree_widget: print("❌ 无法访问树形控件") return None # 获取目标父节点列表 target_parents = tree_widget.get_target_parents_for_creation() if not target_parents: print("❌ 没有找到有效的父节点") return None created_lights = [] render_pipeline = get_render_pipeline() # 为每个有效的父节点创建点光源 for parent_item, parent_node in target_parents: try: # 生成唯一名称 light_name = f"Pointlight_{len(self.Pointlight)}" # 创建挂载节点 - 挂载到选中的父节点 light_np = NodePath(light_name) light_np.reparentTo(parent_node) # 挂载到父节点而不是render light_np.setPos(*pos) # 创建点光源对象 light = PointLight() light.setPos(*pos) 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 # 添加到渲染管线 render_pipeline.add_light(light) # 设置节点属性和标签 light_np.setTag("light_type", "point_light") light_np.setTag("is_scene_element", "1") light_np.setTag("tree_item_type", "LIGHT_NODE") light_np.setTag("light_energy", str(light.energy)) light_np.setTag("created_by_user", "1") # 保存光源对象引用 light_np.setPythonTag("rp_light_object", light) # 添加到管理列表 self.Pointlight.append(light_np) print(f"✅ 为 {parent_item.text(0)} 创建点光源成功: {light_name}") # 在Qt树形控件中添加对应节点 qt_item =tree_widget.add_node_to_tree_widget(light_np, parent_item, "LIGHT_NODE") if qt_item: created_lights.append((light_np, qt_item)) else: created_lights.append((light_np, None)) print("⚠️ Qt树节点添加失败,但Panda3D对象已创建") except Exception as e: print(f"❌ 为 {parent_item.text(0)} 创建点光源失败: {str(e)}") continue # 处理创建结果 if not created_lights: print("❌ 没有成功创建任何点光源") return None # 选中最后创建的光源 if created_lights: last_light_np, last_qt_item = created_lights[-1] if last_qt_item: tree_widget.setCurrentItem(last_qt_item) # 更新选择和属性面板 tree_widget.update_selection_and_properties(last_light_np, last_qt_item) print(f"🎉 总共创建了 {len(created_lights)} 个点光源") # 返回值处理 if len(created_lights) == 1: return created_lights[0][0] # 单个光源返回NodePath else: return [light_np for light_np, _ in created_lights] # 多个光源返回列表 except Exception as e: print(f"❌ 创建点光源过程失败: {str(e)}") import traceback traceback.print_exc() return None def _get_tree_widget(self): """安全获取树形控件""" try: if (hasattr(self.world, 'interface_manager') and hasattr(self.world.interface_manager, 'treeWidget')): return self.world.interface_manager.treeWidget except AttributeError: pass return None # def createSpotLight(self, pos=(0, 0, 0)): # """创建聚光灯 - 使用统一的create_item方法""" # try: # # 调用CustomTreeWidget的create_item方法创建聚光灯节点 # if hasattr(self.world, 'interface_manager') and hasattr(self.world.interface_manager, 'treeWidget'): # tree_widget = self.world.interface_manager.treeWidget # if tree_widget and hasattr(tree_widget, 'create_item'): # # 创建聚光灯节点 # created_nodes = tree_widget.create_item("spot_light") # # if created_nodes: # # 获取创建的节点 # light_np, qt_item = created_nodes[0] # # # 设置位置(如果指定了非默认位置) # if pos != (0, 0, 0): # light_np.setPos(*pos) # # 同时更新光源对象的位置 # light_obj = light_np.getPythonTag("rp_light_object") # if light_obj: # light_obj.setPos(*pos) # # print(f"✅ 通过create_item创建聚光灯成功: {light_np.getName()}") # return light_np # else: # print("❌ create_item创建聚光灯失败") # return None # else: # print("❌ 无法访问树形控件的create_item方法") # return None # else: # print("❌ 无法访问界面管理器或树形控件") # return None # # except Exception as e: # print(f"❌ 创建聚光灯时发生错误: {str(e)}") # import traceback # traceback.print_exc() # return None # def createSpotLight(self, pos=(0, 0, 0)): # from RenderPipelineFile.rpcore import SpotLight, RenderPipeline # from panda3d.core import Vec3,NodePath # # render_pipeline = get_render_pipeline() # # # 创建一个挂载节点(你控制的) # light_np = NodePath("SpotlightAttachNode") # light_np.reparentTo(self.world.render) # #light_np.setPos(*pos) # # self.half_energy = 5000 # self.lamp_fov = 70 # self.lamp_radius = 1000 # # light = SpotLight() # light.direction = Vec3(0, 0, -1) # 光照方向 # light.fov = self.lamp_fov # 光源角度(类似手电筒) # light.set_color_from_temperature(5 * 1000.0) # 色温(K) # light.energy = self.half_energy # 光照强度 # light.radius = self.lamp_radius # 影响范围 # light.casts_shadows = True # 是否投射阴影 # light.shadow_map_resolution = 256 # 阴影分辨率 # light.setPos(*pos) # #light_np.setPos(*pos) # # #light_np = render_pipeline.add_light(light, parent=self.world.render) # render_pipeline.add_light(light) # 添加到渲染管线 # # light_name = f"Spotlight_{len(self.Spotlight)}" # # light_np.setName(light_name) # 设置唯一名称 # #light_np.reparentTo(self.world.render) # 挂载到场景根节点 # # light_np.setTag("light_type", "spot_light") # light_np.setTag("is_scene_element", "1") # light_np.setTag("light_energy", str(light.energy)) # # light_np.setPythonTag("rp_light_object", light) # # self.Spotlight.append(light_np) # # if hasattr(self.world, 'updateSceneTree'): # self.world.updateSceneTree() # # #print("nikan"+light_np.getHpr()) # def createPointLight(self, pos=(0, 0, 0)): # """创建点光源 - 使用统一的create_item方法""" # try: # # 调用CustomTreeWidget的create_item方法创建点光源节点 # if hasattr(self.world, 'interface_manager') and hasattr(self.world.interface_manager, 'treeWidget'): # tree_widget = self.world.interface_manager.treeWidget # if tree_widget and hasattr(tree_widget, 'create_item'): # # 创建点光源节点 # created_nodes = tree_widget.create_item("point_light") # # if created_nodes: # # 获取创建的节点 # light_np, qt_item = created_nodes[0] # # # 设置位置(如果指定了非默认位置) # if pos != (0, 0, 0): # light_np.setPos(*pos) # # 同时更新光源对象的位置 # light_obj = light_np.getPythonTag("rp_light_object") # if light_obj: # light_obj.setPos(*pos) # # print(f"✅ 通过create_item创建点光源成功: {light_np.getName()}") # return light_np # else: # print("❌ create_item创建点光源失败") # return None # else: # print("❌ 无法访问树形控件的create_item方法") # return None # else: # print("❌ 无法访问界面管理器或树形控件") # return None # # except Exception as e: # print(f"❌ 创建点光源时发生错误: {str(e)}") # import traceback # traceback.print_exc() # return None # def createPointLight(self, pos=(0, 0, 0)): # from RenderPipelineFile.rpcore import PointLight, RenderPipeline # from panda3d.core import Vec3, NodePath # # render_pipeline = get_render_pipeline() # # # 创建一个挂载节点(你控制的) # light_np = NodePath("PointlightAttachNode") # light_np.reparentTo(self.world.render) # # # light = PointLight() # light.setPos(*pos) # light_np.setPos(*pos) # light.energy = 5000 # light.radius = 1000 # light.inner_radius = 0.4 # light.set_color_from_temperature(5 * 1000.0) # 色温(K) # light.casts_shadows = True # 是否投射阴影 # light.shadow_map_resolution = 256 # 阴影分辨率 # # render_pipeline.add_light(light) # 添加到渲染管线 # # light_name = f"Pointlight{len(self.Pointlight)}" # # light_np.setName(light_name) # 设置唯一名称 # # #light_np = NodePath(f"PointLight_{len(self.Pointlight)}") # #light_np.reparentTo(self.world.render) # #light_np.setPos(*pos) # # light_np.setTag("light_type", "point_light") # light_np.setTag("is_scene_element", "1") # light_np.setTag("light_energy", str(light.energy)) # # # 保存光源对象引用(重要!用于属性面板) # light_np.setPythonTag("rp_light_object", light) # # self.Pointlight.append(light_np) # # if hasattr(self.world, 'updateSceneTree'): # self.world.updateSceneTree() # # return light,light_np # ==================== GLB 转换方法 ==================== #=================================================== def _shouldConvertToGLB(self, filepath): """判断是否应该转换为GLB格式""" ext = os.path.splitext(filepath)[1].lower() # 需要转换的格式:FBX, OBJ, DAE等(但不转换已经是GLB/GLTF的) convert_formats = ['.fbx', '.obj', '.dae', '.3ds', '.blend'] return ext in convert_formats def _convertToGLBWithProgress(self, filepath): """带进度显示的GLB转换""" try: from PyQt5.QtWidgets import QProgressDialog, QApplication from PyQt5.QtCore import Qt # 创建进度对话框 progress = QProgressDialog("正在转换模型格式以获得更好的动画支持...", "取消", 0, 100) progress.setWindowTitle("模型格式转换") progress.setWindowModality(Qt.WindowModal) progress.show() QApplication.processEvents() try: result = self._convertToGLB(filepath, progress) progress.hide() return result except Exception as e: progress.hide() print(f"转换过程出错: {e}") return None except ImportError: # 如果没有 PyQt5,直接转换 return self._convertToGLB(filepath) def _convertToGLB(self, filepath, progress=None): """将模型文件转换为GLB格式""" try: print(f"[GLB转换] 开始转换: {filepath}") if progress: progress.setValue(10) progress.setLabelText("准备转换...") from PyQt5.QtWidgets import QApplication QApplication.processEvents() # 准备输出路径 base_name = os.path.splitext(os.path.basename(filepath))[0] output_dir = os.path.dirname(filepath) glb_path = os.path.join(output_dir, f"{base_name}_auto_converted.glb") # 如果已经存在转换后的文件,直接使用 if os.path.exists(glb_path): # 检查文件时间,如果原文件更新则重新转换 original_time = os.path.getmtime(filepath) converted_time = os.path.getmtime(glb_path) if converted_time > original_time: print(f"[GLB转换] 使用现有转换文件: {glb_path}") if progress: progress.setValue(100) return glb_path if progress: progress.setValue(20) progress.setLabelText("尝试 Blender 转换...") QApplication.processEvents() # 方法1: 使用 Blender 进行转换 if self._convertWithBlender(filepath, glb_path, progress): return glb_path if progress: progress.setValue(60) progress.setLabelText("尝试 FBX2glTF 转换...") QApplication.processEvents() # 方法2: 使用 FBX2glTF (如果可用) if self._convertWithFBX2glTF(filepath, glb_path, progress): return glb_path if progress: progress.setValue(80) progress.setLabelText("尝试 Assimp 转换...") QApplication.processEvents() # 方法3: 使用 Assimp if self._convertWithAssimp(filepath, glb_path, progress): return glb_path #print(f"[GLB转换] 所有转换方法都失败,既然没有可以转换格式的工具和环境那么就用原始文件,不一定非要转换") return None except Exception as e: print(f"[GLB转换] 转换过程出错: {e}") return None def _convertWithBlender(self, input_path, output_path, progress=None): """使用 Blender 进行转换""" try: import subprocess import tempfile print(f"[Blender转换] {input_path} → {output_path}") # 创建 Blender 脚本 script_content = f''' import bpy import sys import os # 清理默认场景 bpy.ops.object.select_all(action='SELECT') bpy.ops.object.delete(use_global=False) print("开始导入文件...") # 根据文件类型选择导入方法 input_file = "{input_path}" output_file = "{output_path}" try: ext = os.path.splitext(input_file)[1].lower() if ext == '.fbx': bpy.ops.import_scene.fbx(filepath=input_file) elif ext == '.obj': bpy.ops.import_scene.obj(filepath=input_file) elif ext == '.dae': bpy.ops.wm.collada_import(filepath=input_file) elif ext == '.blend': bpy.ops.wm.open_mainfile(filepath=input_file) else: print(f"不支持的格式: {{ext}}") sys.exit(1) print("导入成功,开始导出GLB...") # 导出为 GLB,保留动画 bpy.ops.export_scene.gltf( filepath=output_file, export_format='GLB', export_animations=True, export_force_sampling=True, export_frame_range=True, export_current_frame=False, export_skins=True, export_morph=True, export_lights=True, export_cameras=False ) print("GLB导出成功!") except Exception as e: print(f"转换失败: {{e}}") sys.exit(1) ''' # 写入临时脚本文件 with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as temp_file: temp_file.write(script_content) script_path = temp_file.name try: # 执行 Blender 转换 result = subprocess.run([ 'blender', '--background', '--python', script_path ], capture_output=True, text=True, timeout=180) # 清理临时文件 os.unlink(script_path) if result.returncode == 0 and os.path.exists(output_path): print(f"[Blender转换] 转换成功") return True else: print(f"[Blender转换] 转换失败: {result.stderr}") return False except subprocess.TimeoutExpired: print(f"[Blender转换] 转换超时") return False except FileNotFoundError: print(f"[Blender转换] Blender 未安装") return False except Exception as e: print(f"[Blender转换] 转换过程出错: {e}") return False def _convertWithFBX2glTF(self, input_path, output_path, progress=None): """使用 FBX2glTF 进行转换(仅支持FBX)""" try: import subprocess if not input_path.lower().endswith('.fbx'): return False print(f"[FBX2glTF转换] {input_path} → {output_path}") # 使用 FBX2glTF 转换 result = subprocess.run([ 'FBX2glTF', input_path, '--output', output_path, '--binary' ], capture_output=True, text=True, timeout=120) if result.returncode == 0 and os.path.exists(output_path): print(f"[FBX2glTF转换] 转换成功") return True else: print(f"[FBX2glTF转换] 转换失败: {result.stderr}") return False except subprocess.TimeoutExpired: print(f"[FBX2glTF转换] 转换超时") return False except FileNotFoundError: print(f"[FBX2glTF转换] FBX2glTF 未安装") return False except Exception as e: print(f"[FBX2glTF转换] 转换过程出错: {e}") return False def _convertWithAssimp(self, input_path, output_path, progress=None): """使用 PyAssimp 进行转换""" try: import pyassimp print(f"[PyAssimp转换] {input_path} → {output_path}") # 加载模型 scene = pyassimp.load(input_path) if not scene: print(f"[PyAssimp转换] 加载模型失败") return False if progress: progress.setValue(30) # 导出为GLB格式 pyassimp.export(scene, output_path, "glb2") if progress: progress.setValue(80) # 释放资源 pyassimp.release(scene) if os.path.exists(output_path): print(f"[PyAssimp转换] 转换成功") return True else: print(f"[PyAssimp转换] 转换失败: 输出文件未生成") return False except ImportError: print(f"[PyAssimp转换] PyAssimp 未安装") return False except Exception as e: print(f"[PyAssimp转换] 转换过程出错: {e}") return False def load_cesium_tileset(self, tileset_url, position=(0, 0, 0)): """ 加载 Cesium 3D Tileset - 采用新的创建逻辑,支持多选和更完善的UI交互。 """ try: from panda3d.core import NodePath print(f"🗺️ 开始加载 Cesium 3D Tiles: {tileset_url}") # 1. 获取UI控件和目标父节点 tree_widget = self._get_tree_widget() if not tree_widget: print("❌ 无法访问树形控件") return None target_parents = tree_widget.get_target_parents_for_creation() if not target_parents: print("❌ 没有找到有效的父节点来附加Tileset") return None created_tilesets = [] # 2. 遍历所有选中的父节点,并为其创建Tileset for parent_item, parent_node in target_parents: try: # 生成唯一名称 node_name = f"cesium_tileset_{len(self.tilesets)}" # 创建一个容器节点来管理tileset,并挂载到父节点 tileset_node = parent_node.attachNewNode(node_name) tileset_node.setPos(*position) # 添加标签以便场景识别和保存 tileset_node.setTag("is_scene_element", "1") tileset_node.setTag("tree_item_type", "CESIUM_TILESET_NODE") tileset_node.setTag("element_type", "cesium_tileset") tileset_node.setTag("tileset_url", tileset_url) # 使用唯一名称作为文件标识,代替索引 tileset_node.setTag("file", node_name) # 存储tileset核心信息 tileset_info = { 'url': tileset_url, 'node': tileset_node, 'position': position, 'tiles': {} # 用于后续管理瓦片 } self.tilesets.append(tileset_info) # 创建一个临时的可视化占位符,让用户能看到节点已添加 self._create_placeholder_geometry(tileset_node) # 异步加载tileset的实际数据 self._load_tileset_async(tileset_url, tileset_info) print(f"✅ 为 {parent_item.text(0)} 加载 Tileset 成功: {node_name}") # 在Qt树形控件中添加对应节点 qt_item = tree_widget.add_node_to_tree_widget(tileset_node, parent_item, "CESIUM_TILESET_NODE") if qt_item: created_tilesets.append((tileset_node, qt_item)) else: created_tilesets.append((tileset_node, None)) print("⚠️ Qt树节点添加失败,但Panda3D对象已创建") except Exception as e: print(f"❌ 为 {parent_item.text(0)} 加载 Tileset 失败: {str(e)}") continue # 继续尝试为下一个父节点创建 # 3. 处理创建结果 if not created_tilesets: print("❌ 没有成功加载任何 Tileset") return None # 选中最后创建的Tileset并更新UI if created_tilesets: last_tileset_node, last_qt_item = created_tilesets[-1] if last_qt_item: tree_widget.setCurrentItem(last_qt_item) # 更新选择状态和属性面板 tree_widget.update_selection_and_properties(last_tileset_node, last_qt_item) print(f"🎉 总共加载了 {len(created_tilesets)} 个 Cesium Tileset 实例") # 4. 返回值处理 if len(created_tilesets) == 1: return created_tilesets[0][0] # 单个实例返回NodePath else: return [node for node, _ in created_tilesets] # 多个实例返回NodePath列表 except Exception as e: print(f"❌ 加载 Cesium 3D Tiles 过程失败: {str(e)}") import traceback traceback.print_exc() return None def _load_tileset_async(self, tileset_url, tileset_info): """异步加载 tileset 数据""" async def load_tileset(): try: async with aiohttp.ClientSession() as session: async with session.get(tileset_url) as response: if response.status == 200: tileset_data = await response.json() self._parse_tileset(tileset_data, tileset_info) print(f"✓ Tileset 数据加载完成") else: print(f"✗ Tileset 加载失败: {response.status}") except Exception as e: print(f"✗ Tileset 加载出错: {e}") # 在 Panda3D 的任务系统中运行异步任务 task = asyncio.ensure_future(load_tileset()) self._current_asyncio_task = task # 保存任务引用 self.world.taskMgr.add(self._check_async_task, "check_tileset_load", appendTask=True) def _check_async_task(self, panda3d_task): # 检查 asyncio 任务是否完成 if hasattr(self, '_current_asyncio_task'): if self._current_asyncio_task.done(): try: self._current_asyncio_task.result() except Exception as e: print(f"异步任务出错:{e}") # 返回 Panda3D 任务管理器的完成状态 return panda3d_task.done # 注意是 done 而不是 DONE # 返回 Panda3D 任务管理器的继续状态 return panda3d_task.cont # 注意是 cont 而不是 CONTINUE def _parse_tileset(self,tileset_data,tileset_info): try: root = tileset_data.get('root',{}) self._parse_tile(root,tileset_info['node'],tileset_info) print("✓ Tileset 解析完成") except Exception as e: print(f"✗ Tileset 解析出错: {e}") def _parse_tile(self, tile_data, parent_node, tileset_info): try: # 获取tileID tile_id = f"tile_{len(tileset_info['tiles'])}" print(f"创建tile节点: {tile_id}") # 创建tile节点 tile_node = parent_node.attachNewNode(tile_id) tileset_info['tiles'][tile_id] = { 'node': tile_node, 'data': tile_data, 'loaded': False } # 如果有内容,创建占位几何体 if 'content' in tile_data: print(f"为tile {tile_id} 创建几何体") self._create_tile_geometry(tile_node) # 递归解析子tiles children = tile_data.get('children', []) print(f"Tile {tile_id} 有 {len(children)} 个子节点") for child_data in children: self._parse_tile(child_data, tile_node, tileset_info) except Exception as e: print(f"✗ Tile 解析出错: {e}") import traceback traceback.print_exc() def _create_tile_geometry(self,parent_node): """为 tile 创建占位几何体""" try: # 创建一个简单的立方体作为占位符 from panda3d.core import GeomVertexFormat, GeomVertexData, GeomVertexWriter from panda3d.core import Geom, GeomTriangles, GeomNode format = GeomVertexFormat.getV3n3c4() vdata = GeomVertexData('tile_cube', format, Geom.UHStatic) vertex = GeomVertexWriter(vdata, 'vertex') normal = GeomVertexWriter(vdata, 'normal') color = GeomVertexWriter(vdata, 'color') # 定义立方体顶点 vertices = [ (-0.5, -0.5, -0.5), (0.5, -0.5, -0.5), (0.5, 0.5, -0.5), (-0.5, 0.5, -0.5), (-0.5, -0.5, 0.5), (0.5, -0.5, 0.5), (0.5, 0.5, 0.5), (-0.5, 0.5, 0.5) ] for vert in vertices: vertex.addData3f(*vert) normal.addData3f(0, 0, 1) color.addData4f(0.2, 0.6, 0.8, 1.0) # 创建几何体 geom = Geom(vdata) # 创建面 prim = GeomTriangles(Geom.UHStatic) # 底面 prim.addVertices(0, 1, 2) prim.addVertices(0, 2, 3) # 顶面 prim.addVertices(4, 7, 6) prim.addVertices(4, 6, 5) # 前面 prim.addVertices(0, 4, 5) prim.addVertices(0, 5, 1) # 后面 prim.addVertices(2, 6, 7) prim.addVertices(2, 7, 3) # 左面 prim.addVertices(0, 3, 7) prim.addVertices(0, 7, 4) # 右面 prim.addVertices(1, 5, 6) prim.addVertices(1, 6, 2) prim.closePrimitive() geom.addPrimitive(prim) # 创建几何节点 geom_node = GeomNode('tile_geometry') geom_node.addGeom(geom) # 添加到场景 cube_node = parent_node.attachNewNode(geom_node) cube_node.setScale(1000) # 放大以便观察 # 添加材质 material = Material() material.setBaseColor((0.2, 0.6, 0.8, 1.0)) material.setSpecular((0.1, 0.1, 0.1, 1.0)) material.setShininess(10.0) cube_node.setMaterial(material) except Exception as e: print(f"✗ 创建 tile 几何体出错: {e}") def _create_placeholder_geometry(self, parent_node): """创建一个简单的占位符几何体,让用户能看到节点""" try: from panda3d.core import GeomVertexFormat, GeomVertexData, GeomVertexWriter from panda3d.core import Geom, GeomTriangles, GeomNode # 创建简单的立方体作为占位符 format = GeomVertexFormat.getV3n3c4() vdata = GeomVertexData('placeholder_cube', format, Geom.UHStatic) vertex = GeomVertexWriter(vdata, 'vertex') normal = GeomVertexWriter(vdata, 'normal') color = GeomVertexWriter(vdata, 'color') # 定义立方体顶点 size = 1.0 vertices = [ # 前面 (Z+) (-size, -size, size), (size, -size, size), (size, size, size), (-size, size, size), # 后面 (Z-) (-size, -size, -size), (-size, size, -size), (size, size, -size), (size, -size, -size), # 左面 (X-) (-size, -size, -size), (-size, -size, size), (-size, size, size), (-size, size, -size), # 右面 (X+) (size, -size, -size), (size, size, -size), (size, size, size), (size, -size, size), # 上面 (Y+) (-size, size, -size), (-size, size, size), (size, size, size), (size, size, -size), # 下面 (Y-) (-size, -size, -size), (size, -size, -size), (size, -size, size), (-size, -size, size) ] normals = [ # 前面法线 (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), # 后面法线 (0, 0, -1), (0, 0, -1), (0, 0, -1), (0, 0, -1), # 左面法线 (-1, 0, 0), (-1, 0, 0), (-1, 0, 0), (-1, 0, 0), # 右面法线 (1, 0, 0), (1, 0, 0), (1, 0, 0), (1, 0, 0), # 上面法线 (0, 1, 0), (0, 1, 0), (0, 1, 0), (0, 1, 0), # 下面法线 (0, -1, 0), (0, -1, 0), (0, -1, 0), (0, -1, 0) ] # 青色 face_colors = [ (0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), # 前面 - 青色 (0.0, 0.8, 0.8, 1.0), (0.0, 0.8, 0.8, 1.0), (0.0, 0.8, 0.8, 1.0), (0.0, 0.8, 0.8, 1.0), # 后面 - 稍暗青色 (0.0, 0.9, 0.9, 1.0), (0.0, 0.9, 0.9, 1.0), (0.0, 0.9, 0.9, 1.0), (0.0, 0.9, 0.9, 1.0), # 左面 - 中等青色 (0.0, 0.7, 0.7, 1.0), (0.0, 0.7, 0.7, 1.0), (0.0, 0.7, 0.7, 1.0), (0.0, 0.7, 0.7, 1.0), # 右面 - 稍暗青色 (0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), # 上面 - 青色 (0.0, 0.6, 0.6, 1.0), (0.0, 0.6, 0.6, 1.0), (0.0, 0.6, 0.6, 1.0), (0.0, 0.6, 0.6, 1.0) # 下面 - 更暗青色 ] for i, vert in enumerate(vertices): vertex.addData3f(*vert) normal.addData3f(*normals[i]) color.addData4f(*face_colors[i]) # 创建几何体 geom = Geom(vdata) # 创建面(每个面两个三角形) prim = GeomTriangles(Geom.UHStatic) # 每个面4个顶点,生成2个三角形 for face in range(6): # 6个面 base_index = face * 4 # 第一个三角形 prim.addVertices(base_index, base_index + 1, base_index + 2) # 第二个三角形 prim.addVertices(base_index, base_index + 2, base_index + 3) prim.closePrimitive() geom.addPrimitive(prim) # 创建几何节点 geom_node = GeomNode('tileset_placeholder') geom_node.addGeom(geom) # 添加到场景 cube_node = parent_node.attachNewNode(geom_node) cube_node.setScale(5) # 设置合适的大小 # 设置双面渲染 cube_node.setTwoSided(True) # 添加材质 material = Material() material.setBaseColor((0.0, 1.0, 1.0, 1.0)) # 青色 material.setSpecular((0.5, 0.5, 0.5, 1.0)) material.setShininess(32.0) cube_node.setMaterial(material) # 添加标识标签 cube_node.setTag("element_type", "cesium_placeholder") print("✓ 占位符几何体创建完成") return cube_node except Exception as e: print(f"✗ 创建占位符几何体出错: {e}") import traceback traceback.print_exc() return None