Fix transparency visibility issue

This commit is contained in:
ayuan9957 2026-03-12 22:07:48 +08:00
parent 780536203e
commit b48616d5f2
32 changed files with 1686 additions and 509 deletions

View File

@ -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"

BIN
__codex_opaque_check.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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

154
main.py
View File

@ -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):

View File

@ -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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 785 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 785 KiB

View File

@ -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

View File

@ -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("脚本管理器已打开")

View File

@ -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

View File

@ -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元素属性"""

View File

@ -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), "透明度控制不可用")

View File

@ -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]

View File

@ -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:

View File

@ -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)

View File

@ -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()
# 字体列表

View File

@ -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()
# 视角控制信息