Fix material targeting and update MetaCore branding
This commit is contained in:
parent
f17073160c
commit
a6986002a7
BIN
Assets/Models/dance.glb
Normal file
BIN
Assets/Models/dance.glb
Normal file
Binary file not shown.
8
Assets/Models/dance.glb.meta
Normal file
8
Assets/Models/dance.glb.meta
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"guid": "ffc57139207e4c6a9677f6c29c90baed",
|
||||
"asset_type": "model",
|
||||
"source_hash": "facc0e3a9d60eda47d2a42e34922cbf09173479704536cde6f1bca4e90f5f431",
|
||||
"importer": "model_importer",
|
||||
"import_settings": {},
|
||||
"dependency_guids": []
|
||||
}
|
||||
BIN
Assets/Models/jxb.glb
Normal file
BIN
Assets/Models/jxb.glb
Normal file
Binary file not shown.
8
Assets/Models/jxb.glb.meta
Normal file
8
Assets/Models/jxb.glb.meta
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"guid": "36dd2eccd8314fb6a0bd8a5090bad6b1",
|
||||
"asset_type": "model",
|
||||
"source_hash": "7916e67bf644e61e2d7b7776ef8df40ef8623aaceda16b873c64d0a3f1a4faba",
|
||||
"importer": "model_importer",
|
||||
"import_settings": {},
|
||||
"dependency_guids": []
|
||||
}
|
||||
8
Assets/Models/jyc.glb.meta
Normal file
8
Assets/Models/jyc.glb.meta
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"guid": "7d4bce696eb848338bdcaffbacdbc4b1",
|
||||
"asset_type": "model",
|
||||
"source_hash": "46fc525a88f6d16eed0bac714a14692b68818d36a00065690b5877d878788948",
|
||||
"importer": "model_importer",
|
||||
"import_settings": {},
|
||||
"dependency_guids": []
|
||||
}
|
||||
BIN
Assets/Models/气体检测报警仪_跑动.fbx
Normal file
BIN
Assets/Models/气体检测报警仪_跑动.fbx
Normal file
Binary file not shown.
8
Assets/Models/气体检测报警仪_跑动.fbx.meta
Normal file
8
Assets/Models/气体检测报警仪_跑动.fbx.meta
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"guid": "98fc2abf779348b6b78d0be52d79993e",
|
||||
"asset_type": "model",
|
||||
"source_hash": "c57c36f52fca892855f8d8f0e70a2ceb8889b159f1c40fbbbcddc7d874682353",
|
||||
"importer": "model_importer",
|
||||
"import_settings": {},
|
||||
"dependency_guids": []
|
||||
}
|
||||
@ -10,7 +10,7 @@
|
||||
"importer": "model_importer",
|
||||
"import_settings": {},
|
||||
"dependency_guids": [],
|
||||
"updated_at": "2026-03-19 17:17:48",
|
||||
"updated_at": "2026-03-23 14:37:54",
|
||||
"imported_cache": {
|
||||
"root": "Library/Imported/90ece77e67b54ccda38d2de71cb4694d",
|
||||
"model_bam": "Library/Imported/90ece77e67b54ccda38d2de71cb4694d/model.bam",
|
||||
@ -18,9 +18,85 @@
|
||||
"materials": "Library/Imported/90ece77e67b54ccda38d2de71cb4694d/materials.json",
|
||||
"import_info": "Library/Imported/90ece77e67b54ccda38d2de71cb4694d/import_info.json"
|
||||
}
|
||||
},
|
||||
"36dd2eccd8314fb6a0bd8a5090bad6b1": {
|
||||
"guid": "36dd2eccd8314fb6a0bd8a5090bad6b1",
|
||||
"asset_path": "Assets/Models/jxb.glb",
|
||||
"asset_type": "model",
|
||||
"meta_path": "Assets/Models/jxb.glb.meta",
|
||||
"source_hash": "7916e67bf644e61e2d7b7776ef8df40ef8623aaceda16b873c64d0a3f1a4faba",
|
||||
"importer": "model_importer",
|
||||
"import_settings": {},
|
||||
"dependency_guids": [],
|
||||
"updated_at": "2026-03-23 14:34:07",
|
||||
"imported_cache": {
|
||||
"root": "Library/Imported/36dd2eccd8314fb6a0bd8a5090bad6b1",
|
||||
"model_bam": "Library/Imported/36dd2eccd8314fb6a0bd8a5090bad6b1/model.bam",
|
||||
"hierarchy": "Library/Imported/36dd2eccd8314fb6a0bd8a5090bad6b1/hierarchy.json",
|
||||
"materials": "Library/Imported/36dd2eccd8314fb6a0bd8a5090bad6b1/materials.json",
|
||||
"import_info": "Library/Imported/36dd2eccd8314fb6a0bd8a5090bad6b1/import_info.json"
|
||||
}
|
||||
},
|
||||
"98fc2abf779348b6b78d0be52d79993e": {
|
||||
"guid": "98fc2abf779348b6b78d0be52d79993e",
|
||||
"asset_path": "Assets/Models/气体检测报警仪_跑动.fbx",
|
||||
"asset_type": "model",
|
||||
"meta_path": "Assets/Models/气体检测报警仪_跑动.fbx.meta",
|
||||
"source_hash": "c57c36f52fca892855f8d8f0e70a2ceb8889b159f1c40fbbbcddc7d874682353",
|
||||
"importer": "model_importer",
|
||||
"import_settings": {},
|
||||
"dependency_guids": [],
|
||||
"updated_at": "2026-03-23 14:34:08",
|
||||
"imported_cache": {
|
||||
"root": "Library/Imported/98fc2abf779348b6b78d0be52d79993e",
|
||||
"model_bam": "Library/Imported/98fc2abf779348b6b78d0be52d79993e/model.bam",
|
||||
"hierarchy": "Library/Imported/98fc2abf779348b6b78d0be52d79993e/hierarchy.json",
|
||||
"materials": "Library/Imported/98fc2abf779348b6b78d0be52d79993e/materials.json",
|
||||
"import_info": "Library/Imported/98fc2abf779348b6b78d0be52d79993e/import_info.json"
|
||||
}
|
||||
},
|
||||
"ffc57139207e4c6a9677f6c29c90baed": {
|
||||
"guid": "ffc57139207e4c6a9677f6c29c90baed",
|
||||
"asset_path": "Assets/Models/dance.glb",
|
||||
"asset_type": "model",
|
||||
"meta_path": "Assets/Models/dance.glb.meta",
|
||||
"source_hash": "facc0e3a9d60eda47d2a42e34922cbf09173479704536cde6f1bca4e90f5f431",
|
||||
"importer": "model_importer",
|
||||
"import_settings": {},
|
||||
"dependency_guids": [],
|
||||
"updated_at": "2026-03-23 14:34:07",
|
||||
"imported_cache": {
|
||||
"root": "Library/Imported/ffc57139207e4c6a9677f6c29c90baed",
|
||||
"model_bam": "Library/Imported/ffc57139207e4c6a9677f6c29c90baed/model.bam",
|
||||
"hierarchy": "Library/Imported/ffc57139207e4c6a9677f6c29c90baed/hierarchy.json",
|
||||
"materials": "Library/Imported/ffc57139207e4c6a9677f6c29c90baed/materials.json",
|
||||
"import_info": "Library/Imported/ffc57139207e4c6a9677f6c29c90baed/import_info.json"
|
||||
}
|
||||
},
|
||||
"7d4bce696eb848338bdcaffbacdbc4b1": {
|
||||
"guid": "7d4bce696eb848338bdcaffbacdbc4b1",
|
||||
"asset_path": "Assets/Models/jyc.glb",
|
||||
"asset_type": "model",
|
||||
"meta_path": "Assets/Models/jyc.glb.meta",
|
||||
"source_hash": "46fc525a88f6d16eed0bac714a14692b68818d36a00065690b5877d878788948",
|
||||
"importer": "model_importer",
|
||||
"import_settings": {},
|
||||
"dependency_guids": [],
|
||||
"updated_at": "2026-03-23 14:35:25",
|
||||
"imported_cache": {
|
||||
"root": "Library/Imported/7d4bce696eb848338bdcaffbacdbc4b1",
|
||||
"model_bam": "Library/Imported/7d4bce696eb848338bdcaffbacdbc4b1/model.bam",
|
||||
"hierarchy": "Library/Imported/7d4bce696eb848338bdcaffbacdbc4b1/hierarchy.json",
|
||||
"materials": "Library/Imported/7d4bce696eb848338bdcaffbacdbc4b1/materials.json",
|
||||
"import_info": "Library/Imported/7d4bce696eb848338bdcaffbacdbc4b1/import_info.json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"path_to_guid": {
|
||||
"Assets/Models/box1.glb": "90ece77e67b54ccda38d2de71cb4694d"
|
||||
"Assets/Models/box1.glb": "90ece77e67b54ccda38d2de71cb4694d",
|
||||
"Assets/Models/jxb.glb": "36dd2eccd8314fb6a0bd8a5090bad6b1",
|
||||
"Assets/Models/气体检测报警仪_跑动.fbx": "98fc2abf779348b6b78d0be52d79993e",
|
||||
"Assets/Models/dance.glb": "ffc57139207e4c6a9677f6c29c90baed",
|
||||
"Assets/Models/jyc.glb": "7d4bce696eb848338bdcaffbacdbc4b1"
|
||||
}
|
||||
}
|
||||
@ -3,5 +3,5 @@
|
||||
"asset_path": "Assets/Models/box1.glb",
|
||||
"asset_type": "model",
|
||||
"source_hash": "fc694905c47a9b0005d77b701cc41852b56ef08c7406829a306e98f3ce158a64",
|
||||
"generated_at": "2026-03-19 17:17:48"
|
||||
"generated_at": "2026-03-23 14:37:54"
|
||||
}
|
||||
BIN
Panda3D-1.10/618a39808b4eb20ba86a9f6cabe5b91e.bam
Normal file
BIN
Panda3D-1.10/618a39808b4eb20ba86a9f6cabe5b91e.bam
Normal file
Binary file not shown.
BIN
Panda3D-1.10/index-aa08c2.boo
Normal file
BIN
Panda3D-1.10/index-aa08c2.boo
Normal file
Binary file not shown.
1
Panda3D-1.10/index_name.txt
Normal file
1
Panda3D-1.10/index_name.txt
Normal file
@ -0,0 +1 @@
|
||||
index-aa08c2.boo
|
||||
@ -51,6 +51,7 @@ class CoreWorld(ShowBase):
|
||||
# 设置基本配置
|
||||
loadPrcFileData("", "show-frame-rate-meter 0")
|
||||
loadPrcFileData("", "window-type onscreen")
|
||||
loadPrcFileData("", "window-title MetaCore")
|
||||
loadPrcFileData("", f"win-size {width} {height}")
|
||||
loadPrcFileData("", "win-fixed-size #f") # 允许窗口调整大小
|
||||
|
||||
@ -81,6 +82,13 @@ class CoreWorld(ShowBase):
|
||||
# 初始化 ShowBase
|
||||
ShowBase.__init__(self)
|
||||
|
||||
try:
|
||||
props = WindowProperties()
|
||||
props.setTitle("MetaCore")
|
||||
self.win.requestProperties(props)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 创建渲染管线
|
||||
self.render_pipeline.create(self)
|
||||
install_editor_tag_state_manager(self.render_pipeline, self)
|
||||
|
||||
34
imgui.ini
34
imgui.ini
@ -31,19 +31,19 @@ DockId=0x0000000D,0
|
||||
|
||||
[Window][场景树]
|
||||
Pos=0,20
|
||||
Size=274,1331
|
||||
Size=339,1084
|
||||
Collapsed=0
|
||||
DockId=0x00000007,0
|
||||
|
||||
[Window][属性面板]
|
||||
Pos=2167,20
|
||||
Size=393,1331
|
||||
Pos=1655,20
|
||||
Size=393,1084
|
||||
Collapsed=0
|
||||
DockId=0x00000002,0
|
||||
|
||||
[Window][控制台]
|
||||
Pos=276,952
|
||||
Size=1889,399
|
||||
Pos=341,705
|
||||
Size=1312,399
|
||||
Collapsed=0
|
||||
DockId=0x00000006,1
|
||||
|
||||
@ -59,7 +59,7 @@ Collapsed=0
|
||||
|
||||
[Window][WindowOverViewport_11111111]
|
||||
Pos=0,20
|
||||
Size=2560,1331
|
||||
Size=2048,1084
|
||||
Collapsed=0
|
||||
|
||||
[Window][测试窗口1]
|
||||
@ -98,8 +98,8 @@ Size=600,500
|
||||
Collapsed=0
|
||||
|
||||
[Window][资源管理器]
|
||||
Pos=276,952
|
||||
Size=1889,399
|
||||
Pos=341,705
|
||||
Size=1312,399
|
||||
Collapsed=0
|
||||
DockId=0x00000006,0
|
||||
|
||||
@ -134,7 +134,7 @@ Size=89,250
|
||||
Collapsed=0
|
||||
|
||||
[Window][颜色选择器]
|
||||
Pos=874,352
|
||||
Pos=878,354
|
||||
Size=300,400
|
||||
Collapsed=0
|
||||
|
||||
@ -214,11 +214,21 @@ Pos=1010,515
|
||||
Size=540,320
|
||||
Collapsed=0
|
||||
|
||||
[Window][关于 EG]
|
||||
Pos=794,422
|
||||
Size=460,260
|
||||
Collapsed=0
|
||||
|
||||
[Window][关于 MetaCore]
|
||||
Pos=794,422
|
||||
Size=460,260
|
||||
Collapsed=0
|
||||
|
||||
[Docking][Data]
|
||||
DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=2560,1331 Split=X
|
||||
DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=2048,1084 Split=X
|
||||
DockNode ID=0x00000001 Parent=0x08BD597D SizeRef=1525,1012 Split=X
|
||||
DockNode ID=0x00000007 Parent=0x00000001 SizeRef=274,1084 Selected=0xE0015051
|
||||
DockNode ID=0x00000008 Parent=0x00000001 SizeRef=1249,1084 Split=Y
|
||||
DockNode ID=0x00000007 Parent=0x00000001 SizeRef=339,1084 Selected=0xE0015051
|
||||
DockNode ID=0x00000008 Parent=0x00000001 SizeRef=1312,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
|
||||
|
||||
@ -1008,7 +1008,7 @@ class ProjectManager:
|
||||
load_lui_fn(temp_stub)
|
||||
except Exception:
|
||||
pass
|
||||
return bool(ssbo_loaded or built_nodes or built_spot_lights or built_point_lights or scene_components)
|
||||
return bool(ssbo_loaded or built_model_nodes or built_spot_lights or built_point_lights or scene_components)
|
||||
|
||||
def _iter_top_level_scene_asset_nodes(self, scene_description):
|
||||
nodes = list(scene_description.get("nodes", []) or [])
|
||||
@ -1089,18 +1089,176 @@ class ProjectManager:
|
||||
target_np.setTag("file", os.path.basename(asset_abs_path))
|
||||
if imported_node_key:
|
||||
target_np.setTag("imported_node_key", imported_node_key)
|
||||
target_np.setTag("is_model_root", "1")
|
||||
target_np.setTag("is_scene_element", "1")
|
||||
if asset_guid:
|
||||
target_np.setTag("is_model_root", "1")
|
||||
target_np.setTag("is_scene_element", "1")
|
||||
|
||||
runtime_interactive = bool(metadata_component.get("runtime_interactive", node.get("runtime_interactive", False)))
|
||||
if runtime_interactive:
|
||||
target_np.setTag("runtime_interactive", "true")
|
||||
|
||||
for tag_name in (
|
||||
"has_animations",
|
||||
"has_animations_checked",
|
||||
"can_create_actor_from_memory",
|
||||
"saved_has_animations",
|
||||
"saved_can_create_actor_from_memory",
|
||||
):
|
||||
tag_value = str(
|
||||
metadata_component.get(tag_name, (node.get("tags", {}) or {}).get(tag_name, ""))
|
||||
or ""
|
||||
).strip()
|
||||
if tag_value:
|
||||
target_np.setTag(tag_name, tag_value)
|
||||
if asset_path:
|
||||
asset_abs_path = os.path.join(project_path, asset_path.replace("/", os.sep))
|
||||
if not target_np.hasTag("saved_model_path"):
|
||||
target_np.setTag("saved_model_path", asset_abs_path)
|
||||
|
||||
scripts = list(scripts_component.get("entries", []) or node.get("scripts", []) or [])
|
||||
if scripts:
|
||||
target_np.setTag("has_scripts", "true")
|
||||
target_np.setTag("scripts_info", json.dumps(scripts, ensure_ascii=False))
|
||||
|
||||
self._apply_saved_material_tags_to_node(target_np)
|
||||
|
||||
def _apply_saved_material_tags_to_node(self, target_np):
|
||||
"""Rebuild runtime material state from serialized material_* tags."""
|
||||
if not target_np or target_np.isEmpty():
|
||||
return
|
||||
|
||||
property_helpers = getattr(self.world, "property_helpers", None)
|
||||
if not property_helpers:
|
||||
return
|
||||
|
||||
material_tag_names = (
|
||||
"material_basecolor",
|
||||
"material_emission",
|
||||
"material_roughness",
|
||||
"material_metallic",
|
||||
"material_ior",
|
||||
)
|
||||
if not any(target_np.hasTag(tag_name) for tag_name in material_tag_names):
|
||||
return
|
||||
|
||||
try:
|
||||
from panda3d.core import Vec4
|
||||
except Exception:
|
||||
Vec4 = None
|
||||
|
||||
def _parse_vec4(raw_value):
|
||||
try:
|
||||
cleaned = str(raw_value or "").strip()
|
||||
for prefix in ("LVecBase4f", "Vec4", "LColor"):
|
||||
cleaned = cleaned.replace(prefix, "")
|
||||
cleaned = cleaned.strip("() ")
|
||||
values = [float(part.strip()) for part in cleaned.split(",") if part.strip()]
|
||||
if len(values) == 3:
|
||||
values.append(1.0)
|
||||
if len(values) >= 4:
|
||||
return tuple(values[:4])
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
def _parse_float(tag_name):
|
||||
try:
|
||||
return float(target_np.getTag(tag_name))
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
ensure_material_fn = getattr(property_helpers, "_ensure_material_for_node", None)
|
||||
sync_runtime_fn = getattr(property_helpers, "_sync_material_node_runtime", None)
|
||||
set_base_color_fn = getattr(property_helpers, "_set_material_base_color", None)
|
||||
if not callable(ensure_material_fn):
|
||||
return
|
||||
|
||||
try:
|
||||
material = ensure_material_fn(target_np)
|
||||
except Exception:
|
||||
material = None
|
||||
if material is None:
|
||||
return
|
||||
|
||||
base_color = _parse_vec4(target_np.getTag("material_basecolor")) if target_np.hasTag("material_basecolor") else None
|
||||
if base_color is not None and callable(set_base_color_fn):
|
||||
try:
|
||||
set_base_color_fn(material, base_color)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
emission = _parse_vec4(target_np.getTag("material_emission")) if target_np.hasTag("material_emission") else None
|
||||
if emission is not None and Vec4 is not None and hasattr(material, "set_emission"):
|
||||
try:
|
||||
material.set_emission(Vec4(*emission))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
roughness = _parse_float("material_roughness")
|
||||
if roughness is not None and hasattr(material, "set_roughness"):
|
||||
try:
|
||||
material.set_roughness(roughness)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
metallic = _parse_float("material_metallic")
|
||||
if metallic is not None and hasattr(material, "set_metallic"):
|
||||
try:
|
||||
material.set_metallic(metallic)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
ior = _parse_float("material_ior")
|
||||
if ior is not None and hasattr(material, "set_refractive_index"):
|
||||
try:
|
||||
material.set_refractive_index(ior)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if callable(sync_runtime_fn):
|
||||
try:
|
||||
sync_runtime_fn(target_np, material, refresh_ssbo_runtime=False)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _apply_scene_description_state_to_subtree(self, target_np, node, project_path, node_lookup):
|
||||
"""Apply saved state to one imported node and its serialized descendants by child order."""
|
||||
if not target_np or target_np.isEmpty() or not isinstance(node, dict):
|
||||
return
|
||||
|
||||
self._apply_scene_description_state_to_node(target_np, node, project_path)
|
||||
|
||||
node_id = str(node.get("node_id", "") or "").strip()
|
||||
if not node_id:
|
||||
return
|
||||
|
||||
child_nodes = []
|
||||
for candidate in list((node_lookup or {}).values()):
|
||||
if str(candidate.get("parent_id", "") or "").strip() != node_id:
|
||||
continue
|
||||
child_nodes.append(candidate)
|
||||
|
||||
def _node_index(candidate):
|
||||
candidate_id = str(candidate.get("node_id", "") or "")
|
||||
try:
|
||||
return int(candidate_id.rsplit("/", 1)[-1])
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
child_nodes.sort(key=_node_index)
|
||||
|
||||
runtime_children = []
|
||||
try:
|
||||
runtime_children = [child for child in target_np.getChildren() if child and not child.isEmpty()]
|
||||
except Exception:
|
||||
runtime_children = []
|
||||
|
||||
for child_entry in child_nodes:
|
||||
child_index = _node_index(child_entry)
|
||||
if child_index < 0 or child_index >= len(runtime_children):
|
||||
continue
|
||||
self._apply_scene_description_state_to_subtree(runtime_children[child_index], child_entry, project_path, node_lookup)
|
||||
|
||||
def _load_scene_description_via_ssbo(self, scene_description, project_path, asset_database):
|
||||
if not getattr(self.world, "use_ssbo_mouse_picking", False):
|
||||
return False
|
||||
@ -1109,6 +1267,8 @@ class ProjectManager:
|
||||
if not ssbo_editor:
|
||||
return False
|
||||
|
||||
refresh_runtime_fn = getattr(ssbo_editor, "refresh_runtime_from_source", None)
|
||||
|
||||
top_level_asset_nodes, node_lookup = self._iter_top_level_scene_asset_nodes(scene_description)
|
||||
if not top_level_asset_nodes:
|
||||
return False
|
||||
@ -1133,6 +1293,33 @@ class ProjectManager:
|
||||
if not os.path.exists(asset_abs):
|
||||
continue
|
||||
|
||||
has_saved_animation = False
|
||||
try:
|
||||
metadata_component = dict(components.get("metadata", {}) or {})
|
||||
tags = dict(node.get("tags", {}) or {})
|
||||
has_saved_animation = (
|
||||
str(tags.get("has_animations", "")).lower() == "true"
|
||||
or str(tags.get("saved_has_animations", "")).lower() == "true"
|
||||
or str(metadata_component.get("has_animations", "")).lower() == "true"
|
||||
or str(metadata_component.get("saved_has_animations", "")).lower() == "true"
|
||||
or str(metadata_component.get("can_create_actor_from_memory", "")).lower() == "true"
|
||||
or str(metadata_component.get("saved_can_create_actor_from_memory", "")).lower() == "true"
|
||||
)
|
||||
except Exception:
|
||||
has_saved_animation = False
|
||||
|
||||
if has_saved_animation:
|
||||
scene_manager = getattr(self.world, "scene_manager", None)
|
||||
if scene_manager and hasattr(scene_manager, "importModel"):
|
||||
try:
|
||||
imported_np = scene_manager.importModel(asset_abs)
|
||||
except Exception:
|
||||
imported_np = None
|
||||
if imported_np and not imported_np.isEmpty():
|
||||
self._apply_scene_description_state_to_node(imported_np, node, project_path)
|
||||
loaded_any = True
|
||||
continue
|
||||
|
||||
try:
|
||||
imported_root = ssbo_editor.load_model(
|
||||
asset_abs,
|
||||
@ -1148,7 +1335,15 @@ class ProjectManager:
|
||||
if target_root is None:
|
||||
continue
|
||||
|
||||
self._apply_scene_description_state_to_node(target_root, node, project_path)
|
||||
self._apply_scene_description_state_to_subtree(target_root, node, project_path, node_lookup)
|
||||
# Keep runtime aligned with source after restoring per-node transforms.
|
||||
# Otherwise the next append=True import snapshots stale runtime poses
|
||||
# back into source_model_root and overwrites the restored transform.
|
||||
if callable(refresh_runtime_fn):
|
||||
try:
|
||||
refresh_runtime_fn(preserve_selection=False)
|
||||
except Exception:
|
||||
pass
|
||||
loaded_any = True
|
||||
|
||||
if not loaded_any:
|
||||
@ -1494,6 +1689,14 @@ class ProjectManager:
|
||||
print("错误: 请先创建或打开一个项目!")
|
||||
return False
|
||||
|
||||
ssbo_editor = getattr(self.world, "ssbo_editor", None)
|
||||
snapshot_material_fn = getattr(ssbo_editor, "_snapshot_runtime_materials_to_source_root", None) if ssbo_editor else None
|
||||
if callable(snapshot_material_fn):
|
||||
try:
|
||||
snapshot_material_fn()
|
||||
except Exception as e:
|
||||
print(f"⚠️ 保存前同步 SSBO 材质到源场景失败: {e}")
|
||||
|
||||
project_path = self.current_project_path
|
||||
ensure_project_directories(ProjectLayout(project_path))
|
||||
project_config = self._ensure_v2_project_defaults(project_path, dict(self.project_config or {}))
|
||||
@ -2213,8 +2416,11 @@ class ProjectManager:
|
||||
for model in list(getattr(scene_manager, "models", []) or []):
|
||||
try:
|
||||
if tree_widget:
|
||||
tree_widget.delete_item(model)
|
||||
elif model and not model.isEmpty():
|
||||
try:
|
||||
tree_widget.delete_item(model)
|
||||
except Exception:
|
||||
pass
|
||||
if model and not model.isEmpty():
|
||||
model.removeNode()
|
||||
except Exception:
|
||||
pass
|
||||
@ -2225,8 +2431,11 @@ class ProjectManager:
|
||||
for light_node in collection:
|
||||
try:
|
||||
if tree_widget:
|
||||
tree_widget.delete_item(light_node)
|
||||
elif light_node and not light_node.isEmpty():
|
||||
try:
|
||||
tree_widget.delete_item(light_node)
|
||||
except Exception:
|
||||
pass
|
||||
if light_node and not light_node.isEmpty():
|
||||
light_node.removeNode()
|
||||
except Exception:
|
||||
pass
|
||||
@ -2237,8 +2446,11 @@ class ProjectManager:
|
||||
for terrain in terrains:
|
||||
try:
|
||||
if tree_widget:
|
||||
tree_widget.delete_item(terrain)
|
||||
elif terrain and not terrain.isEmpty():
|
||||
try:
|
||||
tree_widget.delete_item(terrain)
|
||||
except Exception:
|
||||
pass
|
||||
if terrain and not terrain.isEmpty():
|
||||
terrain.removeNode()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@ -174,10 +174,20 @@ def _collect_script_component(scripts: list[dict]) -> dict:
|
||||
|
||||
|
||||
def _collect_node_metadata_component(node, runtime_interactive: bool) -> dict:
|
||||
return {
|
||||
metadata = {
|
||||
"node_class": node.node().getClassType().getName() if node.node() else "",
|
||||
"runtime_interactive": runtime_interactive,
|
||||
}
|
||||
for tag_name in (
|
||||
"has_animations",
|
||||
"has_animations_checked",
|
||||
"can_create_actor_from_memory",
|
||||
"saved_has_animations",
|
||||
"saved_can_create_actor_from_memory",
|
||||
):
|
||||
if node.hasTag(tag_name):
|
||||
metadata[tag_name] = node.getTag(tag_name)
|
||||
return metadata
|
||||
|
||||
|
||||
def _collect_material_override_component(node):
|
||||
@ -232,6 +242,10 @@ def _resolve_imported_node_key(node, fallback_key: str) -> str:
|
||||
def _should_serialize_child_nodes(node, asset_guid: str, scripts: list[dict], runtime_interactive: bool, light_component: dict, material_component: dict) -> bool:
|
||||
if not node:
|
||||
return False
|
||||
# Imported model roots and their descendants must keep hierarchy state so
|
||||
# per-child transforms/material overrides survive save/load.
|
||||
if asset_guid:
|
||||
return True
|
||||
if not asset_guid:
|
||||
return True
|
||||
if scripts:
|
||||
@ -447,6 +461,37 @@ def build_runtime_scene(scene_description: dict):
|
||||
for node in nodes:
|
||||
if not node.get("asset_guid"):
|
||||
continue
|
||||
metadata_component = dict((node.get("components", {}) or {}).get("metadata", {}) or {})
|
||||
has_animations = str(
|
||||
metadata_component.get(
|
||||
"has_animations",
|
||||
metadata_component.get(
|
||||
"saved_has_animations",
|
||||
(node.get("tags", {}) or {}).get(
|
||||
"has_animations",
|
||||
(node.get("tags", {}) or {}).get("saved_has_animations", ""),
|
||||
),
|
||||
),
|
||||
)
|
||||
or ""
|
||||
).lower() == "true"
|
||||
can_create_actor = str(
|
||||
metadata_component.get(
|
||||
"can_create_actor_from_memory",
|
||||
metadata_component.get(
|
||||
"saved_can_create_actor_from_memory",
|
||||
(node.get("tags", {}) or {}).get(
|
||||
"can_create_actor_from_memory",
|
||||
(node.get("tags", {}) or {}).get("saved_can_create_actor_from_memory", ""),
|
||||
),
|
||||
),
|
||||
)
|
||||
or ""
|
||||
).lower() == "true"
|
||||
if has_animations or can_create_actor:
|
||||
interactive_node_ids.append(node.get("node_id", ""))
|
||||
interactive_model_names.append(node.get("name", ""))
|
||||
continue
|
||||
if node.get("runtime_interactive"):
|
||||
interactive_node_ids.append(node.get("node_id", ""))
|
||||
interactive_model_names.append(node.get("name", ""))
|
||||
|
||||
@ -242,6 +242,20 @@ class ObjectController:
|
||||
|
||||
return False
|
||||
|
||||
def get_preferred_selection_ids(self, key):
|
||||
"""Return the IDs that should be selected when a tree node is clicked.
|
||||
|
||||
Prefer the node's own local geometry so parent transforms remain directly
|
||||
selectable. Only fall back to the aggregated subtree when the node itself
|
||||
has no local renderable geometry.
|
||||
"""
|
||||
node = self.tree_nodes.get(key)
|
||||
if node:
|
||||
local_ids = list(node.get("local_ids", []) or [])
|
||||
if local_ids:
|
||||
return local_ids
|
||||
return list(self.name_to_ids.get(key, []) or [])
|
||||
|
||||
def _encode_id_color(self, vdata, object_id):
|
||||
if not vdata.has_column("color"):
|
||||
new_fmt = vdata.get_format().get_union_format(GeomVertexFormat.get_v3c4())
|
||||
|
||||
@ -79,6 +79,7 @@ class SSBOEditor:
|
||||
# Internal State
|
||||
self.selected_name = None
|
||||
self.selected_ids = []
|
||||
self.transform_ids = []
|
||||
self.search_text = ""
|
||||
self.last_search_text = None
|
||||
self.filtered_nodes = []
|
||||
@ -666,22 +667,37 @@ class SSBOEditor:
|
||||
if not self._node_is_valid(source_node):
|
||||
continue
|
||||
|
||||
runtime_snapshot = None
|
||||
if callable(capture_snapshot_fn):
|
||||
try:
|
||||
runtime_snapshot = capture_snapshot_fn(obj_np)
|
||||
except Exception:
|
||||
runtime_snapshot = None
|
||||
|
||||
source_snapshot = None
|
||||
if callable(capture_snapshot_fn):
|
||||
try:
|
||||
source_snapshot = capture_snapshot_fn(obj_np)
|
||||
source_snapshot = capture_snapshot_fn(source_node)
|
||||
except Exception:
|
||||
source_snapshot = None
|
||||
|
||||
source_node_key = id(source_node)
|
||||
entry = grouped_entries.get(source_node_key)
|
||||
candidate_score = _snapshot_score(source_snapshot, obj_np)
|
||||
runtime_score = _snapshot_score(runtime_snapshot, obj_np)
|
||||
source_score = _snapshot_score(source_snapshot, source_node)
|
||||
# Saving should prefer the visible runtime state on ties; otherwise a
|
||||
# stale source snapshot can overwrite a freshly edited child material.
|
||||
if runtime_snapshot is not None and runtime_score >= source_score:
|
||||
chosen_snapshot = runtime_snapshot
|
||||
else:
|
||||
chosen_snapshot = source_snapshot
|
||||
candidate_score = max(runtime_score, source_score)
|
||||
if entry is None or candidate_score > entry["score"]:
|
||||
grouped_entries[source_node_key] = {
|
||||
"gid": gid,
|
||||
"obj_np": obj_np,
|
||||
"source_node": source_node,
|
||||
"snapshot": source_snapshot,
|
||||
"snapshot": chosen_snapshot,
|
||||
"score": candidate_score,
|
||||
}
|
||||
|
||||
@ -870,6 +886,7 @@ class SSBOEditor:
|
||||
self.model = None
|
||||
self.selected_name = None
|
||||
self.selected_ids = []
|
||||
self.transform_ids = []
|
||||
self.last_import_tree_key = None
|
||||
self.last_import_root_name = None
|
||||
if not preserve_source_models:
|
||||
@ -1957,13 +1974,14 @@ class SSBOEditor:
|
||||
if not self.controller:
|
||||
return
|
||||
self._sync_pick_root_transform()
|
||||
if not self.selected_ids:
|
||||
transform_ids = self.get_selection_transform_ids()
|
||||
if not transform_ids:
|
||||
return
|
||||
|
||||
# Group selection can contain thousands of objects.
|
||||
# Only resync when proxy transform has changed.
|
||||
proxy = getattr(self, "_group_proxy", None)
|
||||
if proxy and not proxy.is_empty() and len(self.selected_ids) > 1:
|
||||
if proxy and not proxy.is_empty() and len(transform_ids) > 1:
|
||||
proxy_world = proxy.get_mat(self.base.render)
|
||||
if self._last_group_sync_mat and self._matrices_close(proxy_world, self._last_group_sync_mat):
|
||||
return
|
||||
@ -1971,8 +1989,8 @@ class SSBOEditor:
|
||||
else:
|
||||
self._last_group_sync_mat = None
|
||||
|
||||
if len(self.selected_ids) == 1:
|
||||
gid = self.selected_ids[0]
|
||||
if len(transform_ids) == 1:
|
||||
gid = transform_ids[0]
|
||||
obj_np = self.controller.id_to_object_np.get(gid)
|
||||
pick_np = self.controller.id_to_pick_np.get(gid)
|
||||
if not (obj_np and pick_np and not obj_np.is_empty() and not pick_np.is_empty()):
|
||||
@ -1996,7 +2014,7 @@ class SSBOEditor:
|
||||
|
||||
self._last_single_sync_gid = None
|
||||
self._last_single_sync_mat = None
|
||||
for gid in self.selected_ids:
|
||||
for gid in transform_ids:
|
||||
obj_np = self.controller.id_to_object_np.get(gid)
|
||||
pick_np = self.controller.id_to_pick_np.get(gid)
|
||||
if obj_np and pick_np and not obj_np.is_empty() and not pick_np.is_empty():
|
||||
@ -2112,7 +2130,8 @@ class SSBOEditor:
|
||||
def _update_outline_for_selection(self):
|
||||
if not self._outline_manager:
|
||||
return
|
||||
if not self.controller or not self.selected_ids:
|
||||
transform_ids = self.get_selection_transform_ids()
|
||||
if not self.controller or not transform_ids:
|
||||
self._outline_manager.clear()
|
||||
return
|
||||
|
||||
@ -2126,7 +2145,7 @@ class SSBOEditor:
|
||||
|
||||
targets = []
|
||||
target_limit = max(1, int(getattr(self._outline_manager, "max_targets", 64)))
|
||||
for gid in self.selected_ids:
|
||||
for gid in transform_ids:
|
||||
obj_np = self.controller.id_to_object_np.get(gid)
|
||||
if obj_np and not obj_np.is_empty():
|
||||
targets.append(obj_np)
|
||||
@ -2149,6 +2168,11 @@ class SSBOEditor:
|
||||
def has_active_selection(self):
|
||||
return bool(self.controller and self.selected_name is not None)
|
||||
|
||||
def get_selection_transform_ids(self):
|
||||
if not self.controller or self.selected_name is None:
|
||||
return []
|
||||
return list(getattr(self, "transform_ids", []) or [])
|
||||
|
||||
def _is_root_selection(self):
|
||||
return bool(
|
||||
self.controller and
|
||||
@ -2161,6 +2185,7 @@ class SSBOEditor:
|
||||
return None
|
||||
|
||||
has_dynamic_objects = bool(getattr(self.controller, "id_to_object_np", {}) or {})
|
||||
transform_ids = self.get_selection_transform_ids()
|
||||
|
||||
if self._is_root_selection():
|
||||
return self.model if self._node_is_valid(self.model) else None
|
||||
@ -2168,7 +2193,7 @@ class SSBOEditor:
|
||||
if not has_dynamic_objects:
|
||||
return self.model if self._node_is_valid(self.model) else None
|
||||
|
||||
if len(self.selected_ids) > 1:
|
||||
if len(transform_ids) > 1:
|
||||
proxy = getattr(self, "_group_proxy", None)
|
||||
if proxy and self._node_is_valid(proxy):
|
||||
# Apply the original first node's material/tags to proxy transparently?
|
||||
@ -2182,15 +2207,36 @@ class SSBOEditor:
|
||||
|
||||
return None
|
||||
|
||||
def get_selection_runtime_material_node(self):
|
||||
"""Return the runtime node that material editing should target."""
|
||||
if not self.controller or not self.selected_ids:
|
||||
return None
|
||||
obj_np = self.controller.id_to_object_np.get(self.selected_ids[0])
|
||||
if self._node_is_valid(obj_np):
|
||||
return obj_np
|
||||
return None
|
||||
|
||||
def get_selection_source_material_node(self):
|
||||
"""Return the exact source-tree node for the currently edited material target."""
|
||||
if not self.controller or not self.selected_ids:
|
||||
return None
|
||||
owner_key = self.controller.id_to_name.get(self.selected_ids[0])
|
||||
if not owner_key:
|
||||
return None
|
||||
source_node = self._resolve_source_node_by_tree_key(owner_key)
|
||||
if self._node_is_valid(source_node):
|
||||
return source_node
|
||||
return None
|
||||
|
||||
def get_selection_summary(self):
|
||||
if not self.controller or self.selected_name is None:
|
||||
return None
|
||||
return {
|
||||
"key": self.selected_name,
|
||||
"display_name": self.controller.display_names.get(self.selected_name, self.selected_name),
|
||||
"object_count": len(self.selected_ids),
|
||||
"object_count": len(self.get_selection_transform_ids()),
|
||||
"is_root": self._is_root_selection(),
|
||||
"is_group": len(self.selected_ids) > 1 and not self._is_root_selection(),
|
||||
"is_group": len(self.get_selection_transform_ids()) > 1 and not self._is_root_selection(),
|
||||
}
|
||||
|
||||
def get_selection_key(self):
|
||||
@ -2298,6 +2344,7 @@ class SSBOEditor:
|
||||
self._cleanup_group_proxy()
|
||||
self.selected_name = None
|
||||
self.selected_ids = []
|
||||
self.transform_ids = []
|
||||
if self._outline_manager:
|
||||
self._outline_manager.clear()
|
||||
if self.controller:
|
||||
@ -2355,7 +2402,12 @@ class SSBOEditor:
|
||||
self._reset_pick_sync_cache()
|
||||
|
||||
self.selected_name = key
|
||||
self.selected_ids = self.controller.name_to_ids.get(key, [])
|
||||
preferred_ids_fn = getattr(self.controller, "get_preferred_selection_ids", None)
|
||||
if callable(preferred_ids_fn):
|
||||
self.selected_ids = preferred_ids_fn(key)
|
||||
else:
|
||||
self.selected_ids = self.controller.name_to_ids.get(key, [])
|
||||
self.transform_ids = list(self.controller.name_to_ids.get(key, []) or [])
|
||||
has_dynamic_objects = bool(getattr(self.controller, "id_to_object_np", {}) or {})
|
||||
is_root_selection = (
|
||||
self.controller and
|
||||
@ -2367,7 +2419,7 @@ class SSBOEditor:
|
||||
root_key = getattr(self.controller, "tree_root_key", None)
|
||||
root_node = self.controller.tree_nodes.get(root_key, {}) if root_key else {}
|
||||
top_level_children = set(root_node.get("children", []) or [])
|
||||
is_top_level_heavy_selection = key in top_level_children and len(self.selected_ids) >= 256
|
||||
is_top_level_heavy_selection = key in top_level_children and len(self.transform_ids) >= 256
|
||||
except Exception:
|
||||
is_top_level_heavy_selection = False
|
||||
if sync_world_selection:
|
||||
@ -2388,17 +2440,17 @@ class SSBOEditor:
|
||||
self._stop_pick_sync_task()
|
||||
return
|
||||
|
||||
self.controller.set_active_ids(self.selected_ids)
|
||||
self.controller.set_active_ids(self.transform_ids)
|
||||
self._update_outline_for_selection()
|
||||
|
||||
if not self._transform_gizmo or not self.selected_ids:
|
||||
if not self._transform_gizmo or not self.transform_ids:
|
||||
if self._transform_gizmo:
|
||||
self._transform_gizmo.detach()
|
||||
return
|
||||
|
||||
if len(self.selected_ids) == 1:
|
||||
if len(self.transform_ids) == 1:
|
||||
# Single object: attach gizmo directly
|
||||
obj_np = self.controller.id_to_object_np.get(self.selected_ids[0])
|
||||
obj_np = self.controller.id_to_object_np.get(self.transform_ids[0])
|
||||
if obj_np and not obj_np.is_empty():
|
||||
self._transform_gizmo.attach(obj_np)
|
||||
self._start_pick_sync_task()
|
||||
@ -2416,7 +2468,7 @@ class SSBOEditor:
|
||||
pass
|
||||
center = Vec3(0, 0, 0)
|
||||
valid = []
|
||||
for gid in self.selected_ids:
|
||||
for gid in self.transform_ids:
|
||||
obj_np = self.controller.id_to_object_np.get(gid)
|
||||
if obj_np and not obj_np.is_empty():
|
||||
center += obj_np.get_pos(self.base.render)
|
||||
@ -2494,8 +2546,9 @@ class SSBOEditor:
|
||||
self.filtered_nodes = rows
|
||||
|
||||
def focus_on_selected(self):
|
||||
if self.selected_name and self.selected_ids:
|
||||
first_id = self.selected_ids[0]
|
||||
transform_ids = self.get_selection_transform_ids()
|
||||
if self.selected_name and transform_ids:
|
||||
first_id = transform_ids[0]
|
||||
pos = self.controller.get_world_pos(first_id)
|
||||
dist = 100
|
||||
self.base.camera.set_pos(pos.x, pos.y - dist, pos.z + dist * 0.5)
|
||||
@ -2552,7 +2605,8 @@ class SSBOEditor:
|
||||
|
||||
if io.want_capture_keyboard: return task.cont
|
||||
|
||||
if self.selected_ids and self.controller:
|
||||
transform_ids = self.get_selection_transform_ids()
|
||||
if transform_ids and self.controller:
|
||||
speed = 50 * dt
|
||||
acc = Vec3(0, 0, 0)
|
||||
if self.keys.get('arrow_up'): acc.z += speed
|
||||
@ -2574,7 +2628,7 @@ class SSBOEditor:
|
||||
self.model.set_pos(next_pos)
|
||||
self._sync_pick_root_transform()
|
||||
else:
|
||||
for idx in self.selected_ids:
|
||||
for idx in transform_ids:
|
||||
self.controller.move_object(idx, acc)
|
||||
|
||||
return task.cont
|
||||
|
||||
@ -355,7 +355,7 @@ class MainApp(ShowBase):
|
||||
imported_node_key = str(model_component.get("imported_node_key", "") or node.get("imported_node_key", "") or "")
|
||||
|
||||
if asset_guid:
|
||||
loaded_np = self._load_runtime_asset_node(asset_guid, imported_node_key, node_name)
|
||||
loaded_np = self._load_runtime_asset_node(asset_guid, imported_node_key, node_name, node)
|
||||
rebuilt_np = loaded_np if loaded_np and not loaded_np.isEmpty() else parent_np.attachNewNode(node_name)
|
||||
if rebuilt_np.getParent() != parent_np:
|
||||
rebuilt_np.reparentTo(parent_np)
|
||||
@ -394,27 +394,55 @@ class MainApp(ShowBase):
|
||||
|
||||
return rebuilt_np
|
||||
|
||||
def _load_runtime_asset_node(self, asset_guid, imported_node_key="", node_name=""):
|
||||
def _load_runtime_asset_node(self, asset_guid, imported_node_key="", node_name="", node_data=None):
|
||||
asset_record = dict(self._asset_index.get(str(asset_guid or ""), {}) or {})
|
||||
if not asset_record:
|
||||
return None
|
||||
|
||||
candidate_paths = []
|
||||
imported_cache = dict(asset_record.get("imported_cache", {}) or {})
|
||||
imported_model_rel = str(imported_cache.get("model_bam", "") or "").replace("\\", "/").strip()
|
||||
if imported_model_rel:
|
||||
candidate_paths.append(os.path.join(DATA_ROOT, imported_model_rel.replace("/", os.sep)))
|
||||
node_data = dict(node_data or {})
|
||||
components = dict(node_data.get("components", {}) or {})
|
||||
metadata_component = dict(components.get("metadata", {}) or {})
|
||||
node_tags = dict(node_data.get("tags", {}) or {})
|
||||
has_animations = str(
|
||||
metadata_component.get(
|
||||
"has_animations",
|
||||
metadata_component.get(
|
||||
"saved_has_animations",
|
||||
node_tags.get("has_animations", node_tags.get("saved_has_animations", "")),
|
||||
),
|
||||
)
|
||||
or ""
|
||||
).lower() == "true"
|
||||
can_create_actor = str(
|
||||
metadata_component.get(
|
||||
"can_create_actor_from_memory",
|
||||
metadata_component.get(
|
||||
"saved_can_create_actor_from_memory",
|
||||
node_tags.get(
|
||||
"can_create_actor_from_memory",
|
||||
node_tags.get("saved_can_create_actor_from_memory", ""),
|
||||
),
|
||||
),
|
||||
)
|
||||
or ""
|
||||
).lower() == "true"
|
||||
|
||||
asset_dir = os.path.join(DATA_ROOT, "assets", asset_guid)
|
||||
imported_model_path = os.path.join(asset_dir, "imported", "model.bam")
|
||||
if os.path.exists(imported_model_path):
|
||||
candidate_paths.append(imported_model_path)
|
||||
|
||||
asset_path = str(asset_record.get("asset_path", "") or "").replace("\\", "/").strip()
|
||||
if asset_path:
|
||||
candidate_paths.append(os.path.join(DATA_ROOT, asset_path.replace("/", os.sep)))
|
||||
candidate_paths.append(os.path.join(asset_dir, os.path.basename(asset_path)))
|
||||
|
||||
imported_cache = dict(asset_record.get("imported_cache", {}) or {})
|
||||
imported_model_rel = str(imported_cache.get("model_bam", "") or "").replace("\\", "/").strip()
|
||||
if imported_model_rel and not (has_animations or can_create_actor):
|
||||
candidate_paths.append(os.path.join(DATA_ROOT, imported_model_rel.replace("/", os.sep)))
|
||||
|
||||
imported_model_path = os.path.join(asset_dir, "imported", "model.bam")
|
||||
if os.path.exists(imported_model_path) and not (has_animations or can_create_actor):
|
||||
candidate_paths.append(imported_model_path)
|
||||
|
||||
for candidate_path in candidate_paths:
|
||||
if not candidate_path or not os.path.exists(candidate_path):
|
||||
continue
|
||||
|
||||
@ -948,7 +948,7 @@ class AnimationTools:
|
||||
except Exception:
|
||||
has_animation_nodes = False
|
||||
|
||||
def _try_memory_fallback():
|
||||
def _try_memory_fallback(prefer_scene_proxy=False):
|
||||
def _collect_autobind_source_candidates():
|
||||
candidates = []
|
||||
|
||||
@ -1032,6 +1032,22 @@ class AnimationTools:
|
||||
can_create_from_memory = True
|
||||
|
||||
if can_create_from_memory:
|
||||
autobind_candidates = _collect_autobind_source_candidates()
|
||||
|
||||
if prefer_scene_proxy:
|
||||
for source_node in autobind_candidates:
|
||||
mem_proxy = _try_create_autobind_proxy(
|
||||
source_node,
|
||||
f"内存模型({source_node.getName()})",
|
||||
owns_node=False
|
||||
)
|
||||
if mem_proxy:
|
||||
self._actor_cache[owner_model] = mem_proxy
|
||||
owner_model.setTag("has_animations", "true")
|
||||
if source_node != owner_model:
|
||||
print(f"[Actor加载] 使用可见节点作为动画源: {owner_model.getName()} -> {source_node.getName()}")
|
||||
return mem_proxy
|
||||
|
||||
# 不能直接 Actor(owner_model);会污染当前场景节点,导致播放后模型消失/选择失效。
|
||||
# 先用副本创建真实 Actor,只有失败时才退回 autoBind 代理。
|
||||
clone_parent = self._get_owner_parent_node(owner_model)
|
||||
@ -1055,7 +1071,7 @@ class AnimationTools:
|
||||
pass
|
||||
|
||||
# Actor 副本失败后,从多个候选节点中选择“带几何体”的 autoBind 源
|
||||
for source_node in _collect_autobind_source_candidates():
|
||||
for source_node in autobind_candidates:
|
||||
mem_proxy = _try_create_autobind_proxy(
|
||||
source_node,
|
||||
f"内存模型({source_node.getName()})",
|
||||
@ -1084,9 +1100,25 @@ class AnimationTools:
|
||||
filepath = owner_model.getTag("original_path")
|
||||
|
||||
print(f"[Actor加载调试] 获取到的 filepath: '{filepath}'")
|
||||
model_ext = str(filepath).lower() if filepath else ""
|
||||
try:
|
||||
character_count = owner_model.findAllMatches("**/+Character").getNumPaths()
|
||||
except Exception:
|
||||
character_count = 0
|
||||
prefer_scene_proxy = (
|
||||
(model_ext.endswith(".glb") or model_ext.endswith(".gltf")) and
|
||||
character_count > 1
|
||||
)
|
||||
|
||||
if not filepath:
|
||||
print(f"[Actor加载调试] filepath为空,触发 _try_memory_fallback()")
|
||||
return _try_memory_fallback()
|
||||
return _try_memory_fallback(prefer_scene_proxy=prefer_scene_proxy)
|
||||
|
||||
if prefer_scene_proxy:
|
||||
memory_actor = _try_memory_fallback(prefer_scene_proxy=True)
|
||||
if memory_actor:
|
||||
print(f"[Actor加载] {owner_model.getName()} 使用场景内 autoBind 代理优先路径")
|
||||
return memory_actor
|
||||
|
||||
# 针对Actor加载,必须使用Panda3D规范的Unix风格路径,否则Windows绝对路径会导致加载彻底崩溃并返回空节点
|
||||
panda_specific_path = ""
|
||||
@ -1172,7 +1204,7 @@ class AnimationTools:
|
||||
pass
|
||||
|
||||
# 所有创建路径失败时由内存加载进行兜底
|
||||
return _try_memory_fallback()
|
||||
return _try_memory_fallback(prefer_scene_proxy=prefer_scene_proxy)
|
||||
|
||||
|
||||
def _getModelFormat(self, origin_model):
|
||||
|
||||
@ -41,6 +41,37 @@ class AppActions:
|
||||
|
||||
return node
|
||||
|
||||
def _model_file_has_animation(self, file_path):
|
||||
"""Detect whether a model file contains animation-related structures."""
|
||||
try:
|
||||
if not file_path or not os.path.exists(file_path):
|
||||
return False
|
||||
loader = getattr(self, "loader", None)
|
||||
if not loader:
|
||||
return False
|
||||
normalized_path = file_path
|
||||
try:
|
||||
from scene import util as scene_util
|
||||
normalized_path = scene_util.normalize_model_path(file_path)
|
||||
except Exception:
|
||||
normalized_path = file_path
|
||||
model = loader.loadModel(normalized_path)
|
||||
if not model or model.isEmpty():
|
||||
return False
|
||||
try:
|
||||
has_animation = (
|
||||
model.findAllMatches("**/+Character").getNumPaths() > 0
|
||||
or model.findAllMatches("**/+AnimBundleNode").getNumPaths() > 0
|
||||
)
|
||||
finally:
|
||||
try:
|
||||
model.removeNode()
|
||||
except Exception:
|
||||
pass
|
||||
return bool(has_animation)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _toggle_hot_reload(self):
|
||||
"""切换热重载状态"""
|
||||
@ -1485,11 +1516,13 @@ class AppActions:
|
||||
except Exception as e:
|
||||
print(f"项目资源导入失败,回退直接导入: {e}")
|
||||
|
||||
animated_model = bool(file_path and self._model_file_has_animation(file_path))
|
||||
|
||||
command_manager = getattr(self, 'command_manager', None)
|
||||
if scene_package_import or not command_manager:
|
||||
return self._import_model_for_runtime(file_path, scene_package_import=scene_package_import)
|
||||
|
||||
if self.use_ssbo_mouse_picking and getattr(self, 'ssbo_editor', None):
|
||||
if self.use_ssbo_mouse_picking and getattr(self, 'ssbo_editor', None) and not animated_model:
|
||||
return self._execute_ssbo_import_command(file_path)
|
||||
|
||||
from core.Command_System import CreateNodeCommand
|
||||
@ -1507,31 +1540,44 @@ class AppActions:
|
||||
SSBO mode: load via SSBOEditor always (regardless of prefer_scene_manager).
|
||||
Legacy mode: load via SceneManager.
|
||||
"""
|
||||
if self.use_ssbo_mouse_picking and getattr(self, 'ssbo_editor', None):
|
||||
try:
|
||||
# Clear selection/gizmo first to avoid dangling references to soon-to-be removed nodes.
|
||||
if hasattr(self, 'selection') and self.selection:
|
||||
try:
|
||||
self.selection.clearSelection()
|
||||
except Exception:
|
||||
try:
|
||||
self.selection.updateSelection(None)
|
||||
except Exception:
|
||||
pass
|
||||
animated_model = bool(file_path and self._model_file_has_animation(file_path))
|
||||
if animated_model:
|
||||
prefer_scene_manager = True
|
||||
ssbo_editor = getattr(self, 'ssbo_editor', None)
|
||||
if ssbo_editor and hasattr(ssbo_editor, "clear_selection"):
|
||||
try:
|
||||
ssbo_editor.clear_selection(sync_world_selection=False)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.ssbo_editor.load_model(
|
||||
file_path,
|
||||
keep_source_model=scene_package_import,
|
||||
append=not scene_package_import,
|
||||
scene_package_import=scene_package_import,
|
||||
)
|
||||
return self._refresh_ssbo_runtime_import_bindings(
|
||||
file_path=file_path,
|
||||
scene_package_import=scene_package_import,
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"[SSBO] load_model failed: {e}")
|
||||
return None
|
||||
if self.use_ssbo_mouse_picking and getattr(self, 'ssbo_editor', None):
|
||||
if animated_model:
|
||||
print(f"[AnimationImport] 检测到动画模型,跳过SSBO导入: {file_path}")
|
||||
else:
|
||||
try:
|
||||
# Clear selection/gizmo first to avoid dangling references to soon-to-be removed nodes.
|
||||
if hasattr(self, 'selection') and self.selection:
|
||||
try:
|
||||
self.selection.clearSelection()
|
||||
except Exception:
|
||||
try:
|
||||
self.selection.updateSelection(None)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.ssbo_editor.load_model(
|
||||
file_path,
|
||||
keep_source_model=scene_package_import,
|
||||
append=not scene_package_import,
|
||||
scene_package_import=scene_package_import,
|
||||
)
|
||||
return self._refresh_ssbo_runtime_import_bindings(
|
||||
file_path=file_path,
|
||||
scene_package_import=scene_package_import,
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"[SSBO] load_model failed: {e}")
|
||||
return None
|
||||
|
||||
# Legacy fallback
|
||||
if hasattr(self, 'scene_manager') and self.scene_manager:
|
||||
|
||||
@ -647,17 +647,16 @@ class DialogPanels:
|
||||
)
|
||||
self.style_manager.prepare_centered_dialog(460, 260)
|
||||
|
||||
with imgui_ctx.begin("关于 EG", True, flags) as window:
|
||||
with imgui_ctx.begin("关于 MetaCore", True, flags) as window:
|
||||
if not window.opened:
|
||||
self.show_about_dialog = False
|
||||
return
|
||||
|
||||
current_project_path = getattr(getattr(self, "project_manager", None), "current_project_path", None) or "未打开项目"
|
||||
imgui.text("EG 编辑器")
|
||||
imgui.text("元泰引擎 MetaCore V1.0")
|
||||
imgui.separator()
|
||||
imgui.text(f"Python: {platform.python_version()}")
|
||||
imgui.text(f"平台: {platform.system()} {platform.release()}")
|
||||
imgui.text(f"可执行文件: {os.path.basename(sys.executable)}")
|
||||
imgui.text("可执行文件: MetaCore.exe")
|
||||
imgui.separator()
|
||||
imgui.text("当前项目:")
|
||||
imgui.text_colored((0.7, 0.7, 0.7, 1.0), current_project_path)
|
||||
|
||||
@ -439,7 +439,11 @@ class EditorPanelsLeftMixin:
|
||||
return
|
||||
|
||||
display = controller.display_names.get(key, key)
|
||||
obj_count = len(controller.name_to_ids.get(key, []))
|
||||
preferred_ids_fn = getattr(controller, "get_preferred_selection_ids", None)
|
||||
if callable(preferred_ids_fn):
|
||||
obj_count = len(preferred_ids_fn(key))
|
||||
else:
|
||||
obj_count = len(controller.name_to_ids.get(key, []))
|
||||
children = node_data["children"]
|
||||
is_selected = (getattr(ssbo_editor, "selected_name", None) == key)
|
||||
|
||||
@ -484,7 +488,11 @@ class EditorPanelsLeftMixin:
|
||||
return
|
||||
|
||||
display = str(virtual_node.get("display_name", virtual_node.get("name", key)) or key)
|
||||
obj_count = len(controller.name_to_ids.get(key, []))
|
||||
preferred_ids_fn = getattr(controller, "get_preferred_selection_ids", None)
|
||||
if callable(preferred_ids_fn):
|
||||
obj_count = len(preferred_ids_fn(key))
|
||||
else:
|
||||
obj_count = len(controller.name_to_ids.get(key, []))
|
||||
is_selected = (getattr(ssbo_editor, "selected_name", None) == key)
|
||||
children = list((virtual_node.get("children", {}) or {}).values())
|
||||
|
||||
|
||||
@ -235,10 +235,21 @@ class EditorPanelsRightMixin(
|
||||
elif node_type == "光源":
|
||||
self._draw_property_section("光源", lambda: self.app._draw_light_properties(node), default_open=True)
|
||||
elif node_type == "模型":
|
||||
has_animation = False
|
||||
try:
|
||||
if node.hasTag("has_animations"):
|
||||
has_animation = node.getTag("has_animations").lower() == "true"
|
||||
if not has_animation:
|
||||
has_animation = node.getPythonTag("animation") is True
|
||||
except Exception:
|
||||
has_animation = False
|
||||
self._draw_property_section("模型", lambda: self.app._draw_model_properties(node), default_open=True)
|
||||
self._draw_property_section("动画", lambda: self.app._draw_animation_properties(node), default_open=False)
|
||||
self._draw_property_section("动画", lambda: self.app._draw_animation_properties(node), default_open=has_animation)
|
||||
|
||||
self._draw_property_section("材质", lambda: self.app._draw_appearance_properties(node), default_open=True)
|
||||
material_target = self.app._get_selection_material_node()
|
||||
if not material_target or material_target.isEmpty():
|
||||
material_target = node
|
||||
self._draw_property_section("材质", lambda target=material_target: self.app._draw_appearance_properties(target), default_open=True)
|
||||
self._draw_property_section("碰撞", lambda: self.app._draw_collision_properties(node), default_open=False)
|
||||
self._draw_property_section("操作", lambda: self.app._draw_property_actions(node), default_open=False)
|
||||
|
||||
|
||||
@ -8,11 +8,36 @@ class EditorPanelsRightMaterialMixin:
|
||||
self._material_edit_sessions = {}
|
||||
return self._material_edit_sessions
|
||||
|
||||
def _sync_material_panel_target(self, node):
|
||||
"""Reset per-node material UI state when selection changes."""
|
||||
target_key = None
|
||||
try:
|
||||
target_key = getattr(node, "this", None) or id(node)
|
||||
except Exception:
|
||||
target_key = id(node)
|
||||
|
||||
if getattr(self, "_material_panel_target_key", None) == target_key:
|
||||
return
|
||||
|
||||
self._material_panel_target_key = target_key
|
||||
self._material_edit_sessions = {}
|
||||
property_helpers = getattr(self.app, "property_helpers", None)
|
||||
ensure_unique_fn = getattr(property_helpers, "_ensure_unique_materials_for_node", None)
|
||||
if callable(ensure_unique_fn):
|
||||
ensure_unique_fn(node)
|
||||
|
||||
def _ensure_node_materials_are_editable(self, node):
|
||||
self._sync_material_panel_target(node)
|
||||
ensure_unique_fn = getattr(self.app, "_ensure_unique_materials_for_node", None)
|
||||
if callable(ensure_unique_fn):
|
||||
try:
|
||||
materials = ensure_unique_fn(node)
|
||||
ensure_unique_fn(node)
|
||||
except Exception:
|
||||
pass
|
||||
panel_materials_fn = getattr(self.app, "_get_panel_edit_materials_for_node", None)
|
||||
if callable(panel_materials_fn):
|
||||
try:
|
||||
materials = panel_materials_fn(node)
|
||||
if materials:
|
||||
return materials
|
||||
except Exception:
|
||||
@ -110,11 +135,11 @@ class EditorPanelsRightMaterialMixin:
|
||||
def apply_primary_color(color):
|
||||
for current_material in materials:
|
||||
self.app._set_material_base_color(current_material, color)
|
||||
self.app._apply_material_to_geom_states(node, current_material)
|
||||
if self.app._get_material_surface_type(current_material) == 3:
|
||||
self.app._set_material_opacity(node, current_material, color[3])
|
||||
else:
|
||||
self.app._apply_material_surface_state(node, current_material)
|
||||
self.app._sync_material_node_runtime(node, current_material, refresh_ssbo_runtime=False)
|
||||
self._refresh_ssbo_runtime_for_material_node(node)
|
||||
|
||||
def apply_surface_type(surface_type):
|
||||
for current_material in materials:
|
||||
|
||||
@ -50,6 +50,18 @@ class PanelDelegates:
|
||||
return source_node
|
||||
return self._get_selection_node()
|
||||
|
||||
def _get_selection_material_node(self):
|
||||
"""Return the node that material editing should target."""
|
||||
ssbo_editor = getattr(self, "ssbo_editor", None)
|
||||
if ssbo_editor and hasattr(ssbo_editor, "has_active_selection") and ssbo_editor.has_active_selection():
|
||||
source_node = getattr(ssbo_editor, "get_selection_source_material_node", lambda: None)()
|
||||
if source_node and self._node_is_valid(source_node, require_attached=False):
|
||||
return source_node
|
||||
runtime_node = getattr(ssbo_editor, "get_selection_runtime_material_node", lambda: None)()
|
||||
if runtime_node and self._node_is_valid(runtime_node, require_attached=False):
|
||||
return runtime_node
|
||||
return self._get_selection_source_node()
|
||||
|
||||
def _get_selection_key(self):
|
||||
ssbo_editor = getattr(self, "ssbo_editor", None)
|
||||
if ssbo_editor and hasattr(ssbo_editor, "has_active_selection") and ssbo_editor.has_active_selection():
|
||||
@ -339,6 +351,9 @@ class PanelDelegates:
|
||||
def _apply_material_to_geom_states(self, *args, **kwargs):
|
||||
return self.property_helpers._apply_material_to_geom_states(*args, **kwargs)
|
||||
|
||||
def _sync_material_node_runtime(self, *args, **kwargs):
|
||||
return self.property_helpers._sync_material_node_runtime(*args, **kwargs)
|
||||
|
||||
def _update_material_base_color(self, *args, **kwargs):
|
||||
return self.property_helpers._update_material_base_color(*args, **kwargs)
|
||||
|
||||
|
||||
@ -1101,6 +1101,7 @@ class PropertyHelpers:
|
||||
|
||||
self._apply_material_to_geom_states(node, material)
|
||||
self._apply_material_surface_state(node, material)
|
||||
self._persist_material_tags(node, material)
|
||||
|
||||
self._clear_all_textures(node)
|
||||
texture_tags = node_state.get("textures", {}) or {}
|
||||
@ -1148,31 +1149,68 @@ class PropertyHelpers:
|
||||
if not node or node.isEmpty() or material is None:
|
||||
return
|
||||
|
||||
target_geom_paths = self._get_geom_paths_for_material(node, material)
|
||||
if not target_geom_paths:
|
||||
target_geom_paths = []
|
||||
force_subtree_override = False
|
||||
direct_geom_target = self._is_geom_node_path(node)
|
||||
|
||||
try:
|
||||
if node.hasMaterial():
|
||||
node_material = node.getMaterial()
|
||||
if self._get_material_identity_key(node_material) == self._get_material_identity_key(material):
|
||||
force_subtree_override = True
|
||||
except Exception:
|
||||
force_subtree_override = False
|
||||
|
||||
if direct_geom_target:
|
||||
target_geom_paths = [node]
|
||||
elif force_subtree_override:
|
||||
try:
|
||||
if node.hasMaterial():
|
||||
node.setMaterial(material, 1)
|
||||
node.setMaterial(material, 1)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
target_geom_paths = [geom_path for geom_path in node.find_all_matches("**/+GeomNode")]
|
||||
except Exception:
|
||||
target_geom_paths = []
|
||||
else:
|
||||
target_geom_paths = self._get_geom_paths_for_material(node, material)
|
||||
if not target_geom_paths:
|
||||
self._invalidate_material_render_cache()
|
||||
return
|
||||
try:
|
||||
if node.hasMaterial():
|
||||
node.setMaterial(material, 1)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
target_geom_paths = [geom_path for geom_path in node.find_all_matches("**/+GeomNode")]
|
||||
except Exception:
|
||||
target_geom_paths = []
|
||||
|
||||
if not target_geom_paths:
|
||||
self._invalidate_material_render_cache()
|
||||
return
|
||||
|
||||
if direct_geom_target:
|
||||
try:
|
||||
if node.hasMaterial():
|
||||
node.clearMaterial()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
node.clearAttrib(MaterialAttrib.getClassType())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for geom_path in target_geom_paths:
|
||||
try:
|
||||
geom_path.setMaterial(material, 1)
|
||||
except Exception:
|
||||
pass
|
||||
if not direct_geom_target:
|
||||
try:
|
||||
geom_path.setMaterial(material, 1)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
geom_path.setState(geom_path.getState().setAttrib(MaterialAttrib.make(material)))
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
geom_path.setState(geom_path.getState().setAttrib(MaterialAttrib.make(material)))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
geom_node = geom_path.node()
|
||||
for i in range(geom_node.getNumGeoms()):
|
||||
@ -1189,6 +1227,8 @@ class PropertyHelpers:
|
||||
"""If editing a SSBO source node, rebuild the runtime proxy immediately."""
|
||||
try:
|
||||
ssbo_editor = getattr(self, "ssbo_editor", None)
|
||||
if ssbo_editor is None:
|
||||
ssbo_editor = getattr(getattr(self, "app", None), "ssbo_editor", None)
|
||||
if not ssbo_editor or not node or node.isEmpty():
|
||||
return
|
||||
if not hasattr(ssbo_editor, "is_source_tree_node") or not ssbo_editor.is_source_tree_node(node):
|
||||
@ -1210,6 +1250,7 @@ class PropertyHelpers:
|
||||
continue
|
||||
self._apply_material_to_geom_states(node, current_material)
|
||||
self._apply_material_surface_state(node, current_material)
|
||||
self._persist_material_tags(node, current_material)
|
||||
refresh_pipeline = getattr(self, "_refresh_pipeline_material_mode", None)
|
||||
if callable(refresh_pipeline):
|
||||
try:
|
||||
@ -1222,21 +1263,72 @@ class PropertyHelpers:
|
||||
except Exception as e:
|
||||
print(f"同步材质显示状态失败: {e}")
|
||||
|
||||
def _persist_material_tags(self, node, material):
|
||||
"""Mirror editable material values into node tags for project serialization."""
|
||||
try:
|
||||
if not node or node.isEmpty() or material is None:
|
||||
return
|
||||
|
||||
emission = self._get_material_emission(material)
|
||||
if emission is not None:
|
||||
node.setTag("material_emission", str(tuple(float(v) for v in emission)))
|
||||
|
||||
base_color = self._get_material_base_color(material)
|
||||
if base_color is not None:
|
||||
node.setTag("material_basecolor", str(tuple(float(v) for v in base_color)))
|
||||
|
||||
scalar_values = {
|
||||
"material_roughness": float(material.roughness) if hasattr(material, "roughness") and material.roughness is not None else None,
|
||||
"material_metallic": float(material.metallic) if hasattr(material, "metallic") and material.metallic is not None else None,
|
||||
"material_ior": float(material.refractive_index) if hasattr(material, "refractive_index") and material.refractive_index is not None else None,
|
||||
}
|
||||
for tag_name, value in scalar_values.items():
|
||||
if value is not None:
|
||||
node.setTag(tag_name, str(float(value)))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _get_node_materials(self, node):
|
||||
"""Return the editable materials currently used by a node."""
|
||||
if not node or node.isEmpty():
|
||||
return []
|
||||
|
||||
materials = []
|
||||
|
||||
try:
|
||||
if node.hasMaterial():
|
||||
return [node.getMaterial()]
|
||||
node_material = node.getMaterial()
|
||||
if node_material is not None:
|
||||
materials.append(node_material)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
materials = list(node.find_all_materials())
|
||||
materials.extend(list(node.find_all_materials()))
|
||||
except Exception:
|
||||
materials = []
|
||||
pass
|
||||
|
||||
# `find_all_materials()` can miss effective inherited material state on some
|
||||
# wrapper/transform nodes, so inspect renderable children as a fallback.
|
||||
try:
|
||||
from panda3d.core import MaterialAttrib
|
||||
|
||||
for geom_path in node.find_all_matches("**/+GeomNode"):
|
||||
try:
|
||||
if geom_path.hasMaterial():
|
||||
geom_material = geom_path.getMaterial()
|
||||
if geom_material is not None:
|
||||
materials.append(geom_material)
|
||||
net_state = geom_path.getNetState()
|
||||
if net_state.hasAttrib(MaterialAttrib):
|
||||
material_attrib = net_state.getAttrib(MaterialAttrib)
|
||||
geom_material = material_attrib.getMaterial() if material_attrib else None
|
||||
if geom_material is not None:
|
||||
materials.append(geom_material)
|
||||
except Exception:
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
unique_materials = []
|
||||
seen_keys = set()
|
||||
@ -1284,7 +1376,10 @@ class PropertyHelpers:
|
||||
if not node or node.isEmpty():
|
||||
return []
|
||||
|
||||
materials = self._get_node_materials(node)
|
||||
if self._is_geom_node_path(node):
|
||||
materials = self._get_panel_edit_materials_for_node(node)
|
||||
else:
|
||||
materials = self._get_node_materials(node)
|
||||
if not materials:
|
||||
return []
|
||||
|
||||
@ -1298,8 +1393,12 @@ class PropertyHelpers:
|
||||
stored_signature = tuple(node.getPythonTag("_editable_material_signature"))
|
||||
except Exception:
|
||||
stored_signature = None
|
||||
try:
|
||||
stored_isolated = bool(node.getPythonTag("_editable_material_isolated"))
|
||||
except Exception:
|
||||
stored_isolated = False
|
||||
|
||||
if stored_signature == current_signature:
|
||||
if stored_signature == current_signature and stored_isolated:
|
||||
return materials
|
||||
|
||||
node_material_key = None
|
||||
@ -1311,6 +1410,7 @@ class PropertyHelpers:
|
||||
|
||||
changed = False
|
||||
cloned_materials = []
|
||||
direct_geom_target = self._is_geom_node_path(node)
|
||||
|
||||
for material in materials:
|
||||
if material is None:
|
||||
@ -1324,31 +1424,54 @@ class PropertyHelpers:
|
||||
continue
|
||||
|
||||
try:
|
||||
if node_material_key is not None and source_key == node_material_key:
|
||||
if node_material_key is not None and source_key == node_material_key and not direct_geom_target:
|
||||
node.setMaterial(cloned_material, 1)
|
||||
changed = True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
geom_paths = self._get_geom_paths_for_material(node, material)
|
||||
if direct_geom_target:
|
||||
geom_paths = [node]
|
||||
try:
|
||||
if node.hasMaterial():
|
||||
node.clearMaterial()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
node.clearAttrib(MaterialAttrib.getClassType())
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
geom_paths = self._get_geom_paths_for_material(node, material)
|
||||
if not geom_paths and node_material_key is None:
|
||||
try:
|
||||
node.setMaterial(cloned_material, 1)
|
||||
changed = True
|
||||
has_geom_descendants = bool(node.find_all_matches("**/+GeomNode"))
|
||||
except Exception:
|
||||
pass
|
||||
has_geom_descendants = False
|
||||
|
||||
# Only fall back to applying on the wrapper node when it is the
|
||||
# actual render target. For transform parents, a blind node-level
|
||||
# override creates an editable material that does not affect the
|
||||
# visible child geom states.
|
||||
if not has_geom_descendants:
|
||||
try:
|
||||
node.setMaterial(cloned_material, 1)
|
||||
changed = True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for geom_path in geom_paths:
|
||||
try:
|
||||
geom_path.setMaterial(cloned_material, 1)
|
||||
changed = True
|
||||
except Exception:
|
||||
pass
|
||||
if not direct_geom_target:
|
||||
try:
|
||||
geom_path.setMaterial(cloned_material, 1)
|
||||
changed = True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
geom_path.setState(geom_path.getState().setAttrib(MaterialAttrib.make(cloned_material)))
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
geom_path.setState(geom_path.getState().setAttrib(MaterialAttrib.make(cloned_material)))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
geom_node = geom_path.node()
|
||||
@ -1378,6 +1501,10 @@ class PropertyHelpers:
|
||||
node.setPythonTag("_editable_material_signature", latest_signature)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
node.setPythonTag("_editable_material_isolated", bool(changed))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return latest_materials
|
||||
except Exception as e:
|
||||
@ -1390,6 +1517,27 @@ class PropertyHelpers:
|
||||
except Exception:
|
||||
return id(material)
|
||||
|
||||
def _is_geom_node_path(self, node):
|
||||
"""Return whether the given NodePath directly wraps a GeomNode."""
|
||||
try:
|
||||
if not node or node.isEmpty():
|
||||
return False
|
||||
panda_node = node.node()
|
||||
if panda_node is None:
|
||||
return False
|
||||
if hasattr(panda_node, "isGeomNode"):
|
||||
try:
|
||||
return bool(panda_node.isGeomNode())
|
||||
except Exception:
|
||||
pass
|
||||
from panda3d.core import GeomNode
|
||||
try:
|
||||
return panda_node.is_of_type(GeomNode.get_class_type())
|
||||
except Exception:
|
||||
return isinstance(panda_node, GeomNode)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _geom_uses_material(self, geom_path, material):
|
||||
try:
|
||||
from panda3d.core import MaterialAttrib
|
||||
@ -1474,6 +1622,34 @@ class PropertyHelpers:
|
||||
print(f"创建默认材质失败: {e}")
|
||||
return None
|
||||
|
||||
def _get_panel_edit_materials_for_node(self, node):
|
||||
"""Return the material instances the property panel should edit for this node.
|
||||
|
||||
Parent/group nodes should edit a single node-level override material instead
|
||||
of mutating every descendant material individually.
|
||||
"""
|
||||
if not node or node.isEmpty():
|
||||
return []
|
||||
|
||||
if self._is_geom_node_path(node):
|
||||
material = self._get_renderable_node_material(node)
|
||||
if material is None:
|
||||
material = self._ensure_material_for_node(node)
|
||||
return [material] if material is not None else []
|
||||
|
||||
try:
|
||||
geom_descendants = list(node.find_all_matches("**/+GeomNode"))
|
||||
except Exception:
|
||||
geom_descendants = []
|
||||
|
||||
if node.hasMaterial():
|
||||
return self._get_node_materials(node)
|
||||
|
||||
if geom_descendants:
|
||||
return []
|
||||
|
||||
return self._get_node_materials(node)
|
||||
|
||||
def _get_material_surface_type(self, material):
|
||||
"""Return the RenderPipeline shading model value used by this material."""
|
||||
try:
|
||||
@ -1800,6 +1976,8 @@ class PropertyHelpers:
|
||||
try:
|
||||
if not node or node.isEmpty():
|
||||
return []
|
||||
if self._is_geom_node_path(node):
|
||||
return [node]
|
||||
renderable_nodes = [match for match in node.find_all_matches("**/+GeomNode")]
|
||||
if renderable_nodes:
|
||||
return renderable_nodes
|
||||
@ -1899,13 +2077,28 @@ class PropertyHelpers:
|
||||
opacity = self._get_material_opacity(material) if is_transparent else 1.0
|
||||
base_color = self._get_material_transparent_base_color(material)
|
||||
|
||||
target_nodes = self._get_geom_paths_for_material(node, material)
|
||||
if not target_nodes:
|
||||
force_subtree_override = False
|
||||
direct_geom_target = self._is_geom_node_path(node)
|
||||
try:
|
||||
if node.hasMaterial():
|
||||
node_material = node.getMaterial()
|
||||
if self._get_material_identity_key(node_material) == self._get_material_identity_key(material):
|
||||
force_subtree_override = True
|
||||
except Exception:
|
||||
force_subtree_override = False
|
||||
|
||||
if direct_geom_target:
|
||||
target_nodes = [node]
|
||||
elif force_subtree_override:
|
||||
target_nodes = self._iter_material_state_nodes(node)
|
||||
else:
|
||||
target_nodes = self._get_geom_paths_for_material(node, material)
|
||||
if not target_nodes:
|
||||
target_nodes = self._iter_material_state_nodes(node)
|
||||
|
||||
for target_node in target_nodes:
|
||||
target_material = material
|
||||
if target_material is not None:
|
||||
if target_material is not None and not direct_geom_target:
|
||||
try:
|
||||
target_node.setMaterial(target_material, 1)
|
||||
except Exception:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user