保存读取优化

This commit is contained in:
Rowland 2026-04-08 11:47:42 +08:00
parent eb18e84bc9
commit 3168996afb
5 changed files with 273 additions and 23 deletions

View File

@ -210,7 +210,7 @@ Size=460,240
Collapsed=0
[Window][打包项目]
Pos=754,392
Pos=681,290
Size=540,320
Collapsed=0

View File

@ -173,6 +173,39 @@ class AssetDatabase:
with open(meta_path, "w", encoding="utf-8") as f:
json.dump(payload, f, ensure_ascii=False, indent=4)
def _read_json_file(self, json_path: str) -> dict:
if not os.path.exists(json_path):
return {}
try:
with open(json_path, "r", encoding="utf-8") as f:
payload = json.load(f)
return payload if isinstance(payload, dict) else {}
except Exception:
return {}
def _build_model_import_cache_record(self, asset_record: dict, cache_dir: str, model_bam_path: str, hierarchy_path: str, materials_path: str, import_info_path: str) -> dict:
return {
"root": relative_project_path(self.layout.project_root, cache_dir),
"model_bam": relative_project_path(self.layout.project_root, model_bam_path),
"hierarchy": relative_project_path(self.layout.project_root, hierarchy_path),
"materials": relative_project_path(self.layout.project_root, materials_path),
"import_info": relative_project_path(self.layout.project_root, import_info_path),
}
def _can_reuse_model_import_cache(self, asset_record: dict, *, model_bam_path: str, hierarchy_path: str, materials_path: str, import_info_path: str) -> bool:
if not all(os.path.exists(path) for path in (model_bam_path, hierarchy_path, materials_path, import_info_path)):
return False
import_info = self._read_json_file(import_info_path)
if not import_info:
return False
if str(import_info.get("asset_guid", "") or "").strip() != str(asset_record.get("guid", "") or "").strip():
return False
if str(import_info.get("source_hash", "") or "").strip() != str(asset_record.get("source_hash", "") or "").strip():
return False
return True
def _copy_into_assets(self, source_path: str, preferred_subdir: str = "") -> str:
source_path = normalize_path(source_path)
asset_type = detect_asset_type(source_path)
@ -207,6 +240,24 @@ class AssetDatabase:
hierarchy_path = os.path.join(cache_dir, "hierarchy.json")
materials_path = os.path.join(cache_dir, "materials.json")
import_info_path = os.path.join(cache_dir, "import_info.json")
imported_cache = self._build_model_import_cache_record(
asset_record,
cache_dir,
model_bam_path,
hierarchy_path,
materials_path,
import_info_path,
)
if self._can_reuse_model_import_cache(
asset_record,
model_bam_path=model_bam_path,
hierarchy_path=hierarchy_path,
materials_path=materials_path,
import_info_path=import_info_path,
):
asset_record["imported_cache"] = imported_cache
return
try:
model_np = self.world.loader.loadModel(Filename.fromOsSpecific(asset_path))
@ -282,13 +333,7 @@ class AssetDatabase:
indent=4,
)
asset_record["imported_cache"] = {
"root": relative_project_path(self.layout.project_root, cache_dir),
"model_bam": relative_project_path(self.layout.project_root, model_bam_path),
"hierarchy": relative_project_path(self.layout.project_root, hierarchy_path),
"materials": relative_project_path(self.layout.project_root, materials_path),
"import_info": relative_project_path(self.layout.project_root, import_info_path),
}
asset_record["imported_cache"] = imported_cache
def register_asset(self, asset_path: str, preferred_subdir: str = "", copy_into_assets: bool = False) -> dict:
asset_path = normalize_path(asset_path)

View File

@ -1450,6 +1450,7 @@ class ProjectManager:
return False
refresh_runtime_fn = getattr(ssbo_editor, "refresh_runtime_from_source", None)
normalize_source_dirty_fn = getattr(ssbo_editor, "normalize_source_transform_dirty_tags", None)
top_level_asset_nodes, node_lookup = self._iter_top_level_scene_asset_nodes(scene_description)
if not top_level_asset_nodes:
@ -1540,6 +1541,11 @@ class ProjectManager:
node_lookup,
apply_material_state=False,
)
if callable(normalize_source_dirty_fn):
try:
normalize_source_dirty_fn(target_root)
except Exception:
pass
# Only rebuild between imports, or once after the whole batch below.
# Otherwise reopening a project pays one extra full SSBO rebuild per model.
if has_more_assets and callable(refresh_runtime_fn):

