EG/scene/scene_manager_io_mixin.py
2026-03-13 09:53:12 +08:00

1290 lines
60 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.

"""Scene manager load/save and GUI rebuild operations."""
import os
import shutil
import time
import json
import aiohttp
import asyncio
import inspect
from pathlib import Path
from panda3d.core import (
ModelPool, ModelRoot, Filename, NodePath, GeomNode, Material, Vec4, Vec3,
MaterialAttrib, ColorAttrib, Point3, CollisionNode, CollisionSphere, CollisionBox,
BitMask32, TransparencyAttrib, LColor, TransformState, RenderModeAttrib, ShaderAttrib
)
from panda3d.egg import EggData, EggVertexPool
from direct.actor.Actor import Actor
from RenderPipelineFile.rpplugins.smaa.jitters import halton_seq
from scene import util
class SceneManagerIOMixin:
def _cleanup_untracked_render_children(self):
"""Remove direct render children left behind by previous scene/runtime state."""
render = getattr(self.world, "render", None)
if not render or render.isEmpty():
return
keep_exact = {
"camera",
"ForwardShadingCam",
"EnvmapCamRig",
"PSSMCameraRig",
"PSSMDistShadowsESM",
"PSSMSceneSunShadowCam",
"SkyAOCaptureCam",
"SceneRoot",
"alight",
"dlight",
"ground",
}
keep_prefixes = (
"ShadowCam-",
)
stale_children = []
for child in render.getChildren():
try:
name = child.getName()
except Exception:
continue
if name in keep_exact or any(name.startswith(prefix) for prefix in keep_prefixes):
continue
stale_children.append(child)
for child in stale_children:
try:
child_name = child.getName()
if not child.isEmpty():
child.removeNode()
print(f"清理残留场景节点: {child_name}")
except Exception as e:
print(f"清理残留场景节点失败: {e}")
def _collectGUIElementInfo(self, gui_node):
"""收集GUI元素的信息用于保存"""
try:
# 获取GUI元素类型
gui_type = "unknown"
if hasattr(gui_node, 'hasTag') and gui_node.hasTag("gui_type"):
gui_type = gui_node.getTag("gui_type")
elif hasattr(gui_node, 'hasTag') and gui_node.hasTag("saved_gui_type"):
gui_type = gui_node.getTag("saved_gui_type")
else:
# 尝试从节点名称推断类型
name_lower = gui_node.getName().lower()
if "button" in name_lower:
gui_type = "button"
elif "label" in name_lower:
gui_type = "label"
elif "entry" in name_lower:
gui_type = "entry"
elif "image" in name_lower:
gui_type = "2d_image"
elif "videoscreen" in name_lower:
if "2d" in name_lower:
gui_type = "2d_video_screen"
else:
gui_type = "video_screen"
elif "info_panel" in name_lower:
if "3d" in name_lower:
gui_type = "info_panel_3d"
else:
gui_type = "info_panel"
else:
# 如果无法识别类型,跳过该元素
print(f"跳过无法识别类型的GUI元素: {gui_node.getName()}")
return None
gui_info = {
"name": gui_node.getName(),
"type": gui_type,
"position": list(gui_node.getPos()),
"rotation": list(gui_node.getHpr()),
"scale": list(gui_node.getScale()),
"tags": {},
"parent_name":None,
"video_path":gui_node.getTag("video_path") if gui_node.hasTag("video_path") else None,
"panel_id":gui_node.getTag("panel_id") if gui_node.hasTag("panel_id") else None,
}
parent = gui_node.getParent()
if parent and not parent.isEmpty():
parent_name = parent.getName()
if parent_name not in ["render","aspect2d","render2d"]:
gui_info["parent_name"] = parent_name
# 收集所有标签仅对NodePath类型的对象
if hasattr(gui_node, 'getTagKeys'):
for tag in gui_node.getTagKeys():
gui_info["tags"][tag] = gui_node.getTag(tag)
elif hasattr(gui_node, 'getTags'): # 对于DirectGUI对象
# DirectGUI对象使用不同的方法存储标签
if hasattr(gui_node, '_tags'):
gui_info["tags"] = gui_node._tags.copy()
# 根据类型收集特定信息
if gui_type == "button":
if hasattr(gui_node, 'get'): # DirectButton
gui_info["text"] = gui_node.get()
elif hasattr(gui_node, 'getText'): # 其他类型
gui_info["text"] = gui_node.getText()
elif hasattr(gui_node, 'hasTag') and gui_node.hasTag("gui_text"):
gui_info["text"] = gui_node.getTag("gui_text")
elif gui_type == "label":
if hasattr(gui_node, 'getText'):
gui_info["text"] = gui_node.getText()
elif hasattr(gui_node, 'hasTag') and gui_node.hasTag("gui_text"):
gui_info["text"] = gui_node.getTag("gui_text")
elif gui_type == "entry":
if hasattr(gui_node, 'get'):
gui_info["text"] = gui_node.get()
elif hasattr(gui_node, 'hasTag') and gui_node.hasTag("gui_text"):
gui_info["text"] = gui_node.getTag("gui_text")
elif gui_type == "2d_image":
if hasattr(gui_node, 'hasTag') and gui_node.hasTag("image_path"):
gui_info["image_path"] = gui_node.getTag("image_path")
elif hasattr(gui_node, 'hasTag') and gui_node.hasTag("gui_image_path"):
gui_info["image_path"] = gui_node.getTag("gui_image_path")
elif gui_type == "3d_text":
if hasattr(gui_node,'hasTag') and gui_node.hasTag("gui_text"):
gui_info["text"] = gui_node.getTag("gui_text")
elif hasattr(gui_node,'node') and hasattr(gui_node.node(),'getText'):
gui_info["text"] = gui_node.node().getText()
elif gui_type == "3d_image":
if hasattr(gui_node,'hasTag') and gui_node.hasTag("gui_image_path"):
gui_info["image_path"] = gui_node.getTag("gui_image_path")
elif gui_type == "video_screen":
if hasattr(gui_node, 'hasTag') and gui_node.hasTag("video_path"):
gui_info["video_path"] = gui_node.getTag("video_path")
elif gui_type == "2d_video_screen":
if hasattr(gui_node, 'hasTag') and gui_node.hasTag("video_path"):
gui_info["video_path"] = gui_node.getTag("video_path")
elif gui_type == "virtual_screen":
if hasattr(gui_node, 'hasTag') and gui_node.hasTag("gui_text"):
gui_info["text"] = gui_node.getTag("gui_text")
elif gui_type in ["info_panel", "info_panel_3d"]:
# 收集信息面板的特定信息
if hasattr(gui_node, 'hasTag') and gui_node.hasTag("panel_id"):
gui_info["panel_id"] = gui_node.getTag("panel_id")
# 收集背景图片信息
if hasattr(gui_node, 'hasTag') and gui_node.hasTag("image_path"):
gui_info["image_path"] = gui_node.getTag("image_path")
# 收集GUI元素的可见性信息
user_visible = gui_node.getPythonTag("user_visible")
if user_visible is not None:
gui_info["user_visible"] = user_visible
else:
# 默认为可见
gui_info["user_visible"] = True
# 收集挂载的脚本信息
if hasattr(self.world, 'script_manager') and self.world.script_manager:
try:
script_manager = self.world.script_manager
scripts = script_manager.get_scripts_on_object(gui_node) # 修复:使用 gui_node 而不是 node
if scripts:
gui_info["scripts"] = []
for script_component in scripts:
try:
script_name = script_component.script_name
# 获取脚本路径
script_class = script_component.script_instance.__class__
script_file = self._get_script_file_path(script_class, script_name)
# 只有当脚本文件存在时才保存
if script_file and os.path.exists(script_file):
gui_info["scripts"].append({
"name": script_name,
"file": script_file
})
print(f"收集脚本信息: {script_name} from {script_file}")
else:
print(f"警告: 脚本文件不存在: {script_file}")
except Exception as e:
print(f"收集单个脚本信息失败 {script_name}, 错误: {e}")
continue
except Exception as e:
print(f"收集脚本信息失败: {e}")
print(f"成功收集GUI元素信息: {gui_info}")
return gui_info
except Exception as e:
print(f"收集GUI元素信息失败: {e}")
import traceback
traceback.print_exc()
return None
def saveScene(self, filename,project_path):
"""保存场景到BAM文件 - 完整版支持GUI元素,地形"""
try:
print(f"\n=== 开始保存场景到: {filename} ===")
# 确保文件路径是规范化的
filename = os.path.normpath(filename)
# 确保目录存在
directory = os.path.dirname(filename)
if directory and not os.path.exists(directory):
os.makedirs(directory)
resources_dir = os.path.join(directory,"resources")
if not os.path.exists(resources_dir):
os.makedirs(resources_dir)
# 存储需要临时隐藏的节点,以便保存后恢复
nodes_to_restore = []
# 查找并隐藏所有坐标轴和选择框节点
gizmo_nodes = self.world.render.findAllMatches("**/gizmo*")
selection_box_nodes = self.world.render.findAllMatches("**/selectionBox*")
# 隐藏坐标轴节点
for node in gizmo_nodes:
if not node.isHidden():
nodes_to_restore.append((node, True)) # (节点, 原先是否可见)
node.hide()
print(f"临时隐藏坐标轴节点: {node.getName()}")
# 隐藏选择框节点
for node in selection_box_nodes:
if not node.isHidden():
nodes_to_restore.append((node, True))
node.hide()
print(f"临时隐藏选择框节点: {node.getName()}")
# 收集所有需要保存的节点
all_nodes = []
all_nodes.extend(self.models)
all_nodes.extend(self.Spotlight)
all_nodes.extend(self.Pointlight)
def expand_scene_package_wrappers(nodes):
expanded_nodes = []
ssbo_editor = getattr(self.world, "ssbo_editor", None)
source_scene_model = None
source_scene_root = None
if ssbo_editor:
source_scene_model = getattr(ssbo_editor, "source_model", None)
source_scene_root = getattr(ssbo_editor, "source_model_root", None)
for node in nodes:
if not node or node.isEmpty():
continue
is_ssbo_runtime_root = (
ssbo_editor and
node == getattr(ssbo_editor, "model", None) and
source_scene_root and
not source_scene_root.isEmpty()
)
is_scene_wrapper = (
node.hasTag("scene_import_source") and
node.getTag("scene_import_source") == "project_scene_bam"
)
if not is_scene_wrapper and not is_ssbo_runtime_root:
expanded_nodes.append(node)
continue
source_children = []
source_snapshot = source_scene_root if is_ssbo_runtime_root else source_scene_model
if source_snapshot and not source_snapshot.isEmpty():
for child in source_snapshot.getChildren():
try:
if not child or child.isEmpty():
continue
if child.getName() in {"render", "render2d", "aspect2d"}:
continue
source_children.append(child)
except Exception:
continue
if source_children:
print(f"展开SSBO场景节点 {node.getName()} -> 使用原始场景树的 {len(source_children)} 个顶层子节点")
expanded_nodes.extend(source_children)
continue
print(f"SSBO场景节点 {node.getName()} 缺少原始场景树,回退为包装根节点保存")
expanded_nodes.append(node)
return expanded_nodes
all_nodes = expand_scene_package_wrappers(all_nodes)
# 创建用于保存GUI信息的JSON文件路径
gui_info_file = filename.replace('.bam', '_gui.json')
# 保存所有节点的信息
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()} 的变换信息")
# 保存可见性信息
user_visible = node.getPythonTag("user_visible")
if user_visible is not None:
node.setTag("user_visible", str(user_visible).lower())
print(f"保存节点 {node.getName()} 的可见性信息: {user_visible}")
# 保存父子关系信息 - 关键修改
parent = node.getParent()
if parent and not parent.isEmpty() and parent != self.world.render:
# 只有当父节点不是根节点且父节点是场景中的模型时才保存父子关系
if parent.getName() not in ["render", "aspect2d", "render2d"]:
# 检查父节点是否也是场景中的模型
is_parent_model = False
for model in self.models:
if model == parent:
is_parent_model = True
break
if is_parent_model:
node.setTag("parent_name", parent.getName())
print(f"保存节点 {node.getName()} 的父节点信息: {parent.getName()}")
# 获取当前状态
state = node.getState()
# 如果有材质属性,保存为标签
if state.hasAttrib(MaterialAttrib.getClassType()):
mat_attrib = state.getAttrib(MaterialAttrib.getClassType())
material = mat_attrib.getMaterial()
if material:
# 保存材质属性到标签
node.setTag("material_ambient", str(material.getAmbient()))
node.setTag("material_diffuse", str(material.getDiffuse()))
node.setTag("material_specular", str(material.getSpecular()))
node.setTag("material_emission", str(material.getEmission()))
node.setTag("material_shininess", str(material.getShininess()))
if material.hasBaseColor():
node.setTag("material_basecolor", str(material.getBaseColor()))
# 保存特定类型节点的额外信息
if node.hasTag("light_type"):
# 保存光源特定信息
light_obj = node.getPythonTag("rp_light_object")
if light_obj:
node.setTag("light_energy", str(light_obj.energy))
if node.hasTag("stored_energy"):
node.setTag("stored_energy", node.getTag("stored_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("is_model_root"):
# 保存模型动画相关信息
if node.hasTag("has_animations"):
node.setTag("saved_has_animations", node.getTag("has_animations"))
if node.hasTag("model_path"):
node.setTag("saved_model_path", node.getTag("model_path"))
if node.hasTag("can_create_actor_from_memory"):
node.setTag("saved_can_create_actor_from_memory", node.getTag("can_create_actor_from_memory"))
if hasattr(self.world,'script_manager') and self.world.script_manager:
script_manager = self.world.script_manager
scripts = script_manager.get_scripts_on_object(node)
if scripts:
node.setTag("has_scripts", "true")
script_info_list = []
for script_component in scripts:
script_name = script_component.script_name
print(f"保存脚本信息: {script_name}")
# 获取脚本类的文件路径
script_class = script_component.script_instance.__class__
script_file = self._get_script_file_path(script_class, script_name)
script_info_list.append({
"name": script_name,
"file": script_file
})
# 将脚本信息保存为JSON字符串
import json
node.setTag("scripts_info", json.dumps(script_info_list, ensure_ascii=False))
print(f"为节点 {node.getName()} 保存了 {len(script_info_list)} 个脚本")
try:
unique_nodes = []
for node in all_nodes:
if not node or node.isEmpty():
continue
if any(existing == node for existing in unique_nodes):
continue
unique_nodes.append(node)
save_root = NodePath(ModelRoot("SavedSceneRoot"))
def has_saved_parent(node):
parent = node.getParent()
if not parent or parent.isEmpty() or parent == self.world.render:
return False
return any(candidate == parent for candidate in unique_nodes)
def strip_runtime_render_state(root_np):
for current in [root_np] + list(root_np.findAllMatches("**")):
try:
current.clearShader()
except Exception:
pass
try:
current.clearAttrib(ShaderAttrib.getClassType())
except Exception:
pass
for node in unique_nodes:
if has_saved_parent(node):
continue
clone = node.copyTo(save_root)
strip_runtime_render_state(clone)
self.take_screenshot(project_path)
# 保存场景
success = save_root.writeBamFile(Filename.fromOsSpecific(filename))
if success:
print(f"✓ 场景保存成功: {filename}")
else:
print("✗ 场景保存失败")
return success
finally:
try:
if 'save_root' in locals() and save_root and not save_root.isEmpty():
save_root.removeNode()
except Exception:
pass
# 恢复之前隐藏的节点
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, retry_count=0):
"""从BAM文件加载场景"""
max_retries = 3
try:
print(f"\n=== 开始加载场景: {filename} (尝试 {retry_count + 1}/{max_retries + 1}) ===")
# 确保文件路径是规范化的
filename = os.path.normpath(filename)
# print(f"[DEBUG] 规范化后的文件路径: {filename}")
# 检查文件是否存在
if not os.path.exists(filename):
print(f"场景文件不存在: {filename}")
return False
# print(f"[DEBUG] 文件大小: {os.path.getsize(filename)} bytes")
# 检查文件是否损坏
if os.path.getsize(filename) == 0:
# print(f"[DEBUG] 错误: 场景文件为空")
return False
if retry_count == 0:
try:
selection = getattr(self.world, "selection", None)
if selection:
selection.clearSelection()
except Exception as e:
print(f"清理选择状态失败: {e}")
try:
script_manager = getattr(self.world, "script_manager", None)
if script_manager and hasattr(script_manager, "reset_scene_state"):
script_manager.reset_scene_state()
except Exception as e:
print(f"清理脚本场景状态失败: {e}")
try:
ssbo_editor = getattr(self.world, "ssbo_editor", None)
if ssbo_editor and hasattr(ssbo_editor, "reset_scene_state"):
ssbo_editor.reset_scene_state()
except Exception as e:
print(f"清理 SSBO 场景状态失败: {e}")
# 预防性清理确保Panda3D处于稳定状态
if retry_count > 0:
# print(f"[DEBUG] 执行预防性清理...")
try:
# 清理所有可能的残留状态
from panda3d.core import ModelPool
ModelPool.releaseAllModels()
# 强制垃圾回收
import gc
gc.collect()
# 清理渲染状态
if hasattr(self.world, 'render') and self.world.render:
# 清理渲染节点的子节点(保留基本节点)
children_to_remove = []
for i in range(self.world.render.getNumChildren()):
child = self.world.render.getChild(i)
if child.getName() not in ['camera', 'alight', 'dlight', 'ambient', 'render']:
children_to_remove.append(child)
for child in children_to_remove:
if not child.isEmpty():
child.removeNode()
# print(f"[DEBUG] 清理了 {len(children_to_remove)} 个渲染子节点")
except Exception as cleanup_error:
# print(f"[DEBUG] 预防性清理时发生异常: {cleanup_error}")
pass
# print(f"[DEBUG] 预加载检查完成,开始正式加载...")
tree_widget = self._get_tree_widget()
# print(f"[DEBUG] 获取树形控件: {tree_widget is not None}")
# 清除当前场景
print("\n清除当前场景...")
# print(f"[DEBUG] 需要清理的模型数量: {len(self.models)}")
for i, model in enumerate(self.models):
# print(f"[DEBUG] 清理模型 {i+1}/{len(self.models)}: {model.getName() if model else 'None'}")
if tree_widget:
tree_widget.delete_item(model)
else:
# 直接移除节点
if not model.isEmpty():
model.removeNode()
self.models.clear()
# print(f"[DEBUG] 模型清理完成")
# 清除灯光
for light_node in self.Spotlight:
if tree_widget:
tree_widget.delete_item(light_node)
else:
if not light_node.isEmpty():
light_node.removeNode()
for light_node in self.Pointlight:
if tree_widget:
tree_widget.delete_item(light_node)
else:
if not light_node.isEmpty():
light_node.removeNode()
if hasattr(self.world, 'terrain_manager') and self.world.terrain_manager and hasattr(self.world.terrain_manager, 'terrains'):
for terrain in self.world.terrain_manager.terrains:
if tree_widget:
tree_widget.delete_item(terrain)
else:
if terrain and not terrain.isEmpty():
terrain.removeNode()
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()
# 清理可能存在的辅助节点
self._cleanupAuxiliaryNodes()
self._cleanup_untracked_render_children()
# 加载场景
# print(f"[DEBUG] 开始加载BAM文件...")
# 安全清理措施
try:
# 1. 清理Panda3D模型缓存
from panda3d.core import ModelPool
ModelPool.releaseAllModels()
# print(f"[DEBUG] 模型缓存已清理")
# 2. 强制垃圾回收
import gc
gc.collect()
# print(f"[DEBUG] 垃圾回收完成")
# 3. 检查文件状态
if not os.access(filename, os.R_OK):
# print(f"[DEBUG] 文件不可读: {filename}")
return False
# 4. 使用更安全的加载方式
# print(f"[DEBUG] 尝试加载BAM文件...")
# 设置加载选项
from panda3d.core import LoaderOptions
loader_options = LoaderOptions()
loader_options.setFlags(loader_options.LFNoCache) # 禁用缓存
# 兼容中文路径:优先使用宽字符接口,再回退常规接口。
candidate_files = []
for ctor_name in ("fromOsSpecificW", "from_os_specific_w", "fromOsSpecific", "from_os_specific"):
ctor = getattr(Filename, ctor_name, None)
if not ctor:
continue
try:
fn = ctor(filename)
key = fn.get_fullpath()
if key not in [c.get_fullpath() for c in candidate_files]:
candidate_files.append(fn)
except Exception:
continue
if not candidate_files:
candidate_files = [Filename(filename)]
scene = None
last_error = None
for panda_filename in candidate_files:
try:
scene = self.world.loader.loadModel(panda_filename, loader_options)
if scene and not scene.isEmpty():
break
except Exception as e:
last_error = e
scene = None
# 极端回退把场景复制到ASCII路径再加载规避少数编码问题
if (not scene or scene.isEmpty()) and os.path.exists(filename):
try:
fallback_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "_scene_load_cache")
os.makedirs(fallback_dir, exist_ok=True)
fallback_file = os.path.join(fallback_dir, "scene_fallback.bam")
shutil.copy2(filename, fallback_file)
scene = self.world.loader.loadModel(Filename.fromOsSpecific(fallback_file), loader_options)
if scene and not scene.isEmpty():
print("[SceneLoad] Fallback loaded from ASCII cache path.")
except Exception as e:
last_error = e
if not scene or scene.isEmpty():
print(f"场景加载失败: {filename}")
if last_error:
print(f"[SceneLoad] 最后错误: {last_error}")
return False
# print(f"[DEBUG] BAM文件加载成功")
except Exception as e:
# print(f"[DEBUG] BAM加载过程中发生异常: {e}")
import traceback
traceback.print_exc()
return False
# print(f"[DEBUG] BAM文件加载成功场景根节点: {scene.getName()}")
# print(f"[DEBUG] 场景子节点数量: {scene.getNumChildren()}")
# 验证场景节点的有效性
if scene.isEmpty():
# print(f"[DEBUG] 警告: 加载的场景节点为空")
return False
if not scene.hasParent():
# print(f"[DEBUG] 将场景节点临时挂载到render")
scene.reparentTo(self.world.render)
if tree_widget:
# print(f"[DEBUG] 创建模型项...")
try:
tree_widget.create_model_items(scene)
# print(f"[DEBUG] 模型项创建完成")
except Exception as e:
# print(f"[DEBUG] 创建模型项失败: {e}")
# 继续执行,不影响场景加载
pass
else:
# print(f"[DEBUG] 树形控件为空,跳过创建模型项")
pass
# 遍历场景中的所有模型节点
# 用于存储处理后的灯光节点,避免重复处理
processed_lights = []
# 用于存储处理后的GUI元素避免重复处理
#存储所有加载的节点,用于后续处理父子关系
loaded_nodes = {} #name->nodePath映射
use_ssbo_scene_import = bool(
getattr(self.world, "use_ssbo_mouse_picking", False) and
getattr(self.world, "use_ssbo_scene_import", False) and
getattr(self.world, "ssbo_editor", None) and
callable(getattr(self.world, "_import_model_for_runtime", None))
)
ssbo_scene_model = None
if use_ssbo_scene_import:
try:
print(f"[SSBO] 打开项目使用统一导入链路: {filename}")
ssbo_scene_model = self.world._import_model_for_runtime(
filename,
scene_package_import=True,
)
if ssbo_scene_model and not ssbo_scene_model.isEmpty():
if ssbo_scene_model not in self.models:
self.models.append(ssbo_scene_model)
else:
print("[SSBO] 统一导入未返回有效模型,回退旧流程。")
use_ssbo_scene_import = False
except Exception as e:
print(f"[SSBO] 统一导入失败,回退旧流程: {e}")
use_ssbo_scene_import = False
# 遍历场景中的所有节点
def processNode(nodePath, depth=0):
indent = " " * depth
# print(f"{indent}处理节点: {nodePath.getName()} (类型: {type(nodePath.node()).__name__})")
# 检查节点是否有效
if nodePath.isEmpty():
# print(f"{indent}[DEBUG] 节点为空,跳过处理")
return
#存储节点以便后续处理父子关系
loaded_nodes[nodePath.getName()] = nodePath
if nodePath.getName().startswith('ground'):
print(f"{indent}跳过ground节点: {nodePath.getName()}")
return
# 跳过render节点的递归
if nodePath.getName() == "render" and depth > 0:
print(f"{indent}跳过重复的render节点")
return
# 跳过光源节点
if nodePath.getName() in ["alight", "dlight"]:
print(f"{indent}跳过光源节点: {nodePath.getName()}")
return
# 跳过相机节点
if nodePath.getName() in ["camera", "cam"]:
print(f"{indent}跳过相机节点: {nodePath.getName()}")
return
# 跳过辅助节点
if nodePath.getName().startswith(("gizmo", "selectionBox")):
print(f"{indent}跳过辅助节点: {nodePath.getName()}")
return
if nodePath.getName() in ['SceneRoot'] or \
any(keyword in nodePath.getName() for keyword in ["Skybox", "skybox"]):
print(f"{indent}跳过环境节点:{nodePath.getName()}")
return
# 检查是否是用户创建的场景元素
is_scene_element = (
nodePath.hasTag("is_scene_element") or
nodePath.hasTag("is_model_root") or
nodePath.hasTag("light_type") or
nodePath.hasTag("gui_type") or # 检查gui_type标签
nodePath.hasTag("is_gui_element") or
nodePath.hasTag("saved_gui_type") or
(nodePath.hasTag("element_type") and nodePath.getTag("element_type") == "info_panel")
)
# 特殊处理检查节点名称是否包含GUI相关关键词
is_potential_gui = any(keyword in nodePath.getName().lower() for keyword in
["gui", "button", "label", "entry", "image", "video", "screen", "text"])
if is_scene_element or is_potential_gui:
print(f"{indent}找到场景元素节点: {nodePath.getName()}")
is_model_root = nodePath.hasTag("is_model_root")
# 如果是潜在的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")
# 保留原始材质/颜色状态,避免在场景恢复阶段造成黑模。
# 恢复变换信息
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}")
# 恢复可见性状态
user_visible = True
if nodePath.hasTag("user_visible"):
user_visible = nodePath.getTag("user_visible").lower() == "true"
# 设置用户可见性标记
nodePath.setPythonTag("user_visible", user_visible)
# 应用可见性状态
if hasattr(self.world, 'property_panel'):
self.world.property_panel._syncEffectiveVisibility(nodePath)
else:
# 如果没有属性面板,直接应用可见性
if user_visible:
nodePath.show()
else:
nodePath.hide()
if (
nodePath.hasTag("has_scripts")
and nodePath.getTag("has_scripts") == "true"
and not (use_ssbo_scene_import and is_model_root)
):
if hasattr(self.world,'script_manager') and self.world.script_manager:
try:
import json
scripts_info = json.loads(nodePath.getTag("scripts_info"))
print(f"节点 {nodePath.getName()} 需要重新挂载 {len(scripts_info)} 个脚本")
script_manager = self.world.script_manager
for script_info in scripts_info:
script_name = script_info["name"]
script_file = script_info.get("file","")
print(f"尝试重新挂载脚本{script_name}from {script_file}")
if script_name not in script_manager.loader.script_classes:
if script_file and os.path.exists(script_file):
print(f"从文件加载脚本:{script_file}")
loaded_class = script_manager.load_script_from_file(script_file)
if loaded_class is None:
print(f"从文件加载脚本失败{script_file}")
script_path = self._find_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}")
if script_name in script_manager.loader.script_classes:
script_component = script_manager.add_script_to_object(nodePath,script_name)
if script_component:
print(f"成功为 {nodePath.getName()} 添加脚本: {script_name}")
else:
print(f"{nodePath.getName()} 添加脚本失败: {script_name}")
else:
print(f"脚本 {script_name} 不可用,跳过挂载")
except Exception as e:
print(f"重新挂载脚本失败: {e}")
import traceback
traceback.print_exc()
# 恢复材质属性
def parseColor(color_str):
"""解析颜色字符串为Vec4"""
try:
color_str = color_str.replace('LVecBase4f', '').strip('()')
r, g, b, a = map(float, color_str.split(','))
return Vec4(r, g, b, a)
except:
return Vec4(1, 1, 1, 1)
if not is_model_root:
# 创建并恢复材质
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': {}
}
# 将节点重新挂载到render下如果需要
# 注意GUI元素可能需要挂载到特定的父节点上
if nodePath.hasTag("gui_type") or nodePath.hasTag("is_gui_element"):
# GUI元素通常应该挂载到aspect2d或特定的GUI父节点上
# 这里我们先保持原挂载关系
pass
else:
if use_ssbo_scene_import and is_model_root:
# SSBO 模式下模型节点由新导入链路接管,这里不直接挂载。
pass
# 其他节点确保挂载到render下
elif nodePath.getParent() != self.world.render and not nodePath.getName() in ["render",
"aspect2d",
"render2d"]:
nodePath.wrtReparentTo(self.world.render)
# 为模型节点设置碰撞检测
if is_model_root:
print(f"J{indent}处理模型节点{nodePath.getName()}")
if use_ssbo_scene_import:
# SSBO 模式下整个 scene.bam 已通过统一导入链路载入,
# 这里跳过逐模型旧导入逻辑,避免与菜单导入路径不一致。
pass
else:
#self._validateAndFixAllTransforms(nodePath)
self._fixModelStructure(nodePath)
self._restoreModelAnimationInfo(nodePath)
self._processModelAnimations(nodePath)
try:
ssbo_editor = getattr(self.world, "ssbo_editor", None)
repair_fn = getattr(ssbo_editor, "_repair_missing_textures", None) if ssbo_editor else None
if callable(repair_fn):
model_path = ""
for tag_name in ("model_path", "saved_model_path", "original_path", "file"):
if nodePath.hasTag(tag_name):
candidate = nodePath.getTag(tag_name).strip()
if candidate:
model_path = candidate
break
repair_fn(nodePath, model_path or filename)
except Exception as e:
print(f"[SceneLoad] 贴图修复失败: {e}")
# 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)
# SSBO 模式下模型已在前面统一导入;若失败已自动回退旧流程。
#处理父子关系 - 在所有节点加载完成后设置正确的父子关系
print("\n开始重建父子关系...")
self._rebuildParentChildRelationships(loaded_nodes)
# 加载GUI信息并重新创建非3D的GUI元素
gui_info_file = filename.replace('.bam', '_gui.json')
if os.path.exists(gui_info_file):
try:
with open(gui_info_file, 'r', encoding='utf-8') as f:
content = f.read().strip()
if content: # 检查文件是否为空
import json
gui_data = json.loads(content)
print(f"✓ 成功加载GUI信息文件: {gui_info_file}")
print(f" 发现 {len(gui_data)} 个GUI元素需要重建")
# 使用gui_manager重新创建GUI元素
#self._recreateGUIElementsFromData(gui_data)
else:
print(" GUI信息文件为空")
except json.JSONDecodeError as e:
print(f"✗ GUI信息文件格式错误: {e}")
except Exception as e:
print(f"✗ 加载GUI信息失败: {e}")
import traceback
traceback.print_exc()
else:
print(" 未找到GUI信息文件")
# 移除临时场景节点
if not scene.isEmpty():
scene.removeNode()
# 更新场景树
#self.updateSceneTree()
#self._get_tree_widget().create_model_items(scene)
print("=== 场景加载完成 ===\n")
return True
except Exception as e:
print(f"=== 场景加载失败 ===")
print(f"加载场景时发生错误: {str(e)}")
import traceback
traceback.print_exc()
# 重试机制
if retry_count < 3:
print(f"[DEBUG] 准备重试... ({retry_count + 1}/{3})")
try:
# 清理状态
import gc
gc.collect()
from panda3d.core import ModelPool
ModelPool.releaseAllModels()
# 等待一小段时间后重试
import time
time.sleep(0.5)
return self.loadScene(filename, retry_count + 1)
except Exception as retry_error:
print(f"[DEBUG] 重试失败: {retry_error}")
return False
def _rebuildParentChildRelationships(self, loaded_nodes):
try:
parent_child_relations = []
for node_name, node in loaded_nodes.items():
if node.hasTag("parent_name"):
parent_name = node.getTag("parent_name")
if parent_name in loaded_nodes:
parent_child_relations.append((node, loaded_nodes[parent_name])) # 修复:应该是元组
print(f"发现父子关系:{parent_name}->{node_name}")
else:
print(f"警告:节点{node_name}的父节点{parent_name}不存在")
for child_node, parent_node in parent_child_relations:
try:
child_node.wrtReparentTo(parent_node)
print(f"成功设置父子关系:{parent_node.getName()}->{child_node.getName()}")
except Exception as e:
print(f"设置父子关系失败{parent_node.getName()}->{child_node.getName()}:{e}")
if not parent_child_relations:
print("尝试从场景结构推断父子关系")
self._inferParentChildRelationships(loaded_nodes)
print("父子关系重建完成")
except Exception as e:
print(f"重建父子关系时出错: {e}")
import traceback
traceback.print_exc()
except Exception as e:
print(f"重建父子关系时出错: {e}")
import traceback
traceback.print_exc()
def _inferParentChildRelationships(self, loaded_nodes):
"""从场景结构推断父子关系"""
try:
# 这里可以添加更复杂的父子关系推断逻辑
# 例如,根据节点名称、位置关系等进行推断
# 目前保持简单,后续可以扩展
print("父子关系推断完成(当前为空实现)")
except Exception as e:
print(f"推断父子关系时出错: {e}")
def _shouldSkipNodeInTree(self, nodePath):
"""判断节点是否应该在场景树中跳过显示"""
if nodePath.getName().startswith('ground'):
return True
# 跳过render节点的递归
if nodePath.getName() == "render":
return True
# 跳过光源节点
if nodePath.getName() in ["alight", "dlight"]:
return True
# 跳过相机节点
if nodePath.getName() in ["camera", "cam"]:
return True
# 跳过3D文本和3D图像节点
if (hasattr(nodePath.node(), "hasTag") and
nodePath.node().hasTag("gui_type") and
nodePath.node().getTag("gui_type") in ["3d_text", "3d_image"]):
return True
# 跳过辅助节点
if nodePath.getName().startswith(("gizmo", "selectionBox")):
return True
return False
def _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