3106 lines
131 KiB
Python
3106 lines
131 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
|
||
"""
|
||
场景管理器 - 负责场景和模型管理的核心功能
|
||
处理模型导入、场景树构建、材质系统、碰撞设置等
|
||
"""
|
||
|
||
import os
|
||
|
||
from PyQt5.QtCore import Qt
|
||
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 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)
|
||
|
||
# 使用安全方法将模型添加到场景
|
||
#self._safeReparentTo(model, self.world.render)
|
||
|
||
# 设置模型名称
|
||
model_name = os.path.basename(filepath)
|
||
model.setName(model_name)
|
||
|
||
# 将模型添加到场景
|
||
model.reparentTo(self.world.render)
|
||
# 保存原始路径和转换后的路径
|
||
model.setTag("model_path", filepath)
|
||
model.setTag("original_path", original_filepath)
|
||
if filepath != original_filepath:
|
||
model.setTag("converted_from", os.path.splitext(original_filepath)[1])
|
||
model.setTag("converted_to_glb", "true")
|
||
|
||
# 可选的单位转换(主要针对FBX)
|
||
if apply_unit_conversion and filepath.lower().endswith('.fbx'):
|
||
#print("应用FBX单位转换(厘米到米)...")
|
||
self._applyUnitConversion(model, 0.01)
|
||
|
||
# 智能缩放标准化(处理FBX子节点的大缩放值)
|
||
if normalize_scales and filepath.lower().endswith('.fbx'):
|
||
#print("标准化FBX模型缩放层级...")
|
||
self._normalizeModelScales(model)
|
||
|
||
# 调整模型位置到地面
|
||
self._adjustModelToGround(model)
|
||
|
||
# 创建并设置基础材质
|
||
print("\n=== 开始设置材质 ===")
|
||
self._applyMaterialsToModel(model)
|
||
|
||
# 设置碰撞检测(重要!用于选择功能)
|
||
print("\n=== 设置碰撞检测 ===")
|
||
self.setupCollision(model)
|
||
|
||
# 添加文件标签用于保存/加载
|
||
model.setTag("file", model_name)
|
||
model.setTag("is_model_root", "1")
|
||
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": {}
|
||
}
|
||
|
||
# 收集所有标签(仅对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 in ["video_screen", "2d_video_screen"]:
|
||
if hasattr(gui_node, 'hasTag') and gui_node.hasTag("video_path"):
|
||
gui_info["video_path"] = gui_node.getTag("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("bg_image_path"):
|
||
gui_info["bg_image_path"] = gui_node.getTag("bg_image_path")
|
||
|
||
# 如果是信息面板,收集面板数据
|
||
if hasattr(gui_node, 'hasTag') and gui_node.hasTag("info_panel_data"):
|
||
gui_info["panel_data"] = gui_node.getTag("info_panel_data")
|
||
|
||
if hasattr(self.world, 'script_manager') and self.world.script_manager:
|
||
script_manager = self.world.script_manager
|
||
# 获取挂载在此节点上的所有脚本
|
||
scripts = script_manager.get_scripts_on_object(gui_node)
|
||
if scripts:
|
||
gui_info["scripts"] = []
|
||
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)
|
||
|
||
gui_info["scripts"].append({
|
||
"name": script_name,
|
||
"file": script_file
|
||
})
|
||
print(f"脚本 {script_name} 来自文件: {script_file}")
|
||
|
||
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)
|
||
|
||
# 存储需要临时隐藏的节点,以便保存后恢复
|
||
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 = []
|
||
for gui_node in gui_elements:
|
||
gui_info = self._collectGUIElementInfo(gui_node)
|
||
if gui_info:
|
||
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()
|
||
|
||
# 添加tilesets节点
|
||
for tileset_info in self.tilesets:
|
||
if tileset_info.get('node') and not tileset_info['node'].isEmpty():
|
||
all_nodes.append(tileset_info['node'])
|
||
|
||
# 添加Cesium tilesets节点
|
||
for tileset_name, tileset_info in self.cesium_integration.tilesets.items():
|
||
if tileset_info.get('node') and not tileset_info['node'].isEmpty():
|
||
all_nodes.append(tileset_info['node'])
|
||
|
||
# 保存所有节点的信息
|
||
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()} 的变换信息")
|
||
|
||
# 获取当前状态
|
||
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))
|
||
|
||
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元素,避免重复处理
|
||
|
||
# 遍历场景中的所有节点
|
||
def processNode(nodePath, depth=0):
|
||
indent = " " * depth
|
||
print(f"{indent}处理节点: {nodePath.getName()} (类型: {type(nodePath.node()).__name__})")
|
||
|
||
# 跳过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}")
|
||
|
||
# 恢复材质属性
|
||
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)
|
||
|
||
# 加载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 _shouldSkipNodeInTree(self, nodePath):
|
||
"""判断节点是否应该在场景树中跳过显示"""
|
||
# 跳过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)
|
||
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()
|
||
|
||
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) # 面板数据
|
||
|
||
# 检查是否已经处理过同名元素
|
||
if name in processed_names:
|
||
print(f"跳过重复元素: {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}")
|
||
|
||
# 根据类型创建相应的GUI元素
|
||
new_element = None
|
||
|
||
if gui_type == "button" and hasattr(gui_manager, 'createGUIButton'):
|
||
new_element = gui_manager.createGUIButton(
|
||
pos=tuple(position),
|
||
text=text,
|
||
size=scale[0] if scale and len(scale) > 0 else 1.0
|
||
)
|
||
elif gui_type == "label" and hasattr(gui_manager, 'createGUILabel'):
|
||
scale_value = scale[0] if scale and len(scale) > 0 else 1.0
|
||
new_element = gui_manager.createGUILabel(
|
||
pos=tuple(position),
|
||
text=text,
|
||
size=scale_value
|
||
)
|
||
elif gui_type == "entry" and hasattr(gui_manager, 'createGUIEntry'):
|
||
new_element = gui_manager.createGUIEntry(
|
||
pos=tuple(position),
|
||
placeholder=text,
|
||
size=scale[0] if scale and len(scale) > 0 else 1.0
|
||
)
|
||
elif gui_type == "2d_image" and hasattr(gui_manager, 'createGUI2DImage'):
|
||
scale_value = scale[0] if scale and len(scale) > 0 else 0.2
|
||
new_element = gui_manager.createGUI2DImage(
|
||
pos=tuple(position),
|
||
image_path=image_path,
|
||
size=scale_value*0.2
|
||
)
|
||
elif gui_type == "3d_text" and hasattr(gui_manager,'createGUI3DText'):
|
||
size = scale[0] if scale and len(scale) > 0 else 0.5
|
||
new_element = gui_manager.createGUI3DText(
|
||
pos=tuple(position),
|
||
text=text,
|
||
size=size
|
||
)
|
||
elif gui_type == "3d_image" and hasattr(gui_manager, 'createGUI3DImage'):
|
||
# 处理3D图像
|
||
# 根据缩放值的数量处理尺寸
|
||
if len(scale) >= 3:
|
||
size = (scale[0] * 2, scale[1] * 2)
|
||
elif len(scale) >= 2:
|
||
size = (scale[0] * 2, scale[1] * 2)
|
||
elif len(scale) >= 1:
|
||
size = (scale[0] * 2, scale[0] * 2)
|
||
else:
|
||
size = (1.0, 1.0)
|
||
|
||
new_element = gui_manager.createGUI3DImage(
|
||
pos=tuple(position),
|
||
image_path=image_path,
|
||
size=size
|
||
)
|
||
elif gui_type == "video_screen" and hasattr(gui_manager,'createVideoScreen'):
|
||
new_element = gui_manager.createVideoScreen(
|
||
pos=tuple(position),
|
||
size=scale,
|
||
video_path=video_path
|
||
)
|
||
if video_path and new_element and hasattr(gui_manager, 'loadVideoFile'):
|
||
# 延迟一帧执行,确保节点完全初始化
|
||
from direct.task.TaskManagerGlobal import taskMgr
|
||
def load_video_task(task):
|
||
gui_manager.loadVideoFile(new_element, video_path)
|
||
return task.done
|
||
|
||
taskMgr.doMethodLater(0.1, load_video_task, 'loadVideoTask')
|
||
|
||
elif gui_type == "2d_video_screen" and hasattr(gui_manager,'createGUI2DVideoScreen'):
|
||
new_element = gui_manager.createGUI2DVideoScreen(
|
||
pos=tuple(position),
|
||
size=scale,
|
||
video_path=video_path
|
||
)
|
||
elif gui_type in ["info_panel", "info_panel_3d"]:
|
||
# 重建信息面板
|
||
if info_panel_manager:
|
||
try:
|
||
if panel_data:
|
||
# 从序列化数据重建面板
|
||
import json
|
||
panel_data_obj = json.loads(panel_data)
|
||
new_element = info_panel_manager.recreatePanelFromData(panel_data_obj)
|
||
else:
|
||
# 创建新的面板
|
||
if gui_type == "info_panel_3d":
|
||
# 3D信息面板
|
||
new_element = info_panel_manager.create3DInfoPanel(
|
||
panel_id=panel_id,
|
||
position=tuple(position) if len(position) >= 3 else (0, 0, 0),
|
||
size=tuple(scale) if len(scale) >= 2 else (1.0, 0.6),
|
||
visible=not tags.get("hidden", False)
|
||
)
|
||
else:
|
||
# 2D信息面板
|
||
pos_2d = (position[0], position[2]) if len(position) >= 3 else (0, 0)
|
||
new_element = info_panel_manager.createInfoPanel(
|
||
panel_id=panel_id,
|
||
position=pos_2d,
|
||
size=tuple(scale) if len(scale) >= 2 else (1.0, 0.6),
|
||
visible=not tags.get("hidden", False)
|
||
)
|
||
|
||
# 设置背景图片
|
||
if bg_image_path and hasattr(info_panel_manager, 'setPanelBackgroundImage'):
|
||
info_panel_manager.setPanelBackgroundImage(panel_id, bg_image_path)
|
||
|
||
# 更新面板内容
|
||
if text:
|
||
title, content = text.split('\n', 1) if '\n' in text else ("信息面板", text)
|
||
if gui_type == "info_panel_3d":
|
||
info_panel_manager.update3DPanelContent(panel_id, title=title,
|
||
content=content)
|
||
else:
|
||
info_panel_manager.updatePanelContent(panel_id, title=title,
|
||
content=content)
|
||
except Exception as e:
|
||
print(f"重建信息面板失败: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
else:
|
||
print("信息面板管理器未找到,无法重建信息面板")
|
||
|
||
|
||
# 如果创建成功,设置属性
|
||
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)
|
||
|
||
# 重新挂载脚本(如果有的话)
|
||
# 在 _recreateGUIElementsFromData 方法中找到重新挂载脚本的部分,替换为以下代码:
|
||
|
||
# 重新挂载脚本(如果有的话)
|
||
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}")
|
||
|
||
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
|
||
|
||
print("GUI元素重建完成")
|
||
|
||
except Exception as e:
|
||
print(f"重建GUI元素时发生错误: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
|
||
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 QPanda3D.Panda3DWorld import get_render_pipeline
|
||
|
||
# 创建聚光灯对象
|
||
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.setPos(light_node.getPos())
|
||
|
||
# 添加到渲染管线
|
||
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)}")
|
||
|
||
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)}")
|
||
|
||
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 = 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 = 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
|
||
|
||
# def createSpotLight(self, pos=(0, 0, 0)):
|
||
# """创建聚光灯 - 使用统一的create_item方法"""
|
||
# try:
|
||
# # 调用CustomTreeWidget的create_item方法创建聚光灯节点
|
||
# if hasattr(self.world, 'interface_manager') and hasattr(self.world.interface_manager, 'treeWidget'):
|
||
# tree_widget = self.world.interface_manager.treeWidget
|
||
# if tree_widget and hasattr(tree_widget, 'create_item'):
|
||
# # 创建聚光灯节点
|
||
# created_nodes = tree_widget.create_item("spot_light")
|
||
#
|
||
# if created_nodes:
|
||
# # 获取创建的节点
|
||
# light_np, qt_item = created_nodes[0]
|
||
#
|
||
# # 设置位置(如果指定了非默认位置)
|
||
# if pos != (0, 0, 0):
|
||
# light_np.setPos(*pos)
|
||
# # 同时更新光源对象的位置
|
||
# light_obj = light_np.getPythonTag("rp_light_object")
|
||
# if light_obj:
|
||
# light_obj.setPos(*pos)
|
||
#
|
||
# print(f"✅ 通过create_item创建聚光灯成功: {light_np.getName()}")
|
||
# return light_np
|
||
# else:
|
||
# print("❌ create_item创建聚光灯失败")
|
||
# return None
|
||
# else:
|
||
# print("❌ 无法访问树形控件的create_item方法")
|
||
# return None
|
||
# else:
|
||
# print("❌ 无法访问界面管理器或树形控件")
|
||
# return None
|
||
#
|
||
# except Exception as e:
|
||
# print(f"❌ 创建聚光灯时发生错误: {str(e)}")
|
||
# import traceback
|
||
# traceback.print_exc()
|
||
# return None
|
||
|
||
# def createSpotLight(self, pos=(0, 0, 0)):
|
||
# from RenderPipelineFile.rpcore import SpotLight, RenderPipeline
|
||
# from panda3d.core import Vec3,NodePath
|
||
#
|
||
# render_pipeline = get_render_pipeline()
|
||
#
|
||
# # 创建一个挂载节点(你控制的)
|
||
# light_np = NodePath("SpotlightAttachNode")
|
||
# light_np.reparentTo(self.world.render)
|
||
# #light_np.setPos(*pos)
|
||
#
|
||
# self.half_energy = 5000
|
||
# self.lamp_fov = 70
|
||
# self.lamp_radius = 1000
|
||
#
|
||
# light = SpotLight()
|
||
# light.direction = Vec3(0, 0, -1) # 光照方向
|
||
# light.fov = self.lamp_fov # 光源角度(类似手电筒)
|
||
# light.set_color_from_temperature(5 * 1000.0) # 色温(K)
|
||
# light.energy = self.half_energy # 光照强度
|
||
# light.radius = self.lamp_radius # 影响范围
|
||
# light.casts_shadows = True # 是否投射阴影
|
||
# light.shadow_map_resolution = 256 # 阴影分辨率
|
||
# light.setPos(*pos)
|
||
# #light_np.setPos(*pos)
|
||
#
|
||
# #light_np = render_pipeline.add_light(light, parent=self.world.render)
|
||
# render_pipeline.add_light(light) # 添加到渲染管线
|
||
#
|
||
# light_name = f"Spotlight_{len(self.Spotlight)}"
|
||
#
|
||
# light_np.setName(light_name) # 设置唯一名称
|
||
# #light_np.reparentTo(self.world.render) # 挂载到场景根节点
|
||
#
|
||
# light_np.setTag("light_type", "spot_light")
|
||
# light_np.setTag("is_scene_element", "1")
|
||
# light_np.setTag("light_energy", str(light.energy))
|
||
#
|
||
# light_np.setPythonTag("rp_light_object", light)
|
||
#
|
||
# self.Spotlight.append(light_np)
|
||
#
|
||
# if hasattr(self.world, 'updateSceneTree'):
|
||
# self.world.updateSceneTree()
|
||
#
|
||
# #print("nikan"+light_np.getHpr())
|
||
|
||
# def createPointLight(self, pos=(0, 0, 0)):
|
||
# """创建点光源 - 使用统一的create_item方法"""
|
||
# try:
|
||
# # 调用CustomTreeWidget的create_item方法创建点光源节点
|
||
# if hasattr(self.world, 'interface_manager') and hasattr(self.world.interface_manager, 'treeWidget'):
|
||
# tree_widget = self.world.interface_manager.treeWidget
|
||
# if tree_widget and hasattr(tree_widget, 'create_item'):
|
||
# # 创建点光源节点
|
||
# created_nodes = tree_widget.create_item("point_light")
|
||
#
|
||
# if created_nodes:
|
||
# # 获取创建的节点
|
||
# light_np, qt_item = created_nodes[0]
|
||
#
|
||
# # 设置位置(如果指定了非默认位置)
|
||
# if pos != (0, 0, 0):
|
||
# light_np.setPos(*pos)
|
||
# # 同时更新光源对象的位置
|
||
# light_obj = light_np.getPythonTag("rp_light_object")
|
||
# if light_obj:
|
||
# light_obj.setPos(*pos)
|
||
#
|
||
# print(f"✅ 通过create_item创建点光源成功: {light_np.getName()}")
|
||
# return light_np
|
||
# else:
|
||
# print("❌ create_item创建点光源失败")
|
||
# return None
|
||
# else:
|
||
# print("❌ 无法访问树形控件的create_item方法")
|
||
# return None
|
||
# else:
|
||
# print("❌ 无法访问界面管理器或树形控件")
|
||
# return None
|
||
#
|
||
# except Exception as e:
|
||
# print(f"❌ 创建点光源时发生错误: {str(e)}")
|
||
# import traceback
|
||
# traceback.print_exc()
|
||
# return None
|
||
|
||
# def createPointLight(self, pos=(0, 0, 0)):
|
||
# from RenderPipelineFile.rpcore import PointLight, RenderPipeline
|
||
# from panda3d.core import Vec3, NodePath
|
||
#
|
||
# render_pipeline = get_render_pipeline()
|
||
#
|
||
# # 创建一个挂载节点(你控制的)
|
||
# light_np = NodePath("PointlightAttachNode")
|
||
# light_np.reparentTo(self.world.render)
|
||
#
|
||
#
|
||
# light = PointLight()
|
||
# light.setPos(*pos)
|
||
# light_np.setPos(*pos)
|
||
# light.energy = 5000
|
||
# light.radius = 1000
|
||
# light.inner_radius = 0.4
|
||
# light.set_color_from_temperature(5 * 1000.0) # 色温(K)
|
||
# light.casts_shadows = True # 是否投射阴影
|
||
# light.shadow_map_resolution = 256 # 阴影分辨率
|
||
#
|
||
# render_pipeline.add_light(light) # 添加到渲染管线
|
||
#
|
||
# light_name = f"Pointlight{len(self.Pointlight)}"
|
||
#
|
||
# light_np.setName(light_name) # 设置唯一名称
|
||
#
|
||
# #light_np = NodePath(f"PointLight_{len(self.Pointlight)}")
|
||
# #light_np.reparentTo(self.world.render)
|
||
# #light_np.setPos(*pos)
|
||
#
|
||
# light_np.setTag("light_type", "point_light")
|
||
# light_np.setTag("is_scene_element", "1")
|
||
# light_np.setTag("light_energy", str(light.energy))
|
||
#
|
||
# # 保存光源对象引用(重要!用于属性面板)
|
||
# light_np.setPythonTag("rp_light_object", light)
|
||
#
|
||
# self.Pointlight.append(light_np)
|
||
#
|
||
# if hasattr(self.world, 'updateSceneTree'):
|
||
# self.world.updateSceneTree()
|
||
#
|
||
# return light,light_np
|
||
|
||
# ==================== GLB 转换方法 ====================
|
||
|
||
#===================================================
|
||
|
||
def _shouldConvertToGLB(self, filepath):
|
||
"""判断是否应该转换为GLB格式"""
|
||
ext = os.path.splitext(filepath)[1].lower()
|
||
# 需要转换的格式:FBX, OBJ, DAE等(但不转换已经是GLB/GLTF的)
|
||
convert_formats = ['.fbx', '.obj', '.dae', '.3ds', '.blend']
|
||
return ext in convert_formats
|
||
|
||
def _convertToGLBWithProgress(self, filepath):
|
||
"""带进度显示的GLB转换"""
|
||
try:
|
||
from PyQt5.QtWidgets import QProgressDialog, QApplication
|
||
from PyQt5.QtCore import Qt
|
||
|
||
# 创建进度对话框
|
||
progress = QProgressDialog("正在转换模型格式以获得更好的动画支持...", "取消", 0, 100)
|
||
progress.setWindowTitle("模型格式转换")
|
||
progress.setWindowModality(Qt.WindowModal)
|
||
progress.show()
|
||
QApplication.processEvents()
|
||
|
||
try:
|
||
result = self._convertToGLB(filepath, progress)
|
||
progress.hide()
|
||
return result
|
||
except Exception as e:
|
||
progress.hide()
|
||
print(f"转换过程出错: {e}")
|
||
return None
|
||
|
||
except ImportError:
|
||
# 如果没有 PyQt5,直接转换
|
||
return self._convertToGLB(filepath)
|
||
|
||
def _convertToGLB(self, filepath, progress=None):
|
||
"""将模型文件转换为GLB格式"""
|
||
try:
|
||
print(f"[GLB转换] 开始转换: {filepath}")
|
||
|
||
if progress:
|
||
progress.setValue(10)
|
||
progress.setLabelText("准备转换...")
|
||
from PyQt5.QtWidgets import QApplication
|
||
QApplication.processEvents()
|
||
|
||
# 准备输出路径
|
||
base_name = os.path.splitext(os.path.basename(filepath))[0]
|
||
output_dir = os.path.dirname(filepath)
|
||
glb_path = os.path.join(output_dir, f"{base_name}_auto_converted.glb")
|
||
|
||
# 如果已经存在转换后的文件,直接使用
|
||
if os.path.exists(glb_path):
|
||
# 检查文件时间,如果原文件更新则重新转换
|
||
original_time = os.path.getmtime(filepath)
|
||
converted_time = os.path.getmtime(glb_path)
|
||
if converted_time > original_time:
|
||
print(f"[GLB转换] 使用现有转换文件: {glb_path}")
|
||
if progress:
|
||
progress.setValue(100)
|
||
return glb_path
|
||
|
||
if progress:
|
||
progress.setValue(20)
|
||
progress.setLabelText("尝试 Blender 转换...")
|
||
QApplication.processEvents()
|
||
|
||
# 方法1: 使用 Blender 进行转换
|
||
if self._convertWithBlender(filepath, glb_path, progress):
|
||
return glb_path
|
||
|
||
if progress:
|
||
progress.setValue(60)
|
||
progress.setLabelText("尝试 FBX2glTF 转换...")
|
||
QApplication.processEvents()
|
||
|
||
# 方法2: 使用 FBX2glTF (如果可用)
|
||
if self._convertWithFBX2glTF(filepath, glb_path, progress):
|
||
return glb_path
|
||
|
||
if progress:
|
||
progress.setValue(80)
|
||
progress.setLabelText("尝试 Assimp 转换...")
|
||
QApplication.processEvents()
|
||
|
||
# 方法3: 使用 Assimp
|
||
if self._convertWithAssimp(filepath, glb_path, progress):
|
||
return glb_path
|
||
|
||
#print(f"[GLB转换] 所有转换方法都失败,既然没有可以转换格式的工具和环境那么就用原始文件,不一定非要转换")
|
||
return None
|
||
|
||
except Exception as e:
|
||
print(f"[GLB转换] 转换过程出错: {e}")
|
||
return None
|
||
|
||
def _convertWithBlender(self, input_path, output_path, progress=None):
|
||
"""使用 Blender 进行转换"""
|
||
try:
|
||
import subprocess
|
||
import tempfile
|
||
|
||
print(f"[Blender转换] {input_path} → {output_path}")
|
||
|
||
# 创建 Blender 脚本
|
||
script_content = f'''
|
||
import bpy
|
||
import sys
|
||
import os
|
||
|
||
# 清理默认场景
|
||
bpy.ops.object.select_all(action='SELECT')
|
||
bpy.ops.object.delete(use_global=False)
|
||
|
||
print("开始导入文件...")
|
||
|
||
# 根据文件类型选择导入方法
|
||
input_file = "{input_path}"
|
||
output_file = "{output_path}"
|
||
|
||
try:
|
||
ext = os.path.splitext(input_file)[1].lower()
|
||
|
||
if ext == '.fbx':
|
||
bpy.ops.import_scene.fbx(filepath=input_file)
|
||
elif ext == '.obj':
|
||
bpy.ops.import_scene.obj(filepath=input_file)
|
||
elif ext == '.dae':
|
||
bpy.ops.wm.collada_import(filepath=input_file)
|
||
elif ext == '.blend':
|
||
bpy.ops.wm.open_mainfile(filepath=input_file)
|
||
else:
|
||
print(f"不支持的格式: {{ext}}")
|
||
sys.exit(1)
|
||
|
||
print("导入成功,开始导出GLB...")
|
||
|
||
# 导出为 GLB,保留动画
|
||
bpy.ops.export_scene.gltf(
|
||
filepath=output_file,
|
||
export_format='GLB',
|
||
export_animations=True,
|
||
export_force_sampling=True,
|
||
export_frame_range=True,
|
||
export_current_frame=False,
|
||
export_skins=True,
|
||
export_morph=True,
|
||
export_lights=True,
|
||
export_cameras=False
|
||
)
|
||
|
||
print("GLB导出成功!")
|
||
|
||
except Exception as e:
|
||
print(f"转换失败: {{e}}")
|
||
sys.exit(1)
|
||
'''
|
||
|
||
# 写入临时脚本文件
|
||
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as temp_file:
|
||
temp_file.write(script_content)
|
||
script_path = temp_file.name
|
||
|
||
try:
|
||
# 执行 Blender 转换
|
||
result = subprocess.run([
|
||
'blender', '--background', '--python', script_path
|
||
], capture_output=True, text=True, timeout=180)
|
||
|
||
# 清理临时文件
|
||
os.unlink(script_path)
|
||
|
||
if result.returncode == 0 and os.path.exists(output_path):
|
||
print(f"[Blender转换] 转换成功")
|
||
return True
|
||
else:
|
||
print(f"[Blender转换] 转换失败: {result.stderr}")
|
||
return False
|
||
|
||
except subprocess.TimeoutExpired:
|
||
print(f"[Blender转换] 转换超时")
|
||
return False
|
||
except FileNotFoundError:
|
||
print(f"[Blender转换] Blender 未安装")
|
||
return False
|
||
|
||
except Exception as e:
|
||
print(f"[Blender转换] 转换过程出错: {e}")
|
||
return False
|
||
|
||
def _convertWithFBX2glTF(self, input_path, output_path, progress=None):
|
||
"""使用 FBX2glTF 进行转换(仅支持FBX)"""
|
||
try:
|
||
import subprocess
|
||
|
||
if not input_path.lower().endswith('.fbx'):
|
||
return False
|
||
|
||
print(f"[FBX2glTF转换] {input_path} → {output_path}")
|
||
|
||
# 使用 FBX2glTF 转换
|
||
result = subprocess.run([
|
||
'FBX2glTF', input_path, '--output', output_path, '--binary'
|
||
], capture_output=True, text=True, timeout=120)
|
||
|
||
if result.returncode == 0 and os.path.exists(output_path):
|
||
print(f"[FBX2glTF转换] 转换成功")
|
||
return True
|
||
else:
|
||
print(f"[FBX2glTF转换] 转换失败: {result.stderr}")
|
||
return False
|
||
|
||
except subprocess.TimeoutExpired:
|
||
print(f"[FBX2glTF转换] 转换超时")
|
||
return False
|
||
except FileNotFoundError:
|
||
print(f"[FBX2glTF转换] FBX2glTF 未安装")
|
||
return False
|
||
except Exception as e:
|
||
print(f"[FBX2glTF转换] 转换过程出错: {e}")
|
||
return False
|
||
|
||
def _convertWithAssimp(self, input_path, output_path, progress=None):
|
||
"""使用 PyAssimp 进行转换"""
|
||
try:
|
||
import pyassimp
|
||
|
||
print(f"[PyAssimp转换] {input_path} → {output_path}")
|
||
|
||
# 加载模型
|
||
scene = pyassimp.load(input_path)
|
||
if not scene:
|
||
print(f"[PyAssimp转换] 加载模型失败")
|
||
return False
|
||
|
||
if progress:
|
||
progress.setValue(30)
|
||
|
||
# 导出为GLB格式
|
||
pyassimp.export(scene, output_path, "glb2")
|
||
|
||
if progress:
|
||
progress.setValue(80)
|
||
|
||
# 释放资源
|
||
pyassimp.release(scene)
|
||
|
||
if os.path.exists(output_path):
|
||
print(f"[PyAssimp转换] 转换成功")
|
||
return True
|
||
else:
|
||
print(f"[PyAssimp转换] 转换失败: 输出文件未生成")
|
||
return False
|
||
|
||
except ImportError:
|
||
print(f"[PyAssimp转换] PyAssimp 未安装")
|
||
return False
|
||
except Exception as e:
|
||
print(f"[PyAssimp转换] 转换过程出错: {e}")
|
||
return False
|
||
|
||
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
|
||
|
||
|
||
|