diff --git a/RenderPipelineFile/rpcore/shader/templates/forward.frag.glsl b/RenderPipelineFile/rpcore/shader/templates/forward.frag.glsl index 68937bbe..10036654 100644 --- a/RenderPipelineFile/rpcore/shader/templates/forward.frag.glsl +++ b/RenderPipelineFile/rpcore/shader/templates/forward.frag.glsl @@ -166,8 +166,15 @@ void main() { color += ambient.diffuse; color += ambient.specular; 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); diff --git a/__codex_merge_debug.png b/__codex_merge_debug.png new file mode 100644 index 00000000..073e6b10 Binary files /dev/null and b/__codex_merge_debug.png differ diff --git a/__codex_opacity_high.png b/__codex_opacity_high.png new file mode 100644 index 00000000..bb5401fd Binary files /dev/null and b/__codex_opacity_high.png differ diff --git a/__codex_opacity_low.png b/__codex_opacity_low.png new file mode 100644 index 00000000..3c2f7892 Binary files /dev/null and b/__codex_opacity_low.png differ diff --git a/__codex_overlay_direct_texture.png b/__codex_overlay_direct_texture.png new file mode 100644 index 00000000..7371f97b Binary files /dev/null and b/__codex_overlay_direct_texture.png differ diff --git a/__codex_surface_switch_after_forward_lights.png b/__codex_surface_switch_after_forward_lights.png new file mode 100644 index 00000000..cb27e356 Binary files /dev/null and b/__codex_surface_switch_after_forward_lights.png differ diff --git a/__codex_surface_switch_after_forward_lights_crop.png b/__codex_surface_switch_after_forward_lights_crop.png new file mode 100644 index 00000000..c4b480de Binary files /dev/null and b/__codex_surface_switch_after_forward_lights_crop.png differ diff --git a/__codex_surface_switch_final_preview.png b/__codex_surface_switch_final_preview.png new file mode 100644 index 00000000..5300600f Binary files /dev/null and b/__codex_surface_switch_final_preview.png differ diff --git a/__codex_surface_switch_opacity_06.png b/__codex_surface_switch_opacity_06.png new file mode 100644 index 00000000..881f4a7f Binary files /dev/null and b/__codex_surface_switch_opacity_06.png differ diff --git a/__codex_surface_switch_visible.png b/__codex_surface_switch_visible.png new file mode 100644 index 00000000..70a45c8c Binary files /dev/null and b/__codex_surface_switch_visible.png differ diff --git a/__codex_surface_switch_visible_after_fix.png b/__codex_surface_switch_visible_after_fix.png new file mode 100644 index 00000000..ecb9fdfb Binary files /dev/null and b/__codex_surface_switch_visible_after_fix.png differ diff --git a/__codex_surface_switch_visible_clamped.png b/__codex_surface_switch_visible_clamped.png new file mode 100644 index 00000000..37dce52c Binary files /dev/null and b/__codex_surface_switch_visible_clamped.png differ diff --git a/__codex_surface_switch_visible_reapply.png b/__codex_surface_switch_visible_reapply.png new file mode 100644 index 00000000..96e88ebf Binary files /dev/null and b/__codex_surface_switch_visible_reapply.png differ diff --git a/__codex_surface_switch_visible_sync.png b/__codex_surface_switch_visible_sync.png new file mode 100644 index 00000000..6d16478b Binary files /dev/null and b/__codex_surface_switch_visible_sync.png differ diff --git a/__codex_transparent_dual_pass_test.png b/__codex_transparent_dual_pass_test.png new file mode 100644 index 00000000..d5c8125b Binary files /dev/null and b/__codex_transparent_dual_pass_test.png differ diff --git a/imgui.ini b/imgui.ini index 43a73f88..292225a9 100644 --- a/imgui.ini +++ b/imgui.ini @@ -24,26 +24,26 @@ Size=832,45 Collapsed=0 [Window][工具栏] -Pos=453,20 -Size=1250,32 +Pos=354,20 +Size=1334,32 Collapsed=0 DockId=0x0000000D,0 [Window][场景树] Pos=0,20 -Size=451,1036 +Size=352,748 Collapsed=0 DockId=0x00000007,0 [Window][属性面板] -Pos=1705,20 -Size=855,1036 +Pos=1690,20 +Size=358,748 Collapsed=0 DockId=0x00000002,0 [Window][控制台] -Pos=1705,20 -Size=855,1036 +Pos=1690,20 +Size=358,748 Collapsed=0 DockId=0x00000002,1 @@ -60,7 +60,7 @@ Collapsed=0 [Window][WindowOverViewport_11111111] Pos=0,20 -Size=2560,1372 +Size=2048,1084 Collapsed=0 [Window][测试窗口1] @@ -99,8 +99,8 @@ Size=600,500 Collapsed=0 [Window][资源管理器] -Pos=0,1058 -Size=2560,334 +Pos=0,770 +Size=2048,334 Collapsed=0 DockId=0x00000006,0 @@ -207,13 +207,13 @@ Collapsed=0 DockId=0x00000002,2 [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=0x00000007 Parent=0x00000005 SizeRef=451,1084 Selected=0xE0015051 - DockNode ID=0x00000008 Parent=0x00000005 SizeRef=1595,1084 Split=X - DockNode ID=0x00000001 Parent=0x00000008 SizeRef=1250,989 Split=Y + DockNode ID=0x00000007 Parent=0x00000005 SizeRef=352,1084 Selected=0xE0015051 + DockNode ID=0x00000008 Parent=0x00000005 SizeRef=2206,1084 Split=X + DockNode ID=0x00000001 Parent=0x00000008 SizeRef=1846,989 Split=Y DockNode ID=0x0000000D Parent=0x00000001 SizeRef=1318,32 HiddenTabBar=1 Selected=0x43A39006 DockNode ID=0x0000000E Parent=0x00000001 SizeRef=1318,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 diff --git a/main.py b/main.py index 7976af89..77f717ae 100644 --- a/main.py +++ b/main.py @@ -356,6 +356,9 @@ class MyWorld(PanelDelegates, CoreWorld): self._selected_template = 0 self._mount_script_index = 0 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 diff --git a/scene/scene_manager_io_mixin.py b/scene/scene_manager_io_mixin.py index 299e3ed0..0e15dae1 100644 --- a/scene/scene_manager_io_mixin.py +++ b/scene/scene_manager_io_mixin.py @@ -264,22 +264,35 @@ class SceneManagerIOMixin: def expand_scene_package_wrappers(nodes): expanded_nodes = [] 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: if not node or node.isEmpty(): 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 = ( node.hasTag("scene_import_source") and 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) continue source_children = [] - if source_scene_model and not source_scene_model.isEmpty(): - for child in source_scene_model.getChildren(): + source_snapshot = source_scene_root if is_ssbo_runtime_root else source_scene_model + if source_snapshot and not source_snapshot.isEmpty(): + for child in source_snapshot.getChildren(): try: if not child or child.isEmpty(): continue @@ -290,11 +303,11 @@ class SceneManagerIOMixin: continue if source_children: - print(f"展开场景包节点 {node.getName()} -> 使用原始场景树的 {len(source_children)} 个顶层子节点") + print(f"展开SSBO场景节点 {node.getName()} -> 使用原始场景树的 {len(source_children)} 个顶层子节点") expanded_nodes.extend(source_children) continue - print(f"场景包节点 {node.getName()} 缺少原始场景树,回退为包装根节点保存") + print(f"SSBO场景节点 {node.getName()} 缺少原始场景树,回退为包装根节点保存") expanded_nodes.append(node) return expanded_nodes diff --git a/ssbo_component/ssbo_controller.py b/ssbo_component/ssbo_controller.py index fb9cf2c3..8392cbc9 100644 --- a/ssbo_component/ssbo_controller.py +++ b/ssbo_component/ssbo_controller.py @@ -217,23 +217,17 @@ class ObjectController: def should_hide_tree_node(self, key): """ - Hide a redundant wrapper node directly below the file root, e.g. ROOT. - This keeps `model.glb` as the visible root in the UI. + Hide redundant wrapper nodes like ROOT so the UI shows the imported + model hierarchy instead of source-format packaging nodes. """ node = self.tree_nodes.get(key) if not node: return False - if node["parent"] != self.tree_root_key: - return False name = (node["name"] or "").strip().lower() if name != "root": return False - # Keep node visible if it actually carries direct geoms. - if node["local_ids"]: - return False - return len(node["children"]) > 0 def _encode_id_color(self, vdata, object_id): @@ -435,23 +429,17 @@ class ObjectController: def should_hide_tree_node(self, key): """ - Hide a redundant wrapper node directly below the file root, e.g. ROOT. - This keeps `model.glb` as the visible root in the UI. + Hide redundant wrapper nodes like ROOT so the UI shows the imported + model hierarchy instead of source-format packaging nodes. """ node = self.tree_nodes.get(key) if not node: return False - if node["parent"] != self.tree_root_key: - return False name = (node["name"] or "").strip().lower() if name != "root": return False - # Keep node visible if it actually carries direct geoms. - if node["local_ids"]: - return False - return len(node["children"]) > 0 def _encode_id_color(self, vdata, object_id): diff --git a/ssbo_component/ssbo_editor.py b/ssbo_component/ssbo_editor.py index d66881fe..4835c6e7 100644 --- a/ssbo_component/ssbo_editor.py +++ b/ssbo_component/ssbo_editor.py @@ -66,6 +66,8 @@ class SSBOEditor: self.model = None self.source_model = None self.source_model_root = None + self.last_import_tree_key = None + self.last_import_root_name = None self.ssbo = None self.font_path = font_path self._transform_gizmo = None @@ -277,11 +279,8 @@ class SSBOEditor: except Exception as e: print(f'修复黑色模型材质时出错: {e}') - def load_model(self, model_path, keep_source_model=False): - """Load and process a model using hybrid static/dynamic chunks.""" - print(f"[SSBOEditor] Loading model: {model_path}") - self.source_model = None - self.source_model_root = None + def _load_source_model_from_path(self, model_path): + """Load a source model NodePath from disk without touching current runtime state.""" source_model = None last_error = None for fn in self._build_filename_candidates(model_path): @@ -298,33 +297,245 @@ class SSBOEditor: raise RuntimeError(f"Failed to load model '{model_path}'") self._fixBlackMaterials(source_model) self._repair_missing_textures(source_model, model_path) - model_name = os.path.basename(model_path) - if model_name: - source_model.set_name(model_name) + return source_model - if keep_source_model: - self.source_model_root = NodePath(f"{model_name or 'scene'}__source_snapshot_root") - self.source_model = source_model.copyTo(self.source_model_root) + def _set_node_name(self, node, name): + if not node: + 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() - 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.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) - - # 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() - # Keep pick clone aligned with source 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}") + 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): """Build Filename candidates with wide-char first for Windows CJK paths.""" candidates = [] @@ -1347,36 +1558,7 @@ class SSBOEditor: def reset_scene_state(self): """Remove the current SSBO model/controller state before loading another scene.""" - 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 - 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() + self._clear_runtime_state(preserve_source_models=False) def _cleanup_group_proxy(self): """Reparent objects back to their chunk and remove the group proxy.""" diff --git a/third_party/p3dimgui/backend.py b/third_party/p3dimgui/backend.py index 6279abd7..d43a1718 100644 --- a/third_party/p3dimgui/backend.py +++ b/third_party/p3dimgui/backend.py @@ -456,13 +456,17 @@ class ImGuiBackend(DirectObject): # Some Panda3D / Windows combinations do not emit a reliable "keystroke" # event for ImGui text fields. Fall back to printable key-down events # 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 ( down and not self._keystroke_observed and not self.io.key_ctrl and not self.io.key_alt 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) if had_shift: diff --git a/ui/panels/app_actions.py b/ui/panels/app_actions.py index df478ac0..e9f6100c 100644 --- a/ui/panels/app_actions.py +++ b/ui/panels/app_actions.py @@ -1018,28 +1018,10 @@ class AppActions: except Exception: 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( file_path, keep_source_model=scene_package_import, + append=not scene_package_import, ) model_np = getattr(self.ssbo_editor, 'model', None) # 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_scene_element", "1") 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: try: @@ -1071,6 +1066,9 @@ class AppActions: self.scene_manager._processModelAnimations(model_np) except Exception as 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 except Exception as e: print(f"[SSBO] load_model failed: {e}") @@ -1143,14 +1141,24 @@ class AppActions: except Exception as 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) 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: - self.selection.updateSelection(model_node) + if select_model: + 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: self.add_success_message(f"模型导入成功: {file_name}") diff --git a/ui/panels/editor_panels_left.py b/ui/panels/editor_panels_left.py index 065767fb..b88069d1 100644 --- a/ui/panels/editor_panels_left.py +++ b/ui/panels/editor_panels_left.py @@ -23,60 +23,173 @@ class EditorPanelsLeftMixin: float(window_size.x), float(window_size.y), ) - - imgui.text("场景层级") + + self._draw_scene_tree_header() imgui.separator() - - # 构建动态场景树 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): """构建动态场景树""" - # 渲染节点 - if imgui.tree_node("渲染"): - # 环境光 - 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, 'Spotlight') and self.app.scene_manager.Spotlight: - for i, spotlight in enumerate(self.app.scene_manager.Spotlight): - self._draw_scene_node(spotlight, f"聚光灯_{i+1}", "light") - if hasattr(self.app.scene_manager, 'Pointlight') and self.app.scene_manager.Pointlight: - for i, pointlight in enumerate(self.app.scene_manager.Pointlight): - self._draw_scene_node(pointlight, f"点光源_{i+1}", "light") - - # 地板 - if hasattr(self.app, 'ground') and self.app.ground: - self._draw_scene_node(self.app.ground, "地板", "geometry") - - imgui.tree_pop() - - # 相机节点 - if imgui.tree_node("相机"): - if hasattr(self.app, 'camera') and self.app.camera: - self._draw_scene_node(self.app.camera, "主相机", "camera") - imgui.tree_pop() - - # 3D模型节点 - if imgui.tree_node("模型"): - 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()]) + render_entries = [] + if hasattr(self.app, "ambient_light") and self.app.ambient_light: + render_entries.append((self.app.ambient_light, "环境光", "light")) - # SSBO模式下,模型可能不在 scene_manager.models 中,补充显示 ssbo_editor.model + if hasattr(self.app, "scene_manager") and self.app.scene_manager: + if hasattr(self.app.scene_manager, "Spotlight") and self.app.scene_manager.Spotlight: + for i, spotlight in enumerate(self.app.scene_manager.Spotlight): + render_entries.append((spotlight, f"聚光灯_{i + 1}", "light")) + if hasattr(self.app.scene_manager, "Pointlight") and self.app.scene_manager.Pointlight: + for i, pointlight in enumerate(self.app.scene_manager.Pointlight): + render_entries.append((pointlight, f"点光源_{i + 1}", "light")) + + if hasattr(self.app, "ground") and self.app.ground: + render_entries.append((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() + + camera_entries = [] + if hasattr(self.app, "camera") and self.app.camera and self._node_matches_scene_filter(self.app.camera, "主相机"): + camera_entries.append((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() + + models = [model for model in self._get_scene_tree_models() if self._node_matches_scene_filter(model, model.getName() or "模型")] + model_display_count = self._get_scene_tree_model_display_count() + if model_display_count and imgui.tree_node(f"模型 ({model_display_count})"): 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) + controller = getattr(ssbo_editor, "controller", None) if ssbo_editor else None - if models: - for i, model in enumerate(models): - self._draw_scene_node(model, model.getName() or f"模型_{i+1}", "model") - else: - imgui.text("(空)") + for i, model in enumerate(models): + if ssbo_model and controller and model == ssbo_model: + for child_key in self._get_ssbo_top_level_model_keys(): + self._draw_ssbo_virtual_tree_node(ssbo_editor, controller, child_key) + continue + self._draw_scene_node(model, model.getName() or f"模型_{i + 1}", "model") 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 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(): return + if not self._node_matches_scene_filter(node, name): + return # 检查是否被选中 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): """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) if not node_data: return @@ -291,16 +409,12 @@ class EditorPanelsLeftMixin: self._update_resource_manager_window_rect(rm) - imgui.text("文件浏览器") - imgui.separator() - - self._draw_resource_toolbar_and_filters(rm) - - imgui.separator() - rm.refresh_if_needed() - 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_file_entries(rm, files) self._draw_resource_context_menu(rm) @@ -456,8 +570,42 @@ class EditorPanelsLeftMixin: if imgui.is_item_hovered() and imgui.is_mouse_clicked(1): 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): - """绘制资源管理器顶部工具条与筛选输入。保持原有按钮顺序与文案。""" + """绘制资源管理器顶部工具条与筛选输入。""" + self._sync_resource_path_input(rm) + if imgui.button("◀"): rm.navigate_back() imgui.same_line() @@ -479,20 +627,21 @@ class EditorPanelsLeftMixin: if changed: rm.set_auto_refresh(rm.auto_refresh_enabled) - imgui.same_line() - imgui.text(" ") - imgui.same_line() - - # 路径输入框 - changed, new_path = imgui.input_text("路径", str(rm.current_path), 256) + imgui.text_disabled("路径") + imgui.set_next_item_width(-72) + changed, new_path = imgui.input_text("##resource_path", self.app._resource_path_input, 512) if changed: - try: - rm.navigate_to(Path(new_path)) - except Exception: - pass + self.app._resource_path_input = new_path + imgui.same_line() + if imgui.button("前往##resource_go"): + self._navigate_resource_path_from_input(rm) - # 搜索框 - changed, rm.search_filter = imgui.input_text("搜索", rm.search_filter, 256) + imgui.text_disabled("搜索") + 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): """加载资源图标;失败时返回 None。""" diff --git a/ui/panels/editor_panels_right.py b/ui/panels/editor_panels_right.py index d681dd4a..eb9125f6 100644 --- a/ui/panels/editor_panels_right.py +++ b/ui/panels/editor_panels_right.py @@ -45,35 +45,14 @@ class EditorPanelsRightMixin( if ssbo_summary: self._draw_ssbo_selection_summary(ssbo_summary) return - # 无选中对象时显示提示(模仿Qt版本的空状态样式) - imgui.spacing() - imgui.spacing() - - # 居中显示提示信息 - window_width = imgui.get_window_width() - text_width = 200 # 估算文本宽度 - text_pos_x = (window_width - text_width) / 2 - - imgui.set_cursor_pos_x(text_pos_x) - 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 键删除选中对象") + self._draw_empty_property_state() + + def _draw_empty_property_state(self): + imgui.separator_text("属性") + imgui.text_disabled("当前没有选中对象") + imgui.spacing() + imgui.bullet_text("从左侧场景树或场景视口中选择一个对象") + imgui.bullet_text("按 F 聚焦到对象,按 Delete 删除对象") def _draw_property_section(self, title, draw_callback, default_open=False): flags = imgui.TreeNodeFlags_.span_avail_width.value @@ -85,18 +64,15 @@ class EditorPanelsRightMixin( def _draw_ssbo_selection_summary(self, summary): """Render a safe summary for SSBO group selections without exposing wrong node properties.""" - imgui.spacing() - imgui.text("SSBO 选择摘要") - imgui.separator() + imgui.separator_text("SSBO 选择") imgui.text(f"名称: {summary.get('display_name') or '未命名'}") imgui.text(f"对象数量: {summary.get('object_count', 0)}") if summary.get("is_root"): imgui.text_colored((0.5, 0.8, 1.0, 1.0), "当前选中的是 SSBO 根模型") elif summary.get("is_group"): imgui.text_colored((1.0, 0.8, 0.3, 1.0), "当前选中的是 SSBO 组合节点") - imgui.text_wrapped("这个选择对应多个动态对象。为避免误改错误节点,这里先显示摘要信息。") + imgui.text_wrapped("这个选择对应多个动态对象。这里先显示摘要,避免误改到错误节点。") imgui.separator() - imgui.text("建议:") imgui.bullet_text("在左侧展开到叶子节点后查看单对象属性") imgui.bullet_text("继续使用场景中的 Gizmo 做组合移动") else: @@ -166,78 +142,56 @@ class EditorPanelsRightMixin( except Exception: 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"): - imgui.same_line() - imgui.text_colored((0.8, 0.4, 0.8, 1.0), "[Script]") + def _draw_status_badges(self, node, node_type=None): + """绘制精简后的对象状态徽章行。""" + 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": - imgui.same_line() - imgui.text_colored((0.4, 0.8, 0.4, 1.0), "[Animation]") + badges = [] - def _draw_status_badges(self, node): - """绘制对象状态徽章行。""" - 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" + if node.is_hidden(): + badges.append(("已隐藏", (0.65, 0.65, 0.65, 1.0))) - 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) - type_colors = { - "GUI元素": (0.188, 0.404, 0.753, 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_script = hasattr(node, "getPythonTag") and node.getPythonTag("script") + if has_script: + badges.append(("脚本", (0.86, 0.48, 0.86, 1.0))) - has_collision = hasattr(node, "getChild") and any( - "Collision" in child.getName() for child in node.getChildren() if child.getName() - ) - if has_collision: - badges.append(("Collision", (0.2, 0.4, 0.8, 1.0))) - - 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 + has_animation = False + if node_type == "模型": + if node.hasTag("has_animations"): + has_animation = node.getTag("has_animations").lower() == "true" + if not has_animation: cached_result = node.getPythonTag("animation") if cached_result is True: has_animation = True elif cached_result is False: has_animation = False - else: - has_animation = hasattr(node, "getPythonTag") and node.getPythonTag("animation") + else: + has_animation = hasattr(node, "getPythonTag") and node.getPythonTag("animation") - if has_animation: - badges.append(("Animation", (0.4, 0.8, 0.4, 1.0))) + if has_animation: + badges.append(("动画", (0.45, 0.85, 0.55, 1.0))) - has_material = hasattr(node, "getMaterial") and node.getMaterial() - if has_material: - badges.append(("Material", (0.8, 0.6, 0.2, 1.0))) + has_material = hasattr(node, "getMaterial") and node.getMaterial() + if has_material: + badges.append(("材质", (0.9, 0.72, 0.35, 1.0))) - for index, (badge_text, badge_color) in enumerate(badges): - if index > 0: - imgui.same_line() - imgui.text_colored(badge_color, f"[{badge_text}]") + if not badges: + return + + 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): """绘制GUI元素属性""" diff --git a/ui/panels/editor_panels_right_material.py b/ui/panels/editor_panels_right_material.py index 24d25222..9975f253 100644 --- a/ui/panels/editor_panels_right_material.py +++ b/ui/panels/editor_panels_right_material.py @@ -91,9 +91,10 @@ class EditorPanelsRightMaterialMixin: if self.app._get_material_surface_type(material) == 3: 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: - apply_opacity(new_opacity) + apply_opacity(1.0 - new_transparency) imgui.separator() @@ -210,9 +211,15 @@ class EditorPanelsRightMaterialMixin: imgui.text("透明度设置") try: 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: - self.app._set_material_opacity(node, material, new_opacity) + self.app._set_material_opacity(node, material, 1.0 - new_transparency) except: imgui.text_colored((0.7, 0.7, 0.7, 1.0), "透明度控制不可用") diff --git a/ui/panels/property_helpers.py b/ui/panels/property_helpers.py index 511363f9..66303286 100644 --- a/ui/panels/property_helpers.py +++ b/ui/panels/property_helpers.py @@ -751,13 +751,20 @@ class PropertyHelpers: if previous_surface_type == 3: opacity = self._get_material_opacity(material) else: - base_alpha = float(self._get_material_base_color(material)[3]) - opacity = base_alpha if 0.0 < base_alpha <= 1.0 else 1.0 + # Entering transparent mode should keep the object fully + # 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: opacity = 1.0 - material.set_emission(Vec4(float(surface_type), float(emission.y), opacity, float(emission.w))) - self._apply_material_to_geom_states(node, material) - self._apply_material_surface_state(node, material) + material.set_emission(Vec4(float(surface_type), float(emission.y), opacity, float(emission.w))) + base_color = list(self._get_material_base_color(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: self._refresh_pipeline_material_mode(node, material) except Exception as e: @@ -789,9 +796,16 @@ class PropertyHelpers: if not render_pipeline or not node or node.isEmpty(): return 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._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) + self._set_material_opacity(node, material, current_opacity) + if split_changed: + self._apply_material_surface_state(node, material) except Exception as e: print(f"刷新RenderPipeline材质模式失败: {e}") @@ -1105,6 +1119,12 @@ class PropertyHelpers: surface_type = self._get_material_surface_type(material) if 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))) base_color = list(self._get_material_base_color(material))