EG/scene/scene_manager_model_mixin.py
2026-03-06 09:52:57 +08:00

1158 lines
50 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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()}")