diff --git a/imgui.ini b/imgui.ini index 5760f2f1..14f7d531 100644 --- a/imgui.ini +++ b/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 diff --git a/main.py b/main.py index f5c38661..33e16e19 100644 --- a/main.py +++ b/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) # 在第一帧应用样式 diff --git a/scene/scene_manager_io_mixin.py b/scene/scene_manager_io_mixin.py index 0e15dae1..01e46c65 100644 --- a/scene/scene_manager_io_mixin.py +++ b/scene/scene_manager_io_mixin.py @@ -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): diff --git a/ssbo_component/ssbo_controller.py b/ssbo_component/ssbo_controller.py index 8392cbc9..6f5201b1 100644 --- a/ssbo_component/ssbo_controller.py +++ b/ssbo_component/ssbo_controller.py @@ -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)) diff --git a/ssbo_component/ssbo_editor.py b/ssbo_component/ssbo_editor.py index 288b396a..5739669b 100644 --- a/ssbo_component/ssbo_editor.py +++ b/ssbo_component/ssbo_editor.py @@ -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 diff --git a/ui/panels/app_actions.py b/ui/panels/app_actions.py index 0fecf763..f86cafbd 100644 --- a/ui/panels/app_actions.py +++ b/ui/panels/app_actions.py @@ -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, diff --git a/ui/panels/editor_panels_left.py b/ui/panels/editor_panels_left.py index a0e5ace0..d7702a5d 100644 --- a/ui/panels/editor_panels_left.py +++ b/ui/panels/editor_panels_left.py @@ -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()]) diff --git a/ui/panels/panel_delegates.py b/ui/panels/panel_delegates.py index 36929428..c4e326aa 100644 --- a/ui/panels/panel_delegates.py +++ b/ui/panels/panel_delegates.py @@ -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) diff --git a/ui/panels/property_helpers.py b/ui/panels/property_helpers.py index 680e7608..90aed919 100644 --- a/ui/panels/property_helpers.py +++ b/ui/panels/property_helpers.py @@ -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: