EG/scene/scene_manager_io_mixin.py
2026-03-17 09:24:30 +08:00

1767 lines
81 KiB
Python
Raw Permalink 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, LMatrix4f
)
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 _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 _parse_vec3_tag(self, vec_str, default=None):
"""安全解析保存到Tag里的Vec3字符串。"""
try:
if default is None:
default = Vec3(0, 0, 0)
text = str(vec_str).strip()
for prefix in ("LVecBase3f", "LPoint3f", "LVector3f"):
text = text.replace(prefix, "")
text = text.strip().strip("()")
parts = [p.strip() for p in text.split(",")]
if len(parts) != 3:
return Vec3(default)
x, y, z = map(float, parts)
return Vec3(x, y, z)
except Exception:
return Vec3(default if default is not None else Vec3(0, 0, 0))
def _resolve_saved_model_source_path(self, model_root_node, scene_filename):
"""根据已保存标签尽力恢复模型源文件路径。"""
try:
if not model_root_node or model_root_node.isEmpty():
return None
raw_candidates = []
for tag_name in ("saved_model_path", "model_path", "original_path", "file"):
if model_root_node.hasTag(tag_name):
value = model_root_node.getTag(tag_name).strip()
if value:
raw_candidates.append(value)
if not raw_candidates:
return None
scene_abs = os.path.abspath(scene_filename)
scene_dir = os.path.dirname(scene_abs)
project_dir = os.path.dirname(scene_dir)
search_dirs = [
scene_dir,
project_dir,
os.path.join(project_dir, "resources"),
os.path.join(project_dir, "Resources"),
os.path.join(project_dir, "resources", "models"),
os.path.join(project_dir, "Resources", "models"),
os.path.join(project_dir, "models"),
]
seen = set()
for raw in raw_candidates:
variants = [raw, os.path.normpath(raw)]
if os.path.isabs(raw):
variants.append(os.path.abspath(raw))
else:
for base_dir in search_dirs:
variants.append(os.path.normpath(os.path.join(base_dir, raw)))
basename = os.path.basename(raw)
if basename:
for base_dir in search_dirs:
variants.append(os.path.normpath(os.path.join(base_dir, basename)))
for candidate in variants:
if not candidate:
continue
candidate_abs = os.path.abspath(candidate)
if candidate_abs in seen:
continue
seen.add(candidate_abs)
if candidate_abs == scene_abs:
continue
if os.path.exists(candidate_abs):
return candidate_abs
return None
except Exception:
return None
def _collect_local_transform_snapshot(self, root_node):
"""采集根节点下所有子节点的局部变换快照(不含根节点自身)。"""
snapshot = {"by_index": {}, "by_name": {}}
try:
if not root_node or root_node.isEmpty():
return snapshot
def walk(node, index_path, name_path):
if index_path:
snapshot["by_index"][index_path] = {
"pos": tuple(node.getPos()),
"hpr": tuple(node.getHpr()),
"scale": tuple(node.getScale()),
}
snapshot["by_name"][name_path] = snapshot["by_index"][index_path]
name_count = {}
for child_index, child in enumerate(node.getChildren()):
child_name = child.getName() or ""
occur = name_count.get(child_name, 0)
name_count[child_name] = occur + 1
walk(
child,
index_path + (child_index,),
name_path + (f"{child_name}#{occur}",),
)
walk(root_node, tuple(), tuple())
except Exception:
pass
return snapshot
def _apply_local_transform_snapshot(self, root_node, snapshot):
"""将保存的子节点局部变换回写到新导入模型。"""
try:
if (not root_node) or root_node.isEmpty() or (not snapshot):
return 0
by_index = snapshot.get("by_index", {})
by_name = snapshot.get("by_name", {})
if (not by_index) and (not by_name):
return 0
applied_count = 0
def walk(node, index_path, name_path):
nonlocal applied_count
if index_path:
data = by_index.get(index_path) or by_name.get(name_path)
if data:
try:
node.setPos(*data["pos"])
node.setHpr(*data["hpr"])
node.setScale(*data["scale"])
applied_count += 1
except Exception:
pass
name_count = {}
for child_index, child in enumerate(node.getChildren()):
child_name = child.getName() or ""
occur = name_count.get(child_name, 0)
name_count[child_name] = occur + 1
walk(
child,
index_path + (child_index,),
name_path + (f"{child_name}#{occur}",),
)
walk(root_node, tuple(), tuple())
return applied_count
except Exception:
return 0
def _matrices_close(self, a, b, eps=1e-6):
"""比较两个4x4矩阵是否近似相等。"""
try:
if (a is None) or (b is None):
return False
for r in range(4):
ra = a.get_row(r)
rb = b.get_row(r)
if (
abs(float(ra[0]) - float(rb[0])) > eps or
abs(float(ra[1]) - float(rb[1])) > eps or
abs(float(ra[2]) - float(rb[2])) > eps or
abs(float(ra[3]) - float(rb[3])) > eps
):
return False
return True
except Exception:
return False
def _matrix_to_list(self, mat):
"""把 Panda3D 矩阵转为长度16的列表行主序"""
values = []
try:
for r in range(4):
for c in range(4):
values.append(float(mat.getCell(r, c)))
except Exception:
values = []
return values
def _list_to_matrix(self, values):
"""把长度16列表还原为 Panda3D 矩阵。"""
try:
if not isinstance(values, (list, tuple)) or len(values) != 16:
return None
# LMatrix4f.identMat() 在部分绑定里是只读对象,不能 setCell。
numeric = [float(v) for v in values]
try:
return LMatrix4f(*numeric)
except Exception:
mat = LMatrix4f()
idx = 0
for r in range(4):
for c in range(4):
try:
mat.setCell(r, c, numeric[idx])
except Exception:
mat.set_cell(r, c, numeric[idx])
idx += 1
return mat
except Exception:
return None
def _serialize_ssbo_subobject_transforms(self, model_root):
"""序列化当前 SSBO 模型的子物体变换(按 global_id 记录矩阵)。"""
try:
ssbo_editor = getattr(self.world, "ssbo_editor", None)
if not ssbo_editor:
return ""
cleanup_group_proxy = getattr(ssbo_editor, "_cleanup_group_proxy", None)
if callable(cleanup_group_proxy):
cleanup_group_proxy()
controller = getattr(ssbo_editor, "controller", None)
ssbo_model = getattr(ssbo_editor, "model", None)
if not controller or not ssbo_model or ssbo_model != model_root:
return ""
gid_mats = {}
changed_count = 0
for gid, obj_np in getattr(controller, "id_to_object_np", {}).items():
if not obj_np:
continue
try:
if obj_np.is_empty():
continue
except Exception:
if obj_np.isEmpty():
continue
try:
cur_mat = obj_np.get_mat(controller.model)
except Exception:
cur_mat = obj_np.getMat(controller.model)
base_mat = None
try:
if gid < len(controller.global_transforms):
base_mat = controller.global_transforms[gid]
except Exception:
base_mat = None
if base_mat is not None and self._matrices_close(cur_mat, base_mat):
continue
mat_values = self._matrix_to_list(cur_mat)
if not mat_values:
continue
gid_mats[str(int(gid))] = mat_values
changed_count += 1
if changed_count <= 0:
return ""
payload = {
"version": 1,
"gid_mats": gid_mats,
}
return json.dumps(payload, ensure_ascii=False, separators=(",", ":"))
except Exception as e:
print(f"[SSBO] 序列化子物体变换失败: {e}")
return ""
def _restore_ssbo_subobject_transforms(self, imported_node, saved_node):
"""把已保存的 SSBO 子物体变换恢复到当前导入模型。"""
try:
def _is_empty_np(np):
if not np:
return True
try:
return np.is_empty()
except Exception:
try:
return np.isEmpty()
except Exception:
return True
def _first_path_tag(np):
if _is_empty_np(np):
return ""
for tag_name in ("saved_model_path", "model_path", "original_path", "file"):
try:
if np.hasTag(tag_name):
value = np.getTag(tag_name).strip()
if value:
return value.replace("\\", "/").lower()
except Exception:
continue
return ""
if _is_empty_np(imported_node) or _is_empty_np(saved_node):
print("[SSBO] 子物体恢复跳过: 模型节点无效")
return 0
if not saved_node.hasTag("ssbo_gid_mats"):
return 0
raw_text = saved_node.getTag("ssbo_gid_mats").strip()
if not raw_text:
print(f"[SSBO] 子物体恢复跳过: ssbo_gid_mats 为空 ({saved_node.getName()})")
return 0
try:
payload = json.loads(raw_text)
except Exception as e:
print(f"[SSBO] 子物体恢复失败: ssbo_gid_mats 非法 JSON: {e}")
return 0
gid_mats = payload.get("gid_mats", {}) if isinstance(payload, dict) else {}
if not isinstance(gid_mats, dict) or not gid_mats:
print(f"[SSBO] 子物体恢复跳过: 无 gid 数据 ({saved_node.getName()})")
return 0
ssbo_editor = getattr(self.world, "ssbo_editor", None)
controller = getattr(ssbo_editor, "controller", None) if ssbo_editor else None
model_root = getattr(controller, "model", None) if controller else None
if not controller or _is_empty_np(model_root):
print("[SSBO] 子物体恢复跳过: controller/model 不可用")
return 0
same_target = False
try:
same_target = (model_root == imported_node)
except Exception:
same_target = False
if not same_target:
try:
same_target = (model_root.node() == imported_node.node())
except Exception:
same_target = False
if not same_target:
model_path = _first_path_tag(model_root)
imported_path = _first_path_tag(imported_node)
if model_path and imported_path and model_path == imported_path:
same_target = True
if not same_target:
try:
same_target = (model_root.getName() == imported_node.getName())
except Exception:
same_target = False
if not same_target:
print(
f"[SSBO] 子物体恢复提示: controller/model 与导入节点句柄不一致,"
f"继续按当前 controller 应用 ({imported_node.getName()})"
)
id_to_object_np = getattr(controller, "id_to_object_np", {})
id_to_pick_np = getattr(controller, "id_to_pick_np", {})
id_to_chunk = getattr(controller, "id_to_chunk", {})
pick_model = getattr(controller, "pick_model", None)
print(
f"[SSBO] 子物体恢复准备: model={model_root.getName()} "
f"saved={saved_node.getName()} gids={len(gid_mats)} "
f"controller_objs={len(id_to_object_np)}"
)
applied = 0
fallback_hits = 0
dirty_chunks = set()
missing_ids = []
for gid_text, mat_values in gid_mats.items():
try:
gid = int(gid_text)
except Exception:
continue
mat = self._list_to_matrix(mat_values)
if mat is None:
continue
obj_np = id_to_object_np.get(gid) if isinstance(id_to_object_np, dict) else None
if _is_empty_np(obj_np):
try:
found = model_root.find(f"**/obj_{gid}")
if not _is_empty_np(found):
obj_np = found
fallback_hits += 1
if isinstance(id_to_object_np, dict):
id_to_object_np[gid] = found
except Exception:
obj_np = None
if _is_empty_np(obj_np):
missing_ids.append(gid)
continue
try:
obj_np.set_mat(model_root, mat)
except Exception:
obj_np.setMat(model_root, mat)
pick_np = id_to_pick_np.get(gid) if isinstance(id_to_pick_np, dict) else None
if _is_empty_np(pick_np) and not _is_empty_np(pick_model):
try:
found_pick = pick_model.find(f"**/pick_{gid}")
if not _is_empty_np(found_pick):
pick_np = found_pick
if isinstance(id_to_pick_np, dict):
id_to_pick_np[gid] = found_pick
except Exception:
pick_np = None
if not _is_empty_np(pick_np):
pick_ref = pick_model if not _is_empty_np(pick_model) else model_root
try:
pick_np.set_mat(pick_ref, mat)
except Exception:
pick_np.setMat(pick_ref, mat)
chunk_mapping = id_to_chunk.get(gid) if isinstance(id_to_chunk, dict) else None
chunk_id = None
if isinstance(chunk_mapping, (tuple, list)):
chunk_id = chunk_mapping[0] if chunk_mapping else None
else:
chunk_id = chunk_mapping
if chunk_id is None:
try:
obj_parent = obj_np.getParent()
for cid, chunk_data in getattr(controller, "chunks", {}).items():
chunk_dynamic = chunk_data.get("dynamic_np")
if _is_empty_np(chunk_dynamic):
continue
if obj_parent == chunk_dynamic:
chunk_id = cid
break
except Exception:
chunk_id = None
if chunk_id is not None:
dirty_chunks.add(chunk_id)
applied += 1
rebuild_fn = getattr(controller, "_rebuild_static_chunk", None)
set_dynamic_fn = getattr(controller, "_set_chunk_dynamic", None)
for chunk_id in sorted(dirty_chunks):
if chunk_id not in controller.chunks:
continue
controller.chunks[chunk_id]["dirty"] = True
if callable(rebuild_fn):
rebuild_fn(chunk_id)
if callable(set_dynamic_fn):
set_dynamic_fn(chunk_id, False)
if missing_ids:
show_ids = ",".join(str(v) for v in missing_ids[:12])
if len(missing_ids) > 12:
show_ids += ",..."
print(f"[SSBO] 子物体恢复未命中 gid: {show_ids}")
print(
f"[SSBO] 子物体恢复结果: 应用 {applied}/{len(gid_mats)}"
f"fallback={fallback_hits}dirty_chunks={len(dirty_chunks)}"
)
return applied
except Exception as e:
print(f"[SSBO] 恢复子物体变换失败: {e}")
return 0
def _reimport_saved_models_for_ssbo(self, scene, scene_filename, loaded_nodes):
"""在SSBO模式下按保存的源模型路径重建模型根节点避免加载烘焙后结构。"""
replaced_model_names = set()
try:
runtime_import = getattr(self.world, "_import_model_for_runtime", None)
if not callable(runtime_import):
return replaced_model_names
model_roots = []
for node in scene.findAllMatches("**"):
try:
if node and (not node.isEmpty()) and node.hasTag("is_model_root"):
model_roots.append(node)
except Exception:
continue
if not model_roots:
return replaced_model_names
print(f"[SSBO] 开始按源路径重建模型: {len(model_roots)}")
for saved_node in model_roots:
node_name = saved_node.getName()
source_path = self._resolve_saved_model_source_path(saved_node, scene_filename)
if not source_path:
print(f"[SSBO] 跳过模型 {node_name}: 无法解析源文件路径")
continue
saved_snapshot = self._collect_local_transform_snapshot(saved_node)
try:
imported_node = runtime_import(source_path)
except Exception as import_e:
print(f"[SSBO] 重建模型失败 {node_name}: {import_e}")
continue
if not imported_node or imported_node.isEmpty():
print(f"[SSBO] 重建模型失败 {node_name}: 运行时导入未返回有效节点")
continue
# 复制保存标签,确保后续恢复逻辑可复用。
try:
for tag_key in saved_node.getTagKeys():
imported_node.setTag(tag_key, saved_node.getTag(tag_key))
except Exception:
pass
if node_name:
imported_node.setName(node_name)
imported_node.setTag("is_model_root", "1")
imported_node.setTag("is_scene_element", "1")
imported_node.setTag("model_path", source_path)
imported_node.setTag("saved_model_path", source_path)
imported_node.setTag("original_path", source_path)
# 恢复保存的变换优先Tag其次旧节点当前值
if saved_node.hasTag("transform_pos"):
imported_node.setPos(self._parse_vec3_tag(saved_node.getTag("transform_pos"), saved_node.getPos()))
else:
imported_node.setPos(saved_node.getPos())
if saved_node.hasTag("transform_hpr"):
imported_node.setHpr(self._parse_vec3_tag(saved_node.getTag("transform_hpr"), saved_node.getHpr()))
else:
imported_node.setHpr(saved_node.getHpr())
if saved_node.hasTag("transform_scale"):
imported_node.setScale(self._parse_vec3_tag(saved_node.getTag("transform_scale"), saved_node.getScale()))
else:
imported_node.setScale(saved_node.getScale())
# 恢复可见性。
user_visible = True
if saved_node.hasTag("user_visible"):
user_visible = saved_node.getTag("user_visible").lower() == "true"
imported_node.setPythonTag("user_visible", user_visible)
if user_visible:
imported_node.show()
else:
imported_node.hide()
# 保留父子关系标签,后续统一重建。
if saved_node.hasTag("parent_name"):
imported_node.setTag("parent_name", saved_node.getTag("parent_name"))
restored_ssbo_ids = self._restore_ssbo_subobject_transforms(imported_node, saved_node)
if restored_ssbo_ids > 0:
print(f"[SSBO] 已恢复模型 {node_name} 的子物体变换: {restored_ssbo_ids}")
restored_children = 0
if restored_ssbo_ids <= 0:
restored_children = self._apply_local_transform_snapshot(imported_node, saved_snapshot)
if restored_children > 0:
print(f"[SSBO] 已恢复模型 {node_name} 的子节点变换: {restored_children}")
loaded_nodes[node_name] = imported_node
replaced_model_names.add(node_name)
if imported_node not in self.models:
self.models.append(imported_node)
print(f"[SSBO] 源路径重建完成: 成功 {len(replaced_model_names)}")
except Exception as e:
print(f"[SSBO] 按源路径重建模型时出错: {e}")
return replaced_model_names
def saveScene(self, filename,project_path):
"""保存场景到BAM文件 - 完整版支持GUI元素,地形"""
try:
print(f"\n=== 开始保存场景到: {filename} ===")
# 提交 SSBO 组选择代理,避免保存时子物体仍挂在临时 proxy 下。
ssbo_editor = getattr(self.world, "ssbo_editor", None)
if ssbo_editor:
cleanup_group_proxy = getattr(ssbo_editor, "_cleanup_group_proxy", None)
if callable(cleanup_group_proxy):
cleanup_group_proxy()
# 确保文件路径是规范化的
filename = os.path.normpath(filename)
# 确保目录存在
directory = os.path.dirname(filename)
if directory and not os.path.exists(directory):
os.makedirs(directory)
resources_dir = os.path.join(directory,"resources")
if not os.path.exists(resources_dir):
os.makedirs(resources_dir)
# 存储需要临时隐藏的节点,以便保存后恢复
nodes_to_restore = []
# 查找并隐藏所有坐标轴和选择框节点
gizmo_nodes = self.world.render.findAllMatches("**/gizmo*")
selection_box_nodes = self.world.render.findAllMatches("**/selectionBox*")
# 隐藏坐标轴节点
for node in gizmo_nodes:
if not node.isHidden():
nodes_to_restore.append((node, True)) # (节点, 原先是否可见)
node.hide()
print(f"临时隐藏坐标轴节点: {node.getName()}")
# 隐藏选择框节点
for node in selection_box_nodes:
if not node.isHidden():
nodes_to_restore.append((node, True))
node.hide()
print(f"临时隐藏选择框节点: {node.getName()}")
# 收集所有需要保存的节点
all_nodes = []
all_nodes.extend(self.models)
all_nodes.extend(self.Spotlight)
all_nodes.extend(self.Pointlight)
# 创建用于保存GUI信息的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"))
try:
if hasattr(node, "hasPythonTag") and node.hasPythonTag("selected_animation"):
selected_anim = node.getPythonTag("selected_animation")
if selected_anim:
node.setTag("saved_selected_animation", str(selected_anim))
if hasattr(node, "hasPythonTag") and node.hasPythonTag("anim_speed"):
anim_speed = node.getPythonTag("anim_speed")
if anim_speed is not None:
node.setTag("saved_anim_speed", str(float(anim_speed)))
if hasattr(node, "hasPythonTag") and node.hasPythonTag("anim_play_mode"):
anim_mode = node.getPythonTag("anim_play_mode")
if anim_mode:
node.setTag("saved_anim_play_mode", str(anim_mode).lower())
except Exception:
pass
# 保存 SSBO 子物体变换(仅当前受 SSBOEditor 控制的模型根)。
ssbo_subobject_payload = self._serialize_ssbo_subobject_transforms(node)
if ssbo_subobject_payload:
node.setTag("ssbo_gid_mats", ssbo_subobject_payload)
print(f"[SSBO] 保存模型 {node.getName()} 的子物体变换数据")
else:
if node.hasTag("ssbo_gid_mats"):
node.clearTag("ssbo_gid_mats")
if hasattr(self.world,'script_manager') and self.world.script_manager:
script_manager = self.world.script_manager
scripts = script_manager.get_scripts_on_object(node)
if scripts:
node.setTag("has_scripts", "true")
script_info_list = []
for script_component in scripts:
script_name = script_component.script_name
print(f"保存脚本信息: {script_name}")
# 获取脚本类的文件路径
script_class = script_component.script_instance.__class__
script_file = self._get_script_file_path(script_class, script_name)
script_info_list.append({
"name": script_name,
"file": script_file
})
# 将脚本信息保存为JSON字符串
import json
node.setTag("scripts_info", json.dumps(script_info_list, ensure_ascii=False))
print(f"为节点 {node.getName()} 保存了 {len(script_info_list)} 个脚本")
try:
print("--- 打印当前场景图 (render) ---")
self.world.render.ls()
print("---------------------------------")
self.take_screenshot(project_path)
# 保存场景
success = self.world.render.writeBamFile(Filename.fromOsSpecific(filename))
if success:
print(f"✓ 场景保存成功: {filename}")
else:
print("✗ 场景保存失败")
return success
finally:
# 恢复之前隐藏的节点
for item in nodes_to_restore:
node, was_visible = item
if was_visible and not node.isEmpty():
node.show()
print(f"恢复显示节点: {node.getName()}")
if nodes_to_restore:
print(f"已恢复 {len(nodes_to_restore)} 个辅助节点的显示")
except Exception as e:
print(f"保存场景时发生错误: {str(e)}")
import traceback
traceback.print_exc()
return False
def take_screenshot(self, projectpath):
"""
截图并保存到指定的完整路径
Args:
full_path (str): 完整的文件保存路径,包括文件名和扩展名
Returns:
bool: 截图是否成功
"""
try:
from panda3d.core import Filename
import os
print(f"\n=== 截图保存: {projectpath} ===")
# 确保目录存在
directory = os.path.dirname(projectpath)
if directory and not os.path.exists(directory):
os.makedirs(directory)
print(f"创建目录: {directory}")
# 规范化路径
filename = os.path.basename(os.path.normpath(projectpath))
filename = f'{filename}.png'
print(f'project_path: {projectpath}')
print(f'project_name: {filename}')
full_path = os.path.normpath(os.path.join(projectpath, filename))
p3d_filename = Filename.from_os_specific(full_path)
# 使用 Panda3D 的截图功能
success = self.world.win.saveScreenshot(p3d_filename)
if success:
print(f"✅ 成功截图并保存到: {full_path}")
return True
else:
print(f"❌ 截图保存失败: {full_path}")
return False
except Exception as e:
print(f"保存截图时发生错误: {str(e)}")
import traceback
traceback.print_exc()
return False
def loadScene(self, filename, 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
# 预防性清理确保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()
# 加载场景
# 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映射
normalized_scene_path = filename.replace("\\", "/").lower()
is_project_scene_bam = (
normalized_scene_path.endswith("/scenes/scene.bam")
or (normalized_scene_path.endswith(".bam") and "/scenes/" in normalized_scene_path)
)
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)) and
(not is_project_scene_bam)
)
if is_project_scene_bam and getattr(self.world, "use_ssbo_scene_import", False):
print("[SSBO] 检测到项目场景BAM跳过统一导入链路避免重复加载导致崩溃")
ssbo_scene_model = None
if use_ssbo_scene_import:
try:
print(f"[SSBO] 打开项目使用统一导入链路: {filename}")
ssbo_scene_model = self.world._import_model_for_runtime(filename)
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
ssbo_reimported_model_roots = set()
if (
is_project_scene_bam
and getattr(self.world, "use_ssbo_mouse_picking", False)
and callable(getattr(self.world, "_import_model_for_runtime", None))
):
ssbo_reimported_model_roots = self._reimport_saved_models_for_ssbo(scene, filename, loaded_nodes)
# 遍历场景中的所有节点
def processNode(nodePath, depth=0):
indent = " " * depth
# print(f"{indent}处理节点: {nodePath.getName()} (类型: {type(nodePath.node()).__name__})")
# 检查节点是否有效
if nodePath.isEmpty():
# print(f"{indent}[DEBUG] 节点为空,跳过处理")
return
node_name = nodePath.getName()
is_reimported_ssbo_root = (
node_name in ssbo_reimported_model_roots and nodePath.hasTag("is_model_root")
)
# 存储节点以便后续处理父子关系避免覆盖已重建的SSBO模型根节点映射
if not is_reimported_ssbo_root:
loaded_nodes[node_name] = 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")
# 保留原始材质/颜色状态,避免在场景恢复阶段造成黑模。
if nodePath.hasTag("transform_pos"):
pos = self._parse_vec3_tag(nodePath.getTag("transform_pos"), nodePath.getPos())
nodePath.setPos(pos)
print(f"{indent}恢复位置: {pos}")
if nodePath.hasTag("transform_hpr"):
hpr = self._parse_vec3_tag(nodePath.getTag("transform_hpr"), nodePath.getHpr())
nodePath.setHpr(hpr)
print(f"{indent}恢复旋转: {hpr}")
if nodePath.hasTag("transform_scale"):
scale = self._parse_vec3_tag(nodePath.getTag("transform_scale"), nodePath.getScale())
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)
and not is_reimported_ssbo_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_scrip_in_directory(script_name)
if script_path:
print(f"从目录找到脚本并加载{script_path}")
script_manager.load_script_from_file(script_path)
else:
script_path = self._find_script_in_directory(script_name)
if script_path:
print(f"从目录找到脚本并加载: {script_path}")
script_manager.load_script_from_file(script_path)
else:
print(f"找不到脚本文件: {script_name}")
if script_name in script_manager.loader.script_classes:
script_component = script_manager.add_script_to_object(nodePath,script_name)
if script_component:
print(f"成功为 {nodePath.getName()} 添加脚本: {script_name}")
else:
print(f"{nodePath.getName()} 添加脚本失败: {script_name}")
else:
print(f"脚本 {script_name} 不可用,跳过挂载")
except Exception as e:
print(f"重新挂载脚本失败: {e}")
import traceback
traceback.print_exc()
# 恢复材质属性
def parseColor(color_str):
"""解析颜色字符串为Vec4"""
try:
color_str = color_str.replace('LVecBase4f', '').strip('()')
r, g, b, a = map(float, color_str.split(','))
return Vec4(r, g, b, a)
except:
return Vec4(1, 1, 1, 1)
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 is_reimported_ssbo_root and is_model_root:
# 该模型根节点已按源路径重建旧BAM节点不参与挂载恢复。
pass
elif 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 is_reimported_ssbo_root:
print(f"{indent}[SSBO] 已按源路径重建跳过旧BAM模型恢复: {nodePath.getName()}")
elif use_ssbo_scene_import:
# SSBO 模式下整个 scene.bam 已通过统一导入链路载入,
# 这里跳过逐模型旧导入逻辑,避免与菜单导入路径不一致。
pass
else:
#self._validateAndFixAllTransforms(nodePath)
self._fixModelStructure(nodePath)
self._restoreModelAnimationInfo(nodePath)
self._processModelAnimations(nodePath)
try:
if nodePath.hasTag("saved_selected_animation"):
selected_anim = nodePath.getTag("saved_selected_animation").strip()
if selected_anim:
nodePath.setPythonTag("selected_animation", selected_anim)
if nodePath.hasTag("saved_anim_speed"):
speed_text = nodePath.getTag("saved_anim_speed").strip()
if speed_text:
nodePath.setPythonTag("anim_speed", float(speed_text))
if nodePath.hasTag("saved_anim_play_mode"):
mode_text = nodePath.getTag("saved_anim_play_mode").strip().lower()
if mode_text:
nodePath.setPythonTag("anim_play_mode", mode_text)
except Exception:
pass
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)
# 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