diff --git a/Resources/textures/布料 针织法线贴图.jpg b/Resources/textures/布料 针织法线贴图.jpg new file mode 100644 index 00000000..6cc35fd1 Binary files /dev/null and b/Resources/textures/布料 针织法线贴图.jpg differ diff --git a/Resources/textures/针织 编织 布料法线纹理贴图.jpg b/Resources/textures/针织 编织 布料法线纹理贴图.jpg new file mode 100644 index 00000000..b33d0358 Binary files /dev/null and b/Resources/textures/针织 编织 布料法线纹理贴图.jpg differ diff --git a/core/Command_System.py b/core/Command_System.py index daaf60ed..95b3c088 100644 --- a/core/Command_System.py +++ b/core/Command_System.py @@ -270,10 +270,10 @@ class DeleteNodeCommand(Command): elif light_type == "point_light" and hasattr(scene_manager, 'Pointlight') and self.node in scene_manager.Pointlight: scene_manager.Pointlight.remove(self.node) - elif self.node_type == "IMPORTED_MODEL_NODE" and hasattr(scene_manager, - 'models') and self.node in scene_manager.models: + if hasattr(scene_manager, 'models') and self.node in scene_manager.models: scene_manager.models.remove(self.node) - elif self.node_type.startswith("GUI_") and hasattr(self.world, + + if self.node_type.startswith("GUI_") and hasattr(self.world, 'gui_elements') and self.node in self.world.gui_elements: self.world.gui_elements.remove(self.node) elif self.node_type == "CESIUM_TILESET_NODE": @@ -286,49 +286,43 @@ class DeleteNodeCommand(Command): for i in reversed(tilesets_to_remove): del scene_manager.tilesets[i] - # 从场景图中移除节点 + # 从场景图中移除节点,使用 detachNode 而不是 removeNode 以便可以撤销 if self.node and not self.node.isEmpty(): - self.node.removeNode() + self.node.detachNode() def undo(self): """ - 撤销删除操作(重新创建节点) + 撤销删除操作(恢复旧节点) """ try: - # 使用场景管理器重建节点 - if self.world and hasattr(self.world, 'scene_manager'): - scene_manager = self.world.scene_manager - - # 创建节点数据字典 - node_data = { - 'name': self.node_name, - 'node_type': self.node_type, - 'pos': (self.node_pos.x, self.node_pos.y, self.node_pos.z), - 'hpr': (self.node_hpr.x, self.node_hpr.y, self.node_hpr.z), - 'scale': (self.node_scale.x, self.node_scale.y, self.node_scale.z), - 'tags': self.node_tags - } - - # 添加额外数据 - if self.extra_data: - if self.node_type.startswith("GUI_"): - node_data['gui_data'] = self.extra_data - elif self.node_type == "LIGHT_NODE": - node_data['light_data'] = self.extra_data.get('light_data', {}) + if self.node and not self.node.isEmpty(): + # 直接将节点挂载回原父节点 + self.node.reparentTo(self.parent_node) + + # 恢复到相应的管理器列表中 + if self.world and hasattr(self.world, 'scene_manager'): + scene_manager = self.world.scene_manager + if self.node_type == "LIGHT_NODE": + if self.node.hasTag("light_type"): + light_type = self.node.getTag("light_type") + if light_type == "spot_light" and hasattr(scene_manager, 'Spotlight') and self.node not in scene_manager.Spotlight: + scene_manager.Spotlight.append(self.node) + elif light_type == "point_light" and hasattr(scene_manager, 'Pointlight') and self.node not in scene_manager.Pointlight: + scene_manager.Pointlight.append(self.node) + + if hasattr(scene_manager, 'models') and self.node not in scene_manager.models: + scene_manager.models.append(self.node) + + if self.node_type.startswith("GUI_") and hasattr(self.world, 'gui_elements') and self.node not in self.world.gui_elements: + self.world.gui_elements.append(self.node) elif self.node_type == "CESIUM_TILESET_NODE": - node_data['tileset_url'] = self.extra_data.get('tileset_url', '') - - # 重建节点 - new_node = scene_manager.recreateNodeFromData(node_data, self.parent_node) - - if new_node: - print(f"✅ 成功撤销删除操作,节点 {self.node_name} 已恢复") - # 更新节点引用 - self.node = new_node - else: - print(f"❌ 撤销删除操作失败,无法重建节点 {self.node_name}") + # 简单恢复到 tilesets + if hasattr(scene_manager, 'tilesets'): + scene_manager.tilesets.append({'node': self.node, 'url': self.extra_data.get('tileset_url', '')}) + + print(f"✅ 成功撤销删除操作,节点 {self.node_name} 已恢复") else: - print("❌ 无法撤销删除操作,缺少场景管理器引用") + print("❌ 无法撤销删除操作,节点引用已丢失") except Exception as e: print(f"❌ 撤销删除操作时出错: {e}") @@ -424,7 +418,7 @@ class CreateNodeCommand(Command): 撤销创建节点操作 """ if self.created_node: - self.created_node.removeNode() + self.created_node.detachNode() def redo(self): """ diff --git a/core/terrain_manager.py b/core/terrain_manager.py index a1c57dfb..a0dedff3 100644 --- a/core/terrain_manager.py +++ b/core/terrain_manager.py @@ -39,15 +39,16 @@ class TerrainManager: # 获取树形控件 tree_widget = self._get_tree_widget() - if not tree_widget: - print("❌ 无法访问树形控件") - return None - - # 获取目标父节点列表 - target_parents = tree_widget.get_target_parents_for_creation() + target_parents = None + if tree_widget: + target_parents = tree_widget.get_target_parents_for_creation() + if not target_parents: - print("❌ 没有找到有效的父节点") - return None + if hasattr(self.world, 'render'): + target_parents = [(None, self.world.render)] + else: + print("❌ 没有找到有效的父节点") + return None created_terrains = [] try: @@ -143,18 +144,34 @@ class TerrainManager: self.terrains.append(terrain_info) - print(f"✅ 为 {parent_item.text(0)} 创建高度图地形: {terrain_name}") + parent_name = parent_item.text(0) if parent_item else "root" + print(f"✅ 为 {parent_name} 创建高度图地形: {terrain_name}") # 在Qt树形控件中添加对应节点 - qt_item = tree_widget.add_node_to_tree_widget(terrain_node, parent_item, "TERRAIN_NODE") + qt_item = None + if tree_widget and parent_item: + qt_item = tree_widget.add_node_to_tree_widget(terrain_node, parent_item, "TERRAIN_NODE") + if qt_item: created_terrains.append((terrain_node, qt_item)) else: created_terrains.append((terrain_node, None)) - print("⚠️ Qt树节点添加失败,但Panda3D对象已创建") + print("⚠️ Qt树节点添加跳过或失败,但Panda3D对象已创建") + + # 添加到场景管理器(ImGui环境使用) + if hasattr(self.world, 'scene_manager') and self.world.scene_manager: + if not hasattr(self.world.scene_manager, 'models'): + self.world.scene_manager.models = [] + if terrain_node not in self.world.scene_manager.models: + self.world.scene_manager.models.append(terrain_node) + + # 更新场景树 + if hasattr(self.world, 'updateSceneTree'): + self.world.updateSceneTree() except Exception as e: - print(f"❌ 为 {parent_item.text(0)} 创建高度图地形: {str(e)}") + parent_name = parent_item.text(0) if parent_item else "root" + print(f"❌ 为 {parent_name} 创建高度图地形失败: {str(e)}") return None # 处理创建结果 @@ -191,15 +208,16 @@ class TerrainManager: # 获取树形控件 tree_widget = self._get_tree_widget() - if not tree_widget: - print("❌ 无法访问树形控件") - return None + target_parents = None + if tree_widget: + target_parents = tree_widget.get_target_parents_for_creation() - # 获取目标父节点列表 - target_parents = tree_widget.get_target_parents_for_creation() if not target_parents: - print("❌ 没有找到有效的父节点") - return None + if hasattr(self.world, 'render'): + target_parents = [(None, self.world.render)] + else: + print("❌ 没有找到有效的父节点") + return None created_terrains = [] @@ -260,6 +278,8 @@ class TerrainManager: # 添加材质 self._applyTerrainMaterial(terrain_node) + + terrain_node.setPythonTag("selectable", True) # 保存地形信息(包括高度图) terrain_info = { @@ -273,18 +293,34 @@ class TerrainManager: self.terrains.append(terrain_info) - print(f"✅ 为 {parent_item.text(0)} 创建平面地形: {terrain_name}") + parent_name = parent_item.text(0) if parent_item else "root" + print(f"✅ 为 {parent_name} 创建平面地形: {terrain_name}") # 在Qt树形控件中添加对应节点 - qt_item = tree_widget.add_node_to_tree_widget(terrain_node, parent_item, "TERRAIN_NODE") + qt_item = None + if tree_widget and parent_item: + qt_item = tree_widget.add_node_to_tree_widget(terrain_node, parent_item, "TERRAIN_NODE") + if qt_item: created_terrains.append((terrain_node, qt_item)) else: created_terrains.append((terrain_node, None)) - print("⚠️ Qt树节点添加失败,但Panda3D对象已创建") + print("⚠️ Qt树节点添加跳过或失败,但Panda3D对象已创建") + + # 添加到场景管理器(ImGui环境使用) + if hasattr(self.world, 'scene_manager') and self.world.scene_manager: + if not hasattr(self.world.scene_manager, 'models'): + self.world.scene_manager.models = [] + if terrain_node not in self.world.scene_manager.models: + self.world.scene_manager.models.append(terrain_node) + + # 更新场景树 + if hasattr(self.world, 'updateSceneTree'): + self.world.updateSceneTree() except Exception as e: - print(f"❌ 为 {parent_item.text(0)} 创建平面地形: {str(e)}") + parent_name = parent_item.text(0) if parent_item else "root" + print(f"❌ 为 {parent_name} 创建平面地形失败: {str(e)}") return None # 处理创建结果 @@ -501,14 +537,18 @@ class TerrainManager: tree_widget = self._get_tree_widget() if tree_widget: tree_widget.delete_items(tree_widget.selectedItems()) - # terrain_node.removeNode() - # - # # 从列表中移除 - # self.terrains.remove(terrain_info) + else: + terrain_node.removeNode() + + # 从列表中移除 + self.terrains.remove(terrain_info) - # # 更新场景树 - # if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'updateSceneTree'): - # self.world.scene_manager.updateSceneTree() + # 更新场景树 (如果是使用了ImGui界面或兼容界面) + if hasattr(self.world, 'scene_manager'): + if hasattr(self.world.scene_manager, 'updateSceneTree'): + self.world.scene_manager.updateSceneTree() + if hasattr(self.world.scene_manager, 'models') and terrain_node in self.world.scene_manager.models: + self.world.scene_manager.models.remove(terrain_node) print(f"✓ 地形已删除: {terrain_info.get('name', 'Unknown')}") diff --git a/imgui.ini b/imgui.ini index 4e92f077..a75a3c70 100644 --- a/imgui.ini +++ b/imgui.ini @@ -24,26 +24,26 @@ Size=832,45 Collapsed=0 [Window][工具栏] -Pos=323,20 -Size=1870,32 +Pos=273,20 +Size=1276,32 Collapsed=0 DockId=0x0000000D,0 [Window][场景树] Pos=0,20 -Size=321,854 +Size=271,634 Collapsed=0 DockId=0x00000007,0 [Window][属性面板] -Pos=2195,20 -Size=365,1331 +Pos=1551,20 +Size=369,989 Collapsed=0 DockId=0x00000003,0 [Window][控制台] -Pos=0,876 -Size=321,475 +Pos=0,656 +Size=271,353 Collapsed=0 DockId=0x00000008,0 @@ -60,7 +60,7 @@ Collapsed=0 [Window][WindowOverViewport_11111111] Pos=0,20 -Size=2560,1331 +Size=1920,989 Collapsed=0 [Window][测试窗口1] @@ -84,7 +84,7 @@ Size=400,300 Collapsed=0 [Window][选择路径] -Pos=980,425 +Pos=660,254 Size=600,500 Collapsed=0 @@ -94,13 +94,13 @@ Size=500,400 Collapsed=0 [Window][导入模型] -Pos=980,425 +Pos=660,254 Size=600,500 Collapsed=0 [Window][资源管理器] -Pos=323,977 -Size=1870,374 +Pos=273,817 +Size=1276,192 Collapsed=0 DockId=0x00000006,0 @@ -150,8 +150,8 @@ Size=101,226 Collapsed=0 [Window][LUI编辑器] -Pos=1540,412 -Size=380,597 +Pos=1628,412 +Size=292,597 Collapsed=0 DockId=0x00000004,0 @@ -195,18 +195,23 @@ Pos=474,130 Size=120,384 Collapsed=0 +[Window][选择emission纹理文件##texture_dialog] +Pos=660,304 +Size=600,400 +Collapsed=0 + [Docking][Data] -DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=2560,1331 Split=X - DockNode ID=0x00000001 Parent=0x08BD597D SizeRef=1553,989 Split=X - DockNode ID=0x00000009 Parent=0x00000001 SizeRef=321,989 Split=Y Selected=0xE0015051 +DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=1920,989 Split=X + DockNode ID=0x00000001 Parent=0x08BD597D SizeRef=1549,989 Split=X + DockNode ID=0x00000009 Parent=0x00000001 SizeRef=271,989 Split=Y Selected=0xE0015051 DockNode ID=0x00000007 Parent=0x00000009 SizeRef=271,634 Selected=0xE0015051 DockNode ID=0x00000008 Parent=0x00000009 SizeRef=271,353 Selected=0x5428E753 - DockNode ID=0x0000000A Parent=0x00000001 SizeRef=1230,989 Split=Y + DockNode ID=0x0000000A Parent=0x00000001 SizeRef=1276,989 Split=Y DockNode ID=0x0000000D Parent=0x0000000A SizeRef=1318,32 HiddenTabBar=1 Selected=0x43A39006 DockNode ID=0x0000000E Parent=0x0000000A SizeRef=1318,955 Split=Y - DockNode ID=0x00000005 Parent=0x0000000E SizeRef=1341,921 CentralNode=1 - DockNode ID=0x00000006 Parent=0x0000000E SizeRef=1341,374 Selected=0x3A2E05C3 - DockNode ID=0x00000002 Parent=0x08BD597D SizeRef=365,989 Split=Y Selected=0x3188AB8D + DockNode ID=0x00000005 Parent=0x0000000E SizeRef=1341,761 CentralNode=1 + DockNode ID=0x00000006 Parent=0x0000000E SizeRef=1341,192 Selected=0x3A2E05C3 + DockNode ID=0x00000002 Parent=0x08BD597D SizeRef=369,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 diff --git a/scene/scene_manager.py b/scene/scene_manager.py index 9f89b417..c3d207ac 100644 --- a/scene/scene_manager.py +++ b/scene/scene_manager.py @@ -1,6 +1,3 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - """ 场景管理器 - 负责场景和模型管理的核心功能 处理模型导入、场景树构建、材质系统、碰撞设置等 diff --git a/ui/panels/app_actions.py b/ui/panels/app_actions.py index 751fb79a..8c6143af 100644 --- a/ui/panels/app_actions.py +++ b/ui/panels/app_actions.py @@ -21,6 +21,21 @@ class AppActions: else: setattr(self.app, name, value) + def _resolve_cut_copy_node(self, node): + """Resolve selection to a stable scene root for copy/cut/paste.""" + if not node or node.isEmpty(): + return node + + current = node + while current and not current.isEmpty(): + if current.getName() == "render": + break + if current.hasTag("tree_item_type") or current.hasTag("is_model_root") or current.hasTag("is_scene_element"): + return current + current = current.getParent() + + return node + def _toggle_hot_reload(self): """切换热重载状态""" @@ -508,7 +523,7 @@ class AppActions: return # 获取当前选中的节点 - selected_node = self.selection.selectedNode + selected_node = self._resolve_cut_copy_node(self.selection.selectedNode) if not selected_node: self.add_warning_message("没有选中的节点") return @@ -543,7 +558,7 @@ class AppActions: return # 获取当前选中的节点 - selected_node = self.selection.selectedNode + selected_node = self._resolve_cut_copy_node(self.selection.selectedNode) if not selected_node: self.add_warning_message("没有选中的节点") return @@ -559,15 +574,16 @@ class AppActions: node_data = self.scene_manager.serializeNodeForCopy(selected_node) if node_data: self.clipboard = [node_data] - # Cut uses serialized restore path after original deletion. - self.clipboard_source_nodes = [] + # Cut preserves the source node references for cloning in paste + self.clipboard_source_nodes = [selected_node] self.clipboard_mode = "cut" # 删除原节点 - self._delete_node(selected_node) - self.selection.clearSelection() - - self.add_success_message(f"已剪切节点: {node_name}") + if self._delete_node(selected_node): + self.selection.clearSelection() + self.add_success_message(f"已剪切节点: {node_name}") + else: + self.add_error_message(f"剪切失败,无法删除节点: {node_name}") else: self.add_error_message("节点序列化失败") else: @@ -612,20 +628,24 @@ class AppActions: created = None # Copy mode: prefer direct NodePath clone to preserve visual geometry. - if self.clipboard_mode == "copy" and i < len(source_nodes): + if self.clipboard_mode in ("copy", "cut") and i < len(source_nodes): source_node = source_nodes[i] if source_node and not source_node.isEmpty(): try: - created = source_node.copyTo(parent) - if hasattr(self.scene_manager, "_generateUniqueName"): - unique_name = self.scene_manager._generateUniqueName(source_node.getName(), parent) - created.setName(unique_name) - # Preserve model source tags so later cut/paste can rebuild real model. - for _tag in ("model_path", "saved_model_path", "original_path", "file", "element_type"): - if source_node.hasTag(_tag): - created.setTag(_tag, source_node.getTag(_tag)) - # Offset slightly so the new node can be seen immediately. - created.setPos(created.getX() + 0.2, created.getY() + 0.2, created.getZ()) + if self.clipboard_mode == "cut": + source_node.reparentTo(parent) + created = source_node + else: + created = source_node.copyTo(parent) + if hasattr(self.scene_manager, "_generateUniqueName"): + unique_name = self.scene_manager._generateUniqueName(source_node.getName(), parent) + created.setName(unique_name) + # Preserve model source tags so later cut/paste can rebuild real model. + for _tag in ("model_path", "saved_model_path", "original_path", "file", "element_type"): + if source_node.hasTag(_tag): + created.setTag(_tag, source_node.getTag(_tag)) + # Offset slightly so the new node can be seen immediately. + created.setPos(created.getX() + 0.2, created.getY() + 0.2, created.getZ()) except Exception: created = None diff --git a/ui/panels/editor_panels.py b/ui/panels/editor_panels.py index 6aaf1837..cd944023 100644 --- a/ui/panels/editor_panels.py +++ b/ui/panels/editor_panels.py @@ -386,7 +386,7 @@ class EditorPanels: # SSBO模式下,模型可能不在 scene_manager.models 中,补充显示 ssbo_editor.model ssbo_editor = getattr(self.app, "ssbo_editor", None) ssbo_model = getattr(ssbo_editor, "model", None) if ssbo_editor else None - if ssbo_model and not ssbo_model.isEmpty() and ssbo_model not in models: + if ssbo_model and not ssbo_model.isEmpty() and ssbo_model.hasParent() and ssbo_model not in models: models.append(ssbo_model) if models: diff --git a/ui/panels/property_helpers.py b/ui/panels/property_helpers.py index 3a59c40a..9de3205d 100644 --- a/ui/panels/property_helpers.py +++ b/ui/panels/property_helpers.py @@ -790,68 +790,413 @@ class PropertyHelpers: def _select_texture_for_material(self, node, material, texture_type): """为材质选择纹理""" try: - # 设置当前纹理对话框状态 - self._current_texture_dialog = { - 'node': node, - 'material': material, - 'texture_type': texture_type - } - - # 初始化路径 - if not hasattr(self, '_texture_dialog_path'): - self._texture_dialog_path = '/home/hello/EG/Resources' - - # 设置文件类型过滤 - self._texture_dialog_filter = "*.png" - + import tkinter as tk + from tkinter import filedialog + + # 推断初始目录 + initial_dir = getattr(self, "_texture_dialog_path", "") + if not initial_dir or not os.path.exists(initial_dir): + candidates = [ + os.path.join(os.getcwd(), "Resources", "textures"), + os.path.join(os.getcwd(), "Resources"), + os.getcwd(), + ] + initial_dir = next((p for p in candidates if os.path.exists(p)), os.getcwd()) + + # 弹出系统文件选择窗口 + root = tk.Tk() + root.withdraw() + try: + root.attributes("-topmost", True) + except Exception: + pass + + selected_path = filedialog.askopenfilename( + title=f"选择{texture_type}贴图", + initialdir=initial_dir, + filetypes=[ + ("Image Files", "*.png;*.jpg;*.jpeg;*.bmp;*.tif;*.tiff;*.tga;*.dds;*.ktx;*.hdr;*.exr"), + ("All Files", "*.*"), + ], + ) + root.destroy() + + if selected_path and os.path.exists(selected_path): + self._texture_dialog_path = os.path.dirname(selected_path) + self._apply_texture_to_material(node, material, texture_type, selected_path) + else: + print(f"已取消选择{texture_type}贴图") + except Exception as e: print(f"选择纹理失败: {e}") + def _get_material_texture_slots(self): + """材质贴图类型到固定槽位映射(对应 p3d_TextureN)""" + return { + "diffuse": 0, # p3d_Texture0 + "normal": 1, # p3d_Texture1 + "ior": 2, # p3d_Texture2 + "roughness": 3, # p3d_Texture3 + "parallax": 4, # p3d_Texture4 + "metallic": 5, # p3d_Texture5 + "emission": 6, # p3d_Texture6 + "ao": 7, # p3d_Texture7 + "alpha": 8, # p3d_Texture8 + "detail": 9, # p3d_Texture9 + "gloss": 10 # p3d_Texture10 + } + + + def _get_material_stage_mode_map(self): + """不同贴图类型对应的 TextureStage 模式""" + from panda3d.core import TextureStage + return { + "diffuse": TextureStage.MModulate, + "normal": TextureStage.MNormal, + "roughness": TextureStage.MGloss, + "metallic": TextureStage.MSelector, + "emission": TextureStage.MGlow, + "ao": TextureStage.MSelector, + "alpha": TextureStage.MBlend, + "parallax": TextureStage.MHeight, + "detail": TextureStage.MModulate, + "gloss": TextureStage.MGloss, + "ior": TextureStage.MSelector, + } + + + def _find_texture_stage_by_name(self, node, stage_name): + """按名称查找节点上的纹理阶段""" + texture_stages = node.findAllTextureStages() + for i in range(texture_stages.getNumTextureStages()): + stage = texture_stages.getTextureStage(i) + if stage and stage.getName() == stage_name: + return stage + return None + + + def _get_neutral_texture(self, texture_type): + """获取指定贴图类型的中性占位纹理(1x1)""" + try: + from panda3d.core import Texture + + if not hasattr(self, "_neutral_texture_cache"): + self._neutral_texture_cache = {} + if texture_type in self._neutral_texture_cache: + return self._neutral_texture_cache[texture_type] + + # 各类型中性值,确保在未显式设置该通道时不影响结果 + neutral_rgba = { + "diffuse": (255, 255, 255, 255), # 白色,不改变底色 + "normal": (128, 128, 255, 255), # 平面法线 + "ior": (0, 0, 0, 255), # blend_ior 中的“无附加影响”值 + "roughness": (255, 255, 255, 255), # 1.0 + "parallax": (0, 0, 0, 255), # 0 高度 + "metallic": (255, 255, 255, 255), # 1.0,保持乘法中性 + "emission": (0, 0, 0, 255), # 无自发光 + "ao": (255, 255, 255, 255), # 1.0,不衰减 + "alpha": (255, 255, 255, 255), # 完全不透明 + "detail": (255, 255, 255, 255), # 不改变细节叠加 + "gloss": (0, 0, 0, 255), # 0 光泽(对 roughness 影响最小) + } + + rgba = neutral_rgba.get(texture_type, (255, 255, 255, 255)) + tex = Texture(f"__neutral_{texture_type}") + tex.setup2dTexture(1, 1, Texture.T_unsigned_byte, Texture.F_rgba8) + tex.setRamImage(bytes(rgba)) + tex.setMagfilter(Texture.FTNearest) + tex.setMinfilter(Texture.FTNearest) + + self._neutral_texture_cache[texture_type] = tex + return tex + except Exception as e: + print(f"创建中性纹理失败({texture_type}): {e}") + return None + + + def _ensure_texture_slot_alignment(self, node, target_slot, texture_slots, stage_mode_map): + """补齐低位槽的占位纹理,确保 p3d_TextureN 与固定槽位一致""" + try: + from panda3d.core import TextureStage + except Exception: + return + + slot_to_type = {slot: tex_type for tex_type, slot in texture_slots.items()} + for required_slot in range(target_slot): + required_type = slot_to_type.get(required_slot) + if not required_type: + continue + + stage_name = f"{required_type}_map" + if self._find_texture_stage_by_name(node, stage_name): + continue + + neutral_texture = self._get_neutral_texture(required_type) + if not neutral_texture: + continue + + stage = TextureStage(stage_name) + stage.setSort(required_slot) + stage.setMode(stage_mode_map.get(required_type, TextureStage.MSelector)) + node.setTexture(stage, neutral_texture, 1) + + + def _ensure_metallic_texture_effect(self, node): + """启用支持 p3d_Texture5 的 effect,确保金属性贴图生效""" + try: + 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") + except Exception as e: + print(f"启用金属性贴图 effect 失败: {e}") + + + def _ensure_default_texture_effect(self, node, enable_parallax=False): + """启用默认贴图 effect(主要用于法线/粗糙度等通道)""" + try: + 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 + + parallax_enabled = enable_parallax or node.hasTag("material_effect_parallax_enabled") + 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") + except Exception as e: + print(f"启用默认贴图 effect 失败: {e}") + + + def _set_material_scalar_property(self, material, prop_name, value): + """安全设置材质标量属性(兼容 snake_case / CamelCase)""" + try: + if not material: + return False + + setter_map = { + "roughness": ("set_roughness", "setRoughness"), + "metallic": ("set_metallic", "setMetallic"), + "ior": ("set_refractive_index", "setRefractiveIndex"), + } + setters = setter_map.get(prop_name, ()) + for setter_name in setters: + if hasattr(material, setter_name): + getattr(material, setter_name)(value) + return True + + if hasattr(material, prop_name): + setattr(material, prop_name, value) + return True + except Exception as e: + print(f"设置材质属性失败({prop_name}={value}): {e}") + return False + + + def _get_material_emission(self, material): + """安全获取材质发光向量""" + try: + if not material: + return None + if hasattr(material, "emission") and material.emission is not None: + return material.emission + if hasattr(material, "get_emission"): + return material.get_emission() + if hasattr(material, "getEmission"): + return material.getEmission() + except Exception: + return None + return None + + + def _set_material_emission(self, material, emission_vec): + """安全设置材质发光向量""" + try: + if not material: + return False + if hasattr(material, "set_emission"): + material.set_emission(emission_vec) + return True + if hasattr(material, "setEmission"): + material.setEmission(emission_vec) + return True + if hasattr(material, "emission"): + material.emission = emission_vec + return True + except Exception as e: + print(f"设置材质发光参数失败: {e}") + return False + + + def _configure_material_for_texture_map(self, material, texture_type): + """贴图驱动参数修正:避免必须手动把数值调到 1.0 才生效""" + try: + from panda3d.core import Vec4 + + if not material: + return + + if texture_type == "roughness": + self._set_material_scalar_property(material, "roughness", 1.0) + elif texture_type == "metallic": + # 默认 metallic=0 会把金属贴图乘没,设置为 1 让贴图直接驱动 + self._set_material_scalar_property(material, "metallic", 1.0) + elif texture_type == "normal": + # RenderPipeline 中 normalfactor 映射到 emission.y + emission = self._get_material_emission(material) + if emission is None: + emission = Vec4(0.0, 0.0, 0.0, 1.0) + + new_emission = Vec4( + float(getattr(emission, "x", 0.0)), + 1.0, + float(getattr(emission, "z", 0.0)), + float(getattr(emission, "w", 1.0)), + ) + self._set_material_emission(material, new_emission) + except Exception as e: + print(f"修正贴图驱动参数失败({texture_type}): {e}") + + def _apply_texture_to_material(self, node, material, texture_type, texture_path): """应用纹理到材质""" try: - from panda3d.core import TextureStage - from direct.showbase import Loader - - # 加载纹理 - loader = Loader.Loader(self) - texture = loader.loadTexture(texture_path) + import os + from panda3d.core import TextureStage, Filename + + # 加载纹理(优先使用应用的 loader,避免创建临时 Loader 导致状态不一致) + loader = getattr(self, "loader", None) + if not loader: + print("无法加载纹理: loader 不可用") + return + + # 关键修复:把系统路径统一转换成 Panda3D 可识别格式(避免 D:/ 与 /d/ 冲突) + normalized_path = texture_path + if texture_path: + try: + from scene import util + normalized_path = util.normalize_model_path(texture_path) + except Exception: + # 在 scene 模块初始化阶段可能出现循环导入,回退到 Panda3D 标准路径 + normalized_path = Filename.from_os_specific(texture_path).get_fullpath() + panda_filename = Filename.from_os_specific(texture_path) if texture_path else None + + texture = None + candidate_paths = [] + if normalized_path: + candidate_paths.append(normalized_path) + if panda_filename: + candidate_paths.append(panda_filename) + candidate_paths.append(panda_filename.get_fullpath()) + if texture_path: + candidate_paths.append(texture_path) + + for path_candidate in candidate_paths: + if not path_candidate: + continue + try: + texture = loader.loadTexture(path_candidate) + if texture: + break + except Exception: + continue if not texture: - print(f"无法加载纹理: {texture_path}") + print(f"无法加载纹理: {texture_path} (normalized={normalized_path})") return # 设置纹理属性 texture.setMagfilter(texture.FTLinear) texture.setMinfilter(texture.FTLinearMipmapLinear) - # 纹理槽位映射 - texture_slots = { - "diffuse": 0, # p3d_Texture0 - "ior": 2, # p3d_Texture2 - "normal": 1, # p3d_Texture1 - "roughness": 3, # p3d_Texture3 - "parallax": 4, # p3d_Texture4 - "metallic": 5, # p3d_Texture5 - "emission": 6, # p3d_Texture6 - "ao": 7, # p3d_Texture7 - "alpha": 8, # p3d_Texture8 - "detail": 9, # p3d_Texture9 - "gloss": 10 # p3d_Texture10 - } - - slot = texture_slots.get(texture_type, 0) - - # 创建纹理阶段 - texture_stage = TextureStage(f"{texture_type}_map") + texture_type = (texture_type or "").strip().lower() + texture_slots = self._get_material_texture_slots() + if texture_type not in texture_slots: + print(f"未知纹理类型: {texture_type}") + return + + slot = texture_slots[texture_type] + + # 纹理阶段名保持稳定,便于重复设置时精确替换 + stage_name = f"{texture_type}_map" + + # 修正材质参数,确保贴图不需要手工把滑块调到 1.0 才可见 + self._configure_material_for_texture_map(material, texture_type) + + # 清理同类型旧 stage,避免叠加造成覆盖/污染 + old_stage = self._find_texture_stage_by_name(node, stage_name) + if old_stage: + node.clearTexture(old_stage) + + # 关键修复:补齐低位槽,避免 p3d_TextureN 因“缺槽”而错位 + stage_mode_map = self._get_material_stage_mode_map() + self._ensure_texture_slot_alignment(node, slot, texture_slots, stage_mode_map) + + texture_stage = TextureStage(stage_name) texture_stage.setSort(slot) - texture_stage.setMode(TextureStage.MModulate) + texture_stage.setMode(stage_mode_map.get(texture_type, TextureStage.MSelector)) + + # 应用 stage(用于可视状态与序列化) + node.setTexture(texture_stage, texture, 1) + + # 同时写入 shader 输入,进一步保证固定槽位可直接读取 + node.setShaderInput(f"p3d_Texture{slot}", texture) + + # 金属性贴图需要额外 effect 才会被默认管线采样 + if texture_type == "metallic": + self._ensure_metallic_texture_effect(node) + else: + # 法线贴图依赖 normal_mapping 宏,显式开启默认 effect + self._ensure_default_texture_effect(node, enable_parallax=(texture_type == "parallax")) + + # 记录路径,便于 UI 展示和后续恢复 + if texture_path: + node.setTag(f"material_texture_{texture_type}", os.path.normpath(normalized_path or texture_path)) - # 应用纹理到节点 - node.setTexture(texture_stage, texture) - - print(f"已应用{texture_type}纹理: {texture_path}") + print(f"已应用{texture_type}纹理到槽位 p3d_Texture{slot}: {texture_path}") except Exception as e: print(f"应用纹理失败: {e}") @@ -862,7 +1207,24 @@ class PropertyHelpers: try: # 清除所有纹理阶段 node.clearTexture() - node.clearTexture() + + # 清理额外 shader input 与记录标签 + texture_slots = self._get_material_texture_slots() + for texture_type, slot in texture_slots.items(): + try: + node.clearShaderInput(f"p3d_Texture{slot}") + except Exception: + pass + tag_name = f"material_texture_{texture_type}" + if node.hasTag(tag_name): + node.clearTag(tag_name) + if node.hasTag("material_effect_metallic_enabled"): + node.clearTag("material_effect_metallic_enabled") + if node.hasTag("material_effect_default_texture_enabled"): + node.clearTag("material_effect_default_texture_enabled") + if node.hasTag("material_effect_parallax_enabled"): + node.clearTag("material_effect_parallax_enabled") + print("已清除所有纹理") except Exception as e: print(f"清除纹理失败: {e}") @@ -871,22 +1233,38 @@ class PropertyHelpers: def _display_current_textures(self, node, material): """显示当前纹理信息""" try: - from panda3d.core import TextureStage - # 获取所有纹理阶段 texture_stages = node.findAllTextureStages() - if not texture_stages: + has_any = False + + if texture_stages: + imgui.text("当前纹理:") + for i in range(texture_stages.getNumTextureStages()): + stage = texture_stages.getTextureStage(i) + texture = node.getTexture(stage) + if texture: + stage_name = stage.getName() or "未命名" + # 隐藏未被用户显式设置的占位纹理,避免面板误导 + if stage_name.endswith("_map"): + tex_type = stage_name[:-4] + if tex_type and not node.hasTag(f"material_texture_{tex_type}"): + continue + + texture_name = texture.getName() or "未命名" + imgui.text(f" {stage_name}: {texture_name}") + has_any = True + + # 显示属性面板记录的贴图路径(包含非漫反射通道) + tracked_types = ["diffuse", "normal", "roughness", "metallic", "emission", "ao", "alpha", "parallax", "detail", "gloss", "ior"] + for texture_type in tracked_types: + tag_name = f"material_texture_{texture_type}" + if node.hasTag(tag_name): + imgui.text(f" {texture_type}: {node.getTag(tag_name)}") + has_any = True + + if not has_any: imgui.text_colored((0.7, 0.7, 0.7, 1.0), "当前无纹理") - return - - imgui.text("当前纹理:") - for stage in texture_stages: - texture = node.getTexture(stage) - if texture: - texture_name = texture.getName() or "未命名" - stage_name = stage.getName() or "未命名" - imgui.text(f" {stage_name}: {texture_name}") except Exception as e: print(f"显示纹理信息失败: {e}") @@ -943,147 +1321,8 @@ class PropertyHelpers: def _draw_texture_file_dialog(self): - """绘制纹理文件选择对话框""" - if not hasattr(self, '_current_texture_dialog') or not self._current_texture_dialog: - return - - try: - dialog_data = self._current_texture_dialog - node = dialog_data['node'] - material = dialog_data['material'] - texture_type = dialog_data['texture_type'] - - # 设置对话框标志 - flags = (imgui.WindowFlags_.no_resize | - imgui.WindowFlags_.no_collapse | - imgui.WindowFlags_.modal) - - # 获取屏幕尺寸,居中显示对话框 - display_size = imgui.get_io().display_size - dialog_width = 600 - dialog_height = 400 - imgui.set_next_window_size((dialog_width, dialog_height)) - imgui.set_next_window_pos( - ((display_size.x - dialog_width) / 2, (display_size.y - dialog_height) / 2) - ) - - # 显示文件选择对话框 - opened, window_open = imgui.begin(f"选择{texture_type}纹理文件##texture_dialog", True, flags) - if not window_open: - self._current_texture_dialog = None - imgui.end() - return - - imgui.text(f"选择{texture_type}纹理文件") - imgui.separator() - - # 当前路径显示 - current_path = getattr(self, '_texture_dialog_path', '/home/hello/EG/Resources') - imgui.text(f"当前路径: {current_path}") - - imgui.separator() - - # 文件类型过滤 - imgui.text("支持的纹理格式:") - file_types = ["*.png", "*.jpg", "*.jpeg", "*.bmp", "*.tga", "*.dds"] - - current_filter = getattr(self, '_texture_dialog_filter', "*.png") - if imgui.begin_combo("文件类型##texture_filter", current_filter): - for i, file_type in enumerate(file_types): - if imgui.selectable(file_type, i == file_types.index(current_filter)): - self._texture_dialog_filter = file_type - imgui.end_combo() - - imgui.separator() - - # 路径导航按钮 - if imgui.button("上级目录##up_dir"): - current_path = os.path.dirname(current_path) - self._texture_dialog_path = current_path - - imgui.same_line() - if imgui.button("主目录##home_dir"): - self._texture_dialog_path = '/home/hello/EG/Resources' - - imgui.same_line() - if imgui.button("当前目录##current_dir"): - self._texture_dialog_path = '/home/hello/EG/Resources' - - imgui.same_line() - if imgui.button("纹理目录##textures_dir"): - self._texture_dialog_path = '/home/hello/EG/Resources/textures' - - imgui.separator() - - # 文件列表 - if imgui.begin_child("file_list##texture_files", (580, 200)): - try: - # 列出目录和文件 - items = [] - if os.path.exists(current_path): - for item in os.listdir(current_path): - item_path = os.path.join(current_path, item) - if os.path.isdir(item_path): - items.append(('dir', item, item_path)) - elif any(item.lower().endswith(ext[1:]) for ext in file_types): - items.append(('file', item, item_path)) - - # 排序:目录在前,文件在后 - items.sort(key=lambda x: (x[0], x[1].lower())) - - for item_type, item_name, item_path in items: - if item_type == 'dir': - if imgui.selectable(f"📁 {item_name}##dir_{item_name}", False)[0]: - self._texture_dialog_path = item_path - else: - selected, _ = imgui.selectable(f"📄 {item_name}##file_{item_name}", False) - if selected: - # 应用选择的纹理 - self._apply_texture_to_material(node, material, texture_type, item_path) - # 关闭对话框 - self._current_texture_dialog = None - break - - except Exception as e: - imgui.text_colored((1.0, 0.5, 0.5, 1.0), f"读取目录失败: {e}") - - imgui.end_child() - - imgui.separator() - - # 路径输入框 - changed, new_path = imgui.input_text("文件路径##texture_path", current_path, 512) - if changed: - self._texture_dialog_path = new_path - - imgui.same_line() - if imgui.button("确认路径##confirm_path"): - if os.path.isfile(new_path): - # 检查文件扩展名 - file_ext = os.path.splitext(new_path)[1].lower() - if file_ext in [ext[1:] for ext in file_types]: - self._apply_texture_to_material(node, material, texture_type, new_path) - self._current_texture_dialog = None - else: - print(f"不支持的文件格式: {file_ext}") - else: - print("请选择有效的文件") - - imgui.separator() - - # 按钮 - if imgui.button("取消##cancel_texture"): - self._current_texture_dialog = None - - imgui.end() - - except Exception as e: - print(f"绘制纹理对话框失败: {e}") - # 确保在异常情况下也调用 imgui.end() - try: - imgui.end() - except: - pass + """系统文件对话框模式下无需绘制 ImGui 贴图对话框""" + return def start_transform_monitoring(self, node): """开始变换监控"""