"""Scene manager model and geometry operations.""" import os import shutil import time import json import aiohttp import asyncio import inspect from pathlib import Path from panda3d.core import ( ModelPool, ModelRoot, Filename, NodePath, GeomNode, Material, Vec4, Vec3, MaterialAttrib, ColorAttrib, Point3, CollisionNode, CollisionSphere, CollisionBox, BitMask32, TransparencyAttrib, LColor, TransformState, RenderModeAttrib ) from panda3d.egg import EggData, EggVertexPool from RenderPipelineFile.rpplugins.smaa.jitters import halton_seq from scene import util from core.editor_context import get_editor_context class SceneManagerModelMixin: def _build_model_filename_candidates(self, path_text): """Build robust Panda Filename candidates for Windows/CJK absolute paths.""" candidates = [] seen = set() if not path_text: return candidates for variant in (path_text, util.normalize_model_path(path_text)): if not variant: continue for ctor_name in ("fromOsSpecificW", "from_os_specific_w", "fromOsSpecific", "from_os_specific"): ctor = getattr(Filename, ctor_name, None) if not ctor: continue try: fn = ctor(variant) key = fn.get_fullpath() if not key or key in seen: continue seen.add(key) candidates.append(fn) except Exception: continue try: fn = Filename(variant) key = fn.get_fullpath() if key and key not in seen: seen.add(key) candidates.append(fn) except Exception: continue return candidates def _load_model_from_candidates(self, primary_path, fallback_path=""): """Try multiple Filename constructors, optionally falling back to a second path.""" attempts = [] last_error = None for candidate_path in (primary_path, fallback_path): if not candidate_path or candidate_path in attempts: continue attempts.append(candidate_path) for fn in self._build_model_filename_candidates(candidate_path): try: model = self.world.loader.loadModel(fn) if model and not model.isEmpty(): return model, candidate_path except Exception as exc: last_error = exc continue if last_error: raise RuntimeError(f"Could not load model file(s): {attempts}: {last_error}") raise RuntimeError(f"Could not load model file(s): {attempts}") 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 visual_load_path = filepath gltf_meta = None try: from scene.gltf_support import get_gltf_visual_bam_path, probe_gltf_metadata gltf_meta = probe_gltf_metadata(filepath) if gltf_meta.get("is_gltf"): has_anim = gltf_meta.get("has_animations", False) # 智能加载策略: # 1. 如果模型有动画,仅在缓存已存在时使用缓存;首次导入直接加载原始 glTF, # 避免同步构建 BAM 导致首次导入被完整解析两次。 # 2. 如果模型是纯静态场景,则跳过缓存,使用原生加载器(如 Assimp),这样在大场景下更流畅。 if has_anim: project_manager = getattr(getattr(self, "world", None), "project_manager", None) project_root = getattr(project_manager, "current_project_path", "") if project_manager else "" cached_visual_path = get_gltf_visual_bam_path( filepath, project_root=project_root, skip_animations=False, # 有动画的模型不应跳过动画 flatten_nodes=False, ) if cached_visual_path and cached_visual_path != filepath and os.path.exists(cached_visual_path): visual_load_path = cached_visual_path print(f"[GLTF智能加载] 检测到动画,使用 panda3d-gltf 缓存: {cached_visual_path}") else: print(f"[GLTF智能加载] 检测到动画,首次导入跳过同步BAM构建: {filepath}") else: print(f"[GLTF智能加载] 纯静态模型,跳过缓存以开启流畅模式: {filepath}") except Exception as e: print(f"[GLTF可见缓存] 回退原始模型导入: {e}") # # 在加载前设置忽略未知属性 # 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"⚠️ 转换失败,使用原始文件") loaded_from_path = visual_load_path try: model, loaded_from_path = self._load_model_from_candidates( visual_load_path, fallback_path=filepath if visual_load_path != filepath else "", ) except Exception as e: print(f"导入模型失败: {str(e)}") return None if not model or model.isEmpty(): print("加载模型失败") return None # 设置模型名称 model_name = os.path.basename(filepath) # 确保名称有效 if not model_name: model_name = "imported_model" model.setName(model_name) # 移除统一设置颜色的代码,因为它可能覆盖PBR纹理并导致模型在RenderPipeline中渲染异常纯黑 # # model.setColor(0.8, 0.8, 0.8, 1.0) # 移除以防覆盖PBR纹理 # 将模型添加到场景 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 loaded_from_path != filepath: model.setTag("visual_model_cache_path", loaded_from_path) if normalized_filepath != original_filepath: model.setTag("converted_from", os.path.splitext(original_filepath)[1]) model.setTag("converted_to_glb", "true") if gltf_meta and gltf_meta.get("is_gltf"): model.setTag("gltf_animation_count", str(int(gltf_meta.get("animation_count", 0) or 0))) #特殊处理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._fixBlackMaterials(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") model.setTag("ssbo_managed", "false") # 记录应用的处理选项 if apply_unit_conversion: model.setTag("unit_conversion_applied", "true") if normalize_scales: model.setTag("scale_normalization_applied", "true") # 初始化动画标签,避免属性面板首次读取时误判“无动画” try: self._processModelAnimations(model) except Exception as e: print(f"初始化模型动画标签失败: {e}") # 添加到模型列表 self.models.append(model) # ImGui scene tree is rendered directly from scene state. 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 _fixBlackMaterials(self, model): # 修复模型中全黑的材质问题,同时为缺失材质的几何体添加默认材质,保留原有纹理 try: from panda3d.core import MaterialAttrib, Material, GeomNode for geom_path in model.findAllMatches('**/+GeomNode'): geom_node = geom_path.node() # 级联节点状态 node_state = geom_path.getState() if node_state.hasAttrib(MaterialAttrib.getClassType()): mat_attrib = node_state.getAttrib(MaterialAttrib.getClassType()) mat = mat_attrib.getMaterial() if mat: is_black = False if mat.hasBaseColor(): c = mat.getBaseColor() if c.x <= 0.05 and c.y <= 0.05 and c.z <= 0.05: is_black = True elif mat.hasDiffuse(): c = mat.getDiffuse() if c.x <= 0.05 and c.y <= 0.05 and c.z <= 0.05: is_black = True if is_black or not (mat.hasBaseColor() or mat.hasDiffuse()): new_mat = Material(mat) new_mat.setBaseColor((0.8, 0.8, 0.8, 1.0)) new_mat.setDiffuse((0.8, 0.8, 0.8, 1.0)) geom_path.setState(node_state.setAttrib(MaterialAttrib.make(new_mat))) # 几何体状态 for i in range(geom_node.getNumGeoms()): geom_state = geom_node.getGeomState(i) if geom_state.hasAttrib(MaterialAttrib.getClassType()): mat_attrib = geom_state.getAttrib(MaterialAttrib.getClassType()) mat = mat_attrib.getMaterial() if mat: is_black = False if mat.hasBaseColor(): c = mat.getBaseColor() if c.x <= 0.05 and c.y <= 0.05 and c.z <= 0.05: is_black = True elif mat.hasDiffuse(): c = mat.getDiffuse() if c.x <= 0.05 and c.y <= 0.05 and c.z <= 0.05: is_black = True if is_black or not (mat.hasBaseColor() or mat.hasDiffuse()): new_mat = Material(mat) new_mat.setBaseColor((0.8, 0.8, 0.8, 1.0)) new_mat.setDiffuse((0.8, 0.8, 0.8, 1.0)) geom_node.setGeomState(i, geom_state.setAttrib(MaterialAttrib.make(new_mat))) else: new_mat = Material() new_mat.setBaseColor((0.8, 0.8, 0.8, 1.0)) new_mat.setDiffuse((0.8, 0.8, 0.8, 1.0)) new_mat.setSpecular((0.2, 0.2, 0.2, 1.0)) new_mat.setRoughness(0.8) geom_node.setGeomState(i, geom_state.addAttrib(MaterialAttrib.make(new_mat))) model.clearColor() except Exception as e: print(f'修复黑色模型材质时出错: {e}') 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()} 启用模型间碰撞检测") # 导入阶段避免使用 calcTightBounds 扫描全部几何体。 # 这里优先使用 Panda 已有包围体快速生成一个可用于选择/碰撞的近似球体。 radius = 1.0 center = Point3(0, 0, 0) try: bounds = model.getBounds() if bounds and not bounds.isEmpty(): try: center = bounds.getApproxCenter() except Exception: center = bounds.getCenter() try: radius = float(bounds.getRadius()) except Exception: radius = 1.0 except Exception: pass if not (radius > 0.0 and radius < 1e10): radius = 1.0 cSphere = CollisionSphere(center, 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 refreshCollisionBounds(self, model): """重新计算并更新模型的碰撞框""" try: if not model or model.isEmpty(): return # 使用列表以便在遍历后安全删除 children_to_remove = [] for child in model.getChildren(): name = child.getName() if hasattr(child, 'getName') else "" if name.startswith("modelCollision_"): children_to_remove.append(child) # 如果存在旧碰撞节点,删除它并重新创建 if children_to_remove: for child in children_to_remove: child.removeNode() # 由于 calcTightBounds 被修改为使用局部坐标计算 (第三个参数传 model) # 无需再像之前一样为了解决偏移而将其重置回0点。 self.setupCollision(model) except Exception as e: print(f"刷新碰撞框失败: {e}") def updateSceneTree(self): """ImGui scene tree renders directly from scene state; no explicit refresh hook needed.""" return True 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 _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_checked"): return model_node.hasTag("has_animations") and model_node.getTag("has_animations").lower() == "true" # 检查模型是否已经有动画信息标签(兼容旧数据) if model_node.hasTag("has_animations"): has_animations = model_node.getTag("has_animations").lower() == "true" if has_animations: model_node.setTag("has_animations_checked", "true") print(f"模型 {model_node.getName()} 已有动画信息") return True # 优先从 glTF 元数据探测动画,避免依赖场景内必须保留骨骼节点。 source_path = "" for tag_name in ("original_path", "model_path", "saved_model_path", "file"): try: if model_node.hasTag(tag_name) and model_node.getTag(tag_name): source_path = model_node.getTag(tag_name) break except Exception: continue try: from scene.gltf_support import probe_gltf_metadata gltf_meta = probe_gltf_metadata(source_path) if gltf_meta.get("is_gltf"): has_animations = bool(gltf_meta.get("has_animations")) model_node.setTag("has_animations", "true" if has_animations else "false") model_node.setTag("has_animations_checked", "true") model_node.setTag("gltf_animation_count", str(int(gltf_meta.get("animation_count", 0) or 0))) if has_animations: model_node.setTag("can_create_actor_from_memory", "false") return has_animations except Exception: pass # 检查模型是否包含动画相关节点 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") model_node.setTag("has_animations_checked", "true") return has_animations except Exception as e: print(f"处理模型 {model_node.getName()} 动画时出错: {e}") return False def _cleanupAuxiliaryNodes(self): """清理场景中可能存在的辅助节点""" try: # 检查world和render是否存在 if not hasattr(self, 'world') or not self.world: print("world对象不存在,跳过辅助节点清理") return if not hasattr(self.world, 'render') or self.world.render.isEmpty(): print("render节点不存在,跳过辅助节点清理") return # 查找并移除所有坐标轴节点 gizmo_nodes = self.world.render.findAllMatches("**/gizmo*") for node in gizmo_nodes: if node and not node.isEmpty(): node.removeNode() print(f"清理坐标轴节点: {node.getName()}") # 查找并移除所有选择框节点 selection_box_nodes = self.world.render.findAllMatches("**/selectionBox*") for node in selection_box_nodes: if node and not node.isEmpty(): node.removeNode() print(f"清理选择框节点: {node.getName()}") # 停止相关的更新任务 try: from direct.task.TaskManagerGlobal import taskMgr taskMgr.remove("updateGizmo") taskMgr.remove("updateSelectionBox") except Exception as task_e: print(f"停止任务时出错: {task_e}") print("辅助节点清理完成") except Exception as e: print(f"清理辅助节点时出错: {e}") import traceback traceback.print_exc() 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 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) # 保留调用方已经恢复好的变换。项目重开/场景重建时模型的 # 根节点位置和旋转可能已经由场景文件回灌,不能在这里再强制归零。 # 应用材质 self.processMaterials(model) # 设置碰撞检测 self.setupCollision(model) # 更新场景树 self.updateSceneTree() print(f"异步加载模型完成: {model.getName()}")