修复读取项目

This commit is contained in:
Rowland 2026-03-06 15:14:52 +08:00
parent 4a25031cac
commit da63dd4877
2 changed files with 417 additions and 3 deletions

View File

@ -84,12 +84,12 @@ Size=400,300
Collapsed=0 Collapsed=0
[Window][选择路径] [Window][选择路径]
Pos=390,125 Pos=625,258
Size=600,500 Size=600,500
Collapsed=0 Collapsed=0
[Window][打开项目] [Window][打开项目]
Pos=440,175 Pos=675,308
Size=500,400 Size=500,400
Collapsed=0 Collapsed=0

View File

@ -12,7 +12,7 @@ from pathlib import Path
from panda3d.core import ( from panda3d.core import (
ModelPool, ModelRoot, Filename, NodePath, GeomNode, Material, Vec4, Vec3, ModelPool, ModelRoot, Filename, NodePath, GeomNode, Material, Vec4, Vec3,
MaterialAttrib, ColorAttrib, Point3, CollisionNode, CollisionSphere, CollisionBox, MaterialAttrib, ColorAttrib, Point3, CollisionNode, CollisionSphere, CollisionBox,
BitMask32, TransparencyAttrib, LColor, TransformState, RenderModeAttrib BitMask32, TransparencyAttrib, LColor, TransformState, RenderModeAttrib, LMatrix4f
) )
from panda3d.egg import EggData, EggVertexPool from panda3d.egg import EggData, EggVertexPool
from direct.actor.Actor import Actor from direct.actor.Actor import Actor
@ -251,6 +251,392 @@ class SceneManagerIOMixin:
except Exception: except Exception:
return None 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): def _reimport_saved_models_for_ssbo(self, scene, scene_filename, loaded_nodes):
"""在SSBO模式下按保存的源模型路径重建模型根节点避免加载烘焙后结构。""" """在SSBO模式下按保存的源模型路径重建模型根节点避免加载烘焙后结构。"""
replaced_model_names = set() replaced_model_names = set()
@ -278,6 +664,8 @@ class SceneManagerIOMixin:
print(f"[SSBO] 跳过模型 {node_name}: 无法解析源文件路径") print(f"[SSBO] 跳过模型 {node_name}: 无法解析源文件路径")
continue continue
saved_snapshot = self._collect_local_transform_snapshot(saved_node)
try: try:
imported_node = runtime_import(source_path) imported_node = runtime_import(source_path)
except Exception as import_e: except Exception as import_e:
@ -334,6 +722,16 @@ class SceneManagerIOMixin:
if saved_node.hasTag("parent_name"): if saved_node.hasTag("parent_name"):
imported_node.setTag("parent_name", saved_node.getTag("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 loaded_nodes[node_name] = imported_node
replaced_model_names.add(node_name) replaced_model_names.add(node_name)
if imported_node not in self.models: if imported_node not in self.models:
@ -349,6 +747,13 @@ class SceneManagerIOMixin:
try: try:
print(f"\n=== 开始保存场景到: {filename} ===") 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) filename = os.path.normpath(filename)
@ -485,6 +890,15 @@ class SceneManagerIOMixin:
if node.hasTag("can_create_actor_from_memory"): if node.hasTag("can_create_actor_from_memory"):
node.setTag("saved_can_create_actor_from_memory", node.getTag("can_create_actor_from_memory")) node.setTag("saved_can_create_actor_from_memory", node.getTag("can_create_actor_from_memory"))
# 保存 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: if hasattr(self.world,'script_manager') and self.world.script_manager:
script_manager = self.world.script_manager script_manager = self.world.script_manager
scripts = script_manager.get_scripts_on_object(node) scripts = script_manager.get_scripts_on_object(node)