Merge branch 'hu_migrate_on_geng' into geng_migrate_on_imgui_hu
# Conflicts: # imgui.ini # ui/panels/editor_panels_top.py
This commit is contained in:
commit
04cb0dc441
9
main.py
9
main.py
@ -905,7 +905,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)
|
||||
|
||||
# 在第一帧应用样式
|
||||
|
||||
@ -417,8 +417,10 @@ class ProjectManager:
|
||||
temp_scene_file = os.path.join(scene_dir, f"{scene_name}.tmp{scene_ext}")
|
||||
final_gui_info_file = scene_file.replace('.bam', '_gui.json')
|
||||
temp_gui_info_file = temp_scene_file.replace('.bam', '_gui.json')
|
||||
final_lui_info_file = scene_file.replace('.bam', '_lui.json')
|
||||
temp_lui_info_file = temp_scene_file.replace('.bam', '_lui.json')
|
||||
|
||||
for temp_path in (temp_scene_file, temp_gui_info_file):
|
||||
for temp_path in (temp_scene_file, temp_gui_info_file, temp_lui_info_file):
|
||||
if os.path.exists(temp_path):
|
||||
try:
|
||||
os.remove(temp_path)
|
||||
@ -434,12 +436,15 @@ class ProjectManager:
|
||||
if os.path.exists(temp_gui_info_file):
|
||||
os.replace(temp_gui_info_file, final_gui_info_file)
|
||||
|
||||
if os.path.exists(temp_lui_info_file):
|
||||
os.replace(temp_lui_info_file, final_lui_info_file)
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"原子保存场景失败: {e}")
|
||||
return False
|
||||
finally:
|
||||
for temp_path in (temp_scene_file, temp_gui_info_file):
|
||||
for temp_path in (temp_scene_file, temp_gui_info_file, temp_lui_info_file):
|
||||
if os.path.exists(temp_path):
|
||||
try:
|
||||
os.remove(temp_path)
|
||||
|
||||
@ -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
|
||||
@ -62,6 +62,104 @@ class SceneManagerIOMixin:
|
||||
except Exception as e:
|
||||
print(f"清理残留场景节点失败: {e}")
|
||||
|
||||
def _get_scene_sidecar_path(self, filename, suffix):
|
||||
scene_root, _ = os.path.splitext(os.path.normpath(filename))
|
||||
return f"{scene_root}{suffix}"
|
||||
|
||||
def _reset_lui_runtime_state(self, lui_manager):
|
||||
if lui_manager is None or getattr(lui_manager, "lui_enabled", True) is False:
|
||||
return True
|
||||
|
||||
apply_fn = getattr(lui_manager, "_apply_lui_structure_snapshot", None)
|
||||
if callable(apply_fn):
|
||||
try:
|
||||
apply_fn({})
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"清空LUI结构失败: {e}")
|
||||
return False
|
||||
|
||||
clear_fn = getattr(lui_manager, "_clear_lui_structure_runtime", None)
|
||||
if callable(clear_fn):
|
||||
try:
|
||||
clear_fn()
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"清空LUI结构失败: {e}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _save_lui_snapshot_file(self, filename):
|
||||
lui_manager = getattr(self.world, "lui_manager", None)
|
||||
if lui_manager is None or getattr(lui_manager, "lui_enabled", True) is False:
|
||||
print("ℹ️ 当前会话未启用LUI,跳过LUI结构保存")
|
||||
return True
|
||||
|
||||
capture_fn = getattr(lui_manager, "_capture_lui_structure_snapshot", None)
|
||||
if not callable(capture_fn):
|
||||
print("ℹ️ 未找到LUI结构快照接口,跳过LUI结构保存")
|
||||
return True
|
||||
|
||||
try:
|
||||
snapshot = capture_fn() or {}
|
||||
lui_info_file = self._get_scene_sidecar_path(filename, "_lui.json")
|
||||
with open(lui_info_file, "w", encoding="utf-8") as f:
|
||||
json.dump(snapshot, f, ensure_ascii=False, indent=4)
|
||||
|
||||
canvas_count = len(snapshot.get("canvases", []) or [])
|
||||
component_count = len(snapshot.get("components", []) or [])
|
||||
print(
|
||||
f"✓ LUI信息已保存: {lui_info_file} "
|
||||
f"(Canvas: {canvas_count}, Components: {component_count})"
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"✗ 保存LUI信息失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def _load_lui_snapshot_file(self, filename):
|
||||
lui_manager = getattr(self.world, "lui_manager", None)
|
||||
if lui_manager is None or getattr(lui_manager, "lui_enabled", True) is False:
|
||||
print("ℹ️ 当前会话未启用LUI,跳过LUI结构恢复")
|
||||
return True
|
||||
|
||||
lui_info_file = self._get_scene_sidecar_path(filename, "_lui.json")
|
||||
apply_fn = getattr(lui_manager, "_apply_lui_structure_snapshot", None)
|
||||
|
||||
if not os.path.exists(lui_info_file):
|
||||
print("ℹ️ 未找到LUI信息文件,清空当前LUI结构")
|
||||
return self._reset_lui_runtime_state(lui_manager)
|
||||
|
||||
if not callable(apply_fn):
|
||||
print("ℹ️ 未找到LUI结构恢复接口,跳过LUI结构恢复")
|
||||
return True
|
||||
|
||||
try:
|
||||
with open(lui_info_file, "r", encoding="utf-8") as f:
|
||||
content = f.read().strip()
|
||||
|
||||
snapshot = json.loads(content) if content else {}
|
||||
apply_fn(snapshot)
|
||||
|
||||
canvas_count = len(snapshot.get("canvases", []) or [])
|
||||
component_count = len(snapshot.get("components", []) or [])
|
||||
print(
|
||||
f"✓ LUI信息已恢复: {lui_info_file} "
|
||||
f"(Canvas: {canvas_count}, Components: {component_count})"
|
||||
)
|
||||
return True
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"✗ LUI信息文件格式错误: {e}")
|
||||
except Exception as e:
|
||||
print(f"✗ 加载LUI信息失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
return self._reset_lui_runtime_state(lui_manager)
|
||||
|
||||
def _collectGUIElementInfo(self, gui_node):
|
||||
"""收集GUI元素的信息用于保存"""
|
||||
try:
|
||||
@ -257,6 +355,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)
|
||||
@ -303,12 +418,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')
|
||||
@ -350,18 +543,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"):
|
||||
@ -444,6 +637,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:
|
||||
@ -465,6 +683,8 @@ class SceneManagerIOMixin:
|
||||
|
||||
if success:
|
||||
print(f"✓ 场景保存成功: {filename}")
|
||||
if not self._save_lui_snapshot_file(filename):
|
||||
return False
|
||||
else:
|
||||
print("✗ 场景保存失败")
|
||||
|
||||
@ -805,6 +1025,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
|
||||
@ -981,51 +1413,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"):
|
||||
@ -1116,6 +1511,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):
|
||||
@ -1141,6 +1620,8 @@ class SceneManagerIOMixin:
|
||||
else:
|
||||
print("ℹ️ 未找到GUI信息文件")
|
||||
|
||||
self._load_lui_snapshot_file(filename)
|
||||
|
||||
# 移除临时场景节点
|
||||
if not scene.isEmpty():
|
||||
scene.removeNode()
|
||||
|
||||
@ -44,6 +44,7 @@ class ObjectController:
|
||||
self.id_to_chunk = {} # global_id -> chunk_id
|
||||
self.id_to_object_np = {} # global_id -> dynamic object nodepath
|
||||
self.id_to_pick_np = {} # global_id -> pick-scene nodepath
|
||||
self.id_to_geom_index = {} # global_id -> owner GeomNode geom index
|
||||
|
||||
# chunk_id -> {
|
||||
# "dynamic_np": NodePath,
|
||||
@ -225,10 +226,21 @@ class ObjectController:
|
||||
return False
|
||||
|
||||
name = (node["name"] or "").strip().lower()
|
||||
if name != "root":
|
||||
return False
|
||||
if name == "root":
|
||||
return len(node["children"]) > 0
|
||||
|
||||
return len(node["children"]) > 0
|
||||
# Hide scene-package/runtime wrappers in virtual tree:
|
||||
# - scene.bam container
|
||||
# - chunk_* dynamic/static batching nodes
|
||||
# - modelCollision_* helper nodes
|
||||
if name.endswith(".bam") and len(node["children"]) > 0:
|
||||
return True
|
||||
if name.startswith("chunk_"):
|
||||
return True
|
||||
if name.startswith("modelcollision_"):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _encode_id_color(self, vdata, object_id):
|
||||
if not vdata.has_column("color"):
|
||||
@ -274,6 +286,28 @@ class ObjectController:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _copy_node_tags(self, src_np, dst_np):
|
||||
"""Copy all string tags from source node to rebuilt runtime node."""
|
||||
if not src_np or not dst_np:
|
||||
return
|
||||
try:
|
||||
for tag_name in src_np.get_tag_keys():
|
||||
try:
|
||||
dst_np.set_tag(tag_name, src_np.get_tag(tag_name))
|
||||
except Exception:
|
||||
continue
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
for tag_name in src_np.getTagKeys():
|
||||
try:
|
||||
dst_np.setTag(tag_name, src_np.getTag(tag_name))
|
||||
except Exception:
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def bake_ids_and_collect(self, model):
|
||||
"""
|
||||
Bake IDs into vertex colors, flatten, then build vertex index.
|
||||
@ -513,7 +547,10 @@ class ObjectController:
|
||||
static_np = chunk["dynamic_np"].copy_to(self.model)
|
||||
static_np.set_name(f"chunk_{chunk_id:04d}_static")
|
||||
static_np.unstash()
|
||||
static_np.flatten_strong()
|
||||
# Keep the static representation as a per-object copy instead of flattening.
|
||||
# Even medium flattening can still collapse or rewrite per-object PBR/effect
|
||||
# state after save/load, which manifests as black materials until selection
|
||||
# switches back to the dynamic objects.
|
||||
|
||||
chunk["static_np"] = static_np
|
||||
chunk["dirty"] = False
|
||||
@ -760,8 +797,10 @@ class ObjectController:
|
||||
for chunk_id in list(self.active_chunks):
|
||||
if chunk_id in target_chunks:
|
||||
continue
|
||||
if self.chunks[chunk_id]["dirty"]:
|
||||
self._rebuild_static_chunk(chunk_id)
|
||||
# Always rebuild static chunk when leaving dynamic edit mode.
|
||||
# Material/texture edits may not set `dirty`, but still need to
|
||||
# propagate from dynamic objects to static representation.
|
||||
self._rebuild_static_chunk(chunk_id)
|
||||
self._set_chunk_dynamic(chunk_id, False)
|
||||
|
||||
# Promote target chunks.
|
||||
@ -795,25 +834,48 @@ class ObjectController:
|
||||
owner_key = self._path_to_tree_key.get(str(np), self.tree_root_key)
|
||||
|
||||
world_mat = LMatrix4f(np.get_mat(model))
|
||||
# Preserve the inherited render state, not just the local node state.
|
||||
# Scene/package reload often stores material textures/effects on parent
|
||||
# nodes; using only local state drops those bindings and makes rebuilt
|
||||
# chunk_* runtime objects render black after reopening a project.
|
||||
try:
|
||||
node_state = np.get_net_state()
|
||||
except Exception:
|
||||
try:
|
||||
node_state = np.getNetState()
|
||||
except Exception:
|
||||
try:
|
||||
node_state = np.get_state()
|
||||
except Exception:
|
||||
try:
|
||||
node_state = np.getState()
|
||||
except Exception:
|
||||
node_state = None
|
||||
|
||||
for gi in range(gnode.get_num_geoms()):
|
||||
# Render geometry stays untouched (keep original material/color behavior).
|
||||
render_geom = gnode.get_geom(gi).make_copy()
|
||||
render_gnode = GeomNode(f"obj_{global_id}")
|
||||
render_gnode.add_geom(render_geom, gnode.get_geom_state(gi))
|
||||
geom_state = gnode.get_geom_state(gi)
|
||||
try:
|
||||
merged_state = node_state.compose(geom_state) if node_state is not None else geom_state
|
||||
except Exception:
|
||||
merged_state = geom_state
|
||||
render_gnode.add_geom(render_geom, merged_state)
|
||||
|
||||
# Picking geometry gets encoded ID in vertex color.
|
||||
pick_geom = gnode.get_geom(gi).make_copy()
|
||||
pick_vdata = pick_geom.modify_vertex_data()
|
||||
self._encode_id_color(pick_vdata, global_id)
|
||||
pick_gnode = GeomNode(f"pick_{global_id}")
|
||||
pick_gnode.add_geom(pick_geom, gnode.get_geom_state(gi))
|
||||
pick_gnode.add_geom(pick_geom, merged_state)
|
||||
|
||||
world_pos = world_mat.get_row3(3)
|
||||
chunk_id, chunk = self._allocate_spatial_chunk(scene_root, world_pos)
|
||||
|
||||
obj_np = chunk["dynamic_np"].attach_new_node(render_gnode)
|
||||
obj_np.set_mat(world_mat)
|
||||
self._copy_node_tags(np, obj_np)
|
||||
pick_np = pick_root.attach_new_node(pick_gnode)
|
||||
pick_np.set_mat(world_mat)
|
||||
|
||||
@ -821,6 +883,7 @@ class ObjectController:
|
||||
self.id_to_chunk[global_id] = chunk_id
|
||||
self.id_to_object_np[global_id] = obj_np
|
||||
self.id_to_pick_np[global_id] = pick_np
|
||||
self.id_to_geom_index[global_id] = gi
|
||||
self.tree_nodes[owner_key]["local_ids"].append(global_id)
|
||||
self.id_to_name[global_id] = owner_key
|
||||
self.global_transforms.append(LMatrix4f(world_mat))
|
||||
|
||||
@ -66,6 +66,7 @@ class SSBOEditor:
|
||||
self.model = None
|
||||
self.source_model = None
|
||||
self.source_model_root = None
|
||||
self._source_child_base_mats = {}
|
||||
self.last_import_tree_key = None
|
||||
self.last_import_root_name = None
|
||||
self.ssbo = None
|
||||
@ -279,7 +280,7 @@ class SSBOEditor:
|
||||
except Exception as e:
|
||||
print(f'修复黑色模型材质时出错: {e}')
|
||||
|
||||
def _load_source_model_from_path(self, model_path):
|
||||
def _load_source_model_from_path(self, model_path, apply_black_fix=True, repair_textures=True):
|
||||
"""Load a source model NodePath from disk without touching current runtime state."""
|
||||
source_model = None
|
||||
last_error = None
|
||||
@ -295,8 +296,10 @@ class SSBOEditor:
|
||||
if last_error:
|
||||
raise RuntimeError(f"Failed to load model '{model_path}': {last_error}")
|
||||
raise RuntimeError(f"Failed to load model '{model_path}'")
|
||||
self._fixBlackMaterials(source_model)
|
||||
self._repair_missing_textures(source_model, model_path)
|
||||
if apply_black_fix:
|
||||
self._fixBlackMaterials(source_model)
|
||||
if repair_textures:
|
||||
self._repair_missing_textures(source_model, model_path)
|
||||
return source_model
|
||||
|
||||
def _set_node_name(self, node, name):
|
||||
@ -374,6 +377,26 @@ class SSBOEditor:
|
||||
return candidate
|
||||
index += 1
|
||||
|
||||
def _capture_source_child_base_mats(self):
|
||||
"""Capture baseline local mats for each top-level source child."""
|
||||
self._source_child_base_mats = {}
|
||||
root = self.source_model_root
|
||||
if not root:
|
||||
return
|
||||
for child in self._iter_children(root):
|
||||
if not self._node_is_valid(child):
|
||||
continue
|
||||
name = self._get_node_name(child, None)
|
||||
if not name:
|
||||
continue
|
||||
try:
|
||||
self._source_child_base_mats[name] = LMatrix4f(child.get_mat())
|
||||
except Exception:
|
||||
try:
|
||||
self._source_child_base_mats[name] = LMatrix4f(child.getMat())
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
def _get_top_level_group_keys(self):
|
||||
if not self.controller or not getattr(self.controller, "tree_root_key", None):
|
||||
return []
|
||||
@ -384,68 +407,413 @@ class SSBOEditor:
|
||||
return list(root_node.get("children", []))
|
||||
|
||||
def _snapshot_top_level_transforms_to_source_root(self):
|
||||
"""Persist current top-level imported model transforms back into the source scene root."""
|
||||
"""Persist current runtime transforms back into the source scene tree."""
|
||||
if not self.controller or not self.model or not self.source_model_root:
|
||||
return
|
||||
|
||||
source_children = {}
|
||||
for child in self._iter_children(self.source_model_root):
|
||||
try:
|
||||
source_children[child.get_name()] = child
|
||||
except Exception:
|
||||
try:
|
||||
source_children[child.getName()] = child
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
for key in self._get_top_level_group_keys():
|
||||
display_name = self.controller.display_names.get(key, key)
|
||||
source_child = source_children.get(display_name)
|
||||
if not source_child:
|
||||
grouped_entries = {}
|
||||
for gid, obj_np in self.controller.id_to_object_np.items():
|
||||
if not self._node_is_valid(obj_np):
|
||||
continue
|
||||
|
||||
group_ids = self.controller.name_to_ids.get(key, [])
|
||||
if not group_ids:
|
||||
owner_key = self.controller.id_to_name.get(gid)
|
||||
if not owner_key or owner_key == getattr(self.controller, "tree_root_key", None):
|
||||
continue
|
||||
|
||||
representative_id = None
|
||||
for gid in group_ids:
|
||||
obj_np = self.controller.id_to_object_np.get(gid)
|
||||
if obj_np and not obj_np.is_empty():
|
||||
representative_id = gid
|
||||
break
|
||||
if representative_id is None:
|
||||
source_node = self._resolve_source_node_by_tree_key(owner_key)
|
||||
if not self._node_is_valid(source_node):
|
||||
continue
|
||||
|
||||
try:
|
||||
current_mat = LMatrix4f(self.controller.id_to_object_np[representative_id].get_mat(self.model))
|
||||
current_net_mat = LMatrix4f(obj_np.get_mat(self.model))
|
||||
except Exception:
|
||||
try:
|
||||
current_mat = LMatrix4f(self.controller.id_to_object_np[representative_id].getMat(self.model))
|
||||
current_net_mat = LMatrix4f(obj_np.getMat(self.model))
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if representative_id >= len(self.controller.global_transforms):
|
||||
existing_entry = grouped_entries.get(owner_key)
|
||||
if existing_entry is None:
|
||||
grouped_entries[owner_key] = {
|
||||
"source_node": source_node,
|
||||
"current_net_mat": current_net_mat,
|
||||
}
|
||||
|
||||
for owner_key in sorted(grouped_entries.keys(), key=lambda key: str(key).count("/")):
|
||||
entry = grouped_entries.get(owner_key) or {}
|
||||
source_node = entry.get("source_node")
|
||||
current_net_mat = entry.get("current_net_mat")
|
||||
if not self._node_is_valid(source_node) or current_net_mat is None:
|
||||
continue
|
||||
original_mat = LMatrix4f(self.controller.global_transforms[representative_id])
|
||||
inv_original = LMatrix4f(original_mat)
|
||||
|
||||
try:
|
||||
inv_original.invertInPlace()
|
||||
source_parent = source_node.get_parent()
|
||||
except Exception:
|
||||
try:
|
||||
inv_original.invert_in_place()
|
||||
source_parent = source_node.getParent()
|
||||
except Exception:
|
||||
source_parent = None
|
||||
|
||||
parent_net_mat = LMatrix4f.ident_mat()
|
||||
if self._node_is_valid(source_parent) and source_parent != self.source_model_root:
|
||||
try:
|
||||
parent_net_mat = LMatrix4f(source_parent.get_mat(self.source_model_root))
|
||||
except Exception:
|
||||
try:
|
||||
parent_net_mat = LMatrix4f(source_parent.getMat(self.source_model_root))
|
||||
except Exception:
|
||||
parent_net_mat = LMatrix4f.ident_mat()
|
||||
|
||||
inv_parent_mat = LMatrix4f(parent_net_mat)
|
||||
try:
|
||||
inv_parent_mat.invertInPlace()
|
||||
except Exception:
|
||||
try:
|
||||
inv_parent_mat.invert_in_place()
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
delta_mat = current_mat * inv_original
|
||||
local_mat = current_net_mat * inv_parent_mat
|
||||
try:
|
||||
source_child.set_mat(delta_mat * source_child.get_mat())
|
||||
source_node.set_mat(local_mat)
|
||||
except Exception:
|
||||
try:
|
||||
source_child.setMat(delta_mat * source_child.getMat())
|
||||
source_node.setMat(local_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 +850,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 +958,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 +989,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 +1039,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
|
||||
|
||||
@ -1372,14 +1797,22 @@ class SSBOEditor:
|
||||
|
||||
|
||||
def on_mouse_click(self):
|
||||
io = imgui.get_io()
|
||||
if io.want_capture_mouse: return
|
||||
# Skip SSBO picking when user is interacting with the TransformGizmo,
|
||||
# otherwise pick_object would clear the selection and detach the gizmo
|
||||
# before the gizmo's own mouse handler fires.
|
||||
if self._transform_gizmo and self._transform_gizmo.is_hovering:
|
||||
return
|
||||
if self.base.mouseWatcherNode.has_mouse():
|
||||
try:
|
||||
win_width, win_height = self.base.win.getSize()
|
||||
mpos = self.base.mouseWatcherNode.get_mouse()
|
||||
window_x = (float(mpos.x) + 1.0) * 0.5 * float(win_width)
|
||||
window_y = (1.0 - float(mpos.y)) * 0.5 * float(win_height)
|
||||
process_imgui_click = getattr(self.base, "processImGuiMouseClick", None)
|
||||
if callable(process_imgui_click) and process_imgui_click(window_x, window_y):
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
self._sync_pick_model_transform()
|
||||
self._refresh_ssbo_proxy_center()
|
||||
mpos = self.base.mouseWatcherNode.get_mouse()
|
||||
@ -1652,7 +2085,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
|
||||
|
||||
@ -324,7 +324,7 @@ class LUIFunctionPropertiesMixin:
|
||||
manager.luiFunction._draw_text_font_size_editor(manager, index, comp_data, comp_obj)
|
||||
|
||||
draw_color = manager.luiFunction._draw_text_color_editor(manager, index, comp_data)
|
||||
manager.luiFunction._sync_text_or_button_color(comp_obj, comp_type, draw_color)
|
||||
manager.luiFunction._sync_text_or_button_color(manager, comp_obj, comp_type, draw_color)
|
||||
|
||||
if comp_type == 'Button':
|
||||
manager.luiFunction._draw_button_texture_controls(manager, index, comp_data, comp_obj)
|
||||
@ -387,9 +387,9 @@ class LUIFunctionPropertiesMixin:
|
||||
def _sync_text_or_button_color(manager, comp_obj, comp_type, draw_color):
|
||||
# 保留原行为:每帧同步当前编辑色,避免按钮状态贴图切换后颜色不同步。
|
||||
if comp_type == 'Button':
|
||||
manager.luiFunction._apply_button_layout_color(comp_obj, draw_color)
|
||||
manager.luiFunction._apply_button_layout_color(manager, comp_obj, draw_color)
|
||||
else:
|
||||
manager.luiFunction._apply_text_color(comp_obj, draw_color)
|
||||
manager.luiFunction._apply_text_color(manager, comp_obj, draw_color)
|
||||
|
||||
imgui.separator()
|
||||
imgui.spacing()
|
||||
@ -436,7 +436,7 @@ class LUIFunctionPropertiesMixin:
|
||||
lambda: manager.luiFunction._clear_button_custom_textures(manager, comp_data, comp_obj),
|
||||
)
|
||||
|
||||
manager.luiFunction._draw_button_texture_path_labels(comp_data)
|
||||
manager.luiFunction._draw_button_texture_path_labels(manager, comp_data)
|
||||
|
||||
def _draw_button_texture_pick_button(manager, index, comp_data, comp_obj, button_label, variant):
|
||||
if not imgui.button(button_label, (160, 20)):
|
||||
@ -1382,7 +1382,7 @@ class LUIFunctionPropertiesMixin:
|
||||
if changed:
|
||||
manager._apply_component_change_with_history(
|
||||
index,
|
||||
lambda: manager.luiFunction._set_checkbox_checked(comp_data, comp_obj, new_checked),
|
||||
lambda: manager.luiFunction._set_checkbox_checked(manager, comp_data, comp_obj, new_checked),
|
||||
)
|
||||
|
||||
def _set_checkbox_checked(manager, comp_data, comp_obj, new_checked):
|
||||
@ -1497,7 +1497,7 @@ class LUIFunctionPropertiesMixin:
|
||||
sel_id = opt_ids[new_index]
|
||||
manager._apply_component_change_with_history(
|
||||
index,
|
||||
lambda: manager.luiFunction._set_selectbox_selected_option(comp_data, comp_obj, sel_id),
|
||||
lambda: manager.luiFunction._set_selectbox_selected_option(manager, comp_data, comp_obj, sel_id),
|
||||
)
|
||||
|
||||
imgui.spacing()
|
||||
@ -1526,7 +1526,7 @@ class LUIFunctionPropertiesMixin:
|
||||
if dirty:
|
||||
manager._apply_component_change_with_history(
|
||||
index,
|
||||
lambda: manager.luiFunction._apply_selectbox_options(comp_data, comp_obj, new_labels, current_selected),
|
||||
lambda: manager.luiFunction._apply_selectbox_options(manager, comp_data, comp_obj, new_labels, current_selected),
|
||||
)
|
||||
|
||||
def _set_selectbox_selected_option(manager, comp_data, comp_obj, selected_option_id):
|
||||
@ -1682,7 +1682,7 @@ class LUIFunctionPropertiesMixin:
|
||||
if imgui.button("清除音频", (90, 24)):
|
||||
manager._apply_component_change_with_history(
|
||||
index,
|
||||
lambda: manager.luiFunction._clear_audio_for_component(comp_data),
|
||||
lambda: manager.luiFunction._clear_audio_for_component(manager, comp_data),
|
||||
)
|
||||
|
||||
if imgui.button("Select Audio", (110, 24)):
|
||||
@ -1928,7 +1928,7 @@ class LUIFunctionPropertiesMixin:
|
||||
|
||||
def _clear_video_source(manager, comp_data, comp_obj=None):
|
||||
if comp_data.get("audio_from_video"):
|
||||
manager.luiFunction._clear_audio_for_component(comp_data)
|
||||
manager.luiFunction._clear_audio_for_component(manager, comp_data)
|
||||
manager.luiFunction._clear_video_audio_runtime(manager, comp_data)
|
||||
target_obj = comp_obj or comp_data.get("object")
|
||||
if target_obj is not None:
|
||||
|
||||
@ -452,29 +452,22 @@ class AppActions:
|
||||
def _is_mouse_over_imgui(self):
|
||||
"""检测鼠标是否在ImGui窗口上"""
|
||||
try:
|
||||
# 检查是否有任何ImGui窗口想要捕获鼠标
|
||||
if hasattr(imgui, 'get_io') and imgui.get_io().want_capture_mouse:
|
||||
point = self._get_mouse_window_point()
|
||||
if point and self._is_point_in_known_imgui_rects(point):
|
||||
return True
|
||||
|
||||
# 检查鼠标是否在任何ImGui窗口内
|
||||
mouse_pos = self.mouseWatcherNode.getMouse()
|
||||
if not mouse_pos:
|
||||
return False
|
||||
|
||||
# 简单的边界检查(可以根据需要扩展)
|
||||
display_size = imgui.get_io().display_size
|
||||
mouse_x = mouse_pos.get_x() * display_size.x / 2 + display_size.x / 2
|
||||
mouse_y = display_size.y - (mouse_pos.get_y() * display_size.y / 2 + display_size.y / 2)
|
||||
|
||||
# 检查是否在常见的ImGui界面区域内
|
||||
# 这里可以根据实际的ImGui窗口位置进行更精确的检测
|
||||
if mouse_x < 300 and mouse_y < 200: # 左上角区域(菜单栏)
|
||||
return True
|
||||
if mouse_x < 300 and mouse_y > display_size.y - 200: # 左下角区域(工具栏)
|
||||
return True
|
||||
if mouse_x > display_size.x - 300 and mouse_y < 200: # 右上角区域
|
||||
return True
|
||||
|
||||
|
||||
try:
|
||||
if imgui.is_any_item_active() or imgui.is_any_item_hovered():
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
if point and imgui.is_any_window_hovered() and self._is_point_in_known_imgui_rects(point):
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"ImGui界面检测失败: {e}")
|
||||
@ -484,28 +477,64 @@ class AppActions:
|
||||
def processImGuiMouseClick(self, x, y):
|
||||
"""处理ImGui鼠标点击事件,返回是否消费了该事件"""
|
||||
try:
|
||||
# ImGui优先策略:如果ImGui想要捕获鼠标,则由ImGui处理
|
||||
if hasattr(imgui, 'get_io') and imgui.get_io().want_capture_mouse:
|
||||
point = (float(x), float(y))
|
||||
if self._is_point_in_known_imgui_rects(point):
|
||||
return True
|
||||
|
||||
# 检查是否有任何ImGui窗口悬停
|
||||
|
||||
try:
|
||||
if imgui.is_any_window_hovered():
|
||||
if imgui.is_any_item_active() or imgui.is_any_item_hovered():
|
||||
return True
|
||||
except AttributeError:
|
||||
# 如果方法不存在,跳过这个检查
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 检查鼠标是否在ImGui界面区域内
|
||||
if self._is_mouse_over_imgui():
|
||||
return True
|
||||
|
||||
# 如果以上条件都不满足,则让3D场景处理该事件
|
||||
|
||||
try:
|
||||
if imgui.is_any_window_hovered() and self._is_point_in_known_imgui_rects(point):
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"ImGui鼠标点击处理失败: {e}")
|
||||
return False
|
||||
|
||||
def _get_mouse_window_point(self):
|
||||
try:
|
||||
if not self.mouseWatcherNode.hasMouse():
|
||||
return None
|
||||
mouse_pos = self.mouseWatcherNode.getMouse()
|
||||
if not mouse_pos:
|
||||
return None
|
||||
|
||||
display_size = imgui.get_io().display_size
|
||||
return (
|
||||
float(mouse_pos.get_x() * display_size.x / 2 + display_size.x / 2),
|
||||
float(display_size.y - (mouse_pos.get_y() * display_size.y / 2 + display_size.y / 2)),
|
||||
)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _point_in_rect(point, rect):
|
||||
if not point or not rect:
|
||||
return False
|
||||
x, y = point
|
||||
rx, ry, rw, rh = rect
|
||||
return rx <= x <= rx + rw and ry <= y <= ry + rh
|
||||
|
||||
def _is_point_in_known_imgui_rects(self, point):
|
||||
for rect_name in (
|
||||
"_resource_manager_window_rect",
|
||||
"_scene_tree_window_rect",
|
||||
"_property_panel_window_rect",
|
||||
"_script_panel_window_rect",
|
||||
"_console_window_rect",
|
||||
"_toolbar_window_rect",
|
||||
):
|
||||
if self._point_in_rect(point, getattr(self, rect_name, None)):
|
||||
return True
|
||||
return False
|
||||
|
||||
# ==================== 消息系统 ====================
|
||||
|
||||
@ -1264,7 +1293,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))
|
||||
@ -1377,6 +1415,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,
|
||||
|
||||
@ -19,6 +19,7 @@ class EditorPanelsLeftMixin:
|
||||
with self.app.style_manager.begin_styled_window("场景树", self.app.showSceneTree, flags) as (_, opened):
|
||||
if not opened:
|
||||
self.app.showSceneTree = False
|
||||
self.app._scene_tree_window_rect = None
|
||||
return
|
||||
|
||||
self.app.showSceneTree = opened
|
||||
@ -72,6 +73,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()])
|
||||
|
||||
@ -421,6 +431,7 @@ class EditorPanelsLeftMixin:
|
||||
with self.app.style_manager.begin_styled_window("资源管理器", self.app.showResourceManager, flags) as (_, opened):
|
||||
if not opened:
|
||||
self.app.showResourceManager = False
|
||||
self.app._resource_manager_window_rect = None
|
||||
return
|
||||
|
||||
self.app.showResourceManager = opened
|
||||
|
||||
@ -143,6 +143,7 @@ class EditorPanelsRightMixin(
|
||||
with self.app.style_manager.begin_styled_window("属性面板", self.app.showPropertyPanel, flags) as (_, opened):
|
||||
if not opened:
|
||||
self.app.showPropertyPanel = False
|
||||
self.app._property_panel_window_rect = None
|
||||
return
|
||||
|
||||
self.app.showPropertyPanel = opened
|
||||
|
||||
@ -8,7 +8,19 @@ class EditorPanelsRightMaterialMixin:
|
||||
self._material_edit_sessions = {}
|
||||
return self._material_edit_sessions
|
||||
|
||||
def _ensure_node_materials_are_editable(self, node):
|
||||
ensure_unique_fn = getattr(self.app, "_ensure_unique_materials_for_node", None)
|
||||
if callable(ensure_unique_fn):
|
||||
try:
|
||||
materials = ensure_unique_fn(node)
|
||||
if materials:
|
||||
return materials
|
||||
except Exception:
|
||||
pass
|
||||
return self.app._get_node_materials(node)
|
||||
|
||||
def _begin_material_edit_session(self, node, session_key):
|
||||
self._ensure_node_materials_are_editable(node)
|
||||
sessions = self._ensure_material_edit_sessions()
|
||||
sessions.setdefault(session_key, self.app._capture_node_material_snapshot(node))
|
||||
|
||||
@ -42,12 +54,14 @@ class EditorPanelsRightMaterialMixin:
|
||||
self._record_material_snapshot_command(node, before_snapshot, after_snapshot)
|
||||
|
||||
def _apply_material_change_with_history(self, node, apply_callback):
|
||||
self._ensure_node_materials_are_editable(node)
|
||||
before_snapshot = self.app._capture_node_material_snapshot(node)
|
||||
apply_callback()
|
||||
after_snapshot = self.app._capture_node_material_snapshot(node)
|
||||
self._record_material_snapshot_command(node, before_snapshot, after_snapshot)
|
||||
|
||||
def _select_texture_for_material_with_history(self, node, material, texture_type):
|
||||
self._ensure_node_materials_are_editable(node)
|
||||
before_snapshot = self.app._capture_node_material_snapshot(node)
|
||||
changed = self._select_texture_for_material(node, material, texture_type)
|
||||
if not changed:
|
||||
@ -58,7 +72,7 @@ class EditorPanelsRightMaterialMixin:
|
||||
|
||||
def _draw_appearance_properties(self, node):
|
||||
"""绘制材质属性(Unity风格主材质入口)。"""
|
||||
materials = self.app._get_node_materials(node)
|
||||
materials = self._ensure_node_materials_are_editable(node)
|
||||
if not materials:
|
||||
fallback_material = self.app._ensure_material_for_node(node)
|
||||
materials = [fallback_material] if fallback_material else []
|
||||
@ -168,7 +182,7 @@ class EditorPanelsRightMaterialMixin:
|
||||
|
||||
def _draw_material_properties(self, node):
|
||||
"""绘制材质属性"""
|
||||
materials = self.app._get_node_materials(node)
|
||||
materials = self._ensure_node_materials_are_editable(node)
|
||||
|
||||
if not materials:
|
||||
imgui.text_colored((0.5, 0.5, 0.5, 1.0), "无材质")
|
||||
|
||||
@ -443,9 +443,18 @@ class EditorPanelsTopMixin:
|
||||
with imgui_ctx.begin("工具栏", self.app.showToolbar, flags) as (_, opened):
|
||||
if not opened:
|
||||
self.app.showToolbar = False
|
||||
self.app._toolbar_window_rect = None
|
||||
return
|
||||
|
||||
self.app.showToolbar = opened
|
||||
window_pos = imgui.get_window_pos()
|
||||
window_size = imgui.get_window_size()
|
||||
self.app._toolbar_window_rect = (
|
||||
float(window_pos.x),
|
||||
float(window_pos.y),
|
||||
float(window_size.x),
|
||||
float(window_size.y),
|
||||
)
|
||||
command_manager = getattr(self.app, "command_manager", None)
|
||||
can_undo = command_manager.can_undo() if command_manager else False
|
||||
can_redo = command_manager.can_redo() if command_manager else False
|
||||
|
||||
@ -16,8 +16,10 @@ class PanelDelegates:
|
||||
return False
|
||||
if require_attached:
|
||||
try:
|
||||
return bool(node.hasParent())
|
||||
except Exception:
|
||||
# Add debug output
|
||||
return bool(node.has_parent() if hasattr(node, "has_parent") else node.hasParent())
|
||||
except Exception as e:
|
||||
print(f"[PanelDelegates] Error in has_parent: {e}")
|
||||
return False
|
||||
return True
|
||||
|
||||
@ -26,14 +28,16 @@ class PanelDelegates:
|
||||
ssbo_editor = getattr(self, "ssbo_editor", None)
|
||||
if ssbo_editor and hasattr(ssbo_editor, "has_active_selection") and ssbo_editor.has_active_selection():
|
||||
ssbo_node = ssbo_editor.get_selection_scene_node()
|
||||
return ssbo_node if self._node_is_valid(ssbo_node, require_attached=True) else None
|
||||
if ssbo_node and self._node_is_valid(ssbo_node, require_attached=False):
|
||||
return ssbo_node
|
||||
return None
|
||||
|
||||
selection = getattr(self, "selection", None)
|
||||
if selection and hasattr(selection, "getSelectedNode"):
|
||||
node = selection.getSelectedNode()
|
||||
else:
|
||||
node = getattr(selection, "selectedNode", None) if selection else None
|
||||
return node if self._node_is_valid(node, require_attached=True) else None
|
||||
return node if self._node_is_valid(node, require_attached=False) else None
|
||||
|
||||
def _get_ssbo_selection_summary(self):
|
||||
ssbo_editor = getattr(self, "ssbo_editor", None)
|
||||
|
||||
@ -880,6 +880,109 @@ class PropertyHelpers:
|
||||
if node and hasattr(node, "hasTag") and node.hasTag(tag_name):
|
||||
texture_tags[texture_type] = node.getTag(tag_name)
|
||||
|
||||
# Fallback: some runtime nodes inherit textures from parent/root state and
|
||||
# therefore have no explicit material_texture_* tags on the selected node.
|
||||
# Capture the actual texture bindings as stable paths so save/load can
|
||||
# persist them back into the source scene tree.
|
||||
if node and not node.isEmpty():
|
||||
texture_slots = self._get_material_texture_slots()
|
||||
slot_to_type = {slot: tex_type for tex_type, slot in texture_slots.items()}
|
||||
def record_texture_binding(texture_type, texture):
|
||||
if not texture_type or texture_type in texture_tags or not texture:
|
||||
return
|
||||
|
||||
resolved_path = ""
|
||||
try:
|
||||
if texture.hasFullpath():
|
||||
fullpath = texture.getFullpath()
|
||||
try:
|
||||
resolved_path = fullpath.toOsSpecific()
|
||||
except Exception:
|
||||
resolved_path = str(fullpath)
|
||||
except Exception:
|
||||
resolved_path = ""
|
||||
|
||||
resolved_path = os.path.normpath(str(resolved_path).strip()) if resolved_path else ""
|
||||
if not resolved_path or not os.path.exists(resolved_path):
|
||||
try:
|
||||
texture_name = os.path.normpath(str(texture.getName() or "").strip())
|
||||
except Exception:
|
||||
texture_name = ""
|
||||
if texture_name and os.path.exists(texture_name):
|
||||
resolved_path = texture_name
|
||||
|
||||
if resolved_path:
|
||||
texture_tags[texture_type] = resolved_path
|
||||
|
||||
def infer_texture_type(stage):
|
||||
if not stage:
|
||||
return None
|
||||
try:
|
||||
stage_name = (stage.getName() or "").strip().lower()
|
||||
except Exception:
|
||||
stage_name = ""
|
||||
|
||||
if stage_name.endswith("_map"):
|
||||
inferred_type = stage_name[:-4]
|
||||
if inferred_type in texture_slots:
|
||||
return inferred_type
|
||||
|
||||
try:
|
||||
return slot_to_type.get(int(stage.getSort()))
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
try:
|
||||
texture_stages = node.findAllTextureStages()
|
||||
except Exception:
|
||||
texture_stages = None
|
||||
|
||||
if texture_stages:
|
||||
for stage_index in range(texture_stages.getNumTextureStages()):
|
||||
try:
|
||||
stage = texture_stages.getTextureStage(stage_index)
|
||||
except Exception:
|
||||
continue
|
||||
texture_type = infer_texture_type(stage)
|
||||
|
||||
try:
|
||||
texture = node.getTexture(stage)
|
||||
except Exception:
|
||||
texture = None
|
||||
record_texture_binding(texture_type, texture)
|
||||
|
||||
# Some imported/runtime nodes keep effective textures only in the
|
||||
# inherited RenderState. Read TextureAttrib from net state as a
|
||||
# second fallback so SSBO save can persist those bindings too.
|
||||
if len(texture_tags) < len(texture_slots):
|
||||
try:
|
||||
from panda3d.core import TextureAttrib
|
||||
net_state = node.getNetState()
|
||||
if net_state.hasAttrib(TextureAttrib.getClassType()):
|
||||
texture_attrib = net_state.getAttrib(TextureAttrib.getClassType())
|
||||
if texture_attrib:
|
||||
try:
|
||||
num_on = texture_attrib.getNumOnStages()
|
||||
get_stage = texture_attrib.getOnStage
|
||||
get_texture = texture_attrib.getOnTexture
|
||||
except Exception:
|
||||
num_on = 0
|
||||
get_stage = None
|
||||
get_texture = None
|
||||
|
||||
for stage_index in range(int(num_on or 0)):
|
||||
if get_stage is None or get_texture is None:
|
||||
break
|
||||
try:
|
||||
stage = get_stage(stage_index)
|
||||
texture = get_texture(stage)
|
||||
except Exception:
|
||||
continue
|
||||
texture_type = infer_texture_type(stage)
|
||||
record_texture_binding(texture_type, texture)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
effect_tags = {}
|
||||
for tag_name in (
|
||||
"material_effect_metallic_enabled",
|
||||
@ -888,6 +991,14 @@ class PropertyHelpers:
|
||||
):
|
||||
effect_tags[tag_name] = bool(node and hasattr(node, "hasTag") and node.hasTag(tag_name))
|
||||
|
||||
if texture_tags:
|
||||
if texture_tags.get("metallic"):
|
||||
effect_tags["material_effect_metallic_enabled"] = True
|
||||
if texture_tags.get("parallax"):
|
||||
effect_tags["material_effect_parallax_enabled"] = True
|
||||
if any(texture_path for texture_path in texture_tags.values()):
|
||||
effect_tags["material_effect_default_texture_enabled"] = True
|
||||
|
||||
return {
|
||||
"materials": material_entries,
|
||||
"node_state": {
|
||||
@ -1095,6 +1206,142 @@ class PropertyHelpers:
|
||||
unique_materials.append(material)
|
||||
return unique_materials
|
||||
|
||||
def _clone_material_for_node(self, material, node):
|
||||
"""Clone one material so edits stay scoped to the selected node only."""
|
||||
try:
|
||||
from panda3d.core import Material
|
||||
|
||||
cloned_material = Material(material)
|
||||
source_name = ""
|
||||
try:
|
||||
source_name = material.get_name() or ""
|
||||
except Exception:
|
||||
try:
|
||||
source_name = material.getName() or ""
|
||||
except Exception:
|
||||
source_name = ""
|
||||
|
||||
node_name = self._get_node_name(node, "node") if hasattr(self, "_get_node_name") else "node"
|
||||
clone_name = f"{source_name or 'material'}__editable__{node_name}"
|
||||
try:
|
||||
cloned_material.set_name(clone_name)
|
||||
except Exception:
|
||||
try:
|
||||
cloned_material.setName(clone_name)
|
||||
except Exception:
|
||||
pass
|
||||
return cloned_material
|
||||
except Exception:
|
||||
return material
|
||||
|
||||
def _ensure_unique_materials_for_node(self, node):
|
||||
"""Detach shared/inherited materials so editing one child does not affect siblings."""
|
||||
try:
|
||||
from panda3d.core import MaterialAttrib
|
||||
|
||||
if not node or node.isEmpty():
|
||||
return []
|
||||
|
||||
materials = self._get_node_materials(node)
|
||||
if not materials:
|
||||
return []
|
||||
|
||||
current_signature = tuple(
|
||||
self._get_material_identity_key(material)
|
||||
for material in materials
|
||||
if material is not None
|
||||
)
|
||||
|
||||
try:
|
||||
stored_signature = tuple(node.getPythonTag("_editable_material_signature"))
|
||||
except Exception:
|
||||
stored_signature = None
|
||||
|
||||
if stored_signature == current_signature:
|
||||
return materials
|
||||
|
||||
node_material_key = None
|
||||
try:
|
||||
if node.hasMaterial():
|
||||
node_material_key = self._get_material_identity_key(node.getMaterial())
|
||||
except Exception:
|
||||
node_material_key = None
|
||||
|
||||
changed = False
|
||||
cloned_materials = []
|
||||
|
||||
for material in materials:
|
||||
if material is None:
|
||||
continue
|
||||
|
||||
cloned_material = self._clone_material_for_node(material, node)
|
||||
cloned_materials.append(cloned_material)
|
||||
source_key = self._get_material_identity_key(material)
|
||||
|
||||
if cloned_material is material:
|
||||
continue
|
||||
|
||||
try:
|
||||
if node_material_key is not None and source_key == node_material_key:
|
||||
node.setMaterial(cloned_material, 1)
|
||||
changed = True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
geom_paths = self._get_geom_paths_for_material(node, material)
|
||||
if not geom_paths and node_material_key is None:
|
||||
try:
|
||||
node.setMaterial(cloned_material, 1)
|
||||
changed = True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for geom_path in geom_paths:
|
||||
try:
|
||||
geom_path.setMaterial(cloned_material, 1)
|
||||
changed = True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
geom_path.setState(geom_path.getState().setAttrib(MaterialAttrib.make(cloned_material)))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
geom_node = geom_path.node()
|
||||
for geom_index in range(geom_node.getNumGeoms()):
|
||||
geom_state = geom_node.getGeomState(geom_index)
|
||||
geom_node.setGeomState(
|
||||
geom_index,
|
||||
geom_state.setAttrib(MaterialAttrib.make(cloned_material)),
|
||||
)
|
||||
changed = True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if changed:
|
||||
self._invalidate_material_render_cache()
|
||||
|
||||
latest_materials = self._get_node_materials(node)
|
||||
if not latest_materials:
|
||||
latest_materials = cloned_materials
|
||||
|
||||
latest_signature = tuple(
|
||||
self._get_material_identity_key(material)
|
||||
for material in latest_materials
|
||||
if material is not None
|
||||
)
|
||||
try:
|
||||
node.setPythonTag("_editable_material_signature", latest_signature)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return latest_materials
|
||||
except Exception as e:
|
||||
print(f"隔离节点材质实例失败: {e}")
|
||||
return self._get_node_materials(node)
|
||||
|
||||
def _get_material_identity_key(self, material):
|
||||
try:
|
||||
return getattr(material, "this", None) or id(material)
|
||||
@ -1821,7 +2068,21 @@ class PropertyHelpers:
|
||||
def _reset_material(self, node):
|
||||
"""重置节点材质"""
|
||||
try:
|
||||
materials = list(node.find_all_materials())
|
||||
materials = self._ensure_unique_materials_for_node(node)
|
||||
if not materials:
|
||||
fallback_material = self._ensure_material_for_node(node)
|
||||
materials = [fallback_material] if fallback_material else []
|
||||
|
||||
# 先清理贴图与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
|
||||
|
||||
for material in materials:
|
||||
# 重置为默认材质属性
|
||||
|
||||
@ -25,6 +25,7 @@ class ScriptPanels:
|
||||
with self.style_manager.begin_styled_window("控制台", self.app.showConsole, flags) as (_, opened):
|
||||
if not opened:
|
||||
self.app.showConsole = False
|
||||
self.app._console_window_rect = None
|
||||
return
|
||||
|
||||
self.app.showConsole = opened
|
||||
@ -119,6 +120,7 @@ class ScriptPanels:
|
||||
with self.style_manager.begin_styled_window("脚本管理", self.app.showScriptPanel, flags) as (_, opened):
|
||||
if not opened:
|
||||
self.app.showScriptPanel = False
|
||||
self.app._script_panel_window_rect = None
|
||||
return
|
||||
|
||||
self.app.showScriptPanel = opened
|
||||
|
||||
Loading…
Reference in New Issue
Block a user