模型播放动画与流畅加载大场景并存
This commit is contained in:
parent
5c0a61d253
commit
5d1113cd8b
@ -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.
|
||||
|
||||
@ -9,7 +9,7 @@ enabled:
|
||||
- color_correction
|
||||
- forward_shading
|
||||
- motion_blur
|
||||
- pssm
|
||||
#- pssm
|
||||
- scattering
|
||||
- skin_shading
|
||||
- sky_ao
|
||||
|
||||
@ -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
|
||||
# 添加兼容性属性
|
||||
|
||||
44
imgui.ini
44
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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 模式下模型已在前面统一导入;若失败已自动回退旧流程。
|
||||
|
||||
#处理父子关系 - 在所有节点加载完成后设置正确的父子关系
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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"
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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):
|
||||
|
||||
Loading…
Reference in New Issue
Block a user