#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 场景管理器 - 负责场景和模型管理的核心功能 处理模型导入、场景树构建、材质系统、碰撞设置等 """ import os from panda3d.core import ( ModelPool, ModelRoot, Filename, NodePath, GeomNode, Material, Vec4, Vec3, MaterialAttrib, ColorAttrib, Point3, CollisionNode, CollisionSphere, BitMask32, TransparencyAttrib,LColor ) from panda3d.egg import EggData, EggVertexPool from direct.actor.Actor import Actor from QPanda3D.Panda3DWorld import get_render_pipeline from scene import util class SceneManager: """场景管理器 - 统一管理场景中的所有元素""" def __init__(self, world): """初始化场景管理器 Args: world: 主程序world对象引用 """ self.world = world self.models = [] # 模型列表 self.Spotlight = [] self.Pointlight = [] print("✓ 场景管理系统初始化完成") # ==================== 模型导入和处理 ==================== def importModel(self, filepath, apply_unit_conversion=False, normalize_scales=True, auto_convert_to_glb=True): """导入模型到场景 Args: filepath: 模型文件路径 apply_unit_conversion: 是否应用单位转换(主要针对FBX文件) normalize_scales: 是否标准化子节点缩放(推荐开启) auto_convert_to_glb: 是否自动将非GLB格式转换为GLB以获得更好的动画支持 """ try: print(f"\n=== 开始导入模型: {filepath} ===") print(f"单位转换: {'开启' if apply_unit_conversion else '关闭'}") print(f"自动转换GLB: {'开启' if auto_convert_to_glb else '关闭'}") filepath = util.normalize_model_path(filepath) original_filepath = filepath # 检查是否需要转换为GLB以获得更好的动画支持 if auto_convert_to_glb and self._shouldConvertToGLB(filepath): print(f"🔄 检测到需要转换的格式,尝试转换为GLB...") converted_path = self._convertToGLBWithProgress(filepath) if converted_path: print(f"✅ 转换成功: {converted_path}") filepath = converted_path # 显示成功消息 try: from PyQt5.QtWidgets import QMessageBox original_ext = os.path.splitext(original_filepath)[1].upper() QMessageBox.information(None, "转换成功", f"已将 {original_ext} 格式自动转换为 GLB 格式\n以获得更好的动画支持!") except: pass else: print(f"⚠️ 转换失败,使用原始文件") # 总是重新加载模型以确保材质信息完整 # 不使用ModelPool缓存,避免材质信息丢失问题 print("直接从文件加载模型...") model = self.world.loader.loadModel(filepath) if not model: print("加载模型失败") return None # 设置模型名称 model_name = os.path.basename(filepath) model.setName(model_name) # 将模型添加到场景 model.reparentTo(self.world.render) # 保存原始路径和转换后的路径 model.setTag("model_path", filepath) model.setTag("original_path", original_filepath) if filepath != original_filepath: model.setTag("converted_from", os.path.splitext(original_filepath)[1]) model.setTag("converted_to_glb", "true") # 可选的单位转换(主要针对FBX) if apply_unit_conversion and filepath.lower().endswith('.fbx'): print("应用FBX单位转换(厘米到米)...") self._applyUnitConversion(model, 0.01) # 智能缩放标准化(处理FBX子节点的大缩放值) if normalize_scales and filepath.lower().endswith('.fbx'): print("标准化FBX模型缩放层级...") self._normalizeModelScales(model) # 调整模型位置到地面 self._adjustModelToGround(model) # 创建并设置基础材质 print("\n=== 开始设置材质 ===") self._applyMaterialsToModel(model) # 设置碰撞检测(重要!用于选择功能) print("\n=== 设置碰撞检测 ===") self.setupCollision(model) # 添加文件标签用于保存/加载 model.setTag("file", model_name) model.setTag("is_model_root", "1") # 记录应用的处理选项 if apply_unit_conversion: model.setTag("unit_conversion_applied", "true") if normalize_scales: model.setTag("scale_normalization_applied", "true") # 添加到模型列表 self.models.append(model) # 更新场景树 self.updateSceneTree() print(f"=== 模型导入成功: {model_name} ===\n") return model except Exception as e: print(f"导入模型失败: {str(e)}") return None # def importAnimatedFBX(self, filepath, scale=0.01, auto_play=True): # """导入带动画的FBX模型(使用assimp) # # Args: # filepath: FBX文件路径 # scale: 缩放比例(默认0.01,从厘米转换到米) # auto_play: 是否自动播放第一个动画 # # Returns: # Actor对象,如果加载失败返回None # """ # try: # print(f"\n=== 导入动画FBX模型: {filepath} ===") # filepath = util.normalize_model_path(filepath) # # # 使用动画管理器加载FBX # actor = self.animation_manager.load_fbx_with_animations(filepath, scale) # # if actor: # # 设置模型名称 # model_name = os.path.basename(filepath) # actor.setName(model_name) # # # 调整模型位置到地面 # self._adjustModelToGround(actor) # # # 设置碰撞检测 # self.setupCollision(actor) # # # 添加文件标签 # actor.setTag("file", model_name) # actor.setTag("is_animated_model", "1") # # # 添加到模型列表 # self.models.append(actor) # # # 自动播放第一个动画 # if auto_play: # available_anims = self.animation_manager.get_available_animations(actor) # if available_anims: # first_anim = available_anims[0] # self.animation_manager.play_animation(actor, first_anim, loop=True) # print(f"🎬 自动播放动画: {first_anim}") # # # 更新场景树 # self.updateSceneTree() # # print(f"=== 动画FBX模型导入成功: {model_name} ===\n") # return actor # else: # print("❌ 动画FBX模型导入失败") # return None # # except Exception as e: # print(f"导入动画FBX模型失败: {str(e)}") # import traceback # traceback.print_exc() # return None def _applyMaterialsToModel(self, model): """递归应用材质到模型的所有GeomNode""" def apply_material(node_path, depth=0): indent = " " * depth print(f"{indent}处理节点: {node_path.getName()}") print(f"{indent}节点类型: {node_path.node().__class__.__name__}") if isinstance(node_path.node(), GeomNode): print(f"{indent}发现GeomNode,处理材质") geom_node = node_path.node() # 检查所有几何体的状态 has_color = False color = None # 首先检查节点自身的状态 node_state = node_path.getState() if node_state.hasAttrib(MaterialAttrib.getClassType()): mat_attrib = node_state.getAttrib(MaterialAttrib.getClassType()) node_material = mat_attrib.getMaterial() if node_material and node_material.hasDiffuse(): color = node_material.getDiffuse() has_color = True print(f"{indent}从节点材质获取颜色: {color}") # 检查FBX特有的属性 for tag_key in node_path.getTagKeys(): print(f"{indent}发现标签: {tag_key}") if "color" in tag_key.lower() or "diffuse" in tag_key.lower(): tag_value = node_path.getTag(tag_key) print(f"{indent}颜色相关标签: {tag_key} = {tag_value}") # 如果还没找到颜色,检查几何体 if not has_color: for i in range(geom_node.getNumGeoms()): geom = geom_node.getGeom(i) state = geom_node.getGeomState(i) # 检查顶点颜色 vdata = geom.getVertexData() format = vdata.getFormat() for j in range(format.getNumColumns()): column = format.getColumn(j) # InternalName对象需要使用getName()转换为字符串 column_name = column.getName().getName() if "color" in column_name.lower(): print(f"{indent}发现顶点颜色数据: {column_name}") # 这里可以读取顶点颜色,但先记录发现 # 检查材质属性 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 # 创建新材质 material = Material() if has_color: print(f"{indent}应用找到的颜色: {color}") material.setDiffuse(color) material.setBaseColor(color) # 同时设置基础颜色 node_path.setColor(color) else: print(f"{indent}使用默认颜色") material.setDiffuse((0.8, 0.8, 0.8, 1.0)) # 设置其他材质属性 material.setAmbient((0.2, 0.2, 0.2, 1.0)) material.setSpecular((0.5, 0.5, 0.5, 1.0)) material.setShininess(32.0) #material.set_metallic(1) #material.set_roughness(0) # 应用材质 node_path.setMaterial(material) print(f"{indent}几何体数量: {geom_node.getNumGeoms()}") # 递归处理子节点 child_count = node_path.getNumChildren() print(f"{indent}子节点数量: {child_count}") for i in range(child_count): child = node_path.getChild(i) apply_material(child, depth + 1) # 应用材质 print("\n开始递归应用材质...") apply_material(model) print("=== 材质设置完成 ===\n") def _adjustModelToGround(self, model): """智能调整模型到地面,但保持原有缩放结构""" try: print("调整模型位置到地面...") # 获取模型的边界框 bounds = model.getBounds() if not bounds or bounds.isEmpty(): print("无法获取模型边界,使用默认位置") model.setPos(0, 0, 0) return # 获取边界框的最低点 min_point = bounds.getMin() center = bounds.getCenter() # 计算需要移动的距离,使模型底部贴合地面(Z=0) # 这里不涉及缩放,只是简单的位置调整 ground_offset = -min_point.getZ() # 设置模型位置:X,Y居中,Z调整到地面 model.setPos(0, 0, ground_offset) print(f"模型边界: 最小点{min_point}, 中心{center}") print(f"地面偏移: {ground_offset}") print(f"最终位置: {model.getPos()}") except Exception as e: print(f"调整模型位置失败: {str(e)}") # 失败时使用默认位置 model.setPos(0, 0, 0) def _applyUnitConversion(self, model, scale_factor): """应用单位转换缩放 Args: model: 要转换的模型 scale_factor: 缩放因子(如0.01表示从厘米转换到米) """ try: print(f"应用单位转换缩放: {scale_factor}") # 检查模型是否已经应用过单位转换 if model.hasTag("unit_conversion_applied"): print("模型已应用过单位转换,跳过") return # 获取当前边界用于后续位置调整 original_bounds = model.getBounds() # 应用缩放 model.setScale(scale_factor) # 重新调整位置(因为缩放会影响边界) if original_bounds and not original_bounds.isEmpty(): new_bounds = model.getBounds() min_point = new_bounds.getMin() ground_offset = -min_point.getZ() model.setZ(ground_offset) print(f"缩放后重新调整位置: Z偏移 = {ground_offset}") print(f"单位转换完成,缩放因子: {scale_factor}") except Exception as e: print(f"应用单位转换失败: {str(e)}") def _normalizeModelScales(self, model): """智能标准化模型缩放层级 检测并修复FBX模型中子节点的大缩放值问题 """ try: print("开始分析模型缩放结构...") # 收集所有节点的缩放信息 scale_info = [] self._collectScaleInfo(model, scale_info) if not scale_info: print("没有找到需要处理的缩放信息") return # 分析缩放模式 large_scales = [info for info in scale_info if max(abs(info['scale'].x), abs(info['scale'].y), abs(info['scale'].z)) > 10] if not large_scales: print("没有发现大缩放值,无需标准化") return print(f"发现 {len(large_scales)} 个节点有大缩放值") # 计算标准化因子(基于最常见的大缩放值) common_large_scale = self._findCommonLargeScale(large_scales) if common_large_scale: normalize_factor = 1.0 / common_large_scale print(f"检测到常见大缩放值: {common_large_scale}, 标准化因子: {normalize_factor}") # 应用标准化 self._applyScaleNormalization(model, normalize_factor) print("✓ 缩放标准化完成") else: print("无法确定合适的标准化因子,跳过标准化") except Exception as e: print(f"缩放标准化失败: {str(e)}") def _collectScaleInfo(self, node, scale_info, depth=0): """递归收集节点缩放信息""" try: scale = node.getScale() scale_info.append({ 'node': node, 'name': node.getName(), 'scale': scale, 'depth': depth }) # 递归处理子节点 for i in range(node.getNumChildren()): child = node.getChild(i) self._collectScaleInfo(child, scale_info, depth + 1) except Exception as e: print(f"收集缩放信息失败 ({node.getName()}): {str(e)}") def _findCommonLargeScale(self, large_scales): """找到最常见的大缩放值""" try: # 提取缩放值(取绝对值的最大分量) scale_values = [] for info in large_scales: scale = info['scale'] max_scale = max(abs(scale.x), abs(scale.y), abs(scale.z)) scale_values.append(round(max_scale)) # 四舍五入到整数 if not scale_values: return None # 找到最常见的值 from collections import Counter counter = Counter(scale_values) most_common = counter.most_common(1)[0] print(f"缩放值统计: {dict(counter)}") print(f"最常见的大缩放值: {most_common[0]} (出现{most_common[1]}次)") # 只有当最常见的值确实很大时才返回 if most_common[0] >= 10: return float(most_common[0]) return None except Exception as e: print(f"分析常见缩放值失败: {str(e)}") return None def _applyScaleNormalization(self, node, normalize_factor, depth=0): """递归应用缩放标准化,同时调整位置以保持视觉一致性""" try: indent = " " * depth current_scale = node.getScale() current_pos = node.getPos() # 检查是否需要标准化(只处理明显的大缩放) max_scale_component = max(abs(current_scale.x), abs(current_scale.y), abs(current_scale.z)) if max_scale_component > 10: # 只标准化明显的大缩放 # 应用新的缩放 new_scale = current_scale * normalize_factor node.setScale(new_scale) # 同时调整位置:当缩放变小时,位置也应该相应变小以保持视觉相对位置 # 这确保了子节点之间的相对距离在视觉上保持一致 new_pos = current_pos * normalize_factor node.setPos(new_pos) print(f"{indent}标准化 {node.getName()}:") print(f"{indent} 缩放: {current_scale} -> {new_scale}") print(f"{indent} 位置: {current_pos} -> {new_pos}") # 递归处理子节点 for i in range(node.getNumChildren()): child = node.getChild(i) self._applyScaleNormalization(child, normalize_factor, depth + 1) except Exception as e: print(f"应用缩放标准化失败 ({node.getName()}): {str(e)}") def importModelAsync(self, filepath): """异步导入模型""" try: # 创建异步加载请求 request = self.world.loader.makeAsyncRequest(filepath) # 添加完成回调 def modelLoaded(task): if task.isReady(): model = task.result() if model: # 处理加载完成的模型 self.processLoadedModel(model) return task.done() request.done_event = modelLoaded # 开始异步加载 self.world.loader.loadAsync(request) except Exception as e: print(f"异步加载模型失败: {str(e)}") def loadAnimatedModel(self, model_path, anims=None, auto_play=True): """加载带动画的模型 Args: model_path: 模型文件路径 anims: 动画字典,格式为 {"动画名": "动画文件路径"} auto_play: 是否自动播放第一个动画 """ try: print(f"🎬 加载动画模型: {model_path}") # 如果没有指定动画,尝试自动检测 if anims is None: anims = self._detectAnimations(model_path) # 创建Actor对象 actor = Actor(model_path, anims) if actor: actor.reparentTo(self.world.render) # 设置碰撞检测 self.setupCollision(actor) # 获取可用的动画列表 available_anims = actor.getAnimNames() print(f"📋 检测到动画: {available_anims}") # 自动播放第一个动画 if auto_play and available_anims: first_anim = available_anims[0] actor.loop(first_anim) print(f"▶️ 自动播放动画: {first_anim}") # 添加动画控制标签 actor.setTag("animated", "true") actor.setTag("current_anim", first_anim) actor.setTag("available_anims", str(available_anims)) # 调整模型位置(让它站在地面上) self._adjustModelPosition(actor) self.models.append(actor) # 更新场景树 self.updateSceneTree() print(f"✅ 动画模型加载完成: {actor.getName()}") return actor except Exception as e: print(f"❌ 加载动画模型失败: {str(e)}") import traceback traceback.print_exc() return None # ==================== 材质和几何体处理 ==================== 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): """为模型设置碰撞检测""" # 创建碰撞节点 cNode = CollisionNode(f'modelCollision_{model.getName()}') # 设置碰撞掩码 cNode.setIntoCollideMask(BitMask32.bit(2)) # 使用第2位作为模型的碰撞掩码 # 获取模型的边界 bounds = model.getBounds() center = bounds.getCenter() radius = bounds.getRadius() # 添加碰撞球体 cSphere = CollisionSphere(center, radius) cNode.addSolid(cSphere) # 将碰撞节点附加到模型上 cNodePath = model.attachNewNode(cNode) #cNodePath.hide() # cNodePath.show() # 取消注释可以显示碰撞体,用于调试 # ==================== 场景树管理 ==================== def updateSceneTree(self): """更新场景树显示 - 代理到interface_manager""" if hasattr(self.world, 'interface_manager'): return self.world.interface_manager.updateSceneTree() else: print("界面管理器未初始化,无法更新场景树") # ==================== 场景保存和加载 ==================== def saveScene(self, filename): """保存场景到BAM文件""" try: print(f"\n=== 开始保存场景到: {filename} ===") # 遍历所有模型,保存材质状态和变换信息 for model in self.models: # 保存变换信息(关键!) model.setTag("transform_pos", str(model.getPos())) model.setTag("transform_hpr", str(model.getHpr())) model.setTag("transform_scale", str(model.getScale())) print(f"保存模型 {model.getName()} 的变换信息:") print(f" 位置: {model.getPos()}") print(f" 旋转: {model.getHpr()}") print(f" 缩放: {model.getScale()}") # 获取当前状态 state = model.getState() # 如果有材质属性,保存为标签 if state.hasAttrib(MaterialAttrib.getClassType()): mat_attrib = state.getAttrib(MaterialAttrib.getClassType()) material = mat_attrib.getMaterial() if material: # 保存材质属性到标签 model.setTag("material_ambient", str(material.getAmbient())) model.setTag("material_diffuse", str(material.getDiffuse())) model.setTag("material_specular", str(material.getSpecular())) model.setTag("material_emission", str(material.getEmission())) model.setTag("material_shininess", str(material.getShininess())) if material.hasBaseColor(): model.setTag("material_basecolor", str(material.getBaseColor())) # 如果有颜色属性,保存为标签 if state.hasAttrib(ColorAttrib.getClassType()): color_attrib = state.getAttrib(ColorAttrib.getClassType()) if not color_attrib.isOff(): model.setTag("color", str(color_attrib.getColor())) # 保存场景 success = self.world.render.writeBamFile(filename) return success except Exception as e: print(f"保存场景时发生错误: {str(e)}") return False def loadScene(self, filename): """从BAM文件加载场景""" try: print(f"\n=== 开始加载场景: {filename} ===") # 清除当前场景 print("\n清除当前场景...") for model in self.models: model.removeNode() self.models.clear() # 加载场景 scene = self.world.loader.loadModel(filename) if not scene: return False # 遍历场景中的所有模型节点 def processNode(nodePath, depth=0): indent = " " * depth print(f"{indent}处理节点: {nodePath.getName()}") # 跳过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 isinstance(nodePath.node(), ModelRoot): print(f"{indent}找到模型根节点!") # 清除现有材质状态 nodePath.clearMaterial() nodePath.clearColor() # 创建新材质 material = Material() # 从标签恢复材质属性 def parseColor(color_str): """解析颜色字符串为Vec4""" try: # 移除LVecBase4f标记,只保留数值 color_str = color_str.replace('LVecBase4f', '').strip('()') r, g, b, a = map(float, color_str.split(',')) return Vec4(r, g, b, a) except: return Vec4(1, 1, 1, 1) # 默认白色 if nodePath.hasTag("material_ambient"): material.setAmbient(parseColor(nodePath.getTag("material_ambient"))) if nodePath.hasTag("material_diffuse"): material.setDiffuse(parseColor(nodePath.getTag("material_diffuse"))) if nodePath.hasTag("material_specular"): material.setSpecular(parseColor(nodePath.getTag("material_specular"))) if nodePath.hasTag("material_emission"): material.setEmission(parseColor(nodePath.getTag("material_emission"))) if nodePath.hasTag("material_shininess"): material.setShininess(float(nodePath.getTag("material_shininess"))) if nodePath.hasTag("material_basecolor"): material.setBaseColor(parseColor(nodePath.getTag("material_basecolor"))) # 应用材质 nodePath.setMaterial(material) # 恢复颜色属性 if nodePath.hasTag("color"): nodePath.setColor(parseColor(nodePath.getTag("color"))) # 恢复变换信息(关键!) def parseVec3(vec_str): """解析向量字符串为Vec3""" try: # 移除LVecBase3f标记,只保留数值 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}") # 将模型重新挂载到render下 nodePath.wrtReparentTo(self.world.render) # 为加载的模型设置碰撞检测 self.setupCollision(nodePath) self.models.append(nodePath) # 递归处理子节点 for child in nodePath.getChildren(): processNode(child, depth + 1) print("\n开始处理场景节点...") processNode(scene) # 移除临时场景节点 scene.removeNode() # 更新场景树 self.updateSceneTree() print("=== 场景加载完成 ===\n") return True except Exception as e: print(f"加载场景时发生错误: {str(e)}") return False # ==================== 模型管理 ==================== def deleteModel(self, model): """删除模型""" try: if model in self.models: 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): """清除所有模型""" 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)): from RenderPipelineFile.rpcore import SpotLight, RenderPipeline from panda3d.core import Vec3,NodePath render_pipeline = get_render_pipeline() # 创建一个挂载节点(你控制的) light_np = NodePath("SpotlightAttachNode") light_np.reparentTo(self.world.render) #light_np.setPos(*pos) self.half_energy = 5000 self.lamp_fov = 70 self.lamp_radius = 1000 light = SpotLight() light.direction = Vec3(0, 0, -1) # 光照方向 light.fov = self.lamp_fov # 光源角度(类似手电筒) light.set_color_from_temperature(5 * 1000.0) # 色温(K) light.energy = self.half_energy # 光照强度 light.radius = self.lamp_radius # 影响范围 light.casts_shadows = True # 是否投射阴影 light.shadow_map_resolution = 256 # 阴影分辨率 light.setPos(*pos) #light_np.setPos(*pos) #light_np = render_pipeline.add_light(light, parent=self.world.render) render_pipeline.add_light(light) # 添加到渲染管线 light_name = f"Spotlight_{len(self.Spotlight)}" light_np.setName(light_name) # 设置唯一名称 #light_np.reparentTo(self.world.render) # 挂载到场景根节点 light_np.setTag("light_type", "spot_light") light_np.setTag("is_scene_element", "1") light_np.setTag("light_energy", str(light.energy)) light_np.setPythonTag("rp_light_object", light) self.Spotlight.append(light_np) if hasattr(self.world, 'updateSceneTree'): self.world.updateSceneTree() #print("nikan"+light_np.getHpr()) def createPointLight(self, pos=(0, 0, 0)): from RenderPipelineFile.rpcore import PointLight, RenderPipeline from panda3d.core import Vec3, NodePath render_pipeline = get_render_pipeline() # 创建一个挂载节点(你控制的) light_np = NodePath("PointlightAttachNode") light_np.reparentTo(self.world.render) light = PointLight() light.setPos(*pos) light_np.setPos(*pos) light.energy = 5000 light.radius = 1000 light.inner_radius = 0.4 light.set_color_from_temperature(5 * 1000.0) # 色温(K) light.casts_shadows = True # 是否投射阴影 light.shadow_map_resolution = 256 # 阴影分辨率 render_pipeline.add_light(light) # 添加到渲染管线 light_name = f"Pointlight{len(self.Pointlight)}" light_np.setName(light_name) # 设置唯一名称 #light_np = NodePath(f"PointLight_{len(self.Pointlight)}") #light_np.reparentTo(self.world.render) #light_np.setPos(*pos) light_np.setTag("light_type", "point_light") light_np.setTag("is_scene_element", "1") light_np.setTag("light_energy", str(light.energy)) # 保存光源对象引用(重要!用于属性面板) light_np.setPythonTag("rp_light_object", light) self.Pointlight.append(light_np) if hasattr(self.world, 'updateSceneTree'): self.world.updateSceneTree() return light,light_np # ==================== GLB 转换方法 ==================== def _shouldConvertToGLB(self, filepath): """判断是否应该转换为GLB格式""" ext = os.path.splitext(filepath)[1].lower() # 需要转换的格式:FBX, OBJ, DAE等(但不转换已经是GLB/GLTF的) convert_formats = ['.fbx', '.obj', '.dae', '.3ds', '.blend'] return ext in convert_formats def _convertToGLBWithProgress(self, filepath): """带进度显示的GLB转换""" try: from PyQt5.QtWidgets import QProgressDialog, QApplication from PyQt5.QtCore import Qt # 创建进度对话框 progress = QProgressDialog("正在转换模型格式以获得更好的动画支持...", "取消", 0, 100) progress.setWindowTitle("模型格式转换") progress.setWindowModality(Qt.WindowModal) progress.show() QApplication.processEvents() try: result = self._convertToGLB(filepath, progress) progress.hide() return result except Exception as e: progress.hide() print(f"转换过程出错: {e}") return None except ImportError: # 如果没有 PyQt5,直接转换 return self._convertToGLB(filepath) def _convertToGLB(self, filepath, progress=None): """将模型文件转换为GLB格式""" try: print(f"[GLB转换] 开始转换: {filepath}") if progress: progress.setValue(10) progress.setLabelText("准备转换...") from PyQt5.QtWidgets import QApplication QApplication.processEvents() # 准备输出路径 base_name = os.path.splitext(os.path.basename(filepath))[0] output_dir = os.path.dirname(filepath) glb_path = os.path.join(output_dir, f"{base_name}_auto_converted.glb") # 如果已经存在转换后的文件,直接使用 if os.path.exists(glb_path): # 检查文件时间,如果原文件更新则重新转换 original_time = os.path.getmtime(filepath) converted_time = os.path.getmtime(glb_path) if converted_time > original_time: print(f"[GLB转换] 使用现有转换文件: {glb_path}") if progress: progress.setValue(100) return glb_path if progress: progress.setValue(20) progress.setLabelText("尝试 Blender 转换...") QApplication.processEvents() # 方法1: 使用 Blender 进行转换 if self._convertWithBlender(filepath, glb_path, progress): return glb_path if progress: progress.setValue(60) progress.setLabelText("尝试 FBX2glTF 转换...") QApplication.processEvents() # 方法2: 使用 FBX2glTF (如果可用) if self._convertWithFBX2glTF(filepath, glb_path, progress): return glb_path if progress: progress.setValue(80) progress.setLabelText("尝试 Assimp 转换...") QApplication.processEvents() # 方法3: 使用 Assimp if self._convertWithAssimp(filepath, glb_path, progress): return glb_path #print(f"[GLB转换] 所有转换方法都失败,既然没有可以转换格式的工具和环境那么就用原始文件,不一定非要转换") return None except Exception as e: print(f"[GLB转换] 转换过程出错: {e}") return None def _convertWithBlender(self, input_path, output_path, progress=None): """使用 Blender 进行转换""" try: import subprocess import tempfile print(f"[Blender转换] {input_path} → {output_path}") # 创建 Blender 脚本 script_content = f''' import bpy import sys import os # 清理默认场景 bpy.ops.object.select_all(action='SELECT') bpy.ops.object.delete(use_global=False) print("开始导入文件...") # 根据文件类型选择导入方法 input_file = "{input_path}" output_file = "{output_path}" try: ext = os.path.splitext(input_file)[1].lower() if ext == '.fbx': bpy.ops.import_scene.fbx(filepath=input_file) elif ext == '.obj': bpy.ops.import_scene.obj(filepath=input_file) elif ext == '.dae': bpy.ops.wm.collada_import(filepath=input_file) elif ext == '.blend': bpy.ops.wm.open_mainfile(filepath=input_file) else: print(f"不支持的格式: {{ext}}") sys.exit(1) print("导入成功,开始导出GLB...") # 导出为 GLB,保留动画 bpy.ops.export_scene.gltf( filepath=output_file, export_format='GLB', export_animations=True, export_force_sampling=True, export_frame_range=True, export_current_frame=False, export_skins=True, export_morph=True, export_lights=True, export_cameras=False ) print("GLB导出成功!") except Exception as e: print(f"转换失败: {{e}}") sys.exit(1) ''' # 写入临时脚本文件 with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as temp_file: temp_file.write(script_content) script_path = temp_file.name try: # 执行 Blender 转换 result = subprocess.run([ 'blender', '--background', '--python', script_path ], capture_output=True, text=True, timeout=180) # 清理临时文件 os.unlink(script_path) if result.returncode == 0 and os.path.exists(output_path): print(f"[Blender转换] 转换成功") return True else: print(f"[Blender转换] 转换失败: {result.stderr}") return False except subprocess.TimeoutExpired: print(f"[Blender转换] 转换超时") return False except FileNotFoundError: print(f"[Blender转换] Blender 未安装") return False except Exception as e: print(f"[Blender转换] 转换过程出错: {e}") return False def _convertWithFBX2glTF(self, input_path, output_path, progress=None): """使用 FBX2glTF 进行转换(仅支持FBX)""" try: import subprocess if not input_path.lower().endswith('.fbx'): return False print(f"[FBX2glTF转换] {input_path} → {output_path}") # 使用 FBX2glTF 转换 result = subprocess.run([ 'FBX2glTF', input_path, '--output', output_path, '--binary' ], capture_output=True, text=True, timeout=120) if result.returncode == 0 and os.path.exists(output_path): print(f"[FBX2glTF转换] 转换成功") return True else: print(f"[FBX2glTF转换] 转换失败: {result.stderr}") return False except subprocess.TimeoutExpired: print(f"[FBX2glTF转换] 转换超时") return False except FileNotFoundError: print(f"[FBX2glTF转换] FBX2glTF 未安装") return False except Exception as e: print(f"[FBX2glTF转换] 转换过程出错: {e}") return False def _convertWithAssimp(self, input_path, output_path, progress=None): """使用 PyAssimp 进行转换""" try: import pyassimp print(f"[PyAssimp转换] {input_path} → {output_path}") # 加载模型 scene = pyassimp.load(input_path) if not scene: print(f"[PyAssimp转换] 加载模型失败") return False if progress: progress.setValue(30) # 导出为GLB格式 pyassimp.export(scene, output_path, "glb2") if progress: progress.setValue(80) # 释放资源 pyassimp.release(scene) if os.path.exists(output_path): print(f"[PyAssimp转换] 转换成功") return True else: print(f"[PyAssimp转换] 转换失败: 输出文件未生成") return False except ImportError: print(f"[PyAssimp转换] PyAssimp 未安装") return False except Exception as e: print(f"[PyAssimp转换] 转换过程出错: {e}") return False