diff --git a/.codex/environments/environment.toml b/.codex/environments/environment.toml new file mode 100644 index 00000000..b63d68bb --- /dev/null +++ b/.codex/environments/environment.toml @@ -0,0 +1,11 @@ +# THIS IS AUTOGENERATED. DO NOT EDIT MANUALLY +version = 1 +name = "EG" + +[setup] +script = "" + +[[actions]] +name = "运行" +icon = "run" +command = "python ./main.py" diff --git a/__codex_opaque_check.png b/__codex_opaque_check.png new file mode 100644 index 00000000..cfaf6e21 Binary files /dev/null and b/__codex_opaque_check.png differ diff --git a/__codex_overlay_forward_alpha_mask.png b/__codex_overlay_forward_alpha_mask.png new file mode 100644 index 00000000..d4aa418c Binary files /dev/null and b/__codex_overlay_forward_alpha_mask.png differ diff --git a/__codex_overlay_forwardcolor_raw.png b/__codex_overlay_forwardcolor_raw.png new file mode 100644 index 00000000..ed2bb1be Binary files /dev/null and b/__codex_overlay_forwardcolor_raw.png differ diff --git a/__codex_overlay_forwardcolor_rgb.png b/__codex_overlay_forwardcolor_rgb.png new file mode 100644 index 00000000..61fbcfd7 Binary files /dev/null and b/__codex_overlay_forwardcolor_rgb.png differ diff --git a/__codex_overlay_forwardcolor_rgb_flip.png b/__codex_overlay_forwardcolor_rgb_flip.png new file mode 100644 index 00000000..13f8be8e Binary files /dev/null and b/__codex_overlay_forwardcolor_rgb_flip.png differ diff --git a/__codex_overlay_forwardcolor_rgb_rebind.png b/__codex_overlay_forwardcolor_rgb_rebind.png new file mode 100644 index 00000000..674b55e7 Binary files /dev/null and b/__codex_overlay_forwardcolor_rgb_rebind.png differ diff --git a/__codex_overlay_red_test.png b/__codex_overlay_red_test.png new file mode 100644 index 00000000..64c81903 Binary files /dev/null and b/__codex_overlay_red_test.png differ diff --git a/__codex_transparency_after_material_state.png b/__codex_transparency_after_material_state.png new file mode 100644 index 00000000..03a02a1a Binary files /dev/null and b/__codex_transparency_after_material_state.png differ diff --git a/__codex_transparency_after_split.png b/__codex_transparency_after_split.png new file mode 100644 index 00000000..ebfb0e66 Binary files /dev/null and b/__codex_transparency_after_split.png differ diff --git a/__codex_transparency_bright_red.png b/__codex_transparency_bright_red.png new file mode 100644 index 00000000..74bb44a2 Binary files /dev/null and b/__codex_transparency_bright_red.png differ diff --git a/__codex_transparency_check.png b/__codex_transparency_check.png new file mode 100644 index 00000000..689ec77d Binary files /dev/null and b/__codex_transparency_check.png differ diff --git a/__codex_transparency_manual_prepare.png b/__codex_transparency_manual_prepare.png new file mode 100644 index 00000000..e2f6c1d2 Binary files /dev/null and b/__codex_transparency_manual_prepare.png differ diff --git a/__codex_transparency_overlay_flip_test.png b/__codex_transparency_overlay_flip_test.png new file mode 100644 index 00000000..258c47a7 Binary files /dev/null and b/__codex_transparency_overlay_flip_test.png differ diff --git a/config/default_imgui_layout.ini b/config/default_imgui_layout.ini new file mode 100644 index 00000000..65eaeffb --- /dev/null +++ b/config/default_imgui_layout.ini @@ -0,0 +1,53 @@ +[Window][WindowOverViewport_11111111] +Pos=0,20 +Size=2048,1084 +Collapsed=0 + +[Window][工具栏] +Pos=453,20 +Size=1326,32 +Collapsed=0 +DockId=0x0000000D,0 + +[Window][场景树] +Pos=0,20 +Size=451,748 +Collapsed=0 +DockId=0x00000007,0 + +[Window][属性面板] +Pos=1781,20 +Size=267,390 +Collapsed=0 +DockId=0x00000003,0 + +[Window][脚本管理] +Pos=1781,412 +Size=267,356 +Collapsed=0 +DockId=0x00000004,0 + +[Window][资源管理器] +Pos=0,770 +Size=2048,334 +Collapsed=0 +DockId=0x0000000A,0 + +[Window][控制台] +Pos=0,770 +Size=2048,334 +Collapsed=0 +DockId=0x0000000A,1 + +[Docking][Data] +DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=2048,1084 Split=Y + DockNode ID=0x00000009 Parent=0x08BD597D SizeRef=2560,748 Split=X + DockNode ID=0x00000007 Parent=0x00000009 SizeRef=451,1084 Selected=0xE0015051 + DockNode ID=0x00000008 Parent=0x00000009 SizeRef=1595,1084 Split=X + DockNode ID=0x00000001 Parent=0x00000008 SizeRef=1651,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=267,989 Split=Y Selected=0x3188AB8D + DockNode ID=0x00000003 Parent=0x00000002 SizeRef=351,390 Selected=0x5DB6FF37 + DockNode ID=0x00000004 Parent=0x00000002 SizeRef=351,597 Selected=0x3188AB8D + DockNode ID=0x0000000A Parent=0x08BD597D SizeRef=2560,334 Selected=0x3A2E05C3 diff --git a/core/render_pipeline_tag_state.py b/core/render_pipeline_tag_state.py new file mode 100644 index 00000000..04826f0f --- /dev/null +++ b/core/render_pipeline_tag_state.py @@ -0,0 +1,130 @@ +"""RenderPipeline tag state helpers for the editor runtime. + +The stock native TagStateManager used by RenderPipeline creates pass-specific +tag states from an empty RenderState. That works for static effects, but it +drops material attributes when we edit materials live in the editor. For +transparent forward materials this means the forward pass can no longer see +the current MaterialAttrib. + +This module installs a Python-side tag state manager that preserves the +NodePath state when building tag states, and re-registers the already-created +pipeline cameras. +""" + +from panda3d.core import BitMask32, ColorWriteAttrib, RenderState, ShaderAttrib + + +class EditorTagStateManager: + """Tag-state manager that preserves NodePath state when applying effects.""" + + class StateContainer: + def __init__(self, tag_name, mask, write_color): + self.cameras = [] + self.tag_states = {} + self.tag_name = tag_name + self.mask = BitMask32.bit(mask) + self.write_color = write_color + + def __init__(self, main_cam_node): + self._main_cam_node = main_cam_node + self._main_cam_node.node().set_camera_mask(BitMask32.bit(1)) + self.containers = { + "shadow": self.StateContainer("Shadows", 2, False), + "voxelize": self.StateContainer("Voxelize", 3, False), + "envmap": self.StateContainer("Envmap", 4, True), + "forward": self.StateContainer("Forward", 5, True), + } + + def get_mask(self, container_name): + if container_name == "gbuffer": + return BitMask32.bit(1) + return self.containers[container_name].mask + + def apply_state(self, container_name, np, shader, name, sort): + container = self.containers[container_name] + state = np.get_state() + + if not container.write_color: + state = state.set_attrib( + ColorWriteAttrib.make(ColorWriteAttrib.C_off), 10000 + ) + + state = state.set_attrib(ShaderAttrib.make(shader, sort), sort) + container.tag_states[name] = state + np.set_tag(container.tag_name, name) + + for camera in container.cameras: + camera.set_tag_state(name, state) + + def cleanup_states(self): + self._main_cam_node.node().clear_tag_states() + for container in self.containers.values(): + for camera in container.cameras: + camera.clear_tag_states() + container.tag_states = {} + + def register_camera(self, container_name, source): + container = self.containers[container_name] + source.set_tag_state_key(container.tag_name) + source.set_camera_mask(container.mask) + state = RenderState.make_empty() + if not container.write_color: + state = state.set_attrib( + ColorWriteAttrib.make(ColorWriteAttrib.C_off), 10000 + ) + source.set_initial_state(state) + container.cameras.append(source) + + def unregister_camera(self, container_name, source): + container = self.containers[container_name] + if source not in container.cameras: + return + container.cameras.remove(source) + source.clear_tag_states() + source.set_initial_state(RenderState.make_empty()) + + +def install_editor_tag_state_manager(render_pipeline, base): + """Replace the native tag manager with an editor-safe Python variant.""" + if not render_pipeline or not base: + return None + + if isinstance(render_pipeline.tag_mgr, EditorTagStateManager): + return render_pipeline.tag_mgr + + try: + render_pipeline.tag_mgr.cleanup_states() + except Exception: + pass + + tag_mgr = EditorTagStateManager(base.cam) + + for stage in render_pipeline.stage_mgr.stages: + stage_name = type(stage).__name__ + + if hasattr(stage, "forward_cam"): + tag_mgr.register_camera("forward", stage.forward_cam) + + if stage_name == "EnvironmentCaptureStage" and hasattr(stage, "cameras"): + for camera_np in stage.cameras: + tag_mgr.register_camera("envmap", camera_np.node()) + + if hasattr(stage, "voxel_cam"): + tag_mgr.register_camera("voxelize", stage.voxel_cam) + + if stage_name in ( + "PSSMDistShadowStage", + "PSSMSceneShadowStage", + "SkyAOCaptureStage", + ) and hasattr(stage, "camera"): + tag_mgr.register_camera("shadow", stage.camera) + + pssm_plugin = render_pipeline.plugin_mgr.instances.get("pssm") + if pssm_plugin and hasattr(pssm_plugin, "camera_rig"): + split_count = pssm_plugin.get_setting("split_count") + for index in range(split_count): + camera_np = pssm_plugin.camera_rig.get_camera(index) + tag_mgr.register_camera("shadow", camera_np.node()) + + render_pipeline.tag_mgr = tag_mgr + return tag_mgr diff --git a/core/world.py b/core/world.py index 2712e9f8..6179337d 100644 --- a/core/world.py +++ b/core/world.py @@ -9,8 +9,8 @@ from direct.actor.Actor import Actor warnings.filterwarnings("ignore", category=DeprecationWarning) from panda3d.core import (CardMaker, Vec4, Vec3, AmbientLight, DirectionalLight, - Point3, WindowProperties, Material, LColor, loadPrcFileData, - GraphicsPipeSelection) + Point3, WindowProperties, Material, LColor, Shader, + TransparencyAttrib, loadPrcFileData, GraphicsPipeSelection) from direct.showbase.ShowBase import ShowBase from direct.showbase.ShowBaseGlobal import globalClock from scene.scene_manager import SceneManager @@ -25,6 +25,7 @@ from ssbo_component.ssbo_editor import SSBOEditor # 从渲染管线工具模块导入全局函数 from core.render_pipeline_utils import get_render_pipeline, set_render_pipeline +from core.render_pipeline_tag_state import install_editor_tag_state_manager # 尝试导入插件管理器(如果存在) try: @@ -82,6 +83,8 @@ class CoreWorld(ShowBase): # 创建渲染管线 self.render_pipeline.create(self) + install_editor_tag_state_manager(self.render_pipeline, self) + self._setupForwardTransparencyOverlay() set_render_pipeline(self.render_pipeline) # 设置相机 @@ -171,6 +174,87 @@ class CoreWorld(ShowBase): except Exception as e: print(f"清理缓存时出错: {e}") + def _setupForwardTransparencyOverlay(self): + """Composite the forward transparency result back onto the final frame. + + The bundled RP forward merge stage is unreliable in this editor build + during live material edits, but the forward color buffer itself is + correct. We therefore blend that buffer as a fullscreen overlay and use + the scene/forward depth textures to keep transparent objects behind + opaque geometry clipped. + """ + self.forward_transparency_overlay = None + + try: + forward_stage = next( + ( + stage for stage in self.render_pipeline.stage_mgr.stages + if type(stage).__name__ == "ForwardStage" + ), + None, + ) + gbuffer_stage = next( + ( + stage for stage in self.render_pipeline.stage_mgr.stages + if type(stage).__name__ == "GBufferStage" + ), + None, + ) + + if not forward_stage or not gbuffer_stage: + return + + vert = """ +#version 330 +uniform mat4 p3d_ModelViewProjectionMatrix; +in vec4 p3d_Vertex; +in vec2 p3d_MultiTexCoord0; +out vec2 texcoord; +void main() { + gl_Position = p3d_ModelViewProjectionMatrix * p3d_Vertex; + texcoord = p3d_MultiTexCoord0; +} +""" + frag = """ +#version 330 +uniform sampler2D ForwardColor; +uniform sampler2D ForwardDepth; +uniform sampler2D SceneDepth; +in vec2 texcoord; +out vec4 o_color; +void main() { + vec4 forward_color = texture(ForwardColor, texcoord); + float alpha = clamp(forward_color.a, 0.0, 1.0); + if (alpha <= 1e-4) { + discard; + } + + float forward_depth = texture(ForwardDepth, texcoord).x; + float scene_depth = texture(SceneDepth, texcoord).x; + if (scene_depth > 1e-6 && forward_depth > scene_depth + 1e-5) { + discard; + } + + o_color = vec4(forward_color.rgb, alpha); +} +""" + + cm = CardMaker("ForwardTransparencyOverlay") + cm.setFrameFullscreenQuad() + overlay = self.render2d.attachNewNode(cm.generate()) + overlay.setShader(Shader.make(Shader.SL_GLSL, vert, frag)) + overlay.setShaderInput("ForwardColor", forward_stage.target.color_tex) + overlay.setShaderInput("ForwardDepth", forward_stage.target.depth_tex) + overlay.setShaderInput("SceneDepth", gbuffer_stage.target.depth_tex) + overlay.setTransparency(TransparencyAttrib.M_alpha) + overlay.setDepthTest(False) + overlay.setDepthWrite(False) + overlay.setBin("fixed", 1) + self.forward_transparency_overlay = overlay + print("✓ 前向透明叠加层初始化完成") + except Exception as e: + print(f"⚠ 初始化前向透明叠加层失败: {e}") + def _setupResourcePaths(self): """设置Panda3D资源搜索路径,确保能正确找到Resources文件夹中的模型和贴图""" try: diff --git a/imgui.ini b/imgui.ini index d27cbec9..43a73f88 100644 --- a/imgui.ini +++ b/imgui.ini @@ -25,33 +25,33 @@ Collapsed=0 [Window][工具栏] Pos=453,20 -Size=1326,32 +Size=1250,32 Collapsed=0 DockId=0x0000000D,0 [Window][场景树] Pos=0,20 -Size=451,748 +Size=451,1036 Collapsed=0 DockId=0x00000007,0 [Window][属性面板] -Pos=1781,20 -Size=267,748 +Pos=1705,20 +Size=855,1036 Collapsed=0 -DockId=0x00000003,0 +DockId=0x00000002,0 [Window][控制台] -Pos=0,770 -Size=2048,334 +Pos=1705,20 +Size=855,1036 Collapsed=0 -DockId=0x0000000A,1 +DockId=0x00000002,1 [Window][脚本管理] -Pos=1653,20 -Size=267,390 +Pos=1950,20 +Size=610,995 Collapsed=0 -DockId=0x00000003,1 +DockId=0x00000002,2 [Window][中文显示测试] Pos=60,60 @@ -60,7 +60,7 @@ Collapsed=0 [Window][WindowOverViewport_11111111] Pos=0,20 -Size=2048,1084 +Size=2560,1372 Collapsed=0 [Window][测试窗口1] @@ -79,7 +79,7 @@ Size=93,65 Collapsed=0 [Window][新建项目] -Pos=760,354 +Pos=824,401 Size=400,300 Collapsed=0 @@ -99,10 +99,10 @@ Size=600,500 Collapsed=0 [Window][资源管理器] -Pos=0,770 -Size=2048,334 +Pos=0,1058 +Size=2560,334 Collapsed=0 -DockId=0x0000000A,0 +DockId=0x00000006,0 [Window][创建3D文本] Pos=60,60 @@ -135,7 +135,7 @@ Size=89,250 Collapsed=0 [Window][颜色选择器] -Pos=810,304 +Pos=874,352 Size=300,400 Collapsed=0 @@ -150,10 +150,10 @@ Size=101,226 Collapsed=0 [Window][LUI编辑器] -Pos=1113,310 -Size=267,440 +Pos=1193,20 +Size=855,748 Collapsed=0 -DockId=0x00000004,0 +DockId=0x00000002,2 [Window][LUI测试控制面板] Pos=6,10 @@ -200,16 +200,20 @@ Pos=660,304 Size=600,400 Collapsed=0 +[Window][Web面板] +Pos=1438,20 +Size=610,748 +Collapsed=0 +DockId=0x00000002,2 + [Docking][Data] -DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=2048,1084 Split=Y - DockNode ID=0x00000009 Parent=0x08BD597D SizeRef=2560,748 Split=X - DockNode ID=0x00000007 Parent=0x00000009 SizeRef=451,1084 Selected=0xE0015051 - DockNode ID=0x00000008 Parent=0x00000009 SizeRef=1595,1084 Split=X - DockNode ID=0x00000001 Parent=0x00000008 SizeRef=1651,989 Split=Y +DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=2560,1372 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=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=267,989 Split=Y Selected=0x3188AB8D - DockNode ID=0x00000003 Parent=0x00000002 SizeRef=351,390 Selected=0x5DB6FF37 - DockNode ID=0x00000004 Parent=0x00000002 SizeRef=351,597 Selected=0x1EB923B7 - DockNode ID=0x0000000A Parent=0x08BD597D SizeRef=2560,334 Selected=0x3A2E05C3 + DockNode ID=0x00000002 Parent=0x00000008 SizeRef=855,989 Selected=0x5DB6FF37 + DockNode ID=0x00000006 Parent=0x08BD597D SizeRef=2560,334 Selected=0x3A2E05C3 diff --git a/main.py b/main.py index f32df35d..7976af89 100644 --- a/main.py +++ b/main.py @@ -115,6 +115,17 @@ except: warnings.filterwarnings("ignore", category=DeprecationWarning) class MyWorld(PanelDelegates, CoreWorld): + PANEL_VISIBILITY_ATTRS = { + "scene_tree": "showSceneTree", + "property": "showPropertyPanel", + "console": "showConsole", + "script": "showScriptPanel", + "toolbar": "showToolbar", + "resources": "showResourceManager", + "web": "showWebPanel", + "lui_editor": "showLUIEditor", + } + def __init__(self): super().__init__() self._shutdown_in_progress = False @@ -243,6 +254,9 @@ class MyWorld(PanelDelegates, CoreWorld): wantExplorerManager=False, wantTimeSliderManager=False, ) + self._imgui_ini_path = PROJECT_ROOT / "imgui.ini" + self._imgui_default_layout_path = PROJECT_ROOT / "config" / "default_imgui_layout.ini" + imgui.get_io().set_ini_filename(self._imgui_ini_path.as_posix()) # Let ssbo_component reuse the existing imgui backend instance. self.imgui_backend = self.imgui # Initialize SSBO editor and let it own mouse1 picking. @@ -274,6 +288,7 @@ class MyWorld(PanelDelegates, CoreWorld): self.animation_tools = AnimationTools(self) self.property_helpers = PropertyHelpers(self) self.app_actions = AppActions(self) + self._ensure_default_imgui_layout() # 简化的初始化字体设置(只使用中文字体) try: @@ -320,13 +335,19 @@ class MyWorld(PanelDelegates, CoreWorld): self.showDemoWindow = False # UI状态管理 - self.showSceneTree = True - self.showPropertyPanel = True - self.showConsole = True - self.showScriptPanel = not self.use_ssbo_mouse_picking - self.showToolbar = True - self.showResourceManager = True - self.showWebPanel = False + self._default_panel_visibility = { + "scene_tree": True, + "property": True, + "console": True, + "script": not self.use_ssbo_mouse_picking, + "toolbar": True, + "resources": True, + "web": False, + "lui_editor": not self.use_ssbo_mouse_picking, + } + for attr_name in self.PANEL_VISIBILITY_ATTRS.values(): + setattr(self, attr_name, False) + self.reset_panel_visibility_to_defaults() self.webPanelUrl = "https://www.baidu.com" # 脚本系统状态变量 @@ -334,6 +355,7 @@ class MyWorld(PanelDelegates, CoreWorld): self._new_script_name = "new_script" self._selected_template = 0 self._mount_script_index = 0 + self.console_command_input = "" # 变换监控相关 self._transform_monitoring = False @@ -342,6 +364,8 @@ class MyWorld(PanelDelegates, CoreWorld): self._transform_update_timer = 0 self._transform_update_interval = 0.05 # 50ms检查一次 self._clipboard_pos = None # 位置剪贴板 + self._transform_clipboard = {} + self._transform_scale_locked = True # 颜色选择器相关 self._color_picker_active = False @@ -353,6 +377,7 @@ class MyWorld(PanelDelegates, CoreWorld): self._font_selector_active = False self._font_selector_target = None # (target_object, property_name) self._font_selector_current_font = "" + self._font_selector_search_text = "" self._font_selector_callback = None self._available_fonts = [] # 可用字体列表 self._refresh_available_fonts() @@ -407,8 +432,6 @@ class MyWorld(PanelDelegates, CoreWorld): self._toolbar_window_rect = None self._drag_scene_tree_hover_node = None self.model_drag_drop = ModelDragDropService(self) - self.showLUIEditor = not self.use_ssbo_mouse_picking - # 导入功能状态 self.show_import_dialog = False self.import_file_path = "" @@ -498,6 +521,103 @@ class MyWorld(PanelDelegates, CoreWorld): print("✓ MyWorld 初始化完成") + def is_panel_visible(self, panel_key): + attr_name = self.PANEL_VISIBILITY_ATTRS.get(panel_key) + if not attr_name: + return False + return bool(getattr(self, attr_name, False)) + + def set_panel_visible(self, panel_key, visible): + attr_name = self.PANEL_VISIBILITY_ATTRS.get(panel_key) + if not attr_name: + return + + visible = bool(visible) + previous = bool(getattr(self, attr_name, False)) + setattr(self, attr_name, visible) + + if panel_key == "web" and previous != visible: + if not visible: + try: + self.editor_panels._stop_imgui_webview() + except Exception: + pass + + if panel_key == "lui_editor" and hasattr(self, "lui_manager"): + try: + self.lui_manager.show_editor = visible + except Exception: + pass + + def reset_panel_visibility_to_defaults(self): + for panel_key, default_visible in self._default_panel_visibility.items(): + self.set_panel_visible(panel_key, default_visible) + + def _imgui_layout_looks_valid(self, ini_text): + """Conservatively check whether imgui.ini already contains a usable dock layout.""" + if not ini_text or "[Docking][Data]" not in ini_text or "DockSpace" not in ini_text: + return False + return True + + def _ensure_default_imgui_layout(self): + """Seed a default dock layout when imgui.ini is missing or obviously incomplete.""" + template_path = getattr(self, "_imgui_default_layout_path", None) + ini_path = getattr(self, "_imgui_ini_path", None) + if not template_path or not ini_path or not template_path.exists(): + return + + current_text = "" + if ini_path.exists(): + try: + current_text = ini_path.read_text(encoding="utf-8") + except UnicodeDecodeError: + current_text = ini_path.read_text(encoding="utf-8", errors="ignore") + except Exception as e: + print(f"⚠ 读取 ImGui 布局文件失败: {e}") + + if self._imgui_layout_looks_valid(current_text): + return + + try: + template_text = template_path.read_text(encoding="utf-8") + except Exception as e: + print(f"⚠ 读取默认 ImGui 布局模板失败: {e}") + return + + if not self._imgui_layout_looks_valid(template_text): + print("⚠ 默认 ImGui 布局模板不完整,跳过自动回填") + return + + try: + if ini_path.exists() and current_text.strip(): + backup_path = ini_path.with_suffix(".invalid.bak") + if not backup_path.exists(): + shutil.copyfile(ini_path, backup_path) + print(f"ℹ 已备份无效 ImGui 布局: {backup_path}") + + imgui.load_ini_settings_from_memory(template_text) + ini_path.write_text(template_text, encoding="utf-8") + print(f"✓ 已加载默认 ImGui 布局: {template_path}") + except Exception as e: + print(f"⚠ 应用默认 ImGui 布局失败: {e}") + + def _reset_imgui_layout(self): + """Force-reset the current ImGui dock layout from the default template.""" + template_path = getattr(self, "_imgui_default_layout_path", None) + ini_path = getattr(self, "_imgui_ini_path", None) + if not template_path or not ini_path or not template_path.exists(): + self.add_error_message("默认 ImGui 布局模板不存在") + return + + try: + template_text = template_path.read_text(encoding="utf-8") + imgui.load_ini_settings_from_memory(template_text) + ini_path.write_text(template_text, encoding="utf-8") + self.reset_panel_visibility_to_defaults() + self.add_success_message("ImGui 布局已重置为默认布局") + except Exception as e: + self.add_error_message(f"重置 ImGui 布局失败: {e}") + def _on_window_event(self, window): """窗口事件处理:窗口被关闭时退出应用。""" if self._shutdown_in_progress: @@ -855,7 +975,7 @@ class MyWorld(PanelDelegates, CoreWorld): self._draw_drag_drop_interface() # 绘制LUI编辑器 - if self.showLUIEditor and hasattr(self, 'lui_manager'): + if self.is_panel_visible("lui_editor") and hasattr(self, 'lui_manager'): self.lui_manager.draw_editor() # 更新变换监控 @@ -865,31 +985,31 @@ class MyWorld(PanelDelegates, CoreWorld): def _draw_docked_layout(self, window_width, window_height): """绘制可停靠的布局(支持拖拽)""" # 左侧场景树面板 - if self.showSceneTree: + if self.is_panel_visible("scene_tree"): self._draw_scene_tree() # 资源管理器面板 - if self.showResourceManager: + if self.is_panel_visible("resources"): self._draw_resource_manager() # 属性面板 - if self.showPropertyPanel: + if self.is_panel_visible("property"): self._draw_property_panel() # Web面板 - if self.showWebPanel: + if self.is_panel_visible("web"): self._draw_web_panel() # 脚本面板 - if self.showScriptPanel: + if self.is_panel_visible("script"): self._draw_script_panel() # 底部控制台 - if self.showConsole: + if self.is_panel_visible("console"): self._draw_console() # 顶部工具栏 - if self.showToolbar: + if self.is_panel_visible("toolbar"): self._draw_toolbar() def _sync_rp_light_from_node(self, node): diff --git a/third_party/p3dimgui/backend.py b/third_party/p3dimgui/backend.py index e5735447..6279abd7 100644 --- a/third_party/p3dimgui/backend.py +++ b/third_party/p3dimgui/backend.py @@ -36,6 +36,12 @@ import ctypes import pyperclip import sys +import time + +if sys.platform == "win32": + from ctypes import wintypes +else: + wintypes = None __all__ = ['ImGuiBackend', 'ImGuiStyles'] @@ -199,11 +205,21 @@ class ImGuiBackend(DirectObject): self.textureCounter = 0 self.textures: dict[int, Texture] = {} self.geomData: list[GeomList] = [] + self._keystroke_observed = False + self._last_synthesized_text = "" + self._last_synthesized_text_at = 0.0 + self._ime_candidate_text = "" + self._ime_candidate_highlight = (0, 0, 0) + self._ime_composing = False + self._ime_open = False + self._last_ime_result_text = "" + self._imm32 = None self.__setupStyle(style) self.__setupGeom() self.__setupShader() self.__setupFront() + self.__setupImeSupport() self.__setupEvent() self.__windowEvent() self.__setupButton() @@ -246,16 +262,168 @@ class ImGuiBackend(DirectObject): case _: self.notify.warning(f"Unknown style: \"{style}\"") + @staticmethod + def __modifier_prefixes(): + return ( + 'control-', 'alt-', 'shift-', 'shift-control-', 'shift-alt-', 'shift-control-alt-', + 'meta-', 'control-meta-', 'alt-meta-', 'control-alt-meta-', 'shift-meta-', + 'shift-control-meta', 'shift-alt-meta-', 'shift-control-alt-meta-' + ) + + def __strip_modifier_prefixes(self, keyName: str): + had_shift = False + original_key_name = keyName or "" + if original_key_name.startswith(self.__modifier_prefixes()): + had_shift = 'shift-' in original_key_name + keyName = original_key_name.split('-')[-1] + if keyName == '': + keyName = '-' + return keyName, had_shift + + @staticmethod + def __apply_shift_to_ascii(character: str): + if not character: + return "" + if character.isalpha(): + return character.upper() + + shift_map = { + "1": "!", + "2": "@", + "3": "#", + "4": "$", + "5": "%", + "6": "^", + "7": "&", + "8": "*", + "9": "(", + "0": ")", + "-": "_", + "=": "+", + "[": "{", + "]": "}", + "\\": "|", + ";": ":", + "'": "\"", + ",": "<", + ".": ">", + "/": "?", + "`": "~", + } + return shift_map.get(character, character) + + def __resolve_text_input(self, keyName: str, button: ButtonHandle | None = None): + resolved_key_name = keyName or "" + if resolved_key_name == ' ': + return ' ' + + if button is None: + button = ButtonRegistry.ptr().getButton(resolved_key_name) + if button != ButtonHandle.none() and button.hasAsciiEquivalent(): + character = button.getAsciiEquivalent() + if character and ord(character) >= 32: + return character + + if resolved_key_name and any(ord(character) > 126 for character in resolved_key_name): + if all(ord(character) >= 32 for character in resolved_key_name): + return resolved_key_name + return "" + + def __queue_text_input(self, text: str): + if not text: + return + self.io.add_input_characters_utf8(text) + + def __setupImeSupport(self): + if sys.platform != "win32": + return + try: + self._imm32 = ctypes.WinDLL("imm32", use_last_error=True) + self._imm32.ImmGetContext.argtypes = [wintypes.HWND] + self._imm32.ImmGetContext.restype = wintypes.HANDLE + self._imm32.ImmReleaseContext.argtypes = [wintypes.HWND, wintypes.HANDLE] + self._imm32.ImmReleaseContext.restype = wintypes.BOOL + self._imm32.ImmGetOpenStatus.argtypes = [wintypes.HANDLE] + self._imm32.ImmGetOpenStatus.restype = wintypes.BOOL + self._imm32.ImmGetCompositionStringW.argtypes = [ + wintypes.HANDLE, + wintypes.DWORD, + wintypes.LPVOID, + wintypes.DWORD, + ] + self._imm32.ImmGetCompositionStringW.restype = ctypes.c_long + except Exception: + self._imm32 = None + + def __get_window_handle(self): + if not self.window: + return None + try: + win_handle = self.window.getWindowHandle() + if not win_handle: + return None + return win_handle.getIntHandle() + except Exception: + return None + + def __read_ime_string(self, himc, composition_type): + if not self._imm32: + return "" + byte_length = self._imm32.ImmGetCompositionStringW(himc, composition_type, None, 0) + if byte_length <= 0: + return "" + buffer = ctypes.create_unicode_buffer((byte_length // ctypes.sizeof(ctypes.c_wchar)) + 1) + copied = self._imm32.ImmGetCompositionStringW(himc, composition_type, buffer, byte_length) + if copied <= 0: + return "" + return buffer.value + + def __pollWindowsIme(self): + if sys.platform != "win32" or not self._imm32: + return + + hwnd = self.__get_window_handle() + if not hwnd: + return + + himc = self._imm32.ImmGetContext(hwnd) + if not himc: + self._ime_open = False + self._ime_composing = False + self._last_ime_result_text = "" + return + + GCS_COMPSTR = 0x0008 + GCS_RESULTSTR = 0x0800 + + try: + self._ime_open = bool(self._imm32.ImmGetOpenStatus(himc)) + composition_text = self.__read_ime_string(himc, GCS_COMPSTR) + result_text = self.__read_ime_string(himc, GCS_RESULTSTR) + self._ime_composing = bool(composition_text) + + if result_text and result_text != self._last_ime_result_text: + self.__queue_text_input(result_text) + self._last_ime_result_text = result_text + elif not result_text: + self._last_ime_result_text = "" + finally: + self._imm32.ImmReleaseContext(hwnd, himc) + + def __onCandidate(self, candidate_text, highlight_start = 0, highlight_end = 0, cursor_pos = 0): + if candidate_text is None: + candidate_text = "" + self._ime_candidate_text = str(candidate_text) + self._ime_candidate_highlight = ( + int(highlight_start or 0), + int(highlight_end or 0), + int(cursor_pos or 0), + ) + def __onButton(self, keyName: str, down: bool): # Panda3D adds the prefix of the modifier keys to the key name # if they are held down, so we have to strip them out. - if keyName.startswith(('control-', 'alt-', 'shift-', 'shift-control-', 'shift-alt-', 'shift-control-alt-', - 'meta-', 'control-meta-', 'alt-meta-', 'control-alt-meta-', 'shift-meta-', - 'shift-control-meta', 'shift-alt-meta-', 'shift-control-alt-meta-')): - keyName = keyName.split('-')[-1] - if keyName == '': - # must be minus. - keyName = '-' + keyName, had_shift = self.__strip_modifier_prefixes(keyName) button = ButtonRegistry.ptr().getButton(keyName) if button == ButtonHandle.none(): @@ -285,6 +453,25 @@ class ImGuiBackend(DirectObject): imguiKey = KEYBOARD_BUTTON_TO_IMGUI_KEY.get(button, imgui.Key.none.value) self.io.add_key_event(imguiKey, down) + # 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. + 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) + ): + text = self.__resolve_text_input(keyName, button) + if had_shift: + text = self.__apply_shift_to_ascii(text) + if text: + self._last_synthesized_text = text + self._last_synthesized_text_at = time.monotonic() + self.__queue_text_input(text) + def __onKeystroke(self, keyName): # NOTE: Panda3D for some reason doesn't recognize if # the caps lock is on for macOS. You would have to @@ -294,8 +481,22 @@ class ImGuiBackend(DirectObject): if keyName == ' ': # There is no space button on the ButtonRegistry. button = KeyboardButton.space() - if button.hasAsciiEquivalent(): - self.io.add_input_character(ord(button.getAsciiEquivalent())) + + text = self.__resolve_text_input(keyName, button) + if not text: + return + + now = time.monotonic() + if ( + not self._keystroke_observed + and text == self._last_synthesized_text + and (now - self._last_synthesized_text_at) < 0.25 + ): + self._keystroke_observed = True + return + + self._keystroke_observed = True + self.__queue_text_input(text) def __setupGeom(self): self.notify.debug("__setupGeom") @@ -349,6 +550,7 @@ class ImGuiBackend(DirectObject): base.buttonThrowers[0].node().setButtonDownEvent('buttonDown') base.buttonThrowers[0].node().setButtonUpEvent('buttonUp') base.buttonThrowers[0].node().setKeystrokeEvent('keystroke') + base.buttonThrowers[0].node().setCandidateEvent('candidate') def __buttonDown(keyName): self.__onButton(keyName, True) @@ -359,6 +561,9 @@ class ImGuiBackend(DirectObject): def __keyStroke(keyName): self.__onKeystroke(keyName) + def __candidate(candidate_text, highlight_start, highlight_end, cursor_pos): + self.__onCandidate(candidate_text, highlight_start, highlight_end, cursor_pos) + def __handleOobe(): if base.bboard.get('oobeEnabled'): self.ignore('buttonDown') @@ -378,6 +583,7 @@ class ImGuiBackend(DirectObject): self.accept('buttonDown', __buttonDown) self.accept('buttonUp', __buttonUp) self.accept('keystroke', __keyStroke) + self.accept('candidate', __candidate) self.accept(base.bboard.getEvent('oobeEnabled'), __handleOobe) @@ -394,6 +600,7 @@ class ImGuiBackend(DirectObject): return task.cont self.__refreshDisplayMetrics() + self.__pollWindowsIme() self.io.delta_time = base.clock.getDt() if self.window: mouse = self.window.getPointer(0) diff --git a/tmp_transparency_check/opaque.png b/tmp_transparency_check/opaque.png new file mode 100644 index 00000000..3d4374f4 Binary files /dev/null and b/tmp_transparency_check/opaque.png differ diff --git a/tmp_transparency_check/transparent_01.png b/tmp_transparency_check/transparent_01.png new file mode 100644 index 00000000..3d4374f4 Binary files /dev/null and b/tmp_transparency_check/transparent_01.png differ diff --git a/ui/LUI/lui_manager_editor.py b/ui/LUI/lui_manager_editor.py index b6df64d9..b3a52a8e 100644 --- a/ui/LUI/lui_manager_editor.py +++ b/ui/LUI/lui_manager_editor.py @@ -621,11 +621,15 @@ class LUIManagerEditorMixin: return imgui.set_next_window_size((350, 600), imgui.Cond_.first_use_ever) - with imgui_ctx.begin("LUI编辑器", True) as (opened, show): - self.show_editor = opened - if hasattr(self, "world") and hasattr(self.world, "showLUIEditor"): - self.world.showLUIEditor = opened - if not show: + with imgui_ctx.begin("LUI编辑器", self.show_editor) as window: + self.show_editor = bool(window.opened) + if hasattr(self, "world") and hasattr(self.world, "set_panel_visible"): + self.world.set_panel_visible("lui_editor", window.opened) + elif hasattr(self, "world") and hasattr(self.world, "showLUIEditor"): + self.world.showLUIEditor = bool(window.opened) + if not window.opened: + return + if not window.expanded: return # Play/Stop diff --git a/ui/panels/create_actions.py b/ui/panels/create_actions.py index e8befb67..f5b277f8 100644 --- a/ui/panels/create_actions.py +++ b/ui/panels/create_actions.py @@ -122,7 +122,7 @@ class CreateActions: def _on_open_scripts_manager(self): """打开脚本管理器""" - self.showScriptPanel = True + self.set_panel_visible("script", True) self.add_info_message("脚本管理器已打开") diff --git a/ui/panels/editor_panels_center.py b/ui/panels/editor_panels_center.py index 0840e28d..270be8c0 100644 --- a/ui/panels/editor_panels_center.py +++ b/ui/panels/editor_panels_center.py @@ -15,7 +15,7 @@ class EditorPanelsCenterMixin: def _on_create_web_panel(self): """创建或激活 ImGui Web 面板。""" self._ensure_web_panel_state() - self.app.showWebPanel = True + self.app.set_panel_visible("web", True) webview = getattr(self.app, "_imgui_webview", None) if webview and getattr(webview, "_running", False): @@ -143,18 +143,18 @@ class EditorPanelsCenterMixin: def _draw_web_panel(self): """绘制 Web 面板(ImGui + 后台浏览器截图)。""" self._ensure_web_panel_state() - if not self.app.showWebPanel: + if not self.app.is_panel_visible("web"): self._stop_imgui_webview() return flags = self.app.style_manager.get_window_flags("panel") with self.app.style_manager.begin_styled_window("Web面板", self.app.showWebPanel, flags) as (_, opened): if not opened: - self.app.showWebPanel = False + self.app.set_panel_visible("web", False) self._stop_imgui_webview() return - self.app.showWebPanel = True + self.app.set_panel_visible("web", opened) changed, self.app.web_panel_url_input = imgui.input_text( "URL", self.app.web_panel_url_input, 1024 diff --git a/ui/panels/editor_panels_right.py b/ui/panels/editor_panels_right.py index 1fa79593..d681dd4a 100644 --- a/ui/panels/editor_panels_right.py +++ b/ui/panels/editor_panels_right.py @@ -75,6 +75,14 @@ class EditorPanelsRightMixin( imgui.bullet_text("使用 F 键快速聚焦到选中对象") imgui.bullet_text("使用 Delete 键删除选中对象") + def _draw_property_section(self, title, draw_callback, default_open=False): + flags = imgui.TreeNodeFlags_.span_avail_width.value + if default_open: + flags |= imgui.TreeNodeFlags_.default_open.value + if imgui.collapsing_header(title, flags): + draw_callback() + imgui.spacing() + def _draw_ssbo_selection_summary(self, summary): """Render a safe summary for SSBO group selections without exposing wrong node properties.""" imgui.spacing() @@ -109,127 +117,98 @@ class EditorPanelsRightMixin( # 获取节点基本信息 node_name = node.getName() or "未命名节点" node_type = self.app._get_node_type_from_node(node) - - # 添加一些间距,模仿Qt版本的布局 + self._draw_object_overview(node, node_name, node_type) imgui.spacing() - - # 物体名称组(使用Qt版本的样式) - if imgui.collapsing_header("物体名称"): - # 第一行:可见性复选框和名称输入 - user_visible = node.getPythonTag("user_visible") - if user_visible is None: - user_visible = True - node.setPythonTag("user_visible", True) - - # 可见性复选框(模仿Qt版本的样式) - changed, is_visible = imgui.checkbox("##visibility", user_visible) - if changed: - node.setPythonTag("user_visible", is_visible) - if is_visible: - node.show() - else: - node.hide() - - imgui.same_line() - imgui.text("可见") - imgui.same_line() - imgui.spacing() - imgui.same_line() - - # 名称输入框(模仿Qt版本的样式) - imgui.text("名称:") - imgui.same_line() - changed, new_name = imgui.input_text("##name", node_name, 256) - if changed and hasattr(self.app, 'selection'): - # 更新场景树中的名称 - self.app._update_node_name(node, new_name) - - # 添加分隔线 - imgui.separator() - - # 状态徽章(模仿Qt版本的徽章样式) - self.app._draw_status_badges(node) - - imgui.spacing() - - # 变换属性组 - if imgui.collapsing_header("变换 Transform"): - self.app._draw_transform_properties(node) - - # 根据节点类型显示特定属性组 + + self._draw_property_section("变换", lambda: self.app._draw_transform_properties(node), default_open=True) + if node_type == "GUI元素": - if imgui.collapsing_header("GUI信息"): - self.app._draw_gui_properties(node) + self._draw_property_section("GUI", lambda: self.app._draw_gui_properties(node), default_open=True) elif node_type == "光源": - if imgui.collapsing_header("光源属性"): - self.app._draw_light_properties(node) + self._draw_property_section("光源", lambda: self.app._draw_light_properties(node), default_open=True) elif node_type == "模型": - if imgui.collapsing_header("模型属性"): - self.app._draw_model_properties(node) - - # 动画控制组(只对模型显示) - if imgui.collapsing_header("动画控制"): - self.app._draw_animation_properties(node) - - # 外观属性组(通用) - if imgui.collapsing_header("外观属性"): - self.app._draw_appearance_properties(node) - - # 碰撞检测组 - if imgui.collapsing_header("碰撞检测"): - self.app._draw_collision_properties(node) - - # 操作按钮组 - if imgui.collapsing_header("操作"): - self.app._draw_property_actions(node) + self._draw_property_section("模型", lambda: self.app._draw_model_properties(node), default_open=True) + self._draw_property_section("动画", lambda: self.app._draw_animation_properties(node), default_open=False) + + self._draw_property_section("材质", lambda: self.app._draw_appearance_properties(node), default_open=True) + self._draw_property_section("碰撞", lambda: self.app._draw_collision_properties(node), default_open=False) + self._draw_property_section("操作", lambda: self.app._draw_property_actions(node), default_open=False) + + def _draw_object_overview(self, node, node_name, node_type): + imgui.separator_text("对象") + + user_visible = node.getPythonTag("user_visible") + if user_visible is None: + user_visible = True + node.setPythonTag("user_visible", True) + + changed, is_visible = imgui.checkbox("##node_enabled", user_visible) + if changed: + node.setPythonTag("user_visible", is_visible) + if is_visible: + node.show() + else: + node.hide() + + imgui.same_line() + imgui.text_disabled("启用") + imgui.same_line() + imgui.set_next_item_width(-1) + changed, new_name = imgui.input_text("##name", node_name, 256) + if changed and hasattr(self.app, "selection"): + self.app._update_node_name(node, new_name) + + parent_name = "SceneRoot" + try: + parent = node.getParent() + if parent and not parent.isEmpty(): + parent_name = parent.getName() or "SceneRoot" + except Exception: + parent = None + + imgui.text_disabled(f"{node_type} | Parent: {parent_name}") + + if hasattr(node, "getPythonTag") and node.getPythonTag("script"): + imgui.same_line() + imgui.text_colored((0.8, 0.4, 0.8, 1.0), "[Script]") + + 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]") def _draw_status_badges(self, node): - """绘制状态徽章(模仿Qt版本的徽章样式)""" - imgui.text("状态标签: ") - - # 可见性状态徽章 + """绘制对象状态徽章行。""" 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 = "可见" if is_visible else "隐藏" - - imgui.same_line() - imgui.text_colored(visibility_color, f"[{visibility_text}]") - - # 节点类型徽章 + visibility_text = "Visible" if is_visible else "Hidden" + + badges = [(visibility_text, visibility_color)] + 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), # 灰色 + "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: - imgui.same_line() - imgui.text_colored(type_colors[node_type], f"[{node_type}]") - - # 功能性徽章 - badges = [] - - # 碰撞体徽章 - has_collision = hasattr(node, 'getChild') and any('Collision' in child.getName() for child in node.getChildren() if child.getName()) + badges.append((node_type, type_colors[node_type])) + + 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.2, 0.4, 0.8, 1.0))) # 蓝色 - - # 脚本徽章 - has_script = hasattr(node, 'getPythonTag') and node.getPythonTag('script') + 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(("脚本", (0.8, 0.4, 0.8, 1.0))) # 紫色 - - # 动画徽章(优化检测逻辑,避免重复创建Actor) + badges.append(("Script", (0.8, 0.4, 0.8, 1.0))) + has_animation = False - if node_type == "模型": # 只对模型类型进行动画检测 - # 优先使用场景标签(导入/加载时会写入) + if node_type == "模型": if node.hasTag("has_animations"): has_animation = node.getTag("has_animations").lower() == "true" - - # 再做轻量结构检测(不依赖 Actor) if not has_animation: try: has_character = node.findAllMatches("**/+Character").getNumPaths() > 0 @@ -240,34 +219,25 @@ class EditorPanelsRightMixin( node.setTag("can_create_actor_from_memory", "true") except Exception: pass - - # 只读取已有缓存,避免属性面板在普通模型上触发高噪音 Actor 探测 - cached_result = node.getPythonTag('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') - + has_animation = hasattr(node, "getPythonTag") and node.getPythonTag("animation") + if has_animation: - badges.append(("动画", (0.4, 0.8, 0.4, 1.0))) # 绿色 - - # 材质徽章 - has_material = hasattr(node, 'getMaterial') and node.getMaterial() + badges.append(("Animation", (0.4, 0.8, 0.4, 1.0))) + + has_material = hasattr(node, "getMaterial") and node.getMaterial() if has_material: - badges.append(("材质", (0.8, 0.6, 0.2, 1.0))) # 金色 - - # 绘制功能性徽章 - for badge_text, badge_color in badges: - imgui.same_line() + badges.append(("Material", (0.8, 0.6, 0.2, 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: - imgui.same_line() - imgui.text_colored((0.5, 0.5, 0.5, 1.0), "[标准对象]") 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 c1e62de1..24d25222 100644 --- a/ui/panels/editor_panels_right_material.py +++ b/ui/panels/editor_panels_right_material.py @@ -4,83 +4,105 @@ class EditorPanelsRightMaterialMixin: """Auto-split mixin from editor_panels_right.py.""" def _draw_appearance_properties(self, node): - """绘制外观属性""" - # 颜色属性 - if hasattr(node, 'getColor'): - imgui.text("颜色") - try: - color = node.getColor() - # 确保颜色是有效的 - if not color or len(color) < 3: - color = (1.0, 1.0, 1.0, 1.0) # 默认白色 - except: - color = (1.0, 1.0, 1.0, 1.0) # 默认白色 - - # 颜色滑块 - changed, new_r = imgui.slider_float("R##color_r", color[0], 0.0, 1.0) - if changed: - new_color = (new_r, color[1], color[2], color[3] if len(color) > 3 else 1.0) - node.setColor(new_color) - color = new_color - - changed, new_g = imgui.slider_float("G##color_g", color[1], 0.0, 1.0) - if changed: - new_color = (color[0], new_g, color[2], color[3] if len(color) > 3 else 1.0) - node.setColor(new_color) - color = new_color - - changed, new_b = imgui.slider_float("B##color_b", color[2], 0.0, 1.0) - if changed: - new_color = (color[0], color[1], new_b, color[3] if len(color) > 3 else 1.0) - node.setColor(new_color) - color = new_color - - # 只有当颜色有alpha通道时才显示alpha滑块 - if len(color) > 3: - changed, new_a = imgui.slider_float("A##color_a", color[3], 0.0, 1.0) - if changed: - new_color = (color[0], color[1], color[2], new_a) - node.setColor(new_color) - color = new_color - - # 颜色预览和选择器 - imgui.text("颜色预览") - color_with_alpha = (color[0], color[1], color[2], color[3] if len(color) > 3 else 1.0) - if imgui.color_button("颜色预览##preview", color_with_alpha, 0, (100, 30)): - # 点击颜色按钮打开颜色选择器 - self.show_color_picker(node, 'color', color_with_alpha) - - imgui.same_line() - if imgui.button("选择颜色##color_picker_btn"): - self.show_color_picker(node, 'color', (color.x, color.y, color.z, color.w)) - - # 透明度 - if hasattr(node, 'setTransparency') and hasattr(node, 'getTransparency'): - imgui.text("透明度") - current_transparency = node.getTransparency() - # 将当前的透明度值转换为0.0-1.0范围用于显示 - display_transparency = 1.0 - current_transparency if current_transparency <= 1 else 0.0 - changed, new_transparency = imgui.slider_float("透明度", display_transparency, 0.0, 1.0) - if changed: - # 将0.0-1.0范围转换回Panda3D的透明度格式 - panda_transparency = int((1.0 - new_transparency) * 255) - node.setTransparency(panda_transparency) - - # 材质属性 - self._draw_material_properties(node) - - # 渲染状态 - imgui.text("渲染状态") - if imgui.button("应用材质"): - self._apply_material_to_node(node) - + """绘制材质属性(Unity风格主材质入口)。""" + materials = self.app._get_node_materials(node) + if not materials: + fallback_material = self.app._ensure_material_for_node(node) + materials = [fallback_material] if fallback_material else [] + if not materials: + imgui.text_colored((1.0, 0.5, 0.5, 1.0), "无法获取材质") + return + material = materials[0] + + # 历史上可能通过 node.setColor 留下了额外染色,先清掉避免与材质主颜色打架 + try: + if node.hasColor(): + node.clearColor() + if hasattr(node, "clearColorScale"): + node.clearColorScale() + except Exception: + pass + + base_color = self.app._get_material_base_color(material) + + def apply_primary_color(color): + for current_material in materials: + self.app._set_material_base_color(current_material, color) + self.app._apply_material_to_geom_states(node, current_material) + if self.app._get_material_surface_type(current_material) == 3: + self.app._set_material_opacity(node, current_material, color[3]) + else: + self.app._apply_material_surface_state(node, current_material) + + def apply_surface_type(surface_type): + for current_material in materials: + self.app._set_material_surface_type( + node, + current_material, + surface_type, + refresh_pipeline=False, + ) + if materials: + self.app._apply_material_surface_state(node, materials[0]) + self.app._refresh_pipeline_material_mode(node, materials[0]) + + def apply_opacity(opacity): + for current_material in materials: + self.app._set_material_opacity(node, current_material, opacity) + + imgui.text("主颜色") + changed, new_color = imgui.color_edit4( + "##material_base_color", + base_color, + imgui.ColorEditFlags_.display_rgb.value, + ) + if changed: + apply_primary_color(new_color) + imgui.same_line() - if imgui.button("重置材质"): - self._reset_material(node) + if imgui.button("颜色选择器##material_color_picker"): + self.show_color_picker( + target_object=None, + property_name=None, + initial_color=base_color, + callback=apply_primary_color, + ) + + surface_options = [ + ("不透明", 0), + ("自发光", 1), + ("透明", 3), + ] + current_surface = self.app._get_material_surface_type(material) + current_surface_index = next( + (index for index, (_, value) in enumerate(surface_options) if value == current_surface), + 0, + ) + + imgui.text("表面类型") + changed, selected_index = imgui.combo( + "##material_surface_type", + current_surface_index, + [label for label, _ in surface_options], + ) + if changed: + apply_surface_type(surface_options[selected_index][1]) + current_surface = surface_options[selected_index][1] + + 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) + if changed: + apply_opacity(new_opacity) + + imgui.separator() + + # 详细材质属性 + self._draw_material_properties(node) def _draw_material_properties(self, node): """绘制材质属性""" - materials = node.find_all_materials() + materials = self.app._get_node_materials(node) if not materials: imgui.text_colored((0.5, 0.5, 0.5, 1.0), "无材质") @@ -90,33 +112,9 @@ class EditorPanelsRightMaterialMixin: material_name = material.get_name() if hasattr(material, 'get_name') and material.get_name() else f"材质{i + 1}" if imgui.collapsing_header(f"材质: {material_name}"): - # 材质基础颜色 - base_color = self._get_material_base_color(material) - if base_color: - imgui.text("基础颜色") - changed, new_r = imgui.slider_float(f"R##mat_r_{i}", base_color[0], 0.0, 1.0) - if changed: - self._update_material_base_color(material, 'r', new_r) - base_color = (new_r, base_color[1], base_color[2], base_color[3]) - - changed, new_g = imgui.slider_float(f"G##mat_g_{i}", base_color[1], 0.0, 1.0) - if changed: - self._update_material_base_color(material, 'g', new_g) - base_color = (base_color[0], new_g, base_color[2], base_color[3]) - - changed, new_b = imgui.slider_float(f"B##mat_b_{i}", base_color[2], 0.0, 1.0) - if changed: - self._update_material_base_color(material, 'b', new_b) - base_color = (base_color[0], base_color[1], new_b, base_color[3]) - - changed, new_a = imgui.slider_float(f"A##mat_a_{i}", base_color[3], 0.0, 1.0) - if changed: - self._update_material_base_color(material, 'a', new_a) - base_color = (base_color[0], base_color[1], base_color[2], new_a) - # PBR属性 + imgui.text("PBR") if hasattr(material, 'roughness') and material.roughness is not None: - imgui.text("PBR属性") try: roughness_value = float(material.roughness) changed, new_roughness = imgui.slider_float(f"粗糙度##rough_{i}", roughness_value, 0.0, 1.0) @@ -152,6 +150,7 @@ class EditorPanelsRightMaterialMixin: for j, preset_name in enumerate(presets): if imgui.selectable(preset_name, j == current_preset): self._apply_material_preset(material, preset_name) + self._apply_material_surface_state(node, material) imgui.end_combo() # 纹理信息 @@ -178,47 +177,42 @@ class EditorPanelsRightMaterialMixin: if imgui.button(f"清除所有贴图##clear_{i}"): self._clear_all_textures(node) - # 着色模型选择 - self._draw_shading_model_panel(material, i) - # 显示当前纹理信息 self._display_current_textures(node, material) - def _draw_shading_model_panel(self, material, material_index): + imgui.separator() + if imgui.button("应用材质"): + self._apply_material_to_node(node) + imgui.same_line() + if imgui.button("重置材质"): + self._reset_material(node) + + def _draw_shading_model_panel(self, node, material, material_index): """绘制着色模型选择面板""" try: imgui.text("着色模型") - - # RenderPipeline支持的着色模型 - shading_models = ["默认", "自发光", "透明"] - current_model = 0 # 默认选择 - - # 安全地获取当前着色模型 - try: - if hasattr(material, 'emission') and material.emission is not None: - current_model = int(material.emission.x) - except: - current_model = 0 - - # 着色模型选择 - if imgui.begin_combo(f"着色模型##shading_{material_index}", shading_models[current_model]): - for j, model_name in enumerate(shading_models): - if imgui.selectable(model_name, j == current_model): - self._update_shading_model(material, j) + + shading_models = [ + ("默认", 0), + ("自发光", 1), + ("透明", 3), + ] + current_model = self.app._get_material_surface_type(material) + current_index = next((idx for idx, (_, value) in enumerate(shading_models) if value == current_model), 0) + + if imgui.begin_combo(f"着色模型##shading_{material_index}", shading_models[current_index][0]): + for index, (model_name, model_value) in enumerate(shading_models): + if imgui.selectable(model_name, index == current_index): + self.app._set_material_surface_type(node, material, model_value) imgui.end_combo() - - # 如果是透明着色模型,添加透明度控制 - if current_model == 3: # 透明着色模型 + + if self.app._get_material_surface_type(material) == 3: imgui.text("透明度设置") try: - if hasattr(material, 'shading_model_param0'): - current_opacity = material.shading_model_param0 - else: - current_opacity = 1.0 - + current_opacity = self.app._get_material_opacity(material) changed, new_opacity = imgui.slider_float(f"不透明度##opacity_{material_index}", current_opacity, 0.0, 1.0) if changed: - self._update_transparency(material, new_opacity) + self.app._set_material_opacity(node, material, new_opacity) except: imgui.text_colored((0.7, 0.7, 0.7, 1.0), "透明度控制不可用") diff --git a/ui/panels/editor_panels_right_transform.py b/ui/panels/editor_panels_right_transform.py index a9aafbdb..342e4d80 100644 --- a/ui/panels/editor_panels_right_transform.py +++ b/ui/panels/editor_panels_right_transform.py @@ -1,92 +1,147 @@ -from imgui_bundle import imgui, imgui_ctx +from imgui_bundle import imgui + class EditorPanelsRightTransformMixin: - """Auto-split mixin from editor_panels_right.py.""" + """Compact Unity-style transform editor.""" def _draw_transform_properties(self, node): - """绘制变换属性""" - # 位置组 - if imgui.collapsing_header("位置 Position"): - # 相对位置 - imgui.text("相对位置") - pos = node.getPos() - - # X坐标 - changed, new_x = imgui.input_float("X##pos_x", pos.x, 0.1, 1.0, "%.3f") - if changed: node.setX(new_x) - - # Y坐标 - changed, new_y = imgui.input_float("Y##pos_y", pos.y, 0.1, 1.0, "%.3f") - if changed: node.setY(new_y) - - # Z坐标 - changed, new_z = imgui.input_float("Z##pos_z", pos.z, 0.1, 1.0, "%.3f") - if changed: node.setZ(new_z) - - # 世界位置 - imgui.text("世界位置") - world_pos = node.getPos(self.render) - - imgui.text(f"世界 X: {world_pos.x:.3f}") - imgui.text(f"世界 Y: {world_pos.y:.3f}") - imgui.text(f"世界 Z: {world_pos.z:.3f}") - - # 位置操作按钮 - if imgui.button("重置位置##reset_pos"): - node.setPos(0, 0, 0) - imgui.same_line() - if imgui.button("复制位置##copy_pos"): - self._clipboard_pos = (pos.x, pos.y, pos.z) - imgui.same_line() - if imgui.button("粘贴位置##paste_pos") and hasattr(self, '_clipboard_pos'): - node.setPos(self._clipboard_pos[0], self._clipboard_pos[1], self._clipboard_pos[2]) - - # 旋转组 - if imgui.collapsing_header("旋转 Rotation"): - hpr = node.getHpr() - - # HPR旋转 - imgui.text("HPR 旋转 (度)") - changed, new_h = imgui.input_float("H##rot_h", hpr.x, 1.0, 10.0, "%.1f") - if changed: node.setH(new_h) - - changed, new_p = imgui.input_float("P##rot_p", hpr.y, 1.0, 10.0, "%.1f") - if changed: node.setP(new_p) - - changed, new_r = imgui.input_float("R##rot_r", hpr.z, 1.0, 10.0, "%.1f") - if changed: node.setR(new_r) - - # 旋转操作按钮 - if imgui.button("重置旋转##reset_rot"): - node.setHpr(0, 0, 0) - imgui.same_line() - if imgui.button("随机旋转##random_rot"): - import random - node.setHpr(random.randint(0, 360), random.randint(0, 360), random.randint(0, 360)) - - # 缩放组 - if imgui.collapsing_header("缩放 Scale"): + imgui.push_style_var(imgui.StyleVar_.frame_padding, (6.0, 4.0)) + imgui.push_style_var(imgui.StyleVar_.item_spacing, (6.0, 4.0)) + try: + position = node.getPos() + rotation = node.getHpr() scale = node.getScale() - - # XYZ缩放 - imgui.text("XYZ 缩放") - changed, new_sx = imgui.input_float("X##scale_x", scale.x, 0.1, 1.0, "%.3f") - if changed: node.setSx(new_sx) - - changed, new_sy = imgui.input_float("Y##scale_y", scale.y, 0.1, 1.0, "%.3f") - if changed: node.setSy(new_sy) - - changed, new_sz = imgui.input_float("Z##scale_z", scale.z, 0.1, 1.0, "%.3f") - if changed: node.setSz(new_sz) - - # 统一缩放 - if imgui.button("统一缩放##uniform_scale"): - uniform_scale = (scale.x + scale.y + scale.z) / 3.0 - node.setScale(uniform_scale, uniform_scale, uniform_scale) - imgui.same_line() - if imgui.button("重置缩放##reset_scale"): - node.setScale(1, 1, 1) - imgui.same_line() - if imgui.button("翻倍##double_scale"): - node.setScale(scale.x * 2, scale.y * 2, scale.z * 2) + self._draw_transform_row( + "位置", + "position", + (position.x, position.y, position.z), + lambda values: node.setPos(*values), + speed=0.05, + value_format="%.3f", + reset_values=(0.0, 0.0, 0.0), + ) + self._draw_transform_row( + "旋转", + "rotation", + (rotation.x, rotation.y, rotation.z), + lambda values: node.setHpr(*values), + speed=0.25, + value_format="%.2f", + reset_values=(0.0, 0.0, 0.0), + ) + self._draw_transform_row( + "缩放", + "scale", + (scale.x, scale.y, scale.z), + lambda values: node.setScale(*values), + speed=0.02, + value_format="%.3f", + reset_values=(1.0, 1.0, 1.0), + linked=getattr(self, "_transform_scale_locked", True), + show_lock=True, + ) + finally: + imgui.pop_style_var(2) + + def _draw_transform_row( + self, + label, + row_id, + values, + apply_callback, + speed, + value_format, + reset_values, + linked=False, + show_lock=False, + ): + axis_names = ("X", "Y", "Z") + axis_colors = ( + (0.85, 0.32, 0.32, 1.0), + (0.36, 0.78, 0.42, 1.0), + (0.30, 0.55, 0.92, 1.0), + ) + current_values = [float(value) for value in values] + original_values = list(current_values) + changed_any = False + + table_flags = ( + imgui.TableFlags_.sizing_stretch_same.value + | imgui.TableFlags_.no_pad_outer_x.value + | imgui.TableFlags_.no_borders_in_body.value + ) + if imgui.begin_table(f"{row_id}_table", 8, table_flags): + imgui.table_setup_column("label", imgui.TableColumnFlags_.width_fixed.value, 52.0) + imgui.table_setup_column("lock", imgui.TableColumnFlags_.width_fixed.value, 22.0) + for axis_name in axis_names: + imgui.table_setup_column(f"{axis_name}_label", imgui.TableColumnFlags_.width_fixed.value, 16.0) + imgui.table_setup_column(f"{axis_name}_value", imgui.TableColumnFlags_.width_stretch.value, 1.0) + + imgui.table_next_row() + imgui.table_next_column() + imgui.text_disabled(label) + + imgui.table_next_column() + if show_lock: + changed, self._transform_scale_locked = imgui.checkbox( + f"##{row_id}_lock", + getattr(self, "_transform_scale_locked", True), + ) + if imgui.is_item_hovered(): + imgui.set_tooltip("锁定 XYZ 缩放比例") + else: + imgui.text("") + + for axis_index, axis_name in enumerate(axis_names): + imgui.table_next_column() + imgui.text_colored(axis_colors[axis_index], axis_name) + + imgui.table_next_column() + imgui.set_next_item_width(-1) + axis_changed, new_value = imgui.drag_float( + f"##{row_id}_{axis_name}", + current_values[axis_index], + speed, + format=value_format, + ) + if axis_changed: + current_values = self._apply_vector_value_change( + original_values, + current_values, + axis_index, + float(new_value), + linked, + reset_values, + ) + changed_any = True + + imgui.end_table() + + if changed_any: + apply_callback(tuple(current_values)) + + def _apply_vector_value_change( + self, + original_values, + current_values, + axis_index, + new_value, + linked, + reset_values, + ): + next_values = list(current_values) + if not linked: + next_values[axis_index] = new_value + return next_values + + old_value = original_values[axis_index] + if abs(old_value) > 1e-6: + ratio = new_value / old_value + return [value * ratio for value in original_values] + + if abs(reset_values[axis_index]) > 1e-6: + ratio = new_value / float(reset_values[axis_index]) + return [float(default_value) * ratio for default_value in reset_values] + + return [new_value, new_value, new_value] diff --git a/ui/panels/editor_panels_top.py b/ui/panels/editor_panels_top.py index 96e6fc29..ad4d039e 100644 --- a/ui/panels/editor_panels_top.py +++ b/ui/panels/editor_panels_top.py @@ -141,21 +141,35 @@ class EditorPanelsTopMixin: # 视图菜单 with imgui_ctx.begin_menu("视图") as view_menu: if view_menu: - _, self.app.showToolbar = imgui.menu_item("工具栏", "", self.app.showToolbar, True) - _, self.app.showSceneTree = imgui.menu_item("场景树", "", self.app.showSceneTree, True) - _, self.app.showResourceManager = imgui.menu_item("资源管理器", "", self.app.showResourceManager, True) - _, self.app.showPropertyPanel = imgui.menu_item("属性面板", "", self.app.showPropertyPanel, True) - _, self.app.showConsole = imgui.menu_item("控制台", "", self.app.showConsole, True) - _, self.app.showScriptPanel = imgui.menu_item("脚本管理", "", self.app.showScriptPanel, True) - _, self.app.showLUIEditor = imgui.menu_item("LUI编辑器", "", self.app.showLUIEditor, True) - if hasattr(self.app, "lui_manager"): - self.app.lui_manager.show_editor = self.app.showLUIEditor - prev_show_web_panel = self.app.showWebPanel - _, self.app.showWebPanel = imgui.menu_item("Web面板", "", self.app.showWebPanel, True) - if prev_show_web_panel and not self.app.showWebPanel: - self._stop_imgui_webview() - elif (not prev_show_web_panel) and self.app.showWebPanel: - self._on_create_web_panel() + changed, visible = imgui.menu_item("工具栏", "", self.app.is_panel_visible("toolbar"), True) + if changed: + self.app.set_panel_visible("toolbar", visible) + changed, visible = imgui.menu_item("场景树", "", self.app.is_panel_visible("scene_tree"), True) + if changed: + self.app.set_panel_visible("scene_tree", visible) + changed, visible = imgui.menu_item("资源管理器", "", self.app.is_panel_visible("resources"), True) + if changed: + self.app.set_panel_visible("resources", visible) + changed, visible = imgui.menu_item("属性面板", "", self.app.is_panel_visible("property"), True) + if changed: + self.app.set_panel_visible("property", visible) + changed, visible = imgui.menu_item("控制台", "", self.app.is_panel_visible("console"), True) + if changed: + self.app.set_panel_visible("console", visible) + changed, visible = imgui.menu_item("脚本管理", "", self.app.is_panel_visible("script"), True) + if changed: + self.app.set_panel_visible("script", visible) + changed, visible = imgui.menu_item("LUI编辑器", "", self.app.is_panel_visible("lui_editor"), True) + if changed: + self.app.set_panel_visible("lui_editor", visible) + changed, visible = imgui.menu_item("Web面板", "", self.app.is_panel_visible("web"), True) + if changed: + self.app.set_panel_visible("web", visible) + imgui.separator() + if imgui.menu_item("恢复默认面板", "", False, True)[1]: + self.app.reset_panel_visibility_to_defaults() + if imgui.menu_item("重置布局", "", False, True)[1]: + self.app._reset_imgui_layout() # 工具菜单 with imgui_ctx.begin_menu("工具") as tools_menu: diff --git a/ui/panels/panel_delegates.py b/ui/panels/panel_delegates.py index a64931bc..5c09023a 100644 --- a/ui/panels/panel_delegates.py +++ b/ui/panels/panel_delegates.py @@ -247,6 +247,36 @@ class PanelDelegates: def _get_material_base_color(self, *args, **kwargs): return self.property_helpers._get_material_base_color(*args, **kwargs) + def _get_node_materials(self, *args, **kwargs): + return self.property_helpers._get_node_materials(*args, **kwargs) + + def _ensure_material_for_node(self, *args, **kwargs): + return self.property_helpers._ensure_material_for_node(*args, **kwargs) + + def _get_material_surface_type(self, *args, **kwargs): + return self.property_helpers._get_material_surface_type(*args, **kwargs) + + def _set_material_surface_type(self, *args, **kwargs): + return self.property_helpers._set_material_surface_type(*args, **kwargs) + + def _refresh_pipeline_material_mode(self, *args, **kwargs): + return self.property_helpers._refresh_pipeline_material_mode(*args, **kwargs) + + def _get_material_opacity(self, *args, **kwargs): + return self.property_helpers._get_material_opacity(*args, **kwargs) + + def _set_material_opacity(self, *args, **kwargs): + return self.property_helpers._set_material_opacity(*args, **kwargs) + + def _apply_material_surface_state(self, *args, **kwargs): + return self.property_helpers._apply_material_surface_state(*args, **kwargs) + + def _set_material_base_color(self, *args, **kwargs): + return self.property_helpers._set_material_base_color(*args, **kwargs) + + def _apply_material_to_geom_states(self, *args, **kwargs): + return self.property_helpers._apply_material_to_geom_states(*args, **kwargs) + def _update_material_base_color(self, *args, **kwargs): return self.property_helpers._update_material_base_color(*args, **kwargs) diff --git a/ui/panels/property_helpers.py b/ui/panels/property_helpers.py index c5aeb2f0..511363f9 100644 --- a/ui/panels/property_helpers.py +++ b/ui/panels/property_helpers.py @@ -626,8 +626,543 @@ class PropertyHelpers: return (1.0, 1.0, 1.0, 1.0) # 默认白色 except: return (1.0, 1.0, 1.0, 1.0) # 默认白色 - + def _apply_material_to_geom_states(self, node, material): + """Bake the editable material into every GeomState so RP can see runtime edits.""" + try: + from panda3d.core import MaterialAttrib + + if not node or node.isEmpty() or material is None: + return + + # For imported multi-material models we edit the existing material + # instances in place. Rebroadcasting one material to every GeomNode + # would collapse the whole model to a single material. + try: + if not node.hasMaterial(): + self._invalidate_material_render_cache() + return + except Exception: + pass + + for geom_path in node.find_all_matches("**/+GeomNode"): + try: + geom_path.setState(geom_path.getState().setAttrib(MaterialAttrib.make(material))) + except Exception: + pass + + geom_node = geom_path.node() + for i in range(geom_node.getNumGeoms()): + try: + geom_state = geom_node.getGeomState(i) + geom_node.setGeomState(i, geom_state.setAttrib(MaterialAttrib.make(material))) + except Exception: + pass + self._invalidate_material_render_cache() + except Exception as e: + print(f"同步Geom材质状态失败: {e}") + + def _get_node_materials(self, node): + """Return the editable materials currently used by a node.""" + if not node or node.isEmpty(): + return [] + + try: + if node.hasMaterial(): + return [node.getMaterial()] + except Exception: + pass + + try: + materials = list(node.find_all_materials()) + except Exception: + materials = [] + + unique_materials = [] + seen_keys = set() + for material in materials: + key = getattr(material, "this", None) or id(material) + if key in seen_keys: + continue + seen_keys.add(key) + unique_materials.append(material) + return unique_materials + + def _invalidate_material_render_cache(self): + """Force Panda/RenderPipeline to pick up runtime material edits immediately.""" + try: + from panda3d.core import RenderState + RenderState.clear_cache() + except Exception: + pass + + def _ensure_material_for_node(self, node): + """Ensure a node has at least one editable material and return the primary material.""" + try: + if node and not node.isEmpty() and node.hasMaterial(): + material = node.getMaterial() + self._apply_material_to_geom_states(node, material) + return material + except Exception: + pass + + materials = self._get_node_materials(node) + if materials: + return materials[0] + + try: + from panda3d.core import Material, Vec4 + + material = Material(f"default-material-{node.getName() or 'node'}") + material.set_base_color(Vec4(0.8, 0.8, 0.8, 1.0)) + material.set_roughness(0.5) + material.set_metallic(0.0) + material.set_refractive_index(1.5) + material.set_emission(Vec4(0.0, 0.0, 1.0, 0.0)) + node.setMaterial(material, 1) + self._apply_material_to_geom_states(node, material) + return material + except Exception as e: + print(f"创建默认材质失败: {e}") + return None + + def _get_material_surface_type(self, material): + """Return the RenderPipeline shading model value used by this material.""" + try: + emission = material.emission if hasattr(material, "emission") else None + if emission is None: + return 0 + shading_model = int(round(float(emission.x))) + if shading_model in (0, 1, 3): + return shading_model + except Exception: + pass + return 0 + + def _set_material_surface_type(self, node, material, surface_type, refresh_pipeline=True): + """Update material shading model and sync node transparency state.""" + try: + from panda3d.core import Vec4 + + surface_type = int(surface_type) + previous_surface_type = self._get_material_surface_type(material) + emission = material.emission if hasattr(material, "emission") and material.emission is not None else Vec4(0, 0, 1, 0) + if surface_type == 3: + 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 + 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) + if refresh_pipeline: + self._refresh_pipeline_material_mode(node, material) + except Exception as e: + print(f"设置材质表面类型失败: {e}") + + def _get_material_opacity(self, material): + """Return opacity for transparent materials.""" + try: + if self._get_material_surface_type(material) != 3: + return 1.0 + emission = material.emission if hasattr(material, "emission") else None + if emission is not None: + return max(0.0, min(1.0, float(emission.z))) + except Exception: + pass + return 1.0 + + def _material_uses_transparent_pass(self, material): + """Return whether the material should be rendered through RP forward transparency.""" + try: + return self._get_material_surface_type(material) == 3 + except Exception: + return False + + def _refresh_pipeline_material_mode(self, node, material): + """Let RenderPipeline re-evaluate transparent material routing for this subtree.""" + try: + render_pipeline = getattr(self, "render_pipeline", None) + if not render_pipeline or not node or node.isEmpty(): + return + if self._material_uses_transparent_pass(material) and hasattr(render_pipeline, "prepare_scene"): + self._bake_effective_geom_materials(node) + self._isolate_transparent_geoms(node) + render_pipeline.prepare_scene(node) + except Exception as e: + print(f"刷新RenderPipeline材质模式失败: {e}") + + def _bake_effective_geom_materials(self, node): + """Bake inherited/material override state back into each GeomState for RP scene analysis.""" + try: + from panda3d.core import MaterialAttrib + + if not node or node.isEmpty(): + return False + + changed = False + for geom_np in node.find_all_matches("**/+GeomNode"): + geom_node = geom_np.node() + net_state = None + try: + net_state = geom_np.getNetState() + except Exception: + pass + + for geom_index in range(geom_node.getNumGeoms()): + try: + geom_state = geom_node.getGeomState(geom_index) + except Exception: + continue + + material = None + try: + if geom_state.hasAttrib(MaterialAttrib): + material_attrib = geom_state.getAttrib(MaterialAttrib) + material = material_attrib.getMaterial() if material_attrib else None + except Exception: + material = None + + if material is None and net_state is not None: + try: + if net_state.hasAttrib(MaterialAttrib): + material_attrib = net_state.getAttrib(MaterialAttrib) + material = material_attrib.getMaterial() if material_attrib else None + except Exception: + material = None + + if material is None: + try: + if geom_np.hasMaterial(): + material = geom_np.getMaterial() + except Exception: + material = None + + if material is None: + continue + + try: + geom_node.setGeomState(geom_index, geom_state.setAttrib(MaterialAttrib.make(material))) + changed = True + except Exception: + continue + + if changed: + self._invalidate_material_render_cache() + return changed + except Exception as e: + print(f"烘焙Geom材质状态失败: {e}") + return False + + def _get_renderable_node_material(self, node): + """Resolve the material that should represent a renderable GeomNode.""" + try: + if not node or node.isEmpty(): + return None + except Exception: + return None + + try: + if node.hasMaterial(): + material = node.getMaterial() + if material is not None: + return material + except Exception: + pass + + try: + geom_node = node.node() + for geom_index in range(geom_node.getNumGeoms()): + geom_state = geom_node.getGeomState(geom_index) + if not geom_state.hasAttrib(MaterialAttrib): + continue + material_attrib = geom_state.getAttrib(MaterialAttrib) + material = material_attrib.getMaterial() if material_attrib else None + if material is not None: + return material + except Exception: + pass + + try: + materials = self._get_node_materials(node) + if materials: + return materials[0] + except Exception: + pass + return None + + def _isolate_transparent_geoms(self, node): + """Split multi-geom GeomNodes so RP transparent materials can be routed correctly.""" + try: + from panda3d.core import GeomNode, MaterialAttrib + + if not node or node.isEmpty(): + return False + + geom_paths = [match for match in node.find_all_matches("**/+GeomNode")] + changed = False + + for geom_np in geom_paths: + try: + geom_node = geom_np.node() + geom_count = geom_node.getNumGeoms() + except Exception: + continue + + if geom_count <= 1: + continue + + has_transparent_geom = False + for geom_index in range(geom_count): + try: + geom_state = geom_node.getGeomState(geom_index) + if not geom_state.hasAttrib(MaterialAttrib): + continue + material_attrib = geom_state.getAttrib(MaterialAttrib) + geom_material = material_attrib.getMaterial() if material_attrib else None + if geom_material and self._get_material_surface_type(geom_material) == 3: + has_transparent_geom = True + break + except Exception: + continue + + if not has_transparent_geom: + continue + + parent_np = geom_np.getParent() + if not parent_np or parent_np.isEmpty(): + continue + + try: + local_transform = geom_np.getTransform(parent_np) + except Exception: + local_transform = None + try: + local_mat = geom_np.getMat(parent_np) + except Exception: + local_mat = None + try: + path_state = geom_np.getState() + except Exception: + path_state = None + + tag_keys = [] + python_tag_keys = [] + try: + tag_keys = list(geom_np.getTagKeys()) + except Exception: + pass + try: + python_tag_keys = list(geom_np.getPythonTagKeys()) + except Exception: + pass + + for geom_index in range(geom_count): + try: + split_geom = geom_node.getGeom(geom_index).makeCopy() + split_state = geom_node.getGeomState(geom_index) + except Exception: + continue + + effective_material = None + + try: + if (not split_state.hasAttrib(MaterialAttrib)) or split_state.getAttrib(MaterialAttrib).getMaterial() is None: + effective_state = geom_np.getNetState() + if effective_state.hasAttrib(MaterialAttrib): + effective_material = effective_state.getAttrib(MaterialAttrib).getMaterial() + if effective_material is not None: + split_state = split_state.setAttrib(MaterialAttrib.make(effective_material)) + else: + effective_material = split_state.getAttrib(MaterialAttrib).getMaterial() + except Exception: + pass + + split_name = geom_node.getName() if geom_count == 1 else f"{geom_node.getName()}__geom_{geom_index}" + split_node = GeomNode(split_name) + split_node.addGeom(split_geom, split_state) + split_np = parent_np.attachNewNode(split_node) + + if local_transform is not None: + try: + split_np.setTransform(parent_np, local_transform) + except Exception: + pass + elif local_mat is not None: + try: + split_np.setMat(parent_np, local_mat) + except Exception: + pass + + if path_state is not None: + try: + split_np.setState(path_state) + except Exception: + pass + + if effective_material is not None: + try: + split_np.setMaterial(effective_material, 1) + except Exception: + pass + + if geom_np.isHidden(): + split_np.hide() + + for tag_key in tag_keys: + try: + split_np.setTag(tag_key, geom_np.getTag(tag_key)) + except Exception: + pass + + for tag_key in python_tag_keys: + try: + split_np.setPythonTag(tag_key, geom_np.getPythonTag(tag_key)) + except Exception: + pass + + geom_np.removeNode() + changed = True + + if changed: + self._invalidate_material_render_cache() + return changed + except Exception as e: + print(f"拆分透明几何节点失败: {e}") + return False + + def _iter_material_state_nodes(self, node): + """Yield the concrete renderable nodes that should receive material state updates.""" + try: + if not node or node.isEmpty(): + return [] + renderable_nodes = [match for match in node.find_all_matches("**/+GeomNode")] + if renderable_nodes: + return renderable_nodes + except Exception: + pass + return [node] + + def _sync_material_render_effect(self, node, material=None, force=False, source_node=None): + """Keep RenderPipeline effect options in sync with the current material surface mode.""" + try: + if not node or node.isEmpty(): + return + + render_pipeline = getattr(self, "render_pipeline", None) + if not render_pipeline: + return + + material = material or self._ensure_material_for_node(node) + if not material: + return + + source_node = source_node or node + use_forward = self._material_uses_transparent_pass(material) + use_metallic_effect = source_node.hasTag("material_effect_metallic_enabled") + enable_parallax = source_node.hasTag("material_effect_parallax_enabled") + + effect_path = "effects/pbr_with_metallic.yaml" if use_metallic_effect else "effects/default.yaml" + options = { + "normal_mapping": True, + "render_gbuffer": not use_forward, + "render_forward": use_forward, + "alpha_testing": False, + "parallax_mapping": enable_parallax, + "render_shadow": True, + "render_envmap": True, + } + sort = 60 if use_metallic_effect else 55 + effect_signature = "|".join(( + effect_path, + "1" if options["render_forward"] else "0", + "1" if options["render_gbuffer"] else "0", + "1" if options["parallax_mapping"] else "0", + "1" if use_metallic_effect else "0", + )) + current_signature = node.getTag("material_render_effect_signature") if node.hasTag("material_render_effect_signature") else "" + if not force and current_signature == effect_signature: + return + + apply_effect = getattr(render_pipeline, "_internal_set_effect", None) or getattr(render_pipeline, "set_effect", None) + if not apply_effect: + return + apply_effect(node, effect_path, options, sort) + node.setTag("material_render_effect_signature", effect_signature) + except Exception as e: + print(f"同步材质渲染 effect 失败: {e}") + + def _set_material_opacity(self, node, material, opacity_value): + """Update transparent opacity in the RenderPipeline-compatible slot.""" + try: + from panda3d.core import Vec4 + + opacity_value = max(0.0, min(1.0, float(opacity_value))) + emission = material.emission if hasattr(material, "emission") and material.emission is not None else Vec4(3, 0, 1, 0) + surface_type = self._get_material_surface_type(material) + if surface_type != 3: + surface_type = 3 + 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[3] = opacity_value + self._set_material_base_color(material, tuple(base_color)) + self._apply_material_to_geom_states(node, material) + self._apply_material_surface_state(node, material) + except Exception as e: + print(f"设置材质透明度失败: {e}") + + def _apply_material_surface_state(self, node, material): + """Sync Panda node transparency mode with the material surface type.""" + try: + from panda3d.core import ColorBlendAttrib, TransparencyAttrib + + for target_node in self._iter_material_state_nodes(node): + target_material = self._get_renderable_node_material(target_node) or material + if target_material is not None: + try: + target_node.setMaterial(target_material, 1) + except Exception: + pass + + # Let RenderPipeline handle transparent materials via its + # forward pass. Leaving Panda-side alpha blending enabled here + # causes the object to be blended twice and makes live editing + # hard to reason about. + target_node.setTransparency(TransparencyAttrib.M_none) + target_node.setAlphaScale(1.0) + target_node.setDepthWrite(True) + try: + target_node.clearBin() + except Exception: + pass + try: + target_node.clearAttrib(ColorBlendAttrib.getClassType()) + except Exception: + pass + self._sync_material_render_effect(target_node, target_material, source_node=node) + except Exception as e: + print(f"同步材质透明状态失败: {e}") + + def _set_material_base_color(self, material, color): + """Set material base color using the APIs available in the current Panda build.""" + try: + from panda3d.core import Vec4 + + color_vec = Vec4(*color) + if hasattr(material, "set_base_color"): + material.set_base_color(color_vec) + elif hasattr(material, "setBaseColor"): + material.setBaseColor(color_vec) + elif hasattr(material, "setDiffuse"): + material.setDiffuse(color_vec) + except Exception as e: + print(f"设置材质基础颜色失败: {e}") + + def _update_material_base_color(self, material, component, value): """更新材质基础颜色""" try: @@ -645,12 +1180,7 @@ class PropertyHelpers: new_color_tuple = tuple(new_color) - if hasattr(material, 'set_base_color'): - from panda3d.core import Vec4 - material.set_base_color(Vec4(*new_color_tuple)) - elif hasattr(material, 'setDiffuse'): - from panda3d.core import Vec4 - material.setDiffuse(Vec4(*new_color_tuple)) + self._set_material_base_color(material, new_color_tuple) except Exception as e: print(f"更新材质基础颜色失败: {e}") @@ -730,10 +1260,12 @@ class PropertyHelpers: preset = presets[preset_name] # 应用基础颜色 - if hasattr(material, 'set_base_color'): - material.set_base_color(preset["base_color"]) - elif hasattr(material, 'setDiffuse'): - material.setDiffuse(preset["base_color"]) + self._set_material_base_color(material, ( + preset["base_color"].x, + preset["base_color"].y, + preset["base_color"].z, + preset["base_color"].w, + )) # 应用PBR属性 if hasattr(material, 'set_roughness'): @@ -742,6 +1274,10 @@ class PropertyHelpers: material.set_metallic(preset["metallic"]) if hasattr(material, 'set_refractive_index'): material.set_refractive_index(preset["ior"]) + if hasattr(material, "set_emission"): + surface_type = 3.0 if preset_name == "玻璃" else 0.0 + opacity = preset["base_color"].w if preset_name == "玻璃" else 1.0 + material.set_emission(Vec4(surface_type, 0.0, opacity, 0.0)) print(f"已应用材质预设: {preset_name}") except Exception as e: @@ -751,23 +1287,10 @@ class PropertyHelpers: def _apply_material_to_node(self, node): """为节点应用材质""" try: - from panda3d.core import Material, Vec4 - - # 检查是否已有材质 - materials = node.find_all_materials() - - if not materials: - # 创建新材质 - material = Material(f"default-material-{node.getName()}") - material.setBaseColor(Vec4(0.8, 0.8, 0.8, 1.0)) - material.setDiffuse(Vec4(0.8, 0.8, 0.8, 1.0)) - material.setAmbient(Vec4(0.4, 0.4, 0.4, 1.0)) - material.setSpecular(Vec4(0.1, 0.1, 0.1, 1.0)) - material.setShininess(10.0) - node.setMaterial(material, 1) - print(f"已为新节点创建默认材质") - else: - print(f"节点已有 {len(materials)} 个材质") + material = self._ensure_material_for_node(node) + if material: + self._apply_material_surface_state(node, material) + print("材质已应用到节点") except Exception as e: print(f"应用材质失败: {e}") @@ -775,17 +1298,14 @@ class PropertyHelpers: def _reset_material(self, node): """重置节点材质""" try: - materials = node.find_all_materials() + materials = list(node.find_all_materials()) for material in materials: # 重置为默认材质属性 from panda3d.core import Vec4 default_color = Vec4(0.8, 0.8, 0.8, 1.0) - if hasattr(material, 'set_base_color'): - material.set_base_color(default_color) - elif hasattr(material, 'setDiffuse'): - material.setDiffuse(default_color) + self._set_material_base_color(material, (0.8, 0.8, 0.8, 1.0)) if hasattr(material, 'set_roughness'): material.set_roughness(0.5) @@ -793,6 +1313,9 @@ class PropertyHelpers: material.set_metallic(0.0) if hasattr(material, 'set_refractive_index'): material.set_refractive_index(1.5) + if hasattr(material, 'set_emission'): + material.set_emission(Vec4(0.0, 0.0, 1.0, 0.0)) + self._apply_material_surface_state(node, material) print(f"已重置材质") except Exception as e: @@ -960,28 +1483,8 @@ class PropertyHelpers: if not node or node.isEmpty(): return - render_pipeline = getattr(self, "render_pipeline", None) - if not render_pipeline: - return - - # 避免重复设置 effect - if node.hasTag("material_effect_metallic_enabled"): - return - - render_pipeline.set_effect( - node, - "effects/pbr_with_metallic.yaml", - { - "normal_mapping": True, - "render_gbuffer": True, - "alpha_testing": False, - "parallax_mapping": False, - "render_shadow": True, - "render_envmap": True - }, - 60 - ) node.setTag("material_effect_metallic_enabled", "1") + self._sync_material_render_effect(node) except Exception as e: print(f"启用金属性贴图 effect 失败: {e}") @@ -992,10 +1495,6 @@ class PropertyHelpers: if not node or node.isEmpty(): return - render_pipeline = getattr(self, "render_pipeline", None) - if not render_pipeline: - return - # 已启用金属性增强 effect 时,不覆盖它 if node.hasTag("material_effect_metallic_enabled"): return @@ -1004,21 +1503,8 @@ class PropertyHelpers: if parallax_enabled: node.setTag("material_effect_parallax_enabled", "1") - # 为了避免因未知默认值导致 normal mapping 关闭,这里显式设置 - render_pipeline.set_effect( - node, - "effects/default.yaml", - { - "normal_mapping": True, - "render_gbuffer": True, - "alpha_testing": False, - "parallax_mapping": parallax_enabled, - "render_shadow": True, - "render_envmap": True - }, - 55 - ) node.setTag("material_effect_default_texture_enabled", "1") + self._sync_material_render_effect(node) except Exception as e: print(f"启用默认贴图 effect 失败: {e}") @@ -1298,13 +1784,9 @@ class PropertyHelpers: elif model_index == 3: # 透明着色模型 print("设置透明着色模型...") if hasattr(material, 'set_emission'): - current_emission = material.emission or Vec4(0, 0, 0, 0) - new_emission = Vec4(3.0, 0, 0, 0) # 3表示透明着色模型 + current_emission = material.emission or Vec4(0, 0, 1, 0) + new_emission = Vec4(3.0, current_emission.y, current_emission.z, current_emission.w) material.set_emission(new_emission) - - # 设置默认透明度 - if hasattr(material, 'shading_model_param0'): - material.shading_model_param0 = 0.8 # 默认80%不透明度 print("透明着色模型设置完成") @@ -1325,8 +1807,10 @@ class PropertyHelpers: def _update_transparency(self, material, opacity_value): """更新透明度""" try: - if hasattr(material, 'shading_model_param0'): - material.shading_model_param0 = opacity_value + from panda3d.core import Vec4 + if hasattr(material, 'set_emission'): + current_emission = material.emission or Vec4(3, 0, 1, 0) + material.set_emission(Vec4(current_emission.x, current_emission.y, opacity_value, current_emission.w)) print(f"透明度已更新: {opacity_value}") except Exception as e: print(f"更新透明度失败: {e}") @@ -1492,6 +1976,13 @@ class PropertyHelpers: def _apply_color_selection(self): """应用颜色选择""" + if self._color_picker_callback: + try: + self._color_picker_callback(self._color_picker_current_color) + except Exception as e: + print(f"颜色回调执行失败: {e}") + return + if not self._color_picker_target: return @@ -1501,12 +1992,8 @@ class PropertyHelpers: # 应用颜色到目标对象 if hasattr(target_object, 'setColor'): target_object.setColor(self._color_picker_current_color) - elif hasattr(target_object, property_name): + elif property_name and hasattr(target_object, property_name): setattr(target_object, property_name, self._color_picker_current_color) - - # 调用回调函数 - if self._color_picker_callback: - self._color_picker_callback(self._color_picker_current_color) except Exception as e: print(f"应用颜色失败: {e}") @@ -1602,6 +2089,7 @@ class PropertyHelpers: self._font_selector_active = True self._font_selector_target = (target_object, property_name) self._font_selector_current_font = current_font or "" + self._font_selector_search_text = "" self._font_selector_callback = callback @@ -1632,7 +2120,10 @@ class PropertyHelpers: imgui.text(f"当前字体: {self._font_selector_current_font or '默认'}") # 字体搜索框 - changed, search_text = imgui.input_text("搜索", "", 256) + changed, search_text = imgui.input_text("搜索", self._font_selector_search_text, 256) + if changed: + self._font_selector_search_text = search_text + search_text = self._font_selector_search_text imgui.separator() # 字体列表 diff --git a/ui/panels/script_panels.py b/ui/panels/script_panels.py index 6c3b684d..433b4667 100644 --- a/ui/panels/script_panels.py +++ b/ui/panels/script_panels.py @@ -63,11 +63,21 @@ class ScriptPanels: # 输入框 imgui.separator() - changed, command = imgui.input_text(">", "", 256) - if changed and command: + submit, new_command = imgui.input_text( + ">", + self.console_command_input, + flags=imgui.InputTextFlags_.enter_returns_true.value, + ) + if new_command != self.console_command_input: + self.console_command_input = new_command + imgui.same_line() + execute_clicked = imgui.button("执行") + if (submit or execute_clicked) and self.console_command_input.strip(): + command = self.console_command_input.strip() self.add_info_message(f"执行命令: {command}") + self.console_command_input = "" # TODO: 实现命令执行逻辑 - + imgui.separator() # 视角控制信息