修复保存材质模型位置,保存场景树,加载场景树,
This commit is contained in:
parent
11ad2d5742
commit
4635458300
74
imgui.ini
74
imgui.ini
@ -24,34 +24,34 @@ Size=832,45
|
||||
Collapsed=0
|
||||
|
||||
[Window][工具栏]
|
||||
Pos=354,20
|
||||
Size=1334,32
|
||||
Pos=285,20
|
||||
Size=1190,62
|
||||
Collapsed=0
|
||||
DockId=0x0000000D,0
|
||||
DockId=0x00000009,0
|
||||
|
||||
[Window][场景树]
|
||||
Pos=0,20
|
||||
Size=352,748
|
||||
Size=283,506
|
||||
Collapsed=0
|
||||
DockId=0x00000007,0
|
||||
|
||||
[Window][属性面板]
|
||||
Pos=1690,20
|
||||
Size=358,748
|
||||
Pos=1477,20
|
||||
Size=443,1012
|
||||
Collapsed=0
|
||||
DockId=0x00000002,0
|
||||
DockId=0x0000000B,0
|
||||
|
||||
[Window][控制台]
|
||||
Pos=1690,20
|
||||
Size=358,748
|
||||
Pos=0,528
|
||||
Size=283,504
|
||||
Collapsed=0
|
||||
DockId=0x00000002,1
|
||||
DockId=0x00000008,0
|
||||
|
||||
[Window][脚本管理]
|
||||
Pos=1950,20
|
||||
Size=610,995
|
||||
Pos=1591,20
|
||||
Size=329,1012
|
||||
Collapsed=0
|
||||
DockId=0x00000002,2
|
||||
DockId=0x0000000B,1
|
||||
|
||||
[Window][中文显示测试]
|
||||
Pos=60,60
|
||||
@ -60,7 +60,7 @@ Collapsed=0
|
||||
|
||||
[Window][WindowOverViewport_11111111]
|
||||
Pos=0,20
|
||||
Size=2048,1084
|
||||
Size=1920,1012
|
||||
Collapsed=0
|
||||
|
||||
[Window][测试窗口1]
|
||||
@ -79,17 +79,17 @@ Size=93,65
|
||||
Collapsed=0
|
||||
|
||||
[Window][新建项目]
|
||||
Pos=824,402
|
||||
Pos=760,366
|
||||
Size=400,300
|
||||
Collapsed=0
|
||||
|
||||
[Window][选择路径]
|
||||
Pos=660,254
|
||||
Pos=660,266
|
||||
Size=600,500
|
||||
Collapsed=0
|
||||
|
||||
[Window][打开项目]
|
||||
Pos=710,304
|
||||
Pos=710,316
|
||||
Size=500,400
|
||||
Collapsed=0
|
||||
|
||||
@ -99,10 +99,10 @@ Size=600,500
|
||||
Collapsed=0
|
||||
|
||||
[Window][资源管理器]
|
||||
Pos=0,770
|
||||
Size=2048,334
|
||||
Pos=285,727
|
||||
Size=1190,305
|
||||
Collapsed=0
|
||||
DockId=0x00000006,0
|
||||
DockId=0x00000004,0
|
||||
|
||||
[Window][创建3D文本]
|
||||
Pos=60,60
|
||||
@ -135,7 +135,7 @@ Size=89,250
|
||||
Collapsed=0
|
||||
|
||||
[Window][颜色选择器]
|
||||
Pos=874,352
|
||||
Pos=810,304
|
||||
Size=300,400
|
||||
Collapsed=0
|
||||
|
||||
@ -150,10 +150,10 @@ Size=101,226
|
||||
Collapsed=0
|
||||
|
||||
[Window][LUI编辑器]
|
||||
Pos=1690,20
|
||||
Size=358,748
|
||||
Pos=1477,749
|
||||
Size=443,283
|
||||
Collapsed=0
|
||||
DockId=0x00000002,2
|
||||
DockId=0x0000000C,0
|
||||
|
||||
[Window][LUI测试控制面板]
|
||||
Pos=6,10
|
||||
@ -204,21 +204,25 @@ Collapsed=0
|
||||
Pos=1438,20
|
||||
Size=610,748
|
||||
Collapsed=0
|
||||
DockId=0x00000002,2
|
||||
DockId=0x0000000B,2
|
||||
|
||||
[Window][项目另存为]
|
||||
Pos=794,432
|
||||
Pos=730,396
|
||||
Size=460,240
|
||||
Collapsed=0
|
||||
|
||||
[Docking][Data]
|
||||
DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=2048,1084 Split=Y
|
||||
DockNode ID=0x00000005 Parent=0x08BD597D SizeRef=2560,995 Split=X
|
||||
DockNode ID=0x00000007 Parent=0x00000005 SizeRef=352,1084 Selected=0xE0015051
|
||||
DockNode ID=0x00000008 Parent=0x00000005 SizeRef=2206,1084 Split=X
|
||||
DockNode ID=0x00000001 Parent=0x00000008 SizeRef=1846,989 Split=Y
|
||||
DockNode ID=0x0000000D Parent=0x00000001 SizeRef=1318,32 HiddenTabBar=1 Selected=0x43A39006
|
||||
DockNode ID=0x0000000E Parent=0x00000001 SizeRef=1318,961 CentralNode=1
|
||||
DockNode ID=0x00000002 Parent=0x00000008 SizeRef=358,989 Selected=0x5DB6FF37
|
||||
DockNode ID=0x00000006 Parent=0x08BD597D SizeRef=2560,334 Selected=0x3A2E05C3
|
||||
DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=1920,1012 Split=X
|
||||
DockNode ID=0x00000005 Parent=0x08BD597D SizeRef=283,1012 Split=Y Selected=0xE0015051
|
||||
DockNode ID=0x00000007 Parent=0x00000005 SizeRef=283,506 Selected=0xE0015051
|
||||
DockNode ID=0x00000008 Parent=0x00000005 SizeRef=283,504 Selected=0x5428E753
|
||||
DockNode ID=0x00000006 Parent=0x08BD597D SizeRef=1635,1012 Split=X
|
||||
DockNode ID=0x00000001 Parent=0x00000006 SizeRef=1190,989 Split=Y
|
||||
DockNode ID=0x00000003 Parent=0x00000001 SizeRef=1321,666 Split=Y
|
||||
DockNode ID=0x00000009 Parent=0x00000003 SizeRef=1304,62 Selected=0x43A39006
|
||||
DockNode ID=0x0000000A Parent=0x00000003 SizeRef=1304,618 CentralNode=1
|
||||
DockNode ID=0x00000004 Parent=0x00000001 SizeRef=1321,305 Selected=0x3A2E05C3
|
||||
DockNode ID=0x00000002 Parent=0x00000006 SizeRef=443,989 Split=Y Selected=0x5DB6FF37
|
||||
DockNode ID=0x0000000B Parent=0x00000002 SizeRef=443,727 Selected=0x5DB6FF37
|
||||
DockNode ID=0x0000000C Parent=0x00000002 SizeRef=443,283 Selected=0x1EB923B7
|
||||
|
||||
|
||||
9
main.py
9
main.py
@ -903,7 +903,14 @@ class MyWorld(PanelDelegates, CoreWorld):
|
||||
viewport = imgui.get_main_viewport()
|
||||
dock_flags = imgui.DockNodeFlags_.passthru_central_node
|
||||
if imgui_internal is not None:
|
||||
dock_flags |= imgui_internal.DockNodeFlagsPrivate_.no_window_menu_button
|
||||
# imgui_bundle不同版本下私有Dock标志位的枚举类型可能不兼容
|
||||
# (直接位或会在Python 3.11的Flag枚举里触发异常)
|
||||
try:
|
||||
private_flag = imgui_internal.DockNodeFlagsPrivate_.no_window_menu_button
|
||||
dock_flags = imgui.DockNodeFlags_(int(dock_flags) | int(private_flag))
|
||||
except Exception:
|
||||
# 回退:忽略私有标志,不影响DockSpace主流程
|
||||
pass
|
||||
imgui.dock_space_over_viewport(0, viewport, dock_flags)
|
||||
|
||||
# 在第一帧应用样式
|
||||
|
||||
@ -12,7 +12,7 @@ from pathlib import Path
|
||||
from panda3d.core import (
|
||||
ModelPool, ModelRoot, Filename, NodePath, GeomNode, Material, Vec4, Vec3,
|
||||
MaterialAttrib, ColorAttrib, Point3, CollisionNode, CollisionSphere, CollisionBox,
|
||||
BitMask32, TransparencyAttrib, LColor, TransformState, RenderModeAttrib, ShaderAttrib
|
||||
BitMask32, TransparencyAttrib, LColor, TransformState, RenderModeAttrib, ShaderAttrib, TextureAttrib
|
||||
)
|
||||
from panda3d.egg import EggData, EggVertexPool
|
||||
from direct.actor.Actor import Actor
|
||||
@ -261,6 +261,23 @@ class SceneManagerIOMixin:
|
||||
all_nodes.extend(self.Spotlight)
|
||||
all_nodes.extend(self.Pointlight)
|
||||
|
||||
# SSBO模式下先把运行时编辑后的顶层变换同步回source_model_root,
|
||||
# 再从source树保存,避免把chunk_*运行时结构写入scene.bam。
|
||||
ssbo_editor = getattr(self.world, "ssbo_editor", None)
|
||||
if ssbo_editor:
|
||||
snapshot_fn = getattr(ssbo_editor, "_snapshot_top_level_transforms_to_source_root", None)
|
||||
if callable(snapshot_fn):
|
||||
try:
|
||||
snapshot_fn()
|
||||
except Exception as e:
|
||||
print(f"同步SSBO源场景树变换失败: {e}")
|
||||
snapshot_material_fn = getattr(ssbo_editor, "_snapshot_runtime_materials_to_source_root", None)
|
||||
if callable(snapshot_material_fn):
|
||||
try:
|
||||
snapshot_material_fn()
|
||||
except Exception as e:
|
||||
print(f"同步SSBO源场景树材质失败: {e}")
|
||||
|
||||
def expand_scene_package_wrappers(nodes):
|
||||
expanded_nodes = []
|
||||
ssbo_editor = getattr(self.world, "ssbo_editor", None)
|
||||
@ -307,12 +324,90 @@ class SceneManagerIOMixin:
|
||||
expanded_nodes.extend(source_children)
|
||||
continue
|
||||
|
||||
print(f"SSBO场景节点 {node.getName()} 缺少原始场景树,回退为包装根节点保存")
|
||||
runtime_children = []
|
||||
for child in node.getChildren():
|
||||
try:
|
||||
if not child or child.isEmpty():
|
||||
continue
|
||||
if child.getName() in {"render", "render2d", "aspect2d"}:
|
||||
continue
|
||||
runtime_children.append(child)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if runtime_children:
|
||||
print(f"展开SSBO场景节点 {node.getName()} -> 回退使用运行时场景树的 {len(runtime_children)} 个顶层子节点")
|
||||
expanded_nodes.extend(runtime_children)
|
||||
continue
|
||||
|
||||
print(f"SSBO场景节点 {node.getName()} 缺少可用场景树,回退为包装根节点保存")
|
||||
expanded_nodes.append(node)
|
||||
return expanded_nodes
|
||||
|
||||
all_nodes = expand_scene_package_wrappers(all_nodes)
|
||||
|
||||
def get_material_vector(material, *accessors):
|
||||
for accessor_name in accessors:
|
||||
accessor = getattr(material, accessor_name, None)
|
||||
try:
|
||||
value = accessor() if callable(accessor) else accessor
|
||||
except Exception:
|
||||
continue
|
||||
if value is None:
|
||||
continue
|
||||
try:
|
||||
return Vec4(
|
||||
float(value.x),
|
||||
float(value.y),
|
||||
float(value.z),
|
||||
float(getattr(value, "w", 1.0)),
|
||||
)
|
||||
except Exception:
|
||||
continue
|
||||
return None
|
||||
|
||||
def get_material_scalar(material, *accessors):
|
||||
for accessor_name in accessors:
|
||||
accessor = getattr(material, accessor_name, None)
|
||||
try:
|
||||
value = accessor() if callable(accessor) else accessor
|
||||
except Exception:
|
||||
continue
|
||||
if value is None:
|
||||
continue
|
||||
try:
|
||||
return float(value)
|
||||
except Exception:
|
||||
continue
|
||||
return None
|
||||
|
||||
def save_material_tags(node, material):
|
||||
if not material:
|
||||
return
|
||||
|
||||
vector_tag_map = {
|
||||
"material_ambient": ("getAmbient", "get_ambient", "ambient"),
|
||||
"material_diffuse": ("getDiffuse", "get_diffuse", "diffuse"),
|
||||
"material_specular": ("getSpecular", "get_specular", "specular"),
|
||||
"material_emission": ("getEmission", "get_emission", "emission"),
|
||||
"material_basecolor": ("getBaseColor", "get_base_color", "base_color"),
|
||||
}
|
||||
scalar_tag_map = {
|
||||
"material_shininess": ("getShininess", "get_shininess", "shininess"),
|
||||
"material_roughness": ("getRoughness", "get_roughness", "roughness"),
|
||||
"material_metallic": ("getMetallic", "get_metallic", "metallic"),
|
||||
"material_ior": ("getRefractiveIndex", "get_refractive_index", "refractive_index"),
|
||||
}
|
||||
|
||||
for tag_name, accessors in vector_tag_map.items():
|
||||
value = get_material_vector(material, *accessors)
|
||||
if value is not None:
|
||||
node.setTag(tag_name, str(value))
|
||||
|
||||
for tag_name, accessors in scalar_tag_map.items():
|
||||
value = get_material_scalar(material, *accessors)
|
||||
if value is not None:
|
||||
node.setTag(tag_name, str(value))
|
||||
|
||||
# 创建用于保存GUI信息的JSON文件路径
|
||||
gui_info_file = filename.replace('.bam', '_gui.json')
|
||||
@ -354,18 +449,18 @@ class SceneManagerIOMixin:
|
||||
state = node.getState()
|
||||
|
||||
# 如果有材质属性,保存为标签
|
||||
material = None
|
||||
if state.hasAttrib(MaterialAttrib.getClassType()):
|
||||
mat_attrib = state.getAttrib(MaterialAttrib.getClassType())
|
||||
material = mat_attrib.getMaterial()
|
||||
if material:
|
||||
# 保存材质属性到标签
|
||||
node.setTag("material_ambient", str(material.getAmbient()))
|
||||
node.setTag("material_diffuse", str(material.getDiffuse()))
|
||||
node.setTag("material_specular", str(material.getSpecular()))
|
||||
node.setTag("material_emission", str(material.getEmission()))
|
||||
node.setTag("material_shininess", str(material.getShininess()))
|
||||
if material.hasBaseColor():
|
||||
node.setTag("material_basecolor", str(material.getBaseColor()))
|
||||
material = mat_attrib.getMaterial() if mat_attrib else None
|
||||
if material is None:
|
||||
try:
|
||||
if node.hasMaterial():
|
||||
material = node.getMaterial()
|
||||
except Exception:
|
||||
material = None
|
||||
if material:
|
||||
save_material_tags(node, material)
|
||||
|
||||
# 保存特定类型节点的额外信息
|
||||
if node.hasTag("light_type"):
|
||||
@ -453,6 +548,31 @@ class SceneManagerIOMixin:
|
||||
|
||||
def strip_runtime_render_state(root_np):
|
||||
for current in [root_np] + list(root_np.findAllMatches("**")):
|
||||
try:
|
||||
keep_material_shader = any(
|
||||
current.hasTag(tag_name)
|
||||
for tag_name in (
|
||||
"material_effect_metallic_enabled",
|
||||
"material_effect_default_texture_enabled",
|
||||
"material_effect_parallax_enabled",
|
||||
"material_render_effect_signature",
|
||||
"material_texture_diffuse",
|
||||
"material_texture_normal",
|
||||
"material_texture_ior",
|
||||
"material_texture_roughness",
|
||||
"material_texture_parallax",
|
||||
"material_texture_metallic",
|
||||
"material_texture_emission",
|
||||
"material_texture_ao",
|
||||
"material_texture_alpha",
|
||||
"material_texture_detail",
|
||||
"material_texture_gloss",
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
keep_material_shader = False
|
||||
if keep_material_shader:
|
||||
continue
|
||||
try:
|
||||
current.clearShader()
|
||||
except Exception:
|
||||
@ -814,6 +934,218 @@ class SceneManagerIOMixin:
|
||||
print(f"[SSBO] 统一导入失败,回退旧流程: {e}")
|
||||
use_ssbo_scene_import = False
|
||||
|
||||
def parse_saved_color(color_str, default=None):
|
||||
try:
|
||||
cleaned = str(color_str).strip()
|
||||
for prefix in ("LVecBase4f", "Vec4", "LColor"):
|
||||
cleaned = cleaned.replace(prefix, "")
|
||||
cleaned = cleaned.strip("() ")
|
||||
components = [component.strip() for component in cleaned.split(",") if component.strip()]
|
||||
if len(components) == 3:
|
||||
components.append("1")
|
||||
if len(components) != 4:
|
||||
raise ValueError(f"invalid color component count: {cleaned}")
|
||||
return Vec4(*map(float, components[:4]))
|
||||
except Exception:
|
||||
return default if default is not None else Vec4(1, 1, 1, 1)
|
||||
|
||||
def parse_saved_scalar(value):
|
||||
try:
|
||||
return float(value)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def set_material_scalar(material, value, *setter_names):
|
||||
if value is None:
|
||||
return False
|
||||
for setter_name in setter_names:
|
||||
setter = getattr(material, setter_name, None)
|
||||
if callable(setter):
|
||||
setter(float(value))
|
||||
return True
|
||||
return False
|
||||
|
||||
def resolve_existing_material(node_path):
|
||||
try:
|
||||
if node_path.hasMaterial():
|
||||
material = node_path.getMaterial()
|
||||
if material is not None:
|
||||
return material
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
node_state = node_path.getState()
|
||||
if node_state.hasAttrib(MaterialAttrib.getClassType()):
|
||||
material_attrib = node_state.getAttrib(MaterialAttrib.getClassType())
|
||||
material = material_attrib.getMaterial() if material_attrib else None
|
||||
if material is not None:
|
||||
return material
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
geom_paths = []
|
||||
try:
|
||||
if isinstance(node_path.node(), GeomNode):
|
||||
geom_paths.append(node_path)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
geom_paths.extend(list(node_path.findAllMatches("**/+GeomNode")))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
seen_paths = set()
|
||||
for geom_path in geom_paths:
|
||||
try:
|
||||
key = geom_path.getKey()
|
||||
except Exception:
|
||||
key = id(geom_path)
|
||||
if key in seen_paths:
|
||||
continue
|
||||
seen_paths.add(key)
|
||||
|
||||
try:
|
||||
if geom_path.hasMaterial():
|
||||
material = geom_path.getMaterial()
|
||||
if material is not None:
|
||||
return material
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
geom_node = geom_path.node()
|
||||
for geom_index in range(geom_node.getNumGeoms()):
|
||||
geom_state = geom_node.getGeomState(geom_index)
|
||||
if not geom_state.hasAttrib(MaterialAttrib.getClassType()):
|
||||
continue
|
||||
material_attrib = geom_state.getAttrib(MaterialAttrib.getClassType())
|
||||
material = material_attrib.getMaterial() if material_attrib else None
|
||||
if material is not None:
|
||||
return material
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
net_state = geom_path.getNetState()
|
||||
if net_state.hasAttrib(MaterialAttrib.getClassType()):
|
||||
material_attrib = net_state.getAttrib(MaterialAttrib.getClassType())
|
||||
material = material_attrib.getMaterial() if material_attrib else None
|
||||
if material is not None:
|
||||
return material
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
def restore_saved_material_fallback(node_path):
|
||||
material_tag_names = (
|
||||
"material_ambient",
|
||||
"material_diffuse",
|
||||
"material_specular",
|
||||
"material_emission",
|
||||
"material_shininess",
|
||||
"material_basecolor",
|
||||
"material_roughness",
|
||||
"material_metallic",
|
||||
"material_ior",
|
||||
)
|
||||
if not any(node_path.hasTag(tag_name) for tag_name in material_tag_names):
|
||||
return False
|
||||
|
||||
# BAM already carries the real material object; tags are only a legacy fallback.
|
||||
if resolve_existing_material(node_path) is not None:
|
||||
return False
|
||||
|
||||
material = Material()
|
||||
material_changed = False
|
||||
|
||||
if node_path.hasTag("material_basecolor"):
|
||||
base_color = parse_saved_color(node_path.getTag("material_basecolor"))
|
||||
try:
|
||||
if hasattr(material, "set_base_color"):
|
||||
material.set_base_color(base_color)
|
||||
elif hasattr(material, "setBaseColor"):
|
||||
material.setBaseColor(base_color)
|
||||
elif hasattr(material, "setDiffuse"):
|
||||
material.setDiffuse(base_color)
|
||||
material_changed = True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if node_path.hasTag("material_emission"):
|
||||
emission = parse_saved_color(node_path.getTag("material_emission"))
|
||||
try:
|
||||
if hasattr(material, "set_emission"):
|
||||
material.set_emission(emission)
|
||||
material_changed = True
|
||||
elif hasattr(material, "setEmission"):
|
||||
material.setEmission(emission)
|
||||
material_changed = True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if set_material_scalar(
|
||||
material,
|
||||
parse_saved_scalar(node_path.getTag("material_roughness")) if node_path.hasTag("material_roughness") else None,
|
||||
"set_roughness",
|
||||
"setRoughness",
|
||||
):
|
||||
material_changed = True
|
||||
|
||||
if set_material_scalar(
|
||||
material,
|
||||
parse_saved_scalar(node_path.getTag("material_metallic")) if node_path.hasTag("material_metallic") else None,
|
||||
"set_metallic",
|
||||
"setMetallic",
|
||||
):
|
||||
material_changed = True
|
||||
|
||||
if set_material_scalar(
|
||||
material,
|
||||
parse_saved_scalar(node_path.getTag("material_ior")) if node_path.hasTag("material_ior") else None,
|
||||
"set_refractive_index",
|
||||
"setRefractiveIndex",
|
||||
):
|
||||
material_changed = True
|
||||
|
||||
has_modern_pbr_tags = any(
|
||||
node_path.hasTag(tag_name)
|
||||
for tag_name in (
|
||||
"material_basecolor",
|
||||
"material_roughness",
|
||||
"material_metallic",
|
||||
"material_ior",
|
||||
)
|
||||
)
|
||||
|
||||
if not has_modern_pbr_tags:
|
||||
if node_path.hasTag("material_ambient"):
|
||||
material.setAmbient(parse_saved_color(node_path.getTag("material_ambient")))
|
||||
material_changed = True
|
||||
|
||||
if node_path.hasTag("material_diffuse"):
|
||||
material.setDiffuse(parse_saved_color(node_path.getTag("material_diffuse")))
|
||||
material_changed = True
|
||||
|
||||
if node_path.hasTag("material_specular"):
|
||||
material.setSpecular(parse_saved_color(node_path.getTag("material_specular")))
|
||||
material_changed = True
|
||||
|
||||
if set_material_scalar(
|
||||
material,
|
||||
parse_saved_scalar(node_path.getTag("material_shininess")) if node_path.hasTag("material_shininess") else None,
|
||||
"set_shininess",
|
||||
"setShininess",
|
||||
):
|
||||
material_changed = True
|
||||
|
||||
if material_changed:
|
||||
node_path.setMaterial(material, 1)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
# 遍历场景中的所有节点
|
||||
def processNode(nodePath, depth=0):
|
||||
indent = " " * depth
|
||||
@ -990,51 +1322,14 @@ class SceneManagerIOMixin:
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
# 恢复材质属性
|
||||
def parseColor(color_str):
|
||||
"""解析颜色字符串为Vec4"""
|
||||
try:
|
||||
color_str = color_str.replace('LVecBase4f', '').strip('()')
|
||||
r, g, b, a = map(float, color_str.split(','))
|
||||
return Vec4(r, g, b, a)
|
||||
except:
|
||||
return Vec4(1, 1, 1, 1)
|
||||
|
||||
if not is_model_root:
|
||||
# 创建并恢复材质
|
||||
material = Material()
|
||||
material_changed = False
|
||||
|
||||
if nodePath.hasTag("material_ambient"):
|
||||
material.setAmbient(parseColor(nodePath.getTag("material_ambient")))
|
||||
material_changed = True
|
||||
|
||||
if nodePath.hasTag("material_diffuse"):
|
||||
material.setDiffuse(parseColor(nodePath.getTag("material_diffuse")))
|
||||
material_changed = True
|
||||
|
||||
if nodePath.hasTag("material_specular"):
|
||||
material.setSpecular(parseColor(nodePath.getTag("material_specular")))
|
||||
material_changed = True
|
||||
|
||||
if nodePath.hasTag("material_emission"):
|
||||
material.setEmission(parseColor(nodePath.getTag("material_emission")))
|
||||
material_changed = True
|
||||
|
||||
if nodePath.hasTag("material_shininess"):
|
||||
material.setShininess(float(nodePath.getTag("material_shininess")))
|
||||
material_changed = True
|
||||
|
||||
if nodePath.hasTag("material_basecolor"):
|
||||
material.setBaseColor(parseColor(nodePath.getTag("material_basecolor")))
|
||||
material_changed = True
|
||||
|
||||
if material_changed:
|
||||
nodePath.setMaterial(material)
|
||||
# SSBO场景包导入时,禁止旧的标签材质回放,
|
||||
# 避免覆盖BAM内真实PBR/贴图状态导致发黑或金属贴图丢失。
|
||||
if not use_ssbo_scene_import:
|
||||
restore_saved_material_fallback(nodePath)
|
||||
|
||||
# 恢复颜色属性
|
||||
if nodePath.hasTag("color"):
|
||||
nodePath.setColor(parseColor(nodePath.getTag("color")))
|
||||
nodePath.setColor(parse_saved_color(nodePath.getTag("color")))
|
||||
|
||||
# 处理特定类型的节点
|
||||
if nodePath.hasTag("light_type"):
|
||||
@ -1125,6 +1420,90 @@ class SceneManagerIOMixin:
|
||||
print("\n开始重建父子关系...")
|
||||
self._rebuildParentChildRelationships(loaded_nodes)
|
||||
|
||||
# 重新应用材质渲染effect(尤其是金属性贴图所需的pbr_with_metallic)
|
||||
# 标签会从BAM读取,但effect不会自动重建,导致重开后金属性样式丢失。
|
||||
try:
|
||||
property_helpers = getattr(self.world, "property_helpers", None)
|
||||
sync_effect_fn = getattr(property_helpers, "_sync_material_render_effect", None) if property_helpers else None
|
||||
if callable(sync_effect_fn):
|
||||
effect_synced = 0
|
||||
effect_nodes = []
|
||||
ssbo_controller = None
|
||||
if use_ssbo_scene_import:
|
||||
ssbo_editor = getattr(self.world, "ssbo_editor", None)
|
||||
controller = getattr(ssbo_editor, "controller", None) if ssbo_editor else None
|
||||
ssbo_controller = controller
|
||||
if controller:
|
||||
# SSBO模式优先对运行时对象重建effect(真实显示对象)。
|
||||
effect_nodes = list(controller.id_to_object_np.values())
|
||||
if not effect_nodes:
|
||||
effect_nodes = list(loaded_nodes.values())
|
||||
|
||||
def _has_metallic_texture(np):
|
||||
try:
|
||||
stages = np.findAllTextureStages()
|
||||
for si in range(stages.getNumTextureStages()):
|
||||
stage = stages.getTextureStage(si)
|
||||
if not stage:
|
||||
continue
|
||||
name = (stage.getName() or "").lower()
|
||||
if "metallic" in name:
|
||||
return True
|
||||
try:
|
||||
if int(stage.getSort()) == 5:
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
|
||||
for node in effect_nodes:
|
||||
if not node or node.isEmpty():
|
||||
continue
|
||||
# 关键修复:按对象自身纹理槽决定 effect,避免根节点标签导致整组对象统一金属性/发黑
|
||||
has_metallic = _has_metallic_texture(node) or node.hasTag("material_effect_metallic_enabled")
|
||||
has_any_effect = any(
|
||||
node.hasTag(tag_name)
|
||||
for tag_name in (
|
||||
"material_effect_default_texture_enabled",
|
||||
"material_effect_parallax_enabled",
|
||||
"material_render_effect_signature",
|
||||
)
|
||||
)
|
||||
has_effect_tags = has_metallic or has_any_effect
|
||||
if not has_effect_tags:
|
||||
continue
|
||||
try:
|
||||
if has_metallic:
|
||||
node.setTag("material_effect_metallic_enabled", "1")
|
||||
elif node.hasTag("material_effect_metallic_enabled"):
|
||||
node.clearTag("material_effect_metallic_enabled")
|
||||
sync_effect_fn(node, force=True, source_node=node)
|
||||
effect_synced += 1
|
||||
except Exception:
|
||||
continue
|
||||
if effect_synced:
|
||||
print(f"[SceneLoad] 已重建材质effect节点数量: {effect_synced}")
|
||||
# 关键修复:effect应用在动态对象后,需要重新烘焙静态chunk。
|
||||
# 否则未选中时看到的仍是旧静态副本,点击切到动态对象才会正常。
|
||||
if use_ssbo_scene_import and ssbo_controller and effect_synced:
|
||||
rebuilt_chunks = 0
|
||||
try:
|
||||
for chunk_id in sorted(ssbo_controller.chunks):
|
||||
try:
|
||||
ssbo_controller._rebuild_static_chunk(chunk_id)
|
||||
ssbo_controller._set_chunk_dynamic(chunk_id, False)
|
||||
rebuilt_chunks += 1
|
||||
except Exception:
|
||||
continue
|
||||
if rebuilt_chunks:
|
||||
print(f"[SceneLoad] 已重建静态chunk数量: {rebuilt_chunks}")
|
||||
except Exception as e:
|
||||
print(f"[SceneLoad] 重建静态chunk失败: {e}")
|
||||
except Exception as e:
|
||||
print(f"[SceneLoad] 重建材质effect失败: {e}")
|
||||
|
||||
# 加载GUI信息并重新创建非3D的GUI元素
|
||||
gui_info_file = filename.replace('.bam', '_gui.json')
|
||||
if os.path.exists(gui_info_file):
|
||||
|
||||
@ -44,6 +44,7 @@ class ObjectController:
|
||||
self.id_to_chunk = {} # global_id -> chunk_id
|
||||
self.id_to_object_np = {} # global_id -> dynamic object nodepath
|
||||
self.id_to_pick_np = {} # global_id -> pick-scene nodepath
|
||||
self.id_to_geom_index = {} # global_id -> owner GeomNode geom index
|
||||
|
||||
# chunk_id -> {
|
||||
# "dynamic_np": NodePath,
|
||||
@ -225,10 +226,21 @@ class ObjectController:
|
||||
return False
|
||||
|
||||
name = (node["name"] or "").strip().lower()
|
||||
if name != "root":
|
||||
return False
|
||||
if name == "root":
|
||||
return len(node["children"]) > 0
|
||||
|
||||
return len(node["children"]) > 0
|
||||
# Hide scene-package/runtime wrappers in virtual tree:
|
||||
# - scene.bam container
|
||||
# - chunk_* dynamic/static batching nodes
|
||||
# - modelCollision_* helper nodes
|
||||
if name.endswith(".bam") and len(node["children"]) > 0:
|
||||
return True
|
||||
if name.startswith("chunk_"):
|
||||
return True
|
||||
if name.startswith("modelcollision_"):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _encode_id_color(self, vdata, object_id):
|
||||
if not vdata.has_column("color"):
|
||||
@ -274,6 +286,28 @@ class ObjectController:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _copy_node_tags(self, src_np, dst_np):
|
||||
"""Copy all string tags from source node to rebuilt runtime node."""
|
||||
if not src_np or not dst_np:
|
||||
return
|
||||
try:
|
||||
for tag_name in src_np.get_tag_keys():
|
||||
try:
|
||||
dst_np.set_tag(tag_name, src_np.get_tag(tag_name))
|
||||
except Exception:
|
||||
continue
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
for tag_name in src_np.getTagKeys():
|
||||
try:
|
||||
dst_np.setTag(tag_name, src_np.getTag(tag_name))
|
||||
except Exception:
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def bake_ids_and_collect(self, model):
|
||||
"""
|
||||
Bake IDs into vertex colors, flatten, then build vertex index.
|
||||
@ -513,7 +547,10 @@ class ObjectController:
|
||||
static_np = chunk["dynamic_np"].copy_to(self.model)
|
||||
static_np.set_name(f"chunk_{chunk_id:04d}_static")
|
||||
static_np.unstash()
|
||||
static_np.flatten_strong()
|
||||
# Keep the static representation as a per-object copy instead of flattening.
|
||||
# Even medium flattening can still collapse or rewrite per-object PBR/effect
|
||||
# state after save/load, which manifests as black materials until selection
|
||||
# switches back to the dynamic objects.
|
||||
|
||||
chunk["static_np"] = static_np
|
||||
chunk["dirty"] = False
|
||||
@ -760,8 +797,10 @@ class ObjectController:
|
||||
for chunk_id in list(self.active_chunks):
|
||||
if chunk_id in target_chunks:
|
||||
continue
|
||||
if self.chunks[chunk_id]["dirty"]:
|
||||
self._rebuild_static_chunk(chunk_id)
|
||||
# Always rebuild static chunk when leaving dynamic edit mode.
|
||||
# Material/texture edits may not set `dirty`, but still need to
|
||||
# propagate from dynamic objects to static representation.
|
||||
self._rebuild_static_chunk(chunk_id)
|
||||
self._set_chunk_dynamic(chunk_id, False)
|
||||
|
||||
# Promote target chunks.
|
||||
@ -795,25 +834,48 @@ class ObjectController:
|
||||
owner_key = self._path_to_tree_key.get(str(np), self.tree_root_key)
|
||||
|
||||
world_mat = LMatrix4f(np.get_mat(model))
|
||||
# Preserve the inherited render state, not just the local node state.
|
||||
# Scene/package reload often stores material textures/effects on parent
|
||||
# nodes; using only local state drops those bindings and makes rebuilt
|
||||
# chunk_* runtime objects render black after reopening a project.
|
||||
try:
|
||||
node_state = np.get_net_state()
|
||||
except Exception:
|
||||
try:
|
||||
node_state = np.getNetState()
|
||||
except Exception:
|
||||
try:
|
||||
node_state = np.get_state()
|
||||
except Exception:
|
||||
try:
|
||||
node_state = np.getState()
|
||||
except Exception:
|
||||
node_state = None
|
||||
|
||||
for gi in range(gnode.get_num_geoms()):
|
||||
# Render geometry stays untouched (keep original material/color behavior).
|
||||
render_geom = gnode.get_geom(gi).make_copy()
|
||||
render_gnode = GeomNode(f"obj_{global_id}")
|
||||
render_gnode.add_geom(render_geom, gnode.get_geom_state(gi))
|
||||
geom_state = gnode.get_geom_state(gi)
|
||||
try:
|
||||
merged_state = node_state.compose(geom_state) if node_state is not None else geom_state
|
||||
except Exception:
|
||||
merged_state = geom_state
|
||||
render_gnode.add_geom(render_geom, merged_state)
|
||||
|
||||
# Picking geometry gets encoded ID in vertex color.
|
||||
pick_geom = gnode.get_geom(gi).make_copy()
|
||||
pick_vdata = pick_geom.modify_vertex_data()
|
||||
self._encode_id_color(pick_vdata, global_id)
|
||||
pick_gnode = GeomNode(f"pick_{global_id}")
|
||||
pick_gnode.add_geom(pick_geom, gnode.get_geom_state(gi))
|
||||
pick_gnode.add_geom(pick_geom, merged_state)
|
||||
|
||||
world_pos = world_mat.get_row3(3)
|
||||
chunk_id, chunk = self._allocate_spatial_chunk(scene_root, world_pos)
|
||||
|
||||
obj_np = chunk["dynamic_np"].attach_new_node(render_gnode)
|
||||
obj_np.set_mat(world_mat)
|
||||
self._copy_node_tags(np, obj_np)
|
||||
pick_np = pick_root.attach_new_node(pick_gnode)
|
||||
pick_np.set_mat(world_mat)
|
||||
|
||||
@ -821,6 +883,7 @@ class ObjectController:
|
||||
self.id_to_chunk[global_id] = chunk_id
|
||||
self.id_to_object_np[global_id] = obj_np
|
||||
self.id_to_pick_np[global_id] = pick_np
|
||||
self.id_to_geom_index[global_id] = gi
|
||||
self.tree_nodes[owner_key]["local_ids"].append(global_id)
|
||||
self.id_to_name[global_id] = owner_key
|
||||
self.global_transforms.append(LMatrix4f(world_mat))
|
||||
|
||||
@ -66,6 +66,7 @@ class SSBOEditor:
|
||||
self.model = None
|
||||
self.source_model = None
|
||||
self.source_model_root = None
|
||||
self._source_child_base_mats = {}
|
||||
self.last_import_tree_key = None
|
||||
self.last_import_root_name = None
|
||||
self.ssbo = None
|
||||
@ -279,7 +280,7 @@ class SSBOEditor:
|
||||
except Exception as e:
|
||||
print(f'修复黑色模型材质时出错: {e}')
|
||||
|
||||
def _load_source_model_from_path(self, model_path):
|
||||
def _load_source_model_from_path(self, model_path, apply_black_fix=True, repair_textures=True):
|
||||
"""Load a source model NodePath from disk without touching current runtime state."""
|
||||
source_model = None
|
||||
last_error = None
|
||||
@ -295,8 +296,10 @@ class SSBOEditor:
|
||||
if last_error:
|
||||
raise RuntimeError(f"Failed to load model '{model_path}': {last_error}")
|
||||
raise RuntimeError(f"Failed to load model '{model_path}'")
|
||||
self._fixBlackMaterials(source_model)
|
||||
self._repair_missing_textures(source_model, model_path)
|
||||
if apply_black_fix:
|
||||
self._fixBlackMaterials(source_model)
|
||||
if repair_textures:
|
||||
self._repair_missing_textures(source_model, model_path)
|
||||
return source_model
|
||||
|
||||
def _set_node_name(self, node, name):
|
||||
@ -374,6 +377,26 @@ class SSBOEditor:
|
||||
return candidate
|
||||
index += 1
|
||||
|
||||
def _capture_source_child_base_mats(self):
|
||||
"""Capture baseline local mats for each top-level source child."""
|
||||
self._source_child_base_mats = {}
|
||||
root = self.source_model_root
|
||||
if not root:
|
||||
return
|
||||
for child in self._iter_children(root):
|
||||
if not self._node_is_valid(child):
|
||||
continue
|
||||
name = self._get_node_name(child, None)
|
||||
if not name:
|
||||
continue
|
||||
try:
|
||||
self._source_child_base_mats[name] = LMatrix4f(child.get_mat())
|
||||
except Exception:
|
||||
try:
|
||||
self._source_child_base_mats[name] = LMatrix4f(child.getMat())
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
def _get_top_level_group_keys(self):
|
||||
if not self.controller or not getattr(self.controller, "tree_root_key", None):
|
||||
return []
|
||||
@ -437,15 +460,357 @@ class SSBOEditor:
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
base_child_mat = self._source_child_base_mats.get(display_name)
|
||||
if base_child_mat is None:
|
||||
try:
|
||||
base_child_mat = LMatrix4f(source_child.get_mat())
|
||||
except Exception:
|
||||
try:
|
||||
base_child_mat = LMatrix4f(source_child.getMat())
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
delta_mat = current_mat * inv_original
|
||||
try:
|
||||
source_child.set_mat(delta_mat * source_child.get_mat())
|
||||
source_child.set_mat(delta_mat * base_child_mat)
|
||||
except Exception:
|
||||
try:
|
||||
source_child.setMat(delta_mat * source_child.getMat())
|
||||
source_child.setMat(delta_mat * base_child_mat)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
def _resolve_source_node_by_tree_key(self, tree_key):
|
||||
"""Resolve controller tree key (e.g. 0/1/2) to source_model_root node."""
|
||||
if not self.source_model_root or not tree_key:
|
||||
return None
|
||||
parts = str(tree_key).split("/")
|
||||
if not parts or parts[0] != "0":
|
||||
return None
|
||||
node = self.source_model_root
|
||||
for part in parts[1:]:
|
||||
try:
|
||||
child_index = int(part)
|
||||
except Exception:
|
||||
return None
|
||||
try:
|
||||
node = node.get_child(child_index)
|
||||
except Exception:
|
||||
try:
|
||||
node = node.getChild(child_index)
|
||||
except Exception:
|
||||
return None
|
||||
if not self._node_is_valid(node):
|
||||
return None
|
||||
return node
|
||||
|
||||
def _snapshot_runtime_materials_to_source_root(self):
|
||||
"""
|
||||
Persist runtime-edited material/geom render state back to source_model_root.
|
||||
This keeps project save/load consistent for SSBO editing workflow.
|
||||
"""
|
||||
controller = self.controller
|
||||
if not controller or not self.source_model_root:
|
||||
return
|
||||
|
||||
synced = 0
|
||||
effect_tags_synced = set()
|
||||
root_effect_tags = {}
|
||||
property_helpers = getattr(self.world, "property_helpers", None) if getattr(self, "world", None) else None
|
||||
capture_snapshot_fn = getattr(property_helpers, "_capture_node_material_snapshot", None) if property_helpers else None
|
||||
normalize_snapshot_fn = getattr(property_helpers, "_normalize_material_snapshot", None) if property_helpers else None
|
||||
apply_snapshot_fn = getattr(property_helpers, "_apply_node_material_snapshot", None) if property_helpers else None
|
||||
get_materials_fn = getattr(property_helpers, "_get_node_materials", None) if property_helpers else None
|
||||
ensure_material_fn = getattr(property_helpers, "_ensure_material_for_node", None) if property_helpers else None
|
||||
try:
|
||||
model_root = self.model
|
||||
if model_root and not model_root.is_empty():
|
||||
for tag_name in (
|
||||
"material_effect_metallic_enabled",
|
||||
"material_effect_default_texture_enabled",
|
||||
"material_effect_parallax_enabled",
|
||||
"material_render_effect_signature",
|
||||
):
|
||||
if model_root.hasTag(tag_name):
|
||||
root_effect_tags[tag_name] = model_root.getTag(tag_name)
|
||||
except Exception:
|
||||
root_effect_tags = {}
|
||||
|
||||
def _node_has_metallic_texture(node_np):
|
||||
try:
|
||||
stages = node_np.findAllTextureStages()
|
||||
for i in range(stages.getNumTextureStages()):
|
||||
stage = stages.getTextureStage(i)
|
||||
if not stage:
|
||||
continue
|
||||
try:
|
||||
sname = (stage.getName() or "").lower()
|
||||
except Exception:
|
||||
sname = ""
|
||||
# convention in project helpers and RP metallic workflow
|
||||
if "metallic" in sname or sname == "p3d_texture5":
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
|
||||
def _clone_snapshot_for_target(source_snapshot, target_node):
|
||||
if not callable(normalize_snapshot_fn):
|
||||
return None
|
||||
snapshot = normalize_snapshot_fn(source_snapshot)
|
||||
if snapshot is None:
|
||||
return None
|
||||
|
||||
target_materials = []
|
||||
try:
|
||||
if callable(get_materials_fn):
|
||||
target_materials = list(get_materials_fn(target_node) or [])
|
||||
except Exception:
|
||||
target_materials = []
|
||||
if not target_materials and callable(ensure_material_fn):
|
||||
try:
|
||||
fallback = ensure_material_fn(target_node)
|
||||
if fallback is not None:
|
||||
target_materials = [fallback]
|
||||
except Exception:
|
||||
target_materials = []
|
||||
|
||||
source_entries = snapshot.get("materials", []) or []
|
||||
cloned_entries = []
|
||||
for idx, entry in enumerate(source_entries):
|
||||
target_material = target_materials[idx] if idx < len(target_materials) else None
|
||||
cloned_entries.append({
|
||||
"material": target_material,
|
||||
"base_color": entry.get("base_color"),
|
||||
"roughness": entry.get("roughness"),
|
||||
"metallic": entry.get("metallic"),
|
||||
"ior": entry.get("ior"),
|
||||
"emission": entry.get("emission"),
|
||||
})
|
||||
|
||||
node_state = snapshot.get("node_state", {}) or {}
|
||||
textures = dict(node_state.get("textures", {}) or {})
|
||||
effect_tags = dict(node_state.get("effect_tags", {}) or {})
|
||||
|
||||
return {
|
||||
"materials": cloned_entries,
|
||||
"node_state": {
|
||||
"textures": textures,
|
||||
"effect_tags": effect_tags,
|
||||
},
|
||||
}
|
||||
|
||||
grouped_entries = {}
|
||||
|
||||
def _snapshot_score(snapshot, obj_np):
|
||||
score = 0
|
||||
try:
|
||||
snapshot = normalize_snapshot_fn(snapshot) if callable(normalize_snapshot_fn) else snapshot
|
||||
except Exception:
|
||||
pass
|
||||
if isinstance(snapshot, dict):
|
||||
node_state = snapshot.get("node_state", {}) or {}
|
||||
textures = node_state.get("textures", {}) or {}
|
||||
effect_tags = node_state.get("effect_tags", {}) or {}
|
||||
score += len([value for value in textures.values() if value]) * 100
|
||||
score += len([value for value in effect_tags.values() if value]) * 50
|
||||
for entry in snapshot.get("materials", []) or []:
|
||||
if entry.get("base_color") is not None:
|
||||
score += 5
|
||||
for scalar_name in ("roughness", "metallic", "ior"):
|
||||
if entry.get(scalar_name) is not None:
|
||||
score += 3
|
||||
if entry.get("emission") is not None:
|
||||
score += 2
|
||||
try:
|
||||
if _node_has_metallic_texture(obj_np):
|
||||
score += 25
|
||||
except Exception:
|
||||
pass
|
||||
return score
|
||||
|
||||
for gid, obj_np in controller.id_to_object_np.items():
|
||||
if not self._node_is_valid(obj_np):
|
||||
continue
|
||||
owner_key = controller.id_to_name.get(gid)
|
||||
if not owner_key:
|
||||
continue
|
||||
source_node = self._resolve_source_node_by_tree_key(owner_key)
|
||||
if not self._node_is_valid(source_node):
|
||||
continue
|
||||
|
||||
source_snapshot = None
|
||||
if callable(capture_snapshot_fn):
|
||||
try:
|
||||
source_snapshot = capture_snapshot_fn(obj_np)
|
||||
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)
|
||||
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,
|
||||
"score": candidate_score,
|
||||
}
|
||||
|
||||
for source_node_key, entry in grouped_entries.items():
|
||||
gid = entry["gid"]
|
||||
obj_np = entry["obj_np"]
|
||||
source_node = entry["source_node"]
|
||||
source_snapshot = entry["snapshot"]
|
||||
|
||||
if source_node_key not in effect_tags_synced:
|
||||
inferred_metallic = _node_has_metallic_texture(obj_np)
|
||||
for tag_name in (
|
||||
"material_effect_metallic_enabled",
|
||||
"material_effect_default_texture_enabled",
|
||||
"material_effect_parallax_enabled",
|
||||
"material_render_effect_signature",
|
||||
):
|
||||
try:
|
||||
if obj_np.hasTag(tag_name):
|
||||
source_node.setTag(tag_name, obj_np.getTag(tag_name))
|
||||
elif tag_name == "material_effect_metallic_enabled" and inferred_metallic:
|
||||
source_node.setTag(tag_name, "1")
|
||||
elif tag_name in root_effect_tags:
|
||||
source_node.setTag(tag_name, root_effect_tags[tag_name])
|
||||
except Exception:
|
||||
pass
|
||||
texture_slot_tags = (
|
||||
"material_texture_diffuse",
|
||||
"material_texture_normal",
|
||||
"material_texture_ior",
|
||||
"material_texture_roughness",
|
||||
"material_texture_parallax",
|
||||
"material_texture_metallic",
|
||||
"material_texture_emission",
|
||||
"material_texture_ao",
|
||||
"material_texture_alpha",
|
||||
"material_texture_detail",
|
||||
"material_texture_gloss",
|
||||
)
|
||||
for tag_name in texture_slot_tags:
|
||||
try:
|
||||
if obj_np.hasTag(tag_name):
|
||||
source_node.setTag(tag_name, obj_np.getTag(tag_name))
|
||||
elif source_node.hasTag(tag_name):
|
||||
source_node.clearTag(tag_name)
|
||||
except Exception:
|
||||
pass
|
||||
effect_tags_synced.add(source_node_key)
|
||||
|
||||
if source_snapshot is not None and callable(apply_snapshot_fn):
|
||||
try:
|
||||
target_snapshot = _clone_snapshot_for_target(source_snapshot, source_node)
|
||||
if target_snapshot is not None:
|
||||
apply_snapshot_fn(source_node, target_snapshot)
|
||||
synced += 1
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
runtime_geom_state = None
|
||||
try:
|
||||
geom_paths = obj_np.findAllMatches("**/+GeomNode")
|
||||
if geom_paths and geom_paths.getNumPaths() > 0:
|
||||
runtime_geom_state = geom_paths.getPath(0).getNetState()
|
||||
except Exception:
|
||||
runtime_geom_state = None
|
||||
if runtime_geom_state is None:
|
||||
continue
|
||||
|
||||
try:
|
||||
src_gnode = source_node.node()
|
||||
set_src_state = getattr(src_gnode, "set_geom_state", None) or getattr(src_gnode, "setGeomState", None)
|
||||
get_src_count = getattr(src_gnode, "get_num_geoms", None) or getattr(src_gnode, "getNumGeoms", None)
|
||||
src_geom_count = int(get_src_count()) if callable(get_src_count) else 0
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
src_geom_index = controller.id_to_geom_index.get(gid, 0)
|
||||
if src_geom_index < 0 or src_geom_index >= src_geom_count:
|
||||
continue
|
||||
|
||||
try:
|
||||
if callable(set_src_state):
|
||||
set_src_state(src_geom_index, runtime_geom_state)
|
||||
synced += 1
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if synced:
|
||||
print(f"[SSBOEditor] Synced runtime material states back to source tree: {synced}")
|
||||
|
||||
def _restore_saved_material_bindings_from_tags(self, root_np):
|
||||
"""Rebind saved texture/effect tags back onto loaded source nodes."""
|
||||
if not self._node_is_valid(root_np):
|
||||
return 0
|
||||
|
||||
property_helpers = getattr(self.base, "property_helpers", None)
|
||||
capture_snapshot_fn = getattr(property_helpers, "_capture_node_material_snapshot", None) if property_helpers else None
|
||||
apply_snapshot_fn = getattr(property_helpers, "_apply_node_material_snapshot", None) if property_helpers else None
|
||||
texture_slots_fn = getattr(property_helpers, "_get_material_texture_slots", None) if property_helpers else None
|
||||
if not callable(capture_snapshot_fn) or not callable(apply_snapshot_fn):
|
||||
return 0
|
||||
|
||||
texture_types = []
|
||||
if callable(texture_slots_fn):
|
||||
try:
|
||||
texture_types = list((texture_slots_fn() or {}).keys())
|
||||
except Exception:
|
||||
texture_types = []
|
||||
if not texture_types:
|
||||
texture_types = [
|
||||
"diffuse", "normal", "ior", "roughness", "parallax",
|
||||
"metallic", "emission", "ao", "alpha", "detail", "gloss",
|
||||
]
|
||||
|
||||
effect_tag_names = (
|
||||
"material_effect_metallic_enabled",
|
||||
"material_effect_default_texture_enabled",
|
||||
"material_effect_parallax_enabled",
|
||||
"material_render_effect_signature",
|
||||
)
|
||||
|
||||
restored = 0
|
||||
try:
|
||||
descendant_nodes = list(root_np.find_all_matches("**"))
|
||||
except Exception:
|
||||
try:
|
||||
descendant_nodes = list(root_np.findAllMatches("**"))
|
||||
except Exception:
|
||||
descendant_nodes = []
|
||||
|
||||
for node in [root_np] + descendant_nodes:
|
||||
if not self._node_is_valid(node):
|
||||
continue
|
||||
|
||||
has_texture_tags = any(node.hasTag(f"material_texture_{texture_type}") for texture_type in texture_types)
|
||||
has_effect_tags = any(node.hasTag(tag_name) for tag_name in effect_tag_names)
|
||||
if not (has_texture_tags or has_effect_tags):
|
||||
continue
|
||||
|
||||
try:
|
||||
snapshot = capture_snapshot_fn(node)
|
||||
except Exception:
|
||||
snapshot = None
|
||||
if not snapshot:
|
||||
continue
|
||||
|
||||
try:
|
||||
apply_snapshot_fn(node, snapshot)
|
||||
restored += 1
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if restored:
|
||||
print(f"[SSBOEditor] Restored saved material bindings from tags: {restored}")
|
||||
return restored
|
||||
|
||||
def _clear_runtime_state(self, preserve_source_models=False):
|
||||
"""Remove runtime SSBO controller/model state while optionally keeping source snapshots."""
|
||||
self.clear_selection()
|
||||
@ -482,6 +847,7 @@ class SSBOEditor:
|
||||
if not preserve_source_models:
|
||||
self.source_model = None
|
||||
self.source_model_root = None
|
||||
self._source_child_base_mats = {}
|
||||
self._sync_pick_scene_binding()
|
||||
|
||||
def _get_source_root_children(self):
|
||||
@ -589,12 +955,27 @@ class SSBOEditor:
|
||||
|
||||
print(f"[SSBOEditor] Model loaded. Total objects: {count}")
|
||||
|
||||
def load_model(self, model_path, keep_source_model=False, append=False):
|
||||
def load_model(
|
||||
self,
|
||||
model_path,
|
||||
keep_source_model=False,
|
||||
append=False,
|
||||
scene_package_import=False,
|
||||
):
|
||||
"""Load and process one model into the aggregated SSBO scene."""
|
||||
print(f"[SSBOEditor] Loading model: {model_path}")
|
||||
source_model = self._load_source_model_from_path(model_path)
|
||||
# scene_package_import: loading saved scene.bam from project.
|
||||
# Keep stored material/texture states intact; repair heuristics can
|
||||
# misclassify valid packed/relative texture refs and cause dark materials.
|
||||
should_repair_textures = not scene_package_import
|
||||
should_fix_black_materials = not scene_package_import
|
||||
source_model = self._load_source_model_from_path(
|
||||
model_path,
|
||||
apply_black_fix=should_fix_black_materials,
|
||||
repair_textures=should_repair_textures,
|
||||
)
|
||||
model_name = os.path.basename(model_path)
|
||||
if model_name:
|
||||
if model_name and not scene_package_import:
|
||||
self._set_node_name(source_model, model_name)
|
||||
|
||||
if append and self.source_model_root:
|
||||
@ -605,6 +986,46 @@ class SSBOEditor:
|
||||
self._clear_runtime_state(preserve_source_models=False)
|
||||
|
||||
source_root = self._ensure_source_model_root()
|
||||
|
||||
# 项目场景包导入(scene.bam)时,避免再包一层 "scene.bam" 根节点,
|
||||
# 直接把其顶层子节点并入 source_root,保持场景树与保存时一致。
|
||||
if scene_package_import:
|
||||
imported_roots = []
|
||||
children = []
|
||||
try:
|
||||
children = [c for c in source_model.get_children() if c and not c.is_empty()]
|
||||
except Exception:
|
||||
try:
|
||||
children = [c for c in source_model.getChildren() if c and not c.isEmpty()]
|
||||
except Exception:
|
||||
children = []
|
||||
|
||||
for child in children:
|
||||
try:
|
||||
child_name = self._get_node_name(child, "")
|
||||
if child_name in {"render", "render2d", "aspect2d"}:
|
||||
continue
|
||||
imported_child = child.copyTo(source_root)
|
||||
imported_roots.append(imported_child)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if not imported_roots:
|
||||
fallback_name = self._get_node_name(source_model, "scene_root")
|
||||
unique_root_name = self._make_unique_source_child_name(fallback_name)
|
||||
self._set_node_name(source_model, unique_root_name)
|
||||
imported_root = source_model.copyTo(source_root)
|
||||
self._set_node_name(imported_root, unique_root_name)
|
||||
imported_roots = [imported_root]
|
||||
|
||||
self.source_model = source_root
|
||||
self._restore_saved_material_bindings_from_tags(source_root)
|
||||
self._capture_source_child_base_mats()
|
||||
self._rebuild_runtime_from_source_root(highlight_root_name=None)
|
||||
if len(imported_roots) == 1:
|
||||
return imported_roots[0]
|
||||
return source_root
|
||||
|
||||
unique_root_name = self._make_unique_source_child_name(model_name or "imported_model")
|
||||
self._set_node_name(source_model, unique_root_name)
|
||||
imported_root = source_model.copyTo(source_root)
|
||||
@ -615,6 +1036,7 @@ class SSBOEditor:
|
||||
else:
|
||||
self.source_model = source_root
|
||||
|
||||
self._capture_source_child_base_mats()
|
||||
self._rebuild_runtime_from_source_root(highlight_root_name=unique_root_name)
|
||||
return imported_root
|
||||
|
||||
@ -1652,7 +2074,14 @@ class SSBOEditor:
|
||||
if self._is_root_selection():
|
||||
return self.model if self._node_is_valid(self.model) else None
|
||||
|
||||
if len(self.selected_ids) == 1:
|
||||
if len(self.selected_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?
|
||||
# The property panel gets the proxy node for transform edits.
|
||||
return proxy
|
||||
|
||||
if len(self.selected_ids) >= 1:
|
||||
obj_np = self.controller.id_to_object_np.get(self.selected_ids[0])
|
||||
if self._node_is_valid(obj_np):
|
||||
return obj_np
|
||||
|
||||
@ -1288,7 +1288,16 @@ class AppActions:
|
||||
except Exception:
|
||||
source_children = []
|
||||
|
||||
if len(source_children) > 1 and not scene_package_import:
|
||||
if scene_package_import:
|
||||
if len(source_children) == 1:
|
||||
try:
|
||||
model_np.setName(source_children[0].getName())
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
# 项目场景加载不使用文件名(scene.bam)作为场景树根名称
|
||||
model_np.setName("场景模型")
|
||||
elif len(source_children) > 1:
|
||||
model_np.setName("\u5bfc\u5165\u6a21\u578b")
|
||||
elif file_path:
|
||||
model_np.setName(os.path.basename(file_path))
|
||||
@ -1401,6 +1410,7 @@ class AppActions:
|
||||
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,
|
||||
|
||||
@ -72,6 +72,15 @@ class EditorPanelsLeftMixin:
|
||||
|
||||
def _get_scene_tree_models(self):
|
||||
models = []
|
||||
# SSBO模式下场景树应以SSBO聚合根为唯一模型入口,避免混入scene_manager残留节点
|
||||
# (如 scene.bam / chunk_* 运行时包装节点)导致树结构异常。
|
||||
if getattr(self.app, "use_ssbo_mouse_picking", False):
|
||||
ssbo_editor = getattr(self.app, "ssbo_editor", None)
|
||||
ssbo_model = getattr(ssbo_editor, "model", None) if ssbo_editor else None
|
||||
if ssbo_model and not ssbo_model.isEmpty() and ssbo_model.hasParent():
|
||||
return [ssbo_model]
|
||||
return []
|
||||
|
||||
if hasattr(self.app, "scene_manager") and self.app.scene_manager and hasattr(self.app.scene_manager, "models"):
|
||||
models.extend([m for m in self.app.scene_manager.models if m and not m.isEmpty()])
|
||||
|
||||
|
||||
@ -16,8 +16,10 @@ class PanelDelegates:
|
||||
return False
|
||||
if require_attached:
|
||||
try:
|
||||
return bool(node.hasParent())
|
||||
except Exception:
|
||||
# Add debug output
|
||||
return bool(node.has_parent() if hasattr(node, "has_parent") else node.hasParent())
|
||||
except Exception as e:
|
||||
print(f"[PanelDelegates] Error in has_parent: {e}")
|
||||
return False
|
||||
return True
|
||||
|
||||
@ -26,14 +28,16 @@ class PanelDelegates:
|
||||
ssbo_editor = getattr(self, "ssbo_editor", None)
|
||||
if ssbo_editor and hasattr(ssbo_editor, "has_active_selection") and ssbo_editor.has_active_selection():
|
||||
ssbo_node = ssbo_editor.get_selection_scene_node()
|
||||
return ssbo_node if self._node_is_valid(ssbo_node, require_attached=True) else None
|
||||
if ssbo_node and self._node_is_valid(ssbo_node, require_attached=False):
|
||||
return ssbo_node
|
||||
return None
|
||||
|
||||
selection = getattr(self, "selection", None)
|
||||
if selection and hasattr(selection, "getSelectedNode"):
|
||||
node = selection.getSelectedNode()
|
||||
else:
|
||||
node = getattr(selection, "selectedNode", None) if selection else None
|
||||
return node if self._node_is_valid(node, require_attached=True) else None
|
||||
return node if self._node_is_valid(node, require_attached=False) else None
|
||||
|
||||
def _get_ssbo_selection_summary(self):
|
||||
ssbo_editor = getattr(self, "ssbo_editor", None)
|
||||
|
||||
@ -880,6 +880,109 @@ class PropertyHelpers:
|
||||
if node and hasattr(node, "hasTag") and node.hasTag(tag_name):
|
||||
texture_tags[texture_type] = node.getTag(tag_name)
|
||||
|
||||
# Fallback: some runtime nodes inherit textures from parent/root state and
|
||||
# therefore have no explicit material_texture_* tags on the selected node.
|
||||
# Capture the actual texture bindings as stable paths so save/load can
|
||||
# persist them back into the source scene tree.
|
||||
if node and not node.isEmpty():
|
||||
texture_slots = self._get_material_texture_slots()
|
||||
slot_to_type = {slot: tex_type for tex_type, slot in texture_slots.items()}
|
||||
def record_texture_binding(texture_type, texture):
|
||||
if not texture_type or texture_type in texture_tags or not texture:
|
||||
return
|
||||
|
||||
resolved_path = ""
|
||||
try:
|
||||
if texture.hasFullpath():
|
||||
fullpath = texture.getFullpath()
|
||||
try:
|
||||
resolved_path = fullpath.toOsSpecific()
|
||||
except Exception:
|
||||
resolved_path = str(fullpath)
|
||||
except Exception:
|
||||
resolved_path = ""
|
||||
|
||||
resolved_path = os.path.normpath(str(resolved_path).strip()) if resolved_path else ""
|
||||
if not resolved_path or not os.path.exists(resolved_path):
|
||||
try:
|
||||
texture_name = os.path.normpath(str(texture.getName() or "").strip())
|
||||
except Exception:
|
||||
texture_name = ""
|
||||
if texture_name and os.path.exists(texture_name):
|
||||
resolved_path = texture_name
|
||||
|
||||
if resolved_path:
|
||||
texture_tags[texture_type] = resolved_path
|
||||
|
||||
def infer_texture_type(stage):
|
||||
if not stage:
|
||||
return None
|
||||
try:
|
||||
stage_name = (stage.getName() or "").strip().lower()
|
||||
except Exception:
|
||||
stage_name = ""
|
||||
|
||||
if stage_name.endswith("_map"):
|
||||
inferred_type = stage_name[:-4]
|
||||
if inferred_type in texture_slots:
|
||||
return inferred_type
|
||||
|
||||
try:
|
||||
return slot_to_type.get(int(stage.getSort()))
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
try:
|
||||
texture_stages = node.findAllTextureStages()
|
||||
except Exception:
|
||||
texture_stages = None
|
||||
|
||||
if texture_stages:
|
||||
for stage_index in range(texture_stages.getNumTextureStages()):
|
||||
try:
|
||||
stage = texture_stages.getTextureStage(stage_index)
|
||||
except Exception:
|
||||
continue
|
||||
texture_type = infer_texture_type(stage)
|
||||
|
||||
try:
|
||||
texture = node.getTexture(stage)
|
||||
except Exception:
|
||||
texture = None
|
||||
record_texture_binding(texture_type, texture)
|
||||
|
||||
# Some imported/runtime nodes keep effective textures only in the
|
||||
# inherited RenderState. Read TextureAttrib from net state as a
|
||||
# second fallback so SSBO save can persist those bindings too.
|
||||
if len(texture_tags) < len(texture_slots):
|
||||
try:
|
||||
from panda3d.core import TextureAttrib
|
||||
net_state = node.getNetState()
|
||||
if net_state.hasAttrib(TextureAttrib.getClassType()):
|
||||
texture_attrib = net_state.getAttrib(TextureAttrib.getClassType())
|
||||
if texture_attrib:
|
||||
try:
|
||||
num_on = texture_attrib.getNumOnStages()
|
||||
get_stage = texture_attrib.getOnStage
|
||||
get_texture = texture_attrib.getOnTexture
|
||||
except Exception:
|
||||
num_on = 0
|
||||
get_stage = None
|
||||
get_texture = None
|
||||
|
||||
for stage_index in range(int(num_on or 0)):
|
||||
if get_stage is None or get_texture is None:
|
||||
break
|
||||
try:
|
||||
stage = get_stage(stage_index)
|
||||
texture = get_texture(stage)
|
||||
except Exception:
|
||||
continue
|
||||
texture_type = infer_texture_type(stage)
|
||||
record_texture_binding(texture_type, texture)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
effect_tags = {}
|
||||
for tag_name in (
|
||||
"material_effect_metallic_enabled",
|
||||
@ -888,6 +991,14 @@ class PropertyHelpers:
|
||||
):
|
||||
effect_tags[tag_name] = bool(node and hasattr(node, "hasTag") and node.hasTag(tag_name))
|
||||
|
||||
if texture_tags:
|
||||
if texture_tags.get("metallic"):
|
||||
effect_tags["material_effect_metallic_enabled"] = True
|
||||
if texture_tags.get("parallax"):
|
||||
effect_tags["material_effect_parallax_enabled"] = True
|
||||
if any(texture_path for texture_path in texture_tags.values()):
|
||||
effect_tags["material_effect_default_texture_enabled"] = True
|
||||
|
||||
return {
|
||||
"materials": material_entries,
|
||||
"node_state": {
|
||||
@ -1821,6 +1932,17 @@ class PropertyHelpers:
|
||||
def _reset_material(self, node):
|
||||
"""重置节点材质"""
|
||||
try:
|
||||
# 先清理贴图与effect标签,避免后续再次设置贴图时被旧状态污染
|
||||
try:
|
||||
self._clear_all_textures(node)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
if node.hasTag("material_render_effect_signature"):
|
||||
node.clearTag("material_render_effect_signature")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
materials = list(node.find_all_materials())
|
||||
|
||||
for material in materials:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user