EG/scene/scene_manager.py

1244 lines
49 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.

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