#!/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 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): """导入模型到场景 Args: filepath: 模型文件路径 apply_unit_conversion: 是否应用单位转换(主要针对FBX文件) normalize_scales: 是否标准化子节点缩放(推荐开启) """ try: print(f"\n=== 开始导入模型: {filepath} ===") print(f"单位转换: {'开启' if apply_unit_conversion else '关闭'}") # 总是重新加载模型以确保材质信息完整 # 不使用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) # 可选的单位转换(主要针对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 _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): """加载带动画的模型""" try: # 创建Actor对象 actor = Actor(model_path, anims) if actor: actor.reparentTo(self.world.render) # 设置碰撞检测 self.setupCollision(actor) self.models.append(actor) # 更新场景树 self.updateSceneTree() return actor except Exception as e: print(f"加载动画模型失败: {str(e)}") 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.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