保存读取优化
This commit is contained in:
parent
eb18e84bc9
commit
3168996afb
@ -210,7 +210,7 @@ Size=460,240
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][打包项目]
|
[Window][打包项目]
|
||||||
Pos=754,392
|
Pos=681,290
|
||||||
Size=540,320
|
Size=540,320
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
|
|||||||
@ -173,6 +173,39 @@ class AssetDatabase:
|
|||||||
with open(meta_path, "w", encoding="utf-8") as f:
|
with open(meta_path, "w", encoding="utf-8") as f:
|
||||||
json.dump(payload, f, ensure_ascii=False, indent=4)
|
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:
|
def _copy_into_assets(self, source_path: str, preferred_subdir: str = "") -> str:
|
||||||
source_path = normalize_path(source_path)
|
source_path = normalize_path(source_path)
|
||||||
asset_type = detect_asset_type(source_path)
|
asset_type = detect_asset_type(source_path)
|
||||||
@ -207,6 +240,24 @@ class AssetDatabase:
|
|||||||
hierarchy_path = os.path.join(cache_dir, "hierarchy.json")
|
hierarchy_path = os.path.join(cache_dir, "hierarchy.json")
|
||||||
materials_path = os.path.join(cache_dir, "materials.json")
|
materials_path = os.path.join(cache_dir, "materials.json")
|
||||||
import_info_path = os.path.join(cache_dir, "import_info.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:
|
try:
|
||||||
model_np = self.world.loader.loadModel(Filename.fromOsSpecific(asset_path))
|
model_np = self.world.loader.loadModel(Filename.fromOsSpecific(asset_path))
|
||||||
@ -282,13 +333,7 @@ class AssetDatabase:
|
|||||||
indent=4,
|
indent=4,
|
||||||
)
|
)
|
||||||
|
|
||||||
asset_record["imported_cache"] = {
|
asset_record["imported_cache"] = 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),
|
|
||||||
}
|
|
||||||
|
|
||||||
def register_asset(self, asset_path: str, preferred_subdir: str = "", copy_into_assets: bool = False) -> dict:
|
def register_asset(self, asset_path: str, preferred_subdir: str = "", copy_into_assets: bool = False) -> dict:
|
||||||
asset_path = normalize_path(asset_path)
|
asset_path = normalize_path(asset_path)
|
||||||
|
|||||||
@ -1450,6 +1450,7 @@ class ProjectManager:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
refresh_runtime_fn = getattr(ssbo_editor, "refresh_runtime_from_source", None)
|
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)
|
top_level_asset_nodes, node_lookup = self._iter_top_level_scene_asset_nodes(scene_description)
|
||||||
if not top_level_asset_nodes:
|
if not top_level_asset_nodes:
|
||||||
@ -1540,6 +1541,11 @@ class ProjectManager:
|
|||||||
node_lookup,
|
node_lookup,
|
||||||
apply_material_state=False,
|
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.
|
# Only rebuild between imports, or once after the whole batch below.
|
||||||
# Otherwise reopening a project pays one extra full SSBO rebuild per model.
|
# Otherwise reopening a project pays one extra full SSBO rebuild per model.
|
||||||
if has_more_assets and callable(refresh_runtime_fn):
|
if has_more_assets and callable(refresh_runtime_fn):
|
||||||
|
|||||||
@ -651,6 +651,7 @@ class ObjectController:
|
|||||||
static_np = chunk["dynamic_np"].copy_to(self.model)
|
static_np = chunk["dynamic_np"].copy_to(self.model)
|
||||||
static_np.set_name(f"chunk_{chunk_id:04d}_static")
|
static_np.set_name(f"chunk_{chunk_id:04d}_static")
|
||||||
static_np.unstash()
|
static_np.unstash()
|
||||||
|
self._clear_string_tags_in_subtree(static_np)
|
||||||
# Editor idle performance depends on the static chunk being actually batched.
|
# Editor idle performance depends on the static chunk being actually batched.
|
||||||
# The merged material/state preservation changes above keep per-geom render
|
# The merged material/state preservation changes above keep per-geom render
|
||||||
# state intact, so we can restore the pre-merge flattening behavior here.
|
# state intact, so we can restore the pre-merge flattening behavior here.
|
||||||
@ -665,6 +666,58 @@ class ObjectController:
|
|||||||
else:
|
else:
|
||||||
static_np.unstash()
|
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):
|
def build_virtual_hierarchy(self):
|
||||||
"""Build a readonly virtual tree from node_list path keys."""
|
"""Build a readonly virtual tree from node_list path keys."""
|
||||||
root = {
|
root = {
|
||||||
|
|||||||
@ -68,6 +68,7 @@ class SSBOEditor:
|
|||||||
self.source_model = None
|
self.source_model = None
|
||||||
self.source_model_root = None
|
self.source_model_root = None
|
||||||
self._source_child_base_mats = {}
|
self._source_child_base_mats = {}
|
||||||
|
self._source_transform_baseline_tag = "_ssbo_source_base_local_mat"
|
||||||
self.last_import_tree_key = None
|
self.last_import_tree_key = None
|
||||||
self.last_import_root_name = None
|
self.last_import_root_name = None
|
||||||
self.ssbo = None
|
self.ssbo = None
|
||||||
@ -547,6 +548,52 @@ class SSBOEditor:
|
|||||||
except Exception:
|
except Exception:
|
||||||
continue
|
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):
|
def _get_top_level_group_keys(self):
|
||||||
if not self.controller or not getattr(self.controller, "tree_root_key", None):
|
if not self.controller or not getattr(self.controller, "tree_root_key", None):
|
||||||
return []
|
return []
|
||||||
@ -636,15 +683,101 @@ class SSBOEditor:
|
|||||||
continue
|
continue
|
||||||
composed_mat = LMatrix4f(base_mat)
|
composed_mat = LMatrix4f(base_mat)
|
||||||
composed_mat *= model_root_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:
|
try:
|
||||||
source_child.set_mat(composed_mat)
|
return LMatrix4f(node.getMat())
|
||||||
source_child.setTag("scene_transform_dirty", "true")
|
|
||||||
except Exception:
|
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:
|
try:
|
||||||
source_child.setMat(composed_mat)
|
if str(source_node.getTag("scene_transform_dirty") or "").lower() == "true":
|
||||||
source_child.setTag("scene_transform_dirty", "true")
|
return False
|
||||||
except Exception:
|
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):
|
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."""
|
"""Persist one runtime node transform back to its matching source-tree node."""
|
||||||
@ -689,16 +822,7 @@ class SSBOEditor:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
local_mat = current_net_mat * inv_parent_mat
|
local_mat = current_net_mat * inv_parent_mat
|
||||||
try:
|
return self._apply_source_local_mat_if_changed(source_node, local_mat)
|
||||||
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
|
|
||||||
|
|
||||||
def _snapshot_runtime_object_transforms_to_source_root(self):
|
def _snapshot_runtime_object_transforms_to_source_root(self):
|
||||||
"""
|
"""
|
||||||
@ -1616,7 +1740,11 @@ class SSBOEditor:
|
|||||||
self._set_node_name(imported_root, unique_root_name)
|
self._set_node_name(imported_root, unique_root_name)
|
||||||
if not skip_heavy_material_processing:
|
if not skip_heavy_material_processing:
|
||||||
self._detach_shared_materials_in_subtree(imported_root)
|
self._detach_shared_materials_in_subtree(imported_root)
|
||||||
|
self._capture_source_transform_baselines(imported_root)
|
||||||
imported_roots = [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.source_model = source_root
|
||||||
self._restore_saved_material_bindings_from_tags(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)
|
self._set_node_name(imported_root, unique_root_name)
|
||||||
if not skip_heavy_material_processing:
|
if not skip_heavy_material_processing:
|
||||||
self._detach_shared_materials_in_subtree(imported_root)
|
self._detach_shared_materials_in_subtree(imported_root)
|
||||||
|
self._capture_source_transform_baselines(imported_root)
|
||||||
|
|
||||||
if keep_source_model and not append:
|
if keep_source_model and not append:
|
||||||
self.source_model = imported_root
|
self.source_model = imported_root
|
||||||
@ -2959,6 +3088,23 @@ class SSBOEditor:
|
|||||||
|
|
||||||
rebuilt = 0
|
rebuilt = 0
|
||||||
for chunk_id in sorted(chunks):
|
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:
|
try:
|
||||||
controller._rebuild_static_chunk(chunk_id)
|
controller._rebuild_static_chunk(chunk_id)
|
||||||
controller._set_chunk_dynamic(chunk_id, False)
|
controller._set_chunk_dynamic(chunk_id, False)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user