编辑操作,地形生成,材质系统问题修复

This commit is contained in:
Hector 2026-02-25 17:16:24 +08:00
parent 2766fb9faa
commit 69d83c6ab5
9 changed files with 607 additions and 312 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 MiB

View File

@ -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.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)
# 创建节点数据字典
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 hasattr(scene_manager, 'models') and self.node not in scene_manager.models:
scene_manager.models.append(self.node)
# 添加额外数据
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_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', '')
# 简单恢复到 tilesets
if hasattr(scene_manager, 'tilesets'):
scene_manager.tilesets.append({'node': self.node, '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}")
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):
"""

View File

@ -39,13 +39,14 @@ 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()
if not target_parents:
if hasattr(self.world, 'render'):
target_parents = [(None, self.world.render)]
else:
print("❌ 没有找到有效的父节点")
return None
@ -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 = 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,13 +208,14 @@ 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()
if not target_parents:
if hasattr(self.world, 'render'):
target_parents = [(None, self.world.render)]
else:
print("❌ 没有找到有效的父节点")
return None
@ -261,6 +279,8 @@ class TerrainManager:
# 添加材质
self._applyTerrainMaterial(terrain_node)
terrain_node.setPythonTag("selectable", True)
# 保存地形信息(包括高度图)
terrain_info = {
'terrain': terrain,
@ -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 = 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()
# # 更新场景树
# if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'updateSceneTree'):
# self.world.scene_manager.updateSceneTree()
# 从列表中移除
self.terrains.remove(terrain_info)
# 更新场景树 (如果是使用了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')}")

View File

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

View File

@ -1,6 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
场景管理器 - 负责场景和模型管理的核心功能
处理模型导入场景树构建材质系统碰撞设置等

View File

@ -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)
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,10 +628,14 @@ 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:
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)

View File

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

View File

@ -790,47 +790,53 @@ 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
}
import tkinter as tk
from tkinter import filedialog
# 初始化路径
if not hasattr(self, '_texture_dialog_path'):
self._texture_dialog_path = '/home/hello/EG/Resources'
# 推断初始目录
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())
# 设置文件类型过滤
self._texture_dialog_filter = "*.png"
# 弹出系统文件选择窗口
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 _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)
if not texture:
print(f"无法加载纹理: {texture_path}")
return
# 设置纹理属性
texture.setMagfilter(texture.FTLinear)
texture.setMinfilter(texture.FTLinearMipmapLinear)
# 纹理槽位映射
texture_slots = {
def _get_material_texture_slots(self):
"""材质贴图类型到固定槽位映射(对应 p3d_TextureN"""
return {
"diffuse": 0, # p3d_Texture0
"ior": 2, # p3d_Texture2
"normal": 1, # p3d_Texture1
"ior": 2, # p3d_Texture2
"roughness": 3, # p3d_Texture3
"parallax": 4, # p3d_Texture4
"metallic": 5, # p3d_Texture5
@ -841,17 +847,356 @@ class PropertyHelpers:
"gloss": 10 # p3d_Texture10
}
slot = texture_slots.get(texture_type, 0)
# 创建纹理阶段
texture_stage = TextureStage(f"{texture_type}_map")
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:
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} (normalized={normalized_path})")
return
# 设置纹理属性
texture.setMagfilter(texture.FTLinear)
texture.setMinfilter(texture.FTLinearMipmapLinear)
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))
# 应用纹理到节点
node.setTexture(texture_stage, texture)
# 应用 stage用于可视状态与序列化
node.setTexture(texture_stage, texture, 1)
print(f"已应用{texture_type}纹理: {texture_path}")
# 同时写入 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))
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:
imgui.text_colored((0.7, 0.7, 0.7, 1.0), "当前无纹理")
return
has_any = False
if texture_stages:
imgui.text("当前纹理:")
for stage in texture_stages:
for i in range(texture_stages.getNumTextureStages()):
stage = texture_stages.getTextureStage(i)
texture = node.getTexture(stage)
if texture:
texture_name = texture.getName() or "未命名"
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), "当前无纹理")
except Exception as e:
print(f"显示纹理信息失败: {e}")
@ -943,148 +1321,9 @@ class PropertyHelpers:
def _draw_texture_file_dialog(self):
"""绘制纹理文件选择对话框"""
if not hasattr(self, '_current_texture_dialog') or not self._current_texture_dialog:
"""系统文件对话框模式下无需绘制 ImGui 贴图对话框"""
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
def start_transform_monitoring(self, node):
"""开始变换监控"""
if node and not node.isEmpty():