1158 lines
50 KiB
Python
1158 lines
50 KiB
Python
"""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 direct.actor.Actor import Actor
|
||
from RenderPipelineFile.rpplugins.smaa.jitters import halton_seq
|
||
from scene import util
|
||
from scene.tree_roles import TREE_USER_ROLE
|
||
|
||
class SceneManagerModelMixin:
|
||
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)
|
||
|
||
# 移除统一设置颜色的代码,因为它可能覆盖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 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._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")
|
||
|
||
# 记录应用的处理选项
|
||
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)
|
||
|
||
# 更新场景树
|
||
# 获取树形控件并添加到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, TREE_USER_ROLE) == 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 _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()} 启用模型间碰撞检测")
|
||
|
||
# 获取模型的边界信息,使用与选择框相同的计算方法
|
||
minPoint = Point3()
|
||
maxPoint = Point3()
|
||
|
||
# 使用与选择框相同的calcTightBounds方法获取边界,但是在局部坐标系中进行计算
|
||
# 这样计算出的包围盒直接贴合几何体,并且无论模型自身受到什么平移/缩放/旋转都不会发生两次形变!
|
||
if model.calcTightBounds(minPoint, maxPoint, model):
|
||
# 检查边界框的有效性
|
||
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):
|
||
|
||
# 我们现在获取的是纯局部几何数据,因此不再需要手动乘以100或应用旋转来抵消FBX形变
|
||
|
||
# 创建与选择框完全一致的碰撞体
|
||
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 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):
|
||
"""更新场景树显示 - 代理到interface_manager"""
|
||
if hasattr(self.world, 'interface_manager'):
|
||
return self.world.interface_manager.updateSceneTree()
|
||
else:
|
||
print("界面管理器未初始化,无法更新场景树")
|
||
|
||
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
|
||
|
||
# 检查模型是否包含动画相关节点
|
||
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)
|
||
|
||
# 如果模型树中没检测到,再尝试通过 Actor 从文件路径检测
|
||
if not has_animations:
|
||
model_path = model_node.getTag("model_path") if model_node.hasTag("model_path") else ""
|
||
if model_path:
|
||
normalized_model_path = model_path.replace("\\", "/").lower()
|
||
is_project_scene_bam = (
|
||
normalized_model_path.endswith(".bam")
|
||
and "/scenes/" in normalized_model_path
|
||
)
|
||
# Project scene BAM is an aggregated scene root, not a single actor asset.
|
||
# Probing it via Actor(...) is unstable and may crash native Panda3D loader.
|
||
if is_project_scene_bam:
|
||
model_node.setTag("has_animations", "false")
|
||
model_node.setTag("has_animations_checked", "true")
|
||
return False
|
||
try:
|
||
from direct.actor.Actor import Actor
|
||
from panda3d.core import Filename
|
||
|
||
candidate_paths = []
|
||
# Always prefer normalized Panda paths; avoid raw Windows path fallback,
|
||
# which triggers noisy loader(error) logs for some absolute/Unicode paths.
|
||
try:
|
||
normalized = util.normalize_model_path(model_path)
|
||
if normalized and normalized != model_path:
|
||
candidate_paths.append(normalized)
|
||
except Exception:
|
||
pass
|
||
|
||
for ctor_name in ("fromOsSpecificW", "from_os_specific_w", "fromOsSpecific", "from_os_specific"):
|
||
ctor = getattr(Filename, ctor_name, None)
|
||
if not ctor:
|
||
continue
|
||
try:
|
||
panda_path = ctor(model_path).get_fullpath()
|
||
if panda_path:
|
||
candidate_paths.append(panda_path)
|
||
except Exception:
|
||
continue
|
||
|
||
seen = set()
|
||
unique_paths = []
|
||
for p in candidate_paths:
|
||
if not p or p in seen:
|
||
continue
|
||
seen.add(p)
|
||
unique_paths.append(p)
|
||
|
||
for candidate in unique_paths:
|
||
try:
|
||
actor = Actor(candidate)
|
||
anim_names = actor.getAnimNames()
|
||
actor.cleanup()
|
||
actor.removeNode()
|
||
if anim_names:
|
||
print(f"通过 Actor 路径检测到动画: {candidate} -> {anim_names}")
|
||
has_animations = True
|
||
break
|
||
except Exception:
|
||
continue
|
||
except Exception:
|
||
pass
|
||
|
||
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)
|
||
|
||
# 设置基础变换
|
||
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()}")
|