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",
|
"importer": "model_importer",
|
||||||
"import_settings": {},
|
"import_settings": {},
|
||||||
"dependency_guids": [],
|
"dependency_guids": [],
|
||||||
"updated_at": "2026-03-19 17:17:48",
|
"updated_at": "2026-03-23 14:37:54",
|
||||||
"imported_cache": {
|
"imported_cache": {
|
||||||
"root": "Library/Imported/90ece77e67b54ccda38d2de71cb4694d",
|
"root": "Library/Imported/90ece77e67b54ccda38d2de71cb4694d",
|
||||||
"model_bam": "Library/Imported/90ece77e67b54ccda38d2de71cb4694d/model.bam",
|
"model_bam": "Library/Imported/90ece77e67b54ccda38d2de71cb4694d/model.bam",
|
||||||
@ -18,9 +18,85 @@
|
|||||||
"materials": "Library/Imported/90ece77e67b54ccda38d2de71cb4694d/materials.json",
|
"materials": "Library/Imported/90ece77e67b54ccda38d2de71cb4694d/materials.json",
|
||||||
"import_info": "Library/Imported/90ece77e67b54ccda38d2de71cb4694d/import_info.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": {
|
"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_path": "Assets/Models/box1.glb",
|
||||||
"asset_type": "model",
|
"asset_type": "model",
|
||||||
"source_hash": "fc694905c47a9b0005d77b701cc41852b56ef08c7406829a306e98f3ce158a64",
|
"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("", "show-frame-rate-meter 0")
|
||||||
loadPrcFileData("", "window-type onscreen")
|
loadPrcFileData("", "window-type onscreen")
|
||||||
|
loadPrcFileData("", "window-title MetaCore")
|
||||||
loadPrcFileData("", f"win-size {width} {height}")
|
loadPrcFileData("", f"win-size {width} {height}")
|
||||||
loadPrcFileData("", "win-fixed-size #f") # 允许窗口调整大小
|
loadPrcFileData("", "win-fixed-size #f") # 允许窗口调整大小
|
||||||
|
|
||||||
@ -81,6 +82,13 @@ class CoreWorld(ShowBase):
|
|||||||
# 初始化 ShowBase
|
# 初始化 ShowBase
|
||||||
ShowBase.__init__(self)
|
ShowBase.__init__(self)
|
||||||
|
|
||||||
|
try:
|
||||||
|
props = WindowProperties()
|
||||||
|
props.setTitle("MetaCore")
|
||||||
|
self.win.requestProperties(props)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# 创建渲染管线
|
# 创建渲染管线
|
||||||
self.render_pipeline.create(self)
|
self.render_pipeline.create(self)
|
||||||
install_editor_tag_state_manager(self.render_pipeline, self)
|
install_editor_tag_state_manager(self.render_pipeline, self)
|
||||||
|
|||||||
34
imgui.ini
34
imgui.ini
@ -31,19 +31,19 @@ DockId=0x0000000D,0
|
|||||||
|
|
||||||
[Window][场景树]
|
[Window][场景树]
|
||||||
Pos=0,20
|
Pos=0,20
|
||||||
Size=274,1331
|
Size=339,1084
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000007,0
|
DockId=0x00000007,0
|
||||||
|
|
||||||
[Window][属性面板]
|
[Window][属性面板]
|
||||||
Pos=2167,20
|
Pos=1655,20
|
||||||
Size=393,1331
|
Size=393,1084
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000002,0
|
DockId=0x00000002,0
|
||||||
|
|
||||||
[Window][控制台]
|
[Window][控制台]
|
||||||
Pos=276,952
|
Pos=341,705
|
||||||
Size=1889,399
|
Size=1312,399
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000006,1
|
DockId=0x00000006,1
|
||||||
|
|
||||||
@ -59,7 +59,7 @@ Collapsed=0
|
|||||||
|
|
||||||
[Window][WindowOverViewport_11111111]
|
[Window][WindowOverViewport_11111111]
|
||||||
Pos=0,20
|
Pos=0,20
|
||||||
Size=2560,1331
|
Size=2048,1084
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][测试窗口1]
|
[Window][测试窗口1]
|
||||||
@ -98,8 +98,8 @@ Size=600,500
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][资源管理器]
|
[Window][资源管理器]
|
||||||
Pos=276,952
|
Pos=341,705
|
||||||
Size=1889,399
|
Size=1312,399
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000006,0
|
DockId=0x00000006,0
|
||||||
|
|
||||||
@ -134,7 +134,7 @@ Size=89,250
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][颜色选择器]
|
[Window][颜色选择器]
|
||||||
Pos=874,352
|
Pos=878,354
|
||||||
Size=300,400
|
Size=300,400
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
@ -214,11 +214,21 @@ Pos=1010,515
|
|||||||
Size=540,320
|
Size=540,320
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
|
[Window][关于 EG]
|
||||||
|
Pos=794,422
|
||||||
|
Size=460,260
|
||||||
|
Collapsed=0
|
||||||
|
|
||||||
|
[Window][关于 MetaCore]
|
||||||
|
Pos=794,422
|
||||||
|
Size=460,260
|
||||||
|
Collapsed=0
|
||||||
|
|
||||||
[Docking][Data]
|
[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=0x00000001 Parent=0x08BD597D SizeRef=1525,1012 Split=X
|
||||||
DockNode ID=0x00000007 Parent=0x00000001 SizeRef=274,1084 Selected=0xE0015051
|
DockNode ID=0x00000007 Parent=0x00000001 SizeRef=339,1084 Selected=0xE0015051
|
||||||
DockNode ID=0x00000008 Parent=0x00000001 SizeRef=1249,1084 Split=Y
|
DockNode ID=0x00000008 Parent=0x00000001 SizeRef=1312,1084 Split=Y
|
||||||
DockNode ID=0x00000005 Parent=0x00000008 SizeRef=2048,683 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=0x0000000D Parent=0x00000005 SizeRef=1318,383 HiddenTabBar=1 Selected=0x43A39006
|
||||||
DockNode ID=0x0000000E Parent=0x00000005 SizeRef=1318,363 CentralNode=1 Selected=0xE0015051
|
DockNode ID=0x0000000E Parent=0x00000005 SizeRef=1318,363 CentralNode=1 Selected=0xE0015051
|
||||||
|
|||||||
@ -1008,7 +1008,7 @@ class ProjectManager:
|
|||||||
load_lui_fn(temp_stub)
|
load_lui_fn(temp_stub)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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):
|
def _iter_top_level_scene_asset_nodes(self, scene_description):
|
||||||
nodes = list(scene_description.get("nodes", []) or [])
|
nodes = list(scene_description.get("nodes", []) or [])
|
||||||
@ -1089,18 +1089,176 @@ class ProjectManager:
|
|||||||
target_np.setTag("file", os.path.basename(asset_abs_path))
|
target_np.setTag("file", os.path.basename(asset_abs_path))
|
||||||
if imported_node_key:
|
if imported_node_key:
|
||||||
target_np.setTag("imported_node_key", imported_node_key)
|
target_np.setTag("imported_node_key", imported_node_key)
|
||||||
target_np.setTag("is_model_root", "1")
|
if asset_guid:
|
||||||
target_np.setTag("is_scene_element", "1")
|
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)))
|
runtime_interactive = bool(metadata_component.get("runtime_interactive", node.get("runtime_interactive", False)))
|
||||||
if runtime_interactive:
|
if runtime_interactive:
|
||||||
target_np.setTag("runtime_interactive", "true")
|
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 [])
|
scripts = list(scripts_component.get("entries", []) or node.get("scripts", []) or [])
|
||||||
if scripts:
|
if scripts:
|
||||||
target_np.setTag("has_scripts", "true")
|
target_np.setTag("has_scripts", "true")
|
||||||
target_np.setTag("scripts_info", json.dumps(scripts, ensure_ascii=False))
|
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):
|
def _load_scene_description_via_ssbo(self, scene_description, project_path, asset_database):
|
||||||
if not getattr(self.world, "use_ssbo_mouse_picking", False):
|
if not getattr(self.world, "use_ssbo_mouse_picking", False):
|
||||||
return False
|
return False
|
||||||
@ -1109,6 +1267,8 @@ class ProjectManager:
|
|||||||
if not ssbo_editor:
|
if not ssbo_editor:
|
||||||
return False
|
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)
|
top_level_asset_nodes, node_lookup = self._iter_top_level_scene_asset_nodes(scene_description)
|
||||||
if not top_level_asset_nodes:
|
if not top_level_asset_nodes:
|
||||||
return False
|
return False
|
||||||
@ -1133,6 +1293,33 @@ class ProjectManager:
|
|||||||
if not os.path.exists(asset_abs):
|
if not os.path.exists(asset_abs):
|
||||||
continue
|
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:
|
try:
|
||||||
imported_root = ssbo_editor.load_model(
|
imported_root = ssbo_editor.load_model(
|
||||||
asset_abs,
|
asset_abs,
|
||||||
@ -1148,7 +1335,15 @@ class ProjectManager:
|
|||||||
if target_root is None:
|
if target_root is None:
|
||||||
continue
|
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
|
loaded_any = True
|
||||||
|
|
||||||
if not loaded_any:
|
if not loaded_any:
|
||||||
@ -1494,6 +1689,14 @@ class ProjectManager:
|
|||||||
print("错误: 请先创建或打开一个项目!")
|
print("错误: 请先创建或打开一个项目!")
|
||||||
return False
|
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
|
project_path = self.current_project_path
|
||||||
ensure_project_directories(ProjectLayout(project_path))
|
ensure_project_directories(ProjectLayout(project_path))
|
||||||
project_config = self._ensure_v2_project_defaults(project_path, dict(self.project_config or {}))
|
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 []):
|
for model in list(getattr(scene_manager, "models", []) or []):
|
||||||
try:
|
try:
|
||||||
if tree_widget:
|
if tree_widget:
|
||||||
tree_widget.delete_item(model)
|
try:
|
||||||
elif model and not model.isEmpty():
|
tree_widget.delete_item(model)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if model and not model.isEmpty():
|
||||||
model.removeNode()
|
model.removeNode()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
@ -2225,8 +2431,11 @@ class ProjectManager:
|
|||||||
for light_node in collection:
|
for light_node in collection:
|
||||||
try:
|
try:
|
||||||
if tree_widget:
|
if tree_widget:
|
||||||
tree_widget.delete_item(light_node)
|
try:
|
||||||
elif light_node and not light_node.isEmpty():
|
tree_widget.delete_item(light_node)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if light_node and not light_node.isEmpty():
|
||||||
light_node.removeNode()
|
light_node.removeNode()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
@ -2237,8 +2446,11 @@ class ProjectManager:
|
|||||||
for terrain in terrains:
|
for terrain in terrains:
|
||||||
try:
|
try:
|
||||||
if tree_widget:
|
if tree_widget:
|
||||||
tree_widget.delete_item(terrain)
|
try:
|
||||||
elif terrain and not terrain.isEmpty():
|
tree_widget.delete_item(terrain)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if terrain and not terrain.isEmpty():
|
||||||
terrain.removeNode()
|
terrain.removeNode()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@ -174,10 +174,20 @@ def _collect_script_component(scripts: list[dict]) -> dict:
|
|||||||
|
|
||||||
|
|
||||||
def _collect_node_metadata_component(node, runtime_interactive: bool) -> dict:
|
def _collect_node_metadata_component(node, runtime_interactive: bool) -> dict:
|
||||||
return {
|
metadata = {
|
||||||
"node_class": node.node().getClassType().getName() if node.node() else "",
|
"node_class": node.node().getClassType().getName() if node.node() else "",
|
||||||
"runtime_interactive": runtime_interactive,
|
"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):
|
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:
|
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:
|
if not node:
|
||||||
return False
|
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:
|
if not asset_guid:
|
||||||
return True
|
return True
|
||||||
if scripts:
|
if scripts:
|
||||||
@ -447,6 +461,37 @@ def build_runtime_scene(scene_description: dict):
|
|||||||
for node in nodes:
|
for node in nodes:
|
||||||
if not node.get("asset_guid"):
|
if not node.get("asset_guid"):
|
||||||
continue
|
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"):
|
if node.get("runtime_interactive"):
|
||||||
interactive_node_ids.append(node.get("node_id", ""))
|
interactive_node_ids.append(node.get("node_id", ""))
|
||||||
interactive_model_names.append(node.get("name", ""))
|
interactive_model_names.append(node.get("name", ""))
|
||||||
|
|||||||
@ -242,6 +242,20 @@ class ObjectController:
|
|||||||
|
|
||||||
return False
|
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):
|
def _encode_id_color(self, vdata, object_id):
|
||||||
if not vdata.has_column("color"):
|
if not vdata.has_column("color"):
|
||||||
new_fmt = vdata.get_format().get_union_format(GeomVertexFormat.get_v3c4())
|
new_fmt = vdata.get_format().get_union_format(GeomVertexFormat.get_v3c4())
|
||||||
|
|||||||
@ -79,6 +79,7 @@ class SSBOEditor:
|
|||||||
# Internal State
|
# Internal State
|
||||||
self.selected_name = None
|
self.selected_name = None
|
||||||
self.selected_ids = []
|
self.selected_ids = []
|
||||||
|
self.transform_ids = []
|
||||||
self.search_text = ""
|
self.search_text = ""
|
||||||
self.last_search_text = None
|
self.last_search_text = None
|
||||||
self.filtered_nodes = []
|
self.filtered_nodes = []
|
||||||
@ -666,22 +667,37 @@ class SSBOEditor:
|
|||||||
if not self._node_is_valid(source_node):
|
if not self._node_is_valid(source_node):
|
||||||
continue
|
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
|
source_snapshot = None
|
||||||
if callable(capture_snapshot_fn):
|
if callable(capture_snapshot_fn):
|
||||||
try:
|
try:
|
||||||
source_snapshot = capture_snapshot_fn(obj_np)
|
source_snapshot = capture_snapshot_fn(source_node)
|
||||||
except Exception:
|
except Exception:
|
||||||
source_snapshot = None
|
source_snapshot = None
|
||||||
|
|
||||||
source_node_key = id(source_node)
|
source_node_key = id(source_node)
|
||||||
entry = grouped_entries.get(source_node_key)
|
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"]:
|
if entry is None or candidate_score > entry["score"]:
|
||||||
grouped_entries[source_node_key] = {
|
grouped_entries[source_node_key] = {
|
||||||
"gid": gid,
|
"gid": gid,
|
||||||
"obj_np": obj_np,
|
"obj_np": obj_np,
|
||||||
"source_node": source_node,
|
"source_node": source_node,
|
||||||
"snapshot": source_snapshot,
|
"snapshot": chosen_snapshot,
|
||||||
"score": candidate_score,
|
"score": candidate_score,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -870,6 +886,7 @@ class SSBOEditor:
|
|||||||
self.model = None
|
self.model = None
|
||||||
self.selected_name = None
|
self.selected_name = None
|
||||||
self.selected_ids = []
|
self.selected_ids = []
|
||||||
|
self.transform_ids = []
|
||||||
self.last_import_tree_key = None
|
self.last_import_tree_key = None
|
||||||
self.last_import_root_name = None
|
self.last_import_root_name = None
|
||||||
if not preserve_source_models:
|
if not preserve_source_models:
|
||||||
@ -1957,13 +1974,14 @@ class SSBOEditor:
|
|||||||
if not self.controller:
|
if not self.controller:
|
||||||
return
|
return
|
||||||
self._sync_pick_root_transform()
|
self._sync_pick_root_transform()
|
||||||
if not self.selected_ids:
|
transform_ids = self.get_selection_transform_ids()
|
||||||
|
if not transform_ids:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Group selection can contain thousands of objects.
|
# Group selection can contain thousands of objects.
|
||||||
# Only resync when proxy transform has changed.
|
# Only resync when proxy transform has changed.
|
||||||
proxy = getattr(self, "_group_proxy", None)
|
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)
|
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):
|
if self._last_group_sync_mat and self._matrices_close(proxy_world, self._last_group_sync_mat):
|
||||||
return
|
return
|
||||||
@ -1971,8 +1989,8 @@ class SSBOEditor:
|
|||||||
else:
|
else:
|
||||||
self._last_group_sync_mat = None
|
self._last_group_sync_mat = None
|
||||||
|
|
||||||
if len(self.selected_ids) == 1:
|
if len(transform_ids) == 1:
|
||||||
gid = self.selected_ids[0]
|
gid = transform_ids[0]
|
||||||
obj_np = self.controller.id_to_object_np.get(gid)
|
obj_np = self.controller.id_to_object_np.get(gid)
|
||||||
pick_np = self.controller.id_to_pick_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()):
|
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_gid = None
|
||||||
self._last_single_sync_mat = 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)
|
obj_np = self.controller.id_to_object_np.get(gid)
|
||||||
pick_np = self.controller.id_to_pick_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():
|
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):
|
def _update_outline_for_selection(self):
|
||||||
if not self._outline_manager:
|
if not self._outline_manager:
|
||||||
return
|
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()
|
self._outline_manager.clear()
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -2126,7 +2145,7 @@ class SSBOEditor:
|
|||||||
|
|
||||||
targets = []
|
targets = []
|
||||||
target_limit = max(1, int(getattr(self._outline_manager, "max_targets", 64)))
|
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)
|
obj_np = self.controller.id_to_object_np.get(gid)
|
||||||
if obj_np and not obj_np.is_empty():
|
if obj_np and not obj_np.is_empty():
|
||||||
targets.append(obj_np)
|
targets.append(obj_np)
|
||||||
@ -2149,6 +2168,11 @@ class SSBOEditor:
|
|||||||
def has_active_selection(self):
|
def has_active_selection(self):
|
||||||
return bool(self.controller and self.selected_name is not None)
|
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):
|
def _is_root_selection(self):
|
||||||
return bool(
|
return bool(
|
||||||
self.controller and
|
self.controller and
|
||||||
@ -2161,6 +2185,7 @@ class SSBOEditor:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
has_dynamic_objects = bool(getattr(self.controller, "id_to_object_np", {}) or {})
|
has_dynamic_objects = bool(getattr(self.controller, "id_to_object_np", {}) or {})
|
||||||
|
transform_ids = self.get_selection_transform_ids()
|
||||||
|
|
||||||
if self._is_root_selection():
|
if self._is_root_selection():
|
||||||
return self.model if self._node_is_valid(self.model) else None
|
return self.model if self._node_is_valid(self.model) else None
|
||||||
@ -2168,7 +2193,7 @@ class SSBOEditor:
|
|||||||
if not has_dynamic_objects:
|
if not has_dynamic_objects:
|
||||||
return self.model if self._node_is_valid(self.model) else None
|
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)
|
proxy = getattr(self, "_group_proxy", None)
|
||||||
if proxy and self._node_is_valid(proxy):
|
if proxy and self._node_is_valid(proxy):
|
||||||
# Apply the original first node's material/tags to proxy transparently?
|
# Apply the original first node's material/tags to proxy transparently?
|
||||||
@ -2182,15 +2207,36 @@ class SSBOEditor:
|
|||||||
|
|
||||||
return None
|
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):
|
def get_selection_summary(self):
|
||||||
if not self.controller or self.selected_name is None:
|
if not self.controller or self.selected_name is None:
|
||||||
return None
|
return None
|
||||||
return {
|
return {
|
||||||
"key": self.selected_name,
|
"key": self.selected_name,
|
||||||
"display_name": self.controller.display_names.get(self.selected_name, 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_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):
|
def get_selection_key(self):
|
||||||
@ -2298,6 +2344,7 @@ class SSBOEditor:
|
|||||||
self._cleanup_group_proxy()
|
self._cleanup_group_proxy()
|
||||||
self.selected_name = None
|
self.selected_name = None
|
||||||
self.selected_ids = []
|
self.selected_ids = []
|
||||||
|
self.transform_ids = []
|
||||||
if self._outline_manager:
|
if self._outline_manager:
|
||||||
self._outline_manager.clear()
|
self._outline_manager.clear()
|
||||||
if self.controller:
|
if self.controller:
|
||||||
@ -2355,7 +2402,12 @@ class SSBOEditor:
|
|||||||
self._reset_pick_sync_cache()
|
self._reset_pick_sync_cache()
|
||||||
|
|
||||||
self.selected_name = key
|
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 {})
|
has_dynamic_objects = bool(getattr(self.controller, "id_to_object_np", {}) or {})
|
||||||
is_root_selection = (
|
is_root_selection = (
|
||||||
self.controller and
|
self.controller and
|
||||||
@ -2367,7 +2419,7 @@ class SSBOEditor:
|
|||||||
root_key = getattr(self.controller, "tree_root_key", None)
|
root_key = getattr(self.controller, "tree_root_key", None)
|
||||||
root_node = self.controller.tree_nodes.get(root_key, {}) if root_key else {}
|
root_node = self.controller.tree_nodes.get(root_key, {}) if root_key else {}
|
||||||
top_level_children = set(root_node.get("children", []) or [])
|
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:
|
except Exception:
|
||||||
is_top_level_heavy_selection = False
|
is_top_level_heavy_selection = False
|
||||||
if sync_world_selection:
|
if sync_world_selection:
|
||||||
@ -2388,17 +2440,17 @@ class SSBOEditor:
|
|||||||
self._stop_pick_sync_task()
|
self._stop_pick_sync_task()
|
||||||
return
|
return
|
||||||
|
|
||||||
self.controller.set_active_ids(self.selected_ids)
|
self.controller.set_active_ids(self.transform_ids)
|
||||||
self._update_outline_for_selection()
|
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:
|
if self._transform_gizmo:
|
||||||
self._transform_gizmo.detach()
|
self._transform_gizmo.detach()
|
||||||
return
|
return
|
||||||
|
|
||||||
if len(self.selected_ids) == 1:
|
if len(self.transform_ids) == 1:
|
||||||
# Single object: attach gizmo directly
|
# 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():
|
if obj_np and not obj_np.is_empty():
|
||||||
self._transform_gizmo.attach(obj_np)
|
self._transform_gizmo.attach(obj_np)
|
||||||
self._start_pick_sync_task()
|
self._start_pick_sync_task()
|
||||||
@ -2416,7 +2468,7 @@ class SSBOEditor:
|
|||||||
pass
|
pass
|
||||||
center = Vec3(0, 0, 0)
|
center = Vec3(0, 0, 0)
|
||||||
valid = []
|
valid = []
|
||||||
for gid in self.selected_ids:
|
for gid in self.transform_ids:
|
||||||
obj_np = self.controller.id_to_object_np.get(gid)
|
obj_np = self.controller.id_to_object_np.get(gid)
|
||||||
if obj_np and not obj_np.is_empty():
|
if obj_np and not obj_np.is_empty():
|
||||||
center += obj_np.get_pos(self.base.render)
|
center += obj_np.get_pos(self.base.render)
|
||||||
@ -2494,8 +2546,9 @@ class SSBOEditor:
|
|||||||
self.filtered_nodes = rows
|
self.filtered_nodes = rows
|
||||||
|
|
||||||
def focus_on_selected(self):
|
def focus_on_selected(self):
|
||||||
if self.selected_name and self.selected_ids:
|
transform_ids = self.get_selection_transform_ids()
|
||||||
first_id = self.selected_ids[0]
|
if self.selected_name and transform_ids:
|
||||||
|
first_id = transform_ids[0]
|
||||||
pos = self.controller.get_world_pos(first_id)
|
pos = self.controller.get_world_pos(first_id)
|
||||||
dist = 100
|
dist = 100
|
||||||
self.base.camera.set_pos(pos.x, pos.y - dist, pos.z + dist * 0.5)
|
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 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
|
speed = 50 * dt
|
||||||
acc = Vec3(0, 0, 0)
|
acc = Vec3(0, 0, 0)
|
||||||
if self.keys.get('arrow_up'): acc.z += speed
|
if self.keys.get('arrow_up'): acc.z += speed
|
||||||
@ -2574,7 +2628,7 @@ class SSBOEditor:
|
|||||||
self.model.set_pos(next_pos)
|
self.model.set_pos(next_pos)
|
||||||
self._sync_pick_root_transform()
|
self._sync_pick_root_transform()
|
||||||
else:
|
else:
|
||||||
for idx in self.selected_ids:
|
for idx in transform_ids:
|
||||||
self.controller.move_object(idx, acc)
|
self.controller.move_object(idx, acc)
|
||||||
|
|
||||||
return task.cont
|
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 "")
|
imported_node_key = str(model_component.get("imported_node_key", "") or node.get("imported_node_key", "") or "")
|
||||||
|
|
||||||
if asset_guid:
|
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)
|
rebuilt_np = loaded_np if loaded_np and not loaded_np.isEmpty() else parent_np.attachNewNode(node_name)
|
||||||
if rebuilt_np.getParent() != parent_np:
|
if rebuilt_np.getParent() != parent_np:
|
||||||
rebuilt_np.reparentTo(parent_np)
|
rebuilt_np.reparentTo(parent_np)
|
||||||
@ -394,27 +394,55 @@ class MainApp(ShowBase):
|
|||||||
|
|
||||||
return rebuilt_np
|
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 {})
|
asset_record = dict(self._asset_index.get(str(asset_guid or ""), {}) or {})
|
||||||
if not asset_record:
|
if not asset_record:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
candidate_paths = []
|
candidate_paths = []
|
||||||
imported_cache = dict(asset_record.get("imported_cache", {}) or {})
|
node_data = dict(node_data or {})
|
||||||
imported_model_rel = str(imported_cache.get("model_bam", "") or "").replace("\\", "/").strip()
|
components = dict(node_data.get("components", {}) or {})
|
||||||
if imported_model_rel:
|
metadata_component = dict(components.get("metadata", {}) or {})
|
||||||
candidate_paths.append(os.path.join(DATA_ROOT, imported_model_rel.replace("/", os.sep)))
|
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)
|
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()
|
asset_path = str(asset_record.get("asset_path", "") or "").replace("\\", "/").strip()
|
||||||
if asset_path:
|
if asset_path:
|
||||||
candidate_paths.append(os.path.join(DATA_ROOT, asset_path.replace("/", os.sep)))
|
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)))
|
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:
|
for candidate_path in candidate_paths:
|
||||||
if not candidate_path or not os.path.exists(candidate_path):
|
if not candidate_path or not os.path.exists(candidate_path):
|
||||||
continue
|
continue
|
||||||
|
|||||||
@ -948,7 +948,7 @@ class AnimationTools:
|
|||||||
except Exception:
|
except Exception:
|
||||||
has_animation_nodes = False
|
has_animation_nodes = False
|
||||||
|
|
||||||
def _try_memory_fallback():
|
def _try_memory_fallback(prefer_scene_proxy=False):
|
||||||
def _collect_autobind_source_candidates():
|
def _collect_autobind_source_candidates():
|
||||||
candidates = []
|
candidates = []
|
||||||
|
|
||||||
@ -1032,6 +1032,22 @@ class AnimationTools:
|
|||||||
can_create_from_memory = True
|
can_create_from_memory = True
|
||||||
|
|
||||||
if can_create_from_memory:
|
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(owner_model);会污染当前场景节点,导致播放后模型消失/选择失效。
|
||||||
# 先用副本创建真实 Actor,只有失败时才退回 autoBind 代理。
|
# 先用副本创建真实 Actor,只有失败时才退回 autoBind 代理。
|
||||||
clone_parent = self._get_owner_parent_node(owner_model)
|
clone_parent = self._get_owner_parent_node(owner_model)
|
||||||
@ -1055,7 +1071,7 @@ class AnimationTools:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
# Actor 副本失败后,从多个候选节点中选择“带几何体”的 autoBind 源
|
# Actor 副本失败后,从多个候选节点中选择“带几何体”的 autoBind 源
|
||||||
for source_node in _collect_autobind_source_candidates():
|
for source_node in autobind_candidates:
|
||||||
mem_proxy = _try_create_autobind_proxy(
|
mem_proxy = _try_create_autobind_proxy(
|
||||||
source_node,
|
source_node,
|
||||||
f"内存模型({source_node.getName()})",
|
f"内存模型({source_node.getName()})",
|
||||||
@ -1084,9 +1100,25 @@ class AnimationTools:
|
|||||||
filepath = owner_model.getTag("original_path")
|
filepath = owner_model.getTag("original_path")
|
||||||
|
|
||||||
print(f"[Actor加载调试] 获取到的 filepath: '{filepath}'")
|
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:
|
if not filepath:
|
||||||
print(f"[Actor加载调试] filepath为空,触发 _try_memory_fallback()")
|
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绝对路径会导致加载彻底崩溃并返回空节点
|
# 针对Actor加载,必须使用Panda3D规范的Unix风格路径,否则Windows绝对路径会导致加载彻底崩溃并返回空节点
|
||||||
panda_specific_path = ""
|
panda_specific_path = ""
|
||||||
@ -1172,7 +1204,7 @@ class AnimationTools:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
# 所有创建路径失败时由内存加载进行兜底
|
# 所有创建路径失败时由内存加载进行兜底
|
||||||
return _try_memory_fallback()
|
return _try_memory_fallback(prefer_scene_proxy=prefer_scene_proxy)
|
||||||
|
|
||||||
|
|
||||||
def _getModelFormat(self, origin_model):
|
def _getModelFormat(self, origin_model):
|
||||||
|
|||||||
@ -41,6 +41,37 @@ class AppActions:
|
|||||||
|
|
||||||
return node
|
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):
|
def _toggle_hot_reload(self):
|
||||||
"""切换热重载状态"""
|
"""切换热重载状态"""
|
||||||
@ -1485,11 +1516,13 @@ class AppActions:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"项目资源导入失败,回退直接导入: {e}")
|
print(f"项目资源导入失败,回退直接导入: {e}")
|
||||||
|
|
||||||
|
animated_model = bool(file_path and self._model_file_has_animation(file_path))
|
||||||
|
|
||||||
command_manager = getattr(self, 'command_manager', None)
|
command_manager = getattr(self, 'command_manager', None)
|
||||||
if scene_package_import or not command_manager:
|
if scene_package_import or not command_manager:
|
||||||
return self._import_model_for_runtime(file_path, scene_package_import=scene_package_import)
|
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)
|
return self._execute_ssbo_import_command(file_path)
|
||||||
|
|
||||||
from core.Command_System import CreateNodeCommand
|
from core.Command_System import CreateNodeCommand
|
||||||
@ -1507,31 +1540,44 @@ class AppActions:
|
|||||||
SSBO mode: load via SSBOEditor always (regardless of prefer_scene_manager).
|
SSBO mode: load via SSBOEditor always (regardless of prefer_scene_manager).
|
||||||
Legacy mode: load via SceneManager.
|
Legacy mode: load via SceneManager.
|
||||||
"""
|
"""
|
||||||
if self.use_ssbo_mouse_picking and getattr(self, 'ssbo_editor', None):
|
animated_model = bool(file_path and self._model_file_has_animation(file_path))
|
||||||
try:
|
if animated_model:
|
||||||
# Clear selection/gizmo first to avoid dangling references to soon-to-be removed nodes.
|
prefer_scene_manager = True
|
||||||
if hasattr(self, 'selection') and self.selection:
|
ssbo_editor = getattr(self, 'ssbo_editor', None)
|
||||||
try:
|
if ssbo_editor and hasattr(ssbo_editor, "clear_selection"):
|
||||||
self.selection.clearSelection()
|
try:
|
||||||
except Exception:
|
ssbo_editor.clear_selection(sync_world_selection=False)
|
||||||
try:
|
except Exception:
|
||||||
self.selection.updateSelection(None)
|
pass
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
self.ssbo_editor.load_model(
|
if self.use_ssbo_mouse_picking and getattr(self, 'ssbo_editor', None):
|
||||||
file_path,
|
if animated_model:
|
||||||
keep_source_model=scene_package_import,
|
print(f"[AnimationImport] 检测到动画模型,跳过SSBO导入: {file_path}")
|
||||||
append=not scene_package_import,
|
else:
|
||||||
scene_package_import=scene_package_import,
|
try:
|
||||||
)
|
# Clear selection/gizmo first to avoid dangling references to soon-to-be removed nodes.
|
||||||
return self._refresh_ssbo_runtime_import_bindings(
|
if hasattr(self, 'selection') and self.selection:
|
||||||
file_path=file_path,
|
try:
|
||||||
scene_package_import=scene_package_import,
|
self.selection.clearSelection()
|
||||||
)
|
except Exception:
|
||||||
except Exception as e:
|
try:
|
||||||
print(f"[SSBO] load_model failed: {e}")
|
self.selection.updateSelection(None)
|
||||||
return 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
|
# Legacy fallback
|
||||||
if hasattr(self, 'scene_manager') and self.scene_manager:
|
if hasattr(self, 'scene_manager') and self.scene_manager:
|
||||||
|
|||||||
@ -647,17 +647,16 @@ class DialogPanels:
|
|||||||
)
|
)
|
||||||
self.style_manager.prepare_centered_dialog(460, 260)
|
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:
|
if not window.opened:
|
||||||
self.show_about_dialog = False
|
self.show_about_dialog = False
|
||||||
return
|
return
|
||||||
|
|
||||||
current_project_path = getattr(getattr(self, "project_manager", None), "current_project_path", None) or "未打开项目"
|
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.separator()
|
||||||
imgui.text(f"Python: {platform.python_version()}")
|
|
||||||
imgui.text(f"平台: {platform.system()} {platform.release()}")
|
imgui.text(f"平台: {platform.system()} {platform.release()}")
|
||||||
imgui.text(f"可执行文件: {os.path.basename(sys.executable)}")
|
imgui.text("可执行文件: MetaCore.exe")
|
||||||
imgui.separator()
|
imgui.separator()
|
||||||
imgui.text("当前项目:")
|
imgui.text("当前项目:")
|
||||||
imgui.text_colored((0.7, 0.7, 0.7, 1.0), current_project_path)
|
imgui.text_colored((0.7, 0.7, 0.7, 1.0), current_project_path)
|
||||||
|
|||||||
@ -439,7 +439,11 @@ class EditorPanelsLeftMixin:
|
|||||||
return
|
return
|
||||||
|
|
||||||
display = controller.display_names.get(key, key)
|
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"]
|
children = node_data["children"]
|
||||||
is_selected = (getattr(ssbo_editor, "selected_name", None) == key)
|
is_selected = (getattr(ssbo_editor, "selected_name", None) == key)
|
||||||
|
|
||||||
@ -484,7 +488,11 @@ class EditorPanelsLeftMixin:
|
|||||||
return
|
return
|
||||||
|
|
||||||
display = str(virtual_node.get("display_name", virtual_node.get("name", key)) or key)
|
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)
|
is_selected = (getattr(ssbo_editor, "selected_name", None) == key)
|
||||||
children = list((virtual_node.get("children", {}) or {}).values())
|
children = list((virtual_node.get("children", {}) or {}).values())
|
||||||
|
|
||||||
|
|||||||
@ -235,10 +235,21 @@ class EditorPanelsRightMixin(
|
|||||||
elif node_type == "光源":
|
elif node_type == "光源":
|
||||||
self._draw_property_section("光源", lambda: self.app._draw_light_properties(node), default_open=True)
|
self._draw_property_section("光源", lambda: self.app._draw_light_properties(node), default_open=True)
|
||||||
elif node_type == "模型":
|
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_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_collision_properties(node), default_open=False)
|
||||||
self._draw_property_section("操作", lambda: self.app._draw_property_actions(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 = {}
|
self._material_edit_sessions = {}
|
||||||
return 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):
|
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)
|
ensure_unique_fn = getattr(self.app, "_ensure_unique_materials_for_node", None)
|
||||||
if callable(ensure_unique_fn):
|
if callable(ensure_unique_fn):
|
||||||
try:
|
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:
|
if materials:
|
||||||
return materials
|
return materials
|
||||||
except Exception:
|
except Exception:
|
||||||
@ -110,11 +135,11 @@ class EditorPanelsRightMaterialMixin:
|
|||||||
def apply_primary_color(color):
|
def apply_primary_color(color):
|
||||||
for current_material in materials:
|
for current_material in materials:
|
||||||
self.app._set_material_base_color(current_material, color)
|
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:
|
if self.app._get_material_surface_type(current_material) == 3:
|
||||||
self.app._set_material_opacity(node, current_material, color[3])
|
self.app._set_material_opacity(node, current_material, color[3])
|
||||||
else:
|
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):
|
def apply_surface_type(surface_type):
|
||||||
for current_material in materials:
|
for current_material in materials:
|
||||||
|
|||||||
@ -50,6 +50,18 @@ class PanelDelegates:
|
|||||||
return source_node
|
return source_node
|
||||||
return self._get_selection_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):
|
def _get_selection_key(self):
|
||||||
ssbo_editor = getattr(self, "ssbo_editor", None)
|
ssbo_editor = getattr(self, "ssbo_editor", None)
|
||||||
if ssbo_editor and hasattr(ssbo_editor, "has_active_selection") and ssbo_editor.has_active_selection():
|
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):
|
def _apply_material_to_geom_states(self, *args, **kwargs):
|
||||||
return self.property_helpers._apply_material_to_geom_states(*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):
|
def _update_material_base_color(self, *args, **kwargs):
|
||||||
return self.property_helpers._update_material_base_color(*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_to_geom_states(node, material)
|
||||||
self._apply_material_surface_state(node, material)
|
self._apply_material_surface_state(node, material)
|
||||||
|
self._persist_material_tags(node, material)
|
||||||
|
|
||||||
self._clear_all_textures(node)
|
self._clear_all_textures(node)
|
||||||
texture_tags = node_state.get("textures", {}) or {}
|
texture_tags = node_state.get("textures", {}) or {}
|
||||||
@ -1148,31 +1149,68 @@ class PropertyHelpers:
|
|||||||
if not node or node.isEmpty() or material is None:
|
if not node or node.isEmpty() or material is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
target_geom_paths = self._get_geom_paths_for_material(node, material)
|
target_geom_paths = []
|
||||||
if not 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:
|
try:
|
||||||
if node.hasMaterial():
|
node.setMaterial(material, 1)
|
||||||
node.setMaterial(material, 1)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
try:
|
try:
|
||||||
target_geom_paths = [geom_path for geom_path in node.find_all_matches("**/+GeomNode")]
|
target_geom_paths = [geom_path for geom_path in node.find_all_matches("**/+GeomNode")]
|
||||||
except Exception:
|
except Exception:
|
||||||
target_geom_paths = []
|
target_geom_paths = []
|
||||||
|
else:
|
||||||
|
target_geom_paths = self._get_geom_paths_for_material(node, material)
|
||||||
if not target_geom_paths:
|
if not target_geom_paths:
|
||||||
self._invalidate_material_render_cache()
|
try:
|
||||||
return
|
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:
|
for geom_path in target_geom_paths:
|
||||||
try:
|
if not direct_geom_target:
|
||||||
geom_path.setMaterial(material, 1)
|
try:
|
||||||
except Exception:
|
geom_path.setMaterial(material, 1)
|
||||||
pass
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
geom_path.setState(geom_path.getState().setAttrib(MaterialAttrib.make(material)))
|
geom_path.setState(geom_path.getState().setAttrib(MaterialAttrib.make(material)))
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
geom_node = geom_path.node()
|
geom_node = geom_path.node()
|
||||||
for i in range(geom_node.getNumGeoms()):
|
for i in range(geom_node.getNumGeoms()):
|
||||||
@ -1189,6 +1227,8 @@ class PropertyHelpers:
|
|||||||
"""If editing a SSBO source node, rebuild the runtime proxy immediately."""
|
"""If editing a SSBO source node, rebuild the runtime proxy immediately."""
|
||||||
try:
|
try:
|
||||||
ssbo_editor = getattr(self, "ssbo_editor", None)
|
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():
|
if not ssbo_editor or not node or node.isEmpty():
|
||||||
return
|
return
|
||||||
if not hasattr(ssbo_editor, "is_source_tree_node") or not ssbo_editor.is_source_tree_node(node):
|
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
|
continue
|
||||||
self._apply_material_to_geom_states(node, current_material)
|
self._apply_material_to_geom_states(node, current_material)
|
||||||
self._apply_material_surface_state(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)
|
refresh_pipeline = getattr(self, "_refresh_pipeline_material_mode", None)
|
||||||
if callable(refresh_pipeline):
|
if callable(refresh_pipeline):
|
||||||
try:
|
try:
|
||||||
@ -1222,21 +1263,72 @@ class PropertyHelpers:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"同步材质显示状态失败: {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):
|
def _get_node_materials(self, node):
|
||||||
"""Return the editable materials currently used by a node."""
|
"""Return the editable materials currently used by a node."""
|
||||||
if not node or node.isEmpty():
|
if not node or node.isEmpty():
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
materials = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if node.hasMaterial():
|
if node.hasMaterial():
|
||||||
return [node.getMaterial()]
|
node_material = node.getMaterial()
|
||||||
|
if node_material is not None:
|
||||||
|
materials.append(node_material)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
materials = list(node.find_all_materials())
|
materials.extend(list(node.find_all_materials()))
|
||||||
except Exception:
|
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 = []
|
unique_materials = []
|
||||||
seen_keys = set()
|
seen_keys = set()
|
||||||
@ -1284,7 +1376,10 @@ class PropertyHelpers:
|
|||||||
if not node or node.isEmpty():
|
if not node or node.isEmpty():
|
||||||
return []
|
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:
|
if not materials:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@ -1298,8 +1393,12 @@ class PropertyHelpers:
|
|||||||
stored_signature = tuple(node.getPythonTag("_editable_material_signature"))
|
stored_signature = tuple(node.getPythonTag("_editable_material_signature"))
|
||||||
except Exception:
|
except Exception:
|
||||||
stored_signature = None
|
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
|
return materials
|
||||||
|
|
||||||
node_material_key = None
|
node_material_key = None
|
||||||
@ -1311,6 +1410,7 @@ class PropertyHelpers:
|
|||||||
|
|
||||||
changed = False
|
changed = False
|
||||||
cloned_materials = []
|
cloned_materials = []
|
||||||
|
direct_geom_target = self._is_geom_node_path(node)
|
||||||
|
|
||||||
for material in materials:
|
for material in materials:
|
||||||
if material is None:
|
if material is None:
|
||||||
@ -1324,31 +1424,54 @@ class PropertyHelpers:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
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)
|
node.setMaterial(cloned_material, 1)
|
||||||
changed = True
|
changed = True
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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:
|
if not geom_paths and node_material_key is None:
|
||||||
try:
|
try:
|
||||||
node.setMaterial(cloned_material, 1)
|
has_geom_descendants = bool(node.find_all_matches("**/+GeomNode"))
|
||||||
changed = True
|
|
||||||
except Exception:
|
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:
|
for geom_path in geom_paths:
|
||||||
try:
|
if not direct_geom_target:
|
||||||
geom_path.setMaterial(cloned_material, 1)
|
try:
|
||||||
changed = True
|
geom_path.setMaterial(cloned_material, 1)
|
||||||
except Exception:
|
changed = True
|
||||||
pass
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
geom_path.setState(geom_path.getState().setAttrib(MaterialAttrib.make(cloned_material)))
|
geom_path.setState(geom_path.getState().setAttrib(MaterialAttrib.make(cloned_material)))
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
geom_node = geom_path.node()
|
geom_node = geom_path.node()
|
||||||
@ -1378,6 +1501,10 @@ class PropertyHelpers:
|
|||||||
node.setPythonTag("_editable_material_signature", latest_signature)
|
node.setPythonTag("_editable_material_signature", latest_signature)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
try:
|
||||||
|
node.setPythonTag("_editable_material_isolated", bool(changed))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
return latest_materials
|
return latest_materials
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -1390,6 +1517,27 @@ class PropertyHelpers:
|
|||||||
except Exception:
|
except Exception:
|
||||||
return id(material)
|
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):
|
def _geom_uses_material(self, geom_path, material):
|
||||||
try:
|
try:
|
||||||
from panda3d.core import MaterialAttrib
|
from panda3d.core import MaterialAttrib
|
||||||
@ -1474,6 +1622,34 @@ class PropertyHelpers:
|
|||||||
print(f"创建默认材质失败: {e}")
|
print(f"创建默认材质失败: {e}")
|
||||||
return None
|
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):
|
def _get_material_surface_type(self, material):
|
||||||
"""Return the RenderPipeline shading model value used by this material."""
|
"""Return the RenderPipeline shading model value used by this material."""
|
||||||
try:
|
try:
|
||||||
@ -1800,6 +1976,8 @@ class PropertyHelpers:
|
|||||||
try:
|
try:
|
||||||
if not node or node.isEmpty():
|
if not node or node.isEmpty():
|
||||||
return []
|
return []
|
||||||
|
if self._is_geom_node_path(node):
|
||||||
|
return [node]
|
||||||
renderable_nodes = [match for match in node.find_all_matches("**/+GeomNode")]
|
renderable_nodes = [match for match in node.find_all_matches("**/+GeomNode")]
|
||||||
if renderable_nodes:
|
if renderable_nodes:
|
||||||
return renderable_nodes
|
return renderable_nodes
|
||||||
@ -1899,13 +2077,28 @@ class PropertyHelpers:
|
|||||||
opacity = self._get_material_opacity(material) if is_transparent else 1.0
|
opacity = self._get_material_opacity(material) if is_transparent else 1.0
|
||||||
base_color = self._get_material_transparent_base_color(material)
|
base_color = self._get_material_transparent_base_color(material)
|
||||||
|
|
||||||
target_nodes = self._get_geom_paths_for_material(node, material)
|
force_subtree_override = False
|
||||||
if not target_nodes:
|
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)
|
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:
|
for target_node in target_nodes:
|
||||||
target_material = material
|
target_material = material
|
||||||
if target_material is not None:
|
if target_material is not None and not direct_geom_target:
|
||||||
try:
|
try:
|
||||||
target_node.setMaterial(target_material, 1)
|
target_node.setMaterial(target_material, 1)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user