diff --git a/RenderPipelineFile/config/pipeline.yaml b/RenderPipelineFile/config/pipeline.yaml index 21f3e201..0ebacc1a 100644 --- a/RenderPipelineFile/config/pipeline.yaml +++ b/RenderPipelineFile/config/pipeline.yaml @@ -8,7 +8,7 @@ pipeline: # it will also disable the hotkeys, and give a small performance boost. # Most likely you also don't want to show it in your own game, so set # it to false in that case. - display_debugger: true + display_debugger: false # Affects which debugging information is displayed. If this is set to false, # only frame time is displayed, otherwise much more information is visible. diff --git a/RenderPipelineFile/config/plugins.yaml b/RenderPipelineFile/config/plugins.yaml index dfce42fa..2f57f8a6 100644 --- a/RenderPipelineFile/config/plugins.yaml +++ b/RenderPipelineFile/config/plugins.yaml @@ -9,7 +9,7 @@ enabled: - color_correction - forward_shading - motion_blur - - pssm + #- pssm - scattering - skin_shading - sky_ao diff --git a/core/selection.py b/core/selection.py index 5d03fb0d..0f48ded3 100644 --- a/core/selection.py +++ b/core/selection.py @@ -2099,9 +2099,20 @@ class SelectionSystem: except Exception as e: print(f"同步 SSBO 选择状态失败: {e}") - effective_node = self._get_effective_selected_node() - if effective_node is None: + effective_node = None + ssbo_active_selection = False + if ssbo_editor and hasattr(ssbo_editor, "has_active_selection"): + try: + ssbo_active_selection = bool(ssbo_editor.has_active_selection()) + except Exception: + ssbo_active_selection = False + + if ssbo_active_selection: + effective_node = self._get_effective_selected_node() + elif self._is_valid_node(nodePath, require_attached=True): effective_node = nodePath + else: + effective_node = self._get_effective_selected_node() self.selectedNode = effective_node # 添加兼容性属性 diff --git a/imgui.ini b/imgui.ini index 6424dec7..3436e0e0 100644 --- a/imgui.ini +++ b/imgui.ini @@ -31,21 +31,21 @@ DockId=0x0000000D,0 [Window][场景树] Pos=0,20 -Size=339,1008 +Size=309,588 Collapsed=0 -DockId=0x00000007,0 +DockId=0x00000003,0 [Window][属性面板] -Pos=1506,20 -Size=346,1008 +Pos=1574,20 +Size=346,989 Collapsed=0 DockId=0x00000002,0 [Window][控制台] -Pos=341,629 -Size=1163,399 +Pos=0,610 +Size=309,399 Collapsed=0 -DockId=0x00000006,1 +DockId=0x00000004,0 [Window][脚本管理] Pos=1950,20 @@ -59,7 +59,7 @@ Collapsed=0 [Window][WindowOverViewport_11111111] Pos=0,20 -Size=1852,1008 +Size=1920,989 Collapsed=0 [Window][测试窗口1] @@ -83,12 +83,12 @@ Size=400,300 Collapsed=0 [Window][选择路径] -Pos=626,264 +Pos=660,254 Size=600,500 Collapsed=0 [Window][打开项目] -Pos=676,314 +Pos=710,316 Size=500,400 Collapsed=0 @@ -98,8 +98,8 @@ Size=600,500 Collapsed=0 [Window][资源管理器] -Pos=341,629 -Size=1163,399 +Pos=311,610 +Size=1261,399 Collapsed=0 DockId=0x00000006,0 @@ -119,8 +119,8 @@ Size=88,226 Collapsed=0 [Window][创建点光源] -Pos=60,60 -Size=109,274 +Pos=750,324 +Size=420,360 Collapsed=0 [Window][创建GUI标签] @@ -129,8 +129,8 @@ Size=93,226 Collapsed=0 [Window][创建聚光灯] -Pos=60,60 -Size=89,250 +Pos=750,344 +Size=420,320 Collapsed=0 [Window][颜色选择器] @@ -206,7 +206,7 @@ Collapsed=0 DockId=0x00000005,2 [Window][项目另存为] -Pos=794,432 +Pos=730,384 Size=460,240 Collapsed=0 @@ -226,10 +226,12 @@ Size=460,260 Collapsed=0 [Docking][Data] -DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=1852,1008 Split=X - DockNode ID=0x00000001 Parent=0x08BD597D SizeRef=2212,1012 Split=X - DockNode ID=0x00000007 Parent=0x00000001 SizeRef=339,1084 Selected=0xE0015051 - DockNode ID=0x00000008 Parent=0x00000001 SizeRef=1871,1084 Split=Y +DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=1920,989 Split=X + DockNode ID=0x00000001 Parent=0x08BD597D SizeRef=1572,1012 Split=X + DockNode ID=0x00000007 Parent=0x00000001 SizeRef=309,1084 Split=Y Selected=0xE0015051 + DockNode ID=0x00000003 Parent=0x00000007 SizeRef=339,588 Selected=0xE0015051 + DockNode ID=0x00000004 Parent=0x00000007 SizeRef=339,399 Selected=0x5428E753 + DockNode ID=0x00000008 Parent=0x00000001 SizeRef=1261,1084 Split=Y DockNode ID=0x00000005 Parent=0x00000008 SizeRef=2048,683 Split=Y DockNode ID=0x0000000D Parent=0x00000005 SizeRef=1318,383 HiddenTabBar=1 Selected=0x43A39006 DockNode ID=0x0000000E Parent=0x00000005 SizeRef=1318,363 CentralNode=1 Selected=0xE0015051 diff --git a/project/project_manager.py b/project/project_manager.py index 465a52c3..1007b022 100644 --- a/project/project_manager.py +++ b/project/project_manager.py @@ -939,31 +939,111 @@ class ProjectManager: if not scene_manager: return [] + def _node_is_valid(node): + if not node: + return False + try: + return not node.isEmpty() + except Exception: + try: + return not node.is_empty() + except Exception: + return False + + ssbo_editor = getattr(self.world, "ssbo_editor", None) + source_model_root = getattr(ssbo_editor, "source_model_root", None) if ssbo_editor else None + runtime_model = getattr(ssbo_editor, "model", None) if ssbo_editor else None + + def _is_source_tree_node(node): + if not _node_is_valid(node): + return False + if not ssbo_editor or not hasattr(ssbo_editor, "is_source_tree_node"): + return False + try: + return bool(ssbo_editor.is_source_tree_node(node)) + except Exception: + return False + + def _is_model_root_candidate(node): + if not _node_is_valid(node): + return False + if runtime_model and node == runtime_model: + return False + try: + if node.hasTag("light_type"): + return False + if node.hasTag("gui_type") or node.hasTag("is_gui_element"): + return False + if node.hasTag("is_model_root") or node.hasTag("asset_guid"): + return True + if node.hasTag("model_path") or node.hasTag("saved_model_path") or node.hasTag("original_path"): + return True + except Exception: + return False + return False + + def _append_unique_model_root(target_list, candidate, include_ssbo_source): + if not _node_is_valid(candidate): + return + if runtime_model and candidate == runtime_model: + return + is_source_node = _is_source_tree_node(candidate) + if is_source_node and not include_ssbo_source: + return + if not is_source_node and not _is_model_root_candidate(candidate): + return + for existing in target_list: + try: + if existing == candidate: + return + except Exception: + continue + target_list.append(candidate) + + def _gather_model_roots(include_ssbo_source=True): + model_nodes = [] + if include_ssbo_source and _node_is_valid(source_model_root): + snapshot_fn = getattr(ssbo_editor, "_snapshot_top_level_transforms_to_source_root", None) + snapshot_material_fn = getattr(ssbo_editor, "_snapshot_runtime_materials_to_source_root", None) + if callable(snapshot_fn): + try: + snapshot_fn() + except Exception: + pass + if callable(snapshot_material_fn): + try: + snapshot_material_fn() + except Exception: + pass + try: + source_children = list(source_model_root.getChildren()) + except Exception: + source_children = [] + for child in source_children: + _append_unique_model_root(model_nodes, child, include_ssbo_source=True) + + for node in list(getattr(scene_manager, "models", []) or []): + _append_unique_model_root(model_nodes, node, include_ssbo_source=include_ssbo_source) + + render = getattr(self.world, "render", None) + if _node_is_valid(render): + try: + render_children = list(render.getChildren()) + except Exception: + render_children = [] + for child in render_children: + _append_unique_model_root(model_nodes, child, include_ssbo_source=include_ssbo_source) + + return model_nodes + root_nodes = [] seen = set() model_root_nodes = [] auxiliary_root_nodes = [] - ssbo_editor = getattr(self.world, "ssbo_editor", None) - source_model_root = getattr(ssbo_editor, "source_model_root", None) if ssbo_editor else None - if source_model_root and not source_model_root.isEmpty(): - snapshot_fn = getattr(ssbo_editor, "_snapshot_top_level_transforms_to_source_root", None) - snapshot_material_fn = getattr(ssbo_editor, "_snapshot_runtime_materials_to_source_root", None) - if callable(snapshot_fn): - try: - snapshot_fn() - except Exception: - pass - if callable(snapshot_material_fn): - try: - snapshot_material_fn() - except Exception: - pass - model_nodes = list(source_model_root.getChildren()) - else: - model_nodes = list(getattr(scene_manager, "models", []) or []) + model_nodes = _gather_model_roots(include_ssbo_source=True) for node in model_nodes: - if not node or node.isEmpty(): + if not _node_is_valid(node): continue node_key = id(node) if node_key in seen: @@ -1098,6 +1178,14 @@ class ProjectManager: if scene_manager: if not ssbo_loaded: scene_manager.models = built_model_nodes + else: + merged_models = list(getattr(scene_manager, "models", []) or []) + for child in built_model_nodes: + if not child or child.isEmpty(): + continue + if child not in merged_models: + merged_models.append(child) + scene_manager.models = merged_models scene_manager.Spotlight = built_spot_lights scene_manager.Pointlight = built_point_lights update_tree_fn = getattr(scene_manager, "updateSceneTree", None) @@ -1556,9 +1644,77 @@ class ProjectManager: pass scene_manager = getattr(self.world, "scene_manager", None) - runtime_model = getattr(ssbo_editor, "model", None) if scene_manager: - scene_manager.models = [runtime_model] if runtime_model and not runtime_model.isEmpty() else [] + source_root = getattr(ssbo_editor, "source_model_root", None) + merged_models = [] + + def _append_model(candidate): + if not candidate: + return + try: + if candidate.isEmpty(): + return + except Exception: + try: + if candidate.is_empty(): + return + except Exception: + return + try: + if ssbo_editor and hasattr(ssbo_editor, "is_source_tree_node") and ssbo_editor.is_source_tree_node(candidate): + pass + except Exception: + pass + for existing in merged_models: + try: + if existing == candidate: + return + except Exception: + continue + merged_models.append(candidate) + + for candidate in list(getattr(scene_manager, "models", []) or []): + try: + if ssbo_editor and hasattr(ssbo_editor, "is_source_tree_node") and ssbo_editor.is_source_tree_node(candidate): + continue + except Exception: + pass + _append_model(candidate) + + if source_root and not source_root.isEmpty(): + try: + for child in list(source_root.getChildren()): + _append_model(child) + except Exception: + pass + + render = getattr(self.world, "render", None) + if render and not render.isEmpty(): + try: + for child in list(render.getChildren()): + if child == getattr(ssbo_editor, "model", None): + continue + try: + is_model_root = ( + child.hasTag("is_model_root") + or child.hasTag("asset_guid") + or child.hasTag("model_path") + or child.hasTag("saved_model_path") + ) + except Exception: + is_model_root = False + if not is_model_root: + continue + try: + if ssbo_editor and hasattr(ssbo_editor, "is_source_tree_node") and ssbo_editor.is_source_tree_node(child): + continue + except Exception: + pass + _append_model(child) + except Exception: + pass + + scene_manager.models = merged_models scene_manager.Spotlight = [] scene_manager.Pointlight = [] update_tree_fn = getattr(scene_manager, "updateSceneTree", None) diff --git a/scene/scene_manager_io_mixin.py b/scene/scene_manager_io_mixin.py index b03d431d..bfcd0625 100644 --- a/scene/scene_manager_io_mixin.py +++ b/scene/scene_manager_io_mixin.py @@ -511,6 +511,16 @@ class SceneManagerIOMixin: if node.isEmpty(): continue + if node.hasTag("is_model_root"): + try: + managed_by_ssbo = False + ssbo_editor = getattr(self.world, "ssbo_editor", None) + if ssbo_editor and hasattr(ssbo_editor, "is_source_tree_node"): + managed_by_ssbo = bool(ssbo_editor.is_source_tree_node(node)) + node.setTag("ssbo_managed", "true" if managed_by_ssbo else "false") + except Exception: + pass + # 保存变换信息 node.setTag("transform_pos", str(node.getPos())) node.setTag("transform_hpr", str(node.getHpr())) @@ -1015,16 +1025,51 @@ class SceneManagerIOMixin: filename, scene_package_import=True, ) - 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: + if not ssbo_scene_model or ssbo_scene_model.isEmpty(): print("[SSBO] 统一导入未返回有效模型,回退旧流程。") use_ssbo_scene_import = False except Exception as e: print(f"[SSBO] 统一导入失败,回退旧流程: {e}") use_ssbo_scene_import = False + def node_should_use_ssbo_runtime(node_path): + if not (use_ssbo_scene_import and node_path and not node_path.isEmpty()): + return False + if not node_path.hasTag("is_model_root"): + return False + + def tag_is_enabled(tag_name, default=False): + try: + if not node_path.hasTag(tag_name): + return bool(default) + value = str(node_path.getTag(tag_name) or "").strip().lower() + if value in ("1", "true", "yes", "on"): + return True + if value in ("0", "false", "no", "off"): + return False + except Exception: + pass + return bool(default) + + has_saved_animation = ( + tag_is_enabled("saved_has_animations", default=False) + or tag_is_enabled("has_animations", default=False) + ) + if not has_saved_animation: + try: + if node_path.hasTag("gltf_animation_count"): + has_saved_animation = int(str(node_path.getTag("gltf_animation_count") or "0").strip() or "0") > 0 + except Exception: + has_saved_animation = False + + if has_saved_animation: + return False + + if node_path.hasTag("ssbo_managed"): + return tag_is_enabled("ssbo_managed", default=True) + + return True + def parse_saved_color(color_str, default=None): try: cleaned = str(color_str).strip() @@ -1456,7 +1501,7 @@ class SceneManagerIOMixin: # 这里我们先保持原挂载关系 pass else: - if use_ssbo_scene_import and is_model_root: + if node_should_use_ssbo_runtime(nodePath): # SSBO 模式下模型节点由新导入链路接管,这里不直接挂载。 pass # 其他节点确保挂载到render下 @@ -1468,12 +1513,13 @@ class SceneManagerIOMixin: # 为模型节点设置碰撞检测 if is_model_root: print(f"J{indent}处理模型节点{nodePath.getName()}") - if use_ssbo_scene_import: + if node_should_use_ssbo_runtime(nodePath): # SSBO 模式下整个 scene.bam 已通过统一导入链路载入, # 这里跳过逐模型旧导入逻辑,避免与菜单导入路径不一致。 pass else: #self._validateAndFixAllTransforms(nodePath) + nodePath.setTag("ssbo_managed", "false") self._fixModelStructure(nodePath) self._restoreModelAnimationInfo(nodePath) self._processModelAnimations(nodePath) @@ -1496,7 +1542,8 @@ class SceneManagerIOMixin: # else: # print(f"{indent}为模型{nodePath.getName()}设置碰撞检测") # self.setupCollision(nodePath) - self.models.append(nodePath) + if nodePath not in self.models: + self.models.append(nodePath) # 递归处理子节点 for child in nodePath.getChildren(): @@ -1505,6 +1552,50 @@ class SceneManagerIOMixin: print("\n开始处理场景节点...") processNode(scene) + if use_ssbo_scene_import: + try: + ssbo_editor = getattr(self.world, "ssbo_editor", None) + source_root = getattr(ssbo_editor, "source_model_root", None) if ssbo_editor else None + cache_base_mat = getattr(ssbo_editor, "_cache_top_level_source_child_base_mat", None) if ssbo_editor else None + refresh_runtime = getattr(ssbo_editor, "refresh_runtime_from_source", None) if ssbo_editor else None + synced_transforms = 0 + if source_root and not source_root.isEmpty(): + for source_child in source_root.getChildren(): + if not source_child or source_child.isEmpty(): + continue + child_name = source_child.getName() + matched_node = loaded_nodes.get(child_name) + if not matched_node or matched_node.isEmpty(): + continue + if matched_node.hasTag("ssbo_managed") and matched_node.getTag("ssbo_managed").strip().lower() in ("0", "false", "no", "off"): + continue + try: + local_mat = matched_node.getMat(scene) + except Exception: + try: + local_mat = matched_node.get_mat(scene) + except Exception: + continue + try: + source_child.setMat(local_mat) + except Exception: + try: + source_child.set_mat(local_mat) + except Exception: + continue + if callable(cache_base_mat): + try: + cache_base_mat(source_child, local_mat) + except Exception: + pass + synced_transforms += 1 + + if synced_transforms and callable(refresh_runtime): + print(f"[SceneLoad] 已同步SSBO顶层模型变换: {synced_transforms}") + refresh_runtime(preserve_selection=False) + except Exception as e: + print(f"[SceneLoad] 同步SSBO顶层模型变换失败: {e}") + # SSBO 模式下模型已在前面统一导入;若失败已自动回退旧流程。 #处理父子关系 - 在所有节点加载完成后设置正确的父子关系 diff --git a/scene/scene_manager_model_mixin.py b/scene/scene_manager_model_mixin.py index 59baf7c4..08610d1c 100644 --- a/scene/scene_manager_model_mixin.py +++ b/scene/scene_manager_model_mixin.py @@ -20,6 +20,61 @@ from scene import util from core.editor_context import get_editor_context class SceneManagerModelMixin: + def _build_model_filename_candidates(self, path_text): + """Build robust Panda Filename candidates for Windows/CJK absolute paths.""" + candidates = [] + seen = set() + if not path_text: + return candidates + + for variant in (path_text, util.normalize_model_path(path_text)): + if not variant: + continue + 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(variant) + key = fn.get_fullpath() + if not key or key in seen: + continue + seen.add(key) + candidates.append(fn) + except Exception: + continue + try: + fn = Filename(variant) + key = fn.get_fullpath() + if key and key not in seen: + seen.add(key) + candidates.append(fn) + except Exception: + continue + return candidates + + def _load_model_from_candidates(self, primary_path, fallback_path=""): + """Try multiple Filename constructors, optionally falling back to a second path.""" + attempts = [] + last_error = None + + for candidate_path in (primary_path, fallback_path): + if not candidate_path or candidate_path in attempts: + continue + attempts.append(candidate_path) + for fn in self._build_model_filename_candidates(candidate_path): + try: + model = self.world.loader.loadModel(fn) + if model and not model.isEmpty(): + return model, candidate_path + except Exception as exc: + last_error = exc + continue + + if last_error: + raise RuntimeError(f"Could not load model file(s): {attempts}: {last_error}") + raise RuntimeError(f"Could not load model file(s): {attempts}") + def importModel(self, filepath, apply_unit_conversion=False, normalize_scales=True, auto_convert_to_glb=True): try: if not os.path.exists(filepath): @@ -28,6 +83,35 @@ class SceneManagerModelMixin: filepath = util.normalize_model_path(filepath) original_filepath = filepath + visual_load_path = filepath + gltf_meta = None + + try: + from scene.gltf_support import ensure_gltf_visual_bam, probe_gltf_metadata + + gltf_meta = probe_gltf_metadata(filepath) + if gltf_meta.get("is_gltf"): + has_anim = gltf_meta.get("has_animations", False) + # 智能加载策略: + # 1. 如果模型有动画,则强制使用 panda3d-gltf 构建的可见性缓存(BAM),以保证动画支持。 + # 2. 如果模型是纯静态场景,则跳过缓存,使用原生加载器(如 Assimp),这样在大场景下更流畅。 + if has_anim: + project_manager = getattr(getattr(self, "world", None), "project_manager", None) + project_root = getattr(project_manager, "current_project_path", "") if project_manager else "" + + cached_visual_path = ensure_gltf_visual_bam( + filepath, + project_root=project_root, + skip_animations=False, # 有动画的模型不应跳过动画 + flatten_nodes=False, + ) + if cached_visual_path and cached_visual_path != filepath: + visual_load_path = cached_visual_path + print(f"[GLTF智能加载] 检测到动画,使用 panda3d-gltf 缓存: {cached_visual_path}") + else: + print(f"[GLTF智能加载] 纯静态模型,跳过缓存以开启流畅模式: {filepath}") + except Exception as e: + print(f"[GLTF可见缓存] 回退原始模型导入: {e}") # # 在加载前设置忽略未知属性 # from panda3d.core import ConfigVariableBool @@ -48,8 +132,17 @@ class SceneManagerModelMixin: # else: # print(f"⚠️ 转换失败,使用原始文件") - model = self.world.loader.loadModel(filepath) - if not model: + loaded_from_path = visual_load_path + try: + model, loaded_from_path = self._load_model_from_candidates( + visual_load_path, + fallback_path=filepath if visual_load_path != filepath else "", + ) + except Exception as e: + print(f"导入模型失败: {str(e)}") + return None + + if not model or model.isEmpty(): print("加载模型失败") return None @@ -114,9 +207,13 @@ class SceneManagerModelMixin: model.setTag("model_path", normalized_filepath) model.setTag("original_path", original_filepath) + if loaded_from_path != filepath: + model.setTag("visual_model_cache_path", loaded_from_path) if normalized_filepath != original_filepath: model.setTag("converted_from", os.path.splitext(original_filepath)[1]) model.setTag("converted_to_glb", "true") + if gltf_meta and gltf_meta.get("is_gltf"): + model.setTag("gltf_animation_count", str(int(gltf_meta.get("animation_count", 0) or 0))) #特殊处理FBX模型 if filepath.lower().endswith('.fbx'): @@ -147,6 +244,7 @@ class SceneManagerModelMixin: model.setTag("is_model_root", "1") model.setTag("is_scene_element", "1") model.setTag("tree_item_type", "IMPORTED_MODEL_NODE") + model.setTag("ssbo_managed", "false") # 记录应用的处理选项 if apply_unit_conversion: @@ -937,6 +1035,31 @@ class SceneManagerModelMixin: print(f"模型 {model_node.getName()} 已有动画信息") return True + # 优先从 glTF 元数据探测动画,避免依赖场景内必须保留骨骼节点。 + source_path = "" + for tag_name in ("original_path", "model_path", "saved_model_path", "file"): + try: + if model_node.hasTag(tag_name) and model_node.getTag(tag_name): + source_path = model_node.getTag(tag_name) + break + except Exception: + continue + + try: + from scene.gltf_support import probe_gltf_metadata + + gltf_meta = probe_gltf_metadata(source_path) + if gltf_meta.get("is_gltf"): + has_animations = bool(gltf_meta.get("has_animations")) + model_node.setTag("has_animations", "true" if has_animations else "false") + model_node.setTag("has_animations_checked", "true") + model_node.setTag("gltf_animation_count", str(int(gltf_meta.get("animation_count", 0) or 0))) + if has_animations: + model_node.setTag("can_create_actor_from_memory", "false") + return has_animations + except Exception: + pass + # 检查模型是否包含动画相关节点 character_nodes = model_node.findAllMatches("**/+Character") anim_bundle_nodes = model_node.findAllMatches("**/+AnimBundleNode") diff --git a/ssbo_component/ssbo_controller.py b/ssbo_component/ssbo_controller.py index 6f7c7ef2..8cb73647 100644 --- a/ssbo_component/ssbo_controller.py +++ b/ssbo_component/ssbo_controller.py @@ -1,4 +1,4 @@ - + import math from panda3d.core import ( GeomVertexFormat, GeomVertexWriter, GeomVertexReader, GeomVertexRewriter, @@ -41,6 +41,8 @@ class ObjectController: self.model = None self.pick_model = None + self.lightweight_flat_mode = False + self.supports_gpu_picking = True self.id_to_chunk = {} # global_id -> chunk_id self.id_to_object_np = {} # global_id -> dynamic object nodepath self.id_to_pick_np = {} # global_id -> pick-scene nodepath @@ -397,7 +399,7 @@ class ObjectController: except Exception: pass - def bake_ids_and_collect(self, model): + def bake_ids_and_collect(self, model, lightweight=False): """ Bake IDs into vertex colors, flatten, then build vertex index. @@ -435,6 +437,34 @@ class ObjectController: # selection/tree semantics as hybrid mode. self._build_scene_tree(model) + if lightweight: + self.lightweight_flat_mode = True + self.supports_gpu_picking = False + self.model = model + self.chunk_node = model + chunk_key = model.get_name() or "default" + self.chunks[chunk_key] = {'node': model, 'base_id': 0} + self.key_to_node[self.tree_root_key] = model + try: + # 对于超大模型(节点数 > 8000),使用较温和的 flatten_medium, + # 避免 flatten_strong 造成长时间卡顿或内存激增。 + node_count = model.find_all_matches("**").get_num_paths() + if node_count > 8000: + print(f"[控制器] 超大场景({node_count} 节点),使用 flatten_medium 代替 flatten_strong") + model.flatten_medium() + else: + model.flatten_strong() + except Exception as e: + print(f"[控制器] Flatten 失败: {e}") + pass + self._aggregate_tree_ids(self.tree_root_key) + self.node_list = [] + self._build_tree_preorder(self.tree_root_key, self.node_list) + t1 = time.time() + print(f"[控制器] Flatten took {(t1-t0)*1000:.0f}ms") + print(f"[控制器] Lightweight flat tree built: {len(self.tree_nodes)} nodes") + return len(geom_nodes) + global_id_counter = 0 chunk_key = model.get_name() or "default" diff --git a/ssbo_component/ssbo_editor.py b/ssbo_component/ssbo_editor.py index 24d7231a..bf3bcec9 100644 --- a/ssbo_component/ssbo_editor.py +++ b/ssbo_component/ssbo_editor.py @@ -94,6 +94,8 @@ class SSBOEditor: self._last_group_sync_mat = None self._last_single_sync_gid = None self._last_single_sync_mat = None + self._group_proxy_initial_mat = None + self._group_proxy_source_initial_net_mat = None # Performance toggle: forcing shadow tasks every frame is expensive. # Keep it off by default so frustum/content reduction has clearer FPS impact. self.realtime_shadow_updates = False @@ -302,6 +304,29 @@ class SSBOEditor: """Load a source model NodePath from disk without touching current runtime state.""" source_model = None last_error = None + load_path = model_path + try: + from scene.gltf_support import ensure_gltf_visual_bam, probe_gltf_metadata + + gltf_meta = probe_gltf_metadata(model_path) + if gltf_meta.get("is_gltf"): + has_anim = gltf_meta.get("has_animations", False) + if has_anim: + project_manager = getattr(self.base, "project_manager", None) + project_root = getattr(project_manager, "current_project_path", "") if project_manager else "" + cached_visual_path = ensure_gltf_visual_bam( + model_path, + project_root=project_root, + skip_animations=False, # 既然是为了动画,就不跳过 + flatten_nodes=False, + ) + if cached_visual_path and cached_visual_path != model_path: + load_path = cached_visual_path + print(f"[GLTF智能加载] SSBO检测到动画,使用缓存: {cached_visual_path}") + else: + print(f"[GLTF智能加载] SSBO识别为静态模型,跳过缓存以获得最大流畅度") + except Exception as e: + print(f"[GLTF可见缓存] SSBO回退原始模型加载: {e}") loader_options = None try: from panda3d.core import LoaderOptions @@ -312,7 +337,7 @@ class SSBOEditor: loader_options.setFlags(loader_options.getFlags() | loader_options.LFNoCache) except Exception: loader_options = None - for fn in self._build_filename_candidates(model_path): + for fn in self._build_filename_candidates(load_path): try: if loader_options is not None: source_model = self.base.loader.loadModel(fn, loader_options) @@ -527,6 +552,45 @@ class SSBOEditor: return candidate index += 1 + @staticmethod + def _tag_is_enabled(node, tag_name, default=False): + try: + if not node or not node.hasTag(tag_name): + return bool(default) + value = str(node.getTag(tag_name) or "").strip().lower() + if value in ("1", "true", "yes", "on"): + return True + if value in ("0", "false", "no", "off"): + return False + except Exception: + pass + return bool(default) + + def _node_has_saved_animation_info(self, node): + if not self._node_is_valid(node): + return False + for tag_name in ("saved_has_animations", "has_animations"): + if self._tag_is_enabled(node, tag_name, default=False): + return True + try: + if node.hasTag("gltf_animation_count"): + return int(str(node.getTag("gltf_animation_count") or "0").strip() or "0") > 0 + except Exception: + pass + return False + + def _should_skip_scene_package_child_for_ssbo(self, node): + if not self._node_is_valid(node): + return False + if self._node_has_saved_animation_info(node): + return True + try: + if node.hasTag("ssbo_managed"): + return not self._tag_is_enabled(node, "ssbo_managed", default=True) + except Exception: + pass + return False + def _capture_source_child_base_mats(self): """Capture baseline local mats for each top-level source child.""" self._source_child_base_mats = {} @@ -609,6 +673,40 @@ class SSBOEditor: except Exception: continue + def _cache_top_level_source_child_base_mat(self, source_node, local_mat=None): + """Keep top-level source-child baselines in sync with explicit subtree edits.""" + if not self._node_is_valid(source_node) or not self._node_is_valid(self.source_model_root): + return False + + try: + source_parent = source_node.get_parent() + except Exception: + try: + source_parent = source_node.getParent() + except Exception: + return False + + if source_parent != self.source_model_root: + return False + + child_name = self._get_node_name(source_node, None) + if not child_name: + return False + + if local_mat is None: + try: + local_mat = LMatrix4f(source_node.get_mat()) + except Exception: + try: + local_mat = LMatrix4f(source_node.getMat()) + except Exception: + return False + else: + local_mat = LMatrix4f(local_mat) + + self._source_child_base_mats[child_name] = local_mat + return True + def _snapshot_active_selection_transform_to_source(self): """Persist only the actively edited selection back into the source tree.""" if not self.controller or not self.model or not self.source_model_root: @@ -624,13 +722,46 @@ class SSBOEditor: if not self._node_is_valid(scene_node) or not self._node_is_valid(source_node): return False - try: - current_net_mat = LMatrix4f(scene_node.get_mat(self.model)) - except Exception: + current_net_mat = None + proxy = getattr(self, "_group_proxy", None) + if ( + proxy + and scene_node == proxy + and self._group_proxy_initial_mat is not None + and self._group_proxy_source_initial_net_mat is not None + ): try: - current_net_mat = LMatrix4f(scene_node.getMat(self.model)) + current_proxy_mat = LMatrix4f(proxy.get_mat(self.model)) except Exception: - current_net_mat = None + try: + current_proxy_mat = LMatrix4f(proxy.getMat(self.model)) + except Exception: + current_proxy_mat = None + if current_proxy_mat is not None: + proxy_delta_mat = LMatrix4f(self._group_proxy_initial_mat) + try: + proxy_delta_mat.invertInPlace() + except Exception: + try: + proxy_delta_mat.invert_in_place() + except Exception: + proxy_delta_mat = None + if proxy_delta_mat is not None: + proxy_delta_mat *= current_proxy_mat + current_net_mat = LMatrix4f(self._group_proxy_source_initial_net_mat) + current_net_mat *= proxy_delta_mat + # Make repeated snapshots idempotent until the selection is cleared. + self._group_proxy_initial_mat = LMatrix4f(current_proxy_mat) + self._group_proxy_source_initial_net_mat = LMatrix4f(current_net_mat) + + if current_net_mat is None: + try: + current_net_mat = LMatrix4f(scene_node.get_mat(self.model)) + except Exception: + try: + current_net_mat = LMatrix4f(scene_node.getMat(self.model)) + except Exception: + current_net_mat = None if current_net_mat is None: return False @@ -671,6 +802,7 @@ class SSBOEditor: source_node.setTag("scene_transform_dirty", "true") except Exception: return False + self._cache_top_level_source_child_base_mat(source_node, local_mat) return True def _resolve_source_node_by_tree_key(self, tree_key): @@ -1236,9 +1368,9 @@ class SSBOEditor: print(f"[SSBOEditor] Restored saved material bindings from tags: {restored}") return restored - def _clear_runtime_state(self, preserve_source_models=False): + def _clear_runtime_state(self, preserve_source_models=False, snapshot_transform=True): """Remove runtime SSBO controller/model state while optionally keeping source snapshots.""" - self.clear_selection() + self.clear_selection(snapshot_transform=snapshot_transform) self._cleanup_group_proxy() self._reset_pick_sync_cache() self._teardown_gpu_picking() @@ -1310,7 +1442,7 @@ class SSBOEditor: if self.controller and self.model: self._snapshot_top_level_transforms_to_source_root() - self._clear_runtime_state(preserve_source_models=True) + self._clear_runtime_state(preserve_source_models=True, snapshot_transform=False) try: target.detach_node() @@ -1339,7 +1471,7 @@ class SSBOEditor: return False if self.controller and self.model: self._snapshot_top_level_transforms_to_source_root() - self._clear_runtime_state(preserve_source_models=True) + self._clear_runtime_state(preserve_source_models=True, snapshot_transform=False) for child in children: try: child.detach_node() @@ -1354,7 +1486,7 @@ class SSBOEditor: if self.controller and self.model: self._snapshot_top_level_transforms_to_source_root() - self._clear_runtime_state(preserve_source_models=True) + self._clear_runtime_state(preserve_source_models=True, snapshot_transform=False) try: target.detach_node() except Exception: @@ -1373,7 +1505,7 @@ class SSBOEditor: if self.controller and self.model: self._snapshot_top_level_transforms_to_source_root() - self._clear_runtime_state(preserve_source_models=True) + self._clear_runtime_state(preserve_source_models=True, snapshot_transform=False) source_root = self._ensure_source_model_root() target_name = highlight_root_name or self._get_node_name(child_np, "imported_model") @@ -1415,14 +1547,30 @@ class SSBOEditor: else: self._detach_shared_materials_in_subtree(working_root) self._materialize_explicit_geom_materials(working_root) - # Flat mode keeps huge scenes fast, but it cannot provide stable per-child - # runtime NodePaths for gizmo/outline/property editing. For editor usage we - # prefer hybrid mode for most imported scenes so child nodes remain editable. - use_hybrid_mode = geom_count > 0 and geom_count <= 12000 + # Large scenes should prefer the old flat import path. + # Hybrid mode keeps per-child editing, but for vegetation/campus-like GLB + # scenes it explodes into tens of thousands of runtime objects/chunks and + # the editor becomes unusable or crashes. + # 提高使用 hybrid 模式的阈值。 + # 对于类似 jyc.glb 的场景,hybrid 模式的对象分块(chunks)比一个巨大的 flat 节点拥有更好的剔除性能和帧率。 + # 只有在极端规模(如 > 20000 节点或 > 10000 几何体)的情况下才强制退回到 flat 模式以节省内存。 + prefer_flat_mode = ( + geom_count > 10000 + or node_count > 20000 + ) + use_hybrid_mode = geom_count > 0 and not prefer_flat_mode + if prefer_flat_mode: + print( + f"[SSBOEditor] Large scene uses flat runtime path " + f"(nodes={node_count}, geoms={geom_count})" + ) if use_hybrid_mode: count = self.controller.bake_ids_and_collect_hybrid(working_root) else: - count = self.controller.bake_ids_and_collect(working_root) + count = self.controller.bake_ids_and_collect( + working_root, + lightweight=prefer_flat_mode, + ) self.model = self.controller.model self.model.reparent_to(self.base.render) @@ -1487,7 +1635,7 @@ class SSBOEditor: if append and self.source_model_root: if self.controller and self.model: self._snapshot_top_level_transforms_to_source_root() - self._clear_runtime_state(preserve_source_models=True) + self._clear_runtime_state(preserve_source_models=True, snapshot_transform=False) else: self._clear_runtime_state(preserve_source_models=False) @@ -1497,6 +1645,7 @@ class SSBOEditor: # 直接把其顶层子节点并入 source_root,保持场景树与保存时一致。 if scene_package_import: imported_roots = [] + skipped_legacy_roots = 0 children = [] try: children = [c for c in source_model.get_children() if c and not c.is_empty()] @@ -1511,19 +1660,25 @@ class SSBOEditor: child_name = self._get_node_name(child, "") if child_name in {"render", "render2d", "aspect2d"}: continue + if self._should_skip_scene_package_child_for_ssbo(child): + skipped_legacy_roots += 1 + print(f"[SSBOSceneLoad] 跳过普通/动画模型: {child_name}") + continue imported_child = child.copyTo(source_root) if not skip_heavy_material_processing: self._detach_shared_materials_in_subtree(imported_child) + imported_child.setTag("ssbo_managed", "true") imported_roots.append(imported_child) except Exception: continue - if not imported_roots: + if not imported_roots and skipped_legacy_roots == 0: fallback_name = self._get_node_name(source_model, "scene_root") unique_root_name = self._make_unique_source_child_name(fallback_name) self._set_node_name(source_model, unique_root_name) imported_root = source_model.copyTo(source_root) self._set_node_name(imported_root, unique_root_name) + imported_root.setTag("ssbo_managed", "true") if not skip_heavy_material_processing: self._detach_shared_materials_in_subtree(imported_root) imported_roots = [imported_root] @@ -1532,7 +1687,10 @@ class SSBOEditor: self._restore_saved_material_bindings_from_tags(source_root) self._capture_source_child_base_mats() if rebuild_runtime: - self._rebuild_runtime_from_source_root(highlight_root_name=None) + if self._get_source_root_children(): + self._rebuild_runtime_from_source_root(highlight_root_name=None) + else: + self._rebuild_or_clear_runtime_from_current_source() if len(imported_roots) == 1: return imported_roots[0] return source_root @@ -1541,6 +1699,7 @@ class SSBOEditor: self._set_node_name(source_model, unique_root_name) imported_root = source_model.copyTo(source_root) self._set_node_name(imported_root, unique_root_name) + imported_root.setTag("ssbo_managed", "true") if not skip_heavy_material_processing: self._detach_shared_materials_in_subtree(imported_root) @@ -2130,6 +2289,11 @@ class SSBOEditor: """Setup GPU Picking (Basic implementation)""" self._teardown_gpu_picking() + controller = getattr(self, "controller", None) + if controller and not getattr(controller, "supports_gpu_picking", True): + print("[GPU Picking] Disabled for lightweight large-scene runtime.") + return + win_props = WindowProperties() win_props.set_size(1, 1) fb_props = FrameBufferProperties() @@ -2345,9 +2509,9 @@ class SSBOEditor: selection_key = _resolve_pick_selection_key(node_key) print(f"[Pick] Hit: ID={hit_id} -> {node_key} (select={selection_key})") self.select_node(selection_key) - return - - self.clear_selection() + return True + + return False def on_mouse_click(self): @@ -2370,7 +2534,24 @@ class SSBOEditor: self._sync_pick_model_transform() self._refresh_ssbo_proxy_center() mpos = self.base.mouseWatcherNode.get_mouse() - self.pick_object(mpos.x, mpos.y) + if self.pick_object(mpos.x, mpos.y): + return + + try: + win_width, win_height = self.base.win.getSize() + window_x = (float(mpos.x) + 1.0) * 0.5 * float(win_width) + window_y = (1.0 - float(mpos.y)) * 0.5 * float(win_height) + event_handler = getattr(self.base, "event_handler", None) + if event_handler and hasattr(event_handler, "mousePressEventLeft"): + event_handler.mousePressEventLeft({ + "x": window_x, + "y": window_y, + }) + return + except Exception as e: + print(f"[SSBOEditor] Legacy pick fallback failed: {e}") + + self.clear_selection() def toggle_debug(self): self.debug_mode = not self.debug_mode @@ -2679,6 +2860,25 @@ class SSBOEditor: current_key = parent_key return False + def _should_use_model_root_for_top_level_selection(self, key=None): + if not self.controller: + return False + selected_key = key if key is not None else self.selected_name + root_key = getattr(self.controller, "tree_root_key", None) + if not selected_key or not root_key or selected_key == root_key: + return False + if not ( + self._is_top_level_source_child_selection(selected_key) + or self._is_displayed_top_level_selection(selected_key) + ): + return False + try: + root_node = self.controller.tree_nodes.get(root_key, {}) if root_key else {} + top_level_children = list(root_node.get("children", []) or []) + return len(top_level_children) <= 1 + except Exception: + return False + def get_selection_scene_node(self): """Return a stable scene node for editor features that need one.""" if not self.controller or self.selected_name is None: @@ -2690,14 +2890,14 @@ class SSBOEditor: if self._is_root_selection(): return self.model if self._node_is_valid(self.model) else None - if self._is_top_level_source_child_selection() or self._is_displayed_top_level_selection(): - return self.model if self._node_is_valid(self.model) else None - if len(transform_ids) > 1: proxy = getattr(self, "_group_proxy", None) if proxy and self._node_is_valid(proxy): return proxy + if self._should_use_model_root_for_top_level_selection(): + return self.model if self._node_is_valid(self.model) else None + fallback_node = None try: fallback_node = getattr(self.controller, "key_to_node", {}).get(self.selected_name) @@ -2752,6 +2952,47 @@ class SSBOEditor: "is_group": len(self.get_selection_transform_ids()) > 1 and not self._is_root_selection(), } + def estimate_selection_cost(self, key=None): + """Estimate how expensive a selection would be in hybrid SSBO mode.""" + if not self.controller: + return { + "key": key, + "object_count": 0, + "chunk_count": 0, + "is_root": False, + "is_top_level_like": False, + } + + selected_key = key if key is not None else self.selected_name + if selected_key is None: + return { + "key": None, + "object_count": 0, + "chunk_count": 0, + "is_root": False, + "is_top_level_like": False, + } + + transform_ids = list(getattr(self.controller, "name_to_ids", {}).get(selected_key, []) or []) + chunk_ids = set() + for transform_id in transform_ids: + chunk_id = getattr(self.controller, "id_to_chunk", {}).get(transform_id) + if isinstance(chunk_id, (tuple, list)): + chunk_id = chunk_id[0] if chunk_id else None + if chunk_id is not None: + chunk_ids.add(chunk_id) + + return { + "key": selected_key, + "object_count": len(transform_ids), + "chunk_count": len(chunk_ids), + "is_root": bool(selected_key == getattr(self.controller, "tree_root_key", None)), + "is_top_level_like": bool( + self._is_top_level_source_child_selection(selected_key) + or self._is_displayed_top_level_selection(selected_key) + ), + } + def get_selection_key(self): if not self.controller or self.selected_name is None: return None @@ -2760,7 +3001,7 @@ class SSBOEditor: def get_selection_source_node(self): if not self.controller or self.selected_name is None: return None - if self._is_top_level_source_child_selection() or self._is_displayed_top_level_selection(): + if self._should_use_model_root_for_top_level_selection(): source_children = self._get_source_root_children() if len(source_children) == 1 and self._node_is_valid(source_children[0]): return source_children[0] @@ -2939,8 +3180,9 @@ class SSBOEditor: if self.has_active_selection(): self.clear_selection(sync_world_selection=False) - def clear_selection(self, sync_world_selection=True): - self._snapshot_active_selection_transform_to_source() + def clear_selection(self, sync_world_selection=True, snapshot_transform=True): + if snapshot_transform: + self._snapshot_active_selection_transform_to_source() self._stop_pick_sync_task() self._reset_pick_sync_cache() self._cleanup_group_proxy() @@ -2994,6 +3236,8 @@ class SSBOEditor: proxy.remove_node() self._group_proxy = None self._group_original_parents = {} + self._group_proxy_initial_mat = None + self._group_proxy_source_initial_net_mat = None def update_selection_mask(self): pass # No selection mask texture needed without custom shader @@ -3017,6 +3261,7 @@ class SSBOEditor: key == getattr(self.controller, "tree_root_key", None) ) is_displayed_top_level_selection = self._is_displayed_top_level_selection(key) + use_model_root_for_top_level_selection = self._should_use_model_root_for_top_level_selection(key) is_top_level_heavy_selection = False if self.controller and not is_root_selection and not is_displayed_top_level_selection: try: @@ -3031,7 +3276,12 @@ class SSBOEditor: # Root selection should stay lightweight: # keep static chunks active and transform the model root directly. - if is_root_selection or is_displayed_top_level_selection or is_top_level_heavy_selection or not has_dynamic_objects: + if ( + is_root_selection + or use_model_root_for_top_level_selection + or is_top_level_heavy_selection + or not has_dynamic_objects + ): self.controller.set_active_ids([]) if self._outline_manager and not (not has_dynamic_objects and not is_root_selection): self._outline_manager.clear() @@ -3101,11 +3351,34 @@ class SSBOEditor: self._group_proxy = proxy self._group_original_parents = {} + self._group_proxy_initial_mat = None + self._group_proxy_source_initial_net_mat = None for gid in valid: obj_np = self.controller.id_to_object_np[gid] self._group_original_parents[gid] = obj_np.get_parent() obj_np.wrt_reparent_to(proxy) + source_node = self.get_selection_source_node() + if self._node_is_valid(source_node): + try: + self._group_proxy_initial_mat = LMatrix4f(proxy.get_mat(self.model)) + except Exception: + try: + self._group_proxy_initial_mat = LMatrix4f(proxy.getMat(self.model)) + except Exception: + self._group_proxy_initial_mat = None + try: + self._group_proxy_source_initial_net_mat = LMatrix4f( + source_node.get_mat(self.source_model_root) + ) + except Exception: + try: + self._group_proxy_source_initial_net_mat = LMatrix4f( + source_node.getMat(self.source_model_root) + ) + except Exception: + self._group_proxy_source_initial_net_mat = None + if sync_world_selection: self._sync_editor_selection_reference(proxy) self._transform_gizmo.attach(proxy) diff --git a/ui/panels/app_actions.py b/ui/panels/app_actions.py index 91ddb425..b7dfb7d4 100644 --- a/ui/panels/app_actions.py +++ b/ui/panels/app_actions.py @@ -46,6 +46,14 @@ class AppActions: try: if not file_path or not os.path.exists(file_path): return False + try: + from scene.gltf_support import probe_gltf_metadata + + gltf_meta = probe_gltf_metadata(file_path) + if gltf_meta.get("is_gltf"): + return bool(gltf_meta.get("has_animations")) + except Exception: + pass loader = getattr(self, "loader", None) if not loader: return False @@ -1338,9 +1346,6 @@ class AppActions: model_np = getattr(ssbo_editor, 'model', None) if ssbo_editor else None scene_manager = getattr(self, 'scene_manager', None) - if scene_manager and hasattr(scene_manager, 'models'): - scene_manager.models = [model_np] if model_np else [] - if not model_np: return None @@ -1377,6 +1382,7 @@ class AppActions: model_np.setTag("is_model_root", "1") model_np.setTag("is_scene_element", "1") + model_np.setTag("ssbo_managed", "true") ssbo_source_root = getattr(ssbo_editor, "source_model_root", None) source_children = [] @@ -1436,9 +1442,57 @@ class AppActions: target_source_child.setTag("asset_path", asset_path) target_source_child.setTag("is_model_root", "1") target_source_child.setTag("is_scene_element", "1") + target_source_child.setTag("ssbo_managed", "true") except Exception as e: print(f"[SSBO] sync source child tags failed: {e}") + if scene_manager and hasattr(scene_manager, 'models'): + try: + current_models = [] + existing_models = list(getattr(scene_manager, "models", []) or []) + + for candidate in existing_models: + if not candidate: + continue + try: + if candidate.isEmpty(): + continue + except Exception: + try: + if candidate.is_empty(): + continue + except Exception: + continue + + if candidate == model_np: + continue + + try: + if ssbo_editor and ssbo_editor.is_source_tree_node(candidate): + continue + except Exception: + pass + + current_models.append(candidate) + + if source_children: + current_models.extend( + child for child in source_children + if child and not child.isEmpty() + ) + elif model_np and not model_np.isEmpty(): + current_models.append(model_np) + + deduped_models = [] + for candidate in current_models: + if not candidate or candidate.isEmpty(): + continue + if candidate not in deduped_models: + deduped_models.append(candidate) + scene_manager.models = deduped_models + except Exception as e: + print(f"[SSBO] sync scene_manager.models failed: {e}") + if scene_manager: try: scene_manager.setupCollision(model_np) @@ -1724,10 +1778,8 @@ class AppActions: if set_origin and not getattr(self, "use_ssbo_mouse_picking", False): model_node.setPos(0, 0, 0) - if hasattr(self.scene_manager, 'models'): - if getattr(self, "use_ssbo_mouse_picking", False): - self.scene_manager.models = [model_node] - elif model_node not in self.scene_manager.models: + if hasattr(self.scene_manager, 'models') and not getattr(self, "use_ssbo_mouse_picking", False): + if model_node not in self.scene_manager.models: self.scene_manager.models.append(model_node) if select_model: @@ -1736,7 +1788,43 @@ class AppActions: and getattr(self, "ssbo_editor", None) and getattr(self.ssbo_editor, "last_import_tree_key", None) ): - self.ssbo_editor.select_node(self.ssbo_editor.last_import_tree_key) + auto_select = True + selection_cost = None + estimate_cost_fn = getattr(self.ssbo_editor, "estimate_selection_cost", None) + if callable(estimate_cost_fn): + try: + selection_cost = estimate_cost_fn(self.ssbo_editor.last_import_tree_key) + except Exception: + selection_cost = None + + if selection_cost: + object_count = int(selection_cost.get("object_count", 0) or 0) + chunk_count = int(selection_cost.get("chunk_count", 0) or 0) + auto_select = not ( + object_count > 64 + or chunk_count > 4 + or ( + bool(selection_cost.get("is_top_level_like")) + and (object_count > 16 or chunk_count > 1) + ) + ) + if not auto_select: + print( + "[SSBOImport] Skip auto-selection for heavy model " + f"(objects={object_count}, chunks={chunk_count})" + ) + + if auto_select: + self.ssbo_editor.select_node(self.ssbo_editor.last_import_tree_key) + else: + try: + self.ssbo_editor.clear_selection(sync_world_selection=True) + except Exception: + pass + try: + self.ssbo_editor.force_static_chunk_idle_state() + except Exception: + pass elif hasattr(self, 'selection') and self.selection: self.selection.updateSelection(model_node) diff --git a/ui/panels/editor_panels_left.py b/ui/panels/editor_panels_left.py index 5d756f5c..893f0b99 100644 --- a/ui/panels/editor_panels_left.py +++ b/ui/panels/editor_panels_left.py @@ -73,28 +73,49 @@ class EditorPanelsLeftMixin: def _get_scene_tree_models(self): models = [] - # SSBO模式下场景树应以SSBO聚合根为唯一模型入口,避免混入scene_manager残留节点 - # (如 scene.bam / chunk_* 运行时包装节点)导致树结构异常。 + ssbo_editor = getattr(self.app, "ssbo_editor", None) + scene_manager = getattr(self.app, "scene_manager", None) + + def append_model(candidate): + if not candidate: + return + try: + if candidate.isEmpty(): + return + except Exception: + return + + try: + if ssbo_editor and hasattr(ssbo_editor, "is_source_tree_node") and ssbo_editor.is_source_tree_node(candidate): + return + except Exception: + pass + + if candidate not in models: + models.append(candidate) + + # SSBO模式下不能只显示 ssbo_model。 + # 动画模型目前走普通导入链,若这里硬切成单一 SSBO 根节点, + # 场景中的普通模型会被从场景树里“隐身”。 if getattr(self.app, "use_ssbo_mouse_picking", False): - ssbo_editor = getattr(self.app, "ssbo_editor", None) ssbo_model = getattr(ssbo_editor, "model", None) if ssbo_editor else None source_model_root = getattr(ssbo_editor, "source_model_root", None) if ssbo_editor else None - if ssbo_model and not ssbo_model.isEmpty() and ssbo_model.hasParent(): - return [ssbo_model] - if source_model_root and not source_model_root.isEmpty(): - return [] - scene_manager = getattr(self.app, "scene_manager", None) if scene_manager and hasattr(scene_manager, "models"): - return [m for m in scene_manager.models if m and not m.isEmpty()] - return [] + for candidate in list(scene_manager.models or []): + append_model(candidate) + if ssbo_model and not ssbo_model.isEmpty() and ssbo_model.hasParent(): + append_model(ssbo_model) + elif source_model_root and not source_model_root.isEmpty(): + return models + return models - if hasattr(self.app, "scene_manager") and self.app.scene_manager and hasattr(self.app.scene_manager, "models"): - models.extend([m for m in self.app.scene_manager.models if m and not m.isEmpty()]) + if scene_manager and hasattr(scene_manager, "models"): + for candidate in list(scene_manager.models or []): + append_model(candidate) - ssbo_editor = getattr(self.app, "ssbo_editor", None) ssbo_model = getattr(ssbo_editor, "model", None) if ssbo_editor else None - if ssbo_model and not ssbo_model.isEmpty() and ssbo_model.hasParent() and ssbo_model not in models: - models.append(ssbo_model) + if ssbo_model and not ssbo_model.isEmpty() and ssbo_model.hasParent(): + append_model(ssbo_model) return models def _get_ssbo_top_level_model_keys(self):