from traceback import print_exc from types import new_class from typing import Hashable from PyQt5.QtWidgets import (QLabel, QLineEdit, QDoubleSpinBox, QPushButton, QTreeWidget, QTreeWidgetItem, QMenu, QCheckBox, QComboBox, QHBoxLayout, QWidget, QVBoxLayout, QGroupBox, QGridLayout) from PyQt5.QtCore import Qt from deploy_libs.unicodedata import normalize from direct.actor.Actor import Actor from panda3d.core import Vec3, Vec4, transpose, TransparencyAttrib, PartGroup from scene import util class PropertyPanelManager: """属性面板管理器""" def __init__(self, world): """初始化属性面板管理器""" self.world = world self._propertyLayout = None self._actor_cache={} # 定义紧凑样式 self.compact_style = """ QDoubleSpinBox { min-width: 45px; } QPushButton { min-width: 10px; } QComboBox { min-width: 60px; } QLineEdit { min-width: 60px; } QCheckBox { min-width: 20px; } """ def setPropertyLayout(self, layout): """设置属性面板布局引用""" print("开始设置属性布局") print(f"布局类型: {type(layout)}") # 保存布局引用 self._propertyLayout = layout # 确保布局有父部件 if not layout.parent(): print("布局没有父部件,创建新的容器") from PyQt5.QtWidgets import QWidget container = QWidget() container.setObjectName("PropertyContainer") container.setLayout(layout) print(f"布局父部件: {self._propertyLayout.parent().objectName() if self._propertyLayout.parent() else 'None'}") print(f"布局项目数: {self._propertyLayout.count()}") return True def clearPropertyPanel(self): """清空属性面板""" if self._propertyLayout: while self._propertyLayout.count(): item = self._propertyLayout.takeAt(0) if item.widget(): item.widget().deleteLater() def updateNodeVisibilityAfterDrag(self, item): """拖拽结束后更新节点的可见性状态""" node = item.data(0, Qt.UserRole) if not node: return # 当节点被拖拽后,需要根据新父节点的状态来更新可见性 self._syncEffectiveVisibility(node) def updatePropertyPanel(self, item): """更新属性面板显示""" if not self._propertyLayout or not self._propertyLayout.parent(): print("属性布局未设置或没有父部件!") return self.clearPropertyPanel() # 应用紧凑样式到属性面板容器 if self._propertyLayout.parent(): self._propertyLayout.parent().setStyleSheet(self.compact_style) itemText = item.text(0) # 如果点击的是场景根节点,显示提示信息 if itemText == "场景": tipLabel = QLabel("") tipLabel.setStyleSheet("color: gray;") # self._propertyLayout.addRow(tipLabel) self._propertyLayout.addWidget(tipLabel) return model = item.data(0, Qt.UserRole) user_visible = True if model: user_visible = model.getPythonTag("user_visible") if user_visible is None: user_visible = True model.setPythonTag("user_visible", True) self.name_group = QGroupBox("物体名称") name_layout = QHBoxLayout() self.active_check = QCheckBox() # 根据模型的实际可见性状态设置复选框 self.active_check.setChecked(user_visible) self.name_input = QLineEdit(itemText) name_layout.addWidget(self.active_check) name_layout.addWidget(self.name_input) self.name_group.setLayout(name_layout) self._propertyLayout.addWidget(self.name_group) if model: try: self.active_check.stateChanged.disconnect() except TypeError: pass self.active_check.stateChanged.connect( lambda state,m = model:self._setUserVisible(m,state == Qt.Checked) ) # nameLabel = QLabel("名称:") # nameEdit = QLineEdit(itemText) # self._propertyLayout.addRow(nameLabel, nameEdit) # 获取节点对象 model = item.data(0, Qt.UserRole) # 检查是否是GUI元素 if model and hasattr(model, 'getTag') and model.getTag("gui_type"): self.updateGUIPropertyPanel(model) pass elif model and hasattr(model, 'getTag') and model.getTag("light_type"): self.updateLightPropertyPanel(model) pass # 如果找到模型,显示其属性 elif model: self._updateModelPropertyPanel(model) pass # 显示脚本属性 # self._updateScriptPropertyPanel(model) self._propertyLayout.addStretch() # 强制更新布局 if self._propertyLayout: self._propertyLayout.update() propertyWidget = self._propertyLayout.parentWidget() if propertyWidget: propertyWidget.update() def _setUserVisible(self,node,visible): node.setPythonTag("user_visible",visible) self._syncEffectiveVisibility(node) def _syncEffectiveVisibility(self, start_node): """广度优先,确保父隐藏则子一定隐藏""" # 获取起始节点的父节点 parent_node = start_node.getParent() # 确定父节点的有效可见性 parent_effective_visible = True if parent_node: parent_effective_visible = parent_node.getPythonTag("effective_visible") if parent_effective_visible is None: parent_effective_visible = True q = deque([(start_node, parent_effective_visible)]) # (node, parent_effective_visible) while q: node, parent_eff = q.popleft() user = node.getPythonTag("user_visible") if user is None: user = True eff = parent_eff and user node.setPythonTag("effective_visible", eff) # 特殊处理:检查是否为碰撞体节点 if node.getName().startswith("modelCollision_"): node.hide() else: if eff: node.show() else: node.hide() for child in node.getChildren(): q.append((child, eff)) def _toggleModelVisibility(self, model, state): """切换模型可见性状态""" try: # 用我们自己维护的可见性接口,而不是直接 show/hide visible = (state == Qt.Checked) self._setUserVisible(model, visible) collision_nodes = model.findAllMatches("**/modelCollision_*") for collision_node in collision_nodes: collision_node.hide() except Exception as e: print(f"切换模型可见性失败: {str(e)}") def refreshModelValues(self, nodePath): """实时刷新模型所有属性数值(相对/世界位置、旋转、缩放)""" if not nodePath or self._propertyLayout is None: return parent = nodePath.getParent() render = self.world.render # 世界根节点 # ---------------- 相对位置 ---------------- relPos = nodePath.getPos(parent) if parent else nodePath.getPos() if hasattr(self, '_xSpin') and self._xSpin: self._xSpin.blockSignals(True) self._xSpin.setValue(relPos.x) self._xSpin.blockSignals(False) if hasattr(self, '_ySpin') and self._ySpin: self._ySpin.blockSignals(True) self._ySpin.setValue(relPos.y) self._ySpin.blockSignals(False) if hasattr(self, '_zSpin') and self._zSpin: self._zSpin.blockSignals(True) self._zSpin.setValue(relPos.z) self._zSpin.blockSignals(False) # ---------------- 世界位置 ---------------- worldPos = nodePath.getPos(render) for axis, attr in zip(('x', 'y', 'z'), ('_worldXSpin', '_worldYSpin', '_worldZSpin')): spin = getattr(self, attr, None) if spin: spin.blockSignals(True) spin.setValue(getattr(worldPos, axis)) spin.blockSignals(False) # ---------------- 旋转 ---------------- hpr = nodePath.getHpr() for idx, (attr, val) in enumerate(zip(('_hSpin', '_pSpin', '_rSpin'), hpr)): spin = getattr(self, attr, None) if spin: spin.blockSignals(True) spin.setValue(val) spin.blockSignals(False) # ---------------- 缩放 ---------------- scale = nodePath.getScale() for axis, attr in zip(('x', 'y', 'z'), ('_xScaleSpin', '_yScaleSpin', '_zScaleSpin')): spin = getattr(self, attr, None) if spin: spin.blockSignals(True) spin.setValue(getattr(scale, axis)) spin.blockSignals(False) def _refreshWorldPos(self,model): if not hasattr(self,'worldXSpin'): return world = model.getPos(self.world.render) self._worldXSpin.setValue(world.x) self._worldYSpin.setValue(world.y) self._worldZSpin.setValue(world.z) def _updateModelPropertyPanel(self, model): """更新模型属性面板""" # 获取父节点 parent = model.getParent() # 变换属性部分 (Transform) self.transform_group = QGroupBox("变换 Transform") transform_layout = QGridLayout() # 获取当前值 relativePos = model.getPos(parent) if parent else model.getPos() worldPos = model.getPos(self.world.render) # 位置 (Position) transform_layout.addWidget(QLabel("相对位置"), 0, 0) self.pos_x = QDoubleSpinBox() self.pos_y = QDoubleSpinBox() self.pos_z = QDoubleSpinBox() # 设置位置控件属性 for pos_widget in [self.pos_x, self.pos_y, self.pos_z]: pos_widget.setRange(-1000000.0, 1000000.0) self.pos_x.setValue(relativePos.getX()) self.pos_y.setValue(relativePos.getY()) self.pos_z.setValue(relativePos.getZ()) # 连接位置变化事件 # self.pos_x.valueChanged.connect(lambda v: model.setX(parent, v) if parent else model.setX(v)) # self.pos_y.valueChanged.connect(lambda v: model.setY(parent, v) if parent else model.setY(v)) # self.pos_z.valueChanged.connect(lambda v: model.setZ(parent, v) if parent else model.setZ(v)) def updateXPosition(value): model.setX(value) self.refreshModelValues(model) self.pos_x.valueChanged.connect(updateXPosition) def updateYPosition(value): model.setY(value) self.refreshModelValues(model) self.pos_y.valueChanged.connect(updateYPosition) def updateZPosition(value): model.setZ(value) self.refreshModelValues(model) self.pos_z.valueChanged.connect(updateZPosition) # 创建并设置 X, Y, Z 标签居中 x_label1 = QLabel("X") y_label1 = QLabel("Y") z_label1 = QLabel("Z") x_label1.setAlignment(Qt.AlignCenter) y_label1.setAlignment(Qt.AlignCenter) z_label1.setAlignment(Qt.AlignCenter) transform_layout.addWidget(x_label1, 0, 1) transform_layout.addWidget(y_label1, 0, 2) transform_layout.addWidget(z_label1, 0, 3) transform_layout.addWidget(self.pos_x, 1, 1) transform_layout.addWidget(self.pos_y, 1, 2) transform_layout.addWidget(self.pos_z, 1, 3) # 世界位置 (只读) transform_layout.addWidget(QLabel("世界位置"), 2, 0) self.world_pos_x = QDoubleSpinBox() self.world_pos_y = QDoubleSpinBox() self.world_pos_z = QDoubleSpinBox() # 设置世界位置控件属性 for world_pos_widget in [self.world_pos_x, self.world_pos_y, self.world_pos_z]: world_pos_widget.setRange(-1000000.0, 1000000.0) world_pos_widget.setReadOnly(True) self.world_pos_x.setValue(worldPos.getX()) self.world_pos_y.setValue(worldPos.getY()) self.world_pos_z.setValue(worldPos.getZ()) transform_layout.addWidget(self.world_pos_x, 3, 1) transform_layout.addWidget(self.world_pos_y, 3, 2) transform_layout.addWidget(self.world_pos_z, 3, 3) # 旋转 (Rotation) transform_layout.addWidget(QLabel("旋转"), 4, 0) self.rot_h = QDoubleSpinBox() self.rot_p = QDoubleSpinBox() self.rot_r = QDoubleSpinBox() # 设置旋转控件属性 for rot_widget in [self.rot_h, self.rot_p, self.rot_r]: rot_widget.setRange(-360, 360) self.rot_h.setValue(model.getH()) self.rot_p.setValue(model.getP()) self.rot_r.setValue(model.getR()) # 连接旋转变化事件 self.rot_h.valueChanged.connect(lambda v: model.setH(v)) self.rot_p.valueChanged.connect(lambda v: model.setP(v)) self.rot_r.valueChanged.connect(lambda v: model.setR(v)) # 创建并设置 H, P, R 标签居中 h_label = QLabel("H") p_label = QLabel("P") r_label = QLabel("R") h_label.setAlignment(Qt.AlignCenter) p_label.setAlignment(Qt.AlignCenter) r_label.setAlignment(Qt.AlignCenter) transform_layout.addWidget(h_label, 4, 1) transform_layout.addWidget(p_label, 4, 2) transform_layout.addWidget(r_label, 4, 3) transform_layout.addWidget(self.rot_h, 5, 1) transform_layout.addWidget(self.rot_p, 5, 2) transform_layout.addWidget(self.rot_r, 5, 3) # 缩放 (Scale) transform_layout.addWidget(QLabel("缩放"), 6, 0) self.scale_x = QDoubleSpinBox() self.scale_y = QDoubleSpinBox() self.scale_z = QDoubleSpinBox() # 设置缩放控件属性 for scale_widget in [self.scale_x, self.scale_y, self.scale_z]: scale_widget.setRange(0.01, 100) scale_widget.setSingleStep(0.1) self.scale_x.setValue(model.getScale().getX()) self.scale_y.setValue(model.getScale().getY()) self.scale_z.setValue(model.getScale().getZ()) # 连接缩放变化事件 self.scale_x.valueChanged.connect(lambda v: model.setScale(v, model.getScale().getY(), model.getScale().getZ())) self.scale_y.valueChanged.connect(lambda v: model.setScale(model.getScale().getX(), v, model.getScale().getZ())) self.scale_z.valueChanged.connect(lambda v: model.setScale(model.getScale().getX(), model.getScale().getY(), v)) # 创建并设置 X, Y, Z 标签居中 x_label3 = QLabel("X") y_label3 = QLabel("Y") z_label3 = QLabel("Z") x_label3.setAlignment(Qt.AlignCenter) y_label3.setAlignment(Qt.AlignCenter) z_label3.setAlignment(Qt.AlignCenter) transform_layout.addWidget(x_label3, 6, 1) transform_layout.addWidget(y_label3, 6, 2) transform_layout.addWidget(z_label3, 6, 3) transform_layout.addWidget(self.scale_x, 7, 1) transform_layout.addWidget(self.scale_y, 7, 2) transform_layout.addWidget(self.scale_z, 7, 3) self.transform_group.setLayout(transform_layout) self._propertyLayout.addWidget(self.transform_group) # 动画和太阳方位角面板 self._addAnimationPanel(model) self._addSunAzimuthPanel() # # 材质属性组 self._updateModelMaterialPanel(model) def refreshModelValues(self,nodePath): if not nodePath or self._propertyLayout is None: return parent = nodePath.getParent() render = self.world.render relPos = nodePath.getPos(parent) if parent else nodePath.getPos() if hasattr(self,'pos_x') and self.pos_x: self.pos_x.blockSignals(True) self.pos_x.setValue(relPos.getX()) self.pos_x.blockSignals(False) if hasattr(self,'pos_y') and self.pos_y: self.pos_y.blockSignals(True) self.pos_y.setValue(relPos.getY()) self.pos_y.blockSignals(False) if hasattr(self,'pos_z') and self.pos_z: self.pos_z.blockSignals(True) self.pos_z.setValue(relPos.getZ()) self.pos_z.blockSignals(False) worldPos = nodePath.getPos(render) for axis,attr in zip(('x','y','z'),('world_pos_x','world_pos_y','world_pos_z')): spin = getattr(self,attr,None) if spin: spin.blockSignals(True) spin.setValue(getattr(worldPos,axis)) spin.blockSignals(False) hpr = nodePath.getHpr() for idx,(attr,val) in enumerate(zip(('rot_h','rot_p','rot_r'),hpr)): spin = getattr(self,attr,None) if spin: spin.blockSignals(True) spin.setValue(val) spin.blockSignals(False) scale = nodePath.getScale() for axis,attr in zip(('x','y','z'),('scale_x','scale_y','scale_z')): spin = getattr(self,attr,None) if spin: spin.blockSignals(True) spin.setValue(getattr(scale,axis)) spin.blockSignals(False) def updateGUIPropertyPanel(self, gui_element): """更新GUI元素属性面板""" gui_type = gui_element.getTag("gui_type") gui_text = gui_element.getTag("gui_text") # GUI基本信息组 gui_info_group = QGroupBox("GUI信息") gui_info_layout = QGridLayout() # GUI类型显示 gui_info_layout.addWidget(QLabel("GUI类型:"), 0, 0) typeValue = QLabel(gui_type) # typeValue.setStyleSheet("color: #00AAFF; font-weight: bold;") gui_info_layout.addWidget(typeValue, 0, 1) # 文本属性(如果适用) if gui_type in ["button", "label", "entry", "3d_text", "virtual_screen"]: gui_info_layout.addWidget(QLabel("文本:"), 1, 0) textEdit = QLineEdit(gui_text or "") # 创建一个更新函数来处理文本变化 def updateText(text): success = self.world.gui_manager.editGUIElement(gui_element, "text", text) if success: # 更新场景树显示的名称 self.world.scene_manager.updateSceneTree() textEdit.textChanged.connect(updateText) gui_info_layout.addWidget(textEdit, 1, 1) gui_info_group.setLayout(gui_info_layout) self._propertyLayout.addWidget(gui_info_group) # 变换属性组(合并位置和变换) if hasattr(gui_element, 'getPos'): # 根据GUI类型设置组名 if gui_type in ["button", "label", "entry"]: transform_group = QGroupBox("变换 Rect Transform") else: transform_group = QGroupBox("变换 Transform") transform_layout = QGridLayout() pos = gui_element.getPos() # 根据GUI类型决定位置编辑方式 if gui_type in ["button", "label", "entry"]: # 2D GUI组件使用屏幕坐标 logical_x = pos.getX() / 0.1 # 反向转换为逻辑坐标 logical_z = pos.getZ() / 0.1 # 屏幕位置控件 transform_layout.addWidget(QLabel("屏幕位置"), 0, 0) # X, Z 标签居中 x_label = QLabel("X") z_label = QLabel("Z") x_label.setAlignment(Qt.AlignCenter) z_label.setAlignment(Qt.AlignCenter) transform_layout.addWidget(x_label, 0, 1) transform_layout.addWidget(z_label, 0, 2) xPos = QDoubleSpinBox() xPos.setRange(-50, 50) xPos.setValue(logical_x) xPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUI2DPosition(gui_element, "x", v)) transform_layout.addWidget(xPos, 1, 1) zPos = QDoubleSpinBox() zPos.setRange(-50, 50) zPos.setValue(logical_z) zPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUI2DPosition(gui_element, "z", v)) transform_layout.addWidget(zPos, 1, 2) # 显示实际屏幕坐标(只读) transform_layout.addWidget(QLabel("实际坐标"), 2, 0) actualXLabel = QLabel(f"{pos.getX():.3f}") actualXLabel.setStyleSheet("color: gray; font-size: 10px;") actualZLabel = QLabel(f"{pos.getZ():.3f}") actualZLabel.setStyleSheet("color: gray; font-size: 10px;") transform_layout.addWidget(actualXLabel, 3, 1) transform_layout.addWidget(actualZLabel, 3, 2) else: # 3D GUI组件使用世界坐标 transform_layout.addWidget(QLabel("世界位置"), 0, 0) # X, Y, Z 标签居中 x_label = QLabel("X") y_label = QLabel("Y") z_label = QLabel("Z") x_label.setAlignment(Qt.AlignCenter) y_label.setAlignment(Qt.AlignCenter) z_label.setAlignment(Qt.AlignCenter) transform_layout.addWidget(x_label, 0, 1) transform_layout.addWidget(y_label, 0, 2) transform_layout.addWidget(z_label, 0, 3) xPos = QDoubleSpinBox() xPos.setRange(-1000, 1000) xPos.setValue(pos.getX()) xPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUIElement(gui_element, "position", [v, pos.getY(), pos.getZ()])) transform_layout.addWidget(xPos, 1, 1) yPos = QDoubleSpinBox() yPos.setRange(-1000, 1000) yPos.setValue(pos.getY()) yPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUIElement(gui_element, "position", [pos.getX(), v, pos.getZ()])) transform_layout.addWidget(yPos, 1, 2) zPos = QDoubleSpinBox() zPos.setRange(-1000, 1000) zPos.setValue(pos.getZ()) zPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUIElement(gui_element, "position", [pos.getX(), pos.getY(), v])) transform_layout.addWidget(zPos, 1, 3) # 缩放属性 if hasattr(gui_element, 'getScale'): scale = gui_element.getScale() row_offset = 4 if gui_type in ["button", "label", "entry"] else 2 transform_layout.addWidget(QLabel("缩放"), row_offset, 0) scaleSpinBox = QDoubleSpinBox() scaleSpinBox.setRange(0.01, 10) scaleSpinBox.setSingleStep(0.1) scaleSpinBox.setValue(scale.getX()) scaleSpinBox.valueChanged.connect( lambda v: self.world.gui_manager.editGUIElement(gui_element, "scale", v)) transform_layout.addWidget(scaleSpinBox, row_offset, 1) transform_group.setLayout(transform_layout) self._propertyLayout.addWidget(transform_group) # 外观属性组 if gui_type in ["button", "label"]: appearance_group = QGroupBox("外观属性") appearance_layout = QGridLayout() appearance_layout.addWidget(QLabel("背景颜色:"), 0, 0) colorButton = QPushButton("选择颜色") colorButton.clicked.connect(lambda: self.world.gui_manager.selectGUIColor(gui_element)) appearance_layout.addWidget(colorButton, 0, 1) appearance_group.setLayout(appearance_layout) self._propertyLayout.addWidget(appearance_group) def _updateScriptPropertyPanel(self, game_object): """更新脚本属性面板""" # 获取对象上的脚本 scripts = self.world.getScripts(game_object) if scripts: # 添加脚本信息标题 scriptTitleLabel = QLabel("已挂载脚本:") scriptTitleLabel.setStyleSheet("color: #00AAFF; font-weight: bold; font-size: 12px;") self._propertyLayout.addRow(scriptTitleLabel) # 显示每个脚本的信息 for i, script_component in enumerate(scripts): script_name = script_component.script_name enabled = script_component.enabled # 脚本名称和状态 scriptLabel = QLabel(f"脚本 {i+1}:") scriptInfo = QLabel(f"{script_name}") scriptInfo.setStyleSheet("color: green; font-weight: bold;" if enabled else "color: gray;") self._propertyLayout.addRow(scriptLabel, scriptInfo) # 脚本启用/禁用按钮 enableButton = QPushButton("禁用" if enabled else "启用") enableButton.setStyleSheet( "background-color: #FF6B6B; color: white;" if enabled else "background-color: #4ECDC4; color: white;" ) enableButton.clicked.connect( lambda checked, sc=script_component: self._toggleScriptEnabled(sc) ) self._propertyLayout.addRow("状态:", enableButton) # 分隔线 if i < len(scripts) - 1: separator = QLabel("─" * 20) separator.setStyleSheet("color: lightgray;") self._propertyLayout.addRow(separator) else: # 显示无脚本信息 noScriptLabel = QLabel("无挂载脚本") noScriptLabel.setStyleSheet("color: gray; font-style: italic;") self._propertyLayout.addRow("脚本:", noScriptLabel) def _toggleScriptEnabled(self, script_component): """切换脚本启用状态""" script_component.enabled = not script_component.enabled status = "启用" if script_component.enabled else "禁用" print(f"脚本 {script_component.script_name} 已{status}") # 刷新属性面板显示 if hasattr(self.world.selection, 'selectedObject') and self.world.selection.selectedObject: # 找到当前选中项并更新 tree_widget = self.world.treeWidget if tree_widget and tree_widget.currentItem(): self.updatePropertyPanel(tree_widget.currentItem()) def updateLightPropertyPanel(self, model): """更新光源属性面板""" light_object = model.getPythonTag("rp_light_object") if light_object: # 变换属性组 transform_group = QGroupBox("变换 Transform") transform_layout = QGridLayout() # 位置属性 current_pos = light_object.pos transform_layout.addWidget(QLabel("位置"), 0, 0) # X, Y, Z 标签居中 x_label = QLabel("X") y_label = QLabel("Y") z_label = QLabel("Z") x_label.setAlignment(Qt.AlignCenter) y_label.setAlignment(Qt.AlignCenter) z_label.setAlignment(Qt.AlignCenter) transform_layout.addWidget(x_label, 0, 1) transform_layout.addWidget(y_label, 0, 2) transform_layout.addWidget(z_label, 0, 3) xPos = QDoubleSpinBox() xPos.setRange(-1000, 1000) xPos.setValue(current_pos.getX()) xPos.valueChanged.connect(lambda v: self._updateLightPosition(light_object, model, 'x', v)) transform_layout.addWidget(xPos, 1, 1) yPos = QDoubleSpinBox() yPos.setRange(-1000, 1000) yPos.setValue(current_pos.getY()) yPos.valueChanged.connect(lambda v: self._updateLightPosition(light_object, model, 'y', v)) transform_layout.addWidget(yPos, 1, 2) zPos = QDoubleSpinBox() zPos.setRange(-1000, 1000) zPos.setValue(current_pos.getZ()) zPos.valueChanged.connect(lambda v: self._updateLightPosition(light_object, model, 'z', v)) transform_layout.addWidget(zPos, 1, 3) # 世界位置(只读) worldPos = model.getPos(self.world.render) transform_layout.addWidget(QLabel("世界位置"), 2, 0) worldXPos = QDoubleSpinBox() worldXPos.setRange(-1000, 1000) worldXPos.setValue(worldPos.getX()) worldXPos.setReadOnly(True) transform_layout.addWidget(worldXPos, 3, 1) worldYPos = QDoubleSpinBox() worldYPos.setRange(-1000, 1000) worldYPos.setValue(worldPos.getY()) worldYPos.setReadOnly(True) transform_layout.addWidget(worldYPos, 3, 2) worldZPos = QDoubleSpinBox() worldZPos.setRange(-1000, 1000) worldZPos.setValue(worldPos.getZ()) worldZPos.setReadOnly(True) transform_layout.addWidget(worldZPos, 3, 3) # 旋转属性(仅聚光灯) if hasattr(light_object, 'direction'): current_hpr = model.getHpr() transform_layout.addWidget(QLabel("旋转"), 4, 0) # H, P, R 标签居中 h_label = QLabel("H") p_label = QLabel("P") r_label = QLabel("R") h_label.setAlignment(Qt.AlignCenter) p_label.setAlignment(Qt.AlignCenter) r_label.setAlignment(Qt.AlignCenter) transform_layout.addWidget(h_label, 4, 1) transform_layout.addWidget(p_label, 4, 2) transform_layout.addWidget(r_label, 4, 3) hRot = QDoubleSpinBox() hRot.setRange(-180, 180) hRot.setValue(current_hpr.getX()) hRot.valueChanged.connect(lambda v: self._updateLightRotation(light_object, model, 'h', v)) transform_layout.addWidget(hRot, 5, 1) pRot = QDoubleSpinBox() pRot.setRange(-180, 180) pRot.setValue(current_hpr.getY()) pRot.valueChanged.connect(lambda v: self._updateLightRotation(light_object, model, 'p', v)) transform_layout.addWidget(pRot, 5, 2) rRot = QDoubleSpinBox() rRot.setRange(-180, 180) rRot.setValue(current_hpr.getZ()) rRot.valueChanged.connect(lambda v: self._updateLightRotation(light_object, model, 'r', v)) transform_layout.addWidget(rRot, 5, 3) # 缩放属性 current_scale = model.getScale() scale_row = 6 if hasattr(light_object, 'direction') else 4 transform_layout.addWidget(QLabel("缩放"), scale_row, 0) xScaleSpinBox = QDoubleSpinBox() xScaleSpinBox.setRange(0.01, 100) xScaleSpinBox.setSingleStep(0.1) xScaleSpinBox.setValue(current_scale.getX()) xScaleSpinBox.valueChanged.connect(lambda v: self._updateLightScale(model, 'x', v)) transform_layout.addWidget(xScaleSpinBox, scale_row + 1, 1) yScaleSpinBox = QDoubleSpinBox() yScaleSpinBox.setRange(0.01, 100) yScaleSpinBox.setSingleStep(0.1) yScaleSpinBox.setValue(current_scale.getY()) yScaleSpinBox.valueChanged.connect(lambda v: self._updateLightScale(model, 'y', v)) transform_layout.addWidget(yScaleSpinBox, scale_row + 1, 2) zScaleSpinBox = QDoubleSpinBox() zScaleSpinBox.setRange(0.01, 100) zScaleSpinBox.setSingleStep(0.1) zScaleSpinBox.setValue(current_scale.getZ()) zScaleSpinBox.valueChanged.connect(lambda v: self._updateLightScale(model, 'z', v)) transform_layout.addWidget(zScaleSpinBox, scale_row + 1, 3) transform_group.setLayout(transform_layout) self._propertyLayout.addWidget(transform_group) # 光源属性组 light_group = QGroupBox("光源属性") light_layout = QGridLayout() # 能量 light_layout.addWidget(QLabel("能量:"), 0, 0) energySpinBox = QDoubleSpinBox() energySpinBox.setRange(0, 10000) energySpinBox.setValue(light_object.energy) energySpinBox.valueChanged.connect(lambda v: self._updateLightEnergy(light_object, v)) light_layout.addWidget(energySpinBox, 0, 1) # 半径 light_layout.addWidget(QLabel("半径:"), 1, 0) radiusSpinBox = QDoubleSpinBox() radiusSpinBox.setRange(1, 2000) radiusSpinBox.setValue(light_object.radius) radiusSpinBox.valueChanged.connect(lambda v: self._updateLightRadius(light_object, v)) light_layout.addWidget(radiusSpinBox, 1, 1) # 视野角度(仅聚光灯) if hasattr(light_object, 'fov'): light_layout.addWidget(QLabel("视野角度:"), 2, 0) fovSpinBox = QDoubleSpinBox() fovSpinBox.setRange(1, 180) fovSpinBox.setValue(light_object.fov) fovSpinBox.valueChanged.connect(lambda v: self._updateLightFOV(light_object, v)) light_layout.addWidget(fovSpinBox, 2, 1) light_group.setLayout(light_layout) self._propertyLayout.addWidget(light_group) def _updateLightPosition(self,light_object,node_path,axis,value): current_pos = light_object.pos if axis=='x': new_pos = Vec3(value,current_pos.getY(),current_pos.getZ()) elif axis == 'y': new_pos = Vec3(current_pos.getX(), value, current_pos.getZ()) else: # z new_pos = Vec3(current_pos.getX(), current_pos.getY(), value) # 更新RenderPipeline光源位置 light_object.pos = new_pos # 同步更新场景节点位置(用于显示) node_path.setPos(new_pos) def _updateLightRotation(self,light_object,node_path,axis,value): """更新光源旋转""" from panda3d.core import Vec3 current_hpr = node_path.getHpr() if axis=='h': new_hpr = Vec3(value,current_hpr.getY(),current_hpr.getZ()) elif axis=='p': new_hpr = Vec3(current_hpr.getX(),value,current_hpr.getZ()) else: new_hpr = Vec3(current_hpr.getX(),current_hpr.getY(),value) node_path.setHpr(new_hpr) if hasattr(light_object,'direction'): direction_mat = node_path.getMat() new_direction = direction_mat.xformVec(Vec3(0,1,0)) light_object.direction = new_direction print(f"光源旋转已更新:{axis}={value}") def _updateLightEnergy(self,light_object,value): """更新光源强度""" light_object.energy = value def _updateLightRadius(self,light_object,value): """更新光源半径""" light_object.radius = value def _updateLightFOV(self,light_Object,value): """更新聚光灯视野角度""" if hasattr(light_Object,'fov'): light_Object.fov = value def _updateLightTemperature(self,light_object,value): """更新光源色温""" light_object.set_color_from_temperature(value) #保存色温值以便下次显示 light_object._temperature=value def _updateLightInnerRadius(self,light_object,value): """更新点光源内半径""" if hasattr(light_object,'inner_radius'): light_object.inner_radius=value def _updateLightShaowResolution(self,light_object,value): """更新阴影分辨率""" light_object.shadow_map_resolution = value def _updateLightNearPlane(self,light_object,value): """更新近平面距离""" light_object.near_plane = value def _updateLightCastsShadows(self,light_object,casts_shadows): """更新光源是否投射阴影""" light_object.casts_shadows = casts_shadows def _updateLightScale(self,node_path,axis,value): """更新光源节点缩放""" current_scale = node_path.getScale() if axis=='x': new_scale = Vec3(value,current_scale.getY(),current_scale.getZ()) elif axis=='y': new_scale = Vec3(current_scale.getX(),value,current_scale.getZ()) else: new_scale = Vec3(current_scale.getX(),current_scale.getY(),value) node_path.setScale(new_scale) def _generateUniqueMaterialNames(self, materials, model_name): """生成唯一的材质名称,避免重复""" material_names = {} unique_names = [] for i, material in enumerate(materials): # 获取材质的原始名称 base_name = material.get_name() if hasattr(material, 'get_name') and material.get_name() else f"材质{i + 1}" full_name = f"{base_name}:{model_name}" # 检查是否重复 if full_name in material_names: # 如果重复,增加计数器 material_names[full_name] += 1 unique_name = f"{full_name}_{material_names[full_name]}" else: # 首次出现,记录并使用原名 material_names[full_name] = 0 unique_name = full_name unique_names.append(unique_name) return unique_names def _updateModelMaterialPanel(self,model): """模型材质属性""" if model.is_empty(): print("警告: 无法在空的 NodePath 上查找材质") no_material_group = QGroupBox("材质信息") no_material_layout = QGridLayout() no_material_label = QLabel("无材质") no_material_label.setStyleSheet("color: gray;font-style:italic;") no_material_layout.addWidget(no_material_label, 0, 0) no_material_group.setLayout(no_material_layout) self._propertyLayout.addWidget(no_material_group) return materials = model.find_all_materials() if not materials: no_material_group = QGroupBox("材质信息") no_material_layout = QGridLayout() no_material_label = QLabel("无材质") no_material_label.setStyleSheet("color: gray;font-style:italic;") no_material_layout.addWidget(no_material_label, 0, 0) no_material_group.setLayout(no_material_layout) self._propertyLayout.addWidget(no_material_group) return model_name = model.getName() or "未命名模型" name_counter = {} # 创建材质到几何节点的映射字典 self._material_geom_mapping = {} self._material_display_names = {} for i, material in enumerate(materials): # 查找使用该材质的几何节点,使用几何节点名称作为材质标题 geom_node = self._findSpecificGeomNodeWithMaterial(model, material) if geom_node: geom_node_name = geom_node.getName() unique_name = f"{geom_node_name}({model_name})" print(f"材质 {i}: 使用几何节点名称 '{geom_node_name}'") else: material_name = material.get_name() if hasattr(material, 'get_name') and material.get_name() else f"材质{i + 1}" unique_name = f"{material_name}({model_name})" print(f"材质 {i}: 未找到几何节点,使用材质名称 '{material_name}'") # 处理重复名称 if unique_name in name_counter: name_counter[unique_name] += 1 display_name = f"{unique_name}_{name_counter[unique_name]}" else: name_counter[unique_name] = 1 display_name = unique_name # 存储材质和对应的几何节点信息到映射字典中 material_id = id(material) self._material_geom_mapping[material_id] = geom_node self._material_display_names[material_id] = display_name # 材质信息组 # material_group = QGroupBox(display_name) material_group = QGroupBox("材质属性") material_layout = QGridLayout() material_layout.addWidget(QLabel("名称:"), 0, 0) name_label = QLabel(display_name) # name_label.setStyleSheet("color: #FF6B6B; font-weight: bold;") material_layout.addWidget(name_label, 0, 1, 1, 3) # 检查材质类型并显示状态 material_status = self._getMaterialStatus(material) if material_status != "标准PBR材质": material_layout.addWidget(QLabel("状态:"), 1, 0) status_label = QLabel(material_status) # status_label.setStyleSheet("color:#FFA500;font-style:italic;font-size:10px;") material_layout.addWidget(status_label, 1, 1, 1, 2) # 基础颜色编辑 base_color = self._getOrCreateMaterialBaseColor(material) if base_color is not None: print(f"材质基础颜色: {base_color}") # 基础颜色标题 color_row = 2 if material_status != "标准PBR材质" else 1 material_layout.addWidget(QLabel("基础颜色"), color_row, 0) # R, G, B 标签 r_label = QLabel("R") g_label = QLabel("G") b_label = QLabel("B") r_label.setAlignment(Qt.AlignCenter) g_label.setAlignment(Qt.AlignCenter) b_label.setAlignment(Qt.AlignCenter) material_layout.addWidget(r_label, color_row, 1) material_layout.addWidget(g_label, color_row, 2) material_layout.addWidget(b_label, color_row, 3) # RGB 数值框 r_spinbox = QDoubleSpinBox() r_spinbox.setRange(0.0, 1.0) r_spinbox.setSingleStep(0.01) r_spinbox.setValue(base_color.x) r_spinbox.valueChanged.connect(lambda v, mat=material: self._updateMaterialBaseColor(mat, 'r', v)) material_layout.addWidget(r_spinbox, color_row + 1, 1) g_spinbox = QDoubleSpinBox() g_spinbox.setRange(0.0, 1.0) g_spinbox.setSingleStep(0.01) g_spinbox.setValue(base_color.y) g_spinbox.valueChanged.connect(lambda v, mat=material: self._updateMaterialBaseColor(mat, 'g', v)) material_layout.addWidget(g_spinbox, color_row + 1, 2) b_spinbox = QDoubleSpinBox() b_spinbox.setRange(0.0, 1.0) b_spinbox.setSingleStep(0.01) b_spinbox.setValue(base_color.z) b_spinbox.valueChanged.connect(lambda v, mat=material: self._updateMaterialBaseColor(mat, 'b', v)) material_layout.addWidget(b_spinbox, color_row + 1, 3) else: no_base_color_label = QLabel("无法获取材质基础颜色") no_base_color_label.setStyleSheet("color:#888;font-style:italic;font-size:10px;") material_layout.addWidget(QLabel("基础颜色:"), 1, 0) material_layout.addWidget(no_base_color_label, 1, 1, 1, 3) # 材质属性行 current_row = color_row + 2 if base_color is not None else 2 # 粗糙度 if hasattr(material, 'roughness') and material.roughness is not None: try: roughness_value = float(material.roughness) material_layout.addWidget(QLabel("粗糙度:"), current_row, 0) roughness_spinbox = QDoubleSpinBox() roughness_spinbox.setRange(0.0, 1.0) roughness_spinbox.setSingleStep(0.01) roughness_spinbox.setValue(roughness_value) roughness_spinbox.valueChanged.connect( lambda v, mat=material: self._updateMaterialRoughness(mat, v)) material_layout.addWidget(roughness_spinbox, current_row, 1) except (TypeError, ValueError) as e: print(f"粗糙度值无效: {material.roughness}, 错误: {e}") material_layout.addWidget(QLabel("粗糙度:"), current_row, 0) no_roughness_label = QLabel("粗糙度值无效") no_roughness_label.setStyleSheet("color:#888;font-style:italic;font-size:10px;") material_layout.addWidget(no_roughness_label, current_row, 1) else: material_layout.addWidget(QLabel("粗糙度:"), current_row, 0) no_roughness_label = QLabel("不支持") no_roughness_label.setStyleSheet("color:#888;font-style:italic;font-size:10px;") material_layout.addWidget(no_roughness_label, current_row, 1) current_row += 1 # 金属性 if hasattr(material, 'metallic') and material.metallic is not None: try: metallic_value = float(material.metallic) material_layout.addWidget(QLabel("金属性:"), current_row, 0) metallic_spinbox = QDoubleSpinBox() metallic_spinbox.setRange(0.0, 1.0) metallic_spinbox.setSingleStep(0.01) metallic_spinbox.setValue(metallic_value) metallic_spinbox.valueChanged.connect(lambda v, mat=material: self._updateMaterialMetallic(mat, v)) material_layout.addWidget(metallic_spinbox, current_row, 1) except (TypeError, ValueError) as e: print(f"金属性值无效: {material.metallic}, 错误: {e}") material_layout.addWidget(QLabel("金属性:"), current_row, 0) no_metallic_label = QLabel("值无效") no_metallic_label.setStyleSheet("color:#888;font-style:italic;font-size:10px;") material_layout.addWidget(no_metallic_label, current_row, 1) else: material_layout.addWidget(QLabel("金属性:"), current_row, 0) no_metallic_label = QLabel("不支持") no_metallic_label.setStyleSheet("color:#888;font-style:italic;font-size:10px;") material_layout.addWidget(no_metallic_label, current_row, 1) current_row += 1 # 折射率 if hasattr(material, 'refractive_index') and material.refractive_index is not None: try: ior_value = float(material.refractive_index) material_layout.addWidget(QLabel("折射率:"), current_row, 0) ior_spinbox = QDoubleSpinBox() ior_spinbox.setRange(1.0, 3.0) ior_spinbox.setSingleStep(0.01) ior_spinbox.setValue(ior_value) ior_spinbox.valueChanged.connect(lambda v, mat=material: self._updateMaterialIOR(mat, v)) material_layout.addWidget(ior_spinbox, current_row, 1) except (TypeError, ValueError) as e: print(f"折射率值无效: {material.refractive_index}, 错误: {e}") material_layout.addWidget(QLabel("折射率:"), current_row, 0) no_ior_label = QLabel("折射率值无效") no_ior_label.setStyleSheet("color:#888;font-style:italic;font-size:10px;") material_layout.addWidget(no_ior_label, current_row, 1) else: material_layout.addWidget(QLabel("折射率:"), current_row, 0) no_ior_label = QLabel("此材质不支持折射率编辑") no_ior_label.setStyleSheet("color:#888;font-style:italic;font-size:10px;") material_layout.addWidget(no_ior_label, current_row, 1) current_row += 1 # 纹理贴图标题 texture_title = QLabel("纹理贴图") texture_title.setStyleSheet("font-weight:bold;") material_layout.addWidget(texture_title, current_row, 0, 1, 4) current_row += 1 # 纹理按钮 - 两列布局 diffuse_button = QPushButton("选择漫反射贴图") diffuse_button.clicked.connect(lambda checked, title=unique_name: self._selectDiffuseTexture(title)) material_layout.addWidget(diffuse_button, current_row, 0, 1, 2) normal_button = QPushButton("选择法线贴图") normal_button.clicked.connect(lambda checked, mat=material: self._selectNormalTexture(mat)) material_layout.addWidget(normal_button, current_row, 2, 1, 2) current_row += 1 roughness_button = QPushButton("选择粗糙度贴图") roughness_button.clicked.connect(lambda checked, mat=material: self._selectRoughnessTexture(mat)) material_layout.addWidget(roughness_button, current_row, 0, 1, 2) metallic_button = QPushButton("选择金属性贴图") metallic_button.clicked.connect(lambda checked, mat=material: self._selectMetallicTexture(mat)) material_layout.addWidget(metallic_button, current_row, 2, 1, 2) # #IOR贴图 # ior_button = QPushButton("选择IOR贴图") # ior_button.clicked.connect(lambda checked,mat = material:self._selectIORTexture(mat)) # self._propertyLayout.addRow("IOR贴图",ior_button) # # 视差贴图 # parallax_button = QPushButton("选择视差贴图") # parallax_button.clicked.connect(lambda checked, mat=material: self._selectParallaxTexture(mat)) # self._propertyLayout.addRow("视差贴图:", parallax_button) # # # 自发光贴图 # emission_button = QPushButton("选择自发光贴图") # emission_button.clicked.connect(lambda checked, mat=material: self._selectEmissionTexture(mat)) # self._propertyLayout.addRow("自发光贴图:", emission_button) # # # 环境光遮蔽贴图 # ao_button = QPushButton("选择AO贴图") # ao_button.clicked.connect(lambda checked, mat=material: self._selectAOTexture(mat)) # self._propertyLayout.addRow("AO贴图:", ao_button) # # 透明度贴图 # alpha_button = QPushButton("选择透明度贴图") # alpha_button.clicked.connect(lambda checked, mat=material: self._selectAlphaTexture(mat)) # self._propertyLayout.addRow("透明度贴图:", alpha_button) # # # 细节贴图 # detail_button = QPushButton("选择细节贴图") # detail_button.clicked.connect(lambda checked, mat=material: self._selectDetailTexture(mat)) # self._propertyLayout.addRow("细节贴图:", detail_button) # # # 光泽贴图 # gloss_button = QPushButton("选择光泽贴图") # gloss_button.clicked.connect(lambda checked, mat=material: self._selectGlossTexture(mat)) # self._propertyLayout.addRow("光泽贴图:", gloss_button) # 在纹理按钮后添加当前贴图信息显示 current_row = self._displayCurrentTextures(material, material_layout, current_row) current_row = self._addShadingModelPanel(material, material_layout, current_row) current_row = self._addEmissionPanel(material, material_layout, current_row) current_row = self._addMaterialPresetPanel(material, material_layout, current_row) material_group.setLayout(material_layout) self._propertyLayout.addWidget(material_group) # # 添加太阳方位角控制面板(只在第一个材质时添加,避免重复) # # if i == 0: # # self._addSunAzimuthPanel() # # # # 分隔线 # if i < len(materials) - 1: # separator = QLabel("─" * 30) # separator.setStyleSheet("color: lightgray;") # self._propertyLayout.addRow(separator) def _updateMaterialBaseColor(self, material, component, value): """更新材质基础颜色(智能版本)""" try: from panda3d.core import Vec4 # 获取当前颜色 current_color = self._getOrCreateMaterialBaseColor(material) if current_color is None: print(f"无法获取材质基础颜色,跳过更新") return # 计算新颜色 if component == 'r': new_color = Vec4(value, current_color.y, current_color.z, current_color.w) elif component == 'g': new_color = Vec4(current_color.x, value, current_color.z, current_color.w) elif component == 'b': new_color = Vec4(current_color.x, current_color.y, value, current_color.w) # elif component == 'a': # Alpha分量处理 # self._updateMaterialTransparency(material, value) # return #new_color = Vec4(current_color.x, current_color.y, current_color.z, value) else: print(f"未知的颜色分量: {component}") return # 尝试多种方式设置颜色 success = False # 方法1: 使用set_base_color if hasattr(material, 'set_base_color'): try: material.set_base_color(new_color) print(f"✓ 通过set_base_color更新: {component}={value}") success = True except Exception as e: print(f"set_base_color失败: {e}") # 方法2: 使用setDiffuse作为备选 if not success and hasattr(material, 'setDiffuse'): try: material.setDiffuse(new_color) print(f"✓ 通过setDiffuse更新: {component}={value}") success = True except Exception as e: print(f"setDiffuse失败: {e}") # 方法3: 直接设置属性 if not success and hasattr(material, 'base_color'): try: material.base_color = new_color print(f"✓ 通过直接属性设置更新: {component}={value}") success = True except Exception as e: print(f"直接属性设置失败: {e}") if success: print(f"材质基础颜色已更新: {new_color}") else: print(f"✗ 所有更新方法都失败了") except Exception as e: print(f"更新材质基础颜色失败: {e}") def _updateMaterialTransparency(self,material,alpha_value): try: from panda3d.core import Vec4 if hasattr(material,'emission'): material.emission = Vec4(3,0,0,0) print("设置透明着色器模型") if hasattr(material,'shading_model_param0'): material.shading_model_param0 = alpha_value print(f"设置透明度参数{alpha_value}") if hasattr(material,'base_color'): current_color = material.base_color material.base_color = Vec4(current_color.x,current_color.y,current_color.z,alpha_value) print(f"更新基础颜色透明度{alpha_value}") print(f"材质透明度已更新:{alpha_value}") except Exception as e: print(f"更新材质透明度失败: {e}") def _updateMaterialRoughness(self, material, value): """更新材质粗糙度(安全版本)""" try: if not hasattr(material, 'roughness') or material.roughness is None: print(f"材质不支持粗糙度属性或值为None,跳过更新") return material.set_roughness(value) except Exception as e: print(f"更新材质粗糙度失败: {e}") def _updateMaterialMetallic(self, material, value): """更新材质金属性(安全版本)""" try: if not hasattr(material, 'metallic') or material.metallic is None: print(f"材质不支持金属性属性或值为None,跳过更新") return material.set_metallic(value) except Exception as e: print(f"更新材质金属性失败: {e}") def _updateMaterialIOR(self, material, value): """更新材质折射率(安全版本)""" try: if not hasattr(material, 'refractive_index') or material.refractive_index is None: print(f"材质不支持折射率属性或值为None,跳过更新") return material.set_refractive_index(value) except Exception as e: print(f"更新材质折射率失败: {e}") def _getMaterialStatus(self, material): """获取材质状态描述""" try: # 检查材质的各种属性 has_base_color = hasattr(material, 'has_base_color') and material.has_base_color() has_roughness = hasattr(material, 'has_roughness') and material.has_roughness() has_metallic = hasattr(material, 'has_metallic') and material.has_metallic() has_ior = hasattr(material, 'has_refractive_index') and material.has_refractive_index() # 检查基本属性是否存在 has_base_color_attr = hasattr(material, 'base_color') has_roughness_attr = hasattr(material, 'roughness') has_metallic_attr = hasattr(material, 'metallic') has_ior_attr = hasattr(material, 'refractive_index') if has_base_color and has_roughness and has_metallic and has_ior: return "标准PBR材质" elif has_base_color_attr and has_roughness_attr and has_metallic_attr: return "PBR材质(部分属性可用)" elif has_base_color_attr or has_roughness_attr or has_metallic_attr: return "基础材质(支持部分PBR属性)" else: return "传统材质(可转换为PBR)" except Exception as e: print(f"检查材质状态时出错: {e}") return "未知材质类型(可尝试编辑)" def _getTextureModeString(self, mode): """获取纹理模式的字符串表示""" from panda3d.core import TextureStage mode_map = { TextureStage.MModulate: "Modulate", TextureStage.MDecal: "Decal", TextureStage.MBlend: "Blend", TextureStage.MReplace: "Replace", TextureStage.MAdd: "Add", TextureStage.MCombine: "Combine", TextureStage.MBlendColorScale: "BlendColorScale", TextureStage.MModulateGlow: "ModulateGlow", TextureStage.MModulateGloss: "ModulateGloss", TextureStage.MNormal: "Normal", TextureStage.MNormalHeight: "NormalHeight", TextureStage.MGlow: "Glow", TextureStage.MGloss: "Gloss", TextureStage.MHeight: "Height", TextureStage.MSelector: "Selector", TextureStage.MNormalGloss: "NormalGloss" } return mode_map.get(mode, f"Unknown({mode})") def _checkAndAdjustMaterialProperty(self, material, property_name, current_value, texture_type): """检查并智能调整材质属性值""" if current_value <= 0.01: print(f"⚠️ 警告:材质{property_name}过低({current_value}),{texture_type}贴图可能无效果") print(f" RenderPipeline使用公式: 最终{property_name} = 材质{property_name} × 贴图值") print(f" 当前设置下,即使贴图为白色(1.0),最终效果也只有{current_value}") # 询问用户是否要自动调整(在实际应用中,这里可以弹出对话框) # 目前我们采用保守的自动调整策略 recommended_value = 0.8 # 推荐值 if property_name == "粗糙度": material.set_roughness(recommended_value) elif property_name == "金属性": material.set_metallic(recommended_value) print(f"✓ 已自动调整材质{property_name}为 {recommended_value}") print(f" 现在{texture_type}贴图的效果范围:白色区域={recommended_value},黑色区域=0.0") return recommended_value else: print(f"✓ 材质{property_name}合适: {current_value}") print(f" {texture_type}贴图效果范围:白色区域={current_value:.2f},黑色区域=0.0") return current_value def _getOrCreateMaterialBaseColor(self, material): """智能获取或创建材质的基础颜色""" from panda3d.core import Vec4 try: # 方法1: 尝试获取base_color属性 if hasattr(material, 'base_color') and material.base_color is not None: print(f"✓ 找到base_color属性: {material.base_color}") return material.base_color # 方法2: 尝试调用get_base_color方法 if hasattr(material, 'get_base_color'): try: base_color = material.get_base_color() if base_color is not None: #print(f"✓ 通过get_base_color()获取: {base_color}") return base_color except: pass # 方法3: 尝试从diffuse颜色获取 if hasattr(material, 'getDiffuse'): try: diffuse_color = material.getDiffuse() if diffuse_color is not None: print(f"✓ 从diffuse颜色获取: {diffuse_color}") # 同时设置为base_color if hasattr(material, 'set_base_color'): material.set_base_color(diffuse_color) return diffuse_color except: pass # 方法4: 尝试从ambient颜色获取 if hasattr(material, 'getAmbient'): try: ambient_color = material.getAmbient() if ambient_color is not None: print(f"✓ 从ambient颜色获取: {ambient_color}") # 同时设置为base_color if hasattr(material, 'set_base_color'): material.set_base_color(ambient_color) return ambient_color except: pass # 方法5: 创建默认的基础颜色 print("⚠️ 未找到现有颜色,创建默认基础颜色") default_color = Vec4(0.8, 0.8, 0.8, 1.0) # 默认灰白色 # 尝试设置到材质 if hasattr(material, 'set_base_color'): material.set_base_color(default_color) print(f"✓ 设置默认base_color: {default_color}") elif hasattr(material, 'setDiffuse'): material.setDiffuse(default_color) print(f"✓ 设置默认diffuse: {default_color}") return default_color except Exception as e: print(f"✗ 获取材质基础颜色失败: {e}") return None def _selectDiffuseTexture(self,material_title): """漫反射贴图""" from PyQt5.QtWidgets import QFileDialog import os file_dialog = QFileDialog(None,"选择漫反射贴图","","图像文件(*.png *.jpg *.jpeg *.tga *.bmp)") if file_dialog.exec_(): filename = file_dialog.selectedFiles()[0] if filename: # 使用跨平台路径标准化 normalized_path = util.normalize_model_path(filename) self._applyDiffuseTexture(material_title,normalized_path) print(f"已选择漫反射贴图:{filename} -> 标准化路径:{normalized_path}") def _selectNormalTexture(self,material): """选择法线贴图""" from PyQt5.QtWidgets import QFileDialog file_dialog = QFileDialog(None,"选择法线贴图","","图像文件(*.png *.jpg *.jpeg *.tga *.bmp)") if file_dialog.exec_(): filename = file_dialog.selectedFiles()[0] if filename: # 使用跨平台路径标准化 normalized_path = util.normalize_model_path(filename) self._applyNormalTexture(material,normalized_path) print(f"已选择法线贴图:{filename} -> 标准化路径:{normalized_path}") def _selectRoughnessTexture(self,material): """选择粗糙度贴图""" from PyQt5.QtWidgets import QFileDialog file_dialog = QFileDialog(None,"选择粗糙度贴图","","图像文件(*.png *.jpg *.jpeg *.tga *.bmp)") if file_dialog.exec_(): filename = file_dialog.selectedFiles()[0] if filename: # 使用跨平台路径标准化 normalized_path = util.normalize_model_path(filename) self._applyRoughnessTexture_FINAL(material,normalized_path) print(f"已选择粗糙度贴图: {filename} -> 标准化路径:{normalized_path}") def _selectMetallicTexture(self,material): """选择金属性贴图""" from PyQt5.QtWidgets import QFileDialog file_dialog = QFileDialog(None,"选择金属性贴图","","图像文件(*.png *.jpg *.jpeg *.tga *.bmp)") if file_dialog.exec_(): filename = file_dialog.selectedFiles()[0] if filename: # 使用跨平台路径标准化 normalized_path = util.normalize_model_path(filename) self._applyMetallicTexture_NEW(material,normalized_path) print(f"已选择金属性贴图: {filename} -> 标准化路径:{normalized_path}") #IOR贴图 def _selectIORTexture(self,material): """选择IOR贴图""" from PyQt5.QtWidgets import QFileDialog file_dialong = QFileDialog(None,"选择IOR贴图","","图像(*.png *.jpg *.jpeg *.tga *.bmp)") if file_dialong.exec_(): filename = file_dialong.selectedFiles()[0] if filename: # 使用跨平台路径标准化 normalized_path = util.normalize_model_path(filename) self._applyIORTexture(material,normalized_path) print(f"已选择IOR贴图:{filename} -> 标准化路径:{normalized_path}") def _selectParallaxTexture(self,material): """选择视差贴图""" from PyQt5.QtWidgets import QFileDialog file_dialog = QFileDialog(None,"选择视差贴图","","图像文件(*.png *.jpg *.jpeg *.tga *.bmp)") if file_dialog.exec_(): filename = file_dialog.selectedFiles()[0] if filename: # 使用跨平台路径标准化 normalized_path = util.normalize_model_path(filename) self._applyParallaxTexture(material,normalized_path) print(f"已选择视差贴图:{filename} -> 标准化路径:{normalized_path}") def _selectEmissionTexture(self,material): """选择自发光贴图""" from PyQt5.QtWidgets import QFileDialog file_dialog = QFileDialog(None,"选择自发光贴图","","图像文件(*.png *.jpg *.jpeg *.tga *.bmp)") if file_dialog.exec_(): filename = file_dialog.selectedFiles()[0] if filename: # 使用跨平台路径标准化 normalized_path = util.normalize_model_path(filename) self._applyEmissionTexture(material,normalized_path) print(f"已选择自发光贴图:{filename} -> 标准化路径:{normalized_path}") def _selectAOTexture(self,material): """选择环境光遮蔽贴图""" from PyQt5.QtWidgets import QFileDialog file_dialog = QFileDialog(None,"选择AO贴图","","图像文件(*.png *.jpg *.jpeg *.tga *.bmp)") if file_dialog.exec_(): filename = file_dialog.selectedFiles()[0] if filename: # 使用跨平台路径标准化 normalized_path = util.normalize_model_path(filename) self._applyAOTexture(material,normalized_path) print(f"已选择AO贴图:{filename} -> 标准化路径:{normalized_path}") def _selectAlphaTexture(self,material): """选择透明度贴图""" from PyQt5.QtWidgets import QFileDialog file_dialog = QFileDialog(None,"选择透明度贴图","","图像文件(*.png *.jpg *.jpeg *.tga *.bmp)") if file_dialog.exec_(): filename = file_dialog.selectedFiles()[0] if filename: # 使用跨平台路径标准化 normalized_path = util.normalize_model_path(filename) self._applyAlphaTexture(material,normalized_path) print(f"已选择透明度贴图:{filename} -> 标准化路径:{normalized_path}") def _selectDetailTexture(self,material): """选择细节贴图""" from PyQt5.QtWidgets import QFileDialog file_dialog = QFileDialog(None,"选择细节贴图","","图像文件(*.png *.jpg *.jpeg *.tga *.bmp)") if file_dialog.exec_(): filename = file_dialog.selectedFiles()[0] if filename: # 使用跨平台路径标准化 normalized_path = util.normalize_model_path(filename) self._applyDetailTexture(material,normalized_path) print(f"已选择细节贴图:{filename} -> 标准化路径:{normalized_path}") def _selectGlossTexture(self,material): """选择光泽贴图""" from PyQt5.QtWidgets import QFileDialog file_dialog = QFileDialog(None,"选择光泽贴图","","图像文件(*.png *.jpg *.jpeg *.tga *.bmp)") if file_dialog.exec_(): filename = file_dialog.selectedFiles()[0] if filename: # 使用跨平台路径标准化 normalized_path = util.normalize_model_path(filename) self._applyGlossTexture(material,normalized_path) print(f"已选择光泽贴图:{filename} -> 标准化路径:{normalized_path}") # def _applyDiffuseTexture(self, texture_path): # from panda3d.core import TextureStage # try: # from RenderPipelineFile.rpcore.loader import RPLoader # texture = RPLoader.load_texture(texture_path) # if not texture: # print("纹理加载失败") # return # # node = self.world.selected_np # if not node: # print("未选中节点") # return # # # 1. 直接给节点挂贴图 # diffuse_stage = TextureStage("diffuse") # diffuse_stage.setSort(0) # node.setTexture(diffuse_stage, texture) # # # 2. 再给它刷一个 RenderPipeline 效果 # effect_file = "effects/default.yaml" # self.world.render_pipeline.set_effect( # node, # effect_file, # {"diffuse_texture": texture}, # 100 # ) # print("贴图已直接贴到节点:", node.getName()) # except Exception as e: # print("贴图失败:", e) def _applyDiffuseTexture(self,material_title,texture_path): """应用漫反射贴图""" try: from RenderPipelineFile.rpcore.loader import RPLoader from panda3d.core import TextureStage #加载纹理 texture = RPLoader.load_texture(texture_path) if texture: #获取材质所属的节点 material,node = self._findMaterialAndNodeByTitle(material_title) if node and material: print(f"正在为节点 {node.getName()} 应用漫反射贴图") # 检查是否有金属性贴图,选择合适的PBR效果 print("🔧 检查金属性贴图并选择合适的PBR效果...") has_metallic = self._hasMetallicTexture(node) needs_alpha = self._needsAlphaTesting(node) if has_metallic: print("✅ 检测到金属性贴图,使用支持金属性的PBR效果") effect_file = "effects/pbr_with_metallic.yaml" else: print("✅ 没有金属性贴图,使用默认PBR效果") effect_file = "effects/default.yaml" print(f"🔍 透明度测试: {'需要' if needs_alpha else '不需要'}") try: self.world.render_pipeline.set_effect( node, effect_file, { "normal_mapping": True, # 启用法线映射支持 "render_gbuffer": True, "alpha_testing": needs_alpha, # 根据是否需要透明度决定 "parallax_mapping": False, "render_shadow": True, "render_envmap": True }, 100 ) print(f"✅ {effect_file} 效果已应用(支持透明度和金属性)") except Exception as e: print(f"⚠️ PBR效果应用失败: {e}") # 根据RenderPipeline的gbuffer.frag.glsl模板: # p3d_Texture0 用于漫反射贴图 (line 111: texture(p3d_Texture0, texcoord).xyz) # 清理可能存在的漫反射贴图 existing_stages = node.findAllTextureStages() for stage in existing_stages: if stage.getSort() == 0 or "diffuse" in stage.getName().lower(): node.clearTexture(stage) print(f"清理了现有的漫反射贴图阶段: {stage.getName()}") # 创建漫反射贴图纹理阶段,对应p3d_Texture0 diffuse_stage = TextureStage("diffuse") diffuse_stage.setSort(0) # 对应p3d_Texture0 diffuse_stage.setMode(TextureStage.MModulate) # 标准的调制模式 # 应用漫反射贴图 node.setTexture(diffuse_stage, texture) print("漫反射贴图已应用到p3d_Texture0槽") # 调试信息:显示当前纹理阶段 print("=== 漫反射贴图应用后的纹理阶段信息 ===") all_stages = node.findAllTextureStages() for i, stage in enumerate(all_stages): tex = node.getTexture(stage) mode_name = self._getTextureModeString(stage.getMode()) print(f"阶段 {i}: {stage.getName()}, Sort: {stage.getSort()}, 模式: {mode_name}, 纹理: {tex.getName() if tex else 'None'}") print("==========================================") print(f"漫反射贴图已成功应用:{texture_path}") else: print(f"未找到材质标题对应的材质或节点: {material_title}") else: print("纹理加载失败") except Exception as e: print(f"应用漫反射贴图失败{e}") import traceback traceback.print_exc() def _applyNormalTexture(self, material, texture_path): """应用法线贴图 - Blender风格效果""" try: from RenderPipelineFile.rpcore.loader import RPLoader from panda3d.core import TextureStage, Vec4 texture = RPLoader.load_texture(texture_path) if not texture: print("❌ 纹理加载失败") return # 查找使用该材质的具体几何节点 node = self._findSpecificGeomNodeForMaterial(material) if not node: print("❌ 未找到材质对应的节点") return # 清理现有的法线贴图,避免冲突 print("🧹 清理现有纹理阶段...") existing_stages = node.findAllTextureStages() has_diffuse_texture = False for stage in existing_stages: if "normal" in stage.getName().lower() or stage.getSort() == 1: node.clearTexture(stage) print(f" ✅ 已清理法线纹理阶段: {stage.getName()}") elif stage.getSort() == 0: # 检查是否有漫反射贴图 tex = node.getTexture(stage) if tex: has_diffuse_texture = True print(f" 🔍 发现现有漫反射贴图: {tex.getName()}") # 如果没有漫反射贴图,必须创建白色纹理,否则法线映射会失效 if not has_diffuse_texture: print("⚠️ 没有漫反射贴图,创建白色纹理确保法线映射正常工作...") self._createWhiteDiffuseTexture(node) # 检查是否有金属性贴图,选择合适的PBR效果 print("🔧 检查金属性贴图并选择合适的PBR效果...") has_metallic = self._hasMetallicTexture(node) needs_alpha = self._needsAlphaTesting(node) if has_metallic: print("✅ 检测到金属性贴图,使用支持金属性的PBR效果") effect_file = "effects/pbr_with_metallic.yaml" else: print("✅ 没有金属性贴图,使用默认PBR效果") effect_file = "effects/default.yaml" print(f"🔍 透明度测试: {'需要' if needs_alpha else '不需要'}") try: self.world.render_pipeline.set_effect( node, effect_file, { "normal_mapping": True, # 强制启用法线映射 "render_gbuffer": True, "alpha_testing": needs_alpha, # 根据是否需要透明度决定 "parallax_mapping": False, "render_shadow": True, "render_envmap": True }, 100 ) print(f"✅ {effect_file} 效果已应用(法线映射已启用)") except Exception as e: print(f"⚠️ PBR效果应用失败: {e}") # 这里不需要检查roughness_stage,因为这是法线贴图方法 print("🔍 继续法线贴图处理...") # 创建法线贴图纹理阶段,对应p3d_Texture1 print("🎨 创建法线纹理阶段...") normal_stage = TextureStage("normal_map") normal_stage.setSort(1) # 对应shader中的p3d_Texture1 normal_stage.setMode(TextureStage.MModulate) # 使用标准模式,不是MNormal normal_stage.setTexcoordName("texcoord") print(f"📋 法线纹理阶段信息:") print(f" • 名称: {normal_stage.getName()}") print(f" • 排序: {normal_stage.getSort()} (对应p3d_Texture1)") print(f" • 模式: {normal_stage.getMode()} (MModulate)") # 应用纹理到正确的纹理槽 node.setTexture(normal_stage, texture) print(f"🔗 法线纹理已绑定到p3d_Texture1槽") # 设置材质的normalfactor参数(用于法线强度) current_emission = material.emission if current_emission is None: current_emission = Vec4(0, 0, 0, 0) print("材质emission为None,使用默认值") # emission.y 用于存储 normalfactor new_emission = Vec4(current_emission.x, 1.0, current_emission.z, current_emission.w) material.set_emission(new_emission) print(f"🔧 设置法线强度参数: normalfactor = {new_emission.y}") # 验证纹理应用 applied_texture = node.getTexture(normal_stage) if applied_texture: print(f"✅ 法线贴图成功应用到p3d_Texture1槽") print(f"📊 纹理信息:") print(f" • 纹理名称: {applied_texture.getName()}") print(f" • 纹理尺寸: {applied_texture.getXSize()}x{applied_texture.getYSize()}") print(f"📊 Blender风格效果:") print(f" • 法线贴图将影响表面细节和光照") print(f" • 不会改变材质颜色,只影响表面法线") else: print("❌ 纹理应用验证失败") except Exception as e: print(f"❌ 应用法线贴图失败: {e}") import traceback traceback.print_exc() def _applyRoughnessTexture_FINAL(self, material, texture_path): """应用粗糙度贴图 - 先编译后绑定策略""" try: from RenderPipelineFile.rpcore.loader import RPLoader from panda3d.core import TextureStage import time print(f"🎨 应用粗糙度贴图(先编译后绑定): {texture_path}") # 1. 加载纹理 texture = RPLoader.load_texture(texture_path) if not texture: print("❌ 纹理加载失败") return # 2. 找到节点 node = self._findSpecificGeomNodeForMaterial(material) if not node: print("❌ 未找到材质对应的节点") return print(f"🎯 目标节点: {node.getName()}") # 3. 设置材质粗糙度为1.0 material.set_roughness(1.0) print("🔧 材质粗糙度设置为1.0") # 4. 检查该材质有没有应用法线贴图,如果没有就先添加一个默认的法线贴图 print("🔧 步骤1:检查法线贴图...") has_normal = self._hasNormalTexture(node) if not has_normal: print("⚠️ 检测到材质没有法线贴图,先添加默认法线贴图...") #self._applyDefaultNormalTexture(node) self._applyNormalTexture(material,"RenderPipelineFile/Default_NRM_2K.png") print("✅ 默认法线贴图已添加") else: print("✅ 检测到材质已有法线贴图") # 5. 检查是否有金属性贴图和透明漫反射贴图,选择合适的PBR效果 print("🔧 步骤2:检查金属性贴图和透明度设置...") has_metallic = self._hasMetallicTexture(node) needs_alpha = self._needsAlphaTesting(node) if has_metallic: print("✅ 检测到金属性贴图,使用支持金属性的PBR效果") effect_file = "effects/pbr_with_metallic.yaml" else: print("✅ 没有金属性贴图,使用默认PBR效果") effect_file = "effects/default.yaml" print(f"🔍 透明度测试: {'需要' if needs_alpha else '不需要'}") self.world.render_pipeline.set_effect( node, effect_file, { "normal_mapping": True, # 始终启用法线映射 "render_gbuffer": True, "alpha_testing": needs_alpha, # 根据是否需要透明度决定 "parallax_mapping": False, "render_shadow": True, "render_envmap": True }, 100 ) print(f"✅ {effect_file} 效果已应用") #print("✅ 着色器预编译完成") # 5. 等待编译完成 #time.sleep(0.2) # 200ms等待 #print("⏱️ 等待着色器编译...") # 6. 现在绑定纹理到已编译的着色器 #print("🔧 步骤2:绑定纹理到编译完成的着色器...") # roughness_stage = TextureStage("roughness_map") # roughness_stage.setSort(3) # p3d_Texture3 # roughness_stage.setMode(TextureStage.MModulate) # node.setTexture(roughness_stage, texture) #print("✅ 纹理已绑定到预编译着色器") print("🧹 清理现有粗糙度贴图...") existing_stages = node.findAllTextureStages() for stage in existing_stages: if stage.getSort() == 3: # p3d_Texture3槽 node.clearTexture(stage) print(f" ✅ 已清理现有粗糙度贴图: {stage.getName()}") # 添加验证和重试 for attempt in range(2): # 最多重试3次 time.sleep(0.1) # 短暂等待 roughness_stage = TextureStage("roughness_map") roughness_stage.setSort(3) # p3d_Texture3 roughness_stage.setMode(TextureStage.MModulate) node.setTexture(roughness_stage, texture) # 7. 验证效果 applied_texture = node.getTexture(roughness_stage) if applied_texture: print(f"✅ 粗糙度贴图应用成功(先编译后绑定)") print(f" 纹理: {applied_texture.getName()}") print("🔍 应该立即看到正确的粗糙度效果") else: print("❌ 纹理绑定验证失败") except Exception as e: print(f"❌ 应用粗糙度贴图失败: {e}") import traceback traceback.print_exc() def _hasNormalTexture(self, node): """检查节点是否有法线贴图""" try: all_stages = node.findAllTextureStages() for stage in all_stages: if stage.getSort() == 1: # p3d_Texture1 是法线贴图槽 tex = node.getTexture(stage) if tex: print(f"🔍 发现法线贴图: {tex.getName()}") return True return False except Exception as e: print(f"⚠️ 检查法线贴图失败: {e}") return False def _applySmartPBREffect(self, node, effect_file="effects/default.yaml"): """智能应用PBR效果,自动检测是否需要启用法线映射""" try: # 检查是否有法线贴图 has_normal = self._hasNormalTexture(node) print(f"🔧 应用智能PBR效果 ({effect_file})...") print(f" 法线映射: {'启用' if has_normal else '禁用'}") self.world.render_pipeline.set_effect( node, effect_file, { "normal_mapping": has_normal, # 根据是否有法线贴图决定 "render_gbuffer": True, "alpha_testing": False, "parallax_mapping": False, "render_shadow": True, "render_envmap": True }, 100 ) print("✅ 智能PBR效果已应用") return True except Exception as e: print(f"⚠️ 智能PBR效果应用失败: {e}") return False def _verifyTextureInShader(self, node, texture_type, expected_sort): """验证纹理是否在shader中正确处理""" try: print(f"🔍 验证{texture_type}纹理在shader中的处理...") # 检查当前效果 current_effect = node.getEffect() if current_effect: print(f" 当前shader效果: {current_effect}") else: print(" ⚠️ 节点没有shader效果") return False # 检查纹理槽 stages = node.findAllTextureStages() target_stage = None for stage in stages: if stage.getSort() == expected_sort: target_stage = stage break if target_stage: tex = node.getTexture(target_stage) if tex: print(f" ✅ 找到{texture_type}纹理在sort={expected_sort}槽: {tex.getName()}") # 检查纹理数据 if tex.hasRamImage(): print(f" ✅ 纹理有RAM图像数据") else: print(f" ⚠️ 纹理没有RAM图像数据") return True else: print(f" ❌ sort={expected_sort}槽没有纹理") else: print(f" ❌ 没有找到sort={expected_sort}的纹理阶段") return False except Exception as e: print(f" ❌ 验证失败: {e}") return False def _ensureEnhancedPBREffect(self, node): """确保节点使用增强的PBR效果,支持金属性纹理""" try: print("🔧 应用金属性贴图支持的PBR效果...") self.world.render_pipeline.set_effect( node, "effects/pbr_with_metallic.yaml", { "normal_mapping": False, # 关闭法线贴图避免干扰 "render_gbuffer": True, # 必须启用gbuffer渲染 "alpha_testing": False, "parallax_mapping": False, "render_shadow": True, "render_envmap": True }, 100 # 高优先级确保应用 ) print("✅ 金属性贴图PBR效果已应用") return True except Exception as e: print(f"⚠️ 金属性贴图PBR效果失败: {e}") # 回退到默认效果 try: self.world.render_pipeline.set_effect( node, "effects/default.yaml", { "normal_mapping": False, "render_gbuffer": True, "alpha_testing": False, "parallax_mapping": False, "render_shadow": True, "render_envmap": True }, 100 ) print("✅ 已回退到默认PBR效果(不支持金属性贴图)") return True except Exception as e2: print(f"⚠️ 默认效果也失败: {e2}") return False def _createWhiteDiffuseTexture(self, node): """创建白色漫反射纹理,确保法线贴图能正常工作""" try: from RenderPipelineFile.rpcore.loader import RPLoader from panda3d.core import TextureStage print("🎨 尝试加载白色纹理...") # 尝试加载一个白色纹理文件,如果不存在就创建 white_texture = None # 方法1:尝试从RenderPipeline加载空白基础颜色纹理(应该是白色) try: white_texture = RPLoader.load_texture("RenderPipelineFile/data/empty_textures/empty_basecolor.png") if white_texture: print("✅ 从RenderPipeline空白纹理加载白色纹理") except Exception as e1: print(f"⚠️ 加载空白纹理失败: {e1}") pass # 方法2:如果没有默认纹理,创建简单的白色纹理 if not white_texture: try: from panda3d.core import Texture white_texture = Texture("white_diffuse") white_texture.setup2dTexture(1, 1, Texture.TUnsignedByte, Texture.FRgb) # 设置白色像素数据 white_data = b'\xff\xff\xff' white_texture.setRamImage(white_data) print("✅ 创建了程序生成的白色纹理") except Exception as e2: print(f"⚠️ 创建程序纹理也失败: {e2}") return False if white_texture: # 创建漫反射纹理阶段 diffuse_stage = TextureStage("white_diffuse") diffuse_stage.setSort(0) # 对应p3d_Texture0 diffuse_stage.setMode(TextureStage.MModulate) # 应用白色纹理 node.setTexture(diffuse_stage, white_texture) print("✅ 白色漫反射纹理已应用,材质颜色将正常显示") return True return False except Exception as e: print(f"⚠️ 创建白色纹理失败: {e}") return False def _applyMetallicTexture(self,material,texture_path): """应用金属性贴图 - Blender风格效果""" try: from RenderPipelineFile.rpcore.loader import RPLoader from panda3d.core import TextureStage print(f"🎨 应用金属性贴图: {texture_path}") texture = RPLoader.load_texture(texture_path) if not texture: print("❌ 纹理加载失败") return # 查找使用该材质的具体几何节点 node = self._findSpecificGeomNodeForMaterial(material) if not node: print("❌ 未找到材质对应的节点") return print(f"🎯 目标节点: {node.getName()}") # 保持材质金属性为1.0,让贴图完全控制 material.set_metallic(1.0) print("🔧 材质金属性设置为1.0,启用贴图完全控制") # 清理现有的金属性贴图,避免冲突 print("🧹 清理现有纹理阶段...") existing_stages = node.findAllTextureStages() print(f"🔍 发现 {len(existing_stages)} 个现有纹理阶段:") for stage in existing_stages: tex = node.getTexture(stage) tex_name = tex.getName() if tex else "无纹理" print(f" - {stage.getName()} (sort={stage.getSort()}) -> {tex_name}") # 只清理金属性相关的纹理阶段 if "metallic" in stage.getName().lower() or stage.getSort() == 5: node.clearTexture(stage) print(f" ✅ 已清理金属性纹理阶段: {stage.getName()}") # 确保没有纹理占用sort=5的槽位 sort5_stages = [s for s in existing_stages if s.getSort() == 5] if sort5_stages: print(f"⚠️ 发现占用sort=5槽位的纹理: {[s.getName() for s in sort5_stages]}") for stage in sort5_stages: node.clearTexture(stage) print(f" ✅ 已强制清理: {stage.getName()}") # 使用调试模式来验证金属性纹理是否正确读取 debug_texture_mode = True # 设为True来调试纹理读取 print(f"🔧 应用{'调试' if debug_texture_mode else '增强'}PBR效果...") try: if debug_texture_mode: # 使用调试效果:将金属性纹理显示为颜色 self.world.render_pipeline.set_effect( node, "effects/test_metallic_debug.yaml", { "normal_mapping": False, "render_gbuffer": True, "alpha_testing": False, "parallax_mapping": False, "render_shadow": True, "render_envmap": True }, 100 ) print("✅ 调试效果已应用 - 金属性纹理将显示为蓝黄色变化") print(" 蓝色=非金属区域, 黄色=金属区域") print(" 如果看到颜色变化,说明纹理读取正确") print(" 如果看到统一颜色,说明纹理没有正确绑定") else: # 使用专门的金属性效果,避免与漫反射贴图混淆 self.world.render_pipeline.set_effect( node, "effects/metallic_only.yaml", { "normal_mapping": False, # 关闭法线贴图避免干扰 "render_gbuffer": True, # 必须启用gbuffer渲染 "alpha_testing": False, "parallax_mapping": False, "render_shadow": True, "render_envmap": True }, 100 # 高优先级确保应用 ) print("✅ 专门的金属性贴图效果已应用") # 验证效果是否正确应用 current_effect = node.getEffect() if current_effect: print(f"🔍 当前效果: {current_effect}") else: print("⚠️ 效果应用后为空,可能有问题") except Exception as e: print(f"⚠️ 专门的金属性效果失败: {e}") print("🔄 回退到直接控制金属性效果...") # 回退到直接控制效果 try: self._ensurePBREffectEnabledWithDirectMetallic(node) print("✅ 直接控制金属性效果已应用") except Exception as e2: print(f"⚠️ 直接控制效果也失败: {e2}") # 最后的备选方案:使用稳定效果 try: self._ensurePBREffectEnabledStable(node) print("🔄 已应用稳定PBR效果") except: pass # 创建金属性贴图阶段,对应p3d_Texture5 print("🎨 创建金属性纹理阶段...") metallic_stage = TextureStage("metallic_map") metallic_stage.setSort(5) # 对应shader中的p3d_Texture5 # 重要:设置正确的纹理模式 # MModulate模式用于金属性贴图,不会影响颜色 metallic_stage.setMode(TextureStage.MModulate) # 确保纹理坐标正确 metallic_stage.setTexcoordName("texcoord") print(f"📋 金属性纹理阶段信息:") print(f" • 名称: {metallic_stage.getName()}") print(f" • 排序: {metallic_stage.getSort()} (对应p3d_Texture5)") print(f" • 模式: {metallic_stage.getMode()} (MReplace)") print(f" • 纹理坐标: {metallic_stage.getTexcoordName()}") # 应用纹理到正确的纹理槽 node.setTexture(metallic_stage, texture) print(f"🔗 金属性纹理已绑定到p3d_Texture5槽") # 强制刷新渲染状态 node.setRenderModeWireframe() node.clearRenderMode() print(f"🔄 已刷新渲染状态") # 验证纹理应用 applied_texture = node.getTexture(metallic_stage) if applied_texture: print(f"✅ 金属性贴图成功应用到p3d_Texture5槽") print(f"📊 纹理信息:") print(f" • 纹理名称: {applied_texture.getName()}") print(f" • 纹理尺寸: {applied_texture.getXSize()}x{applied_texture.getYSize()}") print(f"📊 Blender风格效果:") print(f" • 白色区域 = 完全金属 (1.0)") print(f" • 黑色区域 = 非金属 (0.0)") print(f" • 灰色区域 = 部分金属 (0.5)") print(f" • 公式: 最终金属性 = 贴图值") # 列出节点上的所有纹理阶段 print(f"🔍 节点上的所有纹理阶段:") all_stages = node.findAllTextureStages() for i, stage in enumerate(all_stages): tex = node.getTexture(stage) tex_name = tex.getName() if tex else "无纹理" print(f" {i}: {stage.getName()} (sort={stage.getSort()}) -> {tex_name}") else: print("❌ 纹理应用验证失败") print("🔍 尝试诊断问题...") # 检查纹理是否有效 if texture: print(f" 纹理对象有效: {texture.getName()}") else: print(" 纹理对象无效") # 检查节点是否有效 if node: print(f" 节点对象有效: {node.getName()}") else: print(" 节点对象无效") except Exception as e: print(f"❌ 应用金属性贴图失败: {e}") import traceback traceback.print_exc() def _applyIORTexture(self,material,texture_path): """应用IOR贴图到特定材质""" try: from RenderPipelineFile.rpcore.loader import RPLoader from panda3d.core import TextureStage texture = RPLoader.load_texture(texture_path) if texture: # 查找使用该材质的具体几何节点 node = self._findSpecificGeomNodeForMaterial(material) if node: print(f"正在为节点 {node.getName()} 应用IOR贴图") # 确保启用PBR效果 self._ensurePBREffectEnabled(node) # 根据RenderPipeline的gbuffer.frag.glsl模板: # p3d_Texture2 用于IOR贴图 (line 87: texture(p3d_Texture2, texcoord).x) # 清理现有的IOR贴图 existing_stages = node.findAllTextureStages() for stage in existing_stages: if "ior" in stage.getName().lower() or stage.getSort() == 2: node.clearTexture(stage) print(f"清理了现有的IOR贴图阶段: {stage.getName()}") # 创建IOR贴图纹理阶段,对应p3d_Texture2 ior_stage = TextureStage("ior") ior_stage.setSort(2) # 对应p3d_Texture2 ior_stage.setMode(TextureStage.MModulate) node.setTexture(ior_stage,texture) print("IOR贴图已应用到p3d_Texture2槽") # 不再需要手动刷新渲染状态,避免闪烁 print(f"IOR贴图已成功应用:{texture_path}") else: print("未找到材质对应的节点") except Exception as e: print(f"应用IOR贴图失败:{e}") import traceback traceback.print_exc() def _applyParallaxTexture(self,material,texture_path): """应用视差贴图""" try: from RenderPipelineFile.rpcore.loader import RPLoader from panda3d.core import TextureStage texture = RPLoader.load_texture(texture_path) if texture: node = self._findNodeWithMaterial(material) if node: print(f"正在为节点 {node.getName()} 应用视差贴图") # 确保启用PBR效果,包括视差映射 self._ensurePBREffectEnabledWithParallax(node) # 根据RenderPipeline的gbuffer.frag.glsl模板: # p3d_Texture4 用于视差贴图 (line 77: get_parallax_texcoord(p3d_Texture4, mInput.normalfactor)) # 清理现有的视差贴图 existing_stages = node.findAllTextureStages() for stage in existing_stages: if "parallax" in stage.getName().lower() or stage.getSort() == 4: node.clearTexture(stage) print(f"清理了现有的视差贴图阶段: {stage.getName()}") # 创建视差贴图纹理阶段,对应p3d_Texture4 parallax_stage = TextureStage("parallax") parallax_stage.setSort(4) # 对应p3d_Texture4 parallax_stage.setMode(TextureStage.MHeight) # 高度贴图模式 node.setTexture(parallax_stage,texture) print("视差贴图已应用到p3d_Texture4槽") print(f"视差贴图已成功应用:{texture_path}") else: print("未找到材质对应节点") except Exception as e: print(f"应用视差贴图失败:{e}") import traceback traceback.print_exc() def _ensureNormalMappingEnabled(self,model): """确保模型启用了法线映射功能""" try: self.world.render_pipeline.set_effect( model, "effects/default.yaml", { "normal_mapping":True, "render_gbuffer":True, "alpha_testing":True }, 30 ) print(f"已为模型{model.getName()}启用法线映射") except Exception as e: print(f"设置法线映射效果失败:{e}") def _ensurePBREffectEnabled(self, model): """确保模型启用了完整的PBR效果,包括法线映射""" try: self.world.render_pipeline.set_effect( model, "effects/default.yaml", { "normal_mapping": True, "render_gbuffer": True, "alpha_testing": True, "parallax_mapping": False, "render_shadow": True, "render_envmap": True }, 30 ) print(f"已为模型{model.getName()}启用PBR效果") except Exception as e: print(f"设置PBR效果失败:{e}") def _ensurePBREffectEnabledWithParallax(self, model): """确保模型启用了完整的PBR效果,包括视差映射""" try: self.world.render_pipeline.set_effect( model, "effects/default.yaml", { "normal_mapping": True, "render_gbuffer": True, "alpha_testing": True, "parallax_mapping": True, # 启用视差映射 "render_shadow": True, "render_envmap": True }, 30 ) print(f"已为模型{model.getName()}启用PBR效果(包括视差映射)") except Exception as e: print(f"设置PBR效果失败:{e}") def _ensurePBREffectEnabledWithMetallic(self, model): """确保模型启用了支持金属性贴图的PBR效果""" try: # 首先尝试使用自定义的金属性贴图效果 try: self.world.render_pipeline.set_effect( model, "effects/pbr_with_metallic.yaml", { "normal_mapping": True, "render_gbuffer": True, "alpha_testing": True, "parallax_mapping": False, "render_shadow": True, "render_envmap": True }, 30 ) print(f"已为模型{model.getName()}启用支持金属性贴图的PBR效果") except Exception as e1: print(f"自定义金属性效果失败,使用标准PBR效果: {e1}") # 回退到标准PBR效果 self._ensurePBREffectEnabled(model) except Exception as e: print(f"设置PBR效果失败:{e}") def _ensurePBREffectEnabledWithDirectMetallic(self, model): """确保模型启用了支持金属性贴图直接控制的PBR效果""" try: # 首先尝试使用直接控制金属性贴图的效果 try: self.world.render_pipeline.set_effect( model, "effects/pbr_direct_metallic.yaml", { "normal_mapping": True, "render_gbuffer": True, "alpha_testing": True, "parallax_mapping": False, "render_shadow": True, "render_envmap": True }, 30 ) print(f"✅ 已为模型{model.getName()}启用直接控制金属性贴图的PBR效果") except Exception as e1: print(f"⚠️ 直接控制金属性效果失败,使用标准金属性效果: {e1}") # 回退到标准金属性贴图效果 self._ensurePBREffectEnabledWithMetallic(model) except Exception as e: print(f"设置PBR效果失败:{e}") def _onMetallicModeChanged(self, material, mode_index): """处理金属性控制模式变化""" try: mode_names = ["乘法模式", "直接控制模式", "叠加模式"] print(f"🔧 金属性控制模式已切换为: {mode_names[mode_index]}") # 查找使用该材质的节点 node = self._findSpecificGeomNodeForMaterial(material) if node: if mode_index == 0: # 乘法模式:使用标准金属性贴图效果 self._ensurePBREffectEnabledWithMetallic(node) print(" 📊 效果:最终金属性 = 材质金属性 × 贴图值") print(" 💡 适用于:微调现有材质的金属性分布") elif mode_index == 1: # 直接控制模式:使用直接控制效果 self._ensurePBREffectEnabledWithDirectMetallic(node) print(" 📊 效果:最终金属性 = 贴图值") print(" 💡 适用于:贴图完全控制金属性分布") elif mode_index == 2: # 叠加模式:使用叠加效果 self._ensurePBREffectEnabledWithAdditiveMetallic(node) print(" 📊 效果:最终金属性 = 材质金属性 + 贴图值 (限制在0-1)") print(" 💡 适用于:在材质基础上增加金属性区域") print(f"✅ 金属性控制模式已应用到节点: {node.getName()}") else: print("⚠️ 未找到材质对应的节点") except Exception as e: print(f"切换金属性控制模式失败: {e}") def _ensurePBREffectEnabledWithAdditiveMetallic(self, model): """确保模型启用了支持金属性贴图叠加控制的PBR效果""" try: # 首先尝试使用叠加控制金属性贴图的效果 try: self.world.render_pipeline.set_effect( model, "effects/pbr_additive_metallic.yaml", { "normal_mapping": True, "render_gbuffer": True, "alpha_testing": True, "parallax_mapping": False, "render_shadow": True, "render_envmap": True }, 30 ) print(f"✅ 已为模型{model.getName()}启用叠加控制金属性贴图的PBR效果") except Exception as e1: print(f"⚠️ 叠加控制金属性效果失败,使用直接控制效果: {e1}") # 回退到直接控制金属性贴图效果 self._ensurePBREffectEnabledWithDirectMetallic(model) except Exception as e: print(f"设置PBR效果失败:{e}") def _ensurePBREffectEnabledWithEmission(self, model): """确保模型启用了支持自发光贴图的PBR效果""" try: self.world.render_pipeline.set_effect( model, "effects/pbr_with_emission.yaml", { "normal_mapping": True, "render_gbuffer": True, "alpha_testing": True, "parallax_mapping": False, "render_shadow": True, "render_envmap": True }, 30 ) print(f"已为模型{model.getName()}启用支持自发光贴图的PBR效果") except Exception as e: print(f"自定义自发光效果失败,使用标准PBR效果: {e}") # 回退到标准PBR效果 self._ensurePBREffectEnabled(model) def _ensurePBREffectEnabledWithAlpha(self, model): """确保模型启用了支持透明度的PBR效果""" try: self.world.render_pipeline.set_effect( model, "effects/default.yaml", { "normal_mapping": True, "render_gbuffer": False, # 透明物体不渲染到GBuffer "render_forward": True, # 使用前向渲染 "alpha_testing": True, "parallax_mapping": False, "render_shadow": True, "render_envmap": True }, 30 ) print(f"已为模型{model.getName()}启用支持透明度的PBR效果") except Exception as e: print(f"设置透明度PBR效果失败: {e}") # 回退到标准PBR效果 self._ensurePBREffectEnabled(model) def _ensurePBREffectEnabledWithRoughness(self, model): """确保模型启用了支持粗糙度贴图的PBR效果""" try: # 首先尝试使用自定义的粗糙度贴图效果 try: self.world.render_pipeline.set_effect( model, "effects/pbr_with_roughness.yaml", { "normal_mapping": True, "render_gbuffer": True, "alpha_testing": True, "parallax_mapping": False, "render_shadow": True, "render_envmap": True }, 30 ) print(f"已为模型{model.getName()}启用支持粗糙度贴图的PBR效果") except Exception as e1: print(f"自定义粗糙度效果失败,使用标准PBR效果: {e1}") # 回退到标准PBR效果 self._ensurePBREffectEnabled(model) except Exception as e: print(f"设置PBR效果失败:{e}") def _ensurePBREffectEnabledStable(self, model): """确保模型启用了稳定的PBR效果,避免频繁切换导致闪烁""" try: # 检查是否已经有PBR效果 current_effect = model.getEffect() if current_effect: print(f"🔍 当前效果: {current_effect}") # 如果已经有PBR相关效果,就不要切换了 effect_name = str(current_effect) if "pbr" in effect_name.lower() or "default" in effect_name.lower(): print("✅ 已有PBR效果,保持不变避免闪烁") return # 使用最稳定的默认PBR效果 print("🔧 应用稳定的默认PBR效果...") self.world.render_pipeline.set_effect( model, "effects/default.yaml", { "normal_mapping": True, "render_gbuffer": True, "alpha_testing": False, "parallax_mapping": False, "render_shadow": True, "render_envmap": True }, 50 # 较高优先级 ) print(f"✅ 稳定PBR效果已应用到模型: {model.getName()}") except Exception as e: print(f"⚠️ 设置稳定PBR效果失败: {e}") # 最后的备选方案:清除所有效果 try: model.clearEffect() print("🔄 已清除所有效果,使用默认渲染") except: pass def _applyEmissionTexture(self, material, texture_path): """应用自发光贴图""" try: from RenderPipelineFile.rpcore.loader import RPLoader from panda3d.core import TextureStage texture = RPLoader.load_texture(texture_path) if texture: node = self._findNodeWithMaterial(material) if node: print(f"正在为节点 {node.getName()} 应用自发光贴图") # 启用自发光效果 self._ensurePBREffectEnabledWithEmission(node) # 清理现有的自发光贴图 existing_stages = node.findAllTextureStages() for stage in existing_stages: if "emission" in stage.getName().lower() or stage.getSort() == 6: node.clearTexture(stage) print(f"清理了现有的自发光贴图阶段: {stage.getName()}") # 创建自发光贴图纹理阶段,对应p3d_Texture6 emission_stage = TextureStage("emission") emission_stage.setSort(6) # 对应p3d_Texture6 emission_stage.setMode(TextureStage.MModulate) node.setTexture(emission_stage, texture) print("自发光贴图已应用到p3d_Texture6槽") # 设置材质为自发光着色模型 from panda3d.core import Vec4 current_emission = material.emission if current_emission is None: current_emission = Vec4(0, 0, 0, 0) # emission.x 用于存储着色模型,1表示自发光 new_emission = Vec4(1.0, current_emission.y, current_emission.z, current_emission.w) material.set_emission(new_emission) print("材质着色模型已设置为自发光") print(f"自发光贴图已成功应用:{texture_path}") else: print("未找到材质对应的节点") except Exception as e: print(f"应用自发光贴图失败:{e}") import traceback traceback.print_exc() def _applyAOTexture(self, material, texture_path): """应用环境光遮蔽贴图""" try: from RenderPipelineFile.rpcore.loader import RPLoader from panda3d.core import TextureStage texture = RPLoader.load_texture(texture_path) if texture: node = self._findNodeWithMaterial(material) if node: print(f"正在为节点 {node.getName()} 应用AO贴图") # 确保启用PBR效果 self._ensurePBREffectEnabled(node) # 清理现有的AO贴图 existing_stages = node.findAllTextureStages() for stage in existing_stages: if "ao" in stage.getName().lower() or stage.getSort() == 7: node.clearTexture(stage) print(f"清理了现有的AO贴图阶段: {stage.getName()}") # 创建AO贴图纹理阶段,对应p3d_Texture7 ao_stage = TextureStage("ao") ao_stage.setSort(7) # 对应p3d_Texture7 ao_stage.setMode(TextureStage.MModulate) node.setTexture(ao_stage, texture) print("AO贴图已应用到p3d_Texture7槽") print("注意:AO贴图需要自定义shader支持才能正确显示") print(f"AO贴图已成功应用:{texture_path}") else: print("未找到材质对应的节点") except Exception as e: print(f"应用AO贴图失败:{e}") import traceback traceback.print_exc() def _applyAlphaTexture(self, material, texture_path): """应用透明度贴图""" try: from RenderPipelineFile.rpcore.loader import RPLoader from panda3d.core import TextureStage texture = RPLoader.load_texture(texture_path) if texture: node = self._findNodeWithMaterial(material) if node: print(f"正在为节点 {node.getName()} 应用透明度贴图") # 启用透明度测试的PBR效果 self._ensurePBREffectEnabledWithAlpha(node) # 清理现有的透明度贴图 existing_stages = node.findAllTextureStages() for stage in existing_stages: if "alpha" in stage.getName().lower() or stage.getSort() == 8: node.clearTexture(stage) print(f"清理了现有的透明度贴图阶段: {stage.getName()}") # 创建透明度贴图纹理阶段,对应p3d_Texture8 alpha_stage = TextureStage("alpha") alpha_stage.setSort(8) # 对应p3d_Texture8 alpha_stage.setMode(TextureStage.MModulate) node.setTexture(alpha_stage, texture) print("透明度贴图已应用到p3d_Texture8槽") # 设置材质为透明着色模型 from panda3d.core import Vec4 current_emission = material.emission if current_emission is None: current_emission = Vec4(0, 0, 0, 0) # emission.x 用于存储着色模型,3表示透明 new_emission = Vec4(3.0, current_emission.y, current_emission.z, current_emission.w) material.set_emission(new_emission) print("材质着色模型已设置为透明") print(f"透明度贴图已成功应用:{texture_path}") else: print("未找到材质对应的节点") except Exception as e: print(f"应用透明度贴图失败:{e}") import traceback traceback.print_exc() def _applyDetailTexture(self, material, texture_path): """应用细节贴图""" try: from RenderPipelineFile.rpcore.loader import RPLoader from panda3d.core import TextureStage texture = RPLoader.load_texture(texture_path) if texture: node = self._findNodeWithMaterial(material) if node: print(f"正在为节点 {node.getName()} 应用细节贴图") # 确保启用PBR效果 self._ensurePBREffectEnabled(node) # 清理现有的细节贴图 existing_stages = node.findAllTextureStages() for stage in existing_stages: if "detail" in stage.getName().lower() or stage.getSort() == 9: node.clearTexture(stage) print(f"清理了现有的细节贴图阶段: {stage.getName()}") # 创建细节贴图纹理阶段,对应p3d_Texture9 detail_stage = TextureStage("detail") detail_stage.setSort(9) # 对应p3d_Texture9 detail_stage.setMode(TextureStage.MModulate) node.setTexture(detail_stage, texture) print("细节贴图已应用到p3d_Texture9槽") print("注意:细节贴图需要自定义shader支持才能正确显示") print(f"细节贴图已成功应用:{texture_path}") else: print("未找到材质对应的节点") except Exception as e: print(f"应用细节贴图失败:{e}") import traceback traceback.print_exc() def _applyGlossTexture(self, material, texture_path): """应用光泽贴图""" try: from RenderPipelineFile.rpcore.loader import RPLoader from panda3d.core import TextureStage texture = RPLoader.load_texture(texture_path) if texture: node = self._findNodeWithMaterial(material) if node: print(f"正在为节点 {node.getName()} 应用光泽贴图") # 确保启用PBR效果 self._ensurePBREffectEnabled(node) # 清理现有的光泽贴图 existing_stages = node.findAllTextureStages() for stage in existing_stages: if "gloss" in stage.getName().lower() or stage.getSort() == 10: node.clearTexture(stage) print(f"清理了现有的光泽贴图阶段: {stage.getName()}") # 创建光泽贴图纹理阶段,对应p3d_Texture10 gloss_stage = TextureStage("gloss") gloss_stage.setSort(10) # 对应p3d_Texture10 gloss_stage.setMode(TextureStage.MGloss) # 光泽模式 node.setTexture(gloss_stage, texture) print("光泽贴图已应用到p3d_Texture10槽") print("注意:光泽贴图需要自定义shader支持才能正确显示") print(f"光泽贴图已成功应用:{texture_path}") else: print("未找到材质对应的节点") except Exception as e: print(f"应用光泽贴图失败:{e}") import traceback traceback.print_exc() def _clearConflictingTextureStages(self, node): """清理可能冲突的纹理阶段""" try: from panda3d.core import TextureStage # 获取所有纹理阶段 texture_stages = node.findAllTextureStages() # 检查是否有冲突的纹理阶段 stages_to_clear = [] for stage in texture_stages: stage_name = stage.getName() # 如果发现未命名或冲突的阶段,标记清理 if stage_name == "" or stage == TextureStage.getDefault(): # 检查是否有法线贴图在默认阶段 texture = node.getTexture(stage) if texture and "normal" in texture.getName().lower(): stages_to_clear.append(stage) # 清理冲突的阶段 for stage in stages_to_clear: node.clearTexture(stage) print(f"清理了冲突的纹理阶段: {stage.getName()}") except Exception as e: print(f"清理纹理阶段时出错: {e}") def _findNodeWithMaterial(self, target_material): """查找使用指定材质的节点""" # 这里需要根据你的场景结构来实现 # 遍历场景中的所有节点,找到使用该材质的节点 # for model in self.world.scene_manager.models: # materials = model.find_all_materials() # if target_material in materials: # return model """查找使用指定材质的节点""" # 首先尝试在当前选中的模型中查找 current_item = self.world.treeWidget.currentItem() if current_item: current_model = current_item.data(0, Qt.UserRole) if current_model: materials = current_model.find_all_materials() if target_material in materials: return current_model # 如果在当前选中模型中没找到,再遍历所有模型 for model in self.world.scene_manager.models: materials = model.find_all_materials() if target_material in materials: return model return None def _findMaterialAndNodeByTitle(self, material_title): """根据材质标题查找对应的材质和节点""" current_item = self.world.treeWidget.currentItem() if not current_item: print("未找到当前选中项") return None, None current_model = current_item.data(0, Qt.UserRole) if not current_model: print("未找到当前模型") return None, None materials = current_model.find_all_materials() model_name = current_model.getName() or "未命名模型" print(f"模型名称: '{model_name}', 材质数量: {len(materials)}") name_counter = {} for i, material in enumerate(materials): material_name = material.get_name() if hasattr(material, 'get_name') and material.get_name() else f"材质{i + 1}" base_name = f"{material_name}({model_name})" if base_name in name_counter: name_counter[base_name] += 1 unique_name = f"{base_name}_{name_counter[base_name]}" else: name_counter[base_name] = 1 unique_name = base_name print(f"材质 {i}: 生成标题='{unique_name}'") if unique_name == material_title: print(f"找到匹配的材质!") geom_node = self._findSpecificGeomNodeWithMaterial(current_model, material) if geom_node: print(f"找到几何节点: {geom_node.get_name()}") return material, geom_node else: print("未找到对应的几何节点,使用模型节点") return material, current_model print("未找到匹配的材质标题") return None, None def _findSpecificGeomNodeWithMaterial(self, model, target_material): """查找使用指定材质的具体几何节点""" from panda3d.core import MaterialAttrib, GeomNode #print(f"查找材质: {target_material.get_name() if hasattr(target_material, 'get_name') else 'unnamed'}") # 首先尝试查找GeomNode geom_nodes = model.find_all_matches("**/+GeomNode") #print(f"找到 {len(geom_nodes)} 个几何节点") # 如果没有找到GeomNode,尝试查找所有子节点 if len(geom_nodes) == 0: #print("未找到GeomNode,尝试查找所有子节点...") all_nodes = model.find_all_matches("**") #print(f"找到 {len(all_nodes)} 个子节点") for node_np in all_nodes: node = node_np.node() if isinstance(node, GeomNode): geom_nodes.append(node_np) print(f"找到GeomNode: {node_np.getName()}") for geom_np in geom_nodes: geom_node = geom_np.node() geom_count = geom_node.get_num_geoms() #rint(f"检查几何节点 {geom_node.get_name()}: {geom_count} 个几何体") for i in range(geom_count): state = geom_node.get_geom_state(i) if state.has_attrib(MaterialAttrib): material = state.get_attrib(MaterialAttrib).get_material() if material == target_material: #print(f"找到匹配的几何节点: {geom_np.get_name()}") return geom_np print("未找到匹配的几何节点") return None def _findSpecificGeomNodeForMaterial(self, target_material): """查找使用指定材质的具体几何节点(统一方法)""" material_name = target_material.get_name() if hasattr(target_material, 'get_name') else 'unnamed' print(f"查找材质对应的几何节点: {material_name}") # 优先使用存储的几何节点映射 material_id = id(target_material) if hasattr(self, '_material_geom_mapping') and material_id in self._material_geom_mapping: geom_node = self._material_geom_mapping[material_id] if geom_node: # 确保节点仍然有效 print(f"✓ 使用存储的几何节点映射: {geom_node.getName()}") return geom_node # 如果没有存储的映射,使用传统查找方法 current_item = self.world.treeWidget.currentItem() if current_item: current_model = current_item.data(0, Qt.UserRole) if current_model: # 使用现有的精确查找方法 geom_node = self._findSpecificGeomNodeWithMaterial(current_model, target_material) if geom_node: #print(f"✓ 找到特定几何节点: {geom_node.getName()}") # 存储映射以供后续使用 if not hasattr(self, '_material_geom_mapping'): self._material_geom_mapping = {} self._material_geom_mapping[material_id] = geom_node return geom_node else: #print("⚠️ 未找到特定几何节点,使用模型节点(可能影响所有材质)") return current_model print("❌ 未找到当前选中的模型") return None def _findGeomNodeWithMaterial(self, model, target_material): """查找使用指定材质的具体几何节点""" from panda3d.core import MaterialAttrib #print(f"查找材质: {target_material.get_name() if hasattr(target_material, 'get_name') else 'unnamed'}") # 遍历模型下的所有几何节点 geom_nodes = model.find_all_matches("**/+GeomNode") #print(f"找到 {len(geom_nodes)} 个几何节点") for geom_np in geom_nodes: geom_node = geom_np.node() geom_count = geom_node.get_num_geoms() #print(f"几何节点 {geom_node.get_name()}: {geom_count} 个几何体") for i in range(geom_count): state = geom_node.get_geom_state(i) if state.has_attrib(MaterialAttrib): material = state.get_attrib(MaterialAttrib).get_material() if material == target_material: #print(f"找到匹配的几何节点: {geom_np.get_name()}") return geom_np else: print(f"几何体 {i} 没有材质属性") print("未找到匹配的几何节点") return None def _displayCurrentTextures(self, material, material_layout, current_row): """显示当前材质的贴图信息""" node = self._findNodeWithMaterial(material) # 当前贴图信息标题 current_row += 1 texture_info_title = QLabel("当前贴图信息") texture_info_title.setStyleSheet("color: #666; font-weight: bold; font-size: 10px; margin-top: 5px;") material_layout.addWidget(texture_info_title, current_row, 0, 1, 4) current_row += 1 if node: # 显示当前应用的纹理信息 texture = node.getTexture() if texture: texture_name = texture.getName() or "未命名贴图" texture_info = QLabel(f"当前贴图: {texture_name}") texture_info.setStyleSheet("color: #666; font-size: 9px;") material_layout.addWidget(texture_info, current_row, 0, 1, 2) else: no_texture_info = QLabel("当前贴图: 无") no_texture_info.setStyleSheet("color: #888; font-size: 9px; font-style: italic;") material_layout.addWidget(no_texture_info, current_row, 0, 1, 2) else: no_node_info = QLabel("无法获取贴图信息") no_node_info.setStyleSheet("color: #888; font-size: 9px; font-style: italic;") material_layout.addWidget(no_node_info, current_row, 0, 1, 4) return current_row + 1 def _applyToAllMaterials(self, model, property_name, value): """将属性应用到模型的所有材质""" materials = model.find_all_materials() for material in materials: if property_name == "base_color": material.set_base_color(value) elif property_name == "roughness": material.set_roughness(value) elif property_name == "metallic": material.set_metallic(value) elif property_name == "ior": material.set_refractive_index(value) def _addShadingModelPanel(self, material, material_layout, current_row): """添加着色模型选择面板""" from PyQt5.QtWidgets import QComboBox # RenderPipeline 支持的着色模型 SHADING_MODELS = [ ("默认", 0), ("自发光", 1), ("透明涂层", 2), ("透明", 3), ("皮肤", 4), ("植物", 5), ] shading_title = QLabel("着色模型") shading_title.setStyleSheet("font-weight:bold;") material_layout.addWidget(shading_title,current_row, 0, 1, 4) current_row += 1 material_layout.addWidget(QLabel("着色模型:"), current_row, 0) shading_combo = QComboBox() for name, value in SHADING_MODELS: shading_combo.addItem(name) # 安全地获取当前着色模型 current_model = 0 # 默认值 try: if hasattr(material, 'emission') and material.emission is not None: current_model = int(material.emission.x) except (AttributeError, TypeError, ValueError): current_model = 0 shading_combo.setCurrentIndex(current_model) shading_combo.currentIndexChanged.connect( lambda idx: self._onShadingModelChanged(material, idx) ) material_layout.addWidget(shading_combo, current_row, 1, 1, 3) current_row += 1 # 如果是透明着色模型,添加透明度控制 try: if hasattr(material, 'emission') and material.emission is not None and int(material.emission.x) == 3: current_row = self._addTransparencyPanel(material, material_layout, current_row) except Exception as e: print(f"添加透明度面板时出错: {e}") return current_row def _onShadingModelChanged(self, material, model_index): """处理着色模型变化""" print(f"着色模型变化: {model_index}") # 更新着色模型 self._updateShadingModel(material, model_index) # 如果切换到透明模式,立即添加透明度面板 if model_index == 3: print("切换到透明模式,添加透明度面板...") # 延迟一点时间让UI更新完成 from PyQt5.QtCore import QTimer QTimer.singleShot(100, lambda: self._refreshMaterialUI()) # QTimer.singleShot(100, lambda: self._addTransparencyPanel(material)) def _updateShadingModel(self, material, model_index): """更新着色模型""" from panda3d.core import Vec4 # 安全地获取当前 emission 值 current_emission = Vec4(0, 1.0, 0, 0) if hasattr(material, 'emission') and material.emission is not None: current_emission = material.emission # 根据不同的着色模型设置相应的参数 if model_index == 1: # 自发光模式 default_emission_strength = 2.0 if current_emission.z == 0 else current_emission.z new_emission = Vec4(float(model_index), current_emission.y, default_emission_strength, current_emission.w) elif model_index == 3: # 透明模式 print("设置透明着色模型...") # 设置默认透明度值 default_opacity = 0.5 # 设置为较高的值,确保模型可见 # 同时在emission.y和base_color.w中设置透明度值 # emission.y可能被RenderPipeline的某些部分使用 new_emission = Vec4(float(model_index), default_opacity, current_emission.z, current_emission.w) # 透明度通过基础颜色的Alpha通道控制 self._updateMaterialAlphaForTransparency(material, default_opacity) # 应用透明渲染效果 #self._applyTransparentRenderingEffect() print(f"透明着色模型设置完成") print(f" - emission.x = {model_index} (透明着色模型)") print(f" - emission.y = {default_opacity} (可能的透明度参数)") print(f" - base_color.w = {default_opacity} (Alpha透明度)") else: new_emission = Vec4(float(model_index), current_emission.y, current_emission.z, current_emission.w) material.set_emission(new_emission) # 刷新UI以更新相关控件的值 if model_index in [1, 3]: # 自发光或透明模式 self._refreshMaterialUI() print(f"着色模型已更新为: {model_index} ({'自发光' if model_index == 1 else '透明' if model_index == 3 else '默认'})") def _addTransparencyPanel(self, material, material_layout, current_row): """添加透明度控制面板""" transparency_title = QLabel("透明度属性") transparency_title.setStyleSheet("color: #00BFFF; font-weight:bold;") material_layout.addWidget(transparency_title,current_row, 0, 1, 4) current_row += 1 # 不透明度滑块(避免混淆,使用不透明度) material_layout.addWidget(QLabel("不透明度:"), current_row, 0) opacity_spinbox = QDoubleSpinBox() opacity_spinbox.setRange(0.0, 1.0) # 最小值0.1,避免完全消失 opacity_spinbox.setSingleStep(0.01) # 安全地获取当前不透明度值(从基础颜色的Alpha通道) current_opacity = 0.3 # 默认值 try: base_color = self._getOrCreateMaterialBaseColor(material) if base_color is not None: current_opacity = base_color.w except Exception as e: print(f"获取当前透明度失败,使用默认值: {e}") opacity_spinbox.setValue(current_opacity) opacity_spinbox.valueChanged.connect(lambda v: self._updateTransparency(material, v)) material_layout.addWidget(opacity_spinbox, current_row, 1, 1, 3) current_row += 1 return current_row def _updateTransparency(self, material, opacity_value): """更新不透明度值(同时更新emission.y和base_color.w)""" try: from panda3d.core import Vec4 current_emission = material.emission or Vec4(0, 0, 0, 0) new_emission = Vec4(current_emission.x, opacity_value, current_emission.z, current_emission.w) material.set_emission(new_emission) # 更新基础颜色的Alpha通道 self._updateMaterialAlphaForTransparency(material, opacity_value) print(f"透明度已更新:") print(f" - emission.y = {opacity_value}") print(f" - base_color.w = {opacity_value}") print(f" - 视觉透明度 = {1.0 - opacity_value:.2f}") except Exception as e: print(f"更新透明度失败: {e}") def _updateMaterialAlphaForTransparency(self, material, opacity_slider): """ opacity_slider: 0=全透 1=不透 shader 需要 1-opacity_slider 作为真正的 alpha """ from panda3d.core import Vec4, TransparencyAttrib current_item = self.world.treeWidget.currentItem() if not current_item: return model = current_item.data(0, Qt.UserRole) if model.isEmpty(): return model.setTransparency(TransparencyAttrib.MAlpha) model.setTwoSided(True) model.setDepthWrite(False) alpha = opacity_slider # 反转 color = self._getOrCreateMaterialBaseColor(material) or Vec4(1, 1, 1, 1) material.base_color=Vec4(color.x, color.y, color.z, alpha) material.base_color=Vec4(color.x, color.y, color.z, alpha) em = material.emission or Vec4(0, 0, 0, 0) material.set_emission(Vec4(3.0,alpha,em.z,em.w)) self.world.render_pipeline.set_effect( model, "effects/simple_transparent.yaml", options={}, sort=200 ) self.world.render_pipeline.prepare_scene(model) print(f"[透明] 不透明度={opacity_slider:.2f} 已同步") def _applyTransparentRenderingEffect(self): from panda3d.core import TransparencyAttrib """为当前选中的模型应用透明渲染效果(简化版本)""" try: current_item = self.world.treeWidget.currentItem() if current_item: model = current_item.data(0, Qt.UserRole) if model: model.setTransparency(TransparencyAttrib.MAlpha) self.world.render_pipeline.set_effect( model, "effects/default.yaml", { "render_gbuffer":True, "alpha_testing": False, "normal_mapping": True, "render_shadow": True, "render_envmap": True, }, sort=100 ) # 让RenderPipeline自动处理透明材质 # 当emission.x=3时,RenderPipeline会自动设置正确的渲染参数 self.world.render_pipeline.prepare_scene(model) print(" - RenderPipeline自动处理: 已完成") print(f"✓ 已为模型 {model.getName()} 应用透明渲染效果") print(" 注意: 使用简化设置避免渲染冲突") except Exception as e: print(f"✗ 应用透明渲染效果失败: {e}") import traceback traceback.print_exc() def _addEmissionPanel(self, material, material_layout, current_row): """添加自发光控制面板""" emission_title = QLabel("自发光属性") emission_title.setStyleSheet("font-weight:bold;") material_layout.addWidget(emission_title, current_row, 0, 1, 4) current_row += 1 # 自发光强度标签和控件 material_layout.addWidget(QLabel("发光强度:"), current_row, 0) emission_spinbox = QDoubleSpinBox() emission_spinbox.setRange(0.0, 10.0) emission_spinbox.setSingleStep(0.1) # 安全地获取当前自发光强度 current_emission_z = 0.0 # 默认值 if hasattr(material, 'emission') and material.emission is not None: current_emission_z = material.emission.z emission_spinbox.setValue(current_emission_z) emission_spinbox.valueChanged.connect(lambda v: self._updateEmissionStrength(material, v)) material_layout.addWidget(emission_spinbox, current_row, 1, 1, 3) current_row += 1 return current_row def _updateEmissionStrength(self, material, strength): """更新自发光强度""" from panda3d.core import Vec4 # 安全地获取当前 emission 值 current_emission = Vec4(0, 0, 0, 0) if hasattr(material, 'emission') and material.emission is not None: current_emission = material.emission # 更新 emission.z 存储发光强度(用于UI显示) new_emission = Vec4(current_emission.x, current_emission.y, strength, current_emission.w) material.set_emission(new_emission) # 对于自发光材质,直接调整基础颜色的亮度 if current_emission.x == 1: # 如果是自发光着色模型 # 获取原始基础颜色(假设存储在某处,或使用当前值的归一化版本) base_intensity = 0.5 # 基础亮度 intensity_multiplier = strength / 5.0 # 将0-10范围映射到合理的倍数 # 设置发光颜色 emissive_color = Vec4( base_intensity * intensity_multiplier, base_intensity * intensity_multiplier, base_intensity * intensity_multiplier, 1.0 ) material.set_base_color(emissive_color) print(f"自发光强度已更新为: {strength}") def _addMaterialPresetPanel(self, material, material_layout, current_row): """添加材质预设面板""" from PyQt5.QtWidgets import QComboBox preset_title = QLabel("材质预设") preset_title.setStyleSheet("font-weight:bold;") material_layout.addWidget(preset_title, current_row, 0, 1, 4) current_row += 1 # 选择预设标签和控件 material_layout.addWidget(QLabel("选择预设:"), current_row, 0) preset_combo = QComboBox() preset_combo.addItems([ "自定义", "塑料", "金属", "玻璃", "橡胶", "木材", "陶瓷", "皮革" ]) # 优先检查存储的预设名称 if hasattr(material, '_applied_preset'): preset_combo.setCurrentText(material._applied_preset) else: # 安全地检测当前材质最接近的预设 try: current_preset = self._detectCurrentPreset(material) preset_combo.setCurrentText(current_preset) except Exception as e: print(f"检测材质预设时出错: {e}") preset_combo.setCurrentText("自定义") preset_combo.currentTextChanged.connect( lambda preset: self._applyMaterialPreset(material, preset) ) material_layout.addWidget(preset_combo, current_row, 1, 1, 3) current_row += 1 return current_row def _detectCurrentPreset(self, material): """检测当前材质最接近的预设""" # 定义预设的精确匹配条件 presets = { "塑料": {"base_color": (0.8, 0.8, 0.8), "roughness": 0.7, "metallic": 0.0, "ior": 1.4}, "金属": {"base_color": (0.7, 0.7, 0.7), "roughness": 0.1, "metallic": 1.0, "ior": 1.5}, "玻璃": {"base_color": (0.9, 0.9, 1.0), "roughness": 0.0, "metallic": 0.0, "ior": 1.5}, "橡胶": {"base_color": (0.2, 0.2, 0.2), "roughness": 0.9, "metallic": 0.0, "ior": 1.3}, "自发光": {"base_color": (1.0, 1.0, 1.0), "roughness": 0.5, "metallic": 0.0, "ior": 1.0} } # 容差值,用于浮点数比较 tolerance = 0.05 for preset_name, preset_values in presets.items(): # 安全检查基础颜色 base_color_match = False if hasattr(material, 'base_color') and material.base_color is not None: try: base_color_match = ( abs(material.base_color.x - preset_values["base_color"][0]) < tolerance and abs(material.base_color.y - preset_values["base_color"][1]) < tolerance and abs(material.base_color.z - preset_values["base_color"][2]) < tolerance ) except (AttributeError, TypeError): base_color_match = False # 安全检查其他属性 roughness_match = False if hasattr(material, 'roughness') and material.roughness is not None: try: roughness_match = abs(float(material.roughness) - preset_values["roughness"]) < tolerance except (AttributeError, TypeError, ValueError): roughness_match = False metallic_match = False if hasattr(material, 'metallic') and material.metallic is not None: try: metallic_match = abs(float(material.metallic) - preset_values["metallic"]) < tolerance except (AttributeError, TypeError, ValueError): metallic_match = False ior_match = False if hasattr(material, 'refractive_index') and material.refractive_index is not None: try: ior_match = abs(float(material.refractive_index) - preset_values["ior"]) < tolerance except (AttributeError, TypeError, ValueError): ior_match = False # 如果所有属性都匹配,返回预设名称 if base_color_match and roughness_match and metallic_match and ior_match: return preset_name return "自定义" # 如果没有匹配的预设 def _applyMaterialPreset(self, material, preset_name): """应用材质预设""" presets = { "塑料": {"base_color": Vec4(0.8, 0.8, 0.8, 1.0), "roughness": 0.7, "metallic": 0.0, "ior": 1.4}, "金属": {"base_color": Vec4(0.7, 0.7, 0.7, 1.0), "roughness": 0.1, "metallic": 1.0, "ior": 1.5}, "玻璃": {"base_color": Vec4(0.9, 0.9, 1.0, 0.2), "roughness": 0.0, "metallic": 0.0, "ior": 1.5,"shading_model":3,"transparency":0.2}, "橡胶": {"base_color": Vec4(0.2, 0.2, 0.2, 1.0), "roughness": 0.9, "metallic": 0.0, "ior": 1.3}, "木材": {"base_color": Vec4(0.6, 0.4, 0.2, 1.0), "roughness": 0.8, "metallic": 0.0, "ior": 1.3}, "陶瓷": {"base_color": Vec4(0.9, 0.9, 0.85, 1.0), "roughness": 0.1, "metallic": 0.0, "ior": 1.6}, "皮革": {"base_color": Vec4(0.4, 0.3, 0.2, 1.0), "roughness": 0.6, "metallic": 0.0, "ior": 1.4} } if preset_name not in presets: print(f"未知的材质预设: {preset_name}") return preset = presets[preset_name] if "shading_model" in preset: emission = Vec4(float(preset["shading_model"]), 0, 0, 0) if "transparency" in preset: emission.y = preset["transparency"] material.set_emission(emission) print(f"设置着色模型: {preset['shading_model']}") print(f"材质emission值: {material.emission}") print(f"基础颜色alpha: {preset['base_color'].w}") material.set_base_color(preset["base_color"]) material.set_roughness(preset["roughness"]) material.set_metallic(preset["metallic"]) material.set_refractive_index(preset["ior"]) if "shading_model" in preset: emission = Vec4(float (preset["shading_model"]),0,0,0) if "transparency" in preset: emission.y = preset["transparency"] material.set_emission(emission) #关键:为透明材质应用正确的渲染效果 if preset["shading_model"]==3: self._apply_transparent_effect() material._applied_preset = preset_name self._refreshMaterialUI() print(f"已应用材质预设: {preset_name}") def _apply_transparent_effect(self): """为当前选中的模型应用透明渲染效果""" current_item = self.world.treeWidget.currentItem() if current_item: model = current_item.data(0, Qt.UserRole) if model: # 只调用 prepare_scene,让它自动处理透明材质 self.world.render_pipeline.set_effect( model, "effects/default.yaml", { "render_forward": True, "render_gbuffer": False, "normal_mapping": True # 明确启用法线映射 }, 100 ) self.world.render_pipeline.prepare_scene(model) print("已重新准备场景以应用透明效果") def _refreshMaterialUI(self): """刷新材质 UI 显示""" # 重新更新当前选中项的属性面板 if hasattr(self.world, 'treeWidget') and self.world.treeWidget.currentItem(): current_item = self.world.treeWidget.currentItem() # 触发属性面板更新 self.updatePropertyPanel(current_item) def _addColorSpacePanel(self, material): """添加颜色空间选择面板""" from PyQt5.QtWidgets import QButtonGroup, QRadioButton, QHBoxLayout, QWidget # color_space_title = QLabel("颜色空间") # color_space_title.setStyleSheet("color: #FF9800; font-weight:bold;") # self._propertyLayout.addRow(color_space_title) # # color_space_widget = QWidget() # color_space_layout = QHBoxLayout(color_space_widget) # color_space_group = QButtonGroup() # rgb_radio = QRadioButton("RGB") # srgb_radio = QRadioButton("sRGB") # hsv_radio = QRadioButton("HSV") # # rgb_radio.setChecked(True) # # color_space_group.addButton(rgb_radio, 0) # color_space_group.addButton(srgb_radio, 1) # color_space_group.addButton(hsv_radio, 2) # # color_space_layout.addWidget(rgb_radio) # color_space_layout.addWidget(srgb_radio) # color_space_layout.addWidget(hsv_radio) # # color_space_group.buttonClicked.connect( # lambda button: self._updateColorSpace(material, color_space_group.id(button)) # ) # # self._propertyLayout.addRow("颜色空间:", color_space_widget) def _addBatchOperationsPanel(self, model): """添加批量操作面板""" batch_title = QLabel("批量操作") batch_title.setStyleSheet("color: #E91E63; font-weight:bold;") self._propertyLayout.addRow(batch_title) # 批量设置粗糙度 batch_roughness_btn = QPushButton("统一粗糙度") batch_roughness_btn.clicked.connect(lambda: self._batchSetRoughness(model)) self._propertyLayout.addRow("批量粗糙度:", batch_roughness_btn) # 批量设置金属性 batch_metallic_btn = QPushButton("统一金属性") batch_metallic_btn.clicked.connect(lambda: self._batchSetMetallic(model)) self._propertyLayout.addRow("批量金属性:", batch_metallic_btn) def _batchSetRoughness(self, model): """批量设置粗糙度""" from PyQt5.QtWidgets import QInputDialog value, ok = QInputDialog.getDouble(None, "批量设置", "粗糙度值:", 0.5, 0.0, 1.0, 2) if ok: self._applyToAllMaterials(model, "roughness", value) print(f"已将所有材质粗糙度设置为: {value}") def _applyMetallicTexture_NEW(self, material, texture_path): """应用金属性贴图 - 简化重写版本""" try: from RenderPipelineFile.rpcore.loader import RPLoader from panda3d.core import TextureStage print(f"🎨 应用金属性贴图: {texture_path}") # 1. 加载纹理 texture = RPLoader.load_texture(texture_path) if not texture: print("❌ 纹理加载失败") return # 2. 找到节点 node = self._findSpecificGeomNodeForMaterial(material) if not node: print("❌ 未找到材质对应的节点") return print(f"🎯 目标节点: {node.getName()}") # 3. 设置材质金属性为1.0 material.set_metallic(1.0) print("🔧 材质金属性设置为1.0") # # 4. 创建纹理阶段 # metallic_stage = TextureStage("metallic_map") # metallic_stage.setSort(5) # p3d_Texture5 # metallic_stage.setMode(TextureStage.MModulate) # # # 5. 绑定纹理 # node.setTexture(metallic_stage, texture) # print("🔗 纹理已绑定到p3d_Texture5槽") # 6. 应用金属性PBR效果,检查是否需要保持法线映射 has_normal = self._hasNormalTexture(node) if not has_normal: print("⚠️ 检测到材质没有法线贴图,先添加默认法线贴图...") #self._applyDefaultNormalTexture(node) self._applyNormalTexture(material,"RenderPipelineFile/Default_NRM_2K.png") print("✅ 默认法线贴图已添加") else: print("✅ 检测到材质已有法线贴图") print("清理现有金属度贴图...") existing_stages = node.findAllTextureStages() for stage in existing_stages: if stage.getSort() == 5: # p3d_Texture3槽 node.clearTexture(stage) print(f"已清理现有金属度贴图: {stage.getName()}") for i in range(4): metallic_stage = TextureStage("metallic_map") metallic_stage.setSort(5) # p3d_Texture5 metallic_stage.setMode(TextureStage.MModulate) node.setTexture(metallic_stage, texture) try: # 检查是否需要透明度测试 needs_alpha = self._needsAlphaTesting(node) print(f"🔍 透明度测试: {'需要' if needs_alpha else '不需要'}") self.world.render_pipeline.set_effect( node, "effects/pbr_with_metallic.yaml", { "normal_mapping": True, "render_gbuffer": True, "alpha_testing": needs_alpha, # 根据是否需要透明度决定 "parallax_mapping": False, "render_shadow": True, "render_envmap": True }, 100 ) print(f"✅ 金属性PBR效果已应用(法线映射: {'启用' if has_normal else '禁用'})") # 温和的shader刷新方法 print("🔄 温和刷新shader状态...") try: # 只重新绑定纹理,不破坏渲染状态 node.setTexture(metallic_stage, texture) # 重新应用效果,但使用相同优先级 self.world.render_pipeline.set_effect( node, "effects/pbr_with_metallic.yaml", { #"normal_mapping": has_normal, "normal_mapping": True, "render_gbuffer": True, "alpha_testing": False, "parallax_mapping": False, "render_shadow": True, "render_envmap": True }, 100 # 相同优先级 ) print("✅ 金属性PBR效果已应用") # 关键修复:多次重新绑定纹理,确保着色器编译完成后生效 print("🔧 强化纹理绑定机制...") try: import time # 多次重新绑定,模拟手动"应用两次"的效果 for i in range(3): print(f" 第{i+1}次绑定...") # 等待更长时间确保着色器编译完成 time.sleep(0.05) # 50ms延迟 # 强制重新绑定纹理 node.setTexture(metallic_stage, texture) # 强制刷新节点状态 node.setRenderModeWireframe() node.clearRenderMode() # 验证绑定 applied_texture = node.getTexture(metallic_stage) if applied_texture: print(f" ✅ 第{i+1}次绑定成功") else: print(f" ❌ 第{i+1}次绑定失败") # 最终验证 final_texture = node.getTexture(metallic_stage) if final_texture: print(f"✅ 强化绑定完成: {final_texture.getName()}") print("🔍 现在应该看到正确的金属性贴图效果") print("🔍 黑色区域=非金属,白色区域=金属") else: print("❌ 强化绑定最终失败") except Exception as rebind_error: print(f"⚠️ 强化纹理绑定失败: {rebind_error}") except Exception as refresh_error: print(f"⚠️ 金属性PBR效果应用失败: {refresh_error}") except Exception as e: print(f"⚠️ PBR效果应用失败: {e}") except Exception as e: print(f"❌ 应用金属性贴图失败: {e}") import traceback traceback.print_exc() def _hasMetallicTexture(self, node): """检查节点是否有金属性贴图""" try: all_stages = node.findAllTextureStages() for stage in all_stages: if stage.getSort() == 5: # p3d_Texture5 是金属性贴图槽 tex = node.getTexture(stage) if tex: print(f"🔍 发现金属性贴图: {tex.getName()}") return True return False except Exception as e: print(f"⚠️ 检查金属性贴图失败: {e}") return False def _applyDefaultNormalTexture(self, node): """应用RenderPipeline的默认法线贴图""" try: from RenderPipelineFile.rpcore.loader import RPLoader from panda3d.core import TextureStage print("🎨 加载RenderPipeline默认法线贴图...") # 使用RenderPipeline自带的默认法线贴图 default_normal_path = "RenderPipelineFile/Default_NRM_2K.png" texture = RPLoader.load_texture(default_normal_path) if not texture: print(f"❌ 无法加载默认法线贴图: {default_normal_path}") # 回退到虚拟法线贴图 return print(f"✅ 默认法线贴图加载成功: {texture.getName()}") # 创建法线贴图纹理阶段 normal_stage = TextureStage("default_normal") normal_stage.setSort(1) # p3d_Texture1 normal_stage.setMode(TextureStage.MModulate) normal_stage.setTexcoordName("texcoord") # 应用默认法线贴图 node.setTexture(normal_stage, texture) print("✅ RenderPipeline默认法线贴图已应用") return True except Exception as e: print(f"⚠️ 应用默认法线贴图失败: {e}") # 回退到虚拟法线贴图 return self._createDummyNormalTexture(node) def _needsAlphaTesting(self, node): """检查节点是否需要透明度测试(检查漫反射贴图是否有透明通道)""" try: all_stages = node.findAllTextureStages() for stage in all_stages: if stage.getSort() == 0: # p3d_Texture0 是漫反射贴图槽 tex = node.getTexture(stage) if tex: # 检查纹理格式是否支持透明度 format_name = str(tex.getFormat()) has_alpha = 'alpha' in format_name.lower() or 'rgba' in format_name.lower() if has_alpha: print(f"🔍 发现透明漫反射贴图: {tex.getName()}") return True return False except Exception as e: print(f"⚠️ 检查透明度需求失败: {e}") return True # 出错时默认启用透明度测试,更安全 def _addSunAzimuthPanel(self): """添加太阳方位角控制面板""" # 太阳控制组 sun_group = QGroupBox("太阳控制") sun_layout = QGridLayout() # 太阳方位角 sun_layout.addWidget(QLabel("方位角:"), 0, 0) self.azimuthSpinBox = QDoubleSpinBox() self.azimuthSpinBox.setRange(0, 360) self.azimuthSpinBox.setSuffix("°") self.azimuthSpinBox.setValue(180) self.azimuthSpinBox.valueChanged.connect(self._applySunAzimuth_new) sun_layout.addWidget(self.azimuthSpinBox, 0, 1, 1, 2) # 太阳高度角 sun_layout.addWidget(QLabel("高度角:"), 1, 0) self.altitudeSpinBox = QDoubleSpinBox() self.altitudeSpinBox.setRange(0, 90) self.altitudeSpinBox.setSuffix("°") self.altitudeSpinBox.setValue(90) self.altitudeSpinBox.valueChanged.connect(self._applySunAltitude) sun_layout.addWidget(self.altitudeSpinBox, 1, 1, 1, 2) # 太阳预设 sun_layout.addWidget(QLabel("预设:"), 2, 0) presetCombo = QComboBox() presetCombo.addItems(["日出", "正午", "日落", "午夜"]) sun_layout.addWidget(presetCombo, 2, 1, 1, 2) # 映射 preset_map = { "日出": "sunrise", "正午": "noon", "日落": "sunset", "午夜": "midnight" } # 应用预设按钮 applyPresetBtn = QPushButton("应用预设") applyPresetBtn.clicked.connect(lambda: self._setSunPreset(preset_map[presetCombo.currentText()])) sun_layout.addWidget(applyPresetBtn, 2, 3) # 实时更新 # realtimeCheckBox = QCheckBox("实时更新") # realtimeCheckBox.setChecked(True) # sun_layout.addWidget(realtimeCheckBox, 3, 0, 1, 3) # 跨三列 sun_group.setLayout(sun_layout) self._propertyLayout.addWidget(sun_group) def _onSunAzimuthSliderChanged(self, value): """滑块值改变时的回调""" try: # 同步到数值框 self.sun_azimuth_spinbox.blockSignals(True) self.sun_azimuth_spinbox.setValue(value) self.sun_azimuth_spinbox.blockSignals(False) # 应用到Day Time Editor self._applySunAzimuth_new(value) except Exception as e: print(f"❌ 滑块值改变处理失败: {e}") def _onSunAzimuthSpinboxChanged(self, value): """数值框值改变时的回调""" try: # 同步到滑块 self.sun_azimuth_slider.blockSignals(True) self.sun_azimuth_slider.setValue(value) self.sun_azimuth_slider.blockSignals(False) # 应用到Day Time Editor self._applySunAzimuth_new(value) except Exception as e: print(f"❌ 数值框值改变处理失败: {e}") def _onSunAltitudeSliderChanged(self, value): """太阳高度角滑块值改变时的回调""" try: # 同步到数值框 self.sun_altitude_spinbox.blockSignals(True) self.sun_altitude_spinbox.setValue(value) self.sun_altitude_spinbox.blockSignals(False) # 应用太阳高度角 self._applySunAltitude(value) except Exception as e: print(f"❌ 太阳高度角滑块值改变处理失败: {e}") def _onSunAltitudeSpinboxChanged(self, value): """太阳高度角数值框值改变时的回调""" try: # 同步到滑块 self.sun_altitude_slider.blockSignals(True) self.sun_altitude_slider.setValue(value) self.sun_altitude_slider.blockSignals(False) # 应用太阳高度角 self._applySunAltitude(value) except Exception as e: print(f"❌ 太阳高度角数值框值改变处理失败: {e}") def _setSunPreset(self, preset_name): """设置太阳预设位置""" try: presets = { "sunrise": (90, 45), # 东方,低角度 "noon": (180, 90), # 南方,天顶 "sunset": (270, 45), # 西方,低角度 "midnight": (0, 0) # 北方,地平线 } if preset_name in presets: azimuth, altitude = presets[preset_name] # 更新滑块和数值框 self.azimuthSpinBox.blockSignals(True) self.altitudeSpinBox.blockSignals(True) self.azimuthSpinBox.setValue(azimuth) self.altitudeSpinBox.setValue(altitude) self.azimuthSpinBox.blockSignals(False) self.altitudeSpinBox.blockSignals(False) # 应用设置 - 优先使用Day Time Editor azimuth_success = self._updateDayTimeEditorSetting("scattering", "sun_azimuth", azimuth) altitude_success = self._updateDayTimeEditorSetting("scattering", "sun_altitude", altitude) if azimuth_success and altitude_success: print(f"✅ 通过Day Time Editor设置太阳预设 {preset_name}: 方位角{azimuth}°, 高度角{altitude}°") elif hasattr(self.world, 'sun_controller'): self.world.sun_controller.set_sun_position(azimuth, altitude) print(f"✅ 通过太阳控制器设置预设 {preset_name}: 方位角{azimuth}°, 高度角{altitude}°") elif hasattr(self.world, 'setSunPosition'): self.world.setSunPosition(azimuth, altitude) print(f"✅ 通过主程序方法设置预设 {preset_name}: 方位角{azimuth}°, 高度角{altitude}°") else: # 分别应用方位角和高度角 self._applySunAzimuth_new(azimuth) self._applySunAltitude(altitude) print(f"✅ 设置太阳预设 {preset_name}: 方位角{azimuth}°, 高度角{altitude}°") except Exception as e: print(f"❌ 设置太阳预设失败: {e}") import traceback traceback.print_exc() def _applySunAzimuth_new(self, azimuth_value): """应用太阳方位角 - 直接控制Day Time Editor""" try: # 方法1: 直接控制Day Time Editor中的sun_azimuth节点 success = self._updateDayTimeEditorSetting("scattering", "sun_azimuth", azimuth_value) if success: print(f"✅ 通过Day Time Editor设置方位角: {azimuth_value}°") return # 方法2: 使用我们的太阳控制器作为备用 if hasattr(self.world, 'sun_controller'): self.world.sun_controller.set_sun_azimuth(azimuth_value) print(f"✅ 通过太阳控制器设置方位角: {azimuth_value}°") return # 方法3: 直接调用主程序的太阳控制方法 if hasattr(self.world, 'setSunAzimuth'): self.world.setSunAzimuth(azimuth_value) print(f"✅ 通过主程序方法设置方位角: {azimuth_value}°") return print("⚠️ 所有方位角设置方法都不可用") except Exception as e: print(f"❌ 应用太阳方位角失败: {e}") import traceback traceback.print_exc() def _applySunAltitude(self, altitude_value): """应用太阳高度角 - 直接控制Day Time Editor""" try: # 方法1: 直接控制Day Time Editor中的sun_altitude节点 success = self._updateDayTimeEditorSetting("scattering", "sun_altitude", altitude_value) if success: print(f"✅ 通过Day Time Editor设置高度角: {altitude_value}°") return # 方法2: 使用我们的太阳控制器作为备用 if hasattr(self.world, 'sun_controller'): self.world.sun_controller.set_sun_altitude(altitude_value) print(f"✅ 通过太阳控制器设置高度角: {altitude_value}°") return # 方法3: 直接调用主程序的太阳控制方法 if hasattr(self.world, 'setSunAltitude'): self.world.setSunAltitude(altitude_value) print(f"✅ 通过主程序方法设置高度角: {altitude_value}°") return print("⚠️ 所有高度角设置方法都不可用") except Exception as e: print(f"❌ 应用太阳高度角失败: {e}") import traceback traceback.print_exc() def _updateDayTimeEditorSetting(self, plugin_name, setting_name, value): """直接更新Day Time Editor中的设置节点值""" try: # 检查是否有RenderPipeline和插件管理器 if not hasattr(self.world, 'render_pipeline') or not self.world.render_pipeline: print("⚠️ RenderPipeline未初始化") return False pipeline = self.world.render_pipeline if not hasattr(pipeline, 'plugin_mgr') or not pipeline.plugin_mgr: print("⚠️ 插件管理器未初始化") return False plugin_mgr = pipeline.plugin_mgr # 检查插件是否存在 if plugin_name not in plugin_mgr.instances: print(f"⚠️ 插件 '{plugin_name}' 不存在") return False # 检查Day Time设置是否存在 if plugin_name not in plugin_mgr.day_settings: print(f"⚠️ 插件 '{plugin_name}' 没有Day Time设置") return False day_settings = plugin_mgr.day_settings[plugin_name] if setting_name not in day_settings: print(f"⚠️ 设置 '{setting_name}' 在插件 '{plugin_name}' 中不存在") return False setting_handle = day_settings[setting_name] # 根据设置类型转换值 if setting_name == "sun_azimuth": # 方位角:度数转换为0-1范围 normalized_value = (value % 360) / 360.0 elif setting_name == "sun_altitude": # 高度角:度数转换为0-1范围 normalized_value = max(0, min(90, value)) / 90.0 else: # 其他设置直接使用原值 normalized_value = value # 获取当前时间(如果Day Time Editor正在运行) current_time = getattr(self, '_current_daytime', 0.5) # 默认中午12点 # 更新设置的曲线值 if hasattr(setting_handle, 'curves') and setting_handle.curves: # 清除现有的控制点,设置单一值 setting_handle.curves[0].set_single_value(normalized_value) print(f"✅ 更新Day Time设置: {plugin_name}.{setting_name} = {value} (归一化: {normalized_value:.3f})") # 保存设置到配置文件 try: plugin_mgr.save_daytime_overrides("/$$rpconfig/daytime.yaml") print("✅ Day Time设置已保存到配置文件") except Exception as e: print(f"⚠️ 保存配置文件失败: {e}") # 通知Day Time Editor重新加载配置(如果正在运行) try: from RenderPipelineFile.rpcore.util.network_communication import NetworkCommunication NetworkCommunication.send_async(NetworkCommunication.DAYTIME_PORT, "loadconf") print("✅ 已通知Day Time Editor重新加载配置") except Exception as e: print(f"⚠️ 通知Day Time Editor失败: {e}") return True else: print(f"⚠️ 设置 '{setting_name}' 没有可用的曲线") return False except Exception as e: print(f"❌ 更新Day Time Editor设置失败: {e}") import traceback traceback.print_exc() return False def _applySunAzimuth(self, azimuth_degrees): """应用太阳方位角到Day Time Editor中的Sun Azimuth节点""" try: if not hasattr(self, 'world') or not self.world: print("⚠️ World对象不存在") return # Day Time Editor是独立进程,需要通过进程间通信控制 print(f"🌞 尝试控制Day Time Editor中的Sun Azimuth: {azimuth_degrees}°") # 方法1:通过文件通信 success = self._sendSunAzimuthViaFile(azimuth_degrees) if success: return # 方法2:尝试直接控制RenderPipeline的daytime_mgr(如果存在) # success = self._controlLocalDaytimeManager(azimuth_degrees) # if success: # return # 方法3:通过socket通信(如果实现了) # success = self._sendSunAzimuthViaSocket(azimuth_degrees) # if success: # return print("⚠️ 无法控制Day Time Editor中的Sun Azimuth节点") print(" 原因:Day Time Editor运行在独立进程中") print(" 建议:") print(" 1. 直接在Day Time Editor窗口中调整Sun Azimuth") print(" 2. 或者实现进程间通信机制") except Exception as e: print(f"❌ 应用太阳方位角失败: {e}") import traceback traceback.print_exc() def _sendSunAzimuthViaFile(self, azimuth_degrees): """通过文件通信发送Sun Azimuth值到Day Time Editor""" try: import json import os import tempfile # 创建通信文件 comm_dir = os.path.join(tempfile.gettempdir(), "daytime_editor_comm") os.makedirs(comm_dir, exist_ok=True) comm_file = os.path.join(comm_dir, "sun_azimuth_command.json") command = { "command": "set_sun_azimuth", "value": azimuth_degrees, "timestamp": __import__('time').time() } with open(comm_file, 'w') as f: json.dump(command, f) print(f"📁 已通过文件发送Sun Azimuth命令: {azimuth_degrees}°") print(f" 通信文件: {comm_file}") return True except Exception as e: print(f"⚠️ 文件通信失败: {e}") return False def _addAnimationPanel(self, origin_model): try: has_animation = False # 动画控制组 animation_group = QGroupBox("动画控制") animation_layout = QGridLayout() # 首先检测骨骼动画 has_skeletal_anim = False try: actor = self._getActor(origin_model) if actor and actor.getAnimNames(): self._buildSkeletalUI(origin_model, actor, animation_layout) has_animation = True has_skeletal_anim = True print(f"[信息] 检测到骨骼动画: {actor.getAnimNames()}") except Exception as actor_error: # 忽略 Actor 加载错误,很多模型都不是角色动画 print(f"[信息] 该模型不包含骨骼动画: {actor_error}") # 只有在没有骨骼动画时才检测非骨骼动画 if not has_skeletal_anim: non_skeletal_anims = self._detectNonSkeletalAnimations(origin_model) if non_skeletal_anims and self._validateNonSkeletalAnimations(origin_model, non_skeletal_anims): self._buildNonSkeletalUI(origin_model, non_skeletal_anims, animation_layout) has_animation = True print(f"[信息] 检测到非骨骼动画: {list(non_skeletal_anims.keys())}") # 如果都没有动画 if not has_animation: no_anim_label = QLabel("此模型无动画") no_anim_label.setStyleSheet("color:#888;font-style:italic;") animation_layout.addWidget(no_anim_label, 0, 0) animation_group.setLayout(animation_layout) self._propertyLayout.addWidget(animation_group) except Exception as e: print("添加动画面板失败:", e) import traceback traceback.print_exc() def _buildSkeletalUI(self,origin_model,actor,layout): from PyQt5.QtWidgets import QLabel,QComboBox,QHBoxLayout,QWidget,QPushButton,QDoubleSpinBox actor.hide() origin_model.show() # animation_title = QLabel("骨骼动画控制") # animation_title.setStyleSheet("color:#6B6BFF;font-weight:bold;font-size:14px;margin-top:10px;") # self._propertyLayout.addRow(animation_title) # 获取和分析动画名称 anim_names = actor.getAnimNames() processed_names = self._processAnimationNames(origin_model, anim_names) # 显示动画信息 format_info = self._getModelFormat(origin_model) animation_info = self._analyzeAnimationQuality(actor, anim_names, format_info) info_text = f"格式: {format_info} | 动画数量: {len(processed_names)}" if animation_info: info_text += f" | {animation_info}" info_label = QLabel(info_text) info_label.setStyleSheet("color:#888;font-size:10px;") layout.addWidget(QLabel("信息:"), 0, 0) layout.addWidget(info_label, 0, 1, 1, 3) # 如果是 FBX 且动画有问题,添加转换按钮 current_row = 1 if format_info == "FBX" and "⚠️" in animation_info: convert_btn = QPushButton("🔄 转换FBX动画") convert_btn.setStyleSheet("background-color:#FFA500;color:white;font-weight:bold;") convert_btn.clicked.connect(lambda: self._convertFBXManually(origin_model)) layout.addWidget(QLabel("修复:"), current_row, 0) layout.addWidget(convert_btn, current_row, 1, 1, 3) current_row += 1 self.animation_combo = QComboBox() # 使用处理后的名称,但保留原始名称用于播放 for display_name, original_name in processed_names: self.animation_combo.addItem(display_name, original_name) layout.addWidget(QLabel("动画名称:"), current_row, 0) layout.addWidget(self.animation_combo, current_row, 1, 1, 3) current_row += 1 btn_box = QWidget() btn_lay = QHBoxLayout(btn_box) for txt,slot in (("播放",self._playAnimation), ("暂停",self._pauseAnimation), ("停止",self._stopAnimation), ("循环",self._loopAnimation)): btn = QPushButton(txt) btn.clicked.connect(lambda _,f=slot:f(origin_model)) btn_lay.addWidget(btn) layout.addWidget(QLabel("控制:"), current_row, 0) layout.addWidget(btn_box, current_row, 1, 1, 3) current_row += 1 self.speed_spinbox = QDoubleSpinBox() self.speed_spinbox.setRange(0.1,5.0) self.speed_spinbox.setSingleStep(0.1) saved = origin_model.getPythonTag("anim_speed") self.speed_spinbox.setValue(saved if saved is not None else 1.0) #self.speed_spinbox.setValue(1.0) self.speed_spinbox.valueChanged.connect(lambda v:self._setAnimationSpeed(origin_model,v)) layout.addWidget(QLabel("播放速度:"), current_row, 0) layout.addWidget(self.speed_spinbox, current_row, 1) def _getModelFormat(self, origin_model): """获取模型格式信息""" filepath = origin_model.getTag("model_path") original_path = origin_model.getTag("original_path") converted_from = origin_model.getTag("converted_from") if filepath: ext = filepath.lower().split('.')[-1] format_name = ext.upper() # 如果是转换后的文件,显示转换信息 if converted_from and original_path: original_ext = converted_from.upper() format_name = f"{format_name} (从{original_ext}转换)" return format_name return "未知" def _processAnimationNames(self, origin_model, anim_names): """处理和分析动画名称,返回 [(显示名称, 原始名称), ...]""" format_info = self._getModelFormat(origin_model) processed = [] print(f"[动画分析] 格式: {format_info}, 原始动画名称: {anim_names}") for name in anim_names: display_name = name original_name = name if format_info == "GLB": # GLB 格式通常有真实的动画名称 if "|" in name: # 处理类似 'Armature|mixamo.com|Layer0' 的名称 parts = name.split("|") if "mixamo" in name.lower(): # Mixamo 动画 display_name = f"Mixamo_{parts[-1]}" if len(parts) > 1 else name elif len(parts) > 2: # 其他复杂命名 display_name = f"{parts[0]}_{parts[-1]}" else: display_name = parts[-1] elif format_info == "FBX": # FBX 格式可能需要特殊处理 if self._isLikelyBoneGroup(name): # 检查是否是骨骼组而非动画 print(f"[警告] '{name}' 可能不是真正的动画序列,而是骨骼组") display_name = f"⚠️ {name} (可能非动画)" else: display_name = name elif format_info in ["EGG", "BAM"]: # 原生格式通常命名规范 display_name = name processed.append((display_name, original_name)) print(f"[动画分析] {original_name} → {display_name}") return processed def _isLikelyBoneGroup(self, name): """判断动画名称是否更像骨骼组而不是动画序列""" bone_indicators = ['joints', 'bones', 'skeleton', 'surface', 'mesh', 'beta', 'rig'] name_lower = name.lower() # 如果包含这些关键词,可能是骨骼组 for indicator in bone_indicators: if indicator in name_lower: return True # 如果名称太简单(少于3个字符),可能不是动画 if len(name) < 3: return True return False def _analyzeAnimationQuality(self, actor, anim_names, format_info): """分析动画质量和类型""" try: total_frames = 0 valid_anims = 0 for anim_name in anim_names: try: control = actor.getAnimControl(anim_name) if control: frames = control.getNumFrames() if frames > 1: valid_anims += 1 total_frames += frames print(f"[动画分析] '{anim_name}': {frames} 帧") else: print(f"[动画分析] '{anim_name}': 无有效帧数 ({frames})") except Exception as e: print(f"[动画分析] '{anim_name}' 分析失败: {e}") if valid_anims == 0: return "⚠️ 无有效动画序列" elif valid_anims < len(anim_names): return f"⚠️ {valid_anims}/{len(anim_names)} 个有效" else: avg_frames = total_frames // valid_anims return f"✓ 平均 {avg_frames} 帧" except Exception as e: print(f"[动画分析] 分析失败: {e}") return "分析失败" def _getActor(self,origin_model): if origin_model in self._actor_cache: return self._actor_cache[origin_model] filepath = origin_model.getTag("model_path") if not filepath: return None print(f"[Actor加载] 尝试加载: {filepath}") # 检查是否是 FBX 文件,如果是,使用专门的 FBX 动画加载器 if filepath.lower().endswith('.fbx'): return self._createFBXActor(origin_model, filepath) # 其他格式使用标准 Actor 加载 try: test_actor=Actor(filepath) anims = test_actor.getAnimNames() print(f"[Actor加载] 标准加载检测到动画: {anims}") if not anims: test_actor.cleanup() test_actor.removeNode() return None actor = Actor(filepath) actor.reparentTo(self.world.render) self._actor_cache[origin_model] = actor return actor except Exception as e: print(f"创建Actor失败: {e}") return None def _createFBXActor(self, origin_model, filepath): """专门为 FBX 文件创建 Actor,使用转换方式获取真实动画""" try: print(f"[FBX动画] 开始处理 FBX 动画: {filepath}") # 方法1: 尝试转换 FBX 为包含动画的格式 converted_actor = self._convertFBXToActor(filepath) if converted_actor: converted_actor.reparentTo(self.world.render) self._actor_cache[origin_model] = converted_actor print(f"[FBX动画] 转换成功,动画: {converted_actor.getAnimNames()}") return converted_actor # 方法2: 直接加载但进行动画数据修复 actor = Actor(filepath) if actor: fixed_actor = self._fixFBXAnimations(actor, filepath) if fixed_actor and fixed_actor.getAnimNames(): fixed_actor.reparentTo(self.world.render) self._actor_cache[origin_model] = fixed_actor print(f"[FBX动画] 修复成功,动画: {fixed_actor.getAnimNames()}") return fixed_actor print(f"[FBX动画] 无法获取有效动画数据") return None except Exception as e: print(f"[FBX动画] 处理失败: {e}") return None def _convertFBXToActor(self, fbx_path): """将 FBX 转换为可用的 Actor""" try: import tempfile import os # 创建临时文件用于转换 temp_dir = tempfile.mkdtemp() egg_path = os.path.join(temp_dir, "converted.egg") print(f"[FBX转换] 转换 {fbx_path} 到 {egg_path}") # 使用 Panda3D 转换工具链 # FBX -> Collada -> EGG try: # 检查是否有可用的转换工具 import subprocess # 方法1: 尝试直接使用 assimp (如果安装了) result = subprocess.run([ 'assimp', 'export', fbx_path, egg_path ], capture_output=True, text=True, timeout=30) if result.returncode == 0 and os.path.exists(egg_path): actor = Actor(egg_path) if actor.getAnimNames(): print(f"[FBX转换] Assimp 转换成功") return actor except (subprocess.TimeoutExpired, subprocess.CalledProcessError, FileNotFoundError): print(f"[FBX转换] Assimp 转换失败,尝试其他方法") # 方法2: 使用 Blender 脚本转换(如果安装了 Blender) try: blender_script = f''' import bpy import os bpy.ops.wm.read_factory_settings(use_empty=True) bpy.ops.import_scene.fbx(filepath="{fbx_path}") bpy.ops.export_scene.gltf(filepath="{egg_path.replace('.egg', '.gltf')}", export_animations=True) ''' script_path = os.path.join(temp_dir, "convert.py") with open(script_path, 'w') as f: f.write(blender_script) result = subprocess.run([ 'blender', '--background', '--python', script_path ], capture_output=True, text=True, timeout=60) gltf_path = egg_path.replace('.egg', '.gltf') if os.path.exists(gltf_path): # 使用 gltf2bam 转换为 BAM subprocess.run(['gltf2bam', gltf_path, egg_path.replace('.egg', '.bam')]) bam_path = egg_path.replace('.egg', '.bam') if os.path.exists(bam_path): actor = Actor(bam_path) if actor.getAnimNames(): print(f"[FBX转换] Blender 转换成功") return actor except (subprocess.TimeoutExpired, subprocess.CalledProcessError, FileNotFoundError): print(f"[FBX转换] Blender 转换失败") return None except Exception as e: print(f"[FBX转换] 转换过程出错: {e}") return None def _fixFBXAnimations(self, actor, fbx_path): """修复 FBX Actor 的动画数据""" try: print(f"[FBX修复] 尝试修复动画数据") # 获取所有动画名称 anim_names = actor.getAnimNames() print(f"[FBX修复] 原始动画名称: {anim_names}") # 检查每个动画是否真的有动画数据 valid_anims = [] for anim_name in anim_names: try: control = actor.getAnimControl(anim_name) if control and control.getNumFrames() > 1: valid_anims.append(anim_name) print(f"[FBX修复] 有效动画: {anim_name} ({control.getNumFrames()} 帧)") else: print(f"[FBX修复] 无效动画: {anim_name} (帧数: {control.getNumFrames() if control else 0})") except: print(f"[FBX修复] 无法获取动画控制: {anim_name}") if valid_anims: print(f"[FBX修复] 找到 {len(valid_anims)} 个有效动画") return actor else: print(f"[FBX修复] 没有找到有效动画,尝试重新解析") # 尝试重新加载和分析 FBX 文件结构 return self._deepAnalyzeFBX(fbx_path) except Exception as e: print(f"[FBX修复] 修复失败: {e}") return None def _deepAnalyzeFBX(self, fbx_path): """深度分析 FBX 文件并尝试提取动画""" try: print(f"[FBX深度分析] 分析文件: {fbx_path}") # 尝试直接加载为模型,然后手动查找动画节点 from panda3d.core import Loader loader = Loader.getGlobalPtr() model = loader.loadSync(fbx_path) if model: print(f"[FBX深度分析] 成功加载模型") # 查找动画相关节点 anim_nodes = model.findAllMatches("**/+AnimBundleNode") char_nodes = model.findAllMatches("**/+CharacterNode") print(f"[FBX深度分析] AnimBundleNode: {anim_nodes.getNumPaths()}") print(f"[FBX深度分析] CharacterNode: {char_nodes.getNumPaths()}") if not char_nodes.isEmpty(): # 尝试基于 CharacterNode 创建 Actor char_node = char_nodes.getPath(0) character = char_node.node().getCharacter() if character: # 创建新的 Actor 实例并绑定角色 actor = Actor() actor.instance(model, "character") # 检查是否有动画 if actor.getAnimNames(): print(f"[FBX深度分析] 成功提取动画: {actor.getAnimNames()}") return actor return None except Exception as e: print(f"[FBX深度分析] 分析失败: {e}") return None def _convertFBXManually(self, origin_model): """手动转换 FBX 动画""" from PyQt5.QtWidgets import QMessageBox, QProgressDialog from PyQt5.QtCore import QTimer try: filepath = origin_model.getTag("model_path") if not filepath or not filepath.lower().endswith('.fbx'): return # 显示进度对话框 progress = QProgressDialog("正在转换FBX动画...", "取消", 0, 100) progress.setWindowTitle("FBX动画转换") progress.show() print(f"[手动转换] 开始转换: {filepath}") # 尝试使用系统转换工具 converted_path = self._systemConvertFBX(filepath, progress) if converted_path: # 重新加载转换后的模型 progress.setLabelText("重新加载模型...") progress.setValue(80) # 清除缓存 if origin_model in self._actor_cache: del self._actor_cache[origin_model] # 更新模型路径标签 origin_model.setTag("model_path", converted_path) progress.setValue(100) progress.hide() # 显示成功消息 QMessageBox.information(None, "转换成功", f"FBX动画转换成功!\n请重新选择模型查看动画。") print(f"[手动转换] 转换完成: {converted_path}") else: progress.hide() # 显示转换选项 msg = QMessageBox() msg.setWindowTitle("转换建议") msg.setText("自动转换失败,建议使用以下方法:") msg.setDetailedText(""" 1. 使用 Blender 转换: - 打开 Blender - 导入 FBX 文件 - 导出为 glTF (.gltf) 格式,确保选择"包含动画" 2. 使用命令行工具: - gltf2bam your_file.gltf your_file.bam 3. 检查原始 FBX 文件: - 确保 FBX 文件确实包含动画数据 - 尝试在其他软件中验证动画 """) msg.exec_() except Exception as e: print(f"[手动转换] 转换失败: {e}") QMessageBox.warning(None, "转换失败", f"转换过程中出现错误: {e}") def _systemConvertFBX(self, fbx_path, progress=None): """使用系统工具转换 FBX""" import os import subprocess import tempfile try: # 准备输出路径 base_name = os.path.splitext(os.path.basename(fbx_path))[0] output_dir = os.path.dirname(fbx_path) gltf_path = os.path.join(output_dir, f"{base_name}_converted.gltf") bam_path = os.path.join(output_dir, f"{base_name}_converted.bam") if progress: progress.setValue(20) progress.setLabelText("检查转换工具...") # 方法1: 使用 gltf2bam 的逆向功能(如果支持) try: # 首先尝试看看是否有直接的 FBX 支持 result = subprocess.run(['gltf2bam', '--help'], capture_output=True, text=True, timeout=10) print(f"[系统转换] gltf2bam 可用") except: print(f"[系统转换] gltf2bam 不可用") if progress: progress.setValue(40) progress.setLabelText("尝试 Blender 转换...") # 方法2: 使用 Blender 无头模式转换 try: # 创建 Blender 转换脚本 script_content = f''' import bpy import sys import os # 清理默认场景 bpy.ops.object.select_all(action='SELECT') bpy.ops.object.delete(use_global=False) # 导入 FBX try: bpy.ops.import_scene.fbx(filepath="{fbx_path}") print("FBX导入成功") # 导出为 glTF bpy.ops.export_scene.gltf( filepath="{gltf_path}", export_animations=True, export_force_sampling=True, export_frame_range=True ) print("glTF导出成功") except Exception as e: print(f"转换失败: {{e}}") sys.exit(1) ''' temp_script = tempfile.mktemp(suffix='.py') with open(temp_script, 'w') as f: f.write(script_content) if progress: progress.setValue(60) progress.setLabelText("执行 Blender 转换...") # 执行 Blender 转换 result = subprocess.run([ 'blender', '--background', '--python', temp_script ], capture_output=True, text=True, timeout=120) # 清理临时文件 if os.path.exists(temp_script): os.remove(temp_script) if result.returncode == 0 and os.path.exists(gltf_path): if progress: progress.setValue(80) progress.setLabelText("转换为 BAM 格式...") # 转换 glTF 为 BAM result2 = subprocess.run(['gltf2bam', gltf_path, bam_path], capture_output=True, text=True, timeout=60) if result2.returncode == 0 and os.path.exists(bam_path): print(f"[系统转换] 成功转换为: {bam_path}") return bam_path elif os.path.exists(gltf_path): print(f"[系统转换] 成功转换为: {gltf_path}") return gltf_path except subprocess.TimeoutExpired: print(f"[系统转换] Blender 转换超时") except FileNotFoundError: print(f"[系统转换] Blender 未安装") except Exception as e: print(f"[系统转换] Blender 转换出错: {e}") return None except Exception as e: print(f"[系统转换] 系统转换失败: {e}") return None def _playAnimation(self,origin_model): actor=self._getActor(origin_model) if not actor: return actor.setPos(origin_model.getPos()) actor.setHpr(origin_model.getHpr()) actor.setScale(origin_model.getScale()) origin_model.hide() actor.show() if hasattr(self,'animation_combo'): # 获取原始动画名称(存储在 userData 中) current_index = self.animation_combo.currentIndex() if current_index >= 0: original_name = self.animation_combo.itemData(current_index) display_name = self.animation_combo.currentText() if original_name: actor.play(original_name) print(f"『动画播放』:{display_name} (原始名称: {original_name})") else: # 兜底:使用显示名称 actor.play(display_name) print(f"『动画播放』:{display_name}") def _pauseAnimation(self,origin_model): actor = self._getActor(origin_model) if not actor: return actor.setPos(origin_model.getPos()) actor.setHpr(origin_model.getHpr()) actor.setScale(origin_model.getScale()) origin_model.hide() actor.show() actor.stop() print("『动画』暂停") def _stopAnimation(self,origin_model): actor = self._getActor(origin_model) if not actor: return actor.stop() # 获取原始动画名称 current_index = self.animation_combo.currentIndex() if current_index >= 0: original_name = self.animation_combo.itemData(current_index) display_name = self.animation_combo.currentText() anim_name = original_name if original_name else display_name if anim_name and actor.getAnimControl(anim_name): actor.getAnimControl(anim_name).pose(0) actor.hide() origin_model.show() print("『动画』停止切换至原始模型") def _loopAnimation(self,origin_model): actor = self._getActor(origin_model) if not actor: return actor.setPos(origin_model.getPos()) actor.setHpr(origin_model.getHpr()) actor.setScale(origin_model.getScale()) origin_model.hide() actor.show() # 获取原始动画名称 current_index = self.animation_combo.currentIndex() if current_index >= 0: original_name = self.animation_combo.itemData(current_index) display_name = self.animation_combo.currentText() anim_name = original_name if original_name else display_name if anim_name: actor.loop(anim_name) print(f"[动画] 循环: {display_name} (原始名称: {anim_name})") def _setAnimationSpeed(self, origin_model, speed): """ 设置当前动画的播放倍速。 """ actor = self._getActor(origin_model) if not actor: return # 获取原始动画名称 current_index = self.animation_combo.currentIndex() if current_index >= 0: original_name = self.animation_combo.itemData(current_index) display_name = self.animation_combo.currentText() anim_name = original_name if original_name else display_name if anim_name: actor.setPlayRate(speed, anim_name) origin_model.setPythonTag("anim_speed",speed) print(f"[动画] 速度设为: {speed} ({display_name})") def _dispatchAnimCommand(self,origin_model,cmd): cache = self._actor_cache.get(origin_model) if not cache: return kind,player = cache if kind == "actor": actor=player anim_name = self.animation_combo.currentText() actor.setPos(origin_model.getPos()) actor.setHpr(origin_model.getHpr()) actor.setScale(origin_model.getScale()) if cmd == "play": origin_model.hide() actor.show() actor.play(anim_name) elif cmd == "pause": origin_model.hide() actor.show() actor.stop() elif cmd == "stop": actor.stop() if anim_name and actor.getAnimControl(anim_name): actor.getAnimControl(anim_name).pose(0) actor.hide();origin_model.show() elif cmd == "loop": origin_model.hide() actor.show() actor.loop(anim_name) elif isinstance(cmd,tuple) and cmd[0] == "speed": actor.setPlayRate(cmd[1], anim_name) def removeActorForModel(self, model): """删除 model 对应的 Actor(如果存在)""" actor = self._actor_cache.pop(model, None) if actor: actor.stop() actor.cleanup() actor.removeNode()