修复保存材质模型位置,保存场景树,加载场景树,

This commit is contained in:
Hector 2026-03-18 11:48:13 +08:00
parent 11ad2d5742
commit 4635458300
9 changed files with 1139 additions and 112 deletions

View File

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

View File

@ -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)
# 在第一帧应用样式

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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