1853 lines
87 KiB
Python
1853 lines
87 KiB
Python
"""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, TextureAttrib
|
||
)
|
||
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 _get_scene_sidecar_path(self, filename, suffix):
|
||
scene_root, _ = os.path.splitext(os.path.normpath(filename))
|
||
return f"{scene_root}{suffix}"
|
||
|
||
def _reset_lui_runtime_state(self, lui_manager):
|
||
if lui_manager is None or getattr(lui_manager, "lui_enabled", True) is False:
|
||
return True
|
||
|
||
apply_fn = getattr(lui_manager, "_apply_lui_structure_snapshot", None)
|
||
if callable(apply_fn):
|
||
try:
|
||
apply_fn({})
|
||
return True
|
||
except Exception as e:
|
||
print(f"清空LUI结构失败: {e}")
|
||
return False
|
||
|
||
clear_fn = getattr(lui_manager, "_clear_lui_structure_runtime", None)
|
||
if callable(clear_fn):
|
||
try:
|
||
clear_fn()
|
||
return True
|
||
except Exception as e:
|
||
print(f"清空LUI结构失败: {e}")
|
||
return False
|
||
|
||
return True
|
||
|
||
def _save_lui_snapshot_file(self, filename):
|
||
lui_manager = getattr(self.world, "lui_manager", None)
|
||
if lui_manager is None or getattr(lui_manager, "lui_enabled", True) is False:
|
||
print("ℹ️ 当前会话未启用LUI,跳过LUI结构保存")
|
||
return True
|
||
|
||
capture_fn = getattr(lui_manager, "_capture_lui_structure_snapshot", None)
|
||
if not callable(capture_fn):
|
||
print("ℹ️ 未找到LUI结构快照接口,跳过LUI结构保存")
|
||
return True
|
||
|
||
try:
|
||
snapshot = capture_fn() or {}
|
||
lui_info_file = self._get_scene_sidecar_path(filename, "_lui.json")
|
||
with open(lui_info_file, "w", encoding="utf-8") as f:
|
||
json.dump(snapshot, f, ensure_ascii=False, indent=4)
|
||
|
||
canvas_count = len(snapshot.get("canvases", []) or [])
|
||
component_count = len(snapshot.get("components", []) or [])
|
||
print(
|
||
f"✓ LUI信息已保存: {lui_info_file} "
|
||
f"(Canvas: {canvas_count}, Components: {component_count})"
|
||
)
|
||
return True
|
||
except Exception as e:
|
||
print(f"✗ 保存LUI信息失败: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
return False
|
||
|
||
def _load_lui_snapshot_file(self, filename):
|
||
lui_manager = getattr(self.world, "lui_manager", None)
|
||
if lui_manager is None or getattr(lui_manager, "lui_enabled", True) is False:
|
||
print("ℹ️ 当前会话未启用LUI,跳过LUI结构恢复")
|
||
return True
|
||
|
||
lui_info_file = self._get_scene_sidecar_path(filename, "_lui.json")
|
||
apply_fn = getattr(lui_manager, "_apply_lui_structure_snapshot", None)
|
||
|
||
if not os.path.exists(lui_info_file):
|
||
print("ℹ️ 未找到LUI信息文件,清空当前LUI结构")
|
||
return self._reset_lui_runtime_state(lui_manager)
|
||
|
||
if not callable(apply_fn):
|
||
print("ℹ️ 未找到LUI结构恢复接口,跳过LUI结构恢复")
|
||
return True
|
||
|
||
try:
|
||
with open(lui_info_file, "r", encoding="utf-8") as f:
|
||
content = f.read().strip()
|
||
|
||
snapshot = json.loads(content) if content else {}
|
||
apply_fn(snapshot)
|
||
|
||
canvas_count = len(snapshot.get("canvases", []) or [])
|
||
component_count = len(snapshot.get("components", []) or [])
|
||
print(
|
||
f"✓ LUI信息已恢复: {lui_info_file} "
|
||
f"(Canvas: {canvas_count}, Components: {component_count})"
|
||
)
|
||
return True
|
||
except json.JSONDecodeError as e:
|
||
print(f"✗ LUI信息文件格式错误: {e}")
|
||
except Exception as e:
|
||
print(f"✗ 加载LUI信息失败: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
|
||
return self._reset_lui_runtime_state(lui_manager)
|
||
|
||
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_file = script_manager.loader.find_script_file(script_name) or ""
|
||
script_reference = script_manager.build_script_reference(script_name, script_file)
|
||
# 只有当脚本文件存在时才保存
|
||
if script_reference.get("file") or script_reference.get("project_relative_path"):
|
||
gui_info["scripts"].append(script_reference)
|
||
print(f"收集脚本信息: {script_name} from {script_reference.get('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)
|
||
|
||
# SSBO模式下先把运行时编辑后的层级变换同步回source_model_root,
|
||
# 再从source树保存,避免把chunk_*运行时结构写入scene.bam。
|
||
ssbo_editor = getattr(self.world, "ssbo_editor", None)
|
||
if ssbo_editor:
|
||
snapshot_fn = getattr(ssbo_editor, "_snapshot_top_level_transforms_to_source_root", None)
|
||
if callable(snapshot_fn):
|
||
try:
|
||
snapshot_fn()
|
||
except Exception as e:
|
||
print(f"同步SSBO源场景树变换失败: {e}")
|
||
snapshot_material_fn = getattr(ssbo_editor, "_snapshot_runtime_materials_to_source_root", None)
|
||
if callable(snapshot_material_fn):
|
||
try:
|
||
snapshot_material_fn()
|
||
except Exception as e:
|
||
print(f"同步SSBO源场景树材质失败: {e}")
|
||
|
||
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
|
||
|
||
runtime_children = []
|
||
for child in node.getChildren():
|
||
try:
|
||
if not child or child.isEmpty():
|
||
continue
|
||
if child.getName() in {"render", "render2d", "aspect2d"}:
|
||
continue
|
||
runtime_children.append(child)
|
||
except Exception:
|
||
continue
|
||
|
||
if runtime_children:
|
||
print(f"展开SSBO场景节点 {node.getName()} -> 回退使用运行时场景树的 {len(runtime_children)} 个顶层子节点")
|
||
expanded_nodes.extend(runtime_children)
|
||
continue
|
||
|
||
print(f"SSBO场景节点 {node.getName()} 缺少可用场景树,回退为包装根节点保存")
|
||
expanded_nodes.append(node)
|
||
return expanded_nodes
|
||
|
||
all_nodes = expand_scene_package_wrappers(all_nodes)
|
||
|
||
def get_material_vector(material, *accessors):
|
||
for accessor_name in accessors:
|
||
accessor = getattr(material, accessor_name, None)
|
||
try:
|
||
value = accessor() if callable(accessor) else accessor
|
||
except Exception:
|
||
continue
|
||
if value is None:
|
||
continue
|
||
try:
|
||
return Vec4(
|
||
float(value.x),
|
||
float(value.y),
|
||
float(value.z),
|
||
float(getattr(value, "w", 1.0)),
|
||
)
|
||
except Exception:
|
||
continue
|
||
return None
|
||
|
||
def get_material_scalar(material, *accessors):
|
||
for accessor_name in accessors:
|
||
accessor = getattr(material, accessor_name, None)
|
||
try:
|
||
value = accessor() if callable(accessor) else accessor
|
||
except Exception:
|
||
continue
|
||
if value is None:
|
||
continue
|
||
try:
|
||
return float(value)
|
||
except Exception:
|
||
continue
|
||
return None
|
||
|
||
def save_material_tags(node, material):
|
||
if not material:
|
||
return
|
||
|
||
vector_tag_map = {
|
||
"material_ambient": ("getAmbient", "get_ambient", "ambient"),
|
||
"material_diffuse": ("getDiffuse", "get_diffuse", "diffuse"),
|
||
"material_specular": ("getSpecular", "get_specular", "specular"),
|
||
"material_emission": ("getEmission", "get_emission", "emission"),
|
||
"material_basecolor": ("getBaseColor", "get_base_color", "base_color"),
|
||
}
|
||
scalar_tag_map = {
|
||
"material_shininess": ("getShininess", "get_shininess", "shininess"),
|
||
"material_roughness": ("getRoughness", "get_roughness", "roughness"),
|
||
"material_metallic": ("getMetallic", "get_metallic", "metallic"),
|
||
"material_ior": ("getRefractiveIndex", "get_refractive_index", "refractive_index"),
|
||
}
|
||
|
||
for tag_name, accessors in vector_tag_map.items():
|
||
value = get_material_vector(material, *accessors)
|
||
if value is not None:
|
||
node.setTag(tag_name, str(value))
|
||
|
||
for tag_name, accessors in scalar_tag_map.items():
|
||
value = get_material_scalar(material, *accessors)
|
||
if value is not None:
|
||
node.setTag(tag_name, str(value))
|
||
|
||
# 创建用于保存GUI信息的JSON文件路径
|
||
gui_info_file = filename.replace('.bam', '_gui.json')
|
||
|
||
# 保存所有节点的信息
|
||
for node in all_nodes:
|
||
if node.isEmpty():
|
||
continue
|
||
|
||
if node.hasTag("is_model_root"):
|
||
try:
|
||
managed_by_ssbo = False
|
||
ssbo_editor = getattr(self.world, "ssbo_editor", None)
|
||
if ssbo_editor and hasattr(ssbo_editor, "is_source_tree_node"):
|
||
managed_by_ssbo = bool(ssbo_editor.is_source_tree_node(node))
|
||
node.setTag("ssbo_managed", "true" if managed_by_ssbo else "false")
|
||
except Exception:
|
||
pass
|
||
|
||
# 保存变换信息
|
||
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()
|
||
|
||
# 如果有材质属性,保存为标签
|
||
material = None
|
||
if state.hasAttrib(MaterialAttrib.getClassType()):
|
||
mat_attrib = state.getAttrib(MaterialAttrib.getClassType())
|
||
material = mat_attrib.getMaterial() if mat_attrib else None
|
||
if material is None:
|
||
try:
|
||
if node.hasMaterial():
|
||
material = node.getMaterial()
|
||
except Exception:
|
||
material = None
|
||
if material:
|
||
save_material_tags(node, material)
|
||
|
||
# 保存特定类型节点的额外信息
|
||
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_file = script_manager.loader.find_script_file(script_name) or ""
|
||
script_info_list.append(
|
||
script_manager.build_script_reference(script_name, 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:
|
||
keep_material_shader = any(
|
||
current.hasTag(tag_name)
|
||
for tag_name in (
|
||
"material_effect_metallic_enabled",
|
||
"material_effect_default_texture_enabled",
|
||
"material_effect_parallax_enabled",
|
||
"material_render_effect_signature",
|
||
"material_texture_diffuse",
|
||
"material_texture_normal",
|
||
"material_texture_ior",
|
||
"material_texture_roughness",
|
||
"material_texture_parallax",
|
||
"material_texture_metallic",
|
||
"material_texture_emission",
|
||
"material_texture_ao",
|
||
"material_texture_alpha",
|
||
"material_texture_detail",
|
||
"material_texture_gloss",
|
||
)
|
||
)
|
||
except Exception:
|
||
keep_material_shader = False
|
||
if keep_material_shader:
|
||
continue
|
||
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}")
|
||
if not self._save_lui_snapshot_file(filename):
|
||
return False
|
||
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 not ssbo_scene_model or ssbo_scene_model.isEmpty():
|
||
print("[SSBO] 统一导入未返回有效模型,回退旧流程。")
|
||
use_ssbo_scene_import = False
|
||
except Exception as e:
|
||
print(f"[SSBO] 统一导入失败,回退旧流程: {e}")
|
||
use_ssbo_scene_import = False
|
||
|
||
def node_should_use_ssbo_runtime(node_path):
|
||
if not (use_ssbo_scene_import and node_path and not node_path.isEmpty()):
|
||
return False
|
||
if not node_path.hasTag("is_model_root"):
|
||
return False
|
||
|
||
def tag_is_enabled(tag_name, default=False):
|
||
try:
|
||
if not node_path.hasTag(tag_name):
|
||
return bool(default)
|
||
value = str(node_path.getTag(tag_name) or "").strip().lower()
|
||
if value in ("1", "true", "yes", "on"):
|
||
return True
|
||
if value in ("0", "false", "no", "off"):
|
||
return False
|
||
except Exception:
|
||
pass
|
||
return bool(default)
|
||
|
||
has_saved_animation = (
|
||
tag_is_enabled("saved_has_animations", default=False)
|
||
or tag_is_enabled("has_animations", default=False)
|
||
)
|
||
if not has_saved_animation:
|
||
try:
|
||
if node_path.hasTag("gltf_animation_count"):
|
||
has_saved_animation = int(str(node_path.getTag("gltf_animation_count") or "0").strip() or "0") > 0
|
||
except Exception:
|
||
has_saved_animation = False
|
||
|
||
if has_saved_animation:
|
||
return False
|
||
|
||
if node_path.hasTag("ssbo_managed"):
|
||
return tag_is_enabled("ssbo_managed", default=True)
|
||
|
||
return True
|
||
|
||
def parse_saved_color(color_str, default=None):
|
||
try:
|
||
cleaned = str(color_str).strip()
|
||
for prefix in ("LVecBase4f", "Vec4", "LColor"):
|
||
cleaned = cleaned.replace(prefix, "")
|
||
cleaned = cleaned.strip("() ")
|
||
components = [component.strip() for component in cleaned.split(",") if component.strip()]
|
||
if len(components) == 3:
|
||
components.append("1")
|
||
if len(components) != 4:
|
||
raise ValueError(f"invalid color component count: {cleaned}")
|
||
return Vec4(*map(float, components[:4]))
|
||
except Exception:
|
||
return default if default is not None else Vec4(1, 1, 1, 1)
|
||
|
||
def parse_saved_scalar(value):
|
||
try:
|
||
return float(value)
|
||
except Exception:
|
||
return None
|
||
|
||
def set_material_scalar(material, value, *setter_names):
|
||
if value is None:
|
||
return False
|
||
for setter_name in setter_names:
|
||
setter = getattr(material, setter_name, None)
|
||
if callable(setter):
|
||
setter(float(value))
|
||
return True
|
||
return False
|
||
|
||
def resolve_existing_material(node_path):
|
||
try:
|
||
if node_path.hasMaterial():
|
||
material = node_path.getMaterial()
|
||
if material is not None:
|
||
return material
|
||
except Exception:
|
||
pass
|
||
|
||
try:
|
||
node_state = node_path.getState()
|
||
if node_state.hasAttrib(MaterialAttrib.getClassType()):
|
||
material_attrib = node_state.getAttrib(MaterialAttrib.getClassType())
|
||
material = material_attrib.getMaterial() if material_attrib else None
|
||
if material is not None:
|
||
return material
|
||
except Exception:
|
||
pass
|
||
|
||
geom_paths = []
|
||
try:
|
||
if isinstance(node_path.node(), GeomNode):
|
||
geom_paths.append(node_path)
|
||
except Exception:
|
||
pass
|
||
try:
|
||
geom_paths.extend(list(node_path.findAllMatches("**/+GeomNode")))
|
||
except Exception:
|
||
pass
|
||
|
||
seen_paths = set()
|
||
for geom_path in geom_paths:
|
||
try:
|
||
key = geom_path.getKey()
|
||
except Exception:
|
||
key = id(geom_path)
|
||
if key in seen_paths:
|
||
continue
|
||
seen_paths.add(key)
|
||
|
||
try:
|
||
if geom_path.hasMaterial():
|
||
material = geom_path.getMaterial()
|
||
if material is not None:
|
||
return material
|
||
except Exception:
|
||
pass
|
||
|
||
try:
|
||
geom_node = geom_path.node()
|
||
for geom_index in range(geom_node.getNumGeoms()):
|
||
geom_state = geom_node.getGeomState(geom_index)
|
||
if not geom_state.hasAttrib(MaterialAttrib.getClassType()):
|
||
continue
|
||
material_attrib = geom_state.getAttrib(MaterialAttrib.getClassType())
|
||
material = material_attrib.getMaterial() if material_attrib else None
|
||
if material is not None:
|
||
return material
|
||
except Exception:
|
||
pass
|
||
|
||
try:
|
||
net_state = geom_path.getNetState()
|
||
if net_state.hasAttrib(MaterialAttrib.getClassType()):
|
||
material_attrib = net_state.getAttrib(MaterialAttrib.getClassType())
|
||
material = material_attrib.getMaterial() if material_attrib else None
|
||
if material is not None:
|
||
return material
|
||
except Exception:
|
||
pass
|
||
|
||
return None
|
||
|
||
def restore_saved_material_fallback(node_path):
|
||
material_tag_names = (
|
||
"material_ambient",
|
||
"material_diffuse",
|
||
"material_specular",
|
||
"material_emission",
|
||
"material_shininess",
|
||
"material_basecolor",
|
||
"material_roughness",
|
||
"material_metallic",
|
||
"material_ior",
|
||
)
|
||
if not any(node_path.hasTag(tag_name) for tag_name in material_tag_names):
|
||
return False
|
||
|
||
# BAM already carries the real material object; tags are only a legacy fallback.
|
||
if resolve_existing_material(node_path) is not None:
|
||
return False
|
||
|
||
material = Material()
|
||
material_changed = False
|
||
|
||
if node_path.hasTag("material_basecolor"):
|
||
base_color = parse_saved_color(node_path.getTag("material_basecolor"))
|
||
try:
|
||
if hasattr(material, "set_base_color"):
|
||
material.set_base_color(base_color)
|
||
elif hasattr(material, "setBaseColor"):
|
||
material.setBaseColor(base_color)
|
||
elif hasattr(material, "setDiffuse"):
|
||
material.setDiffuse(base_color)
|
||
material_changed = True
|
||
except Exception:
|
||
pass
|
||
|
||
if node_path.hasTag("material_emission"):
|
||
emission = parse_saved_color(node_path.getTag("material_emission"))
|
||
try:
|
||
if hasattr(material, "set_emission"):
|
||
material.set_emission(emission)
|
||
material_changed = True
|
||
elif hasattr(material, "setEmission"):
|
||
material.setEmission(emission)
|
||
material_changed = True
|
||
except Exception:
|
||
pass
|
||
|
||
if set_material_scalar(
|
||
material,
|
||
parse_saved_scalar(node_path.getTag("material_roughness")) if node_path.hasTag("material_roughness") else None,
|
||
"set_roughness",
|
||
"setRoughness",
|
||
):
|
||
material_changed = True
|
||
|
||
if set_material_scalar(
|
||
material,
|
||
parse_saved_scalar(node_path.getTag("material_metallic")) if node_path.hasTag("material_metallic") else None,
|
||
"set_metallic",
|
||
"setMetallic",
|
||
):
|
||
material_changed = True
|
||
|
||
if set_material_scalar(
|
||
material,
|
||
parse_saved_scalar(node_path.getTag("material_ior")) if node_path.hasTag("material_ior") else None,
|
||
"set_refractive_index",
|
||
"setRefractiveIndex",
|
||
):
|
||
material_changed = True
|
||
|
||
has_modern_pbr_tags = any(
|
||
node_path.hasTag(tag_name)
|
||
for tag_name in (
|
||
"material_basecolor",
|
||
"material_roughness",
|
||
"material_metallic",
|
||
"material_ior",
|
||
)
|
||
)
|
||
|
||
if not has_modern_pbr_tags:
|
||
if node_path.hasTag("material_ambient"):
|
||
material.setAmbient(parse_saved_color(node_path.getTag("material_ambient")))
|
||
material_changed = True
|
||
|
||
if node_path.hasTag("material_diffuse"):
|
||
material.setDiffuse(parse_saved_color(node_path.getTag("material_diffuse")))
|
||
material_changed = True
|
||
|
||
if node_path.hasTag("material_specular"):
|
||
material.setSpecular(parse_saved_color(node_path.getTag("material_specular")))
|
||
material_changed = True
|
||
|
||
if set_material_scalar(
|
||
material,
|
||
parse_saved_scalar(node_path.getTag("material_shininess")) if node_path.hasTag("material_shininess") else None,
|
||
"set_shininess",
|
||
"setShininess",
|
||
):
|
||
material_changed = True
|
||
|
||
if material_changed:
|
||
node_path.setMaterial(material, 1)
|
||
return True
|
||
|
||
return 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_manager.resolve_script_path(script_info)
|
||
|
||
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()
|
||
|
||
|
||
# SSBO场景包导入时,禁止旧的标签材质回放,
|
||
# 避免覆盖BAM内真实PBR/贴图状态导致发黑或金属贴图丢失。
|
||
if not use_ssbo_scene_import:
|
||
restore_saved_material_fallback(nodePath)
|
||
|
||
# 恢复颜色属性
|
||
if nodePath.hasTag("color"):
|
||
nodePath.setColor(parse_saved_color(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 node_should_use_ssbo_runtime(nodePath):
|
||
# 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 node_should_use_ssbo_runtime(nodePath):
|
||
# SSBO 模式下整个 scene.bam 已通过统一导入链路载入,
|
||
# 这里跳过逐模型旧导入逻辑,避免与菜单导入路径不一致。
|
||
pass
|
||
else:
|
||
#self._validateAndFixAllTransforms(nodePath)
|
||
nodePath.setTag("ssbo_managed", "false")
|
||
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)
|
||
if nodePath not in self.models:
|
||
self.models.append(nodePath)
|
||
|
||
# 递归处理子节点
|
||
for child in nodePath.getChildren():
|
||
processNode(child, depth + 1)
|
||
|
||
print("\n开始处理场景节点...")
|
||
processNode(scene)
|
||
|
||
if use_ssbo_scene_import:
|
||
try:
|
||
ssbo_editor = getattr(self.world, "ssbo_editor", None)
|
||
source_root = getattr(ssbo_editor, "source_model_root", None) if ssbo_editor else None
|
||
cache_base_mat = getattr(ssbo_editor, "_cache_top_level_source_child_base_mat", None) if ssbo_editor else None
|
||
refresh_runtime = getattr(ssbo_editor, "refresh_runtime_from_source", None) if ssbo_editor else None
|
||
synced_transforms = 0
|
||
if source_root and not source_root.isEmpty():
|
||
for source_child in source_root.getChildren():
|
||
if not source_child or source_child.isEmpty():
|
||
continue
|
||
child_name = source_child.getName()
|
||
matched_node = loaded_nodes.get(child_name)
|
||
if not matched_node or matched_node.isEmpty():
|
||
continue
|
||
if matched_node.hasTag("ssbo_managed") and matched_node.getTag("ssbo_managed").strip().lower() in ("0", "false", "no", "off"):
|
||
continue
|
||
try:
|
||
local_mat = matched_node.getMat(scene)
|
||
except Exception:
|
||
try:
|
||
local_mat = matched_node.get_mat(scene)
|
||
except Exception:
|
||
continue
|
||
try:
|
||
source_child.setMat(local_mat)
|
||
except Exception:
|
||
try:
|
||
source_child.set_mat(local_mat)
|
||
except Exception:
|
||
continue
|
||
if callable(cache_base_mat):
|
||
try:
|
||
cache_base_mat(source_child, local_mat)
|
||
except Exception:
|
||
pass
|
||
synced_transforms += 1
|
||
|
||
if synced_transforms and callable(refresh_runtime):
|
||
print(f"[SceneLoad] 已同步SSBO顶层模型变换: {synced_transforms}")
|
||
refresh_runtime(preserve_selection=False)
|
||
except Exception as e:
|
||
print(f"[SceneLoad] 同步SSBO顶层模型变换失败: {e}")
|
||
|
||
# SSBO 模式下模型已在前面统一导入;若失败已自动回退旧流程。
|
||
|
||
#处理父子关系 - 在所有节点加载完成后设置正确的父子关系
|
||
print("\n开始重建父子关系...")
|
||
self._rebuildParentChildRelationships(loaded_nodes)
|
||
|
||
# 重新应用材质渲染effect(尤其是金属性贴图所需的pbr_with_metallic)
|
||
# 标签会从BAM读取,但effect不会自动重建,导致重开后金属性样式丢失。
|
||
try:
|
||
property_helpers = getattr(self.world, "property_helpers", None)
|
||
sync_effect_fn = getattr(property_helpers, "_sync_material_render_effect", None) if property_helpers else None
|
||
if callable(sync_effect_fn):
|
||
effect_synced = 0
|
||
effect_nodes = []
|
||
ssbo_controller = None
|
||
if use_ssbo_scene_import:
|
||
ssbo_editor = getattr(self.world, "ssbo_editor", None)
|
||
controller = getattr(ssbo_editor, "controller", None) if ssbo_editor else None
|
||
ssbo_controller = controller
|
||
if controller:
|
||
# SSBO模式优先对运行时对象重建effect(真实显示对象)。
|
||
effect_nodes = list(controller.id_to_object_np.values())
|
||
if not effect_nodes:
|
||
effect_nodes = list(loaded_nodes.values())
|
||
|
||
def _has_metallic_texture(np):
|
||
try:
|
||
stages = np.findAllTextureStages()
|
||
for si in range(stages.getNumTextureStages()):
|
||
stage = stages.getTextureStage(si)
|
||
if not stage:
|
||
continue
|
||
name = (stage.getName() or "").lower()
|
||
if "metallic" in name:
|
||
return True
|
||
try:
|
||
if int(stage.getSort()) == 5:
|
||
return True
|
||
except Exception:
|
||
pass
|
||
except Exception:
|
||
pass
|
||
return False
|
||
|
||
for node in effect_nodes:
|
||
if not node or node.isEmpty():
|
||
continue
|
||
# 关键修复:按对象自身纹理槽决定 effect,避免根节点标签导致整组对象统一金属性/发黑
|
||
has_metallic = _has_metallic_texture(node) or node.hasTag("material_effect_metallic_enabled")
|
||
has_any_effect = any(
|
||
node.hasTag(tag_name)
|
||
for tag_name in (
|
||
"material_effect_default_texture_enabled",
|
||
"material_effect_parallax_enabled",
|
||
"material_render_effect_signature",
|
||
)
|
||
)
|
||
has_effect_tags = has_metallic or has_any_effect
|
||
if not has_effect_tags:
|
||
continue
|
||
try:
|
||
if has_metallic:
|
||
node.setTag("material_effect_metallic_enabled", "1")
|
||
elif node.hasTag("material_effect_metallic_enabled"):
|
||
node.clearTag("material_effect_metallic_enabled")
|
||
sync_effect_fn(node, force=True, source_node=node)
|
||
effect_synced += 1
|
||
except Exception:
|
||
continue
|
||
if effect_synced:
|
||
print(f"[SceneLoad] 已重建材质effect节点数量: {effect_synced}")
|
||
# 关键修复:effect应用在动态对象后,需要重新烘焙静态chunk。
|
||
# 否则未选中时看到的仍是旧静态副本,点击切到动态对象才会正常。
|
||
if use_ssbo_scene_import and ssbo_controller and effect_synced:
|
||
rebuilt_chunks = 0
|
||
try:
|
||
for chunk_id in sorted(ssbo_controller.chunks):
|
||
try:
|
||
ssbo_controller._rebuild_static_chunk(chunk_id)
|
||
ssbo_controller._set_chunk_dynamic(chunk_id, False)
|
||
rebuilt_chunks += 1
|
||
except Exception:
|
||
continue
|
||
if rebuilt_chunks:
|
||
print(f"[SceneLoad] 已重建静态chunk数量: {rebuilt_chunks}")
|
||
except Exception as e:
|
||
print(f"[SceneLoad] 重建静态chunk失败: {e}")
|
||
except Exception as e:
|
||
print(f"[SceneLoad] 重建材质effect失败: {e}")
|
||
|
||
# 加载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信息文件")
|
||
|
||
self._load_lui_snapshot_file(filename)
|
||
|
||
# 移除临时场景节点
|
||
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', '.pyc')):
|
||
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', '.pyc')):
|
||
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
|