diff --git a/scene/scene_manager.py b/scene/scene_manager.py index 6d501949..2d184891 100644 --- a/scene/scene_manager.py +++ b/scene/scene_manager.py @@ -1,4356 +1,4478 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -场景管理器 - 负责场景和模型管理的核心功能 -处理模型导入、场景树构建、材质系统、碰撞设置等 -""" - -import os -import shutil -import time - -from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import QTreeWidgetItem -from panda3d.core import ( - ModelPool, ModelRoot, Filename, NodePath, GeomNode, Material, Vec4, Vec3, - MaterialAttrib, ColorAttrib, Point3, CollisionNode, CollisionSphere, CollisionBox, - BitMask32, TransparencyAttrib, LColor, TransformState, RenderModeAttrib -) -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 QMeta3D.Meta3DWorld import get_render_pipeline -from RenderPipelineFile.rpplugins.smaa.jitters import halton_seq -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): - try: - if not os.path.exists(filepath): - print("文件不存在") - return None - - filepath = util.normalize_model_path(filepath) - original_filepath = filepath - - # # 在加载前设置忽略未知属性 - # from panda3d.core import ConfigVariableBool - # ConfigVariableBool("model-cache-ignore-unknown-properties").setValue(True) - # - # # 清除可能存在的模型缓存 - # from panda3d.core import ModelPool - # ModelPool.releaseAllModels() - # - # # 检查是否需要转换为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 - # # 转换成功的消息已在控制台显示,不再弹窗提示 - # else: - # print(f"⚠️ 转换失败,使用原始文件") - - model = self.world.loader.loadModel(filepath) - if not model: - print("加载模型失败") - return None - - # 设置模型名称 - model_name = os.path.basename(filepath) - # 确保名称有效 - if not model_name: - model_name = "imported_model" - model.setName(model_name) - # 将模型添加到场景 - model.reparentTo(self.world.render) - - # 设置模型名称 - model_name = os.path.basename(filepath) - model.setName(model_name) - - - # 保存原始路径和转换后的路径 - 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 filepath.lower().endswith('.fbx'): - print("检测到FBX模型,应用特殊处理...") - - # 将模型缩放设置为原来的1/100 - model.setScale(0.01) - print("设置模型缩放为 0.01 (原始大小的1/100)") - - # 设置模型旋转为 (0, 90, 0) - model.setHpr(0, 90, 0) - print("设置模型旋转为 (0, 90, 0)") - - # 调整模型位置到地面 - model.setPos(0,0,0) - #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 _fixModelStructure(self, model): - """修复模型结构""" - try: - # 使用正确的方式查找动画相关节点 - character_nodes = model.findAllMatches("**/+Character") - anim_bundle_nodes = model.findAllMatches("**/+AnimBundleNode") - - if character_nodes.getNumPaths() > 0 or anim_bundle_nodes.getNumPaths() > 0: - print(f"检测到模型{model.getName()}包含角色相节点:") - if character_nodes.getNumPaths() > 0: - print(f"CharacterNode数量:{character_nodes.getNumPaths()}") - if anim_bundle_nodes.getNumPaths() > 0: - print(f"AnimBundleNode数量: {anim_bundle_nodes.getNumPaths()}") - - model.setTag("fixed_structure", "true") - return True - except Exception as e: - print(f"修复模型结构时出错: {e}") - return False - - def _validateAndFixAllTransforms(self, model): - """递归验证并修复模型中所有节点的变换矩阵""" - try: - fixed_count = 0 - - # 先处理根节点 - if not self._validateAndFixTransform(model): - fixed_count += 1 - - # 递归处理所有子节点 - def process_children(node, depth=0): - nonlocal fixed_count - for i in range(node.getNumChildren()): - try: - child = node.getChild(i) - if not self._validateAndFixTransform(child): - fixed_count += 1 - # 递归处理孙节点 - process_children(child, depth + 1) - except Exception as e: - print(f"处理子节点时出错 (深度 {depth}): {e}") - continue - - process_children(model) - - if fixed_count > 0: - print(f"共修复了 {fixed_count} 个节点的变换") - - return True - except Exception as e: - print(f"验证所有变换时出错: {e}") - return False - - def _validateAndFixTransform(self, node_path): - """验证并修复单个节点的变换矩阵""" - try: - node_name = node_path.getName() - - # 获取当前变换状态 - original_pos = node_path.getPos() - original_hpr = node_path.getHpr() - original_scale = node_path.getScale() - - # 检查位置是否包含无效值 - if not original_pos.isFinite(): - print(f"警告: 节点 {node_name} 位置包含无效值 {original_pos},重置为 (0,0,0)") - node_path.setPos(0, 0, 0) - return False - - # 检查旋转是否包含无效值 - if not original_hpr.isFinite(): - print(f"警告: 节点 {node_name} 旋转包含无效值 {original_hpr},重置为 (0,0,0)") - node_path.setHpr(0, 0, 0) - return False - - # 检查缩放是否包含无效值或为零 - if not original_scale.isFinite(): - print(f"警告: 节点 {node_name} 缩放包含无效值 {original_scale},重置为 (1,1,1)") - node_path.setScale(1, 1, 1) - return False - - # 检查缩放是否为零或接近零 - min_scale = 1e-10 - if (abs(original_scale.x) < min_scale or - abs(original_scale.y) < min_scale or - abs(original_scale.z) < min_scale): - print(f"警告: 节点 {node_name} 缩放接近零 {original_scale},重置为 (1,1,1)") - node_path.setScale(1, 1, 1) - return False - - # 检查缩放是否过大(防止异常大的缩放) - max_scale = 1000000 # 100万倍作为上限 - if (abs(original_scale.x) > max_scale or - abs(original_scale.y) > max_scale or - abs(original_scale.z) > max_scale): - print(f"警告: 节点 {node_name} 缩放过异常 {original_scale},重置为 (1,1,1)") - node_path.setScale(1, 1, 1) - return False - - return True - - except Exception as e: - print(f"验证/修复节点 {node_path.getName()} 变换时出错: {e}") - # 只在出现严重错误时才重置变换 - try: - node_path.setPos(0, 0, 0) - node_path.setHpr(0, 0, 0) - node_path.setScale(1, 1, 1) - except: - pass - return False - - 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: - if node_material.hasBaseColor(): - color = node_material.getBaseColor() - has_color = True - #print(f"{indent}从节点材质获取基础颜色: {color}") - elif node_material.hasDiffuse(): - color = node_material.getDiffuse() - has_color = True - #print(f"{indent}从节点材质获取漫反射颜色: {color}") - - # 检查几何体材质 - if not has_color: - for i in range(geom_node.getNumGeoms()): - try: - geom = geom_node.getGeom(i) - state = geom_node.getGeomState(i) - - # 检查材质属性 - 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 and color: - #print(f"{indent}应用找到的颜色: {color}") - try: - # 确保颜色值有效 - if (color.getX() == color.getX() and color.getY() == color.getY() and - color.getZ() == color.getZ() and color.getW() == color.getW()): - material.setBaseColor(color) - material.setDiffuse(color) - node_path.setColor(color) - else: - print(f"{indent}⚠️ 颜色值无效,使用默认颜色") - material.setBaseColor((0.8, 0.8, 0.8, 1.0)) - material.setDiffuse((0.8, 0.8, 0.8, 1.0)) - except Exception as color_error: - print(f"{indent}设置颜色时出错: {color_error}") - material.setBaseColor((0.8, 0.8, 0.8, 1.0)) - material.setDiffuse((0.8, 0.8, 0.8, 1.0)) - else: - print(f"{indent}使用默认颜色") - material.setBaseColor((0.8, 0.8, 0.8, 1.0)) - 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) - - # 应用材质 - try: - node_path.setMaterial(material, 1) # 1表示强制应用 - #print(f"{indent}材质应用成功") - except Exception as mat_error: - print(f"{indent}⚠️ 应用材质时出错: {mat_error}") - - #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 _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: # 只标准化明显的大缩放 - # 确保标准化因子有效 - if normalize_factor <= 0 or normalize_factor > 1000: - print(f"{indent}无效的标准化因子: {normalize_factor},跳过") - return - - # 应用新的缩放 - new_scale = current_scale * normalize_factor - - # 检查新缩放是否有效 - if any(s <= 0 for s in [new_scale.x, new_scale.y, new_scale.z]): - print(f"{indent}标准化后产生无效缩放,跳过") - return - - 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}") - - 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()} 启用模型间碰撞检测") - - # 获取模型的边界信息,使用与选择框相同的计算方法 - minPoint = Point3() - maxPoint = Point3() - - # 使用与选择框相同的calcTightBounds方法获取边界 - if model.calcTightBounds(minPoint, maxPoint, self.world.render): - # 检查边界框的有效性 - if (abs(minPoint.x) < 1e10 and abs(minPoint.y) < 1e10 and abs(minPoint.z) < 1e10 and - abs(maxPoint.x) < 1e10 and abs(maxPoint.y) < 1e10 and abs(maxPoint.z) < 1e10): - # 特殊处理FBX模型的碰撞体 - if model.hasTag("model_path") and model.getTag("model_path").lower().endswith('.fbx'): - print("检测到FBX模型,调整碰撞体...") - # 反向应用FBX的变换以匹配视觉表现 - # 缩放调整: 乘以100(因为模型被缩小了0.01倍) - minPoint *= 100 - maxPoint *= 100 - - # 旋转调整: 绕P轴旋转-90度 - # 创建旋转矩阵 - from panda3d.core import Mat4, LRotation - rotation = LRotation(0, -90, 0) # 绕P轴旋转-90度 - rot_matrix = Mat4() - rotation.extractToMatrix(rot_matrix) - - # 应用旋转变换到边界点 - minPoint = rot_matrix.xformPoint(minPoint) - maxPoint = rot_matrix.xformPoint(maxPoint) - - # 创建与选择框完全一致的碰撞体 - cBox = CollisionBox(minPoint, maxPoint) - cNode.addSolid(cBox) - radius = max(maxPoint.x - minPoint.x, maxPoint.y - minPoint.y, maxPoint.z - minPoint.z) / 2 - else: - # 使用默认球体 - radius = 1.0 - cSphere = CollisionSphere(Point3(0, 0, 0), radius) - cNode.addSolid(cSphere) - else: - # 使用默认球体 - radius = 1.0 - cSphere = CollisionSphere(Point3(0, 0, 0), radius) - 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)) - - 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" - 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, '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 == "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 _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) - - 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) - - # 添加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 = [] - copied_resources = {} - for gui_node in gui_elements: - gui_info = self._collectGUIElementInfo(gui_node) - if gui_info: - gui_type = gui_info.get("type","") - #处理2d图片 - if gui_type =="2d_image" and "image_path" in gui_info: - original_path = gui_info["image_path"] - if original_path and os.path.exists(original_path): - resource_name = os.path.basename(original_path) - new_path = os.path.join(resources_dir,resource_name) - if original_path not in copied_resources: - try: - shutil.copy2(original_path,new_path) - copied_resources[original_path] = new_path - print(f"复制图片资源: {original_path} -> {new_path}") - except Exception as e: - print(f"复制图片资源失败: {original_path}, 错误: {e}") - gui_info["image_path"] = new_path - - # 处理3D图片 - elif gui_type == "3d_image" and "image_path" in gui_info: - original_path = gui_info["image_path"] - # 确保original_path是有效字符串且文件存在 - if original_path and isinstance(original_path, str) and os.path.exists(original_path): - resource_name = os.path.basename(original_path) - new_path = os.path.join(resources_dir, resource_name) - if original_path not in copied_resources: - try: - shutil.copy2(original_path, new_path) - copied_resources[original_path] = new_path - print(f"复制3D图片资源: {original_path} -> {new_path}") - except Exception as e: - print(f"复制3D图片资源失败: {original_path}, 错误: {e}") - gui_info["image_path"] = new_path - - # 处理背景图片 - if "bg_image_path" in gui_info and gui_info["bg_image_path"]: - original_path = gui_info["bg_image_path"] - # 确保original_path是有效字符串且文件存在 - if original_path and isinstance(original_path, str) and os.path.exists(original_path): - resource_name = os.path.basename(original_path) - new_path = os.path.join(resources_dir, resource_name) - if original_path not in copied_resources: - try: - shutil.copy2(original_path, new_path) - copied_resources[original_path] = new_path - print(f"复制背景图片资源: {original_path} -> {new_path}") - except Exception as e: - print(f"复制背景图片资源失败: {original_path}, 错误: {e}") - gui_info["bg_image_path"] = new_path - - # 处理视频资源 - if gui_type in ["video_screen", "2d_video_screen"] and "video_path" in gui_info: - original_path = gui_info["video_path"] - # 确保original_path是有效字符串且文件存在 - if original_path and isinstance(original_path, str) and os.path.exists(original_path): - resource_name = os.path.basename(original_path) - new_path = os.path.join(resources_dir, resource_name) - if original_path not in copied_resources: - try: - shutil.copy2(original_path, new_path) - copied_resources[original_path] = new_path - print(f"复制视频资源: {original_path} -> {new_path}") - except Exception as e: - print(f"复制视频资源失败: {original_path}, 错误: {e}") - gui_info["video_path"] = new_path - - 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() - - # 保存所有节点的信息 - 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("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)) - - 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: - 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) - - # 清除tilesets - for tileset_info in self.tilesets: - tree_widget.delete_item(tileset_info['node']) - - 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元素,避免重复处理 - - #存储所有加载的节点,用于后续处理父子关系 - loaded_nodes = {} #name->nodePath映射 - - # 遍历场景中的所有节点 - def processNode(nodePath, depth=0): - indent = " " * depth - print(f"{indent}处理节点: {nodePath.getName()} (类型: {type(nodePath.node()).__name__})") - - #存储节点以便后续处理父子关系 - 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()}") - - # 如果是潜在的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}") - - # 恢复可见性状态 - 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": - 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_scrip_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) - - # 创建并恢复材质 - 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"): - print(f"J{indent}处理模型节点{nodePath.getName()}") - - #self._validateAndFixAllTransforms(nodePath) - - self._fixModelStructure(nodePath) - - # 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) - - #处理父子关系 - 在所有节点加载完成后设置正确的父子关系 - 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(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 _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 _recreateGUIElementsFromData(self, gui_data): - """根据保存的GUI数据重新创建GUI元素""" - try: - gui_manager = getattr(self.world, 'gui_manager', None) - property_manager = getattr(self.world, 'property_panel', None) - info_panel_manager = getattr(self.world, 'info_panel_manager', None) - if not gui_manager: - print("GUI管理器未找到,无法重建GUI元素") - return - print(f"开始重建 {len(gui_data)} 个GUI元素...") - - processed_names = set() - created_elements = {} - # 存储原始的缩放和位置信息,用于后续计算 - element_original_data = {} - - # 第一遍:收集所有元素信息 - for i, gui_info in enumerate(gui_data): - name = gui_info.get("name", f"gui_element_{i}") - element_original_data[name] = { - "scale": gui_info.get("scale", [1, 1, 1]), - "position": gui_info.get("position", [0, 0, 0]), - "parent_name": gui_info.get("parent_name") - } - - valid_parents = set() - for gui_info in gui_data: - name = gui_info.get("name", f"gui_element_{gui_info.get('index', 0)}") - valid_parents.add(name) - - if hasattr(self.world, 'gui_elements'): - for elem in self.world.gui_elements: - if elem and not elem.isEmpty(): - valid_parents.add(elem.getName()) - - valid_parents.add("render") - valid_parents.add("aspect2d") - valid_parents.add("render2d") - - pos = (0, 0, 0) - 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", "") - bg_image_path = gui_info.get("bg_image_path", "") # 背景图片路径 - panel_id = gui_info.get("panel_id", name) # 信息面板ID - panel_data = gui_info.get("panel_data", None) # 面板数据 - parent_name = gui_info.get("parent_name") - - # 检查是否已经处理过同名元素 - if name in processed_names: - print(f"跳过重复元素: {name}") - continue - - if parent_name and parent_name not in valid_parents: - print(f"⚠️ 跳过元素 {name},因为其父级 {parent_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" 背景图片路径: {bg_image_path}") - print(f" 视频路径: {video_path}") - - absolute_position = list(position) - absolute_scale = list(scale) - - if parent_name and parent_name in element_original_data: - parent_data = element_original_data[parent_name] - parent_scale = parent_data["scale"] - - if gui_type in ["3d_text", "3d_image", "button", "label", "entry", "2d_image", - "2d_video_screen"]: - # 位置需要乘以父级缩放来得到绝对位置 - for j in range(min(len(absolute_position), len(parent_scale))): - absolute_position[j] *= parent_scale[j] if len(parent_scale) > j else parent_scale[0] - - # 缩放需要乘以父级缩放来得到绝对缩放 - for j in range(min(len(absolute_scale), len(parent_scale))): - absolute_scale[j] *= parent_scale[j] if len(parent_scale) > j else parent_scale[0] - - print(f" 绝对位置: {absolute_position}") - print(f" 绝对缩放: {absolute_scale}") - - # 根据类型创建相应的GUI元素 - new_element = None - - if gui_type == "button" and hasattr(gui_manager, 'createGUIButton'): - new_element = gui_manager.createGUIButton( - pos=tuple(absolute_position), - text=text, - size=absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 1.0 - ) - elif gui_type == "label" and hasattr(gui_manager, 'createGUILabel'): - scale_value = absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 1.0 - new_element = gui_manager.createGUILabel( - pos=tuple(absolute_position), - text=text, - size=scale_value - ) - elif gui_type == "entry" and hasattr(gui_manager, 'createGUIEntry'): - new_element = gui_manager.createGUIEntry( - pos=tuple(absolute_position), - placeholder=text, - size=absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 1.0 - ) - elif gui_type == "2d_image" and hasattr(gui_manager, 'createGUI2DImage'): - new_element = gui_manager.createGUI2DImage( - pos=tuple(absolute_position), - image_path=image_path, - size=(0.8,0.8,0.8) - ) - elif gui_type == "3d_text" and hasattr(gui_manager, 'createGUI3DText'): - size = absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 0.5 - new_element = gui_manager.createGUI3DText( - pos=tuple(absolute_position), - text=text, - size=absolute_scale - ) - elif gui_type == "3d_image" and hasattr(gui_manager, 'createGUI3DImage'): - # 处理3D图像 - # 根据缩放值的数量处理尺寸 - # 修复:不使用 absolute_scale,因为 createGUI3DImage 中已经处理了尺寸 - # 而是在创建后通过 setScale 设置缩放 - size = (1.0, 1.0, 1.0) # 使用默认尺寸创建 - - new_element = gui_manager.createGUI3DImage( - pos=tuple(absolute_position), - image_path=image_path, - size=size # 使用默认尺寸 - ) - elif gui_type == "video_screen" and hasattr(gui_manager, 'createVideoScreen'): - print(f"重建的3d视频屏幕视频地址是{video_path}") - new_element = gui_manager.createVideoScreen( - pos=tuple(absolute_position), - size=absolute_scale, - video_path=video_path - ) - if video_path and new_element: - if video_path.startswith("http://") or video_path.startswith("https://"): - pass - else: - if hasattr(gui_manager, 'loadVideoFile'): - from direct.task.TaskManagerGlobal import taskMgr - - def load_video_file_task(task): - gui_manager.loadVideoFile(new_element, video_path) - return task.done - - taskMgr.doMethodLater(0.1, load_video_file_task, 'loadVideoFileTask') - - elif gui_type == "2d_video_screen" and hasattr(gui_manager, 'createGUI2DVideoScreen'): - print(f"重建的2d视频屏幕视频地址是{video_path}") - new_element = gui_manager.createGUI2DVideoScreen( - pos=tuple(absolute_position), - size=absolute_scale, - video_path=video_path - ) - if video_path and new_element: - if video_path.startswith("http://") or video_path.startswith("https://"): - pass - else: - if hasattr(property_manager, 'load2DVideoFile'): - from direct.task.TaskManagerGlobal import taskMgr - - def load_2d_video_file_task(task): - property_manager.load2DVideoFile(new_element, video_path) - return task.done - - taskMgr.doMethodLater(0.1, load_2d_video_file_task, 'load2DVideoFileTask') - elif gui_type == "info_panel": - new_element = self.world.info_panel_manager.onCreateSampleInfoPanel() - # 如果创建成功,设置属性 - 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]) - - # 恢复GUI元素的可见性状态 - user_visible = gui_info.get("user_visible", True) - new_element.setPythonTag("user_visible", user_visible) - - # 应用可见性状态 - if hasattr(self.world, 'property_panel'): - self.world.property_panel._syncEffectiveVisibility(new_element) - else: - # 如果没有属性面板,直接应用可见性 - if user_visible: - new_element.show() - else: - new_element.hide() - - # 设置标签 - # 对于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) - - created_elements[name] = new_element - - 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 - - # 第二遍:设置父子级关系并更新Qt树 - print("开始设置父子级关系...") - try: - # 创建父子级关系映射 - parent_child_map = {} - for gui_info in gui_data: - name = gui_info.get("name") - parent_name = gui_info.get("parent_name") - - if name and parent_name and parent_name in created_elements: - parent_child_map[name] = parent_name - print(f"父子级关系映射: {parent_name} -> {name}") - - # 按正确的顺序设置父子级关系并更新Qt树 - tree_widget = self._get_tree_widget() - if tree_widget: - # 先将所有元素添加到Qt树中 - qt_tree_items = {} - for name, element in created_elements.items(): - # 尝试在Qt树中找到对应的项,如果找不到则创建 - qt_item = self._findOrCreateQtTreeItem(tree_widget, element, name) - if qt_item: - qt_tree_items[name] = qt_item - - # 然后设置父子级关系 - for child_name, parent_name in parent_child_map.items(): - try: - if child_name in created_elements and parent_name in created_elements: - child_element = created_elements[child_name] - parent_element = created_elements[parent_name] - - # 设置父子级关系 - if hasattr(child_element, 'reparentTo'): - child_element.reparentTo(parent_element) - print(f"成功设置父子级关系: {parent_name} -> {child_name}") - - # 更新Qt树显示 - if child_name in qt_tree_items and parent_name in qt_tree_items: - child_item = qt_tree_items[child_name] - parent_item = qt_tree_items[parent_name] - - # 从当前位置移除子项 - if child_item.parent(): - child_item.parent().removeChild(child_item) - else: - # 如果是顶级项,从树中移除 - index = tree_widget.indexOfTopLevelItem(child_item) - if index >= 0: - tree_widget.takeTopLevelItem(index) - - # 将子项添加到新的父项下 - parent_item.addChild(child_item) - print(f"Qt树更新: {child_name} 移动到 {parent_name} 下") - else: - print(f"元素 {child_name} 不支持 reparentTo 操作") - else: - print(f"元素未找到: 父级={parent_name}, 子级={child_name}") - except Exception as e: - print(f"设置父子级关系失败 {parent_name} -> {child_name}: {e}") - continue - else: - # 如果没有tree_widget,只设置父子级关系 - for child_name, parent_name in parent_child_map.items(): - try: - if child_name in created_elements and parent_name in created_elements: - child_element = created_elements[child_name] - parent_element = created_elements[parent_name] - - # 设置父子级关系 - if hasattr(child_element, 'reparentTo'): - child_element.reparentTo(parent_element) - print(f"成功设置父子级关系: {parent_name} -> {child_name}") - except Exception as e: - print(f"设置父子级关系失败 {parent_name} -> {child_name}: {e}") - continue - - except Exception as e: - print(f"设置父子级关系时出错: {e}") - # 第三遍:重新挂载脚本 - print("开始重新挂载脚本...") - for gui_info in gui_data: - try: - name = gui_info.get("name") - if name in created_elements and "scripts" in gui_info: - new_element = created_elements[name] - - # 重新挂载脚本(如果有的话) - 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}") - except Exception as e: - print(f"重新挂载脚本失败: {e}") - import traceback - traceback.print_exc() - continue - - print(f"GUI元素重建完成,共创建 {len(created_elements)} 个元素") - - except Exception as e: - print(f"重建GUI元素时出错: {e}") - import traceback - traceback.print_exc() - - def _findOrCreateQtTreeItem(self, tree_widget, target_element, element_name): - """在Qt树中查找或创建指定元素对应的项""" - try: - # 首先尝试查找现有的项 - existing_item = self._findQtTreeItem(tree_widget, target_element) - if existing_item: - return existing_item - - # 如果找不到,创建新的项 - # 找到场景根节点 - scene_root = None - for i in range(tree_widget.topLevelItemCount()): - top_item = tree_widget.topLevelItem(i) - if top_item.data(0, Qt.UserRole + 1) == "SCENE_ROOT": - scene_root = top_item - break - - if not scene_root: - print("无法找到场景根节点") - return None - - # 创建新的Qt树项 - new_item = QTreeWidgetItem(scene_root, [element_name]) - new_item.setData(0, Qt.UserRole, target_element) - new_item.setData(0, Qt.UserRole + 1, "SCENE_NODE") # 或根据元素类型设置适当的类型 - - print(f"为元素 {element_name} 创建了新的Qt树项") - return new_item - - except Exception as e: - print(f"查找或创建Qt树项失败: {e}") - return None - - def _findQtTreeItem(self, tree_widget, target_element): - """在Qt树中查找指定元素对应的项""" - try: - def search_recursive(parent_item): - # 检查当前项 - if parent_item: - item_element = parent_item.data(0, Qt.UserRole) - if item_element == target_element: - return parent_item - - # 递归检查子项 - for i in range(parent_item.childCount()): - child_item = parent_item.child(i) - result = search_recursive(child_item) - if result: - return result - return None - - # 从根节点开始搜索 - root = tree_widget.invisibleRootItem() - for i in range(root.childCount()): - top_item = root.child(i) - result = search_recursive(top_item) - if result: - return result - - return None - except Exception as e: - print(f"查找Qt树项失败: {e}") - return None - 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 panda3d.core import Vec3 - - # 创建聚光灯对象 - light = SpotLight() - light.direction = Vec3(0, 0, -1) - light.fov = 70 - light.set_color_from_temperature(5 * 1000.0) - - # 恢复保存的属性 - if light_node.hasTag("light_energy"): - light.energy = float(light_node.getTag("light_energy")) - else: - light.energy = 5000 - - light.radius = 1000 - light.casts_shadows = True - light.shadow_map_resolution = 256 - - light_pos = light_node.getPos() - light.setPos(light_pos) - - # 添加到渲染管线 - 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") - light_node.setTag("is_scene_element", "1") - light_node.setTag("element_type", "spotlight") - light_node.setTag("tree_item_type", "LIGHT_NODE") - - if light_node.hasTag("stored_energy"): - stored_energy = float(light_node.getTag("stored_energy")) - if stored_energy > 0: - light_node.setTag("stored_energy", str(stored_energy)) - - user_visible = True - if light_node.hasTag("user_visible"): - user_visible = light_node.getTag("user_visible").lower() == "true" - - light_node.setPythonTag("user_visible",user_visible) - if not user_visible: - self.toggleLightVisibility(light_node,False) - except Exception as e: - print(f"重新创建聚光灯失败: {str(e)}") - import traceback - traceback.print_exc() - - def _recreatePointLight(self, light_node): - """重新创建点光源""" - try: - from RenderPipelineFile.rpcore import PointLight - from QMeta3D.Meta3DWorld 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") - - light_node.setTag("is_scene_element", "1") - light_node.setTag("element_type", "pointlight") - light_node.setTag("tree_item_type", "LIGHT_NODE") - - if light_node.hasTag("stored_energy"): - stored_energy = float(light_node.getTag("stored_energy")) - if stored_energy > 0: - light_node.setTag("stored_energy", str(stored_energy)) - - user_visible = True - if light_node.hasTag("user_visible"): - user_visible = light_node.getTag("user_visible").lower()=="true" - - light_node.setPythonTag("user_visible",user_visible) - - if not user_visible: - self.toggleLightVisibility(light_node,False) - except Exception as e: - print(f"重新创建点光源失败: {str(e)}") - import traceback - traceback.print_exc() - - 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 QMeta3D.Meta3DWorld 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_np.setTransform(TransformState.makeIdentity()) - - # 创建聚光灯对象 - 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.setTag("element_type","spotlight") - - # 保存光源对象引用 - 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 QMeta3D.Meta3DWorld 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_np.setTransform(TransformState.makeIdentity()) - - # 创建点光源对象 - 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.setTag("element_type","pointlight") - - # 保存光源对象引用 - 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 isLightObject(self, nodePath): - """检查是否为灯光对象""" - try: - if not nodePath: - return False - - - # 方法1: 检查PythonTag - if nodePath.hasPythonTag("rp_light_object"): - rp_light = nodePath.getPythonTag("rp_light_object") - if rp_light is not None: - return True - - # 方法2: 检查element_type标签 - if nodePath.hasTag("element_type"): - element_type = nodePath.getTag("element_type") - if element_type in ["spotlight", "pointlight"]: - return True - - # 方法3: 检查tree_item_type标签 - if nodePath.hasTag("tree_item_type"): - tree_item_type = nodePath.getTag("tree_item_type") - if tree_item_type == "LIGHT_NODE": - return True - - # 方法4: 通过名称模式匹配(作为后备方案) - node_name = nodePath.getName().lower() - if "spotlight" in node_name or "pointlight" in node_name: - return True - - return False - except Exception as e: - print(f"检查灯光对象时出错: {e}") - import traceback - traceback.print_exc() - return False - - def toggleLightVisibility(self, light_node, visible): - """切换灯光可见性""" - try: - print(f"切换灯光可见性: {light_node.getName()}, 可见={visible}") - - # 保存用户可见性状态到该特定节点 - light_node.setPythonTag("user_visible", visible) - - # 获取该特定灯光对象 - rp_light_object = light_node.getPythonTag("rp_light_object") - if not rp_light_object: - print(f"错误: {light_node.getName()} 未找到RP灯光对象引用") - return - - # 获取RenderPipeline实例 - from QMeta3D.Meta3DWorld import get_render_pipeline - render_pipeline = get_render_pipeline() - - if not render_pipeline: - print("错误: 无法获取RenderPipeline实例") - return - - try: - if visible: - if light_node.hasTag("stored_energy"): - stored_energy = float(light_node.getTag("stored_energy")) - rp_light_object.energy=stored_energy - print(f"已恢复灯光强度: {light_node.getName()}, 能量={stored_energy}") - # 启用特定灯光 - # render_pipeline.add_light(rp_light_object) - # print(f"已添加灯光到渲染管线: {light_node.getName()}") - else: - # 禁用特定灯光 - current_energy = rp_light_object.energy - if current_energy != 0.0: - light_node.setTag("stored_energy", str(current_energy)) - elif light_node.hasTag("stored_energy"): - stored_energy = float(light_node.getTag("stored_energy")) - current_energy = stored_energy - else: - current_energy = 0.0 - rp_light_object.energy = 0.0 - print(f"已禁用灯光: {light_node.getName()}, 保存的能量={current_energy}") - # render_pipeline.remove_light(rp_light_object) - # print(f"已从渲染管线移除灯光: {light_node.getName()}") - except Exception as e: - print(f"操作RenderPipeline灯光时出错: {e}") - - # 控制节点显示状态(可选,主要是视觉上的) - if visible: - light_node.show() - else: - light_node.hide() - - print(f"灯光可见性设置完成: {visible}") - except Exception as e: - print(f"切换灯光可见性失败: {str(e)}") - import traceback - traceback.print_exc() - - 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 - - - # ==================== 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 - - - def serializeNode(self, node): - """序列化节点为字典数据""" - try: - node_data = { - 'name': node.getName(), - 'type': type(node.node()).__name__, - 'pos': (node.getX(), node.getY(), node.getZ()), - 'hpr': (node.getH(), node.getP(), node.getR()), - 'scale': (node.getSx(), node.getSy(), node.getSz()), - 'tags': {}, - 'children': [] - } - - # 保存所有标签 - for tag_key in node.getTagKeys(): - node_data['tags'][tag_key] = node.getTag(tag_key) - - # 特殊处理不同类型的节点 - if hasattr(node.node(), 'getClassType'): - node_class = node.node().getClassType().getName() - node_data['node_class'] = node_class - - # 递归序列化子节点 - for child in node.getChildren(): - # 跳过辅助节点 - if not child.getName().startswith(('gizmo', 'selectionBox', 'grid')): - child_data = self.serializeNode(child) - if child_data: - node_data['children'].append(child_data) - - return node_data - - except Exception as e: - print(f"序列化节点 {node.getName()} 失败: {e}") - import traceback - traceback.print_exc() - return None - - def deserializeNode(self, node_data, parent_node): - """从字典数据反序列化节点""" - try: - # 创建新节点 - node_name = node_data.get('name', 'node') - new_node = parent_node.attachNewNode(node_name) - - # 设置变换 - pos = node_data.get('pos', (0, 0, 0)) - hpr = node_data.get('hpr', (0, 0, 0)) - scale = node_data.get('scale', (1, 1, 1)) - - new_node.setPos(*pos) - new_node.setHpr(*hpr) - new_node.setScale(*scale) - - # 恢复标签 - for tag_key, tag_value in node_data.get('tags', {}).items(): - new_node.setTag(tag_key, tag_value) - - # 根据节点类型进行特殊处理 - node_type = node_data.get('type', '') - node_class = node_data.get('node_class', '') - - # 特殊处理光源节点 - if 'light_type' in node_data.get('tags', {}): - light_type = node_data['tags']['light_type'] - if light_type == 'spot_light': - self._recreateSpotLight(new_node) - elif light_type == 'point_light': - self._recreatePointLight(new_node) - - # 递归创建子节点 - for child_data in node_data.get('children', []): - self.deserializeNode(child_data, new_node) - - return new_node - - except Exception as e: - print(f"反序列化节点 {node_data.get('name', 'unknown')} 失败: {e}") - import traceback - traceback.print_exc() - return None - - def serializeNodeForCopy(self, node): - """序列化节点用于复制操作,完整保存视觉属性""" - try: - if not node or node.isEmpty(): - return None - - node_data = { - 'name': node.getName(), - 'type': type(node.node()).__name__, - 'pos': (node.getX(), node.getY(), node.getZ()), - 'hpr': (node.getH(), node.getP(), node.getR()), - 'scale': (node.getSx(), node.getSy(), node.getSz()), - 'tags': {}, - 'children': [] - } - - # 保存所有标签 - try: - if hasattr(node, 'getTagKeys'): - for tag_key in node.getTagKeys(): - node_data['tags'][tag_key] = node.getTag(tag_key) - except Exception as e: - print(f"获取标签时出错: {e}") - - # 保存视觉属性 - try: - # 保存颜色属性 - if hasattr(node, 'getColor'): - color = node.getColor() - node_data['color'] = (color.getX(), color.getY(), color.getZ(), color.getW()) - - # 保存材质属性 - if hasattr(node, 'getMaterial'): - material = node.getMaterial() - if material: - material_data = {} - material_data['base_color'] = ( - material.getBaseColor().getX(), - material.getBaseColor().getY(), - material.getBaseColor().getZ(), - material.getBaseColor().getW() - ) - material_data['ambient'] = ( - material.getAmbient().getX(), - material.getAmbient().getY(), - material.getAmbient().getZ(), - material.getAmbient().getW() - ) - material_data['diffuse'] = ( - material.getDiffuse().getX(), - material.getDiffuse().getY(), - material.getDiffuse().getZ(), - material.getDiffuse().getW() - ) - material_data['specular'] = ( - material.getSpecular().getX(), - material.getSpecular().getY(), - material.getSpecular().getZ(), - material.getSpecular().getW() - ) - material_data['shininess'] = material.getShininess() - node_data['material'] = material_data - - except Exception as e: - print(f"保存视觉属性时出错: {e}") - - # 根据节点类型保存特定信息 - if node.hasTag("tree_item_type"): - node_type = node.getTag("tree_item_type") - node_data['node_type'] = node_type - - # 保存特定类型节点的额外信息 - if node_type in ["LIGHT_NODE", "SPOT_LIGHT_NODE", "POINT_LIGHT_NODE"]: - # 保存光源特定信息 - rp_light = node.getPythonTag("rp_light_object") - if rp_light: - node_data['light_data'] = { - 'energy': getattr(rp_light, 'energy', 5000), - 'radius': getattr(rp_light, 'radius', 1000), - 'fov': getattr(rp_light, 'fov', 70) if hasattr(rp_light, 'fov') else None, - 'inner_radius': getattr(rp_light, 'inner_radius', 0.4) if hasattr(rp_light, - 'inner_radius') else None, - 'casts_shadows': getattr(rp_light, 'casts_shadows', True) if hasattr(rp_light, - 'casts_shadows') else True, - 'shadow_map_resolution': getattr(rp_light, 'shadow_map_resolution', 256) if hasattr( - rp_light, 'shadow_map_resolution') else 256 - } - elif node_type in ["GUI_BUTTON", "GUI_LABEL", "GUI_ENTRY", "GUI_IMAGE", - "GUI_3D_TEXT", "GUI_3D_IMAGE", "GUI_VIRTUAL_SCREEN"]: - # 保存GUI元素特定信息 - node_data['gui_data'] = self._serializeGUIData(node) - elif node_type == "IMPORTED_MODEL_NODE": - # 保存模型特定信息 - node_data['model_data'] = self._serializeModelData(node) - - return node_data - - except Exception as e: - print(f"序列化节点失败: {e}") - import traceback - traceback.print_exc() - return None - - def _serializeGUIData(self, node): - """序列化GUI元素数据""" - try: - gui_data = {} - - # 保存GUI相关的通用属性 - if node.hasTag("gui_type"): - gui_data['gui_type'] = node.getTag("gui_type") - - # 保存文本内容(如果有的话) - if node.hasTag("text"): - gui_data['text'] = node.getTag("text") - - # 保存其他GUI相关标签 - gui_tags = ['font', 'font_size', 'text_color', 'bg_color', 'size'] - for tag in gui_tags: - if node.hasTag(tag): - gui_data[tag] = node.getTag(tag) - - return gui_data - except Exception as e: - print(f"序列化GUI数据失败: {e}") - return {} - - def _serializeModelTextures(self, node): - """序列化模型纹理信息""" - try: - texture_data = {} - - # 获取节点的所有纹理阶段 - from panda3d.core import TextureStage - texture_stages = node.findAllTextureStages() - - if texture_stages.getNumTextureStages() > 0: - texture_data['textures'] = {} - - # 遍历所有纹理阶段 - for i in range(texture_stages.getNumTextureStages()): - stage = texture_stages.getTextureStage(i) - stage_name = stage.getName() - - # 获取该阶段的纹理 - texture = node.getTexture(stage) - if texture: - # 保存纹理信息 - texture_info = { - 'stage_name': stage_name, - 'stage_mode': stage.getMode(), - 'stage_sort': stage.getSort(), # 保存纹理阶段排序 - 'texture_path': texture.getFullpath().toOsSpecific() if texture.hasFullpath() else '', - 'texture_name': texture.getName(), - 'wrap_u': texture.getWrapU(), - 'wrap_v': texture.getWrapV(), - 'minfilter': texture.getMinfilter(), - 'magfilter': texture.getMagfilter(), - 'anisotropic_degree': texture.getAnisotropicDegree() - } - - # 保存颜色比例和偏移(使用安全的方法) - try: - texture_info['color_scale'] = tuple(node.getTextureScale(stage)) - except: - texture_info['color_scale'] = (1.0, 1.0, 1.0, 1.0) - - try: - texture_info['color_offset'] = tuple(node.getTextureOffset(stage)) - except: - texture_info['color_offset'] = (0.0, 0.0, 0.0, 0.0) - - texture_data['textures'][stage_name] = texture_info - - return texture_data - except Exception as e: - print(f"序列化模型纹理时出错: {e}") - return {} - - def _serializeModelData(self, node): - """序列化模型数据,包括材质和纹理信息""" - try: - model_data = {} - - # 保存模型相关的标签 - model_tags = ['model_path', 'file', 'element_type'] - for tag in model_tags: - if node.hasTag(tag): - model_data[tag] = node.getTag(tag) - - # 保存材质信息 - try: - # 获取模型的材质信息 - if hasattr(node, 'getState'): - state = node.getState() - if state: - # 保存基础颜色信息(使用正确的方法) - from panda3d.core import ColorAttrib - color_attrib = state.getAttrib(ColorAttrib) - if color_attrib and not color_attrib.isOff(): - color = color_attrib.getColor() - model_data['base_color'] = ( - color.getX(), - color.getY(), - color.getZ(), - color.getW() - ) - - # 保存其他材质属性 - from panda3d.core import MaterialAttrib - material_attrib = state.getAttrib(MaterialAttrib.getClassType()) - if material_attrib: - material = material_attrib.getMaterial() - if material: - # 保存基础颜色 - base_color = material.getBaseColor() - model_data['material_base_color'] = ( - base_color.getX(), base_color.getY(), base_color.getZ(), base_color.getW() - ) - - # 保存环境光颜色 - ambient_color = material.getAmbient() - model_data['material_ambient_color'] = ( - ambient_color.getX(), ambient_color.getY(), ambient_color.getZ(), - ambient_color.getW() - ) - - # 保存漫反射颜色 - diffuse_color = material.getDiffuse() - model_data['material_diffuse_color'] = ( - diffuse_color.getX(), diffuse_color.getY(), diffuse_color.getZ(), - diffuse_color.getW() - ) - - # 保存高光颜色 - specular_color = material.getSpecular() - model_data['material_specular_color'] = ( - specular_color.getX(), specular_color.getY(), specular_color.getZ(), - specular_color.getW() - ) - - # 保存粗糙度和金属度等参数 - model_data['material_roughness'] = material.getRoughness() - model_data['material_metallic'] = material.getMetallic() - - # 保存自发光颜色 - emission_color = material.getEmission() - model_data['material_emission_color'] = ( - emission_color.getX(), emission_color.getY(), emission_color.getZ(), - emission_color.getW() - ) - - # 保存光泽度 - model_data['material_shininess'] = material.getShininess() - - # 保存透明度信息 - from panda3d.core import TransparencyAttrib - transparency_attrib = state.getAttrib(TransparencyAttrib.getClassType()) - if transparency_attrib: - model_data['transparency_mode'] = transparency_attrib.get_mode() - - except Exception as e: - print(f"保存材质信息时出错: {e}") - - # 保存纹理信息 - try: - texture_data = self._serializeModelTextures(node) - if texture_data: - model_data['texture_data'] = texture_data - except Exception as e: - print(f"保存纹理信息时出错: {e}") - - return model_data - except Exception as e: - print(f"序列化模型数据失败: {e}") - return {} - - def recreateNodeFromData(self, node_data, parent_node): - """根据数据重建节点,并确保在场景树中显示""" - try: - if not node_data or not parent_node or parent_node.isEmpty(): - return None - - print(f"正在重建节点 {node_data}") - node_type = node_data.get('node_type', '') - original_name = node_data.get('name', 'node') - - # 生成唯一名称 - unique_name = self._generateUniqueName(original_name, parent_node) - - # 根据节点类型调用相应的重建方法 - new_node = None - if node_type in ["LIGHT_NODE", "SPOT_LIGHT_NODE", "POINT_LIGHT_NODE"]: - new_node = self._recreateLightFromData(node_data, parent_node, unique_name) - elif node_type == "CESIUM_TILESET_NODE": - new_node = self._recreateTilesetFromData(node_data, parent_node, unique_name) - elif node_type in ["GUI_BUTTON", "GUI_LABEL", "GUI_ENTRY", "GUI_IMAGE", - "GUI_3DTEXT", "GUI_3DIMAGE", "GUI_VIDEO_SCREEN","GUI_2D_VIDEO_SCREEN"]: - new_node = self._recreateGUIFromData(node_data, parent_node, unique_name) - elif node_type == "IMPORTED_MODEL_NODE": - new_node = self._recreateModelFromData(node_data, parent_node, unique_name) - else: - # 创建普通节点 - new_node = self._createBasicNodeFromData(node_data, parent_node, unique_name) - - # 如果成功创建节点,确保它在场景树中显示 - if new_node: - # 尝试更新场景树以显示新节点 - try: - if hasattr(self.world, 'interface_manager') and self.world.interface_manager: - # 查找父节点在场景树中的对应项 - parent_item = self._findTreeItemForNode(parent_node) - if parent_item: - # 添加新节点到场景树 - tree_widget = self.world.interface_manager.treeWidget - if tree_widget: - tree_widget.add_node_to_tree_widget(new_node, parent_item, node_type or "NODE") - except Exception as e: - print(f"添加节点到场景树时出错: {e}") - - return new_node - - except Exception as e: - print(f"重建节点失败: {e}") - import traceback - traceback.print_exc() - return None - - def _findTreeItemForNode(self, node): - """根据节点查找对应的场景树项""" - try: - if hasattr(self.world, 'interface_manager') and self.world.interface_manager: - tree_widget = self.world.interface_manager.treeWidget - if tree_widget: - # 遍历场景树查找匹配的节点项 - for i in range(tree_widget.topLevelItemCount()): - item = tree_widget.topLevelItem(i) - result = self._findTreeItemForNodeRecursive(item, node) - if result: - return result - return None - except Exception as e: - print(f"查找场景树项时出错: {e}") - return None - - def _findTreeItemForNodeRecursive(self, item, target_node): - """递归查找场景树项""" - try: - # 检查当前项是否匹配 - item_node = getattr(item, 'node_path', None) - if not item_node: - item_node = getattr(item, 'node', None) - - if item_node and item_node == target_node: - return item - - # 递归检查子项 - for i in range(item.childCount()): - child_item = item.child(i) - result = self._findTreeItemForNodeRecursive(child_item, target_node) - if result: - return result - - return None - except Exception as e: - print(f"递归查找场景树项时出错: {e}") - return None - - def _recreateLightFromData(self, node_data, parent_node, name): - """根据数据重建光源""" - try: - light_type = node_data.get('tags', {}).get('light_type', 'spot_light') - - # 创建光源 - if light_type == 'spot_light': - light_node = self.createSpotLight(pos=node_data.get('pos', (0, 0, 0))) - else: # point_light - light_node = self.createPointLight(pos=node_data.get('pos', (0, 0, 0))) - - if light_node: - # 设置名称 - light_node.setName(name) - - # 恢复其他属性 - light_data = node_data.get('light_data', {}) - rp_light = light_node.getPythonTag("rp_light_object") - if rp_light and light_data: - if 'energy' in light_data: - rp_light.energy = light_data['energy'] - if 'radius' in light_data: - rp_light.radius = light_data['radius'] - if 'fov' in light_data and hasattr(rp_light, 'fov'): - rp_light.fov = light_data['fov'] - if 'inner_radius' in light_data and hasattr(rp_light, 'inner_radius'): - rp_light.inner_radius = light_data['inner_radius'] - if 'casts_shadows' in light_data and hasattr(rp_light, 'casts_shadows'): - rp_light.casts_shadows = light_data['casts_shadows'] - if 'shadow_map_resolution' in light_data and hasattr(rp_light, 'shadow_map_resolution'): - rp_light.shadow_map_resolution = light_data['shadow_map_resolution'] - - # 恢复其他标签 - for tag_key, tag_value in node_data.get('tags', {}).items(): - if tag_key not in ['name', 'light_type']: - light_node.setTag(tag_key, str(tag_value)) - - return light_node - - except Exception as e: - print(f"重建光源失败: {e}") - return None - - def _recreateTilesetFromData(self, node_data, parent_node, name): - """根据数据重建Tileset""" - try: - tileset_url = node_data.get('tileset_url', '') - if not tileset_url: - return None - - # 使用现有方法加载tileset - position = node_data.get('pos', (0, 0, 0)) - tileset_node = self.load_cesium_tileset(tileset_url, position) - - if tileset_node: - # 设置名称 - tileset_node.setName(name) - - # 恢复其他标签 - for tag_key, tag_value in node_data.get('tags', {}).items(): - if tag_key not in ['name']: - tileset_node.setTag(tag_key, str(tag_value)) - - return tileset_node - - except Exception as e: - print(f"重建Tileset失败: {e}") - return None - - def _recreateGUIFromData(self, node_data, parent_node, name): - """根据数据重建GUI元素""" - try: - gui_data = node_data.get('gui_data', {}) - #gui_type = gui_data.get('gui_type', '') - gui_type = node_data.get("tags").get("gui_type", "") - - print(f"正在重建GUI元素: {gui_type}") - print(f"正在重建GUI元素: {node_data}") - - # 根据GUI类型调用相应的创建方法 - new_gui_element = None - - if gui_type == "button" and hasattr(self.world, 'createGUIButton'): - pos = node_data.get('pos', (0, 0, 0)) - text = node_data.get('tags').get('gui_text', '') - size = node_data.get('scale', 1) - print(pos,text,size) - new_gui_element = self.world.createGUIButton(pos,text,size) - elif gui_type == "label" and hasattr(self.world, 'createGUILabel'): - pos = node_data.get('pos', (0, 0, 0)) - text = node_data.get('tags').get('gui_text', '') - size = node_data.get('scale', 1) - new_gui_element = self.world.createGUILabel(pos,text,size) - elif gui_type == "entry" and hasattr(self.world, 'createGUIEntry'): - pos = node_data.get('pos', (0, 0, 0)) - text = node_data.get('tags').get('gui_text', '') - size = node_data.get('scale', 1) - new_gui_element = self.world.createGUIEntry(pos,text,size) - elif gui_type == "2d_image" and hasattr(self.world, 'createGUI2DImage'): - pos = node_data.get('pos', (0, 0, 0)) - image_path = node_data.get('tags').get('image_path', '') - size = node_data.get('size', 1) - new_gui_element = self.world.createGUI2DImage(pos, image_path, size) - elif gui_type == "3d_text" and hasattr(self.world, 'createGUI3DText'): - print("正在创建3D文本!!!") - pos = node_data.get('pos', (0, 0, 0)) - text = node_data.get('tags', {}).get('gui_text', '') - scale = node_data.get('scale', 1) - if isinstance(scale, (list, tuple)): - scale = scale[0] if len(scale) > 0 else 1 - print(f"正在创建3D文本: 位置={pos}, 文本={text}, 大小={scale}") - new_gui_element = self.world.createGUI3DText(pos, text, scale) - elif gui_type == "3d_image" and hasattr(self.world, 'createGUI3DImage'): - pos = node_data.get('pos', (0, 0, 0)) - image_path = node_data.get('tags').get('gui_image_path', '') - scale = node_data.get('scale', (1, 1)) - if isinstance(scale, (int, float)): - scale = (scale, scale) - elif isinstance(scale, (list, tuple)) and len(scale) >= 2: - scale = (scale[0], scale[1]) - else: - scale = (1, 1) - print(f"正在创建3D图片: 位置={pos}, 路径={image_path}, 大小={scale}") - new_gui_element = self.world.gui_manager.createGUI3DImage(pos, image_path, scale) - elif gui_type == "video_screen" and hasattr(self.world.gui_manager, 'createVideoScreen'): - pos = node_data.get('pos', (0, 0, 0)) - video_path = node_data.get('tags').get('video_path', '') - scale = node_data.get('scale', (1, 1,1)) - new_gui_element = self.world.gui_manager.createVideoScreen(pos,scale,video_path) - elif gui_type == "2d_video_screen" and hasattr(self.world.gui_manager, 'createGUI2DVideoScreen'): - pos = node_data.get('pos', (0, 0, 0)) - video_path = node_data.get('tags').get('video_path', '') - scale = node_data.get('scale', (1, 1, 1)) - new_gui_element = self.world.gui_manager.createGUI2DVideoScreen(pos,scale,video_path) - - if new_gui_element: - # 设置名称和变换 - if hasattr(new_gui_element, 'setName'): - new_gui_element.setName(name) - - # 设置位置、旋转、缩放 - pos = node_data.get('pos', (0, 0, 0)) - hpr = node_data.get('hpr', (0, 0, 0)) - scale = node_data.get('scale', (1, 1, 1)) - - if hasattr(new_gui_element, 'setPos'): - new_gui_element.setPos(*pos) - if hasattr(new_gui_element, 'setHpr'): - new_gui_element.setHpr(*hpr) - if hasattr(new_gui_element, 'setScale'): - new_gui_element.setScale(*scale) - - # 恢复文本内容 - if 'text' in gui_data and hasattr(new_gui_element, 'setText'): - new_gui_element.setText(gui_data['text']) - - # 恢复其他标签 - for tag_key, tag_value in node_data.get('tags', {}).items(): - if hasattr(new_gui_element, 'setTag') and tag_key not in ['name']: - new_gui_element.setTag(tag_key, str(tag_value)) - - print(f"GUI元素重建成功: {name}") - - return new_gui_element - - except Exception as e: - print(f"重建GUI元素失败: {e}") - import traceback - traceback.print_exc() - return None - - def _recreateModelFromData(self, node_data, parent_node, name): - """根据数据重建模型,保持材质""" - try: - model_data = node_data.get('model_data', {}) - model_path = model_data.get('model_path', model_data.get('file', '')) - - if not model_path or not os.path.exists(model_path): - # 如果原始模型文件不存在,创建一个基本节点 - return self._createBasicNodeFromData(node_data, parent_node, name) - - # 导入模型,保持原有参数 - model = self.importModel( - model_path, - apply_unit_conversion=False, # 已经处理过的模型不需要再次转换 - normalize_scales=False, # 保持原有缩放 - auto_convert_to_glb=False # 已经处理过的模型不需要再次转换 - ) - - if model: - # 设置名称 - model.setName(name) - - # 设置变换 - pos = node_data.get('pos', (0, 0, 0)) - hpr = node_data.get('hpr', (0, 0, 0)) - scale = node_data.get('scale', (1, 1, 1)) - - model.setPos(*pos) - model.setHpr(*hpr) - model.setScale(*scale) - - # 恢复材质和纹理信息 - try: - self._restoreModelMaterial(model, model_data) - except Exception as e: - print(f"恢复模型材质时出错: {e}") - - # 恢复标签 - for tag_key, tag_value in node_data.get('tags', {}).items(): - if tag_key not in ['name']: - model.setTag(tag_key, str(tag_value)) - - # 添加到模型列表 - if model not in self.models: - self.models.append(model) - - return model - - except Exception as e: - print(f"重建模型失败: {e}") - # 出错时创建基本节点 - return self._createBasicNodeFromData(node_data, parent_node, name) - - def _restoreModelTextures(self, model, texture_data): - """恢复模型纹理""" - try: - if not texture_data or 'textures' not in texture_data: - return - - from panda3d.core import TextureStage, SamplerState - - textures_info = texture_data['textures'] - - # 为每个纹理阶段恢复纹理 - for stage_name, texture_info in textures_info.items(): - # 创建纹理阶段 - stage = TextureStage(stage_name) - stage.setMode(texture_info.get('stage_mode', TextureStage.M_modulate)) - # 恢复纹理阶段排序 - stage.setSort(texture_info.get('stage_sort', 0)) # 默认为0(p3d_Texture0) - - # 加载纹理 - texture_path = texture_info['texture_path'] - if texture_path and os.path.exists(texture_path): - texture = self.world.loader.loadTexture(texture_path) - if texture: - # 设置纹理属性 - texture.setWrapU(texture_info.get('wrap_u', SamplerState.WM_repeat)) - texture.setWrapV(texture_info.get('wrap_v', SamplerState.WM_repeat)) - texture.setMinfilter(texture_info.get('minfilter', SamplerState.FT_linear)) - texture.setMagfilter(texture_info.get('magfilter', SamplerState.FT_linear)) - texture.setAnisotropicDegree(texture_info.get('anisotropic_degree', 1)) - - # 应用纹理到模型 - model.setTexture(stage, texture, 1) # 1 表示强制应用 - - # 恢复颜色比例和偏移(使用安全的方法) - if 'color_scale' in texture_info: - try: - model.setTextureScale(stage, *texture_info['color_scale']) - except Exception as e: - print(f"恢复纹理比例失败: {e}") - - if 'color_offset' in texture_info: - try: - model.setTextureOffset(stage, *texture_info['color_offset']) - except Exception as e: - print(f"恢复纹理偏移失败: {e}") - - print(f"恢复纹理: {stage_name} <- {texture_path}") - else: - print(f"纹理文件不存在或路径为空: {texture_path}") - - except Exception as e: - print(f"恢复模型纹理时出错: {e}") - - def _restoreModelMaterial(self, model, model_data): - """恢复模型材质和纹理""" - try: - # 恢复基础颜色 - if 'base_color' in model_data: - from panda3d.core import ColorAttrib - base_color = model_data['base_color'] - color = (base_color[0], base_color[1], base_color[2], base_color[3]) - model.setColor(color) - - # 恢复复杂材质属性 - if any(key.startswith('material_') for key in model_data.keys()): - from panda3d.core import Material - - # 创建新材质或获取现有材质 - material = Material() - - # 恢复基础颜色 - if 'material_base_color' in model_data: - base_color = model_data['material_base_color'] - material.setBaseColor((base_color[0], base_color[1], base_color[2], base_color[3])) - - # 恢复环境光颜色 - if 'material_ambient_color' in model_data: - ambient_color = model_data['material_ambient_color'] - material.setAmbient((ambient_color[0], ambient_color[1], ambient_color[2], ambient_color[3])) - - # 恢复漫反射颜色 - if 'material_diffuse_color' in model_data: - diffuse_color = model_data['material_diffuse_color'] - material.setDiffuse((diffuse_color[0], diffuse_color[1], diffuse_color[2], diffuse_color[3])) - - # 恢复高光颜色 - if 'material_specular_color' in model_data: - specular_color = model_data['material_specular_color'] - material.setSpecular((specular_color[0], specular_color[1], specular_color[2], specular_color[3])) - - # 恢复自发光颜色 - if 'material_emission_color' in model_data: - emission_color = model_data['material_emission_color'] - material.setEmission((emission_color[0], emission_color[1], emission_color[2], emission_color[3])) - - # 恢复粗糙度和金属度 - if 'material_roughness' in model_data: - material.setRoughness(model_data['material_roughness']) - - if 'material_metallic' in model_data: - material.setMetallic(model_data['material_metallic']) - - # 恢复光泽度 - if 'material_shininess' in model_data: - material.setShininess(model_data['material_shininess']) - - # 应用材质到模型 - model.setMaterial(material) - - # 恢复透明度设置 - if 'transparency_mode' in model_data: - from panda3d.core import TransparencyAttrib - transparency_mode = model_data['transparency_mode'] - model.setTransparency(transparency_mode) - - # 恢复纹理信息 - if 'texture_data' in model_data: - self._restoreModelTextures(model, model_data['texture_data']) - - except Exception as e: - print(f"恢复材质失败: {e}") - - def _createBasicNodeFromData(self, node_data, parent_node, name): - """创建基本节点,保持视觉属性""" - try: - new_node = parent_node.attachNewNode(name) - - # 设置变换 - pos = node_data.get('pos', (0, 0, 0)) - hpr = node_data.get('hpr', (0, 0, 0)) - scale = node_data.get('scale', (1, 1, 1)) - - new_node.setPos(*pos) - new_node.setHpr(*hpr) - new_node.setScale(*scale) - - # 恢复视觉属性 - try: - # 恢复颜色 - if 'color' in node_data: - color_data = node_data['color'] - new_node.setColor(color_data[0], color_data[1], color_data[2], color_data[3]) - - # 恢复材质 - if 'material' in node_data: - from panda3d.core import Material - material_data = node_data['material'] - material = Material() - - if 'base_color' in material_data: - bc = material_data['base_color'] - material.setBaseColor((bc[0], bc[1], bc[2], bc[3])) - - if 'ambient' in material_data: - ac = material_data['ambient'] - material.setAmbient((ac[0], ac[1], ac[2], ac[3])) - - if 'diffuse' in material_data: - dc = material_data['diffuse'] - material.setDiffuse((dc[0], dc[1], dc[2], dc[3])) - - if 'specular' in material_data: - sc = material_data['specular'] - material.setSpecular((sc[0], sc[1], sc[2], sc[3])) - - if 'shininess' in material_data: - material.setShininess(material_data['shininess']) - - new_node.setMaterial(material) - - except Exception as e: - print(f"恢复视觉属性时出错: {e}") - - # 恢复标签 - for tag_key, tag_value in node_data.get('tags', {}).items(): - if tag_key not in ['name']: - new_node.setTag(tag_key, str(tag_value)) - - return new_node - - except Exception as e: - print(f"创建基本节点失败: {e}") - return None - - def _generateUniqueName(self, base_name, parent_node): - """生成唯一节点名称""" - try: - # 移除可能的数字后缀 - import re - import time - name_base = re.sub(r'_\d+$', '', base_name) - - # 查找现有同名节点 - counter = 1 - unique_name = base_name - while True: - # 检查父节点下是否已存在同名子节点 - existing_node = parent_node.find(unique_name) - if existing_node.isEmpty(): - break - unique_name = f"{name_base}_{counter}" - counter += 1 - if counter > 1000: # 防止无限循环 - unique_name = f"{name_base}_{int(time.time())}" - break - - return unique_name - except: - return f"{base_name}_{int(time.time())}" - - +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +场景管理器 - 负责场景和模型管理的核心功能 +处理模型导入、场景树构建、材质系统、碰撞设置等 +""" + +import os +import shutil +import time + +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QTreeWidgetItem +from panda3d.core import ( + ModelPool, ModelRoot, Filename, NodePath, GeomNode, Material, Vec4, Vec3, + MaterialAttrib, ColorAttrib, Point3, CollisionNode, CollisionSphere, CollisionBox, + BitMask32, TransparencyAttrib, LColor, TransformState, RenderModeAttrib +) +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 QMeta3D.Meta3DWorld import get_render_pipeline +from RenderPipelineFile.rpplugins.smaa.jitters import halton_seq +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): + try: + if not os.path.exists(filepath): + print("文件不存在") + return None + + filepath = util.normalize_model_path(filepath) + original_filepath = filepath + + # # 在加载前设置忽略未知属性 + # from panda3d.core import ConfigVariableBool + # ConfigVariableBool("model-cache-ignore-unknown-properties").setValue(True) + # + # # 清除可能存在的模型缓存 + # from panda3d.core import ModelPool + # ModelPool.releaseAllModels() + # + # # 检查是否需要转换为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 + # # 转换成功的消息已在控制台显示,不再弹窗提示 + # else: + # print(f"⚠️ 转换失败,使用原始文件") + + model = self.world.loader.loadModel(filepath) + if not model: + print("加载模型失败") + return None + + # 设置模型名称 + model_name = os.path.basename(filepath) + # 确保名称有效 + if not model_name: + model_name = "imported_model" + model.setName(model_name) + # 将模型添加到场景 + model.reparentTo(self.world.render) + + # 设置模型名称 + model_name = os.path.basename(filepath) + model.setName(model_name) + + + # 保存原始路径和转换后的路径,处理跨平台路径问题 + # 确保路径在当前系统上有效 + normalized_filepath = filepath + # 检查路径是否有效,如果无效则尝试修复 + if not os.path.exists(filepath): + original_filepath = filepath + # 尝试多种修复策略 + fixed = False + + # 策略1: 处理Linux风格路径在Windows上的问题 + if filepath.startswith('/') and ':' not in filepath: + # 提取文件名并尝试在当前目录查找 + filename = os.path.basename(filepath) + potential_path = os.path.join(os.getcwd(), filename) + if os.path.exists(potential_path): + normalized_filepath = potential_path + fixed = True + + # 策略2: 处理路径分隔符问题 + if not fixed: + # 尝试规范化路径 + normalized_path = os.path.normpath(filepath) + if os.path.exists(normalized_path): + normalized_filepath = normalized_path + fixed = True + + # 策略3: 在Resources目录中查找 + if not fixed: + # 尝试在Resources目录中查找文件 + resources_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "Resources", "models") + potential_path = os.path.join(resources_path, filename) + if os.path.exists(potential_path): + normalized_filepath = potential_path + fixed = True + + if fixed: + print(f"路径修复: {original_filepath} -> {normalized_filepath}") + else: + print(f"[警告] 模型文件不存在: {filepath},将尝试继续加载") + # 即使文件不存在,也保存路径,以便属性面板可以尝试修复它 + + model.setTag("model_path", normalized_filepath) + model.setTag("original_path", original_filepath) + if normalized_filepath != original_filepath: + model.setTag("converted_from", os.path.splitext(original_filepath)[1]) + model.setTag("converted_to_glb", "true") + + #特殊处理FBX模型 + if filepath.lower().endswith('.fbx'): + print("检测到FBX模型,应用特殊处理...") + + # 将模型缩放设置为原来的1/100 + model.setScale(0.01) + print("设置模型缩放为 0.01 (原始大小的1/100)") + + # 设置模型旋转为 (0, 90, 0) + model.setHpr(0, 90, 0) + print("设置模型旋转为 (0, 90, 0)") + + # 调整模型位置到地面 + model.setPos(0,0,0) + #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 _fixModelStructure(self, model): + """修复模型结构""" + try: + # 使用正确的方式查找动画相关节点 + character_nodes = model.findAllMatches("**/+Character") + anim_bundle_nodes = model.findAllMatches("**/+AnimBundleNode") + + if character_nodes.getNumPaths() > 0 or anim_bundle_nodes.getNumPaths() > 0: + print(f"检测到模型{model.getName()}包含角色相节点:") + if character_nodes.getNumPaths() > 0: + print(f"CharacterNode数量:{character_nodes.getNumPaths()}") + if anim_bundle_nodes.getNumPaths() > 0: + print(f"AnimBundleNode数量: {anim_bundle_nodes.getNumPaths()}") + + model.setTag("fixed_structure", "true") + return True + except Exception as e: + print(f"修复模型结构时出错: {e}") + return False + + def _validateAndFixAllTransforms(self, model): + """递归验证并修复模型中所有节点的变换矩阵""" + try: + fixed_count = 0 + + # 先处理根节点 + if not self._validateAndFixTransform(model): + fixed_count += 1 + + # 递归处理所有子节点 + def process_children(node, depth=0): + nonlocal fixed_count + for i in range(node.getNumChildren()): + try: + child = node.getChild(i) + if not self._validateAndFixTransform(child): + fixed_count += 1 + # 递归处理孙节点 + process_children(child, depth + 1) + except Exception as e: + print(f"处理子节点时出错 (深度 {depth}): {e}") + continue + + process_children(model) + + if fixed_count > 0: + print(f"共修复了 {fixed_count} 个节点的变换") + + return True + except Exception as e: + print(f"验证所有变换时出错: {e}") + return False + + def _validateAndFixTransform(self, node_path): + """验证并修复单个节点的变换矩阵""" + try: + node_name = node_path.getName() + + # 获取当前变换状态 + original_pos = node_path.getPos() + original_hpr = node_path.getHpr() + original_scale = node_path.getScale() + + # 检查位置是否包含无效值 + if not original_pos.isFinite(): + print(f"警告: 节点 {node_name} 位置包含无效值 {original_pos},重置为 (0,0,0)") + node_path.setPos(0, 0, 0) + return False + + # 检查旋转是否包含无效值 + if not original_hpr.isFinite(): + print(f"警告: 节点 {node_name} 旋转包含无效值 {original_hpr},重置为 (0,0,0)") + node_path.setHpr(0, 0, 0) + return False + + # 检查缩放是否包含无效值或为零 + if not original_scale.isFinite(): + print(f"警告: 节点 {node_name} 缩放包含无效值 {original_scale},重置为 (1,1,1)") + node_path.setScale(1, 1, 1) + return False + + # 检查缩放是否为零或接近零 + min_scale = 1e-10 + if (abs(original_scale.x) < min_scale or + abs(original_scale.y) < min_scale or + abs(original_scale.z) < min_scale): + print(f"警告: 节点 {node_name} 缩放接近零 {original_scale},重置为 (1,1,1)") + node_path.setScale(1, 1, 1) + return False + + # 检查缩放是否过大(防止异常大的缩放) + max_scale = 1000000 # 100万倍作为上限 + if (abs(original_scale.x) > max_scale or + abs(original_scale.y) > max_scale or + abs(original_scale.z) > max_scale): + print(f"警告: 节点 {node_name} 缩放过异常 {original_scale},重置为 (1,1,1)") + node_path.setScale(1, 1, 1) + return False + + return True + + except Exception as e: + print(f"验证/修复节点 {node_path.getName()} 变换时出错: {e}") + # 只在出现严重错误时才重置变换 + try: + node_path.setPos(0, 0, 0) + node_path.setHpr(0, 0, 0) + node_path.setScale(1, 1, 1) + except: + pass + return False + + 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: + if node_material.hasBaseColor(): + color = node_material.getBaseColor() + has_color = True + #print(f"{indent}从节点材质获取基础颜色: {color}") + elif node_material.hasDiffuse(): + color = node_material.getDiffuse() + has_color = True + #print(f"{indent}从节点材质获取漫反射颜色: {color}") + + # 检查几何体材质 + if not has_color: + for i in range(geom_node.getNumGeoms()): + try: + geom = geom_node.getGeom(i) + state = geom_node.getGeomState(i) + + # 检查材质属性 + 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 and color: + #print(f"{indent}应用找到的颜色: {color}") + try: + # 确保颜色值有效 + if (color.getX() == color.getX() and color.getY() == color.getY() and + color.getZ() == color.getZ() and color.getW() == color.getW()): + material.setBaseColor(color) + material.setDiffuse(color) + node_path.setColor(color) + else: + print(f"{indent}⚠️ 颜色值无效,使用默认颜色") + material.setBaseColor((0.8, 0.8, 0.8, 1.0)) + material.setDiffuse((0.8, 0.8, 0.8, 1.0)) + except Exception as color_error: + print(f"{indent}设置颜色时出错: {color_error}") + material.setBaseColor((0.8, 0.8, 0.8, 1.0)) + material.setDiffuse((0.8, 0.8, 0.8, 1.0)) + else: + print(f"{indent}使用默认颜色") + material.setBaseColor((0.8, 0.8, 0.8, 1.0)) + 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) + + # 应用材质 + try: + node_path.setMaterial(material, 1) # 1表示强制应用 + #print(f"{indent}材质应用成功") + except Exception as mat_error: + print(f"{indent}⚠️ 应用材质时出错: {mat_error}") + + #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 _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: # 只标准化明显的大缩放 + # 确保标准化因子有效 + if normalize_factor <= 0 or normalize_factor > 1000: + print(f"{indent}无效的标准化因子: {normalize_factor},跳过") + return + + # 应用新的缩放 + new_scale = current_scale * normalize_factor + + # 检查新缩放是否有效 + if any(s <= 0 for s in [new_scale.x, new_scale.y, new_scale.z]): + print(f"{indent}标准化后产生无效缩放,跳过") + return + + 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}") + + 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()} 启用模型间碰撞检测") + + # 获取模型的边界信息,使用与选择框相同的计算方法 + minPoint = Point3() + maxPoint = Point3() + + # 使用与选择框相同的calcTightBounds方法获取边界 + if model.calcTightBounds(minPoint, maxPoint, self.world.render): + # 检查边界框的有效性 + if (abs(minPoint.x) < 1e10 and abs(minPoint.y) < 1e10 and abs(minPoint.z) < 1e10 and + abs(maxPoint.x) < 1e10 and abs(maxPoint.y) < 1e10 and abs(maxPoint.z) < 1e10): + # 特殊处理FBX模型的碰撞体 + if model.hasTag("model_path") and model.getTag("model_path").lower().endswith('.fbx'): + print("检测到FBX模型,调整碰撞体...") + # 反向应用FBX的变换以匹配视觉表现 + # 缩放调整: 乘以100(因为模型被缩小了0.01倍) + minPoint *= 100 + maxPoint *= 100 + + # 旋转调整: 绕P轴旋转-90度 + # 创建旋转矩阵 + from panda3d.core import Mat4, LRotation + rotation = LRotation(0, -90, 0) # 绕P轴旋转-90度 + rot_matrix = Mat4() + rotation.extractToMatrix(rot_matrix) + + # 应用旋转变换到边界点 + minPoint = rot_matrix.xformPoint(minPoint) + maxPoint = rot_matrix.xformPoint(maxPoint) + + # 创建与选择框完全一致的碰撞体 + cBox = CollisionBox(minPoint, maxPoint) + cNode.addSolid(cBox) + radius = max(maxPoint.x - minPoint.x, maxPoint.y - minPoint.y, maxPoint.z - minPoint.z) / 2 + else: + # 使用默认球体 + radius = 1.0 + cSphere = CollisionSphere(Point3(0, 0, 0), radius) + cNode.addSolid(cSphere) + else: + # 使用默认球体 + radius = 1.0 + cSphere = CollisionSphere(Point3(0, 0, 0), radius) + 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)) + + 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" + 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, '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 == "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 _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) + + 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) + + # 添加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 = [] + copied_resources = {} + for gui_node in gui_elements: + gui_info = self._collectGUIElementInfo(gui_node) + if gui_info: + gui_type = gui_info.get("type","") + #处理2d图片 + if gui_type =="2d_image" and "image_path" in gui_info: + original_path = gui_info["image_path"] + if original_path and os.path.exists(original_path): + resource_name = os.path.basename(original_path) + new_path = os.path.join(resources_dir,resource_name) + if original_path not in copied_resources: + try: + shutil.copy2(original_path,new_path) + copied_resources[original_path] = new_path + print(f"复制图片资源: {original_path} -> {new_path}") + except Exception as e: + print(f"复制图片资源失败: {original_path}, 错误: {e}") + gui_info["image_path"] = new_path + + # 处理3D图片 + elif gui_type == "3d_image" and "image_path" in gui_info: + original_path = gui_info["image_path"] + # 确保original_path是有效字符串且文件存在 + if original_path and isinstance(original_path, str) and os.path.exists(original_path): + resource_name = os.path.basename(original_path) + new_path = os.path.join(resources_dir, resource_name) + if original_path not in copied_resources: + try: + shutil.copy2(original_path, new_path) + copied_resources[original_path] = new_path + print(f"复制3D图片资源: {original_path} -> {new_path}") + except Exception as e: + print(f"复制3D图片资源失败: {original_path}, 错误: {e}") + gui_info["image_path"] = new_path + + # 处理背景图片 + if "bg_image_path" in gui_info and gui_info["bg_image_path"]: + original_path = gui_info["bg_image_path"] + # 确保original_path是有效字符串且文件存在 + if original_path and isinstance(original_path, str) and os.path.exists(original_path): + resource_name = os.path.basename(original_path) + new_path = os.path.join(resources_dir, resource_name) + if original_path not in copied_resources: + try: + shutil.copy2(original_path, new_path) + copied_resources[original_path] = new_path + print(f"复制背景图片资源: {original_path} -> {new_path}") + except Exception as e: + print(f"复制背景图片资源失败: {original_path}, 错误: {e}") + gui_info["bg_image_path"] = new_path + + # 处理视频资源 + if gui_type in ["video_screen", "2d_video_screen"] and "video_path" in gui_info: + original_path = gui_info["video_path"] + # 确保original_path是有效字符串且文件存在 + if original_path and isinstance(original_path, str) and os.path.exists(original_path): + resource_name = os.path.basename(original_path) + new_path = os.path.join(resources_dir, resource_name) + if original_path not in copied_resources: + try: + shutil.copy2(original_path, new_path) + copied_resources[original_path] = new_path + print(f"复制视频资源: {original_path} -> {new_path}") + except Exception as e: + print(f"复制视频资源失败: {original_path}, 错误: {e}") + gui_info["video_path"] = new_path + + 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() + + # 保存所有节点的信息 + 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("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)) + + # 保存模型动画信息 + 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: + 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) + + # 清除tilesets + for tileset_info in self.tilesets: + tree_widget.delete_item(tileset_info['node']) + + 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元素,避免重复处理 + + #存储所有加载的节点,用于后续处理父子关系 + loaded_nodes = {} #name->nodePath映射 + + # 遍历场景中的所有节点 + def processNode(nodePath, depth=0): + indent = " " * depth + print(f"{indent}处理节点: {nodePath.getName()} (类型: {type(nodePath.node()).__name__})") + + #存储节点以便后续处理父子关系 + 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()}") + + # 如果是潜在的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}") + + # 恢复可见性状态 + 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": + 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_scrip_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) + + # 创建并恢复材质 + 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"): + print(f"J{indent}处理模型节点{nodePath.getName()}") + + #self._validateAndFixAllTransforms(nodePath) + + self._fixModelStructure(nodePath) + + # 恢复模型动画信息 + self._restoreModelAnimationInfo(nodePath) + + # 检测并处理模型动画(类似property_panel.py中的逻辑) + self._processModelAnimations(nodePath) + + # 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) + + #处理父子关系 - 在所有节点加载完成后设置正确的父子关系 + 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(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 _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 _restoreModelAnimationInfo(self, model_node): + """恢复模型的动画信息""" + try: + # 从保存的标签中恢复动画信息 + if model_node.hasTag("saved_has_animations"): + model_node.setTag("has_animations", model_node.getTag("saved_has_animations")) + print(f"恢复模型 {model_node.getName()} 的动画信息") + + # 从保存的标签中恢复模型路径 + if model_node.hasTag("saved_model_path"): + model_path = model_node.getTag("saved_model_path") + # 处理跨平台路径问题 + if model_path: + # 将Linux风格路径转换为Windows风格路径(如果需要) + if model_path.startswith('/'): + # 尝试将其转换为Windows路径 + if ':' not in model_path: # 不是已经有效的Windows路径 + # 简单处理:移除前导斜杠 + model_path = model_path[1:] if len(model_path) > 1 else model_path + model_node.setTag("model_path", model_path) + + # 恢复内存创建标记 + if model_node.hasTag("saved_can_create_actor_from_memory"): + model_node.setTag("can_create_actor_from_memory", model_node.getTag("saved_can_create_actor_from_memory")) + + except Exception as e: + print(f"恢复模型 {model_node.getName()} 动画信息时出错: {e}") + + def _processModelAnimations(self, model_node): + """处理模型动画,确保在场景加载时正确识别动画信息""" + try: + # 检查模型是否已经有动画信息标签 + if model_node.hasTag("has_animations"): + has_animations = model_node.getTag("has_animations").lower() == "true" + if has_animations: + print(f"模型 {model_node.getName()} 已有动画信息") + return True + + # 检查模型是否包含动画相关节点 + character_nodes = model_node.findAllMatches("**/+Character") + anim_bundle_nodes = model_node.findAllMatches("**/+AnimBundleNode") + + has_animations = (character_nodes.getNumPaths() > 0 or + anim_bundle_nodes.getNumPaths() > 0) + + if has_animations: + print(f"检测到模型 {model_node.getName()} 包含动画:") + if character_nodes.getNumPaths() > 0: + print(f" CharacterNode数量: {character_nodes.getNumPaths()}") + if anim_bundle_nodes.getNumPaths() > 0: + print(f" AnimBundleNode数量: {anim_bundle_nodes.getNumPaths()}") + + # 保存动画信息到标签 + model_node.setTag("has_animations", "true") + + # 标记模型可以直接从内存中创建Actor + model_node.setTag("can_create_actor_from_memory", "true") + else: + model_node.setTag("has_animations", "false") + + return has_animations + + except Exception as e: + print(f"处理模型 {model_node.getName()} 动画时出错: {e}") + return False + + 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 _recreateGUIElementsFromData(self, gui_data): + """根据保存的GUI数据重新创建GUI元素""" + try: + gui_manager = getattr(self.world, 'gui_manager', None) + property_manager = getattr(self.world, 'property_panel', None) + info_panel_manager = getattr(self.world, 'info_panel_manager', None) + if not gui_manager: + print("GUI管理器未找到,无法重建GUI元素") + return + print(f"开始重建 {len(gui_data)} 个GUI元素...") + + processed_names = set() + created_elements = {} + # 存储原始的缩放和位置信息,用于后续计算 + element_original_data = {} + + # 第一遍:收集所有元素信息 + for i, gui_info in enumerate(gui_data): + name = gui_info.get("name", f"gui_element_{i}") + element_original_data[name] = { + "scale": gui_info.get("scale", [1, 1, 1]), + "position": gui_info.get("position", [0, 0, 0]), + "parent_name": gui_info.get("parent_name") + } + + valid_parents = set() + for gui_info in gui_data: + name = gui_info.get("name", f"gui_element_{gui_info.get('index', 0)}") + valid_parents.add(name) + + if hasattr(self.world, 'gui_elements'): + for elem in self.world.gui_elements: + if elem and not elem.isEmpty(): + valid_parents.add(elem.getName()) + + valid_parents.add("render") + valid_parents.add("aspect2d") + valid_parents.add("render2d") + + pos = (0, 0, 0) + 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", "") + bg_image_path = gui_info.get("bg_image_path", "") # 背景图片路径 + panel_id = gui_info.get("panel_id", name) # 信息面板ID + panel_data = gui_info.get("panel_data", None) # 面板数据 + parent_name = gui_info.get("parent_name") + + # 检查是否已经处理过同名元素 + if name in processed_names: + print(f"跳过重复元素: {name}") + continue + + if parent_name and parent_name not in valid_parents: + print(f"⚠️ 跳过元素 {name},因为其父级 {parent_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" 背景图片路径: {bg_image_path}") + print(f" 视频路径: {video_path}") + + absolute_position = list(position) + absolute_scale = list(scale) + + if parent_name and parent_name in element_original_data: + parent_data = element_original_data[parent_name] + parent_scale = parent_data["scale"] + + if gui_type in ["3d_text", "3d_image", "button", "label", "entry", "2d_image", + "2d_video_screen"]: + # 位置需要乘以父级缩放来得到绝对位置 + for j in range(min(len(absolute_position), len(parent_scale))): + absolute_position[j] *= parent_scale[j] if len(parent_scale) > j else parent_scale[0] + + # 缩放需要乘以父级缩放来得到绝对缩放 + for j in range(min(len(absolute_scale), len(parent_scale))): + absolute_scale[j] *= parent_scale[j] if len(parent_scale) > j else parent_scale[0] + + print(f" 绝对位置: {absolute_position}") + print(f" 绝对缩放: {absolute_scale}") + + # 根据类型创建相应的GUI元素 + new_element = None + + if gui_type == "button" and hasattr(gui_manager, 'createGUIButton'): + new_element = gui_manager.createGUIButton( + pos=tuple(absolute_position), + text=text, + size=absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 1.0 + ) + elif gui_type == "label" and hasattr(gui_manager, 'createGUILabel'): + scale_value = absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 1.0 + new_element = gui_manager.createGUILabel( + pos=tuple(absolute_position), + text=text, + size=scale_value + ) + elif gui_type == "entry" and hasattr(gui_manager, 'createGUIEntry'): + new_element = gui_manager.createGUIEntry( + pos=tuple(absolute_position), + placeholder=text, + size=absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 1.0 + ) + elif gui_type == "2d_image" and hasattr(gui_manager, 'createGUI2DImage'): + new_element = gui_manager.createGUI2DImage( + pos=tuple(absolute_position), + image_path=image_path, + size=(0.8,0.8,0.8) + ) + elif gui_type == "3d_text" and hasattr(gui_manager, 'createGUI3DText'): + size = absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 0.5 + new_element = gui_manager.createGUI3DText( + pos=tuple(absolute_position), + text=text, + size=absolute_scale + ) + elif gui_type == "3d_image" and hasattr(gui_manager, 'createGUI3DImage'): + # 处理3D图像 + # 根据缩放值的数量处理尺寸 + # 修复:不使用 absolute_scale,因为 createGUI3DImage 中已经处理了尺寸 + # 而是在创建后通过 setScale 设置缩放 + size = (1.0, 1.0, 1.0) # 使用默认尺寸创建 + + new_element = gui_manager.createGUI3DImage( + pos=tuple(absolute_position), + image_path=image_path, + size=size # 使用默认尺寸 + ) + elif gui_type == "video_screen" and hasattr(gui_manager, 'createVideoScreen'): + print(f"重建的3d视频屏幕视频地址是{video_path}") + new_element = gui_manager.createVideoScreen( + pos=tuple(absolute_position), + size=absolute_scale, + video_path=video_path + ) + if video_path and new_element: + if video_path.startswith("http://") or video_path.startswith("https://"): + pass + else: + if hasattr(gui_manager, 'loadVideoFile'): + from direct.task.TaskManagerGlobal import taskMgr + + def load_video_file_task(task): + gui_manager.loadVideoFile(new_element, video_path) + return task.done + + taskMgr.doMethodLater(0.1, load_video_file_task, 'loadVideoFileTask') + + elif gui_type == "2d_video_screen" and hasattr(gui_manager, 'createGUI2DVideoScreen'): + print(f"重建的2d视频屏幕视频地址是{video_path}") + new_element = gui_manager.createGUI2DVideoScreen( + pos=tuple(absolute_position), + size=absolute_scale, + video_path=video_path + ) + if video_path and new_element: + if video_path.startswith("http://") or video_path.startswith("https://"): + pass + else: + if hasattr(property_manager, 'load2DVideoFile'): + from direct.task.TaskManagerGlobal import taskMgr + + def load_2d_video_file_task(task): + property_manager.load2DVideoFile(new_element, video_path) + return task.done + + taskMgr.doMethodLater(0.1, load_2d_video_file_task, 'load2DVideoFileTask') + elif gui_type == "info_panel": + new_element = self.world.info_panel_manager.onCreateSampleInfoPanel() + # 如果创建成功,设置属性 + 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]) + + # 恢复GUI元素的可见性状态 + user_visible = gui_info.get("user_visible", True) + new_element.setPythonTag("user_visible", user_visible) + + # 应用可见性状态 + if hasattr(self.world, 'property_panel'): + self.world.property_panel._syncEffectiveVisibility(new_element) + else: + # 如果没有属性面板,直接应用可见性 + if user_visible: + new_element.show() + else: + new_element.hide() + + # 设置标签 + # 对于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) + + created_elements[name] = new_element + + 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 + + # 第二遍:设置父子级关系并更新Qt树 + print("开始设置父子级关系...") + try: + # 创建父子级关系映射 + parent_child_map = {} + for gui_info in gui_data: + name = gui_info.get("name") + parent_name = gui_info.get("parent_name") + + if name and parent_name and parent_name in created_elements: + parent_child_map[name] = parent_name + print(f"父子级关系映射: {parent_name} -> {name}") + + # 按正确的顺序设置父子级关系并更新Qt树 + tree_widget = self._get_tree_widget() + if tree_widget: + # 先将所有元素添加到Qt树中 + qt_tree_items = {} + for name, element in created_elements.items(): + # 尝试在Qt树中找到对应的项,如果找不到则创建 + qt_item = self._findOrCreateQtTreeItem(tree_widget, element, name) + if qt_item: + qt_tree_items[name] = qt_item + + # 然后设置父子级关系 + for child_name, parent_name in parent_child_map.items(): + try: + if child_name in created_elements and parent_name in created_elements: + child_element = created_elements[child_name] + parent_element = created_elements[parent_name] + + # 设置父子级关系 + if hasattr(child_element, 'reparentTo'): + child_element.reparentTo(parent_element) + print(f"成功设置父子级关系: {parent_name} -> {child_name}") + + # 更新Qt树显示 + if child_name in qt_tree_items and parent_name in qt_tree_items: + child_item = qt_tree_items[child_name] + parent_item = qt_tree_items[parent_name] + + # 从当前位置移除子项 + if child_item.parent(): + child_item.parent().removeChild(child_item) + else: + # 如果是顶级项,从树中移除 + index = tree_widget.indexOfTopLevelItem(child_item) + if index >= 0: + tree_widget.takeTopLevelItem(index) + + # 将子项添加到新的父项下 + parent_item.addChild(child_item) + print(f"Qt树更新: {child_name} 移动到 {parent_name} 下") + else: + print(f"元素 {child_name} 不支持 reparentTo 操作") + else: + print(f"元素未找到: 父级={parent_name}, 子级={child_name}") + except Exception as e: + print(f"设置父子级关系失败 {parent_name} -> {child_name}: {e}") + continue + else: + # 如果没有tree_widget,只设置父子级关系 + for child_name, parent_name in parent_child_map.items(): + try: + if child_name in created_elements and parent_name in created_elements: + child_element = created_elements[child_name] + parent_element = created_elements[parent_name] + + # 设置父子级关系 + if hasattr(child_element, 'reparentTo'): + child_element.reparentTo(parent_element) + print(f"成功设置父子级关系: {parent_name} -> {child_name}") + except Exception as e: + print(f"设置父子级关系失败 {parent_name} -> {child_name}: {e}") + continue + + except Exception as e: + print(f"设置父子级关系时出错: {e}") + # 第三遍:重新挂载脚本 + print("开始重新挂载脚本...") + for gui_info in gui_data: + try: + name = gui_info.get("name") + if name in created_elements and "scripts" in gui_info: + new_element = created_elements[name] + + # 重新挂载脚本(如果有的话) + 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}") + except Exception as e: + print(f"重新挂载脚本失败: {e}") + import traceback + traceback.print_exc() + continue + + print(f"GUI元素重建完成,共创建 {len(created_elements)} 个元素") + + except Exception as e: + print(f"重建GUI元素时出错: {e}") + import traceback + traceback.print_exc() + + def _findOrCreateQtTreeItem(self, tree_widget, target_element, element_name): + """在Qt树中查找或创建指定元素对应的项""" + try: + # 首先尝试查找现有的项 + existing_item = self._findQtTreeItem(tree_widget, target_element) + if existing_item: + return existing_item + + # 如果找不到,创建新的项 + # 找到场景根节点 + scene_root = None + for i in range(tree_widget.topLevelItemCount()): + top_item = tree_widget.topLevelItem(i) + if top_item.data(0, Qt.UserRole + 1) == "SCENE_ROOT": + scene_root = top_item + break + + if not scene_root: + print("无法找到场景根节点") + return None + + # 创建新的Qt树项 + new_item = QTreeWidgetItem(scene_root, [element_name]) + new_item.setData(0, Qt.UserRole, target_element) + new_item.setData(0, Qt.UserRole + 1, "SCENE_NODE") # 或根据元素类型设置适当的类型 + + print(f"为元素 {element_name} 创建了新的Qt树项") + return new_item + + except Exception as e: + print(f"查找或创建Qt树项失败: {e}") + return None + + def _findQtTreeItem(self, tree_widget, target_element): + """在Qt树中查找指定元素对应的项""" + try: + def search_recursive(parent_item): + # 检查当前项 + if parent_item: + item_element = parent_item.data(0, Qt.UserRole) + if item_element == target_element: + return parent_item + + # 递归检查子项 + for i in range(parent_item.childCount()): + child_item = parent_item.child(i) + result = search_recursive(child_item) + if result: + return result + return None + + # 从根节点开始搜索 + root = tree_widget.invisibleRootItem() + for i in range(root.childCount()): + top_item = root.child(i) + result = search_recursive(top_item) + if result: + return result + + return None + except Exception as e: + print(f"查找Qt树项失败: {e}") + return None + 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 panda3d.core import Vec3 + + # 创建聚光灯对象 + light = SpotLight() + light.direction = Vec3(0, 0, -1) + light.fov = 70 + light.set_color_from_temperature(5 * 1000.0) + + # 恢复保存的属性 + if light_node.hasTag("light_energy"): + light.energy = float(light_node.getTag("light_energy")) + else: + light.energy = 5000 + + light.radius = 1000 + light.casts_shadows = True + light.shadow_map_resolution = 256 + + light_pos = light_node.getPos() + light.setPos(light_pos) + + # 添加到渲染管线 + 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") + light_node.setTag("is_scene_element", "1") + light_node.setTag("element_type", "spotlight") + light_node.setTag("tree_item_type", "LIGHT_NODE") + + if light_node.hasTag("stored_energy"): + stored_energy = float(light_node.getTag("stored_energy")) + if stored_energy > 0: + light_node.setTag("stored_energy", str(stored_energy)) + + user_visible = True + if light_node.hasTag("user_visible"): + user_visible = light_node.getTag("user_visible").lower() == "true" + + light_node.setPythonTag("user_visible",user_visible) + if not user_visible: + self.toggleLightVisibility(light_node,False) + except Exception as e: + print(f"重新创建聚光灯失败: {str(e)}") + import traceback + traceback.print_exc() + + def _recreatePointLight(self, light_node): + """重新创建点光源""" + try: + from RenderPipelineFile.rpcore import PointLight + from QMeta3D.Meta3DWorld 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") + + light_node.setTag("is_scene_element", "1") + light_node.setTag("element_type", "pointlight") + light_node.setTag("tree_item_type", "LIGHT_NODE") + + if light_node.hasTag("stored_energy"): + stored_energy = float(light_node.getTag("stored_energy")) + if stored_energy > 0: + light_node.setTag("stored_energy", str(stored_energy)) + + user_visible = True + if light_node.hasTag("user_visible"): + user_visible = light_node.getTag("user_visible").lower()=="true" + + light_node.setPythonTag("user_visible",user_visible) + + if not user_visible: + self.toggleLightVisibility(light_node,False) + except Exception as e: + print(f"重新创建点光源失败: {str(e)}") + import traceback + traceback.print_exc() + + 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 QMeta3D.Meta3DWorld 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_np.setTransform(TransformState.makeIdentity()) + + # 创建聚光灯对象 + 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.setTag("element_type","spotlight") + + # 保存光源对象引用 + 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 QMeta3D.Meta3DWorld 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_np.setTransform(TransformState.makeIdentity()) + + # 创建点光源对象 + 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.setTag("element_type","pointlight") + + # 保存光源对象引用 + 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 isLightObject(self, nodePath): + """检查是否为灯光对象""" + try: + if not nodePath: + return False + + + # 方法1: 检查PythonTag + if nodePath.hasPythonTag("rp_light_object"): + rp_light = nodePath.getPythonTag("rp_light_object") + if rp_light is not None: + return True + + # 方法2: 检查element_type标签 + if nodePath.hasTag("element_type"): + element_type = nodePath.getTag("element_type") + if element_type in ["spotlight", "pointlight"]: + return True + + # 方法3: 检查tree_item_type标签 + if nodePath.hasTag("tree_item_type"): + tree_item_type = nodePath.getTag("tree_item_type") + if tree_item_type == "LIGHT_NODE": + return True + + # 方法4: 通过名称模式匹配(作为后备方案) + node_name = nodePath.getName().lower() + if "spotlight" in node_name or "pointlight" in node_name: + return True + + return False + except Exception as e: + print(f"检查灯光对象时出错: {e}") + import traceback + traceback.print_exc() + return False + + def toggleLightVisibility(self, light_node, visible): + """切换灯光可见性""" + try: + print(f"切换灯光可见性: {light_node.getName()}, 可见={visible}") + + # 保存用户可见性状态到该特定节点 + light_node.setPythonTag("user_visible", visible) + + # 获取该特定灯光对象 + rp_light_object = light_node.getPythonTag("rp_light_object") + if not rp_light_object: + print(f"错误: {light_node.getName()} 未找到RP灯光对象引用") + return + + # 获取RenderPipeline实例 + from QMeta3D.Meta3DWorld import get_render_pipeline + render_pipeline = get_render_pipeline() + + if not render_pipeline: + print("错误: 无法获取RenderPipeline实例") + return + + try: + if visible: + if light_node.hasTag("stored_energy"): + stored_energy = float(light_node.getTag("stored_energy")) + rp_light_object.energy=stored_energy + print(f"已恢复灯光强度: {light_node.getName()}, 能量={stored_energy}") + # 启用特定灯光 + # render_pipeline.add_light(rp_light_object) + # print(f"已添加灯光到渲染管线: {light_node.getName()}") + else: + # 禁用特定灯光 + current_energy = rp_light_object.energy + if current_energy != 0.0: + light_node.setTag("stored_energy", str(current_energy)) + elif light_node.hasTag("stored_energy"): + stored_energy = float(light_node.getTag("stored_energy")) + current_energy = stored_energy + else: + current_energy = 0.0 + rp_light_object.energy = 0.0 + print(f"已禁用灯光: {light_node.getName()}, 保存的能量={current_energy}") + # render_pipeline.remove_light(rp_light_object) + # print(f"已从渲染管线移除灯光: {light_node.getName()}") + except Exception as e: + print(f"操作RenderPipeline灯光时出错: {e}") + + # 控制节点显示状态(可选,主要是视觉上的) + if visible: + light_node.show() + else: + light_node.hide() + + print(f"灯光可见性设置完成: {visible}") + except Exception as e: + print(f"切换灯光可见性失败: {str(e)}") + import traceback + traceback.print_exc() + + 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 + + + # ==================== 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 + + + def serializeNode(self, node): + """序列化节点为字典数据""" + try: + node_data = { + 'name': node.getName(), + 'type': type(node.node()).__name__, + 'pos': (node.getX(), node.getY(), node.getZ()), + 'hpr': (node.getH(), node.getP(), node.getR()), + 'scale': (node.getSx(), node.getSy(), node.getSz()), + 'tags': {}, + 'children': [] + } + + # 保存所有标签 + for tag_key in node.getTagKeys(): + node_data['tags'][tag_key] = node.getTag(tag_key) + + # 特殊处理不同类型的节点 + if hasattr(node.node(), 'getClassType'): + node_class = node.node().getClassType().getName() + node_data['node_class'] = node_class + + # 递归序列化子节点 + for child in node.getChildren(): + # 跳过辅助节点 + if not child.getName().startswith(('gizmo', 'selectionBox', 'grid')): + child_data = self.serializeNode(child) + if child_data: + node_data['children'].append(child_data) + + return node_data + + except Exception as e: + print(f"序列化节点 {node.getName()} 失败: {e}") + import traceback + traceback.print_exc() + return None + + def deserializeNode(self, node_data, parent_node): + """从字典数据反序列化节点""" + try: + # 创建新节点 + node_name = node_data.get('name', 'node') + new_node = parent_node.attachNewNode(node_name) + + # 设置变换 + pos = node_data.get('pos', (0, 0, 0)) + hpr = node_data.get('hpr', (0, 0, 0)) + scale = node_data.get('scale', (1, 1, 1)) + + new_node.setPos(*pos) + new_node.setHpr(*hpr) + new_node.setScale(*scale) + + # 恢复标签 + for tag_key, tag_value in node_data.get('tags', {}).items(): + new_node.setTag(tag_key, tag_value) + + # 根据节点类型进行特殊处理 + node_type = node_data.get('type', '') + node_class = node_data.get('node_class', '') + + # 特殊处理光源节点 + if 'light_type' in node_data.get('tags', {}): + light_type = node_data['tags']['light_type'] + if light_type == 'spot_light': + self._recreateSpotLight(new_node) + elif light_type == 'point_light': + self._recreatePointLight(new_node) + + # 递归创建子节点 + for child_data in node_data.get('children', []): + self.deserializeNode(child_data, new_node) + + return new_node + + except Exception as e: + print(f"反序列化节点 {node_data.get('name', 'unknown')} 失败: {e}") + import traceback + traceback.print_exc() + return None + + def serializeNodeForCopy(self, node): + """序列化节点用于复制操作,完整保存视觉属性""" + try: + if not node or node.isEmpty(): + return None + + node_data = { + 'name': node.getName(), + 'type': type(node.node()).__name__, + 'pos': (node.getX(), node.getY(), node.getZ()), + 'hpr': (node.getH(), node.getP(), node.getR()), + 'scale': (node.getSx(), node.getSy(), node.getSz()), + 'tags': {}, + 'children': [] + } + + # 保存所有标签 + try: + if hasattr(node, 'getTagKeys'): + for tag_key in node.getTagKeys(): + node_data['tags'][tag_key] = node.getTag(tag_key) + except Exception as e: + print(f"获取标签时出错: {e}") + + # 保存视觉属性 + try: + # 保存颜色属性 + if hasattr(node, 'getColor'): + color = node.getColor() + node_data['color'] = (color.getX(), color.getY(), color.getZ(), color.getW()) + + # 保存材质属性 + if hasattr(node, 'getMaterial'): + material = node.getMaterial() + if material: + material_data = {} + material_data['base_color'] = ( + material.getBaseColor().getX(), + material.getBaseColor().getY(), + material.getBaseColor().getZ(), + material.getBaseColor().getW() + ) + material_data['ambient'] = ( + material.getAmbient().getX(), + material.getAmbient().getY(), + material.getAmbient().getZ(), + material.getAmbient().getW() + ) + material_data['diffuse'] = ( + material.getDiffuse().getX(), + material.getDiffuse().getY(), + material.getDiffuse().getZ(), + material.getDiffuse().getW() + ) + material_data['specular'] = ( + material.getSpecular().getX(), + material.getSpecular().getY(), + material.getSpecular().getZ(), + material.getSpecular().getW() + ) + material_data['shininess'] = material.getShininess() + node_data['material'] = material_data + + except Exception as e: + print(f"保存视觉属性时出错: {e}") + + # 根据节点类型保存特定信息 + if node.hasTag("tree_item_type"): + node_type = node.getTag("tree_item_type") + node_data['node_type'] = node_type + + # 保存特定类型节点的额外信息 + if node_type in ["LIGHT_NODE", "SPOT_LIGHT_NODE", "POINT_LIGHT_NODE"]: + # 保存光源特定信息 + rp_light = node.getPythonTag("rp_light_object") + if rp_light: + node_data['light_data'] = { + 'energy': getattr(rp_light, 'energy', 5000), + 'radius': getattr(rp_light, 'radius', 1000), + 'fov': getattr(rp_light, 'fov', 70) if hasattr(rp_light, 'fov') else None, + 'inner_radius': getattr(rp_light, 'inner_radius', 0.4) if hasattr(rp_light, + 'inner_radius') else None, + 'casts_shadows': getattr(rp_light, 'casts_shadows', True) if hasattr(rp_light, + 'casts_shadows') else True, + 'shadow_map_resolution': getattr(rp_light, 'shadow_map_resolution', 256) if hasattr( + rp_light, 'shadow_map_resolution') else 256 + } + elif node_type in ["GUI_BUTTON", "GUI_LABEL", "GUI_ENTRY", "GUI_IMAGE", + "GUI_3D_TEXT", "GUI_3D_IMAGE", "GUI_VIRTUAL_SCREEN"]: + # 保存GUI元素特定信息 + node_data['gui_data'] = self._serializeGUIData(node) + elif node_type == "IMPORTED_MODEL_NODE": + # 保存模型特定信息 + node_data['model_data'] = self._serializeModelData(node) + + return node_data + + except Exception as e: + print(f"序列化节点失败: {e}") + import traceback + traceback.print_exc() + return None + + def _serializeGUIData(self, node): + """序列化GUI元素数据""" + try: + gui_data = {} + + # 保存GUI相关的通用属性 + if node.hasTag("gui_type"): + gui_data['gui_type'] = node.getTag("gui_type") + + # 保存文本内容(如果有的话) + if node.hasTag("text"): + gui_data['text'] = node.getTag("text") + + # 保存其他GUI相关标签 + gui_tags = ['font', 'font_size', 'text_color', 'bg_color', 'size'] + for tag in gui_tags: + if node.hasTag(tag): + gui_data[tag] = node.getTag(tag) + + return gui_data + except Exception as e: + print(f"序列化GUI数据失败: {e}") + return {} + + def _serializeModelTextures(self, node): + """序列化模型纹理信息""" + try: + texture_data = {} + + # 获取节点的所有纹理阶段 + from panda3d.core import TextureStage + texture_stages = node.findAllTextureStages() + + if texture_stages.getNumTextureStages() > 0: + texture_data['textures'] = {} + + # 遍历所有纹理阶段 + for i in range(texture_stages.getNumTextureStages()): + stage = texture_stages.getTextureStage(i) + stage_name = stage.getName() + + # 获取该阶段的纹理 + texture = node.getTexture(stage) + if texture: + # 保存纹理信息 + texture_info = { + 'stage_name': stage_name, + 'stage_mode': stage.getMode(), + 'stage_sort': stage.getSort(), # 保存纹理阶段排序 + 'texture_path': texture.getFullpath().toOsSpecific() if texture.hasFullpath() else '', + 'texture_name': texture.getName(), + 'wrap_u': texture.getWrapU(), + 'wrap_v': texture.getWrapV(), + 'minfilter': texture.getMinfilter(), + 'magfilter': texture.getMagfilter(), + 'anisotropic_degree': texture.getAnisotropicDegree() + } + + # 保存颜色比例和偏移(使用安全的方法) + try: + texture_info['color_scale'] = tuple(node.getTextureScale(stage)) + except: + texture_info['color_scale'] = (1.0, 1.0, 1.0, 1.0) + + try: + texture_info['color_offset'] = tuple(node.getTextureOffset(stage)) + except: + texture_info['color_offset'] = (0.0, 0.0, 0.0, 0.0) + + texture_data['textures'][stage_name] = texture_info + + return texture_data + except Exception as e: + print(f"序列化模型纹理时出错: {e}") + return {} + + def _serializeModelData(self, node): + """序列化模型数据,包括材质和纹理信息""" + try: + model_data = {} + + # 保存模型相关的标签 + model_tags = ['model_path', 'file', 'element_type'] + for tag in model_tags: + if node.hasTag(tag): + model_data[tag] = node.getTag(tag) + + # 保存材质信息 + try: + # 获取模型的材质信息 + if hasattr(node, 'getState'): + state = node.getState() + if state: + # 保存基础颜色信息(使用正确的方法) + from panda3d.core import ColorAttrib + color_attrib = state.getAttrib(ColorAttrib) + if color_attrib and not color_attrib.isOff(): + color = color_attrib.getColor() + model_data['base_color'] = ( + color.getX(), + color.getY(), + color.getZ(), + color.getW() + ) + + # 保存其他材质属性 + from panda3d.core import MaterialAttrib + material_attrib = state.getAttrib(MaterialAttrib.getClassType()) + if material_attrib: + material = material_attrib.getMaterial() + if material: + # 保存基础颜色 + base_color = material.getBaseColor() + model_data['material_base_color'] = ( + base_color.getX(), base_color.getY(), base_color.getZ(), base_color.getW() + ) + + # 保存环境光颜色 + ambient_color = material.getAmbient() + model_data['material_ambient_color'] = ( + ambient_color.getX(), ambient_color.getY(), ambient_color.getZ(), + ambient_color.getW() + ) + + # 保存漫反射颜色 + diffuse_color = material.getDiffuse() + model_data['material_diffuse_color'] = ( + diffuse_color.getX(), diffuse_color.getY(), diffuse_color.getZ(), + diffuse_color.getW() + ) + + # 保存高光颜色 + specular_color = material.getSpecular() + model_data['material_specular_color'] = ( + specular_color.getX(), specular_color.getY(), specular_color.getZ(), + specular_color.getW() + ) + + # 保存粗糙度和金属度等参数 + model_data['material_roughness'] = material.getRoughness() + model_data['material_metallic'] = material.getMetallic() + + # 保存自发光颜色 + emission_color = material.getEmission() + model_data['material_emission_color'] = ( + emission_color.getX(), emission_color.getY(), emission_color.getZ(), + emission_color.getW() + ) + + # 保存光泽度 + model_data['material_shininess'] = material.getShininess() + + # 保存透明度信息 + from panda3d.core import TransparencyAttrib + transparency_attrib = state.getAttrib(TransparencyAttrib.getClassType()) + if transparency_attrib: + model_data['transparency_mode'] = transparency_attrib.get_mode() + + except Exception as e: + print(f"保存材质信息时出错: {e}") + + # 保存纹理信息 + try: + texture_data = self._serializeModelTextures(node) + if texture_data: + model_data['texture_data'] = texture_data + except Exception as e: + print(f"保存纹理信息时出错: {e}") + + return model_data + except Exception as e: + print(f"序列化模型数据失败: {e}") + return {} + + def recreateNodeFromData(self, node_data, parent_node): + """根据数据重建节点,并确保在场景树中显示""" + try: + if not node_data or not parent_node or parent_node.isEmpty(): + return None + + print(f"正在重建节点 {node_data}") + node_type = node_data.get('node_type', '') + original_name = node_data.get('name', 'node') + + # 生成唯一名称 + unique_name = self._generateUniqueName(original_name, parent_node) + + # 根据节点类型调用相应的重建方法 + new_node = None + if node_type in ["LIGHT_NODE", "SPOT_LIGHT_NODE", "POINT_LIGHT_NODE"]: + new_node = self._recreateLightFromData(node_data, parent_node, unique_name) + elif node_type == "CESIUM_TILESET_NODE": + new_node = self._recreateTilesetFromData(node_data, parent_node, unique_name) + elif node_type in ["GUI_BUTTON", "GUI_LABEL", "GUI_ENTRY", "GUI_IMAGE", + "GUI_3DTEXT", "GUI_3DIMAGE", "GUI_VIDEO_SCREEN","GUI_2D_VIDEO_SCREEN"]: + new_node = self._recreateGUIFromData(node_data, parent_node, unique_name) + elif node_type == "IMPORTED_MODEL_NODE": + new_node = self._recreateModelFromData(node_data, parent_node, unique_name) + else: + # 创建普通节点 + new_node = self._createBasicNodeFromData(node_data, parent_node, unique_name) + + # 如果成功创建节点,确保它在场景树中显示 + if new_node: + # 尝试更新场景树以显示新节点 + try: + if hasattr(self.world, 'interface_manager') and self.world.interface_manager: + # 查找父节点在场景树中的对应项 + parent_item = self._findTreeItemForNode(parent_node) + if parent_item: + # 添加新节点到场景树 + tree_widget = self.world.interface_manager.treeWidget + if tree_widget: + tree_widget.add_node_to_tree_widget(new_node, parent_item, node_type or "NODE") + except Exception as e: + print(f"添加节点到场景树时出错: {e}") + + return new_node + + except Exception as e: + print(f"重建节点失败: {e}") + import traceback + traceback.print_exc() + return None + + def _findTreeItemForNode(self, node): + """根据节点查找对应的场景树项""" + try: + if hasattr(self.world, 'interface_manager') and self.world.interface_manager: + tree_widget = self.world.interface_manager.treeWidget + if tree_widget: + # 遍历场景树查找匹配的节点项 + for i in range(tree_widget.topLevelItemCount()): + item = tree_widget.topLevelItem(i) + result = self._findTreeItemForNodeRecursive(item, node) + if result: + return result + return None + except Exception as e: + print(f"查找场景树项时出错: {e}") + return None + + def _findTreeItemForNodeRecursive(self, item, target_node): + """递归查找场景树项""" + try: + # 检查当前项是否匹配 + item_node = getattr(item, 'node_path', None) + if not item_node: + item_node = getattr(item, 'node', None) + + if item_node and item_node == target_node: + return item + + # 递归检查子项 + for i in range(item.childCount()): + child_item = item.child(i) + result = self._findTreeItemForNodeRecursive(child_item, target_node) + if result: + return result + + return None + except Exception as e: + print(f"递归查找场景树项时出错: {e}") + return None + + def _recreateLightFromData(self, node_data, parent_node, name): + """根据数据重建光源""" + try: + light_type = node_data.get('tags', {}).get('light_type', 'spot_light') + + # 创建光源 + if light_type == 'spot_light': + light_node = self.createSpotLight(pos=node_data.get('pos', (0, 0, 0))) + else: # point_light + light_node = self.createPointLight(pos=node_data.get('pos', (0, 0, 0))) + + if light_node: + # 设置名称 + light_node.setName(name) + + # 恢复其他属性 + light_data = node_data.get('light_data', {}) + rp_light = light_node.getPythonTag("rp_light_object") + if rp_light and light_data: + if 'energy' in light_data: + rp_light.energy = light_data['energy'] + if 'radius' in light_data: + rp_light.radius = light_data['radius'] + if 'fov' in light_data and hasattr(rp_light, 'fov'): + rp_light.fov = light_data['fov'] + if 'inner_radius' in light_data and hasattr(rp_light, 'inner_radius'): + rp_light.inner_radius = light_data['inner_radius'] + if 'casts_shadows' in light_data and hasattr(rp_light, 'casts_shadows'): + rp_light.casts_shadows = light_data['casts_shadows'] + if 'shadow_map_resolution' in light_data and hasattr(rp_light, 'shadow_map_resolution'): + rp_light.shadow_map_resolution = light_data['shadow_map_resolution'] + + # 恢复其他标签 + for tag_key, tag_value in node_data.get('tags', {}).items(): + if tag_key not in ['name', 'light_type']: + light_node.setTag(tag_key, str(tag_value)) + + return light_node + + except Exception as e: + print(f"重建光源失败: {e}") + return None + + def _recreateTilesetFromData(self, node_data, parent_node, name): + """根据数据重建Tileset""" + try: + tileset_url = node_data.get('tileset_url', '') + if not tileset_url: + return None + + # 使用现有方法加载tileset + position = node_data.get('pos', (0, 0, 0)) + tileset_node = self.load_cesium_tileset(tileset_url, position) + + if tileset_node: + # 设置名称 + tileset_node.setName(name) + + # 恢复其他标签 + for tag_key, tag_value in node_data.get('tags', {}).items(): + if tag_key not in ['name']: + tileset_node.setTag(tag_key, str(tag_value)) + + return tileset_node + + except Exception as e: + print(f"重建Tileset失败: {e}") + return None + + def _recreateGUIFromData(self, node_data, parent_node, name): + """根据数据重建GUI元素""" + try: + gui_data = node_data.get('gui_data', {}) + #gui_type = gui_data.get('gui_type', '') + gui_type = node_data.get("tags").get("gui_type", "") + + print(f"正在重建GUI元素: {gui_type}") + print(f"正在重建GUI元素: {node_data}") + + # 根据GUI类型调用相应的创建方法 + new_gui_element = None + + if gui_type == "button" and hasattr(self.world, 'createGUIButton'): + pos = node_data.get('pos', (0, 0, 0)) + text = node_data.get('tags').get('gui_text', '') + size = node_data.get('scale', 1) + print(pos,text,size) + new_gui_element = self.world.createGUIButton(pos,text,size) + elif gui_type == "label" and hasattr(self.world, 'createGUILabel'): + pos = node_data.get('pos', (0, 0, 0)) + text = node_data.get('tags').get('gui_text', '') + size = node_data.get('scale', 1) + new_gui_element = self.world.createGUILabel(pos,text,size) + elif gui_type == "entry" and hasattr(self.world, 'createGUIEntry'): + pos = node_data.get('pos', (0, 0, 0)) + text = node_data.get('tags').get('gui_text', '') + size = node_data.get('scale', 1) + new_gui_element = self.world.createGUIEntry(pos,text,size) + elif gui_type == "2d_image" and hasattr(self.world, 'createGUI2DImage'): + pos = node_data.get('pos', (0, 0, 0)) + image_path = node_data.get('tags').get('image_path', '') + size = node_data.get('size', 1) + new_gui_element = self.world.createGUI2DImage(pos, image_path, size) + elif gui_type == "3d_text" and hasattr(self.world, 'createGUI3DText'): + print("正在创建3D文本!!!") + pos = node_data.get('pos', (0, 0, 0)) + text = node_data.get('tags', {}).get('gui_text', '') + scale = node_data.get('scale', 1) + if isinstance(scale, (list, tuple)): + scale = scale[0] if len(scale) > 0 else 1 + print(f"正在创建3D文本: 位置={pos}, 文本={text}, 大小={scale}") + new_gui_element = self.world.createGUI3DText(pos, text, scale) + elif gui_type == "3d_image" and hasattr(self.world, 'createGUI3DImage'): + pos = node_data.get('pos', (0, 0, 0)) + image_path = node_data.get('tags').get('gui_image_path', '') + scale = node_data.get('scale', (1, 1)) + if isinstance(scale, (int, float)): + scale = (scale, scale) + elif isinstance(scale, (list, tuple)) and len(scale) >= 2: + scale = (scale[0], scale[1]) + else: + scale = (1, 1) + print(f"正在创建3D图片: 位置={pos}, 路径={image_path}, 大小={scale}") + new_gui_element = self.world.gui_manager.createGUI3DImage(pos, image_path, scale) + elif gui_type == "video_screen" and hasattr(self.world.gui_manager, 'createVideoScreen'): + pos = node_data.get('pos', (0, 0, 0)) + video_path = node_data.get('tags').get('video_path', '') + scale = node_data.get('scale', (1, 1,1)) + new_gui_element = self.world.gui_manager.createVideoScreen(pos,scale,video_path) + elif gui_type == "2d_video_screen" and hasattr(self.world.gui_manager, 'createGUI2DVideoScreen'): + pos = node_data.get('pos', (0, 0, 0)) + video_path = node_data.get('tags').get('video_path', '') + scale = node_data.get('scale', (1, 1, 1)) + new_gui_element = self.world.gui_manager.createGUI2DVideoScreen(pos,scale,video_path) + + if new_gui_element: + # 设置名称和变换 + if hasattr(new_gui_element, 'setName'): + new_gui_element.setName(name) + + # 设置位置、旋转、缩放 + pos = node_data.get('pos', (0, 0, 0)) + hpr = node_data.get('hpr', (0, 0, 0)) + scale = node_data.get('scale', (1, 1, 1)) + + if hasattr(new_gui_element, 'setPos'): + new_gui_element.setPos(*pos) + if hasattr(new_gui_element, 'setHpr'): + new_gui_element.setHpr(*hpr) + if hasattr(new_gui_element, 'setScale'): + new_gui_element.setScale(*scale) + + # 恢复文本内容 + if 'text' in gui_data and hasattr(new_gui_element, 'setText'): + new_gui_element.setText(gui_data['text']) + + # 恢复其他标签 + for tag_key, tag_value in node_data.get('tags', {}).items(): + if hasattr(new_gui_element, 'setTag') and tag_key not in ['name']: + new_gui_element.setTag(tag_key, str(tag_value)) + + print(f"GUI元素重建成功: {name}") + + return new_gui_element + + except Exception as e: + print(f"重建GUI元素失败: {e}") + import traceback + traceback.print_exc() + return None + + def _recreateModelFromData(self, node_data, parent_node, name): + """根据数据重建模型,保持材质""" + try: + model_data = node_data.get('model_data', {}) + model_path = model_data.get('model_path', model_data.get('file', '')) + + if not model_path or not os.path.exists(model_path): + # 如果原始模型文件不存在,创建一个基本节点 + return self._createBasicNodeFromData(node_data, parent_node, name) + + # 导入模型,保持原有参数 + model = self.importModel( + model_path, + apply_unit_conversion=False, # 已经处理过的模型不需要再次转换 + normalize_scales=False, # 保持原有缩放 + auto_convert_to_glb=False # 已经处理过的模型不需要再次转换 + ) + + if model: + # 设置名称 + model.setName(name) + + # 设置变换 + pos = node_data.get('pos', (0, 0, 0)) + hpr = node_data.get('hpr', (0, 0, 0)) + scale = node_data.get('scale', (1, 1, 1)) + + model.setPos(*pos) + model.setHpr(*hpr) + model.setScale(*scale) + + # 恢复材质和纹理信息 + try: + self._restoreModelMaterial(model, model_data) + except Exception as e: + print(f"恢复模型材质时出错: {e}") + + # 恢复标签 + for tag_key, tag_value in node_data.get('tags', {}).items(): + if tag_key not in ['name']: + model.setTag(tag_key, str(tag_value)) + + # 添加到模型列表 + if model not in self.models: + self.models.append(model) + + return model + + except Exception as e: + print(f"重建模型失败: {e}") + # 出错时创建基本节点 + return self._createBasicNodeFromData(node_data, parent_node, name) + + def _restoreModelTextures(self, model, texture_data): + """恢复模型纹理""" + try: + if not texture_data or 'textures' not in texture_data: + return + + from panda3d.core import TextureStage, SamplerState + + textures_info = texture_data['textures'] + + # 为每个纹理阶段恢复纹理 + for stage_name, texture_info in textures_info.items(): + # 创建纹理阶段 + stage = TextureStage(stage_name) + stage.setMode(texture_info.get('stage_mode', TextureStage.M_modulate)) + # 恢复纹理阶段排序 + stage.setSort(texture_info.get('stage_sort', 0)) # 默认为0(p3d_Texture0) + + # 加载纹理 + texture_path = texture_info['texture_path'] + if texture_path and os.path.exists(texture_path): + texture = self.world.loader.loadTexture(texture_path) + if texture: + # 设置纹理属性 + texture.setWrapU(texture_info.get('wrap_u', SamplerState.WM_repeat)) + texture.setWrapV(texture_info.get('wrap_v', SamplerState.WM_repeat)) + texture.setMinfilter(texture_info.get('minfilter', SamplerState.FT_linear)) + texture.setMagfilter(texture_info.get('magfilter', SamplerState.FT_linear)) + texture.setAnisotropicDegree(texture_info.get('anisotropic_degree', 1)) + + # 应用纹理到模型 + model.setTexture(stage, texture, 1) # 1 表示强制应用 + + # 恢复颜色比例和偏移(使用安全的方法) + if 'color_scale' in texture_info: + try: + model.setTextureScale(stage, *texture_info['color_scale']) + except Exception as e: + print(f"恢复纹理比例失败: {e}") + + if 'color_offset' in texture_info: + try: + model.setTextureOffset(stage, *texture_info['color_offset']) + except Exception as e: + print(f"恢复纹理偏移失败: {e}") + + print(f"恢复纹理: {stage_name} <- {texture_path}") + else: + print(f"纹理文件不存在或路径为空: {texture_path}") + + except Exception as e: + print(f"恢复模型纹理时出错: {e}") + + def _restoreModelMaterial(self, model, model_data): + """恢复模型材质和纹理""" + try: + # 恢复基础颜色 + if 'base_color' in model_data: + from panda3d.core import ColorAttrib + base_color = model_data['base_color'] + color = (base_color[0], base_color[1], base_color[2], base_color[3]) + model.setColor(color) + + # 恢复复杂材质属性 + if any(key.startswith('material_') for key in model_data.keys()): + from panda3d.core import Material + + # 创建新材质或获取现有材质 + material = Material() + + # 恢复基础颜色 + if 'material_base_color' in model_data: + base_color = model_data['material_base_color'] + material.setBaseColor((base_color[0], base_color[1], base_color[2], base_color[3])) + + # 恢复环境光颜色 + if 'material_ambient_color' in model_data: + ambient_color = model_data['material_ambient_color'] + material.setAmbient((ambient_color[0], ambient_color[1], ambient_color[2], ambient_color[3])) + + # 恢复漫反射颜色 + if 'material_diffuse_color' in model_data: + diffuse_color = model_data['material_diffuse_color'] + material.setDiffuse((diffuse_color[0], diffuse_color[1], diffuse_color[2], diffuse_color[3])) + + # 恢复高光颜色 + if 'material_specular_color' in model_data: + specular_color = model_data['material_specular_color'] + material.setSpecular((specular_color[0], specular_color[1], specular_color[2], specular_color[3])) + + # 恢复自发光颜色 + if 'material_emission_color' in model_data: + emission_color = model_data['material_emission_color'] + material.setEmission((emission_color[0], emission_color[1], emission_color[2], emission_color[3])) + + # 恢复粗糙度和金属度 + if 'material_roughness' in model_data: + material.setRoughness(model_data['material_roughness']) + + if 'material_metallic' in model_data: + material.setMetallic(model_data['material_metallic']) + + # 恢复光泽度 + if 'material_shininess' in model_data: + material.setShininess(model_data['material_shininess']) + + # 应用材质到模型 + model.setMaterial(material) + + # 恢复透明度设置 + if 'transparency_mode' in model_data: + from panda3d.core import TransparencyAttrib + transparency_mode = model_data['transparency_mode'] + model.setTransparency(transparency_mode) + + # 恢复纹理信息 + if 'texture_data' in model_data: + self._restoreModelTextures(model, model_data['texture_data']) + + except Exception as e: + print(f"恢复材质失败: {e}") + + def _createBasicNodeFromData(self, node_data, parent_node, name): + """创建基本节点,保持视觉属性""" + try: + new_node = parent_node.attachNewNode(name) + + # 设置变换 + pos = node_data.get('pos', (0, 0, 0)) + hpr = node_data.get('hpr', (0, 0, 0)) + scale = node_data.get('scale', (1, 1, 1)) + + new_node.setPos(*pos) + new_node.setHpr(*hpr) + new_node.setScale(*scale) + + # 恢复视觉属性 + try: + # 恢复颜色 + if 'color' in node_data: + color_data = node_data['color'] + new_node.setColor(color_data[0], color_data[1], color_data[2], color_data[3]) + + # 恢复材质 + if 'material' in node_data: + from panda3d.core import Material + material_data = node_data['material'] + material = Material() + + if 'base_color' in material_data: + bc = material_data['base_color'] + material.setBaseColor((bc[0], bc[1], bc[2], bc[3])) + + if 'ambient' in material_data: + ac = material_data['ambient'] + material.setAmbient((ac[0], ac[1], ac[2], ac[3])) + + if 'diffuse' in material_data: + dc = material_data['diffuse'] + material.setDiffuse((dc[0], dc[1], dc[2], dc[3])) + + if 'specular' in material_data: + sc = material_data['specular'] + material.setSpecular((sc[0], sc[1], sc[2], sc[3])) + + if 'shininess' in material_data: + material.setShininess(material_data['shininess']) + + new_node.setMaterial(material) + + except Exception as e: + print(f"恢复视觉属性时出错: {e}") + + # 恢复标签 + for tag_key, tag_value in node_data.get('tags', {}).items(): + if tag_key not in ['name']: + new_node.setTag(tag_key, str(tag_value)) + + return new_node + + except Exception as e: + print(f"创建基本节点失败: {e}") + return None + + def _generateUniqueName(self, base_name, parent_node): + """生成唯一节点名称""" + try: + # 移除可能的数字后缀 + import re + import time + name_base = re.sub(r'_\d+$', '', base_name) + + # 查找现有同名节点 + counter = 1 + unique_name = base_name + while True: + # 检查父节点下是否已存在同名子节点 + existing_node = parent_node.find(unique_name) + if existing_node.isEmpty(): + break + unique_name = f"{name_base}_{counter}" + counter += 1 + if counter > 1000: # 防止无限循环 + unique_name = f"{name_base}_{int(time.time())}" + break + + return unique_name + except: + return f"{base_name}_{int(time.time())}" + + diff --git a/ui/property_panel.py b/ui/property_panel.py index 5890cd6c..e7c64529 100644 --- a/ui/property_panel.py +++ b/ui/property_panel.py @@ -9169,12 +9169,75 @@ class PropertyPanelManager: def _getActor(self, origin_model): if origin_model in self._actor_cache: return self._actor_cache[origin_model] + + # 首先检查是否可以直接从内存中的模型创建Actor + if origin_model.hasTag("can_create_actor_from_memory") and origin_model.getTag("can_create_actor_from_memory").lower() == "true": + try: + print(f"[Actor加载] 直接从内存模型创建Actor: {origin_model.getName()}") + # 直接从内存中的模型创建Actor + test_actor = Actor(origin_model) + anims = test_actor.getAnimNames() + test_actor.reparentTo(self.world.render) + self._actor_cache[origin_model] = test_actor + print(f"[Actor加载] 内存创建检测到动画: {anims}") + if anims: + return test_actor + else: + test_actor.cleanup() + test_actor.removeNode() + except Exception as e: + print(f"从内存模型创建Actor失败: {e}") + + # 如果不能直接从内存创建,再尝试通过文件路径加载 filepath = origin_model.getTag("model_path") if not filepath: return None print(f"[Actor加载] 尝试加载: {filepath}") + # 处理跨平台路径问题 + import os + # 检查路径是否有效,如果无效则尝试修复 + if not os.path.exists(filepath): + original_filepath = filepath + # 尝试多种修复策略 + fixed = False + + # 策略1: 处理Linux风格路径在Windows上的问题 + if filepath.startswith('/') and ':' not in filepath: + # 提取文件名并尝试在当前目录查找 + filename = os.path.basename(filepath) + potential_path = os.path.join(os.getcwd(), filename) + if os.path.exists(potential_path): + filepath = potential_path + fixed = True + + # 策略2: 处理路径分隔符问题 + if not fixed: + # 尝试规范化路径 + normalized_path = os.path.normpath(filepath) + if os.path.exists(normalized_path): + filepath = normalized_path + fixed = True + + # 策略3: 在Resources目录中查找 + if not fixed: + # 尝试在Resources目录中查找文件 + resources_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "Resources") + filename = os.path.basename(filepath) + potential_path = os.path.join(resources_path, filename) + if os.path.exists(potential_path): + filepath = potential_path + fixed = True + + if fixed: + print(f"路径修复: {original_filepath} -> {filepath}") + # 更新模型标签 + origin_model.setTag("model_path", filepath) + else: + print(f"[警告] 模型文件不存在: {filepath}") + return None + # 检查是否是 FBX 文件,如果是,使用专门的 FBX 动画加载器 if filepath.lower().endswith('.fbx'): pass