EG/scene/scene_manager.py
2025-09-18 10:55:06 +08:00

3106 lines
131 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

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

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

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
场景管理器 - 负责场景和模型管理的核心功能
处理模型导入、场景树构建、材质系统、碰撞设置等
"""
import os
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