1
0
forked from Rowland/EG
EG/scene/scene_manager.py

4078 lines
177 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
import shutil
import time
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QTreeWidgetItem
from panda3d.core import (
ModelPool, ModelRoot, Filename, NodePath, GeomNode, Material, Vec4, Vec3,
MaterialAttrib, ColorAttrib, Point3, CollisionNode, CollisionSphere,
BitMask32, TransparencyAttrib, LColor, TransformState
)
import json
import aiohttp
import asyncio
import inspect
from pathlib import Path
from panda3d.egg import EggData, EggVertexPool
from direct.actor.Actor import Actor
from QPanda3D.Panda3DWorld import get_render_pipeline
from RenderPipelineFile.rpplugins.smaa.jitters import halton_seq
from scene import util
class CesiumIntegration:
def __init__(self, scene_manager):
self.scene_manager = scene_manager
self.world = scene_manager.world
self.tilesets = {}
def add_tileset(self,name,url,position=(0,0,0)):
try:
tileset_node = self.scene_manager.load_cesium_tileset(url,position)
if tileset_node:
self.tilesets[name] = {
'node':tileset_node,
'url':url,
'position':position
}
print(f"✓ 添加 Cesium tileset: {name}")
return tileset_node
else:
print(f"✗ 添加 Cesium tileset 失败: {name}")
return None
except Exception as e:
print(f"✗ 添加 Cesium tileset 出错: {e}")
return None
def remove_tileset(self, name):
"""移除 tileset"""
if name in self.tilesets:
tileset_info = self.tilesets[name]
tileset_info['node'].removeNode()
del self.tilesets[name]
print(f"✓ 移除 Cesium tileset: {name}")
return True
return False
def get_tileset(self, name):
"""获取 tileset"""
return self.tilesets.get(name, None)
def list_tilesets(self):
"""列出所有 tilesets"""
return list(self.tilesets.keys())
class SceneManager:
"""场景管理器 - 统一管理场景中的所有元素"""
def __init__(self, world):
"""初始化场景管理器
Args:
world: 主程序world对象引用
"""
self.world = world
self.models = [] # 模型列表
self.Spotlight = []
self.Pointlight = []
self.tilesets = [] #来存储tilesets
self.cesium_integration = CesiumIntegration(self)
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
# 在加载前设置忽略未知属性
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
# 显示成功消息
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)
# 确保名称有效
if not model_name:
model_name = "imported_model"
model.setName(model_name)
# 将模型添加到场景
model.reparentTo(self.world.render)
# 设置模型名称
model_name = os.path.basename(filepath)
model.setName(model_name)
# 保存原始路径和转换后的路径
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 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)")
# # 可选的单位转换主要针对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)
# 调整模型位置到地面
model.setPos(0,0,0)
#self._adjustModelToGround(model)
# 创建并设置基础材质
print("\n=== 开始设置材质 ===")
self._applyMaterialsToModel(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")
# 添加到模型列表
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, Qt.UserRole) == 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 _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)
# 应用缩放(添加异常处理)
try:
model.setScale(scale_factor)
except Exception as e:
print(f"直接设置缩放失败: {e},尝试使用变换状态")
try:
model.setTransform(TransformState.makeScale(scale_factor))
except Exception as e2:
print(f"使用变换状态设置缩放也失败: {e2}")
# 应用缩放后验证变换
self._validateAndFixTransform(model)
# 重新调整位置(因为缩放会影响边界)
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 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()} 启用模型间碰撞检测")
# 获取模型的边界
bounds = model.getBounds()
if bounds.isEmpty():
print(f"⚠️ 模型 {model.getName()} 边界为空,使用默认碰撞体")
# 使用默认的小球体
cSphere = CollisionSphere(Point3(0, 0, 0), 1.0)
else:
center = bounds.getCenter()
radius = bounds.getRadius()
# 确保半径不为零
if radius <= 0:
radius = 1.0
print(f"⚠️ 模型 {model.getName()} 半径为零,使用默认半径 1.0")
#
# # 添加碰撞球体
# cSphere = CollisionSphere(center, radius)
cSphere = self.world.collision_manager.createCollisionShape(model, 'polygon')
cNode.addSolid(cSphere)
# 将碰撞节点附加到模型上
cNodePath = model.attachNewNode(cNode)
# 根据调试设置决定是否显示碰撞体
if hasattr(self.world, 'debug_collision') and self.world.debug_collision:
cNodePath.hide()
else:
cNodePath.hide()
# 为模型添加碰撞相关标签
model.setTag("has_collision", "true")
model.setTag("collision_radius", str(radius if 'radius' in locals() else 1.0))
print(f"✅ 为模型 {model.getName()} 设置碰撞检测完成")
return cNodePath
except Exception as e:
print(f"❌ 为模型 {model.getName()} 设置碰撞检测失败: {str(e)}")
import traceback
traceback.print_exc()
return None
# ==================== 场景树管理 ====================
def updateSceneTree(self):
"""更新场景树显示 - 代理到interface_manager"""
if hasattr(self.world, 'interface_manager'):
return self.world.interface_manager.updateSceneTree()
else:
print("界面管理器未初始化,无法更新场景树")
# ==================== 场景保存和加载 ====================
def _collectGUIElementInfo(self, gui_node):
"""收集GUI元素的信息用于保存"""
try:
# 获取GUI元素类型
gui_type = "unknown"
if hasattr(gui_node, 'hasTag') and gui_node.hasTag("gui_type"):
gui_type = gui_node.getTag("gui_type")
elif hasattr(gui_node, 'hasTag') and gui_node.hasTag("saved_gui_type"):
gui_type = gui_node.getTag("saved_gui_type")
else:
# 尝试从节点名称推断类型
name_lower = gui_node.getName().lower()
if "button" in name_lower:
gui_type = "button"
elif "label" in name_lower:
gui_type = "label"
elif "entry" in name_lower:
gui_type = "entry"
elif "image" in name_lower:
gui_type = "2d_image"
elif "videoscreen" in name_lower:
if "2d" in name_lower:
gui_type = "2d_video_screen"
else:
gui_type = "video_screen"
elif "info_panel" in name_lower:
if "3d" in name_lower:
gui_type = "info_panel_3d"
else:
gui_type = "info_panel"
else:
# 如果无法识别类型,跳过该元素
print(f"跳过无法识别类型的GUI元素: {gui_node.getName()}")
return None
gui_info = {
"name": gui_node.getName(),
"type": gui_type,
"position": list(gui_node.getPos()),
"rotation": list(gui_node.getHpr()),
"scale": list(gui_node.getScale()),
"tags": {},
"parent_name":None,
"video_path":gui_node.getTag("video_path") if gui_node.hasTag("video_path") else None,
"panel_id":gui_node.getTag("panel_id") if gui_node.hasTag("panel_id") else None,
}
parent = gui_node.getParent()
if parent and not parent.isEmpty():
parent_name = parent.getName()
if parent_name not in ["render","aspect2d","render2d"]:
gui_info["parent_name"] = parent_name
# 收集所有标签仅对NodePath类型的对象
if hasattr(gui_node, 'getTagNames'):
for tag in gui_node.getTagNames():
gui_info["tags"][tag] = gui_node.getTag(tag)
elif hasattr(gui_node, 'getTags'): # 对于DirectGUI对象
# DirectGUI对象使用不同的方法存储标签
if hasattr(gui_node, '_tags'):
gui_info["tags"] = gui_node._tags.copy()
# 根据类型收集特定信息
if gui_type == "button":
if hasattr(gui_node, 'get'): # DirectButton
gui_info["text"] = gui_node.get()
elif hasattr(gui_node, 'getText'): # 其他类型
gui_info["text"] = gui_node.getText()
elif hasattr(gui_node, 'hasTag') and gui_node.hasTag("gui_text"):
gui_info["text"] = gui_node.getTag("gui_text")
elif gui_type == "label":
if hasattr(gui_node, 'getText'):
gui_info["text"] = gui_node.getText()
elif hasattr(gui_node, 'hasTag') and gui_node.hasTag("gui_text"):
gui_info["text"] = gui_node.getTag("gui_text")
elif gui_type == "entry":
if hasattr(gui_node, 'get'):
gui_info["text"] = gui_node.get()
elif hasattr(gui_node, 'hasTag') and gui_node.hasTag("gui_text"):
gui_info["text"] = gui_node.getTag("gui_text")
elif gui_type == "2d_image":
if hasattr(gui_node, 'hasTag') and gui_node.hasTag("image_path"):
gui_info["image_path"] = gui_node.getTag("image_path")
elif hasattr(gui_node, 'hasTag') and gui_node.hasTag("gui_image_path"):
gui_info["image_path"] = gui_node.getTag("gui_image_path")
elif gui_type == "3d_text":
if hasattr(gui_node,'hasTag') and gui_node.hasTag("gui_text"):
gui_info["text"] = gui_node.getTag("gui_text")
elif hasattr(gui_node,'node') and hasattr(gui_node.node(),'getText'):
gui_info["text"] = gui_node.node().getText()
elif gui_type == "3d_image":
if hasattr(gui_node,'hasTag') and gui_node.hasTag("gui_image_path"):
gui_info["image_path"] = gui_node.getTag("gui_image_path")
elif gui_type == "video_screen":
if hasattr(gui_node, 'hasTag') and gui_node.hasTag("video_path"):
gui_info["video_path"] = gui_node.getTag("video_path")
elif gui_type == "2d_video_screen":
if hasattr(gui_node, 'hasTag') and gui_node.hasTag("video_path"):
gui_info["video_path"] = gui_node.getTag("video_path")
elif gui_type == "virtual_screen":
if hasattr(gui_node, 'hasTag') and gui_node.hasTag("gui_text"):
gui_info["text"] = gui_node.getTag("gui_text")
elif gui_type in ["info_panel", "info_panel_3d"]:
# 收集信息面板的特定信息
if hasattr(gui_node, 'hasTag') and gui_node.hasTag("panel_id"):
gui_info["panel_id"] = gui_node.getTag("panel_id")
# 收集背景图片信息
if hasattr(gui_node, 'hasTag') and gui_node.hasTag("image_path"):
gui_info["image_path"] = gui_node.getTag("image_path")
# 收集挂载的脚本信息
if hasattr(self.world, 'script_manager') and self.world.script_manager:
try:
script_manager = self.world.script_manager
scripts = script_manager.get_scripts_on_object(gui_node) # 修复:使用 gui_node 而不是 node
if scripts:
gui_info["scripts"] = []
for script_component in scripts:
try:
script_name = script_component.script_name
# 获取脚本路径
script_class = script_component.script_instance.__class__
script_file = self._get_script_file_path(script_class, script_name)
# 只有当脚本文件存在时才保存
if script_file and os.path.exists(script_file):
gui_info["scripts"].append({
"name": script_name,
"file": script_file
})
print(f"收集脚本信息: {script_name} from {script_file}")
else:
print(f"警告: 脚本文件不存在: {script_file}")
except Exception as e:
print(f"收集单个脚本信息失败 {script_name}, 错误: {e}")
continue
except Exception as e:
print(f"收集脚本信息失败: {e}")
print(f"成功收集GUI元素信息: {gui_info}")
return gui_info
except Exception as e:
print(f"收集GUI元素信息失败: {e}")
import traceback
traceback.print_exc()
return None
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 saveScene(self, filename,project_path):
"""保存场景到BAM文件 - 完整版支持GUI元素,地形"""
try:
print(f"\n=== 开始保存场景到: {filename} ===")
# 确保文件路径是规范化的
filename = os.path.normpath(filename)
# 确保目录存在
directory = os.path.dirname(filename)
if directory and not os.path.exists(directory):
os.makedirs(directory)
resources_dir = os.path.join(directory,"resources")
if not os.path.exists(resources_dir):
os.makedirs(resources_dir)
# 存储需要临时隐藏的节点,以便保存后恢复
nodes_to_restore = []
# 查找并隐藏所有坐标轴和选择框节点
gizmo_nodes = self.world.render.findAllMatches("**/gizmo*")
selection_box_nodes = self.world.render.findAllMatches("**/selectionBox*")
# 隐藏坐标轴节点
for node in gizmo_nodes:
if not node.isHidden():
nodes_to_restore.append((node, True)) # (节点, 原先是否可见)
node.hide()
print(f"临时隐藏坐标轴节点: {node.getName()}")
# 隐藏选择框节点
for node in selection_box_nodes:
if not node.isHidden():
nodes_to_restore.append((node, True))
node.hide()
print(f"临时隐藏选择框节点: {node.getName()}")
# 收集所有需要保存的节点
all_nodes = []
all_nodes.extend(self.models)
all_nodes.extend(self.Spotlight)
all_nodes.extend(self.Pointlight)
# 添加GUI元素节点
gui_elements = []
if hasattr(self.world, 'gui_elements'):
# 过滤掉空的或重复的GUI元素
unique_gui_elements = []
seen_names = set()
for elem in self.world.gui_elements:
if elem and not elem.isEmpty():
if not elem.isEmpty() and elem.getName() not in seen_names:
unique_gui_elements.append(elem)
seen_names.add(elem.getName())
gui_elements = unique_gui_elements
print(f"保存时GUI元素列表=>>>>>>>>>>>>{self.world.gui_elements}")
all_nodes.extend(gui_elements)
# 创建用于保存GUI信息的JSON文件路径
gui_info_file = filename.replace('.bam', '_gui.json')
print(self.world.gui_elements)
# 收集GUI元素信息排除3D文本和3D图像
gui_data = []
copied_resources = {}
for gui_node in gui_elements:
gui_info = self._collectGUIElementInfo(gui_node)
if gui_info:
gui_type = gui_info.get("type","")
#处理2d图片
if gui_type =="2d_image" and "image_path" in gui_info:
original_path = gui_info["image_path"]
if original_path and os.path.exists(original_path):
resource_name = os.path.basename(original_path)
new_path = os.path.join(resources_dir,resource_name)
if original_path not in copied_resources:
try:
shutil.copy2(original_path,new_path)
copied_resources[original_path] = new_path
print(f"复制图片资源: {original_path} -> {new_path}")
except Exception as e:
print(f"复制图片资源失败: {original_path}, 错误: {e}")
gui_info["image_path"] = new_path
# 处理3D图片
elif gui_type == "3d_image" and "image_path" in gui_info:
original_path = gui_info["image_path"]
# 确保original_path是有效字符串且文件存在
if original_path and isinstance(original_path, str) and os.path.exists(original_path):
resource_name = os.path.basename(original_path)
new_path = os.path.join(resources_dir, resource_name)
if original_path not in copied_resources:
try:
shutil.copy2(original_path, new_path)
copied_resources[original_path] = new_path
print(f"复制3D图片资源: {original_path} -> {new_path}")
except Exception as e:
print(f"复制3D图片资源失败: {original_path}, 错误: {e}")
gui_info["image_path"] = new_path
# 处理背景图片
if "bg_image_path" in gui_info and gui_info["bg_image_path"]:
original_path = gui_info["bg_image_path"]
# 确保original_path是有效字符串且文件存在
if original_path and isinstance(original_path, str) and os.path.exists(original_path):
resource_name = os.path.basename(original_path)
new_path = os.path.join(resources_dir, resource_name)
if original_path not in copied_resources:
try:
shutil.copy2(original_path, new_path)
copied_resources[original_path] = new_path
print(f"复制背景图片资源: {original_path} -> {new_path}")
except Exception as e:
print(f"复制背景图片资源失败: {original_path}, 错误: {e}")
gui_info["bg_image_path"] = new_path
# 处理视频资源
if gui_type in ["video_screen", "2d_video_screen"] and "video_path" in gui_info:
original_path = gui_info["video_path"]
# 确保original_path是有效字符串且文件存在
if original_path and isinstance(original_path, str) and os.path.exists(original_path):
resource_name = os.path.basename(original_path)
new_path = os.path.join(resources_dir, resource_name)
if original_path not in copied_resources:
try:
shutil.copy2(original_path, new_path)
copied_resources[original_path] = new_path
print(f"复制视频资源: {original_path} -> {new_path}")
except Exception as e:
print(f"复制视频资源失败: {original_path}, 错误: {e}")
gui_info["video_path"] = new_path
gui_data.append(gui_info)
print(f"添加GUI信息: {gui_info['name']}")
# 保存GUI信息到JSON文件确保即使没有GUI元素也创建有效的空JSON数组
try:
import json
with open(gui_info_file, 'w', encoding='utf-8') as f:
json.dump(gui_data, f, ensure_ascii=False, indent=2)
print(f"✓ GUI信息已保存到: {gui_info_file}")
except Exception as e:
print(f"✗ 保存GUI信息失败: {e}")
import traceback
traceback.print_exc()
# 保存所有节点的信息
for node in all_nodes:
if node.isEmpty():
continue
# 保存变换信息
node.setTag("transform_pos", str(node.getPos()))
node.setTag("transform_hpr", str(node.getHpr()))
node.setTag("transform_scale", str(node.getScale()))
print(f"保存节点 {node.getName()} 的变换信息")
# 保存父子关系信息 - 关键修改
parent = node.getParent()
if parent and not parent.isEmpty() and parent != self.world.render:
# 只有当父节点不是根节点且父节点是场景中的模型时才保存父子关系
if parent.getName() not in ["render", "aspect2d", "render2d"]:
# 检查父节点是否也是场景中的模型
is_parent_model = False
for model in self.models:
if model == parent:
is_parent_model = True
break
if is_parent_model:
node.setTag("parent_name", parent.getName())
print(f"保存节点 {node.getName()} 的父节点信息: {parent.getName()}")
# 获取当前状态
state = node.getState()
# 如果有材质属性,保存为标签
if state.hasAttrib(MaterialAttrib.getClassType()):
mat_attrib = state.getAttrib(MaterialAttrib.getClassType())
material = mat_attrib.getMaterial()
if material:
# 保存材质属性到标签
node.setTag("material_ambient", str(material.getAmbient()))
node.setTag("material_diffuse", str(material.getDiffuse()))
node.setTag("material_specular", str(material.getSpecular()))
node.setTag("material_emission", str(material.getEmission()))
node.setTag("material_shininess", str(material.getShininess()))
if material.hasBaseColor():
node.setTag("material_basecolor", str(material.getBaseColor()))
# 保存特定类型节点的额外信息
if node.hasTag("light_type"):
# 保存光源特定信息
light_obj = node.getPythonTag("rp_light_object")
if light_obj:
node.setTag("light_energy", str(light_obj.energy))
node.setTag("light_radius", str(getattr(light_obj, 'radius', 0)))
if hasattr(light_obj, 'fov'):
node.setTag("light_fov", str(light_obj.fov))
elif node.hasTag("element_type"):
element_type = node.getTag("element_type")
if element_type == "cesium_tileset":
# 保存tileset特定信息
if node.hasTag("tileset_url"):
node.setTag("saved_tileset_url", node.getTag("tileset_url"))
elif node.hasTag("gui_type") or node.hasTag("is_gui_element"):
# 保存GUI元素特定信息
gui_type = node.getTag("gui_type") if node.hasTag("gui_type") else \
node.getTag("saved_gui_type") if node.hasTag("saved_gui_type") else "unknown"
node.setTag("saved_gui_type", gui_type)
# 保存GUI元素的通用属性
if hasattr(node, 'getPythonTag'):
# 保存任何Python标签数据
for tag_name in node.getPythonTagKeys():
try:
tag_value = node.getPythonTag(tag_name)
node.setTag(f"python_tag_{tag_name}", str(tag_value))
except:
pass
elif node.hasTag("element_type") and node.getTag("element_type") == "info_panel":
# 保存信息面板特定信息
print(f"保存信息面板信息: {node.getName()}")
panel_id = node.getTag("panel_id") if node.hasTag("panel_id") else node.getName()
if hasattr(self.world, 'info_panel_manager'):
panel_data = self.world.info_panel_manager.serializePanelData(panel_id)
if panel_data:
import json
node.setTag("info_panel_data", json.dumps(panel_data, ensure_ascii=False))
if hasattr(self.world,'script_manager') and self.world.script_manager:
script_manager = self.world.script_manager
scripts = script_manager.get_scripts_on_object(node)
if scripts:
node.setTag("has_scripts", "true")
script_info_list = []
for script_component in scripts:
script_name = script_component.script_name
print(f"保存脚本信息: {script_name}")
# 获取脚本类的文件路径
script_class = script_component.script_instance.__class__
script_file = self._get_script_file_path(script_class, script_name)
script_info_list.append({
"name": script_name,
"file": script_file
})
# 将脚本信息保存为JSON字符串
import json
node.setTag("scripts_info", json.dumps(script_info_list, ensure_ascii=False))
print(f"为节点 {node.getName()} 保存了 {len(script_info_list)} 个脚本")
try:
print("--- 打印当前场景图 (render) ---")
self.world.render.ls()
print("---------------------------------")
self.take_screenshot(project_path)
# 保存场景
success = self.world.render.writeBamFile(Filename.fromOsSpecific(filename))
if success:
print(f"✓ 场景保存成功: {filename}")
else:
print("✗ 场景保存失败")
return success
finally:
# 恢复之前隐藏的节点
for item in nodes_to_restore:
node, was_visible = item
if was_visible and not node.isEmpty():
node.show()
print(f"恢复显示节点: {node.getName()}")
if nodes_to_restore:
print(f"已恢复 {len(nodes_to_restore)} 个辅助节点的显示")
except Exception as e:
print(f"保存场景时发生错误: {str(e)}")
import traceback
traceback.print_exc()
return False
def take_screenshot(self, projectpath):
"""
截图并保存到指定的完整路径
Args:
full_path (str): 完整的文件保存路径,包括文件名和扩展名
Returns:
bool: 截图是否成功
"""
try:
from panda3d.core import Filename
import os
print(f"\n=== 截图保存: {projectpath} ===")
# 确保目录存在
directory = os.path.dirname(projectpath)
if directory and not os.path.exists(directory):
os.makedirs(directory)
print(f"创建目录: {directory}")
# 规范化路径
filename = os.path.basename(os.path.normpath(projectpath))
filename = f'{filename}.png'
print(f'project_path: {projectpath}')
print(f'project_name: {filename}')
full_path = os.path.normpath(os.path.join(projectpath, filename))
p3d_filename = Filename.from_os_specific(full_path)
# 使用 Panda3D 的截图功能
success = self.world.win.saveScreenshot(p3d_filename)
if success:
print(f"✅ 成功截图并保存到: {full_path}")
return True
else:
print(f"❌ 截图保存失败: {full_path}")
return False
except Exception as e:
print(f"保存截图时发生错误: {str(e)}")
import traceback
traceback.print_exc()
return False
def loadScene(self, filename):
"""从BAM文件加载场景"""
try:
print(f"\n=== 开始加载场景: {filename} ===")
# 确保文件路径是规范化的
filename = os.path.normpath(filename)
# 检查文件是否存在
if not os.path.exists(filename):
print(f"场景文件不存在: {filename}")
return False
tree_widget = self._get_tree_widget()
# 清除当前场景
print("\n清除当前场景...")
for model in self.models:
tree_widget.delete_item(model)
# 清除灯光
for light_node in self.Spotlight:
tree_widget.delete_item(light_node)
for light_node in self.Pointlight:
tree_widget.delete_item(light_node)
for terrain in self.world.terrain_manager.terrains:
tree_widget.delete_item(terrain)
# 清除tilesets
for tileset_info in self.tilesets:
tree_widget.delete_item(tileset_info['node'])
for light in self.Spotlight:
if not light.isEmpty():
light.removeNode()
self.Spotlight.clear()
for light in self.Pointlight:
if not light.isEmpty():
light.removeNode()
self.Pointlight.clear()
# 清理tilesets
for tileset_info in self.tilesets:
if tileset_info['node'] and not tileset_info['node'].isEmpty():
tileset_info['node'].removeNode()
self.tilesets.clear()
# 清理Cesium tilesets
for tileset_name, tileset_info in list(self.cesium_integration.tilesets.items()):
if tileset_info['node'] and not tileset_info['node'].isEmpty():
tileset_info['node'].removeNode()
self.cesium_integration.tilesets.clear()
for gui in self.world.gui_elements:
if not gui.isEmpty():
gui.removeNode()
self.world.gui_elements.clear()
if hasattr(self.world,'info_panel_manager'):
self.world.info_panel_manager.removeAllPanels()
# 清理可能存在的辅助节点
self._cleanupAuxiliaryNodes()
# 加载场景
scene = self.world.loader.loadModel(Filename.fromOsSpecific(filename))
if not scene:
print("场景加载失败")
return False
tree_widget.create_model_items(scene)
# 遍历场景中的所有模型节点
# 用于存储处理后的灯光节点,避免重复处理
processed_lights = []
# 用于存储处理后的GUI元素避免重复处理
#存储所有加载的节点,用于后续处理父子关系
loaded_nodes = {} #name->nodePath映射
# 遍历场景中的所有节点
def processNode(nodePath, depth=0):
indent = " " * depth
print(f"{indent}处理节点: {nodePath.getName()} (类型: {type(nodePath.node()).__name__})")
#存储节点以便后续处理父子关系
loaded_nodes[nodePath.getName()] = nodePath
if nodePath.getName().startswith('ground'):
print(f"{indent}跳过ground节点: {nodePath.getName()}")
return
# 跳过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 nodePath.getName().startswith(("gizmo", "selectionBox")):
print(f"{indent}跳过辅助节点: {nodePath.getName()}")
return
if nodePath.getName() in ['SceneRoot'] or \
any(keyword in nodePath.getName() for keyword in ["Skybox", "skybox"]):
print(f"{indent}跳过环境节点:{nodePath.getName()}")
return
# 检查是否是用户创建的场景元素
is_scene_element = (
nodePath.hasTag("is_scene_element") or
nodePath.hasTag("is_model_root") or
nodePath.hasTag("light_type") or
nodePath.hasTag("gui_type") or # 检查gui_type标签
nodePath.hasTag("is_gui_element") or
nodePath.hasTag("saved_gui_type") or
(nodePath.hasTag("element_type") and nodePath.getTag("element_type") == "info_panel")
)
# 特殊处理检查节点名称是否包含GUI相关关键词
is_potential_gui = any(keyword in nodePath.getName().lower() for keyword in
["gui", "button", "label", "entry", "image", "video", "screen", "text"])
if is_scene_element or is_potential_gui:
print(f"{indent}找到场景元素节点: {nodePath.getName()}")
# 如果是潜在的GUI元素但没有标签添加基本标签
if is_potential_gui and not (nodePath.hasTag("gui_type") or nodePath.hasTag("is_gui_element")):
print(f"{indent}为潜在GUI元素添加标签: {nodePath.getName()}")
nodePath.setTag("is_gui_element", "1")
nodePath.setTag("is_scene_element", "1")
# 尝试从名称推断类型
name_lower = nodePath.getName().lower()
if "button" in name_lower:
nodePath.setTag("gui_type", "button")
elif "label" in name_lower:
nodePath.setTag("gui_type", "label")
elif "entry" in name_lower:
nodePath.setTag("gui_type", "entry")
elif "image" in name_lower:
nodePath.setTag("gui_type", "image")
elif "video" in name_lower or "screen" in name_lower:
nodePath.setTag("gui_type", "video_screen")
else:
nodePath.setTag("gui_type", "unknown")
# 清除现有材质状态
nodePath.clearMaterial()
nodePath.clearColor()
# 恢复变换信息
def parseVec3(vec_str):
"""解析向量字符串为Vec3"""
try:
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}")
if nodePath.hasTag("has_scripts") and nodePath.getTag("has_scripts") == "true":
if hasattr(self.world,'script_manager') and self.world.script_manager:
try:
import json
scripts_info = json.loads(nodePath.getTag("scripts_info"))
print(f"节点 {nodePath.getName()} 需要重新挂载 {len(scripts_info)} 个脚本")
script_manager = self.world.script_manager
for script_info in scripts_info:
script_name = script_info["name"]
script_file = script_info.get("file","")
print(f"尝试重新挂载脚本{script_name}from {script_file}")
if script_name not in script_manager.loader.script_classes:
if script_file and os.path.exists(script_file):
print(f"从文件加载脚本:{script_file}")
loaded_class = script_manager.load_script_from_file(script_file)
if loaded_class is None:
print(f"从文件加载脚本失败{script_file}")
script_path = self._find_scrip_in_directory(script_name)
if script_path:
print(f"从目录找到脚本并加载{script_path}")
script_manager.load_script_from_file(script_path)
else:
script_path = self._find_script_in_directory(script_name)
if script_path:
print(f"从目录找到脚本并加载: {script_path}")
script_manager.load_script_from_file(script_path)
else:
print(f"找不到脚本文件: {script_name}")
if script_name in script_manager.loader.script_classes:
script_component = script_manager.add_script_to_object(nodePath,script_name)
if script_component:
print(f"成功为 {nodePath.getName()} 添加脚本: {script_name}")
else:
print(f"{nodePath.getName()} 添加脚本失败: {script_name}")
else:
print(f"脚本 {script_name} 不可用,跳过挂载")
except Exception as e:
print(f"重新挂载脚本失败: {e}")
import traceback
traceback.print_exc()
# 恢复材质属性
def parseColor(color_str):
"""解析颜色字符串为Vec4"""
try:
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)
# 创建并恢复材质
material = Material()
material_changed = False
if nodePath.hasTag("material_ambient"):
material.setAmbient(parseColor(nodePath.getTag("material_ambient")))
material_changed = True
if nodePath.hasTag("material_diffuse"):
material.setDiffuse(parseColor(nodePath.getTag("material_diffuse")))
material_changed = True
if nodePath.hasTag("material_specular"):
material.setSpecular(parseColor(nodePath.getTag("material_specular")))
material_changed = True
if nodePath.hasTag("material_emission"):
material.setEmission(parseColor(nodePath.getTag("material_emission")))
material_changed = True
if nodePath.hasTag("material_shininess"):
material.setShininess(float(nodePath.getTag("material_shininess")))
material_changed = True
if nodePath.hasTag("material_basecolor"):
material.setBaseColor(parseColor(nodePath.getTag("material_basecolor")))
material_changed = True
if material_changed:
nodePath.setMaterial(material)
# 恢复颜色属性
if nodePath.hasTag("color"):
nodePath.setColor(parseColor(nodePath.getTag("color")))
# 处理特定类型的节点
if nodePath.hasTag("light_type"):
light_type = nodePath.getTag("light_type")
print(f"{indent}检测到光源类型: {light_type}")
# 检查是否已经处理过这个灯光
if nodePath not in processed_lights:
# 重新创建RP光源对象
if light_type == "spot_light":
self._recreateSpotLight(nodePath)
elif light_type == "point_light":
self._recreatePointLight(nodePath)
# 标记为已处理
processed_lights.append(nodePath)
elif nodePath.hasTag("element_type"):
element_type = nodePath.getTag("element_type")
if element_type == "cesium_tileset":
tileset_url = nodePath.getTag("saved_tileset_url") if nodePath.hasTag(
"saved_tileset_url") else ""
tileset_info = {
'url': tileset_url,
'node': nodePath,
'position': nodePath.getPos(),
'tiles': {}
}
self.tilesets.append(tileset_info)
self.cesium_integration.tilesets[nodePath.getName()] = tileset_info
# 将节点重新挂载到render下如果需要
# 注意GUI元素可能需要挂载到特定的父节点上
if nodePath.hasTag("gui_type") or nodePath.hasTag("is_gui_element"):
# GUI元素通常应该挂载到aspect2d或特定的GUI父节点上
# 这里我们先保持原挂载关系
pass
else:
# 其他节点确保挂载到render下
if nodePath.getParent() != self.world.render and not nodePath.getName() in ["render",
"aspect2d",
"render2d"]:
nodePath.wrtReparentTo(self.world.render)
# 为模型节点设置碰撞检测
if nodePath.hasTag("is_model_root"):
print(f"J{indent}处理模型节点{nodePath.getName()}")
#self._validateAndFixAllTransforms(nodePath)
self._fixModelStructure(nodePath)
# if self.world.property_panel._hasCollision(nodePath):
# print(f"{indent}模型{nodePath.getName()}已有碰撞体,跳过碰撞体设置")
# else:
# print(f"{indent}为模型{nodePath.getName()}设置碰撞检测")
# self.setupCollision(nodePath)
self.models.append(nodePath)
# 递归处理子节点
for child in nodePath.getChildren():
processNode(child, depth + 1)
print("\n开始处理场景节点...")
processNode(scene)
#处理父子关系 - 在所有节点加载完成后设置正确的父子关系
print("\n开始重建父子关系...")
self._rebuildParentChildRelationships(loaded_nodes)
# 加载GUI信息并重新创建非3D的GUI元素
gui_info_file = filename.replace('.bam', '_gui.json')
if os.path.exists(gui_info_file):
try:
with open(gui_info_file, 'r', encoding='utf-8') as f:
content = f.read().strip()
if content: # 检查文件是否为空
import json
gui_data = json.loads(content)
print(f"✓ 成功加载GUI信息文件: {gui_info_file}")
print(f" 发现 {len(gui_data)} 个GUI元素需要重建")
# 使用gui_manager重新创建GUI元素
self._recreateGUIElementsFromData(gui_data)
else:
print(" GUI信息文件为空")
except json.JSONDecodeError as e:
print(f"✗ GUI信息文件格式错误: {e}")
except Exception as e:
print(f"✗ 加载GUI信息失败: {e}")
import traceback
traceback.print_exc()
else:
print(" 未找到GUI信息文件")
# 移除临时场景节点
if not scene.isEmpty():
scene.removeNode()
# 更新场景树
#self.updateSceneTree()
#self._get_tree_widget().create_model_items(scene)
print(f"加载完成GUI元素数量: {len(self.world.gui_elements)}")
if len(self.world.gui_elements) > 0:
print("GUI元素列表:")
for i, elem in enumerate(self.world.gui_elements):
print(
f" {i + 1}. {elem.getName()} (类型: {elem.getTag('gui_type') if elem.hasTag('gui_type') else 'unknown'})")
print("=== 场景加载完成 ===\n")
return True
except Exception as e:
print(f"加载场景时发生错误: {str(e)}")
import traceback
traceback.print_exc()
return False
def _rebuildParentChildRelationships(self, loaded_nodes):
try:
parent_child_relations = []
for node_name, node in loaded_nodes.items():
if node.hasTag("parent_name"):
parent_name = node.getTag("parent_name")
if parent_name in loaded_nodes:
parent_child_relations.append((node, loaded_nodes[parent_name])) # 修复:应该是元组
print(f"发现父子关系:{parent_name}->{node_name}")
else:
print(f"警告:节点{node_name}的父节点{parent_name}不存在")
for child_node, parent_node in parent_child_relations:
try:
child_node.wrtReparentTo(parent_node)
print(f"成功设置父子关系:{parent_node.getName()}->{child_node.getName()}")
except Exception as e:
print(f"设置父子关系失败{parent_node.getName()}->{child_node.getName()}:{e}")
if not parent_child_relations:
print("尝试从场景结构推断父子关系")
self._inferParentChildRelationships(loaded_nodes)
print("父子关系重建完成")
except Exception as e:
print(f"重建父子关系时出错: {e}")
import traceback
traceback.print_exc()
except Exception as e:
print(f"重建父子关系时出错: {e}")
import traceback
traceback.print_exc()
def _inferParentChildRelationships(self, loaded_nodes):
"""从场景结构推断父子关系"""
try:
# 这里可以添加更复杂的父子关系推断逻辑
# 例如,根据节点名称、位置关系等进行推断
# 目前保持简单,后续可以扩展
print("父子关系推断完成(当前为空实现)")
except Exception as e:
print(f"推断父子关系时出错: {e}")
def _shouldSkipNodeInTree(self, nodePath):
"""判断节点是否应该在场景树中跳过显示"""
if nodePath.getName().startswith('ground'):
return True
# 跳过render节点的递归
if nodePath.getName() == "render":
return True
# 跳过光源节点
if nodePath.getName() in ["alight", "dlight"]:
return True
# 跳过相机节点
if nodePath.getName() in ["camera", "cam"]:
return True
# 跳过3D文本和3D图像节点
if (hasattr(nodePath.node(), "hasTag") and
nodePath.node().hasTag("gui_type") and
nodePath.node().getTag("gui_type") in ["3d_text", "3d_image"]):
return True
# 跳过辅助节点
if nodePath.getName().startswith(("gizmo", "selectionBox")):
return True
return False
def _recreateGUIElementsFromData(self, gui_data):
"""根据保存的GUI数据重新创建GUI元素"""
try:
gui_manager = getattr(self.world, 'gui_manager', None)
property_manager = getattr(self.world, 'property_panel', None)
info_panel_manager = getattr(self.world, 'info_panel_manager', None)
if not gui_manager:
print("GUI管理器未找到无法重建GUI元素")
return
print(f"开始重建 {len(gui_data)} 个GUI元素...")
processed_names = set()
created_elements = {}
# 存储原始的缩放和位置信息,用于后续计算
element_original_data = {}
# 第一遍:收集所有元素信息
for i, gui_info in enumerate(gui_data):
name = gui_info.get("name", f"gui_element_{i}")
element_original_data[name] = {
"scale": gui_info.get("scale", [1, 1, 1]),
"position": gui_info.get("position", [0, 0, 0]),
"parent_name": gui_info.get("parent_name")
}
valid_parents = set()
for gui_info in gui_data:
name = gui_info.get("name", f"gui_element_{gui_info.get('index', 0)}")
valid_parents.add(name)
if hasattr(self.world, 'gui_elements'):
for elem in self.world.gui_elements:
if elem and not elem.isEmpty():
valid_parents.add(elem.getName())
valid_parents.add("render")
valid_parents.add("aspect2d")
valid_parents.add("render2d")
pos = (0, 0, 0)
for i, gui_info in enumerate(gui_data):
try:
gui_type = gui_info.get("type", "unknown")
name = gui_info.get("name", f"gui_element_{i}")
position = gui_info.get("position", [0, 0, 0])
scale = gui_info.get("scale", [1, 1, 1])
tags = gui_info.get("tags", {})
text = gui_info.get("text", "")
image_path = gui_info.get("image_path", "")
video_path = gui_info.get("video_path", "")
bg_image_path = gui_info.get("bg_image_path", "") # 背景图片路径
panel_id = gui_info.get("panel_id", name) # 信息面板ID
panel_data = gui_info.get("panel_data", None) # 面板数据
parent_name = gui_info.get("parent_name")
# 检查是否已经处理过同名元素
if name in processed_names:
print(f"跳过重复元素: {name}")
continue
if parent_name and parent_name not in valid_parents:
print(f"⚠️ 跳过元素 {name},因为其父级 {parent_name} 不存在")
continue
processed_names.add(name)
print(f"重建GUI元素: {name} (类型: {gui_type})")
print(f" 位置: {position}")
print(f" 缩放: {scale}")
print(f" 文本: {text}")
print(f" 图像路径: {image_path}")
print(f" 背景图片路径: {bg_image_path}")
print(f" 视频路径: {video_path}")
absolute_position = list(position)
absolute_scale = list(scale)
if parent_name and parent_name in element_original_data:
parent_data = element_original_data[parent_name]
parent_scale = parent_data["scale"]
if gui_type in ["3d_text", "3d_image", "button", "label", "entry", "2d_image",
"2d_video_screen"]:
# 位置需要乘以父级缩放来得到绝对位置
for j in range(min(len(absolute_position), len(parent_scale))):
absolute_position[j] *= parent_scale[j] if len(parent_scale) > j else parent_scale[0]
# 缩放需要乘以父级缩放来得到绝对缩放
for j in range(min(len(absolute_scale), len(parent_scale))):
absolute_scale[j] *= parent_scale[j] if len(parent_scale) > j else parent_scale[0]
print(f" 绝对位置: {absolute_position}")
print(f" 绝对缩放: {absolute_scale}")
# 根据类型创建相应的GUI元素
new_element = None
if gui_type == "button" and hasattr(gui_manager, 'createGUIButton'):
new_element = gui_manager.createGUIButton(
pos=tuple(absolute_position),
text=text,
size=absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 1.0
)
elif gui_type == "label" and hasattr(gui_manager, 'createGUILabel'):
scale_value = absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 1.0
new_element = gui_manager.createGUILabel(
pos=tuple(absolute_position),
text=text,
size=scale_value
)
elif gui_type == "entry" and hasattr(gui_manager, 'createGUIEntry'):
new_element = gui_manager.createGUIEntry(
pos=tuple(absolute_position),
placeholder=text,
size=absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 1.0
)
elif gui_type == "2d_image" and hasattr(gui_manager, 'createGUI2DImage'):
new_element = gui_manager.createGUI2DImage(
pos=tuple(absolute_position),
image_path=image_path,
size=(0.8,0.8,0.8)
)
elif gui_type == "3d_text" and hasattr(gui_manager, 'createGUI3DText'):
size = absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 0.5
new_element = gui_manager.createGUI3DText(
pos=tuple(absolute_position),
text=text,
size=absolute_scale
)
elif gui_type == "3d_image" and hasattr(gui_manager, 'createGUI3DImage'):
# 处理3D图像
# 根据缩放值的数量处理尺寸
size = (absolute_scale[0] * 0.2, absolute_scale[1] * 0.2, absolute_scale[2] * 0.2)
new_element = gui_manager.createGUI3DImage(
pos=tuple(absolute_position),
image_path=image_path,
size=size
)
elif gui_type == "video_screen" and hasattr(gui_manager, 'createVideoScreen'):
print(f"重建的3d视频屏幕视频地址是{video_path}")
new_element = gui_manager.createVideoScreen(
pos=tuple(absolute_position),
size=absolute_scale,
video_path=video_path
)
if video_path and new_element:
if video_path.startswith("http://") or video_path.startswith("https://"):
pass
else:
if hasattr(gui_manager, 'loadVideoFile'):
from direct.task.TaskManagerGlobal import taskMgr
def load_video_file_task(task):
gui_manager.loadVideoFile(new_element, video_path)
return task.done
taskMgr.doMethodLater(0.1, load_video_file_task, 'loadVideoFileTask')
elif gui_type == "2d_video_screen" and hasattr(gui_manager, 'createGUI2DVideoScreen'):
print(f"重建的2d视频屏幕视频地址是{video_path}")
new_element = gui_manager.createGUI2DVideoScreen(
pos=tuple(absolute_position),
size=absolute_scale,
video_path=video_path
)
if video_path and new_element:
if video_path.startswith("http://") or video_path.startswith("https://"):
pass
else:
if hasattr(property_manager, 'load2DVideoFile'):
from direct.task.TaskManagerGlobal import taskMgr
def load_2d_video_file_task(task):
property_manager.load2DVideoFile(new_element, video_path)
return task.done
taskMgr.doMethodLater(0.1, load_2d_video_file_task, 'load2DVideoFileTask')
elif gui_type == "info_panel":
new_element = self.world.info_panel_manager.onCreateSampleInfoPanel()
# 如果创建成功,设置属性
if new_element:
# 如果返回的是列表(多选创建),取第一个
if isinstance(new_element, list):
new_element = new_element[0]
# 设置名称
new_element.setName(name)
# 设置变换
new_element.setPos(*position)
if len(scale) >= 3:
new_element.setScale(scale[0], scale[1], scale[2])
elif len(scale) >= 1:
new_element.setScale(scale[0])
# 设置标签
# 对于NodePath对象
if hasattr(new_element, 'setTag'):
for tag_name, tag_value in tags.items():
# 跳过变换标签,因为我们已经设置了
if tag_name not in ["transform_pos", "transform_hpr", "transform_scale"]:
new_element.setTag(tag_name, tag_value)
# 对于DirectGUI对象使用自定义标签存储
elif hasattr(new_element, '_tags'):
new_element._tags.update(tags)
created_elements[name] = new_element
print(f"GUI元素重建成功: {name}")
else:
print(f"无法重建GUI元素: {name} (类型: {gui_type})")
except Exception as e:
print(f"重建GUI元素失败 {name}: {e}")
import traceback
traceback.print_exc()
continue
# 第二遍设置父子级关系并更新Qt树
print("开始设置父子级关系...")
try:
# 创建父子级关系映射
parent_child_map = {}
for gui_info in gui_data:
name = gui_info.get("name")
parent_name = gui_info.get("parent_name")
if name and parent_name and parent_name in created_elements:
parent_child_map[name] = parent_name
print(f"父子级关系映射: {parent_name} -> {name}")
# 按正确的顺序设置父子级关系并更新Qt树
tree_widget = self._get_tree_widget()
if tree_widget:
# 先将所有元素添加到Qt树中
qt_tree_items = {}
for name, element in created_elements.items():
# 尝试在Qt树中找到对应的项如果找不到则创建
qt_item = self._findOrCreateQtTreeItem(tree_widget, element, name)
if qt_item:
qt_tree_items[name] = qt_item
# 然后设置父子级关系
for child_name, parent_name in parent_child_map.items():
try:
if child_name in created_elements and parent_name in created_elements:
child_element = created_elements[child_name]
parent_element = created_elements[parent_name]
# 设置父子级关系
if hasattr(child_element, 'reparentTo'):
child_element.reparentTo(parent_element)
print(f"成功设置父子级关系: {parent_name} -> {child_name}")
# 更新Qt树显示
if child_name in qt_tree_items and parent_name in qt_tree_items:
child_item = qt_tree_items[child_name]
parent_item = qt_tree_items[parent_name]
# 从当前位置移除子项
if child_item.parent():
child_item.parent().removeChild(child_item)
else:
# 如果是顶级项,从树中移除
index = tree_widget.indexOfTopLevelItem(child_item)
if index >= 0:
tree_widget.takeTopLevelItem(index)
# 将子项添加到新的父项下
parent_item.addChild(child_item)
print(f"Qt树更新: {child_name} 移动到 {parent_name}")
else:
print(f"元素 {child_name} 不支持 reparentTo 操作")
else:
print(f"元素未找到: 父级={parent_name}, 子级={child_name}")
except Exception as e:
print(f"设置父子级关系失败 {parent_name} -> {child_name}: {e}")
continue
else:
# 如果没有tree_widget只设置父子级关系
for child_name, parent_name in parent_child_map.items():
try:
if child_name in created_elements and parent_name in created_elements:
child_element = created_elements[child_name]
parent_element = created_elements[parent_name]
# 设置父子级关系
if hasattr(child_element, 'reparentTo'):
child_element.reparentTo(parent_element)
print(f"成功设置父子级关系: {parent_name} -> {child_name}")
except Exception as e:
print(f"设置父子级关系失败 {parent_name} -> {child_name}: {e}")
continue
except Exception as e:
print(f"设置父子级关系时出错: {e}")
# 第三遍:重新挂载脚本
print("开始重新挂载脚本...")
for gui_info in gui_data:
try:
name = gui_info.get("name")
if name in created_elements and "scripts" in gui_info:
new_element = created_elements[name]
# 重新挂载脚本(如果有的话)
if "scripts" in gui_info and hasattr(self.world,
'script_manager') and self.world.script_manager:
script_manager = self.world.script_manager
for script_info in gui_info["scripts"]:
script_name = script_info["name"]
script_file = script_info.get("file", "")
print(f"尝试重新挂载脚本: {script_name} from {script_file}")
# 检查脚本是否已加载
if script_name not in script_manager.loader.script_classes:
# 如果脚本未加载,尝试从保存的文件路径加载
if script_file and os.path.exists(script_file):
print(f"从文件加载脚本: {script_file}")
loaded_class = script_manager.load_script_from_file(script_file)
if loaded_class is None:
print(f"从文件加载脚本失败: {script_file}")
# 如果从文件加载失败,尝试在脚本目录中查找
script_path = self._find_script_in_directory(script_name)
if script_path:
print(f"从目录找到脚本并加载: {script_path}")
script_manager.load_script_from_file(script_path)
else:
# 如果没有文件路径或文件不存在,尝试在脚本目录中查找
script_path = self._find_script_in_directory(script_name)
if script_path:
print(f"从目录找到脚本并加载: {script_path}")
script_manager.load_script_from_file(script_path)
else:
print(f"找不到脚本文件: {script_name}")
# 为元素添加脚本
script_component = script_manager.add_script_to_object(new_element, script_name)
if script_component:
print(f"成功为 {name} 添加脚本: {script_name}")
else:
print(f"{name} 添加脚本失败: {script_name}")
except Exception as e:
print(f"重新挂载脚本失败: {e}")
import traceback
traceback.print_exc()
continue
print(f"GUI元素重建完成共创建 {len(created_elements)} 个元素")
except Exception as e:
print(f"重建GUI元素时出错: {e}")
import traceback
traceback.print_exc()
def _findOrCreateQtTreeItem(self, tree_widget, target_element, element_name):
"""在Qt树中查找或创建指定元素对应的项"""
try:
# 首先尝试查找现有的项
existing_item = self._findQtTreeItem(tree_widget, target_element)
if existing_item:
return existing_item
# 如果找不到,创建新的项
# 找到场景根节点
scene_root = None
for i in range(tree_widget.topLevelItemCount()):
top_item = tree_widget.topLevelItem(i)
if top_item.data(0, Qt.UserRole + 1) == "SCENE_ROOT":
scene_root = top_item
break
if not scene_root:
print("无法找到场景根节点")
return None
# 创建新的Qt树项
new_item = QTreeWidgetItem(scene_root, [element_name])
new_item.setData(0, Qt.UserRole, target_element)
new_item.setData(0, Qt.UserRole + 1, "SCENE_NODE") # 或根据元素类型设置适当的类型
print(f"为元素 {element_name} 创建了新的Qt树项")
return new_item
except Exception as e:
print(f"查找或创建Qt树项失败: {e}")
return None
def _findQtTreeItem(self, tree_widget, target_element):
"""在Qt树中查找指定元素对应的项"""
try:
def search_recursive(parent_item):
# 检查当前项
if parent_item:
item_element = parent_item.data(0, Qt.UserRole)
if item_element == target_element:
return parent_item
# 递归检查子项
for i in range(parent_item.childCount()):
child_item = parent_item.child(i)
result = search_recursive(child_item)
if result:
return result
return None
# 从根节点开始搜索
root = tree_widget.invisibleRootItem()
for i in range(root.childCount()):
top_item = root.child(i)
result = search_recursive(top_item)
if result:
return result
return None
except Exception as e:
print(f"查找Qt树项失败: {e}")
return None
def _find_script_in_directory(self, script_name):
"""在脚本目录中查找脚本文件"""
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:
return os.path.join(scripts_dir, file_name)
# 如果没有精确匹配,尝试模糊匹配
for file_name in os.listdir(scripts_dir):
if file_name.endswith('.py'):
base_name = os.path.splitext(file_name)[0]
if script_name.lower() in base_name.lower() or base_name.lower() in script_name.lower():
return os.path.join(scripts_dir, file_name)
except Exception as e:
print(f"查找脚本文件时出错: {e}")
return None
def _recreateSpotLight(self, light_node):
"""重新创建聚光灯"""
try:
from RenderPipelineFile.rpcore import SpotLight
from panda3d.core import Vec3
# 创建聚光灯对象
light = SpotLight()
light.direction = Vec3(0, 0, -1)
light.fov = 70
light.set_color_from_temperature(5 * 1000.0)
# 恢复保存的属性
if light_node.hasTag("light_energy"):
light.energy = float(light_node.getTag("light_energy"))
else:
light.energy = 5000
light.radius = 1000
light.casts_shadows = True
light.shadow_map_resolution = 256
light_pos = light_node.getPos()
light.setPos(light_pos)
# 添加到渲染管线
render_pipeline = get_render_pipeline()
render_pipeline.add_light(light)
# 保存光源对象引用
light_node.setPythonTag("rp_light_object", light)
# 添加到管理列表
self.Spotlight.append(light_node)
# 确保灯光节点有正确的标签,以便在场景树更新时被识别
if not light_node.hasTag("is_scene_element"):
light_node.setTag("is_scene_element", "1")
print(f"重新创建聚光灯: {light_node.getName()}")
except Exception as e:
print(f"重新创建聚光灯失败: {str(e)}")
import traceback
traceback.print_exc()
def _recreatePointLight(self, light_node):
"""重新创建点光源"""
try:
from RenderPipelineFile.rpcore import PointLight
from QPanda3D.Panda3DWorld import get_render_pipeline
# 创建点光源对象
light = PointLight()
# 恢复保存的属性
if light_node.hasTag("light_energy"):
light.energy = float(light_node.getTag("light_energy"))
else:
light.energy = 5000
light.radius = 1000
light.inner_radius = 0.4
light.set_color_from_temperature(5 * 1000.0)
light.casts_shadows = True
light.shadow_map_resolution = 256
# 设置位置
light.setPos(light_node.getPos())
# 添加到渲染管线
render_pipeline = get_render_pipeline()
render_pipeline.add_light(light)
# 保存光源对象引用
light_node.setPythonTag("rp_light_object", light)
# 添加到管理列表
self.Pointlight.append(light_node)
# 确保灯光节点有正确的标签,以便在场景树更新时被识别
if not light_node.hasTag("is_scene_element"):
light_node.setTag("is_scene_element", "1")
print(f"重新创建点光源: {light_node.getName()}")
except Exception as e:
print(f"重新创建点光源失败: {str(e)}")
import traceback
traceback.print_exc()
def _cleanupAuxiliaryNodes(self):
"""清理场景中可能存在的辅助节点"""
try:
# 查找并移除所有坐标轴节点
gizmo_nodes = self.world.render.findAllMatches("**/gizmo*")
for node in gizmo_nodes:
if not node.isEmpty():
node.removeNode()
print(f"清理坐标轴节点: {node.getName()}")
# 查找并移除所有选择框节点
selection_box_nodes = self.world.render.findAllMatches("**/selectionBox*")
for node in selection_box_nodes:
if not node.isEmpty():
node.removeNode()
print(f"清理选择框节点: {node.getName()}")
# 停止相关的更新任务
from direct.task.TaskManagerGlobal import taskMgr
taskMgr.remove("updateGizmo")
taskMgr.remove("updateSelectionBox")
print("辅助节点清理完成")
except Exception as e:
print(f"清理辅助节点时出错: {e}")
# ==================== 模型管理 ====================
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
# 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)):
"""创建聚光灯 - 支持多选创建,优化版本"""
try:
from RenderPipelineFile.rpcore import SpotLight
from QPanda3D.Panda3DWorld import get_render_pipeline
from panda3d.core import Vec3, NodePath
from PyQt5.QtCore import Qt
print(f"🔆 开始创建聚光灯,位置: {pos}")
# 获取树形控件
tree_widget = self._get_tree_widget()
if not tree_widget:
print("❌ 无法访问树形控件")
return None
# 获取目标父节点列表
target_parents = tree_widget.get_target_parents_for_creation()
if not target_parents:
print("❌ 没有找到有效的父节点")
return None
created_lights = []
render_pipeline = get_render_pipeline()
# 为每个有效的父节点创建聚光灯
for parent_item, parent_node in target_parents:
try:
# 生成唯一名称
light_name = f"Spotlight_{len(self.Spotlight)}"
# 创建挂载节点 - 挂载到选中的父节点
light_np = NodePath(light_name)
light_np.reparentTo(parent_node) # 挂载到父节点而不是render
light_np.setPos(*pos)
light_np.setTransform(TransformState.makeIdentity())
# 创建聚光灯对象
light = SpotLight()
light.direction = Vec3(0, 0, -1)
light.fov = 70
light.set_color_from_temperature(5 * 1000.0)
light.energy = 5000
light.radius = 1000
light.casts_shadows = True
light.shadow_map_resolution = 256
light.setPos(pos)
# 添加到渲染管线
render_pipeline.add_light(light)
# 设置节点属性和标签
light_np.setTag("light_type", "spot_light")
light_np.setTag("is_scene_element", "1")
light_np.setTag("tree_item_type", "LIGHT_NODE")
light_np.setTag("light_energy", str(light.energy))
light_np.setTag("created_by_user", "1")
# 保存光源对象引用
light_np.setPythonTag("rp_light_object", light)
# 添加到管理列表
self.Spotlight.append(light_np)
print(f"✅ 为 {parent_item.text(0)} 创建聚光灯成功: {light_name}")
# 在Qt树形控件中添加对应节点
qt_item = tree_widget.add_node_to_tree_widget(light_np, parent_item, "LIGHT_NODE")
if qt_item:
created_lights.append((light_np, qt_item))
else:
created_lights.append((light_np, None))
print("⚠️ Qt树节点添加失败但Panda3D对象已创建")
except Exception as e:
print(f"❌ 为 {parent_item.text(0)} 创建聚光灯失败: {str(e)}")
import traceback
traceback.print_exc()
continue
# 处理创建结果
if not created_lights:
print("❌ 没有成功创建任何聚光灯")
return None
# 选中最后创建的光源
if created_lights:
last_light_np, last_qt_item = created_lights[-1]
if last_qt_item:
tree_widget.setCurrentItem(last_qt_item)
# 更新选择和属性面板
tree_widget.update_selection_and_properties(last_light_np, last_qt_item)
print(f"🎉 总共创建了 {len(created_lights)} 个聚光灯")
# 返回值处理
if len(created_lights) == 1:
return created_lights[0][0] # 单个光源返回NodePath
else:
return [light_np for light_np, _ in created_lights] # 多个光源返回列表
except Exception as e:
print(f"❌ 创建聚光灯过程失败: {str(e)}")
import traceback
traceback.print_exc()
return None
def createPointLight(self, pos=(0, 0, 0)):
"""创建点光源 - 支持多选创建,优化版本"""
try:
from RenderPipelineFile.rpcore import PointLight
from QPanda3D.Panda3DWorld import get_render_pipeline
from panda3d.core import Vec3, NodePath
from PyQt5.QtCore import Qt
print(f"💡 开始创建点光源,位置: {pos}")
# 获取树形控件
tree_widget = self._get_tree_widget()
if not tree_widget:
print("❌ 无法访问树形控件")
return None
# 获取目标父节点列表
target_parents = tree_widget.get_target_parents_for_creation()
if not target_parents:
print("❌ 没有找到有效的父节点")
return None
created_lights = []
render_pipeline = get_render_pipeline()
# 为每个有效的父节点创建点光源
for parent_item, parent_node in target_parents:
try:
# 生成唯一名称
light_name = f"Pointlight_{len(self.Pointlight)}"
# 创建挂载节点 - 挂载到选中的父节点
light_np = NodePath(light_name)
light_np.reparentTo(parent_node) # 挂载到父节点而不是render
light_np.setPos(*pos)
# 确保变换矩阵有效
light_np.setTransform(TransformState.makeIdentity())
# 创建点光源对象
light = PointLight()
light.setPos(*pos)
light.energy = 5000
light.radius = 1000
light.inner_radius = 0.4
light.set_color_from_temperature(5 * 1000.0)
light.casts_shadows = True
light.shadow_map_resolution = 256
# 添加到渲染管线
render_pipeline.add_light(light)
# 设置节点属性和标签
light_np.setTag("light_type", "point_light")
light_np.setTag("is_scene_element", "1")
light_np.setTag("tree_item_type", "LIGHT_NODE")
light_np.setTag("light_energy", str(light.energy))
light_np.setTag("created_by_user", "1")
# 保存光源对象引用
light_np.setPythonTag("rp_light_object", light)
# 添加到管理列表
self.Pointlight.append(light_np)
print(f"✅ 为 {parent_item.text(0)} 创建点光源成功: {light_name}")
# 在Qt树形控件中添加对应节点
qt_item =tree_widget.add_node_to_tree_widget(light_np, parent_item, "LIGHT_NODE")
if qt_item:
created_lights.append((light_np, qt_item))
else:
created_lights.append((light_np, None))
print("⚠️ Qt树节点添加失败但Panda3D对象已创建")
except Exception as e:
print(f"❌ 为 {parent_item.text(0)} 创建点光源失败: {str(e)}")
continue
# 处理创建结果
if not created_lights:
print("❌ 没有成功创建任何点光源")
return None
# 选中最后创建的光源
if created_lights:
last_light_np, last_qt_item = created_lights[-1]
if last_qt_item:
tree_widget.setCurrentItem(last_qt_item)
# 更新选择和属性面板
tree_widget.update_selection_and_properties(last_light_np, last_qt_item)
print(f"🎉 总共创建了 {len(created_lights)} 个点光源")
# 返回值处理
if len(created_lights) == 1:
return created_lights[0][0] # 单个光源返回NodePath
else:
return [light_np for light_np, _ in created_lights] # 多个光源返回列表
except Exception as e:
print(f"❌ 创建点光源过程失败: {str(e)}")
import traceback
traceback.print_exc()
return None
def _get_tree_widget(self):
"""安全获取树形控件"""
try:
if (hasattr(self.world, 'interface_manager') and
hasattr(self.world.interface_manager, 'treeWidget')):
return self.world.interface_manager.treeWidget
except AttributeError:
pass
return None
# ==================== 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
def load_cesium_tileset(self, tileset_url, position=(0, 0, 0)):
"""
加载 Cesium 3D Tileset - 采用新的创建逻辑支持多选和更完善的UI交互。
"""
try:
from panda3d.core import NodePath
print(f"🗺️ 开始加载 Cesium 3D Tiles: {tileset_url}")
# 1. 获取UI控件和目标父节点
tree_widget = self._get_tree_widget()
if not tree_widget:
print("❌ 无法访问树形控件")
return None
target_parents = tree_widget.get_target_parents_for_creation()
if not target_parents:
print("❌ 没有找到有效的父节点来附加Tileset")
return None
created_tilesets = []
# 2. 遍历所有选中的父节点并为其创建Tileset
for parent_item, parent_node in target_parents:
try:
# 生成唯一名称
node_name = f"cesium_tileset_{len(self.tilesets)}"
# 创建一个容器节点来管理tileset并挂载到父节点
tileset_node = parent_node.attachNewNode(node_name)
tileset_node.setPos(*position)
# 添加标签以便场景识别和保存
tileset_node.setTag("is_scene_element", "1")
tileset_node.setTag("tree_item_type", "CESIUM_TILESET_NODE")
tileset_node.setTag("element_type", "cesium_tileset")
tileset_node.setTag("tileset_url", tileset_url)
# 使用唯一名称作为文件标识,代替索引
tileset_node.setTag("file", node_name)
# 存储tileset核心信息
tileset_info = {
'url': tileset_url,
'node': tileset_node,
'position': position,
'tiles': {} # 用于后续管理瓦片
}
self.tilesets.append(tileset_info)
# 创建一个临时的可视化占位符,让用户能看到节点已添加
self._create_placeholder_geometry(tileset_node)
# 异步加载tileset的实际数据
self._load_tileset_async(tileset_url, tileset_info)
print(f"✅ 为 {parent_item.text(0)} 加载 Tileset 成功: {node_name}")
# 在Qt树形控件中添加对应节点
qt_item = tree_widget.add_node_to_tree_widget(tileset_node, parent_item, "CESIUM_TILESET_NODE")
if qt_item:
created_tilesets.append((tileset_node, qt_item))
else:
created_tilesets.append((tileset_node, None))
print("⚠️ Qt树节点添加失败但Panda3D对象已创建")
except Exception as e:
print(f"❌ 为 {parent_item.text(0)} 加载 Tileset 失败: {str(e)}")
continue # 继续尝试为下一个父节点创建
# 3. 处理创建结果
if not created_tilesets:
print("❌ 没有成功加载任何 Tileset")
return None
# 选中最后创建的Tileset并更新UI
if created_tilesets:
last_tileset_node, last_qt_item = created_tilesets[-1]
if last_qt_item:
tree_widget.setCurrentItem(last_qt_item)
# 更新选择状态和属性面板
tree_widget.update_selection_and_properties(last_tileset_node, last_qt_item)
print(f"🎉 总共加载了 {len(created_tilesets)} 个 Cesium Tileset 实例")
# 4. 返回值处理
if len(created_tilesets) == 1:
return created_tilesets[0][0] # 单个实例返回NodePath
else:
return [node for node, _ in created_tilesets] # 多个实例返回NodePath列表
except Exception as e:
print(f"❌ 加载 Cesium 3D Tiles 过程失败: {str(e)}")
import traceback
traceback.print_exc()
return None
def _load_tileset_async(self, tileset_url, tileset_info):
"""异步加载 tileset 数据"""
async def load_tileset():
try:
async with aiohttp.ClientSession() as session:
async with session.get(tileset_url) as response:
if response.status == 200:
tileset_data = await response.json()
self._parse_tileset(tileset_data, tileset_info)
print(f"✓ Tileset 数据加载完成")
else:
print(f"✗ Tileset 加载失败: {response.status}")
except Exception as e:
print(f"✗ Tileset 加载出错: {e}")
# 在 Panda3D 的任务系统中运行异步任务
task = asyncio.ensure_future(load_tileset())
self._current_asyncio_task = task # 保存任务引用
self.world.taskMgr.add(self._check_async_task, "check_tileset_load", appendTask=True)
def _check_async_task(self, panda3d_task):
# 检查 asyncio 任务是否完成
if hasattr(self, '_current_asyncio_task'):
if self._current_asyncio_task.done():
try:
self._current_asyncio_task.result()
except Exception as e:
print(f"异步任务出错:{e}")
# 返回 Panda3D 任务管理器的完成状态
return panda3d_task.done # 注意是 done 而不是 DONE
# 返回 Panda3D 任务管理器的继续状态
return panda3d_task.cont # 注意是 cont 而不是 CONTINUE
def _parse_tileset(self,tileset_data,tileset_info):
try:
root = tileset_data.get('root',{})
self._parse_tile(root,tileset_info['node'],tileset_info)
print("✓ Tileset 解析完成")
except Exception as e:
print(f"✗ Tileset 解析出错: {e}")
def _parse_tile(self, tile_data, parent_node, tileset_info):
try:
# 获取tileID
tile_id = f"tile_{len(tileset_info['tiles'])}"
print(f"创建tile节点: {tile_id}")
# 创建tile节点
tile_node = parent_node.attachNewNode(tile_id)
tileset_info['tiles'][tile_id] = {
'node': tile_node,
'data': tile_data,
'loaded': False
}
# 如果有内容,创建占位几何体
if 'content' in tile_data:
print(f"为tile {tile_id} 创建几何体")
self._create_tile_geometry(tile_node)
# 递归解析子tiles
children = tile_data.get('children', [])
print(f"Tile {tile_id}{len(children)} 个子节点")
for child_data in children:
self._parse_tile(child_data, tile_node, tileset_info)
except Exception as e:
print(f"✗ Tile 解析出错: {e}")
import traceback
traceback.print_exc()
def _create_tile_geometry(self,parent_node):
"""为 tile 创建占位几何体"""
try:
# 创建一个简单的立方体作为占位符
from panda3d.core import GeomVertexFormat, GeomVertexData, GeomVertexWriter
from panda3d.core import Geom, GeomTriangles, GeomNode
format = GeomVertexFormat.getV3n3c4()
vdata = GeomVertexData('tile_cube', format, Geom.UHStatic)
vertex = GeomVertexWriter(vdata, 'vertex')
normal = GeomVertexWriter(vdata, 'normal')
color = GeomVertexWriter(vdata, 'color')
# 定义立方体顶点
vertices = [
(-0.5, -0.5, -0.5), (0.5, -0.5, -0.5), (0.5, 0.5, -0.5), (-0.5, 0.5, -0.5),
(-0.5, -0.5, 0.5), (0.5, -0.5, 0.5), (0.5, 0.5, 0.5), (-0.5, 0.5, 0.5)
]
for vert in vertices:
vertex.addData3f(*vert)
normal.addData3f(0, 0, 1)
color.addData4f(0.2, 0.6, 0.8, 1.0)
# 创建几何体
geom = Geom(vdata)
# 创建面
prim = GeomTriangles(Geom.UHStatic)
# 底面
prim.addVertices(0, 1, 2)
prim.addVertices(0, 2, 3)
# 顶面
prim.addVertices(4, 7, 6)
prim.addVertices(4, 6, 5)
# 前面
prim.addVertices(0, 4, 5)
prim.addVertices(0, 5, 1)
# 后面
prim.addVertices(2, 6, 7)
prim.addVertices(2, 7, 3)
# 左面
prim.addVertices(0, 3, 7)
prim.addVertices(0, 7, 4)
# 右面
prim.addVertices(1, 5, 6)
prim.addVertices(1, 6, 2)
prim.closePrimitive()
geom.addPrimitive(prim)
# 创建几何节点
geom_node = GeomNode('tile_geometry')
geom_node.addGeom(geom)
# 添加到场景
cube_node = parent_node.attachNewNode(geom_node)
cube_node.setScale(1000) # 放大以便观察
# 添加材质
material = Material()
material.setBaseColor((0.2, 0.6, 0.8, 1.0))
material.setSpecular((0.1, 0.1, 0.1, 1.0))
material.setShininess(10.0)
cube_node.setMaterial(material)
except Exception as e:
print(f"✗ 创建 tile 几何体出错: {e}")
def _create_placeholder_geometry(self, parent_node):
"""创建一个简单的占位符几何体,让用户能看到节点"""
try:
from panda3d.core import GeomVertexFormat, GeomVertexData, GeomVertexWriter
from panda3d.core import Geom, GeomTriangles, GeomNode
# 创建简单的立方体作为占位符
format = GeomVertexFormat.getV3n3c4()
vdata = GeomVertexData('placeholder_cube', format, Geom.UHStatic)
vertex = GeomVertexWriter(vdata, 'vertex')
normal = GeomVertexWriter(vdata, 'normal')
color = GeomVertexWriter(vdata, 'color')
# 定义立方体顶点
size = 1.0
vertices = [
# 前面 (Z+)
(-size, -size, size), (size, -size, size), (size, size, size), (-size, size, size),
# 后面 (Z-)
(-size, -size, -size), (-size, size, -size), (size, size, -size), (size, -size, -size),
# 左面 (X-)
(-size, -size, -size), (-size, -size, size), (-size, size, size), (-size, size, -size),
# 右面 (X+)
(size, -size, -size), (size, size, -size), (size, size, size), (size, -size, size),
# 上面 (Y+)
(-size, size, -size), (-size, size, size), (size, size, size), (size, size, -size),
# 下面 (Y-)
(-size, -size, -size), (size, -size, -size), (size, -size, size), (-size, -size, size)
]
normals = [
# 前面法线
(0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1),
# 后面法线
(0, 0, -1), (0, 0, -1), (0, 0, -1), (0, 0, -1),
# 左面法线
(-1, 0, 0), (-1, 0, 0), (-1, 0, 0), (-1, 0, 0),
# 右面法线
(1, 0, 0), (1, 0, 0), (1, 0, 0), (1, 0, 0),
# 上面法线
(0, 1, 0), (0, 1, 0), (0, 1, 0), (0, 1, 0),
# 下面法线
(0, -1, 0), (0, -1, 0), (0, -1, 0), (0, -1, 0)
]
# 青色
face_colors = [
(0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), # 前面 - 青色
(0.0, 0.8, 0.8, 1.0), (0.0, 0.8, 0.8, 1.0), (0.0, 0.8, 0.8, 1.0), (0.0, 0.8, 0.8, 1.0), # 后面 - 稍暗青色
(0.0, 0.9, 0.9, 1.0), (0.0, 0.9, 0.9, 1.0), (0.0, 0.9, 0.9, 1.0), (0.0, 0.9, 0.9, 1.0), # 左面 - 中等青色
(0.0, 0.7, 0.7, 1.0), (0.0, 0.7, 0.7, 1.0), (0.0, 0.7, 0.7, 1.0), (0.0, 0.7, 0.7, 1.0), # 右面 - 稍暗青色
(0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), # 上面 - 青色
(0.0, 0.6, 0.6, 1.0), (0.0, 0.6, 0.6, 1.0), (0.0, 0.6, 0.6, 1.0), (0.0, 0.6, 0.6, 1.0) # 下面 - 更暗青色
]
for i, vert in enumerate(vertices):
vertex.addData3f(*vert)
normal.addData3f(*normals[i])
color.addData4f(*face_colors[i])
# 创建几何体
geom = Geom(vdata)
# 创建面(每个面两个三角形)
prim = GeomTriangles(Geom.UHStatic)
# 每个面4个顶点生成2个三角形
for face in range(6): # 6个面
base_index = face * 4
# 第一个三角形
prim.addVertices(base_index, base_index + 1, base_index + 2)
# 第二个三角形
prim.addVertices(base_index, base_index + 2, base_index + 3)
prim.closePrimitive()
geom.addPrimitive(prim)
# 创建几何节点
geom_node = GeomNode('tileset_placeholder')
geom_node.addGeom(geom)
# 添加到场景
cube_node = parent_node.attachNewNode(geom_node)
cube_node.setScale(5) # 设置合适的大小
# 设置双面渲染
cube_node.setTwoSided(True)
# 添加材质
material = Material()
material.setBaseColor((0.0, 1.0, 1.0, 1.0)) # 青色
material.setSpecular((0.5, 0.5, 0.5, 1.0))
material.setShininess(32.0)
cube_node.setMaterial(material)
# 添加标识标签
cube_node.setTag("element_type", "cesium_placeholder")
print("✓ 占位符几何体创建完成")
return cube_node
except Exception as e:
print(f"✗ 创建占位符几何体出错: {e}")
import traceback
traceback.print_exc()
return None
def serializeNode(self, node):
"""序列化节点为字典数据"""
try:
node_data = {
'name': node.getName(),
'type': type(node.node()).__name__,
'pos': (node.getX(), node.getY(), node.getZ()),
'hpr': (node.getH(), node.getP(), node.getR()),
'scale': (node.getSx(), node.getSy(), node.getSz()),
'tags': {},
'children': []
}
# 保存所有标签
for tag_key in node.getTagKeys():
node_data['tags'][tag_key] = node.getTag(tag_key)
# 特殊处理不同类型的节点
if hasattr(node.node(), 'getClassType'):
node_class = node.node().getClassType().getName()
node_data['node_class'] = node_class
# 递归序列化子节点
for child in node.getChildren():
# 跳过辅助节点
if not child.getName().startswith(('gizmo', 'selectionBox', 'grid')):
child_data = self.serializeNode(child)
if child_data:
node_data['children'].append(child_data)
return node_data
except Exception as e:
print(f"序列化节点 {node.getName()} 失败: {e}")
import traceback
traceback.print_exc()
return None
def deserializeNode(self, node_data, parent_node):
"""从字典数据反序列化节点"""
try:
# 创建新节点
node_name = node_data.get('name', 'node')
new_node = parent_node.attachNewNode(node_name)
# 设置变换
pos = node_data.get('pos', (0, 0, 0))
hpr = node_data.get('hpr', (0, 0, 0))
scale = node_data.get('scale', (1, 1, 1))
new_node.setPos(*pos)
new_node.setHpr(*hpr)
new_node.setScale(*scale)
# 恢复标签
for tag_key, tag_value in node_data.get('tags', {}).items():
new_node.setTag(tag_key, tag_value)
# 根据节点类型进行特殊处理
node_type = node_data.get('type', '')
node_class = node_data.get('node_class', '')
# 特殊处理光源节点
if 'light_type' in node_data.get('tags', {}):
light_type = node_data['tags']['light_type']
if light_type == 'spot_light':
self._recreateSpotLight(new_node)
elif light_type == 'point_light':
self._recreatePointLight(new_node)
# 递归创建子节点
for child_data in node_data.get('children', []):
self.deserializeNode(child_data, new_node)
return new_node
except Exception as e:
print(f"反序列化节点 {node_data.get('name', 'unknown')} 失败: {e}")
import traceback
traceback.print_exc()
return None
def serializeNodeForCopy(self, node):
"""序列化节点用于复制操作,完整保存视觉属性"""
try:
if not node or node.isEmpty():
return None
node_data = {
'name': node.getName(),
'type': type(node.node()).__name__,
'pos': (node.getX(), node.getY(), node.getZ()),
'hpr': (node.getH(), node.getP(), node.getR()),
'scale': (node.getSx(), node.getSy(), node.getSz()),
'tags': {},
'children': []
}
# 保存所有标签
try:
if hasattr(node, 'getTagKeys'):
for tag_key in node.getTagKeys():
node_data['tags'][tag_key] = node.getTag(tag_key)
except Exception as e:
print(f"获取标签时出错: {e}")
# 保存视觉属性
try:
# 保存颜色属性
if hasattr(node, 'getColor'):
color = node.getColor()
node_data['color'] = (color.getX(), color.getY(), color.getZ(), color.getW())
# 保存材质属性
if hasattr(node, 'getMaterial'):
material = node.getMaterial()
if material:
material_data = {}
material_data['base_color'] = (
material.getBaseColor().getX(),
material.getBaseColor().getY(),
material.getBaseColor().getZ(),
material.getBaseColor().getW()
)
material_data['ambient'] = (
material.getAmbient().getX(),
material.getAmbient().getY(),
material.getAmbient().getZ(),
material.getAmbient().getW()
)
material_data['diffuse'] = (
material.getDiffuse().getX(),
material.getDiffuse().getY(),
material.getDiffuse().getZ(),
material.getDiffuse().getW()
)
material_data['specular'] = (
material.getSpecular().getX(),
material.getSpecular().getY(),
material.getSpecular().getZ(),
material.getSpecular().getW()
)
material_data['shininess'] = material.getShininess()
node_data['material'] = material_data
except Exception as e:
print(f"保存视觉属性时出错: {e}")
# 根据节点类型保存特定信息
if node.hasTag("tree_item_type"):
node_type = node.getTag("tree_item_type")
node_data['node_type'] = node_type
# 保存特定类型节点的额外信息
if node_type in ["LIGHT_NODE", "SPOT_LIGHT_NODE", "POINT_LIGHT_NODE"]:
# 保存光源特定信息
rp_light = node.getPythonTag("rp_light_object")
if rp_light:
node_data['light_data'] = {
'energy': getattr(rp_light, 'energy', 5000),
'radius': getattr(rp_light, 'radius', 1000),
'fov': getattr(rp_light, 'fov', 70) if hasattr(rp_light, 'fov') else None,
'inner_radius': getattr(rp_light, 'inner_radius', 0.4) if hasattr(rp_light,
'inner_radius') else None,
'casts_shadows': getattr(rp_light, 'casts_shadows', True) if hasattr(rp_light,
'casts_shadows') else True,
'shadow_map_resolution': getattr(rp_light, 'shadow_map_resolution', 256) if hasattr(
rp_light, 'shadow_map_resolution') else 256
}
elif node_type in ["GUI_BUTTON", "GUI_LABEL", "GUI_ENTRY", "GUI_IMAGE",
"GUI_3D_TEXT", "GUI_3D_IMAGE", "GUI_VIRTUAL_SCREEN"]:
# 保存GUI元素特定信息
node_data['gui_data'] = self._serializeGUIData(node)
elif node_type == "IMPORTED_MODEL_NODE":
# 保存模型特定信息
node_data['model_data'] = self._serializeModelData(node)
return node_data
except Exception as e:
print(f"序列化节点失败: {e}")
import traceback
traceback.print_exc()
return None
def _serializeGUIData(self, node):
"""序列化GUI元素数据"""
try:
gui_data = {}
# 保存GUI相关的通用属性
if node.hasTag("gui_type"):
gui_data['gui_type'] = node.getTag("gui_type")
# 保存文本内容(如果有的话)
if node.hasTag("text"):
gui_data['text'] = node.getTag("text")
# 保存其他GUI相关标签
gui_tags = ['font', 'font_size', 'text_color', 'bg_color', 'size']
for tag in gui_tags:
if node.hasTag(tag):
gui_data[tag] = node.getTag(tag)
return gui_data
except Exception as e:
print(f"序列化GUI数据失败: {e}")
return {}
def _serializeModelData(self, node):
"""序列化模型数据,包括材质信息"""
try:
model_data = {}
# 保存模型相关的标签
model_tags = ['model_path', 'file', 'element_type']
for tag in model_tags:
if node.hasTag(tag):
model_data[tag] = node.getTag(tag)
# 保存材质信息
try:
# 获取模型的材质信息
if hasattr(node, 'getState'):
state = node.getState()
if state:
# 保存基础颜色信息
from panda3d.core import ColorAttrib
color_attrib = state.getColor()
if color_attrib:
model_data['base_color'] = (
color_attrib.getColor().getX(),
color_attrib.getColor().getY(),
color_attrib.getColor().getZ(),
color_attrib.getColor().getW()
)
# 保存其他材质属性
from panda3d.core import MaterialAttrib
material_attrib = state.getAttrib(MaterialAttrib.getClassType())
if material_attrib:
material = material_attrib.getMaterial()
if material:
# 保存基础颜色
base_color = material.getBaseColor()
model_data['material_base_color'] = (
base_color.getX(), base_color.getY(), base_color.getZ(), base_color.getW()
)
# 保存环境光颜色
ambient_color = material.getAmbient()
model_data['material_ambient_color'] = (
ambient_color.getX(), ambient_color.getY(), ambient_color.getZ(),
ambient_color.getW()
)
# 保存漫反射颜色
diffuse_color = material.getDiffuse()
model_data['material_diffuse_color'] = (
diffuse_color.getX(), diffuse_color.getY(), diffuse_color.getZ(),
diffuse_color.getW()
)
# 保存高光颜色
specular_color = material.getSpecular()
model_data['material_specular_color'] = (
specular_color.getX(), specular_color.getY(), specular_color.getZ(),
specular_color.getW()
)
# 保存粗糙度和金属度等参数
model_data['material_roughness'] = material.getRoughness()
model_data['material metallic'] = material.getMetallic()
except Exception as e:
print(f"保存材质信息时出错: {e}")
return model_data
except Exception as e:
print(f"序列化模型数据失败: {e}")
return {}
def recreateNodeFromData(self, node_data, parent_node):
"""根据数据重建节点,并确保在场景树中显示"""
try:
if not node_data or not parent_node or parent_node.isEmpty():
return None
print(f"正在重建节点 {node_data}")
node_type = node_data.get('node_type', '')
original_name = node_data.get('name', 'node')
# 生成唯一名称
unique_name = self._generateUniqueName(original_name, parent_node)
# 根据节点类型调用相应的重建方法
new_node = None
if node_type in ["LIGHT_NODE", "SPOT_LIGHT_NODE", "POINT_LIGHT_NODE"]:
new_node = self._recreateLightFromData(node_data, parent_node, unique_name)
elif node_type == "CESIUM_TILESET_NODE":
new_node = self._recreateTilesetFromData(node_data, parent_node, unique_name)
elif node_type in ["GUI_BUTTON", "GUI_LABEL", "GUI_ENTRY", "GUI_IMAGE",
"GUI_3DTEXT", "GUI_3DIMAGE", "GUI_VIDEO_SCREEN","GUI_2D_VIDEO_SCREEN"]:
new_node = self._recreateGUIFromData(node_data, parent_node, unique_name)
elif node_type == "IMPORTED_MODEL_NODE":
new_node = self._recreateModelFromData(node_data, parent_node, unique_name)
else:
# 创建普通节点
new_node = self._createBasicNodeFromData(node_data, parent_node, unique_name)
# 如果成功创建节点,确保它在场景树中显示
if new_node:
# 尝试更新场景树以显示新节点
try:
if hasattr(self.world, 'interface_manager') and self.world.interface_manager:
# 查找父节点在场景树中的对应项
parent_item = self._findTreeItemForNode(parent_node)
if parent_item:
# 添加新节点到场景树
tree_widget = self.world.interface_manager.treeWidget
if tree_widget:
tree_widget.add_node_to_tree_widget(new_node, parent_item, node_type or "NODE")
except Exception as e:
print(f"添加节点到场景树时出错: {e}")
return new_node
except Exception as e:
print(f"重建节点失败: {e}")
import traceback
traceback.print_exc()
return None
def _findTreeItemForNode(self, node):
"""根据节点查找对应的场景树项"""
try:
if hasattr(self.world, 'interface_manager') and self.world.interface_manager:
tree_widget = self.world.interface_manager.treeWidget
if tree_widget:
# 遍历场景树查找匹配的节点项
for i in range(tree_widget.topLevelItemCount()):
item = tree_widget.topLevelItem(i)
result = self._findTreeItemForNodeRecursive(item, node)
if result:
return result
return None
except Exception as e:
print(f"查找场景树项时出错: {e}")
return None
def _findTreeItemForNodeRecursive(self, item, target_node):
"""递归查找场景树项"""
try:
# 检查当前项是否匹配
item_node = getattr(item, 'node_path', None)
if not item_node:
item_node = getattr(item, 'node', None)
if item_node and item_node == target_node:
return item
# 递归检查子项
for i in range(item.childCount()):
child_item = item.child(i)
result = self._findTreeItemForNodeRecursive(child_item, target_node)
if result:
return result
return None
except Exception as e:
print(f"递归查找场景树项时出错: {e}")
return None
def _recreateLightFromData(self, node_data, parent_node, name):
"""根据数据重建光源"""
try:
light_type = node_data.get('tags', {}).get('light_type', 'spot_light')
# 创建光源
if light_type == 'spot_light':
light_node = self.createSpotLight(pos=node_data.get('pos', (0, 0, 0)))
else: # point_light
light_node = self.createPointLight(pos=node_data.get('pos', (0, 0, 0)))
if light_node:
# 设置名称
light_node.setName(name)
# 恢复其他属性
light_data = node_data.get('light_data', {})
rp_light = light_node.getPythonTag("rp_light_object")
if rp_light and light_data:
if 'energy' in light_data:
rp_light.energy = light_data['energy']
if 'radius' in light_data:
rp_light.radius = light_data['radius']
if 'fov' in light_data and hasattr(rp_light, 'fov'):
rp_light.fov = light_data['fov']
if 'inner_radius' in light_data and hasattr(rp_light, 'inner_radius'):
rp_light.inner_radius = light_data['inner_radius']
if 'casts_shadows' in light_data and hasattr(rp_light, 'casts_shadows'):
rp_light.casts_shadows = light_data['casts_shadows']
if 'shadow_map_resolution' in light_data and hasattr(rp_light, 'shadow_map_resolution'):
rp_light.shadow_map_resolution = light_data['shadow_map_resolution']
# 恢复其他标签
for tag_key, tag_value in node_data.get('tags', {}).items():
if tag_key not in ['name', 'light_type']:
light_node.setTag(tag_key, str(tag_value))
return light_node
except Exception as e:
print(f"重建光源失败: {e}")
return None
def _recreateTilesetFromData(self, node_data, parent_node, name):
"""根据数据重建Tileset"""
try:
tileset_url = node_data.get('tileset_url', '')
if not tileset_url:
return None
# 使用现有方法加载tileset
position = node_data.get('pos', (0, 0, 0))
tileset_node = self.load_cesium_tileset(tileset_url, position)
if tileset_node:
# 设置名称
tileset_node.setName(name)
# 恢复其他标签
for tag_key, tag_value in node_data.get('tags', {}).items():
if tag_key not in ['name']:
tileset_node.setTag(tag_key, str(tag_value))
return tileset_node
except Exception as e:
print(f"重建Tileset失败: {e}")
return None
def _recreateGUIFromData(self, node_data, parent_node, name):
"""根据数据重建GUI元素"""
try:
gui_data = node_data.get('gui_data', {})
#gui_type = gui_data.get('gui_type', '')
gui_type = node_data.get("tags").get("gui_type", "")
print(f"正在重建GUI元素: {gui_type}")
print(f"正在重建GUI元素: {node_data}")
# 根据GUI类型调用相应的创建方法
new_gui_element = None
if gui_type == "button" and hasattr(self.world, 'createGUIButton'):
pos = node_data.get('pos', (0, 0, 0))
text = node_data.get('tags').get('gui_text', '')
size = node_data.get('scale', 1)
print(pos,text,size)
new_gui_element = self.world.createGUIButton(pos,text,size)
elif gui_type == "label" and hasattr(self.world, 'createGUILabel'):
pos = node_data.get('pos', (0, 0, 0))
text = node_data.get('tags').get('gui_text', '')
size = node_data.get('scale', 1)
new_gui_element = self.world.createGUILabel(pos,text,size)
elif gui_type == "entry" and hasattr(self.world, 'createGUIEntry'):
pos = node_data.get('pos', (0, 0, 0))
text = node_data.get('tags').get('gui_text', '')
size = node_data.get('scale', 1)
new_gui_element = self.world.createGUIEntry(pos,text,size)
elif gui_type == "2d_image" and hasattr(self.world, 'createGUI2DImage'):
pos = node_data.get('pos', (0, 0, 0))
image_path = node_data.get('tags').get('image_path', '')
size = node_data.get('size', 1)
new_gui_element = self.world.createGUI2DImage(pos, image_path, size)
elif gui_type == "3d_text" and hasattr(self.world, 'createGUI3DText'):
print("正在创建3D文本!!!")
pos = node_data.get('pos', (0, 0, 0))
text = node_data.get('tags', {}).get('gui_text', '')
scale = node_data.get('scale', 1)
if isinstance(scale, (list, tuple)):
scale = scale[0] if len(scale) > 0 else 1
print(f"正在创建3D文本: 位置={pos}, 文本={text}, 大小={scale}")
new_gui_element = self.world.createGUI3DText(pos, text, scale)
elif gui_type == "3d_image" and hasattr(self.world, 'createGUI3DImage'):
pos = node_data.get('pos', (0, 0, 0))
image_path = node_data.get('tags').get('gui_image_path', '')
scale = node_data.get('scale', (1, 1))
if isinstance(scale, (int, float)):
scale = (scale, scale)
elif isinstance(scale, (list, tuple)) and len(scale) >= 2:
scale = (scale[0], scale[1])
else:
scale = (1, 1)
print(f"正在创建3D图片: 位置={pos}, 路径={image_path}, 大小={scale}")
new_gui_element = self.world.gui_manager.createGUI3DImage(pos, image_path, scale)
elif gui_type == "video_screen" and hasattr(self.world.gui_manager, 'createVideoScreen'):
pos = node_data.get('pos', (0, 0, 0))
video_path = node_data.get('tags').get('video_path', '')
scale = node_data.get('scale', (1, 1,1))
new_gui_element = self.world.gui_manager.createVideoScreen(pos,scale,video_path)
elif gui_type == "2d_video_screen" and hasattr(self.world.gui_manager, 'createGUI2DVideoScreen'):
pos = node_data.get('pos', (0, 0, 0))
video_path = node_data.get('tags').get('video_path', '')
scale = node_data.get('scale', (1, 1, 1))
new_gui_element = self.world.gui_manager.createGUI2DVideoScreen(pos,scale,video_path)
if new_gui_element:
# 设置名称和变换
if hasattr(new_gui_element, 'setName'):
new_gui_element.setName(name)
# 设置位置、旋转、缩放
pos = node_data.get('pos', (0, 0, 0))
hpr = node_data.get('hpr', (0, 0, 0))
scale = node_data.get('scale', (1, 1, 1))
if hasattr(new_gui_element, 'setPos'):
new_gui_element.setPos(*pos)
if hasattr(new_gui_element, 'setHpr'):
new_gui_element.setHpr(*hpr)
if hasattr(new_gui_element, 'setScale'):
new_gui_element.setScale(*scale)
# 恢复文本内容
if 'text' in gui_data and hasattr(new_gui_element, 'setText'):
new_gui_element.setText(gui_data['text'])
# 恢复其他标签
for tag_key, tag_value in node_data.get('tags', {}).items():
if hasattr(new_gui_element, 'setTag') and tag_key not in ['name']:
new_gui_element.setTag(tag_key, str(tag_value))
print(f"GUI元素重建成功: {name}")
return new_gui_element
except Exception as e:
print(f"重建GUI元素失败: {e}")
import traceback
traceback.print_exc()
return None
def _recreateModelFromData(self, node_data, parent_node, name):
"""根据数据重建模型,保持材质"""
try:
model_data = node_data.get('model_data', {})
model_path = model_data.get('model_path', model_data.get('file', ''))
if not model_path or not os.path.exists(model_path):
# 如果原始模型文件不存在,创建一个基本节点
return self._createBasicNodeFromData(node_data, parent_node, name)
# 导入模型,保持原有参数
model = self.importModel(
model_path,
apply_unit_conversion=False, # 已经处理过的模型不需要再次转换
normalize_scales=False, # 保持原有缩放
auto_convert_to_glb=False # 已经处理过的模型不需要再次转换
)
if model:
# 设置名称
model.setName(name)
# 设置变换
pos = node_data.get('pos', (0, 0, 0))
hpr = node_data.get('hpr', (0, 0, 0))
scale = node_data.get('scale', (1, 1, 1))
model.setPos(*pos)
model.setHpr(*hpr)
model.setScale(*scale)
# 恢复材质信息
try:
self._restoreModelMaterial(model, model_data)
except Exception as e:
print(f"恢复模型材质时出错: {e}")
# 恢复标签
for tag_key, tag_value in node_data.get('tags', {}).items():
if tag_key not in ['name']:
model.setTag(tag_key, str(tag_value))
# 添加到模型列表
if model not in self.models:
self.models.append(model)
return model
except Exception as e:
print(f"重建模型失败: {e}")
# 出错时创建基本节点
return self._createBasicNodeFromData(node_data, parent_node, name)
def _restoreModelMaterial(self, model, model_data):
"""恢复模型材质"""
try:
# 恢复基础颜色
if 'base_color' in model_data:
from panda3d.core import ColorAttrib
base_color = model_data['base_color']
color = (base_color[0], base_color[1], base_color[2], base_color[3])
model.setColor(color)
# 恢复复杂材质属性
if any(key.startswith('material_') for key in model_data.keys()):
from panda3d.core import Material
# 创建新材质或获取现有材质
material = Material()
# 恢复基础颜色
if 'material_base_color' in model_data:
base_color = model_data['material_base_color']
material.setBaseColor((base_color[0], base_color[1], base_color[2], base_color[3]))
# 恢复环境光颜色
if 'material_ambient_color' in model_data:
ambient_color = model_data['material_ambient_color']
material.setAmbient((ambient_color[0], ambient_color[1], ambient_color[2], ambient_color[3]))
# 恢复漫反射颜色
if 'material_diffuse_color' in model_data:
diffuse_color = model_data['material_diffuse_color']
material.setDiffuse((diffuse_color[0], diffuse_color[1], diffuse_color[2], diffuse_color[3]))
# 恢复高光颜色
if 'material_specular_color' in model_data:
specular_color = model_data['material_specular_color']
material.setSpecular((specular_color[0], specular_color[1], specular_color[2], specular_color[3]))
# 恢复粗糙度和金属度
if 'material_roughness' in model_data:
material.setRoughness(model_data['material_roughness'])
if 'material_metallic' in model_data:
material.setMetallic(model_data['material_metallic'])
# 应用材质到模型
model.setMaterial(material)
except Exception as e:
print(f"恢复材质失败: {e}")
def _createBasicNodeFromData(self, node_data, parent_node, name):
"""创建基本节点,保持视觉属性"""
try:
new_node = parent_node.attachNewNode(name)
# 设置变换
pos = node_data.get('pos', (0, 0, 0))
hpr = node_data.get('hpr', (0, 0, 0))
scale = node_data.get('scale', (1, 1, 1))
new_node.setPos(*pos)
new_node.setHpr(*hpr)
new_node.setScale(*scale)
# 恢复视觉属性
try:
# 恢复颜色
if 'color' in node_data:
color_data = node_data['color']
new_node.setColor(color_data[0], color_data[1], color_data[2], color_data[3])
# 恢复材质
if 'material' in node_data:
from panda3d.core import Material
material_data = node_data['material']
material = Material()
if 'base_color' in material_data:
bc = material_data['base_color']
material.setBaseColor((bc[0], bc[1], bc[2], bc[3]))
if 'ambient' in material_data:
ac = material_data['ambient']
material.setAmbient((ac[0], ac[1], ac[2], ac[3]))
if 'diffuse' in material_data:
dc = material_data['diffuse']
material.setDiffuse((dc[0], dc[1], dc[2], dc[3]))
if 'specular' in material_data:
sc = material_data['specular']
material.setSpecular((sc[0], sc[1], sc[2], sc[3]))
if 'shininess' in material_data:
material.setShininess(material_data['shininess'])
new_node.setMaterial(material)
except Exception as e:
print(f"恢复视觉属性时出错: {e}")
# 恢复标签
for tag_key, tag_value in node_data.get('tags', {}).items():
if tag_key not in ['name']:
new_node.setTag(tag_key, str(tag_value))
return new_node
except Exception as e:
print(f"创建基本节点失败: {e}")
return None
def _generateUniqueName(self, base_name, parent_node):
"""生成唯一节点名称"""
try:
# 移除可能的数字后缀
import re
import time
name_base = re.sub(r'_\d+$', '', base_name)
# 查找现有同名节点
counter = 1
unique_name = base_name
while True:
# 检查父节点下是否已存在同名子节点
existing_node = parent_node.find(unique_name)
if existing_node.isEmpty():
break
unique_name = f"{name_base}_{counter}"
counter += 1
if counter > 1000: # 防止无限循环
unique_name = f"{name_base}_{int(time.time())}"
break
return unique_name
except:
return f"{base_name}_{int(time.time())}"