模型播放动画与流畅加载大场景并存

This commit is contained in:
Hector 2026-03-25 17:58:14 +08:00
parent 5c0a61d253
commit 5d1113cd8b
11 changed files with 906 additions and 111 deletions

View File

@ -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.

View File

@ -9,7 +9,7 @@ enabled:
- color_correction
- forward_shading
- motion_blur
- pssm
#- pssm
- scattering
- skin_shading
- sky_ao

View File

@ -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
# 添加兼容性属性

View File

@ -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

View File

@ -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)

View File

@ -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 模式下模型已在前面统一导入;若失败已自动回退旧流程。
#处理父子关系 - 在所有节点加载完成后设置正确的父子关系

View File

@ -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")

View File

@ -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"

View File

@ -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)

View File

@ -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)

View File

@ -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):