优化模型导入中英文输入

This commit is contained in:
ayuan9957 2026-03-13 09:53:12 +08:00
parent b48616d5f2
commit 1cfbeda77b
26 changed files with 615 additions and 280 deletions

View File

@ -166,8 +166,15 @@ void main() {
color += ambient.diffuse; color += ambient.diffuse;
color += ambient.specular; color += ambient.specular;
color += get_sun_shading(m_out, view_dir); color += get_sun_shading(m_out, view_dir);
color += get_forward_light_shading(m_out);
// XXX: Apply shading from lights too // Transparent forward materials end up noticeably darker than the same
// object in the deferred path in this editor build, which makes them look
// "gone" when artists first switch the material type. Apply a small
// preview-space lift so the result stays visually comparable.
if (m_out.shading_model == SHADING_MODEL_TRANSPARENT) {
color *= 1.55;
}
alpha = mix(alpha, 1.0, ambient.fresnel); alpha = mix(alpha, 1.0, ambient.fresnel);

BIN
__codex_merge_debug.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

BIN
__codex_opacity_high.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

BIN
__codex_opacity_low.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 609 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 774 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 778 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

View File

@ -24,26 +24,26 @@ Size=832,45
Collapsed=0 Collapsed=0
[Window][工具栏] [Window][工具栏]
Pos=453,20 Pos=354,20
Size=1250,32 Size=1334,32
Collapsed=0 Collapsed=0
DockId=0x0000000D,0 DockId=0x0000000D,0
[Window][场景树] [Window][场景树]
Pos=0,20 Pos=0,20
Size=451,1036 Size=352,748
Collapsed=0 Collapsed=0
DockId=0x00000007,0 DockId=0x00000007,0
[Window][属性面板] [Window][属性面板]
Pos=1705,20 Pos=1690,20
Size=855,1036 Size=358,748
Collapsed=0 Collapsed=0
DockId=0x00000002,0 DockId=0x00000002,0
[Window][控制台] [Window][控制台]
Pos=1705,20 Pos=1690,20
Size=855,1036 Size=358,748
Collapsed=0 Collapsed=0
DockId=0x00000002,1 DockId=0x00000002,1
@ -60,7 +60,7 @@ Collapsed=0
[Window][WindowOverViewport_11111111] [Window][WindowOverViewport_11111111]
Pos=0,20 Pos=0,20
Size=2560,1372 Size=2048,1084
Collapsed=0 Collapsed=0
[Window][测试窗口1] [Window][测试窗口1]
@ -99,8 +99,8 @@ Size=600,500
Collapsed=0 Collapsed=0
[Window][资源管理器] [Window][资源管理器]
Pos=0,1058 Pos=0,770
Size=2560,334 Size=2048,334
Collapsed=0 Collapsed=0
DockId=0x00000006,0 DockId=0x00000006,0
@ -207,13 +207,13 @@ Collapsed=0
DockId=0x00000002,2 DockId=0x00000002,2
[Docking][Data] [Docking][Data]
DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=2560,1372 Split=Y 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=0x00000005 Parent=0x08BD597D SizeRef=2560,995 Split=X
DockNode ID=0x00000007 Parent=0x00000005 SizeRef=451,1084 Selected=0xE0015051 DockNode ID=0x00000007 Parent=0x00000005 SizeRef=352,1084 Selected=0xE0015051
DockNode ID=0x00000008 Parent=0x00000005 SizeRef=1595,1084 Split=X DockNode ID=0x00000008 Parent=0x00000005 SizeRef=2206,1084 Split=X
DockNode ID=0x00000001 Parent=0x00000008 SizeRef=1250,989 Split=Y DockNode ID=0x00000001 Parent=0x00000008 SizeRef=1846,989 Split=Y
DockNode ID=0x0000000D Parent=0x00000001 SizeRef=1318,32 HiddenTabBar=1 Selected=0x43A39006 DockNode ID=0x0000000D Parent=0x00000001 SizeRef=1318,32 HiddenTabBar=1 Selected=0x43A39006
DockNode ID=0x0000000E Parent=0x00000001 SizeRef=1318,714 CentralNode=1 DockNode ID=0x0000000E Parent=0x00000001 SizeRef=1318,714 CentralNode=1
DockNode ID=0x00000002 Parent=0x00000008 SizeRef=855,989 Selected=0x5DB6FF37 DockNode ID=0x00000002 Parent=0x00000008 SizeRef=358,989 Selected=0x5428E753
DockNode ID=0x00000006 Parent=0x08BD597D SizeRef=2560,334 Selected=0x3A2E05C3 DockNode ID=0x00000006 Parent=0x08BD597D SizeRef=2560,334 Selected=0x3A2E05C3

View File

@ -356,6 +356,9 @@ class MyWorld(PanelDelegates, CoreWorld):
self._selected_template = 0 self._selected_template = 0
self._mount_script_index = 0 self._mount_script_index = 0
self.console_command_input = "" self.console_command_input = ""
self._scene_tree_search_text = ""
self._resource_path_input = str(self.resource_manager.current_path)
self._resource_path_source = self._resource_path_input
# 变换监控相关 # 变换监控相关
self._transform_monitoring = False self._transform_monitoring = False

View File

@ -264,22 +264,35 @@ class SceneManagerIOMixin:
def expand_scene_package_wrappers(nodes): def expand_scene_package_wrappers(nodes):
expanded_nodes = [] expanded_nodes = []
ssbo_editor = getattr(self.world, "ssbo_editor", None) ssbo_editor = getattr(self.world, "ssbo_editor", None)
source_scene_model = getattr(ssbo_editor, "source_model", None) if ssbo_editor else None source_scene_model = None
source_scene_root = None
if ssbo_editor:
source_scene_model = getattr(ssbo_editor, "source_model", None)
source_scene_root = getattr(ssbo_editor, "source_model_root", None)
for node in nodes: for node in nodes:
if not node or node.isEmpty(): if not node or node.isEmpty():
continue continue
is_ssbo_runtime_root = (
ssbo_editor and
node == getattr(ssbo_editor, "model", None) and
source_scene_root and
not source_scene_root.isEmpty()
)
is_scene_wrapper = ( is_scene_wrapper = (
node.hasTag("scene_import_source") and node.hasTag("scene_import_source") and
node.getTag("scene_import_source") == "project_scene_bam" node.getTag("scene_import_source") == "project_scene_bam"
) )
if not is_scene_wrapper:
if not is_scene_wrapper and not is_ssbo_runtime_root:
expanded_nodes.append(node) expanded_nodes.append(node)
continue continue
source_children = [] source_children = []
if source_scene_model and not source_scene_model.isEmpty(): source_snapshot = source_scene_root if is_ssbo_runtime_root else source_scene_model
for child in source_scene_model.getChildren(): if source_snapshot and not source_snapshot.isEmpty():
for child in source_snapshot.getChildren():
try: try:
if not child or child.isEmpty(): if not child or child.isEmpty():
continue continue
@ -290,11 +303,11 @@ class SceneManagerIOMixin:
continue continue
if source_children: if source_children:
print(f"展开场景节点 {node.getName()} -> 使用原始场景树的 {len(source_children)} 个顶层子节点") print(f"展开SSBO场景节点 {node.getName()} -> 使用原始场景树的 {len(source_children)} 个顶层子节点")
expanded_nodes.extend(source_children) expanded_nodes.extend(source_children)
continue continue
print(f"场景节点 {node.getName()} 缺少原始场景树,回退为包装根节点保存") print(f"SSBO场景节点 {node.getName()} 缺少原始场景树,回退为包装根节点保存")
expanded_nodes.append(node) expanded_nodes.append(node)
return expanded_nodes return expanded_nodes

View File

@ -217,23 +217,17 @@ class ObjectController:
def should_hide_tree_node(self, key): def should_hide_tree_node(self, key):
""" """
Hide a redundant wrapper node directly below the file root, e.g. ROOT. Hide redundant wrapper nodes like ROOT so the UI shows the imported
This keeps `model.glb` as the visible root in the UI. model hierarchy instead of source-format packaging nodes.
""" """
node = self.tree_nodes.get(key) node = self.tree_nodes.get(key)
if not node: if not node:
return False return False
if node["parent"] != self.tree_root_key:
return False
name = (node["name"] or "").strip().lower() name = (node["name"] or "").strip().lower()
if name != "root": if name != "root":
return False return False
# Keep node visible if it actually carries direct geoms.
if node["local_ids"]:
return False
return len(node["children"]) > 0 return len(node["children"]) > 0
def _encode_id_color(self, vdata, object_id): def _encode_id_color(self, vdata, object_id):
@ -435,23 +429,17 @@ class ObjectController:
def should_hide_tree_node(self, key): def should_hide_tree_node(self, key):
""" """
Hide a redundant wrapper node directly below the file root, e.g. ROOT. Hide redundant wrapper nodes like ROOT so the UI shows the imported
This keeps `model.glb` as the visible root in the UI. model hierarchy instead of source-format packaging nodes.
""" """
node = self.tree_nodes.get(key) node = self.tree_nodes.get(key)
if not node: if not node:
return False return False
if node["parent"] != self.tree_root_key:
return False
name = (node["name"] or "").strip().lower() name = (node["name"] or "").strip().lower()
if name != "root": if name != "root":
return False return False
# Keep node visible if it actually carries direct geoms.
if node["local_ids"]:
return False
return len(node["children"]) > 0 return len(node["children"]) > 0
def _encode_id_color(self, vdata, object_id): def _encode_id_color(self, vdata, object_id):

View File

@ -66,6 +66,8 @@ class SSBOEditor:
self.model = None self.model = None
self.source_model = None self.source_model = None
self.source_model_root = None self.source_model_root = None
self.last_import_tree_key = None
self.last_import_root_name = None
self.ssbo = None self.ssbo = None
self.font_path = font_path self.font_path = font_path
self._transform_gizmo = None self._transform_gizmo = None
@ -277,11 +279,8 @@ class SSBOEditor:
except Exception as e: except Exception as e:
print(f'修复黑色模型材质时出错: {e}') print(f'修复黑色模型材质时出错: {e}')
def load_model(self, model_path, keep_source_model=False): def _load_source_model_from_path(self, model_path):
"""Load and process a model using hybrid static/dynamic chunks.""" """Load a source model NodePath from disk without touching current runtime state."""
print(f"[SSBOEditor] Loading model: {model_path}")
self.source_model = None
self.source_model_root = None
source_model = None source_model = None
last_error = None last_error = None
for fn in self._build_filename_candidates(model_path): for fn in self._build_filename_candidates(model_path):
@ -298,33 +297,245 @@ class SSBOEditor:
raise RuntimeError(f"Failed to load model '{model_path}'") raise RuntimeError(f"Failed to load model '{model_path}'")
self._fixBlackMaterials(source_model) self._fixBlackMaterials(source_model)
self._repair_missing_textures(source_model, model_path) self._repair_missing_textures(source_model, model_path)
model_name = os.path.basename(model_path) return source_model
if model_name:
source_model.set_name(model_name)
if keep_source_model: def _set_node_name(self, node, name):
self.source_model_root = NodePath(f"{model_name or 'scene'}__source_snapshot_root") if not node:
self.source_model = source_model.copyTo(self.source_model_root) return
try:
node.set_name(name)
return
except Exception:
pass
try:
node.setName(name)
except Exception:
pass
def _iter_children(self, node):
if not node:
return []
try:
return list(node.get_children())
except Exception:
try:
return list(node.getChildren())
except Exception:
return []
def _ensure_source_model_root(self):
root = self.source_model_root
if root:
try:
if not root.is_empty():
return root
except Exception:
try:
if not root.isEmpty():
return root
except Exception:
pass
self.source_model_root = NodePath("ssbo_source_scene_root")
return self.source_model_root
def _make_unique_source_child_name(self, desired_name):
root = self._ensure_source_model_root()
existing_names = set()
for child in self._iter_children(root):
try:
existing_names.add(child.get_name())
except Exception:
try:
existing_names.add(child.getName())
except Exception:
continue
base_name = desired_name or "imported_model"
if base_name not in existing_names:
return base_name
stem, ext = os.path.splitext(base_name)
index = 2
while True:
candidate = f"{stem}_{index}{ext}"
if candidate not in existing_names:
return candidate
index += 1
def _get_top_level_group_keys(self):
if not self.controller or not getattr(self.controller, "tree_root_key", None):
return []
root_key = self.controller.tree_root_key
root_node = self.controller.tree_nodes.get(root_key)
if not root_node:
return []
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."""
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:
continue
group_ids = self.controller.name_to_ids.get(key, [])
if not group_ids:
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:
continue
try:
current_mat = LMatrix4f(self.controller.id_to_object_np[representative_id].get_mat(self.model))
except Exception:
try:
current_mat = LMatrix4f(self.controller.id_to_object_np[representative_id].getMat(self.model))
except Exception:
continue
if representative_id >= len(self.controller.global_transforms):
continue
original_mat = LMatrix4f(self.controller.global_transforms[representative_id])
inv_original = LMatrix4f(original_mat)
try:
inv_original.invertInPlace()
except Exception:
try:
inv_original.invert_in_place()
except Exception:
continue
delta_mat = current_mat * inv_original
try:
source_child.set_mat(delta_mat * source_child.get_mat())
except Exception:
try:
source_child.setMat(delta_mat * source_child.getMat())
except Exception:
continue
def _clear_runtime_state(self, preserve_source_models=False):
"""Remove runtime SSBO controller/model state while optionally keeping source snapshots."""
self.clear_selection()
self._cleanup_group_proxy()
self._reset_pick_sync_cache()
controller = self.controller
pick_model = getattr(controller, "pick_model", None) if controller else None
model = self.model
removable_nodes = [pick_model, model]
if not preserve_source_models:
removable_nodes.extend([self.source_model, self.source_model_root])
for node in removable_nodes:
if not node:
continue
try:
if not node.is_empty():
node.remove_node()
except Exception:
try:
if not node.isEmpty():
node.removeNode()
except Exception:
pass
self.controller = None
self.model = None
self.selected_name = None
self.selected_ids = []
self.last_import_tree_key = None
self.last_import_root_name = None
if not preserve_source_models:
self.source_model = None
self.source_model_root = None
self._sync_pick_scene_binding()
def _rebuild_runtime_from_source_root(self, highlight_root_name=None):
root = self._ensure_source_model_root()
working_holder = NodePath("ssbo_source_scene_work")
working_root = root.copy_to(working_holder)
self.controller = ObjectController() self.controller = ObjectController()
count = self.controller.bake_ids_and_collect(source_model) count = self.controller.bake_ids_and_collect(working_root)
self.model = self.controller.model self.model = self.controller.model
self.model.reparent_to(self.base.render) self.model.reparent_to(self.base.render)
# Keep this off by default for better overall FPS/scaling with visibility.
self.set_realtime_shadow_updates(self.realtime_shadow_updates) self.set_realtime_shadow_updates(self.realtime_shadow_updates)
# NO rp.set_effect() — use RP default rendering for max FPS
# NO SSBO creation — vertex positions are baked
# Setup GPU Picking (uses simple vertex-color shader)
self.setup_gpu_picking() self.setup_gpu_picking()
# Keep pick clone aligned with source model transform.
self._sync_pick_model_transform() self._sync_pick_model_transform()
self.last_import_tree_key = None
self.last_import_root_name = highlight_root_name
if highlight_root_name:
root_key = getattr(self.controller, "tree_root_key", None)
root_node = self.controller.tree_nodes.get(root_key, {}) if root_key else {}
for child_key in root_node.get("children", []):
if self.controller.display_names.get(child_key) == highlight_root_name:
self.last_import_tree_key = child_key
break
try:
if not working_holder.is_empty():
working_holder.remove_node()
except Exception:
try:
if not working_holder.isEmpty():
working_holder.removeNode()
except Exception:
pass
print(f"[SSBOEditor] Model loaded. Total objects: {count}") print(f"[SSBOEditor] Model loaded. Total objects: {count}")
def load_model(self, model_path, keep_source_model=False, append=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)
model_name = os.path.basename(model_path)
if model_name:
self._set_node_name(source_model, model_name)
if append and self.source_model_root:
if self.controller and self.model:
self._snapshot_top_level_transforms_to_source_root()
self._clear_runtime_state(preserve_source_models=True)
else:
self._clear_runtime_state(preserve_source_models=False)
source_root = self._ensure_source_model_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)
self._set_node_name(imported_root, unique_root_name)
if keep_source_model and not append:
self.source_model = imported_root
else:
self.source_model = source_root
self._rebuild_runtime_from_source_root(highlight_root_name=unique_root_name)
def _build_filename_candidates(self, path_text): def _build_filename_candidates(self, path_text):
"""Build Filename candidates with wide-char first for Windows CJK paths.""" """Build Filename candidates with wide-char first for Windows CJK paths."""
candidates = [] candidates = []
@ -1347,36 +1558,7 @@ class SSBOEditor:
def reset_scene_state(self): def reset_scene_state(self):
"""Remove the current SSBO model/controller state before loading another scene.""" """Remove the current SSBO model/controller state before loading another scene."""
self.clear_selection() self._clear_runtime_state(preserve_source_models=False)
self._cleanup_group_proxy()
self._reset_pick_sync_cache()
controller = self.controller
pick_model = getattr(controller, "pick_model", None) if controller else None
model = self.model
source_model = self.source_model
source_model_root = self.source_model_root
for node in (pick_model, model, source_model, source_model_root):
if not node:
continue
try:
if not node.is_empty():
node.remove_node()
except Exception:
try:
if not node.isEmpty():
node.removeNode()
except Exception:
pass
self.controller = None
self.model = None
self.source_model = None
self.source_model_root = None
self.selected_name = None
self.selected_ids = []
self._sync_pick_scene_binding()
def _cleanup_group_proxy(self): def _cleanup_group_proxy(self):
"""Reparent objects back to their chunk and remove the group proxy.""" """Reparent objects back to their chunk and remove the group proxy."""

View File

@ -456,13 +456,17 @@ class ImGuiBackend(DirectObject):
# Some Panda3D / Windows combinations do not emit a reliable "keystroke" # Some Panda3D / Windows combinations do not emit a reliable "keystroke"
# event for ImGui text fields. Fall back to printable key-down events # event for ImGui text fields. Fall back to printable key-down events
# until we observe real text events in this session. # until we observe real text events in this session.
# On Windows, do not block this fallback just because IME is open:
# many Chinese input methods stay "open" even while typing raw
# English / digits. We only suppress ASCII synthesis while an IME
# composition is actively in progress.
if ( if (
down down
and not self._keystroke_observed and not self._keystroke_observed
and not self.io.key_ctrl and not self.io.key_ctrl
and not self.io.key_alt and not self.io.key_alt
and not self.io.key_super and not self.io.key_super
and not (sys.platform == "win32" and self._ime_open and self.io.want_text_input) and not (sys.platform == "win32" and self._ime_composing and self.io.want_text_input)
): ):
text = self.__resolve_text_input(keyName, button) text = self.__resolve_text_input(keyName, button)
if had_shift: if had_shift:

View File

@ -1018,28 +1018,10 @@ class AppActions:
except Exception: except Exception:
pass pass
# Remove legacy scene-manager models to avoid duplicate rendering
if hasattr(self, 'scene_manager') and self.scene_manager and hasattr(self.scene_manager, 'models'):
for m in list(self.scene_manager.models):
try:
if m and not m.isEmpty():
m.removeNode()
except Exception:
pass
self.scene_manager.models = []
# Replace previous SSBO model
old_model = getattr(self.ssbo_editor, 'model', None)
if old_model is not None:
try:
if not old_model.isEmpty():
old_model.removeNode()
except Exception:
pass
self.ssbo_editor.load_model( self.ssbo_editor.load_model(
file_path, file_path,
keep_source_model=scene_package_import, keep_source_model=scene_package_import,
append=not scene_package_import,
) )
model_np = getattr(self.ssbo_editor, 'model', None) model_np = getattr(self.ssbo_editor, 'model', None)
# Keep legacy ray-pick fallback usable by adding a collision body. # Keep legacy ray-pick fallback usable by adding a collision body.
@ -1058,7 +1040,20 @@ class AppActions:
model_np.setTag("is_model_root", "1") model_np.setTag("is_model_root", "1")
model_np.setTag("is_scene_element", "1") model_np.setTag("is_scene_element", "1")
model_np.setTag("file", os.path.basename(file_path)) model_np.setTag("file", os.path.basename(file_path))
model_np.setName(os.path.basename(file_path)) ssbo_source_root = getattr(self.ssbo_editor, "source_model_root", None)
source_children = []
if ssbo_source_root is not None:
try:
source_children = list(ssbo_source_root.getChildren())
except Exception:
try:
source_children = list(ssbo_source_root.get_children())
except Exception:
source_children = []
if len(source_children) > 1 and not scene_package_import:
model_np.setName("导入模型")
else:
model_np.setName(os.path.basename(file_path))
if hasattr(self, 'scene_manager') and self.scene_manager: if hasattr(self, 'scene_manager') and self.scene_manager:
try: try:
@ -1071,6 +1066,9 @@ class AppActions:
self.scene_manager._processModelAnimations(model_np) self.scene_manager._processModelAnimations(model_np)
except Exception as e: except Exception as e:
print(f"[SSBO] setup components failed: {e}") print(f"[SSBO] setup components failed: {e}")
if hasattr(self, 'scene_manager') and self.scene_manager and hasattr(self.scene_manager, 'models'):
self.scene_manager.models = [model_np]
return model_np return model_np
except Exception as e: except Exception as e:
print(f"[SSBO] load_model failed: {e}") print(f"[SSBO] load_model failed: {e}")
@ -1143,14 +1141,24 @@ class AppActions:
except Exception as e: except Exception as e:
self.add_warning_message(f"材质处理警告: {e}") self.add_warning_message(f"材质处理警告: {e}")
if set_origin: if set_origin and not getattr(self, "use_ssbo_mouse_picking", False):
model_node.setPos(0, 0, 0) model_node.setPos(0, 0, 0)
if hasattr(self.scene_manager, 'models'): if hasattr(self.scene_manager, 'models'):
self.scene_manager.models.append(model_node) if getattr(self, "use_ssbo_mouse_picking", False):
self.scene_manager.models = [model_node]
else:
self.scene_manager.models.append(model_node)
if select_model and hasattr(self, 'selection') and self.selection: if select_model:
self.selection.updateSelection(model_node) if (
getattr(self, "use_ssbo_mouse_picking", False)
and getattr(self, "ssbo_editor", None)
and getattr(self.ssbo_editor, "last_import_tree_key", None)
):
self.ssbo_editor.select_node(self.ssbo_editor.last_import_tree_key)
elif hasattr(self, 'selection') and self.selection:
self.selection.updateSelection(model_node)
if show_success_message: if show_success_message:
self.add_success_message(f"模型导入成功: {file_name}") self.add_success_message(f"模型导入成功: {file_name}")

View File

@ -24,59 +24,172 @@ class EditorPanelsLeftMixin:
float(window_size.y), float(window_size.y),
) )
imgui.text("场景层级") self._draw_scene_tree_header()
imgui.separator() imgui.separator()
# 构建动态场景树
self._build_scene_tree() self._build_scene_tree()
def _draw_scene_tree_header(self):
"""绘制场景树头部摘要与检索框。"""
model_count = self._get_scene_tree_model_display_count()
selected_node = self.app._get_selection_node()
selected_name = "未选择"
if selected_node and not selected_node.isEmpty():
selected_name = selected_node.getName() or "未命名对象"
imgui.text_disabled(f"模型 {model_count}")
imgui.same_line()
imgui.text_disabled(f"当前: {selected_name}")
imgui.set_next_item_width(-64)
changed, search_text = imgui.input_text("##scene_tree_search", self.app._scene_tree_search_text, 256)
if changed:
self.app._scene_tree_search_text = search_text
imgui.same_line()
if imgui.button("清空##scene_tree_search"):
self.app._scene_tree_search_text = ""
def _get_scene_tree_filter(self):
return getattr(self.app, "_scene_tree_search_text", "").strip().lower()
def _get_scene_tree_models(self):
models = []
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()])
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() and ssbo_model not in models:
models.append(ssbo_model)
return models
def _get_ssbo_top_level_model_keys(self):
ssbo_editor = getattr(self.app, "ssbo_editor", None)
controller = getattr(ssbo_editor, "controller", None) if ssbo_editor else None
if not ssbo_editor or not controller or not getattr(controller, "tree_root_key", None):
return []
root_node = controller.tree_nodes.get(controller.tree_root_key)
if not root_node:
return []
keys = []
for child_key in root_node.get("children", []):
if not self._ssbo_key_matches_scene_filter(controller, child_key):
continue
keys.append(child_key)
return keys
def _get_scene_tree_model_display_count(self):
models = self._get_scene_tree_models()
ssbo_editor = getattr(self.app, "ssbo_editor", None)
ssbo_model = getattr(ssbo_editor, "model", None) if ssbo_editor else None
count = 0
for model in models:
if ssbo_model and model == ssbo_model:
count += len(self._get_ssbo_top_level_model_keys())
else:
count += 1
return count
def _ssbo_key_matches_scene_filter(self, controller, key):
filter_text = self._get_scene_tree_filter()
if not filter_text:
return True
node_data = controller.tree_nodes.get(key)
if not node_data:
return False
display_name = controller.display_names.get(key, key)
if filter_text in display_name.lower() or filter_text in str(key).lower():
return True
return any(self._ssbo_key_matches_scene_filter(controller, child_key) for child_key in node_data["children"])
def _node_matches_scene_filter(self, node, name):
filter_text = self._get_scene_tree_filter()
if not filter_text:
return True
display_name = (name or "").lower()
node_name = ""
try:
node_name = (node.getName() or "").lower()
except Exception:
node_name = ""
if filter_text in display_name or filter_text in node_name:
return True
ssbo_editor = getattr(self.app, "ssbo_editor", None)
controller = getattr(ssbo_editor, "controller", None) if ssbo_editor else None
ssbo_model = getattr(ssbo_editor, "model", None) if ssbo_editor else None
if controller and ssbo_model and node == ssbo_model:
root_key = getattr(controller, "tree_root_key", None)
if root_key and self._ssbo_key_matches_scene_filter(controller, root_key):
return True
try:
for child in node.getChildren():
if not child or child.isEmpty():
continue
child_name = child.getName() or ""
if child_name.startswith("modelCollision_"):
continue
if self._node_matches_scene_filter(child, child_name):
return True
except Exception:
pass
return False
def _build_scene_tree(self): def _build_scene_tree(self):
"""构建动态场景树""" """构建动态场景树"""
# 渲染节点 render_entries = []
if imgui.tree_node("渲染"): if hasattr(self.app, "ambient_light") and self.app.ambient_light:
# 环境光 render_entries.append((self.app.ambient_light, "环境光", "light"))
if hasattr(self.app, 'ambient_light') and self.app.ambient_light:
self._draw_scene_node(self.app.ambient_light, "环境光", "light")
# 聚光灯 if hasattr(self.app, "scene_manager") and self.app.scene_manager:
if hasattr(self.app, 'scene_manager') and self.app.scene_manager: if hasattr(self.app.scene_manager, "Spotlight") and self.app.scene_manager.Spotlight:
if hasattr(self.app.scene_manager, 'Spotlight') and self.app.scene_manager.Spotlight: for i, spotlight in enumerate(self.app.scene_manager.Spotlight):
for i, spotlight in enumerate(self.app.scene_manager.Spotlight): render_entries.append((spotlight, f"聚光灯_{i + 1}", "light"))
self._draw_scene_node(spotlight, f"聚光灯_{i+1}", "light") if hasattr(self.app.scene_manager, "Pointlight") and self.app.scene_manager.Pointlight:
if hasattr(self.app.scene_manager, 'Pointlight') and self.app.scene_manager.Pointlight: for i, pointlight in enumerate(self.app.scene_manager.Pointlight):
for i, pointlight in enumerate(self.app.scene_manager.Pointlight): render_entries.append((pointlight, f"点光源_{i + 1}", "light"))
self._draw_scene_node(pointlight, f"点光源_{i+1}", "light")
# 地板 if hasattr(self.app, "ground") and self.app.ground:
if hasattr(self.app, 'ground') and self.app.ground: render_entries.append((self.app.ground, "地板", "geometry"))
self._draw_scene_node(self.app.ground, "地板", "geometry")
render_entries = [entry for entry in render_entries if self._node_matches_scene_filter(entry[0], entry[1])]
if render_entries and imgui.tree_node(f"渲染 ({len(render_entries)})"):
for node, name, node_type in render_entries:
self._draw_scene_node(node, name, node_type)
imgui.tree_pop() imgui.tree_pop()
# 相机节点 camera_entries = []
if imgui.tree_node("相机"): if hasattr(self.app, "camera") and self.app.camera and self._node_matches_scene_filter(self.app.camera, "主相机"):
if hasattr(self.app, 'camera') and self.app.camera: camera_entries.append((self.app.camera, "主相机", "camera"))
self._draw_scene_node(self.app.camera, "主相机", "camera") if camera_entries and imgui.tree_node(f"相机 ({len(camera_entries)})"):
for node, name, node_type in camera_entries:
self._draw_scene_node(node, name, node_type)
imgui.tree_pop() imgui.tree_pop()
# 3D模型节点 models = [model for model in self._get_scene_tree_models() if self._node_matches_scene_filter(model, model.getName() or "模型")]
if imgui.tree_node("模型"): model_display_count = self._get_scene_tree_model_display_count()
models = [] if model_display_count and imgui.tree_node(f"模型 ({model_display_count})"):
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()])
# SSBO模式下模型可能不在 scene_manager.models 中,补充显示 ssbo_editor.model
ssbo_editor = getattr(self.app, "ssbo_editor", None) ssbo_editor = getattr(self.app, "ssbo_editor", None)
ssbo_model = getattr(ssbo_editor, "model", None) if ssbo_editor else 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() and ssbo_model not in models: controller = getattr(ssbo_editor, "controller", None) if ssbo_editor else None
models.append(ssbo_model)
if models: for i, model in enumerate(models):
for i, model in enumerate(models): if ssbo_model and controller and model == ssbo_model:
self._draw_scene_node(model, model.getName() or f"模型_{i+1}", "model") for child_key in self._get_ssbo_top_level_model_keys():
else: self._draw_ssbo_virtual_tree_node(ssbo_editor, controller, child_key)
imgui.text("(空)") continue
self._draw_scene_node(model, model.getName() or f"模型_{i + 1}", "model")
imgui.tree_pop() imgui.tree_pop()
elif not render_entries and not camera_entries and not model_display_count and self._get_scene_tree_filter():
imgui.text_disabled("没有匹配的 3D 节点")
# if imgui.tree_node("GUI元素"): # if imgui.tree_node("GUI元素"):
# if hasattr(self,'gui_manager') and self.app.gui_manager and hasattr(self.app.gui_manager,'gui_elements'): # if hasattr(self,'gui_manager') and self.app.gui_manager and hasattr(self.app.gui_manager,'gui_elements'):
@ -121,6 +234,8 @@ class EditorPanelsLeftMixin:
"""绘制单个场景节点""" """绘制单个场景节点"""
if not node or node.isEmpty(): if not node or node.isEmpty():
return return
if not self._node_matches_scene_filter(node, name):
return
# 检查是否被选中 # 检查是否被选中
is_selected = (self.app._get_selection_node() == node) is_selected = (self.app._get_selection_node() == node)
@ -234,6 +349,9 @@ class EditorPanelsLeftMixin:
def _draw_ssbo_virtual_tree_node(self, ssbo_editor, controller, key): def _draw_ssbo_virtual_tree_node(self, ssbo_editor, controller, key):
"""Recursively draw SSBO tree_nodes hierarchy in scene tree.""" """Recursively draw SSBO tree_nodes hierarchy in scene tree."""
if not self._ssbo_key_matches_scene_filter(controller, key):
return
node_data = controller.tree_nodes.get(key) node_data = controller.tree_nodes.get(key)
if not node_data: if not node_data:
return return
@ -291,16 +409,12 @@ class EditorPanelsLeftMixin:
self._update_resource_manager_window_rect(rm) self._update_resource_manager_window_rect(rm)
imgui.text("文件浏览器")
imgui.separator()
self._draw_resource_toolbar_and_filters(rm)
imgui.separator()
rm.refresh_if_needed() rm.refresh_if_needed()
dirs, files = rm.get_directory_contents(rm.current_path) dirs, files = rm.get_directory_contents(rm.current_path)
self._draw_resource_manager_header(rm, dirs, files)
imgui.separator()
self._draw_resource_toolbar_and_filters(rm)
imgui.separator()
self._draw_resource_directory_entries(rm, dirs) self._draw_resource_directory_entries(rm, dirs)
self._draw_resource_file_entries(rm, files) self._draw_resource_file_entries(rm, files)
self._draw_resource_context_menu(rm) self._draw_resource_context_menu(rm)
@ -456,8 +570,42 @@ class EditorPanelsLeftMixin:
if imgui.is_item_hovered() and imgui.is_mouse_clicked(1): if imgui.is_item_hovered() and imgui.is_mouse_clicked(1):
self._open_resource_item_context_menu(rm, file_path) self._open_resource_item_context_menu(rm, file_path)
def _draw_resource_manager_header(self, rm, dirs, files):
"""绘制资源管理器头部摘要。"""
total_items = len(dirs) + len(files)
try:
location = rm.current_path.relative_to(rm.project_root).as_posix()
except ValueError:
location = str(rm.current_path)
imgui.text_disabled(location or ".")
imgui.same_line()
imgui.text_disabled(f"{total_items}")
if rm.selected_files:
imgui.same_line()
imgui.text_colored((0.5, 0.8, 1.0, 1.0), f"已选 {len(rm.selected_files)}")
def _sync_resource_path_input(self, rm):
current_path_text = str(rm.current_path)
if getattr(self.app, "_resource_path_source", "") != current_path_text:
self.app._resource_path_input = current_path_text
self.app._resource_path_source = current_path_text
def _navigate_resource_path_from_input(self, rm):
raw_path = (getattr(self.app, "_resource_path_input", "") or "").strip().strip('"')
if not raw_path:
return
try:
rm.navigate_to(Path(raw_path))
except Exception:
return
self.app._resource_path_input = str(rm.current_path)
self.app._resource_path_source = self.app._resource_path_input
def _draw_resource_toolbar_and_filters(self, rm): def _draw_resource_toolbar_and_filters(self, rm):
"""绘制资源管理器顶部工具条与筛选输入。保持原有按钮顺序与文案。""" """绘制资源管理器顶部工具条与筛选输入。"""
self._sync_resource_path_input(rm)
if imgui.button(""): if imgui.button(""):
rm.navigate_back() rm.navigate_back()
imgui.same_line() imgui.same_line()
@ -479,20 +627,21 @@ class EditorPanelsLeftMixin:
if changed: if changed:
rm.set_auto_refresh(rm.auto_refresh_enabled) rm.set_auto_refresh(rm.auto_refresh_enabled)
imgui.same_line() imgui.text_disabled("路径")
imgui.text(" ") imgui.set_next_item_width(-72)
imgui.same_line() changed, new_path = imgui.input_text("##resource_path", self.app._resource_path_input, 512)
# 路径输入框
changed, new_path = imgui.input_text("路径", str(rm.current_path), 256)
if changed: if changed:
try: self.app._resource_path_input = new_path
rm.navigate_to(Path(new_path)) imgui.same_line()
except Exception: if imgui.button("前往##resource_go"):
pass self._navigate_resource_path_from_input(rm)
# 搜索框 imgui.text_disabled("搜索")
changed, rm.search_filter = imgui.input_text("搜索", rm.search_filter, 256) imgui.set_next_item_width(-72)
changed, rm.search_filter = imgui.input_text("##resource_search", rm.search_filter, 256)
imgui.same_line()
if imgui.button("清除##resource_search"):
rm.search_filter = ""
def _load_resource_icon(self, icon_name: str): def _load_resource_icon(self, icon_name: str):
"""加载资源图标;失败时返回 None。""" """加载资源图标;失败时返回 None。"""

View File

@ -45,35 +45,14 @@ class EditorPanelsRightMixin(
if ssbo_summary: if ssbo_summary:
self._draw_ssbo_selection_summary(ssbo_summary) self._draw_ssbo_selection_summary(ssbo_summary)
return return
# 无选中对象时显示提示模仿Qt版本的空状态样式 self._draw_empty_property_state()
imgui.spacing()
imgui.spacing()
# 居中显示提示信息 def _draw_empty_property_state(self):
window_width = imgui.get_window_width() imgui.separator_text("属性")
text_width = 200 # 估算文本宽度 imgui.text_disabled("当前没有选中对象")
text_pos_x = (window_width - text_width) / 2 imgui.spacing()
imgui.bullet_text("从左侧场景树或场景视口中选择一个对象")
imgui.set_cursor_pos_x(text_pos_x) imgui.bullet_text("按 F 聚焦到对象,按 Delete 删除对象")
imgui.text_colored((0.5, 0.5, 0.5, 1.0), "🔍 未选择任何对象")
imgui.set_cursor_pos_x(text_pos_x - 20)
imgui.text("请从场景树中选择一个对象")
imgui.set_cursor_pos_x(text_pos_x + 10)
imgui.text("以查看其属性")
imgui.spacing()
imgui.spacing()
# 添加一些分隔线和装饰
imgui.separator()
# 显示快速提示
imgui.text("💡 快速提示:")
imgui.bullet_text("单击场景树中的对象进行选择")
imgui.bullet_text("使用 F 键快速聚焦到选中对象")
imgui.bullet_text("使用 Delete 键删除选中对象")
def _draw_property_section(self, title, draw_callback, default_open=False): def _draw_property_section(self, title, draw_callback, default_open=False):
flags = imgui.TreeNodeFlags_.span_avail_width.value flags = imgui.TreeNodeFlags_.span_avail_width.value
@ -85,18 +64,15 @@ class EditorPanelsRightMixin(
def _draw_ssbo_selection_summary(self, summary): def _draw_ssbo_selection_summary(self, summary):
"""Render a safe summary for SSBO group selections without exposing wrong node properties.""" """Render a safe summary for SSBO group selections without exposing wrong node properties."""
imgui.spacing() imgui.separator_text("SSBO 选择")
imgui.text("SSBO 选择摘要")
imgui.separator()
imgui.text(f"名称: {summary.get('display_name') or '未命名'}") imgui.text(f"名称: {summary.get('display_name') or '未命名'}")
imgui.text(f"对象数量: {summary.get('object_count', 0)}") imgui.text(f"对象数量: {summary.get('object_count', 0)}")
if summary.get("is_root"): if summary.get("is_root"):
imgui.text_colored((0.5, 0.8, 1.0, 1.0), "当前选中的是 SSBO 根模型") imgui.text_colored((0.5, 0.8, 1.0, 1.0), "当前选中的是 SSBO 根模型")
elif summary.get("is_group"): elif summary.get("is_group"):
imgui.text_colored((1.0, 0.8, 0.3, 1.0), "当前选中的是 SSBO 组合节点") imgui.text_colored((1.0, 0.8, 0.3, 1.0), "当前选中的是 SSBO 组合节点")
imgui.text_wrapped("这个选择对应多个动态对象。为避免误改错误节点,这里先显示摘要信息") imgui.text_wrapped("这个选择对应多个动态对象。这里先显示摘要,避免误改到错误节点")
imgui.separator() imgui.separator()
imgui.text("建议:")
imgui.bullet_text("在左侧展开到叶子节点后查看单对象属性") imgui.bullet_text("在左侧展开到叶子节点后查看单对象属性")
imgui.bullet_text("继续使用场景中的 Gizmo 做组合移动") imgui.bullet_text("继续使用场景中的 Gizmo 做组合移动")
else: else:
@ -166,78 +142,56 @@ class EditorPanelsRightMixin(
except Exception: except Exception:
parent = None parent = None
imgui.text_disabled(f"{node_type} | Parent: {parent_name}") imgui.text_disabled(f"{node_type} · 父级: {parent_name}")
self._draw_status_badges(node, node_type)
if hasattr(node, "getPythonTag") and node.getPythonTag("script"): def _draw_status_badges(self, node, node_type=None):
imgui.same_line() """绘制精简后的对象状态徽章行。"""
imgui.text_colored((0.8, 0.4, 0.8, 1.0), "[Script]") if node_type is None:
node_type = self.app._get_node_type_from_node(node)
if node_type == "模型" and node.hasTag("has_animations") and node.getTag("has_animations").lower() == "true": badges = []
imgui.same_line()
imgui.text_colored((0.4, 0.8, 0.4, 1.0), "[Animation]")
def _draw_status_badges(self, node): if node.is_hidden():
"""绘制对象状态徽章行。""" badges.append(("已隐藏", (0.65, 0.65, 0.65, 1.0)))
is_visible = not node.is_hidden()
visibility_color = (0.176, 1.0, 0.769, 1.0) if is_visible else (0.953, 0.616, 0.471, 1.0)
visibility_text = "Visible" if is_visible else "Hidden"
badges = [(visibility_text, visibility_color)] has_collision = hasattr(node, "getChild") and any(
"Collision" in child.getName() for child in node.getChildren() if child.getName()
)
if has_collision:
badges.append(("碰撞", (0.35, 0.65, 1.0, 1.0)))
node_type = self._get_node_type_from_node(node) has_script = hasattr(node, "getPythonTag") and node.getPythonTag("script")
type_colors = { if has_script:
"GUI元素": (0.188, 0.404, 0.753, 1.0), badges.append(("脚本", (0.86, 0.48, 0.86, 1.0)))
"光源": (1.0, 0.8, 0.2, 1.0),
"模型": (0.6, 0.8, 1.0, 1.0),
"相机": (0.8, 0.8, 0.2, 1.0),
"几何体": (0.5, 0.5, 0.5, 1.0),
}
if node_type in type_colors:
badges.append((node_type, type_colors[node_type]))
has_collision = hasattr(node, "getChild") and any( has_animation = False
"Collision" in child.getName() for child in node.getChildren() if child.getName() if node_type == "模型":
) if node.hasTag("has_animations"):
if has_collision: has_animation = node.getTag("has_animations").lower() == "true"
badges.append(("Collision", (0.2, 0.4, 0.8, 1.0))) if not has_animation:
has_script = hasattr(node, "getPythonTag") and node.getPythonTag("script")
if has_script:
badges.append(("Script", (0.8, 0.4, 0.8, 1.0)))
has_animation = False
if node_type == "模型":
if node.hasTag("has_animations"):
has_animation = node.getTag("has_animations").lower() == "true"
if not has_animation:
try:
has_character = node.findAllMatches("**/+Character").getNumPaths() > 0
has_bundle = node.findAllMatches("**/+AnimBundleNode").getNumPaths() > 0
has_animation = has_character or has_bundle
if has_animation:
node.setTag("has_animations", "true")
node.setTag("can_create_actor_from_memory", "true")
except Exception:
pass
cached_result = node.getPythonTag("animation") cached_result = node.getPythonTag("animation")
if cached_result is True: if cached_result is True:
has_animation = True has_animation = True
elif cached_result is False: elif cached_result is False:
has_animation = False has_animation = False
else: else:
has_animation = hasattr(node, "getPythonTag") and node.getPythonTag("animation") has_animation = hasattr(node, "getPythonTag") and node.getPythonTag("animation")
if has_animation: if has_animation:
badges.append(("Animation", (0.4, 0.8, 0.4, 1.0))) badges.append(("动画", (0.45, 0.85, 0.55, 1.0)))
has_material = hasattr(node, "getMaterial") and node.getMaterial() has_material = hasattr(node, "getMaterial") and node.getMaterial()
if has_material: if has_material:
badges.append(("Material", (0.8, 0.6, 0.2, 1.0))) badges.append(("材质", (0.9, 0.72, 0.35, 1.0)))
for index, (badge_text, badge_color) in enumerate(badges): if not badges:
if index > 0: return
imgui.same_line()
imgui.text_colored(badge_color, f"[{badge_text}]") for index, (badge_text, badge_color) in enumerate(badges):
if index > 0:
imgui.same_line()
imgui.text_colored(badge_color, f"[{badge_text}]")
def _draw_gui_properties(self, node): def _draw_gui_properties(self, node):
"""绘制GUI元素属性""" """绘制GUI元素属性"""

View File

@ -91,9 +91,10 @@ class EditorPanelsRightMaterialMixin:
if self.app._get_material_surface_type(material) == 3: if self.app._get_material_surface_type(material) == 3:
opacity = self.app._get_material_opacity(material) opacity = self.app._get_material_opacity(material)
changed, new_opacity = imgui.slider_float("透明度", opacity, 0.0, 1.0) transparency = 1.0 - opacity
changed, new_transparency = imgui.slider_float("透明度", transparency, 0.0, 1.0)
if changed: if changed:
apply_opacity(new_opacity) apply_opacity(1.0 - new_transparency)
imgui.separator() imgui.separator()
@ -210,9 +211,15 @@ class EditorPanelsRightMaterialMixin:
imgui.text("透明度设置") imgui.text("透明度设置")
try: try:
current_opacity = self.app._get_material_opacity(material) current_opacity = self.app._get_material_opacity(material)
changed, new_opacity = imgui.slider_float(f"不透明度##opacity_{material_index}", current_opacity, 0.0, 1.0) current_transparency = 1.0 - current_opacity
changed, new_transparency = imgui.slider_float(
f"透明度##opacity_{material_index}",
current_transparency,
0.0,
1.0,
)
if changed: if changed:
self.app._set_material_opacity(node, material, new_opacity) self.app._set_material_opacity(node, material, 1.0 - new_transparency)
except: except:
imgui.text_colored((0.7, 0.7, 0.7, 1.0), "透明度控制不可用") imgui.text_colored((0.7, 0.7, 0.7, 1.0), "透明度控制不可用")

View File

@ -751,13 +751,20 @@ class PropertyHelpers:
if previous_surface_type == 3: if previous_surface_type == 3:
opacity = self._get_material_opacity(material) opacity = self._get_material_opacity(material)
else: else:
base_alpha = float(self._get_material_base_color(material)[3]) # Entering transparent mode should keep the object fully
opacity = base_alpha if 0.0 < base_alpha <= 1.0 else 1.0 # visible by default. Reusing historical base-color alpha
# makes some assets appear to "disappear" immediately.
opacity = 0.92
material.set_emission(Vec4(float(surface_type), float(emission.y), float(emission.z), float(emission.w)))
self._set_material_opacity(node, material, opacity)
else: else:
opacity = 1.0 opacity = 1.0
material.set_emission(Vec4(float(surface_type), float(emission.y), opacity, float(emission.w))) material.set_emission(Vec4(float(surface_type), float(emission.y), opacity, float(emission.w)))
self._apply_material_to_geom_states(node, material) base_color = list(self._get_material_base_color(material))
self._apply_material_surface_state(node, material) base_color[3] = 1.0
self._set_material_base_color(material, tuple(base_color))
self._apply_material_to_geom_states(node, material)
self._apply_material_surface_state(node, material)
if refresh_pipeline: if refresh_pipeline:
self._refresh_pipeline_material_mode(node, material) self._refresh_pipeline_material_mode(node, material)
except Exception as e: except Exception as e:
@ -789,9 +796,16 @@ class PropertyHelpers:
if not render_pipeline or not node or node.isEmpty(): if not render_pipeline or not node or node.isEmpty():
return return
if self._material_uses_transparent_pass(material) and hasattr(render_pipeline, "prepare_scene"): if self._material_uses_transparent_pass(material) and hasattr(render_pipeline, "prepare_scene"):
current_opacity = self._get_material_opacity(material)
self._bake_effective_geom_materials(node) self._bake_effective_geom_materials(node)
self._isolate_transparent_geoms(node) split_changed = self._isolate_transparent_geoms(node)
if split_changed:
self._bake_effective_geom_materials(node)
self._apply_material_surface_state(node, material)
render_pipeline.prepare_scene(node) render_pipeline.prepare_scene(node)
self._set_material_opacity(node, material, current_opacity)
if split_changed:
self._apply_material_surface_state(node, material)
except Exception as e: except Exception as e:
print(f"刷新RenderPipeline材质模式失败: {e}") print(f"刷新RenderPipeline材质模式失败: {e}")
@ -1105,6 +1119,12 @@ class PropertyHelpers:
surface_type = self._get_material_surface_type(material) surface_type = self._get_material_surface_type(material)
if surface_type != 3: if surface_type != 3:
surface_type = 3 surface_type = 3
else:
# The RP forward transparent path in this editor build becomes
# visually unusable at exact opacity 1.0. Treat transparent
# mode as a slightly blended surface; users can switch back to
# "不透明" when they want a fully opaque result.
opacity_value = min(opacity_value, 0.92)
material.set_emission(Vec4(float(surface_type), float(emission.y), opacity_value, float(emission.w))) material.set_emission(Vec4(float(surface_type), float(emission.y), opacity_value, float(emission.w)))
base_color = list(self._get_material_base_color(material)) base_color = list(self._get_material_base_color(material))