View File

@ -651,6 +651,7 @@ class ObjectController:
static_np = chunk["dynamic_np"].copy_to(self.model)
static_np.set_name(f"chunk_{chunk_id:04d}_static")
static_np.unstash()
self._clear_string_tags_in_subtree(static_np)
# Editor idle performance depends on the static chunk being actually batched.
# The merged material/state preservation changes above keep per-geom render
# state intact, so we can restore the pre-merge flattening behavior here.
@ -665,6 +666,58 @@ class ObjectController:
else:
static_np.unstash()
def _clear_string_tags_in_subtree(self, root_np):
"""Static chunk copies do not need scene/editor tags; remove them before flatten."""
if not root_np:
return 0
cleared = 0
stack = [root_np]
while stack:
node = stack.pop()
if not node:
continue
try:
if node.is_empty():
continue
except Exception:
try:
if node.isEmpty():
continue
except Exception:
continue
try:
tag_names = list(node.get_tag_keys())
except Exception:
try:
tag_names = list(node.getTagKeys())
except Exception:
tag_names = []
for tag_name in tag_names:
try:
node.clearTag(tag_name)
cleared += 1
except Exception:
try:
node.setTag(tag_name, "")
cleared += 1
except Exception:
continue
try:
children = list(node.get_children())
except Exception:
try:
children = list(node.getChildren())
except Exception:
children = []
for child in reversed(children):
stack.append(child)
return cleared
def build_virtual_hierarchy(self):
"""Build a readonly virtual tree from node_list path keys."""
root = {

View File

@ -68,6 +68,7 @@ class SSBOEditor:
self.source_model = None
self.source_model_root = None
self._source_child_base_mats = {}
self._source_transform_baseline_tag = "_ssbo_source_base_local_mat"
self.last_import_tree_key = None
self.last_import_root_name = None
self.ssbo = None
@ -547,6 +548,52 @@ class SSBOEditor:
except Exception:
continue
def _set_source_node_transform_baseline(self, node):
"""Attach the imported local transform baseline onto one source node."""
if not self._node_is_valid(node):
return False
local_mat = self._get_node_local_mat_copy(node)
if local_mat is None:
return False
try:
node.setPythonTag(self._source_transform_baseline_tag, LMatrix4f(local_mat))
return True
except Exception:
return False
def _get_source_node_transform_baseline(self, node):
"""Read the imported local transform baseline for one source node."""
if not self._node_is_valid(node):
return None
try:
baseline_mat = node.getPythonTag(self._source_transform_baseline_tag)
except Exception:
baseline_mat = None
if baseline_mat is None:
return None
try:
return LMatrix4f(baseline_mat)
except Exception:
return None
def _capture_source_transform_baselines(self, root_node=None):
"""Snapshot imported local transforms so dirty tags can be normalized later."""
start_node = root_node if self._node_is_valid(root_node) else self.source_model_root
if not self._node_is_valid(start_node):
return 0
captured = 0
stack = [start_node]
while stack:
node = stack.pop()
if self._set_source_node_transform_baseline(node):
captured += 1
children = list(self._iter_children(node))
for child in reversed(children):
if self._node_is_valid(child):
stack.append(child)
return captured
def _get_top_level_group_keys(self):
if not self.controller or not getattr(self.controller, "tree_root_key", None):
return []
@ -636,15 +683,101 @@ class SSBOEditor:
continue
composed_mat = LMatrix4f(base_mat)
composed_mat *= model_root_mat
self._apply_source_local_mat_if_changed(source_child, composed_mat)
def _get_node_local_mat_copy(self, node):
"""Return a defensive copy of a node's current local matrix."""
if not self._node_is_valid(node):
return None
try:
return LMatrix4f(node.get_mat())
except Exception:
try:
source_child.set_mat(composed_mat)
source_child.setTag("scene_transform_dirty", "true")
return LMatrix4f(node.getMat())
except Exception:
return None
def _apply_source_local_mat_if_changed(self, source_node, local_mat):
"""Write a local matrix back only when it materially differs."""
if not self._node_is_valid(source_node) or local_mat is None:
return False
current_local_mat = self._get_node_local_mat_copy(source_node)
dirty_tag_changed = self._update_source_transform_dirty_tag(source_node, local_mat)
if current_local_mat is not None and self._matrices_close(current_local_mat, local_mat):
return dirty_tag_changed
try:
source_node.set_mat(local_mat)
except Exception:
try:
source_node.setMat(local_mat)
except Exception:
return False
dirty_tag_changed = self._update_source_transform_dirty_tag(source_node, local_mat) or dirty_tag_changed
return True
def _update_source_transform_dirty_tag(self, source_node, local_mat):
"""Keep scene_transform_dirty aligned with the imported baseline."""
if not self._node_is_valid(source_node) or local_mat is None:
return False
baseline_local_mat = self._get_source_node_transform_baseline(source_node)
if baseline_local_mat is None:
return False
try:
has_dirty_tag = bool(source_node.hasTag("scene_transform_dirty"))
except Exception:
has_dirty_tag = False
is_dirty = not self._matrices_close(local_mat, baseline_local_mat)
if is_dirty:
if has_dirty_tag:
try:
source_child.setMat(composed_mat)
source_child.setTag("scene_transform_dirty", "true")
if str(source_node.getTag("scene_transform_dirty") or "").lower() == "true":
return False
except Exception:
continue
pass
try:
source_node.setTag("scene_transform_dirty", "true")
return True
except Exception:
return False
if not has_dirty_tag:
return False
try:
source_node.clearTag("scene_transform_dirty")
return True
except Exception:
try:
source_node.setTag("scene_transform_dirty", "")
return True
except Exception:
return False
def normalize_source_transform_dirty_tags(self, root_node=None):
"""Normalize transform dirty tags against import-time baselines."""
start_node = root_node if self._node_is_valid(root_node) else self.source_model_root
if not self._node_is_valid(start_node):
return 0
normalized = 0
stack = [start_node]
while stack:
node = stack.pop()
local_mat = self._get_node_local_mat_copy(node)
if local_mat is not None and self._update_source_transform_dirty_tag(node, local_mat):
normalized += 1
children = list(self._iter_children(node))
for child in reversed(children):
if self._node_is_valid(child):
stack.append(child)
if normalized:
print(f"[SSBOEditor] Normalized transform dirty tags: {normalized}")
return normalized
def _snapshot_runtime_node_transform_to_source_node(self, scene_node, source_node):
"""Persist one runtime node transform back to its matching source-tree node."""
@ -689,16 +822,7 @@ class SSBOEditor:
return False
local_mat = current_net_mat * inv_parent_mat
try:
source_node.set_mat(local_mat)
source_node.setTag("scene_transform_dirty", "true")
except Exception:
try:
source_node.setMat(local_mat)
source_node.setTag("scene_transform_dirty", "true")
except Exception:
return False
return True
return self._apply_source_local_mat_if_changed(source_node, local_mat)
def _snapshot_runtime_object_transforms_to_source_root(self):
"""
@ -1616,7 +1740,11 @@ class SSBOEditor:
self._set_node_name(imported_root, unique_root_name)
if not skip_heavy_material_processing:
self._detach_shared_materials_in_subtree(imported_root)
self._capture_source_transform_baselines(imported_root)
imported_roots = [imported_root]
else:
for imported_root in imported_roots:
self._capture_source_transform_baselines(imported_root)
self.source_model = source_root
self._restore_saved_material_bindings_from_tags(source_root)
@ -1633,6 +1761,7 @@ class SSBOEditor:
self._set_node_name(imported_root, unique_root_name)
if not skip_heavy_material_processing:
self._detach_shared_materials_in_subtree(imported_root)
self._capture_source_transform_baselines(imported_root)
if keep_source_model and not append:
self.source_model = imported_root
@ -2959,6 +3088,23 @@ class SSBOEditor:
rebuilt = 0
for chunk_id in sorted(chunks):
chunk = chunks.get(chunk_id)
if not isinstance(chunk, dict):
continue
needs_rebuild = bool(chunk.get("dynamic_enabled") or chunk.get("dirty"))
static_np = chunk.get("static_np")
if not needs_rebuild:
try:
needs_rebuild = not (static_np and not static_np.is_empty())
except Exception:
try:
needs_rebuild = not (static_np and not static_np.isEmpty())
except Exception:
needs_rebuild = True
if not needs_rebuild:
continue
try:
controller._rebuild_static_chunk(chunk_id)
controller._set_chunk_dynamic(chunk_id, False)