import os from collections import deque from traceback import print_exc, print_last from types import new_class from typing import Hashable from PyQt5.QtGui import QColor from PyQt5.QtWidgets import (QLabel, QLineEdit, QDoubleSpinBox, QPushButton, QTreeWidget, QTreeWidgetItem, QMenu, QCheckBox, QComboBox, QHBoxLayout, QWidget, QVBoxLayout, QGroupBox, QGridLayout, QSpinBox, QFileDialog, QMessageBox, QSizePolicy) from PyQt5.QtCore import Qt, QTimer from deploy_libs.unicodedata import normalize from direct.actor.Actor import Actor from direct.gui import DirectGui from direct.task.TaskManagerGlobal import taskMgr from idna import check_label from jinja2.compiler import has_safe_repr from panda3d.core import Vec3, Vec4, transpose, TransparencyAttrib, PartGroup, ColorAttrib, NodePath, Point3 from scene import util from direct.gui.DirectGui import DirectLabel, DirectFrame from panda3d.core import TextNode from ui.icon_manager import get_icon_manager, has_icon, get_icon class PropertyPanelManager: """属性面板管理器""" def __init__(self, world): """初始化属性面板管理器""" self.world = world self._propertyLayout = None self._actor_cache = {} self._spherical_video_controls = {} self._transform_monitor_timer = None self._last_transform_values = {} self.column_minimum_width = 85 # 初始化地形编辑参数 if not hasattr(self.world, 'terrain_edit_radius'): self.world.terrain_edit_radius = 3.0 if not hasattr(self.world, 'terrain_edit_strength'): self.world.terrain_edit_strength = 0.3 if not hasattr(self.world, 'terrain_edit_operation'): # 这里原来是 terrain_edit_opertaion self.world.terrain_edit_operation = "add" # 初始化碰撞参数加载标志位 self._loading_collision_params = False # 定义现代化紧凑样式 self.compact_style = """ QDoubleSpinBox, QSpinBox { min-width: 60px; background-color: rgba(89, 100, 113, 0.15); border: 1px solid rgba(76, 92, 110, 0.4); border-radius: 4px; padding: 4px 6px; font-size: 10px; } QDoubleSpinBox:focus, QSpinBox:focus { border: 1px solid #4d74bd; background-color: rgba(77, 116, 189, 0.1); } QDoubleSpinBox::up-button, QSpinBox::up-button { background-color: rgba(77, 116, 189, 0.2); border: none; border-radius: 2px; width: 14px; subcontrol-origin: border; subcontrol-position: top right; } QDoubleSpinBox::down-button, QSpinBox::down-button { background-color: rgba(77, 116, 189, 0.2); border: none; border-radius: 2px; width: 14px; subcontrol-origin: border; subcontrol-position: bottom right; } QDoubleSpinBox::up-button:hover, QSpinBox::up-button:hover, QDoubleSpinBox::down-button:hover, QSpinBox::down-button:hover { background-color: rgba(77, 116, 189, 0.4); } QDoubleSpinBox::up-arrow, QSpinBox::up-arrow { image: url(icons/up_arrows.png); width: 10px; height: 10px; } QDoubleSpinBox::down-arrow, QSpinBox::down-arrow { image: url(icons/down_arrows.png); width: 10px; height: 10px; } QDoubleSpinBox::up-arrow:hover, QSpinBox::up-arrow:hover { image: url(icons/up_arrows.png); } QDoubleSpinBox::down-arrow:hover, QSpinBox::down-arrow:hover { image: url(icons/down_arrows.png); } QPushButton { min-width: 60px; background-color: rgba(89, 100, 113, 0.4); color: rgba(255, 255, 255, 0.8); border: 1px solid rgba(76, 92, 110, 0.4); border-radius: 4px; padding: 6px 10px; font-size: 10px; font-weight: 400; } QPushButton:hover { background-color: rgba(89, 100, 113, 0.6); border: 1px solid rgba(77, 116, 189, 0.6); color: #ffffff; } QPushButton:pressed, QPushButton:checked { background-color: rgba(77, 116, 189, 0.8); border: 1px solid #4d74bd; color: #ffffff; } QPushButton:disabled { background-color: rgba(89, 100, 113, 0.2); color: rgba(235, 235, 235, 0.4); border: 1px solid rgba(76, 92, 110, 0.2); } QComboBox { min-width: 80px; background-color: rgba(89, 100, 113, 0.15); border: 1px solid rgba(76, 92, 110, 0.4); border-radius: 4px; padding: 4px 6px; font-size: 10px; } QComboBox:focus { border: 1px solid #4d74bd; } QComboBox::drop-down { border: none; border-left: 1px solid rgba(76, 92, 110, 0.4); background-color: rgba(77, 116, 189, 0.2); border-radius: 0 4px 4px 0; width: 16px; } QComboBox::drop-down:hover { background-color: rgba(77, 116, 189, 0.4); } QComboBox::down-arrow { image: url(icons/down_arrows.png); width: 10px; height: 10px; } QLineEdit { min-width: 80px; background-color: rgba(89, 100, 113, 0.15); border: 1px solid rgba(76, 92, 110, 0.4); border-radius: 4px; padding: 4px 6px; font-size: 10px; } QLineEdit:focus { border: 1px solid #4d74bd; background-color: rgba(77, 116, 189, 0.1); } QCheckBox { min-width: 20px; font-size: 10px; spacing: 6px; } QCheckBox::indicator { width: 14px; height: 14px; border: 1px solid rgba(76, 92, 110, 0.6); border-radius: 3px; background-color: rgba(89, 100, 113, 0.15); } QCheckBox::indicator:checked { background-color: #4d74bd; border: 1px solid #4d74bd; } """ self.compact_style += """ QGroupBox { margin-left: 0px; padding-left: 0px; } QGroupBox::title { left: 0px; padding: 8px 5px 5px 0; margin-top: 0px; } """ # 定义现代化状态徽章样式 self.badge_style_blue = """ QLabel { background-color: rgba(77, 116, 189, 0.15); border: 1px solid rgba(77, 116, 189, 0.6); color: #4d74bd; border-radius: 6px; padding: 4px 10px; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 9px; font-weight: 500; letter-spacing: 0.5px; } """ self.badge_style_green = """ QLabel { background-color: rgba(45, 255, 196, 0.15); border: 1px solid rgba(45, 255, 196, 0.6); color: #2dffc4; border-radius: 6px; padding: 4px 10px; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 9px; font-weight: 500; letter-spacing: 0.5px; } """ # 添加新的徽章样式 self.badge_style_orange = """ QLabel { background-color: rgba(255, 165, 0, 0.15); border: 1px solid rgba(255, 165, 0, 0.6); color: #ffa500; border-radius: 6px; padding: 4px 10px; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 9px; font-weight: 500; letter-spacing: 0.5px; } """ self.badge_style_red = """ QLabel { background-color: rgba(255, 99, 99, 0.15); border: 1px solid rgba(255, 99, 99, 0.6); color: #ff6363; border-radius: 6px; padding: 4px 10px; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 9px; font-weight: 500; letter-spacing: 0.5px; } """ # 固定宽度的徽章样式(用于碰撞检测状态) self.badge_style_green_fixed = """ QLabel { background-color: rgba(45, 255, 196, 0.15); border: 1px solid rgba(45, 255, 196, 0.6); color: #2dffc4; border-radius: 6px; padding: 4px 10px; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 9px; font-weight: 500; letter-spacing: 0.5px; min-width: 50px; max-width: 50px; text-align: center; } """ self.badge_style_red_fixed = """ QLabel { background-color: rgba(255, 99, 99, 0.15); border: 1px solid rgba(255, 99, 99, 0.6); color: #ff6363; border-radius: 6px; padding: 4px 10px; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 9px; font-weight: 500; letter-spacing: 0.5px; min-width: 50px; max-width: 50px; text-align: center; } """ def createStatusBadge(self, text, badge_type="green"): """创建现代化状态徽章 Args: text: 徽章文字 badge_type: 徽章类型,支持 "green", "blue", "orange", "red" Returns: QLabel: 配置好样式的标签 """ badge = QLabel(text) if badge_type == "green": badge.setStyleSheet(self.badge_style_green) elif badge_type == "blue": badge.setStyleSheet(self.badge_style_blue) elif badge_type == "orange": badge.setStyleSheet(self.badge_style_orange) elif badge_type == "red": badge.setStyleSheet(self.badge_style_red) else: badge.setStyleSheet(self.badge_style_blue) # 默认蓝色 return badge def createFixedStatusBadge(self, text, badge_type="green"): """创建固定宽度的状态徽章(用于碰撞检测等需要保持一致宽度的场景) Args: text: 徽章文字 badge_type: 徽章类型,支持 "green", "red" Returns: QLabel: 配置好固定宽度样式的标签 """ badge = QLabel(text) badge.setAlignment(Qt.AlignCenter) # 确保文字居中 if badge_type == "green": badge.setStyleSheet(self.badge_style_green_fixed) elif badge_type == "red": badge.setStyleSheet(self.badge_style_red_fixed) else: badge.setStyleSheet(self.badge_style_green_fixed) # 默认绿色 return badge def createModernButton(self, text, button_type="default"): """创建现代化按钮 Args: text: 按钮文字 button_type: 按钮类型 - "default", "primary", "success", "warning", "danger" Returns: QPushButton: 配置好样式的按钮 """ button = QPushButton(text) if button_type == "primary": button.setStyleSheet(""" QPushButton { background-color: rgba(77, 116, 189, 0.8); color: #ffffff; border: 1px solid #4d74bd; border-radius: 4px; padding: 8px 12px; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 10px; font-weight: 500; min-height: 16px; } QPushButton:hover { background-color: rgba(77, 116, 189, 1.0); border: 1px solid rgba(77, 116, 189, 1.0); } QPushButton:pressed { background-color: rgba(61, 96, 169, 1.0); border: 1px solid rgba(61, 96, 169, 1.0); } """) elif button_type == "success": button.setStyleSheet(""" QPushButton { background-color: rgba(45, 255, 196, 0.15); color: #2dffc4; border: 1px solid rgba(45, 255, 196, 0.6); border-radius: 4px; padding: 8px 12px; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 10px; font-weight: 400; min-height: 16px; } QPushButton:hover { background-color: rgba(45, 255, 196, 0.25); border: 1px solid #2dffc4; } QPushButton:pressed { background-color: rgba(45, 255, 196, 0.4); color: #ffffff; } """) elif button_type == "warning": button.setStyleSheet(""" QPushButton { background-color: rgba(255, 165, 0, 0.15); color: #ffa500; border: 1px solid rgba(255, 165, 0, 0.6); border-radius: 4px; padding: 8px 12px; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 10px; font-weight: 400; min-height: 16px; } QPushButton:hover { background-color: rgba(255, 165, 0, 0.25); border: 1px solid #ffa500; } QPushButton:pressed { background-color: rgba(255, 165, 0, 0.4); color: #ffffff; } """) elif button_type == "danger": button.setStyleSheet(""" QPushButton { background-color: rgba(255, 99, 99, 0.15); color: #ff6363; border: 1px solid rgba(255, 99, 99, 0.6); border-radius: 4px; padding: 8px 12px; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 10px; font-weight: 400; min-height: 16px; } QPushButton:hover { background-color: rgba(255, 99, 99, 0.25); border: 1px solid #ff6363; } QPushButton:pressed { background-color: rgba(255, 99, 99, 0.4); color: #ffffff; } """) else: # default # 使用全局默认样式 pass return button def _collision_button_style(self): """返回碰撞检测面板按钮统一样式""" return """ QPushButton { background-color: rgba(89, 98, 118, 0.4); color: rgba(235, 235, 235, 0.85); border: 1px solid rgba(89, 98, 118, 0.65); border-radius: 4px; padding: 8px 12px; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 10px; font-weight: 400; min-height: 16px; } QPushButton:hover { background-color: rgba(89, 98, 118, 0.55); border: 1px solid rgba(89, 98, 118, 0.9); color: #ffffff; } QPushButton:pressed { background-color: rgba(89, 98, 118, 0.7); border: 1px solid rgba(89, 98, 118, 0.95); color: #ffffff; } QPushButton:disabled { background-color: rgba(89, 98, 118, 0.18); border: 1px solid rgba(89, 98, 118, 0.25); color: rgba(235, 235, 235, 0.35); } """ def createModernGroupBox(self, title, parent_layout): """创建现代化的分组框 Args: title: 分组框标题 parent_layout: 父布局 Returns: tuple: (QGroupBox, QVBoxLayout) 分组框和其内部布局 """ group_box = QGroupBox(title) group_layout = QVBoxLayout(group_box) group_layout.setSpacing(0) # 减少内部间距 group_layout.setContentsMargins(0, 3, 0, 6) # 减少内容边距 # 应用现代化样式,使用更紧凑的间距 group_box.setStyleSheet(""" QGroupBox { font-weight: 500; font-size: 11px; color: rgba(255, 255, 255, 0.9); margin-top: 12px; margin-bottom: 4px; padding-top: 15px; border-top: 1px solid rgba(77, 116, 189, 0.3); } QGroupBox::title { subcontrol-origin: padding; subcontrol-position: top left; left: 0px; padding: 8px 5px 5px 0; margin-top: 0px; } """) parent_layout.addWidget(group_box) return group_box, group_layout def _getFigmaSpinBoxStyle(self): """获取符合Figma设计的SpinBox样式""" return """ QDoubleSpinBox, QSpinBox { background-color: rgba(89, 100, 113, 0.2); color: #ebebeb; border: 1px solid rgba(76, 92, 110, 0.6); border-radius: 2px; padding: 3px 6px; font-family: 'Inter', 'Microsoft YaHei', sans-serif; font-size: 10px; font-weight: 300; letter-spacing: 0.5px; min-width: 70px; max-width: 70px; min-height: 18px; max-height: 18px; } QDoubleSpinBox:focus, QSpinBox:focus { border: 1px solid #4d74bd; background-color: rgba(77, 116, 189, 0.1); } QDoubleSpinBox:hover, QSpinBox:hover { border: 1px solid rgba(77, 116, 189, 0.6); background-color: rgba(89, 100, 113, 0.25); } QDoubleSpinBox::up-button, QSpinBox::up-button { background-color: rgba(77, 116, 189, 0.2); border: none; border-radius: 2px; width: 14px; subcontrol-origin: border; subcontrol-position: top right; } QDoubleSpinBox::down-button, QSpinBox::down-button { background-color: rgba(77, 116, 189, 0.2); border: none; border-radius: 2px; width: 14px; subcontrol-origin: border; subcontrol-position: bottom right; } QDoubleSpinBox::up-button:hover, QSpinBox::up-button:hover, QDoubleSpinBox::down-button:hover, QSpinBox::down-button:hover { background-color: rgba(77, 116, 189, 0.4); } QDoubleSpinBox::up-arrow, QSpinBox::up-arrow { image: url(icons/up_arrows.png); width: 10px; height: 10px; } QDoubleSpinBox::down-arrow, QSpinBox::down-arrow { image: url(icons/down_arrows.png); width: 10px; height: 10px; } """ def createPropertyRow(self, label_text, widget, layout, tooltip=None): """创建属性行 Args: label_text: 标签文字 widget: 控件 layout: 布局 tooltip: 工具提示 """ row_layout = QHBoxLayout() row_layout.setContentsMargins(0, 0, 0, 0) row_layout.setSpacing(8) # 创建标签 label = QLabel(label_text) label.setMinimumWidth(60) label.setStyleSheet(""" QLabel { color: rgba(255, 255, 255, 0.8); font-size: 10px; font-weight: 300; } """) if tooltip: label.setToolTip(tooltip) widget.setToolTip(tooltip) row_layout.addWidget(label) row_layout.addWidget(widget) row_layout.addStretch() layout.addLayout(row_layout) 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): """清空属性面板""" # 停止变换监控 self.stopTransformMonitoring() 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) self._syncSceneVisibility() def _syncSceneVisibility(self): scene_root = self.world.render self._syncEffectiveVisibility(scene_root) def updatePropertyPanel(self, item): """更新属性面板显示""" # if not self._propertyLayout or not self._propertyLayout.parent(): # print("属性布局未设置或没有父部件!") # return #更健壮的有效性检查 self._cleanupAllReferences() 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("物体名称") self.name_group.setProperty("groupRole", "first") name_layout = QGridLayout() self.active_check = QCheckBox() # 根据模型的实际可见性状态设置复选框 self.active_check.setChecked(user_visible) self.active_check.stateChanged.connect(lambda state: self._toggleModelVisibility(model, state)) self.name_input = QLineEdit(itemText) self.name_input.returnPressed.connect( lambda: self.world.treeWidget.update_item_name(self.name_input.text(), item) ) name_layout.addWidget(self.active_check, 0, 0) name_layout.setColumnMinimumWidth(0, self.column_minimum_width) name_layout.addWidget(self.name_input, 0, 1, 1, 3) 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) if self._isTerrainNode(model, item): self._showTerrainProperties(model, item) elif model and hasattr(model, 'getTag') and model.getTag("element_type") == "cesium_tileset": self._showCesiumTilesetProperties(model, item) elif model and hasattr(model, 'getTag') and model.getTag("gui_type"): self.updateGUIPropertyPanel(model, item) elif model and hasattr(model, 'getTag') and model.getTag("light_type"): self.updateLightPropertyPanel(model) elif model: self._updateModelPropertyPanel(model) # 启动变换监控 self.startTransformMonitoring(model) self._propertyLayout.addStretch() # 重置碰撞相关标志,确保下次选择时正常显示 self._just_added_collision = False # 强制更新布局 if self._propertyLayout: self._propertyLayout.update() propertyWidget = self._propertyLayout.parentWidget() if propertyWidget: propertyWidget.update() def _isTerrainNode(self, node, item): """检查是否是地形节点""" item_data = item.data(0, Qt.UserRole + 1) if item_data == "terrain": return True if hasattr(self.world, 'terrain_manager') and self.world.terrain_manager.terrains: for terrain_info in self.world.terrain_manager.terrains: if terrain_info['node'] == node: return True return False def _showTerrainProperties(self, terrain_node, item): """显示地形属性面板""" try: terrain_info = None if hasattr(self.world, 'terrain_manager'): for info in self.world.terrain_manager.terrains: if info['node'] == terrain_node: terrain_info = info break if not terrain_info: no_info_label = QLabel("未找到地形信息") no_info_label.setStyleSheet("color:red;font-weight:bold;") self._propertyLayout.addWidget(no_info_label) return info_group = QGroupBox("地形信息") info_layout = QGridLayout() info_layout.setColumnMinimumWidth(0, self.column_minimum_width) info_layout.addWidget(QLabel("名称:"), 0, 0) name_label = QLabel(terrain_info.get('name', '未知')) info_layout.addWidget(name_label, 0, 1, 1, 3) info_layout.addWidget(QLabel("类型:"), 1, 0) type_label = QLabel("高度图地形" if terrain_info.get('heightmap') else "平面地形") info_layout.addWidget(type_label, 1, 1, 1, 3) if terrain_info.get('heightmap'): info_layout.addWidget(QLabel("高度图:"), 2, 0) heightmap_label = QLabel(os.path.basename(terrain_info['heightmap'])) heightmap_label.setWordWrap(True) info_layout.addWidget(heightmap_label, 2, 1, 1, 3) info_group.setLayout(info_layout) self._propertyLayout.addWidget(info_group) # 变换属性 self._updateTerrainTransformPanel(terrain_node) # 地形编辑控制面板 self._createTerrainEditPanel(terrain_info) # 材质属性 self._updateTerrainMaterialPanel(terrain_node, terrain_info) # #删除按钮 # delete_btn = QPushButton("删除地形") # delete_btn.setStyleSheet(""" # QPushButton{ # background-color:#ff4444; # color:white; # border:none; # padding:8px; # border-radius:4px; # margin-top:10px # } # QPushButton:hover{ # background-color:#ff6666; # } # """) # delete_btn.clicked.connect(lambda:self._deleteTerrain(terrain_info,item)) # self._propertyLayout.addWidget(delete_btn) except Exception as e: print(f"显示地形属性时出错: {e}") import traceback traceback.print_exc() def _updateTerrainTransformPanel(self, terrain_node): """更新地形变化属性面板""" try: transform_group = QGroupBox("变换 Transform") transform_layout = QGridLayout() transform_layout.setColumnMinimumWidth(0, self.column_minimum_width) pos = terrain_node.getPos() scale = terrain_node.getScale() transform_layout.addWidget(QLabel("位置"), 0, 0) position_row = QHBoxLayout() position_row.setContentsMargins(0, 0, 0, 0) position_row.setSpacing(4) 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(pos.getX()) self.pos_y.setValue(pos.getY()) self.pos_z.setValue(pos.getZ()) def updateXPosition(value): terrain_node.setX(value) self.refreshModelValues(terrain_node) self.pos_x.valueChanged.connect(updateXPosition) def updateYPosition(value): terrain_node.setY(value) self.refreshModelValues(terrain_node) self.pos_y.valueChanged.connect(updateYPosition) def updateZPosition(value): terrain_node.setZ(value) self.refreshModelValues(terrain_node) self.pos_z.valueChanged.connect(updateZPosition) for axis_label, widget in (("X:", self.pos_x), ("Y:", self.pos_y), ("Z:", self.pos_z)): axis = QLabel(axis_label) axis.setAlignment(Qt.AlignVCenter) axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) position_row.addWidget(axis) position_row.addWidget(widget) transform_layout.addLayout(position_row, 0, 1, 1, 3) transform_layout.addWidget(QLabel("旋转"), 1, 0) rotation_row = QHBoxLayout() rotation_row.setContentsMargins(0, 0, 0, 0) rotation_row.setSpacing(4) self.rot_x = QDoubleSpinBox() self.rot_y = QDoubleSpinBox() self.rot_z = QDoubleSpinBox() for rot_widget in [self.rot_x, self.rot_y, self.rot_z]: rot_widget.setRange(-360, 360) self.rot_x.setValue(terrain_node.getH()) self.rot_y.setValue(terrain_node.getP()) self.rot_z.setValue(terrain_node.getR()) self.rot_x.valueChanged.connect(lambda v: terrain_node.setH(v)) self.rot_y.valueChanged.connect(lambda v: terrain_node.setP(v)) self.rot_z.valueChanged.connect(lambda v: terrain_node.setR(v)) for axis_label, widget in (("H:", self.rot_x), ("P:", self.rot_y), ("R:", self.rot_z)): axis = QLabel(axis_label) axis.setAlignment(Qt.AlignVCenter) axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) rotation_row.addWidget(axis) rotation_row.addWidget(widget) transform_layout.addLayout(rotation_row, 1, 1, 1, 3) transform_layout.addWidget(QLabel("缩放"), 2, 0) scale_row = QHBoxLayout() scale_row.setContentsMargins(0, 0, 0, 0) scale_row.setSpacing(4) 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(-1000, 1000) scale_widget.setSingleStep(0.1) self.scale_x.setValue(scale.getX()) self.scale_y.setValue(scale.getY()) self.scale_z.setValue(scale.getZ()) self.scale_x.valueChanged.connect(lambda v: self._updateXScale(terrain_node, v)) self.scale_y.valueChanged.connect(lambda v: self._updateYScale(terrain_node, v)) self.scale_z.valueChanged.connect(lambda v: self._updateZScale(terrain_node, v)) for axis_label, widget in (("X:", self.scale_x), ("Y:", self.scale_y), ("Z:", self.scale_z)): axis = QLabel(axis_label) axis.setAlignment(Qt.AlignVCenter) axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) scale_row.addWidget(axis) scale_row.addWidget(widget) transform_layout.addLayout(scale_row, 2, 1, 1, 3) transform_group.setLayout(transform_layout) self._propertyLayout.addWidget(transform_group) except Exception as e: print(f"更新地形变换面板时出错: {e}") def _createTerrainEditPanel(self, terrain_info): try: edit_group = QGroupBox("地形编辑") edit_layout = QGridLayout() edit_layout.setColumnMinimumWidth(0, self.column_minimum_width) # 编辑半径 edit_layout.addWidget(QLabel("编辑半径:"), 0, 0) self.terrain_radius_spin = QDoubleSpinBox() self.terrain_radius_spin.setRange(0.1, 50.0) self.terrain_radius_spin.setSingleStep(0.5) self.terrain_radius_spin.setValue(getattr(self.world, 'terrain_edit_radius', 3.0)) self.terrain_radius_spin.valueChanged.connect(self._onTerrainRadiusChanged) edit_layout.addWidget(self.terrain_radius_spin, 0, 1, 1, 3) # 编辑强度 edit_layout.addWidget(QLabel("编辑强度:"), 1, 0) self.terrain_strength_spin = QDoubleSpinBox() self.terrain_strength_spin.setRange(0.01, 50) self.terrain_strength_spin.setSingleStep(0.1) self.terrain_strength_spin.setValue(getattr(self.world, 'terrain_edit_strength', 0.3)) self.terrain_strength_spin.valueChanged.connect(self._onTerrainStrengthChanged) edit_layout.addWidget(self.terrain_strength_spin, 1, 1, 1, 3) edit_layout.addWidget(QLabel("操作类型:"), 2, 0) self.terrain_operation_combo = QComboBox() self.terrain_operation_combo.addItems(["升高地形", "降低地形", "平坦化"]) current_op = getattr(self.world, 'terrain_edit_operation', "add") if current_op == "add": self.terrain_operation_combo.setCurrentText("升高地形") elif current_op == "subtract": self.terrain_operation_combo.setCurrentText("降低地形") else: self.terrain_operation_combo.setCurrentText("平坦化") self.terrain_operation_combo.currentTextChanged.connect(self._onTerrainOperationChanged) edit_layout.addWidget(self.terrain_operation_combo, 2, 1, 1, 3) help_label = QLabel("在地形编辑模式下,使用鼠标左键升高地形,右键降低地形") help_label.setWordWrap(True) help_label.setStyleSheet("font-size:10px;color:gray;") edit_layout.addWidget(help_label, 3, 0, 1, 4) edit_group.setLayout(edit_layout) self._propertyLayout.addWidget(edit_group) # 确保初始值被正确设置到 world 对象上 self._onTerrainRadiusChanged(self.terrain_radius_spin.value()) self._onTerrainStrengthChanged(self.terrain_strength_spin.value()) self._onTerrainOperationChanged(self.terrain_operation_combo.currentText()) except Exception as e: print(f"创建地形编辑面板时出错: {e}") def _onTerrainRadiusChanged(self, value): """地形编辑半径改变""" if hasattr(self.world, 'terrain_edit_radius'): self.world.terrain_edit_radius = float(value) def _onTerrainStrengthChanged(self, value): """地形编辑强度改变""" if hasattr(self.world, 'terrain_edit_strength'): self.world.terrain_edit_strength = float(value) def _onTerrainOperationChanged(self, text): """地形编辑操作类型改变""" operation_map = { "升高地形": "add", "降低地形": "subtract", "平坦化": "set" } if hasattr(self.world, 'terrain_edit_operation'): # 正确设置地形编辑操作属性 self.world.terrain_edit_operation = operation_map.get(text, "add") print(f"地形编辑操作已设置为: {self.world.terrain_edit_operation}") def _updateTerrainMaterialPanel(self, terrain_node, terrain_info): """更新地形材质属性面板""" try: material_group = QGroupBox("材质属性") material_layout = QGridLayout() material_layout.setColumnMinimumWidth(0, self.column_minimum_width) # 颜色设置 material_layout.addWidget(QLabel("颜色:"), 0, 0) color_button = QPushButton("选择颜色") color_button.clicked.connect(lambda: self._selectTerrainColor(terrain_info)) material_layout.addWidget(color_button, 0, 1, 1, 3) # 纹理设置 material_layout.addWidget(QLabel("纹理:"), 1, 0) texture_button = QPushButton("选择纹理") texture_button.clicked.connect(lambda: self._selectTerrainTexture(terrain_info)) material_layout.addWidget(texture_button, 1, 1, 1, 3) material_group.setLayout(material_layout) self._propertyLayout.addWidget(material_group) except Exception as e: print(f"更新材质面板时出错{e}") def _selectTerrainColor(self, terrain_info): try: from PyQt5.QtWidgets import QColorDialog from PyQt5.QtGui import QColor current_color = QColor(255, 255, 255) color = QColorDialog.getColor(current_color, None, "选择地形颜色") if color.isValid(): r, g, b = color.red() / 255.0, color.green() / 255.0, color.blue() / 255.0 if hasattr(self.world, 'terrain_manager'): self.world.terrain_manager.setTerrainColor(terrain_info, (r, g, b)) except Exception as e: print(f"选择地形颜色时出错: {e}") def _selectTerrainTexture(self, terrain_info): """选择地形纹理""" try: from PyQt5.QtWidgets import QFileDialog file_path, _ = QFileDialog.getOpenFileName( None, "选择地形纹理", "", "图像文件(*.png *.jpg *.jpeg *.bmp *.tga *.dds)" ) if file_path and os.path.exists(file_path): if hasattr(self.world, 'terrain_manager'): success = self.world.terrain_manager.setTerrainTexture(terrain_info, file_path) if success: print(f"地形纹理已应用{file_path}") else: print(f"应用地形纹理失败") except Exception as e: print(f"选择地形纹理时出错: {e}") def _deleteTerrain(self, terrain_info, item): """删除地形""" try: from PyQt5.QtWidgets import QMessageBox reply = QMessageBox.question( None, '确认删除', f'确定要删除地形 "{terrain_info.get("name", "未知地形")}" 吗?', QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply == QMessageBox.Yes: # 调用 interface_manager 中已经实现的 deleteNode 方法 if hasattr(self.world, 'interface_manager') and self.world.interface_manager.treeWidget: self.world.interface_manager.deleteNode(terrain_info['node'], item) else: # 如果 interface_manager 不可用,使用原来的删除逻辑 if hasattr(self.world, 'terrain_manager'): success = self.world.terrain_manager.deleteTerrain(terrain_info) if success: # 更新场景树 # if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, # 'updateSceneTree'): # self.world.scene_manager.updateSceneTree() # # # 清空属性面板 # self.clearPropertyPanel() print(f"✓ 地形已删除: {terrain_info.get('name', '未知')}") else: print("✗ 删除地形失败") except Exception as e: print(f"删除地形时出错: {e}") def _cleanupAllReferences(self): """清理所有控件引用""" # 停止变换监控 self.stopTransformMonitoring() # 清理变换控件引用 self._cleanupTransformControls() # 清理信息面板监控 if hasattr(self, '_info_panel_timer'): self._info_panel_timer.stop() self._info_panel_timer.deleteLater() del self._info_panel_timer # 清理信息面板控件引用 if hasattr(self, '_info_panel_controls'): self._info_panel_controls.clear() del self._info_panel_controls if hasattr(self, '_current_info_panel'): del self._current_info_panel # 清理碰撞相关控件引用 collision_controls = [ 'collision_status_badge', 'collision_shape_combo', 'collision_shape_label', 'collision_visibility_button', 'collision_button', 'collision_layout', 'collision_group', 'collision_pos_x', 'collision_pos_y', 'collision_pos_z', 'collision_radius', 'collision_width', 'collision_length', 'collision_height', 'collision_capsule_radius', 'collision_capsule_height', 'collision_normal_x', 'collision_normal_y', 'collision_normal_z' ] for name in collision_controls: if hasattr(self, name): delattr(self, name) # 清理其他可能的控件引用 other_controls = ['scale_x', 'scale_y', 'scale_z', 'pos_x', 'pos_y', 'pos_z'] for name in other_controls: if hasattr(self, name): setattr(self, name, None) def _setUserVisible(self, node, visible): node.setPythonTag("user_visible", visible) self._syncEffectiveVisibility(node) def _setUserVisible_light(self, node, visible): """设置用户可见性状态""" try: # 保存可见性状态 node.setPythonTag("user_visible", visible) except Exception as e: print(f"设置用户可见性失败: {e}") 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: # 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)}") # import traceback # traceback.print_exc() def _toggleModelVisibility(self, model, state): """切换模型可见性状态""" try: # 特殊处理灯光对象 scene_manager = None if hasattr(self.world, 'scene_manager'): scene_manager = self.world.scene_manager if scene_manager and hasattr(scene_manager, 'isLightObject'): is_light = scene_manager.isLightObject(model) if is_light: visible = (state == Qt.Checked) self._setUserVisible_light(model, visible) if hasattr(scene_manager, 'toggleLightVisibility'): scene_manager.toggleLightVisibility(model, visible) return # 关键:这里必须return,避免执行下面的普通模型逻辑 else: 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)}") import traceback traceback.print_exc() def refreshModelValues(self, nodePath): """刷新模型值显示""" if not nodePath or self._propertyLayout is None: return # 检查面板是否仍然有效 if not self._isPropertyPanelValid(): return try: # 检查是否是GUI元素 is_gui_element = (hasattr(nodePath, 'getTag') and nodePath.getTag("is_gui_element") == "1") if is_gui_element: # 对于GUI元素,更新所有相关属性 self._refreshGUIElementValues(nodePath) else: # 对于普通3D模型,更新位置、旋转、缩放 self._refreshModelValues(nodePath) except Exception as e: print(f"刷新模型值显示失败: {e}") def startTransformMonitoring(self, nodePath): """开始监控节点的变换变化""" # 如果已有监控器在运行,先停止它 self.stopTransformMonitoring() # 保存初始变换值 self._saveCurrentTransformValues(nodePath) # 创建并启动定时器 self._transform_monitor_timer = QTimer() self._transform_monitor_timer.timeout.connect(lambda: self._checkTransformChanges(nodePath)) self._transform_monitor_timer.start(100) # 每100毫秒检查一次 def stopTransformMonitoring(self): """停止监控节点的变换变化""" if self._transform_monitor_timer: self._transform_monitor_timer.stop() self._transform_monitor_timer.deleteLater() self._transform_monitor_timer = None # 清除保存的变换值 self._last_transform_values.clear() def _saveCurrentTransformValues(self, nodePath): """保存当前节点的变换值""" try: pos = nodePath.getPos() hpr = nodePath.getHpr() scale = nodePath.getScale() self._last_transform_values = { 'pos': (pos.getX(), pos.getY(), pos.getZ()), 'hpr': (hpr.getX(), hpr.getY(), hpr.getZ()), 'scale': (scale.getX(), scale.getY(), scale.getZ()) } except Exception as e: print(f"保存变换值失败: {e}") def _checkTransformChanges(self, nodePath): """检查节点变换是否发生变化""" try: # 获取当前变换值 pos = nodePath.getPos() hpr = nodePath.getHpr() scale = nodePath.getScale() current_values = { 'pos': (pos.getX(), pos.getY(), pos.getZ()), 'hpr': (hpr.getX(), hpr.getY(), hpr.getZ()), 'scale': (scale.getX(), scale.getY(), scale.getZ()) } # 比较变换值是否发生变化 if current_values != self._last_transform_values: # 变换已更改,刷新属性面板 self.refreshModelValues(nodePath) # 更新保存的变换值 self._last_transform_values = current_values except Exception as e: print(f"检查变换变化失败: {e}") def _refreshGUIElementValues(self, gui_element): """刷新GUI元素值显示""" try: # 更新位置属性 if hasattr(self, 'pos_x') and self.pos_x: pos = gui_element.getPos() gui_type = gui_element.getTag("gui_type") if gui_type in ["button", "label", "entry", "2d_image"]: # 2D GUI组件使用屏幕坐标 logical_x = pos.getX() / 0.1 logical_z = pos.getZ() / 0.1 self._safeUpdateSpinBox('pos_x', logical_x) self._safeUpdateSpinBox('pos_z', logical_z) else: # 3D GUI组件使用世界坐标(包括 video_screen) pos = gui_element.getPos() # 修复:确保 video_screen 也能正确更新位置控件 if hasattr(self, 'pos_x') and self.pos_x: self._safeUpdateSpinBox('pos_x', pos.getX()) if hasattr(self, 'pos_y') and self.pos_y: self._safeUpdateSpinBox('pos_y', pos.getY()) if hasattr(self, 'pos_z') and self.pos_z: self._safeUpdateSpinBox('pos_z', pos.getZ()) # 更新缩放属性 if hasattr(self, 'scale_x') and self.scale_x: scale = gui_element.getScale() self._safeUpdateSpinBox('scale_x', scale.getX()) if hasattr(self, 'scale_y') and self.scale_y: self._safeUpdateSpinBox('scale_y', scale.getY()) if hasattr(self, 'scale_z') and self.scale_z: self._safeUpdateSpinBox('scale_z', scale.getZ()) # 更新旋转属性(如果存在) if hasattr(self, 'rot_x') and self.rot_x: hpr = gui_element.getHpr() self._safeUpdateSpinBox('rot_x', hpr.getX()) self._safeUpdateSpinBox('rot_y', hpr.getY()) self._safeUpdateSpinBox('rot_z', hpr.getZ()) except Exception as e: print(f"刷新GUI元素值显示失败: {e}") def _refreshModelValues(self, nodePath): """刷新普通模型值显示""" try: parent = nodePath.getParent() render = self.world.render relPos = nodePath.getPos(parent) if parent else nodePath.getPos() # 安全地更新位置控件 self._safeUpdateSpinBox('pos_x', relPos.getX()) self._safeUpdateSpinBox('pos_y', relPos.getY()) self._safeUpdateSpinBox('pos_z', relPos.getZ()) # 安全地更新世界位置控件 worldPos = nodePath.getPos(render) self._safeUpdateSpinBox('world_pos_x', worldPos.getX()) self._safeUpdateSpinBox('world_pos_y', worldPos.getY()) self._safeUpdateSpinBox('world_pos_z', worldPos.getZ()) # 安全地更新旋转控件(如果存在) if hasattr(self, 'rot_x') or hasattr(self, 'rot_y') or hasattr(self, 'rot_z'): hpr = nodePath.getHpr() self._safeUpdateSpinBox('rot_x', hpr.getX()) self._safeUpdateSpinBox('rot_y', hpr.getY()) self._safeUpdateSpinBox('rot_z', hpr.getZ()) # 安全地更新缩放控件(如果存在) if hasattr(self, 'scale_x') or hasattr(self, 'scale_y') or hasattr(self, 'scale_z'): scale = nodePath.getScale() self._safeUpdateSpinBox('scale_x', scale.getX()) self._safeUpdateSpinBox('scale_y', scale.getY()) self._safeUpdateSpinBox('scale_z', scale.getZ()) except Exception as e: print(f"刷新模型值显示失败: {e}") def _isPropertyPanelValid(self): """检查属性面板是否仍然有效""" try: # 检查布局是否仍然存在且有效 if not self._propertyLayout: return False # 检查父控件是否仍然存在 parent = self._propertyLayout.parent() if not parent: return False # 检查父控件是否仍然在窗口中 return parent.isVisible() except: return False def _safeUpdateSpinBox(self, attr_name, value): """安全地更新数值框""" try: spinbox = getattr(self, attr_name, None) if spinbox and not spinbox.isHidden(): # 检查控件是否仍然存在且可见 # 检查对象是否仍然有效 spinbox.blockSignals(True) spinbox.setKeyboardTracking(False) # 确保禁用键盘跟踪 spinbox.setValue(value) spinbox.blockSignals(False) except RuntimeError as e: # 对象已被删除 setattr(self, attr_name, None) print(f"警告: 数值框 {attr_name} 已被删除: {e}") except Exception as e: print(f"更新数值框 {attr_name} 时出错: {e}") # 在创建位置和变换控件时,增加安全检查 def _createTransformControls(self, nodePath): """创建变换控制控件""" try: # 清理旧的引用 self._cleanupTransformControls() # 创建新的控件引用 transform_layout = QGridLayout() # 位置控件 transform_layout.addWidget(QLabel("相对位置"), 0, 0) # X坐标 transform_layout.addWidget(QLabel("X:"), 1, 0) self.pos_x = QLineEdit() self.pos_x.setText(str(round(nodePath.getX(), 6))) self.pos_x.editingFinished.connect(lambda: self._onPositionEditFinished(nodePath, 'x')) transform_layout.addWidget(self.pos_x, 1, 1) # Y坐标 transform_layout.addWidget(QLabel("Y:"), 1, 2) self.pos_y = QLineEdit() self.pos_y.setText(str(round(nodePath.getY(), 6))) self.pos_y.editingFinished.connect(lambda: self._onPositionEditFinished(nodePath, 'y')) transform_layout.addWidget(self.pos_y, 1, 3) # Z坐标 transform_layout.addWidget(QLabel("Z:"), 1, 4) self.pos_z = QLineEdit() self.pos_z.setText(str(round(nodePath.getZ(), 6))) self.pos_z.editingFinished.connect(lambda: self._onPositionEditFinished(nodePath, 'z')) transform_layout.addWidget(self.pos_z, 1, 5) return transform_layout except Exception as e: print(f"创建变换控件时出错: {e}") return None def _onPositionEditFinished(self, nodePath, axis): """位置编辑完成时的处理""" try: # 检查控件是否仍然有效 if not hasattr(self, f'pos_{axis}') or getattr(self, f'pos_{axis}') is None: return line_edit = getattr(self, f'pos_{axis}') if line_edit is None or line_edit.isHidden(): return # 获取文本并转换为数值 text = line_edit.text() try: new_value = float(text) except ValueError: print(f"无效的数值输入: {text}") # 恢复原来的值 if axis == 'x': line_edit.setText(str(round(nodePath.getX(), 6))) elif axis == 'y': line_edit.setText(str(round(nodePath.getY(), 6))) elif axis == 'z': line_edit.setText(str(round(nodePath.getZ(), 6))) return # 根据轴设置位置 if axis == 'x': nodePath.setX(new_value) elif axis == 'y': nodePath.setY(new_value) elif axis == 'z': nodePath.setZ(new_value) print(f"位置已更新: {nodePath.getName()} {axis.upper()} = {new_value}") # 如果是坐标轴节点,需要更新坐标轴位置 if hasattr(self.world, 'selection_manager'): selection_manager = self.world.selection_manager if (hasattr(selection_manager, 'gizmoTarget') and selection_manager.gizmoTarget == nodePath): # 更新坐标轴位置 selection_manager._updateGizmoPositionAndOrientation() except Exception as e: print(f"更新位置时出错: {e}") def _createSafeSpinBox(self, min_val, max_val, read_only=False): """创建安全的数值框""" try: spinbox = QDoubleSpinBox() spinbox.setRange(min_val, max_val) spinbox.setSingleStep(0.1) if read_only: spinbox.setReadOnly(True) spinbox.setStyleSheet("background-color: #f0f0f0;") return spinbox except Exception as e: print(f"创建数值框失败: {e}") return None def _cleanupTransformControls(self): """清理变换控件引用""" control_names = ['pos_x', 'pos_y', 'pos_z', 'world_pos_x', 'world_pos_y', 'world_pos_z'] for name in control_names: if hasattr(self, name): control = getattr(self, name) if control: try: # 断开所有信号连接 control.valueChanged.disconnect() except: pass setattr(self, name, None) 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 _showCesiumTilesetProperties(self, nodePath, item): """显示 Cesium tileset 属性""" from PyQt5.QtWidgets import QLabel, QDoubleSpinBox, QPushButton, QGroupBox, QFormLayout from PyQt5.QtCore import Qt print(f"显示 Cesium tileset 属性: {nodePath.getName()}") # 标题 tileset_group = QGroupBox("Cesium 3D Tiles") tileset_layout = QGridLayout() tileset_layout.setColumnMinimumWidth(0, self.column_minimum_width) # self._propertyLayout.addWidget(title) # URL 信息 if nodePath.hasTag("tileset_url"): url_label = QLabel("URL:") tileset_layout.addWidget(url_label, 0, 0) url_value = QLabel(nodePath.getTag("tileset_url")) url_value.setWordWrap(True) url_value.setStyleSheet("font-size: 9px; color: #666;") tileset_layout.addWidget(url_value, 0, 1, 1, 3) # self._propertyLayout.addWidget(url_label) # self._propertyLayout.addWidget(url_value) tileset_group.setLayout(tileset_layout) self._propertyLayout.addWidget(tileset_group) # 位置控制 pos_group = QGroupBox("位置") pos_layout = QGridLayout() pos_layout.setColumnMinimumWidth(0, self.column_minimum_width) pos_row = QHBoxLayout() pos_row.setContentsMargins(0, 0, 0, 0) pos_row.setSpacing(4) pos_layout.addWidget(QLabel("位置"), 0, 0) # X 坐标 x_spin = QDoubleSpinBox() x_spin.setRange(-10000, 10000) x_spin.setSingleStep(1.0) x_spin.setDecimals(2) pos = nodePath.getPos() x_spin.setValue(pos.getX()) x_spin.valueChanged.connect(lambda v: self._updateTilesetPosition(nodePath, 'x', v)) x_label = QLabel("X:") x_label.setAlignment(Qt.AlignVCenter) x_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) pos_row.addWidget(x_label) pos_row.addWidget(x_spin) # Y 坐标 y_spin = QDoubleSpinBox() y_spin.setRange(-10000, 10000) y_spin.setSingleStep(1.0) y_spin.setDecimals(2) y_spin.setValue(pos.getY()) y_spin.valueChanged.connect(lambda v: self._updateTilesetPosition(nodePath, 'y', v)) y_label = QLabel("Y:") y_label.setAlignment(Qt.AlignVCenter) y_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) pos_row.addWidget(y_label) pos_row.addWidget(y_spin) # Z 坐标 z_spin = QDoubleSpinBox() z_spin.setRange(-10000, 10000) z_spin.setSingleStep(1.0) z_spin.setDecimals(2) z_spin.setValue(pos.getZ()) z_spin.valueChanged.connect(lambda v: self._updateTilesetPosition(nodePath, 'z', v)) z_label = QLabel("Z:") z_label.setAlignment(Qt.AlignVCenter) z_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) pos_row.addWidget(z_label) pos_row.addWidget(z_spin) pos_layout.addLayout(pos_row, 0, 1, 1, 3) pos_group.setLayout(pos_layout) # 缩放控制 scale_label = QLabel("缩放") pos_layout.addWidget(scale_label, 1, 0) scale_spin = QDoubleSpinBox() scale_spin.setRange(0.01, 1000) scale_spin.setSingleStep(0.1) scale_spin.setDecimals(2) scale_spin.setValue(nodePath.getScale().getX()) scale_spin.valueChanged.connect(lambda v: self._updateTilesetScale(nodePath, v)) pos_layout.addWidget(scale_spin, 1, 1, 1, 3) self._propertyLayout.addWidget(pos_group) # # 删除按钮 # delete_btn = QPushButton("删除 Tileset") # delete_btn.setStyleSheet(""" # QPushButton { # background-color: #ff4444; # color: white; # border: none; # padding: 8px; # border-radius: 4px; # margin-top: 10px; # } # QPushButton:hover { # background-color: #ff6666; # } # """) # delete_btn.clicked.connect(lambda: self._deleteCesiumTileset(nodePath, item)) # self._propertyLayout.addWidget(delete_btn) # 添加弹性空间 self._propertyLayout.addStretch() def _createPositionControl(self, label, nodePath, axis): """创建位置控制控件""" from PyQt5.QtWidgets import QHBoxLayout, QLabel, QDoubleSpinBox layout = QHBoxLayout() axis_label = QLabel(label) axis_label.setFixedWidth(20) layout.addWidget(axis_label) spinbox = QDoubleSpinBox() spinbox.setRange(-10000, 10000) spinbox.setSingleStep(1.0) spinbox.setDecimals(2) # 获取当前坐标值 pos = nodePath.getPos() if axis == 'x': spinbox.setValue(pos.getX()) elif axis == 'y': spinbox.setValue(pos.getY()) elif axis == 'z': spinbox.setValue(pos.getZ()) # 连接值变化信号 def onValueChanged(value): self._updateTilesetPosition(nodePath, axis, value) spinbox.valueChanged.connect(onValueChanged) layout.addWidget(spinbox) return layout def _createScaleControl(self, nodePath): """创建缩放控制控件""" from PyQt5.QtWidgets import QHBoxLayout, QLabel, QDoubleSpinBox layout = QHBoxLayout() scale_label = QLabel("缩放:") scale_label.setFixedWidth(40) layout.addWidget(scale_label) spinbox = QDoubleSpinBox() spinbox.setRange(0.01, 1000) spinbox.setSingleStep(0.1) spinbox.setDecimals(2) spinbox.setValue(nodePath.getScale().getX()) # 假设均匀缩放 def onScaleChanged(value): self._updateTilesetScale(nodePath, value) spinbox.valueChanged.connect(onScaleChanged) layout.addWidget(spinbox) return layout def _updateTilesetPosition(self, nodePath, axis, value): """更新 tileset 位置""" try: pos = nodePath.getPos() if axis == 'x': nodePath.setPos(value, pos.getY(), pos.getZ()) elif axis == 'y': nodePath.setPos(pos.getX(), value, pos.getZ()) elif axis == 'z': nodePath.setPos(pos.getX(), pos.getY(), value) print(f"更新 {nodePath.getName()} 位置: {axis} = {value}") except Exception as e: print(f"更新位置失败: {e}") def _updateTilesetScale(self, nodePath, value): """更新 tileset 缩放""" try: nodePath.setScale(value) print(f"更新 {nodePath.getName()} 缩放: {value}") except Exception as e: print(f"更新缩放失败: {e}") def _deleteCesiumTileset(self, nodePath, item): """删除 Cesium tileset""" try: from PyQt5.QtWidgets import QMessageBox reply = QMessageBox.question( None, '确认删除', f'确定要删除 Cesium tileset "{nodePath.getName()}" 吗?', QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply == QMessageBox.Yes: tree_widget = self.world.treeWidget tree_widget.delete_item(nodePath) # # 从场景中移除 # nodePath.removeNode() # # # 从 tilesets 列表中移除 # if hasattr(self.world, 'scene_manager'): # tilesets_to_remove = [] # for i, tileset_info in enumerate(self.world.scene_manager.tilesets): # if tileset_info['node'] == nodePath: # tilesets_to_remove.append(i) # # # 从后往前删除,避免索引问题 # for i in reversed(tilesets_to_remove): # del self.world.scene_manager.tilesets[i] # # # 更新场景树 # self.world.scene_manager.updateSceneTree() # # # 清空属性面板 # self.clearPropertyPanel() # print(f"成功删除 Cesium tileset: {nodePath.getName()}") except Exception as e: print(f"删除 Cesium tileset 失败: {str(e)}") def _updateModelPropertyPanel(self, model): """更新模型属性面板""" if hasattr(model, 'getTag') and model.getTag("is_gui_element") == "1": self.updateGUIPropertyPanel(model) return # 获取父节点 parent = model.getParent() # 变换属性部分 (Transform) self.transform_group = QGroupBox("变换 Transform") transform_layout = QGridLayout() transform_layout.setColumnMinimumWidth(0, self.column_minimum_width) # transform_layout.setColumnStretch(1, 1) # transform_layout.setColumnStretch(2, 1) # transform_layout.setColumnStretch(3, 1) # 获取当前值 relativePos = model.getPos(parent) if parent else model.getPos() worldPos = model.getPos(self.world.render) position_label = QLabel("位置") transform_layout.addWidget(position_label, 0, 0) # 位置 (Position) position_row = QHBoxLayout() position_row.setContentsMargins(0, 0, 0, 0) position_row.setSpacing(4) # position_row.addWidget(QLabel("位置")) 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()) # 连接位置变化事件 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 for axis_label, widget in (("X:", self.pos_x), ("Y:", self.pos_y), ("Z:", self.pos_z)): axis = QLabel(axis_label) axis.setAlignment(Qt.AlignVCenter) axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) position_row.addWidget(axis) position_row.addWidget(widget) transform_layout.addLayout(position_row, 0, 1, 1, 3) # 世界位置 (只读) world_position_row = QHBoxLayout() world_position_row.setContentsMargins(0, 0, 0, 0) world_position_row.setSpacing(4) # world_position_row.addWidget(QLabel("世界位置")) world_position_label = QLabel("世界位置") transform_layout.addWidget(world_position_label, 1, 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()) for axis_label, widget in (("X:", self.world_pos_x), ("Y:", self.world_pos_y), ("Z:", self.world_pos_z)): axis = QLabel(axis_label) axis.setAlignment(Qt.AlignVCenter) axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) world_position_row.addWidget(axis) world_position_row.addWidget(widget) transform_layout.addLayout(world_position_row, 1, 1, 1, 3) # 旋转 (Rotation) rotation_row = QHBoxLayout() rotation_row.setContentsMargins(0, 0, 0, 0) rotation_row.setSpacing(4) rotation_label = QLabel("旋转") transform_layout.addWidget(rotation_label, 2, 0) self.rot_x = QDoubleSpinBox() self.rot_y = QDoubleSpinBox() self.rot_z = QDoubleSpinBox() for rot_widget in [self.rot_x, self.rot_y, self.rot_z]: rot_widget.setRange(-360, 360) self.rot_x.setValue(model.getH()) self.rot_y.setValue(model.getP()) self.rot_z.setValue(model.getR()) self.rot_x.valueChanged.connect(lambda v: model.setH(v)) self.rot_y.valueChanged.connect(lambda v: model.setP(v)) self.rot_z.valueChanged.connect(lambda v: model.setR(v)) for axis_label, widget in (("H:", self.rot_x), ("P:", self.rot_y), ("R:", self.rot_z)): axis = QLabel(axis_label) axis.setAlignment(Qt.AlignVCenter) axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) rotation_row.addWidget(axis) rotation_row.addWidget(widget) transform_layout.addLayout(rotation_row, 2, 1, 1, 3) # 缩放 (Scale) scale_row = QHBoxLayout() scale_row.setContentsMargins(0, 0, 0, 0) scale_row.setSpacing(4) scale_label = QLabel("缩放") transform_layout.addWidget(scale_label, 3, 0) self.scale_x = QDoubleSpinBox() self.scale_y = QDoubleSpinBox() self.scale_z = QDoubleSpinBox() current_scale = model.getScale() for scale_widget, scale_value in zip([self.scale_x, self.scale_y, self.scale_z], [current_scale.getX(), current_scale.getY(), current_scale.getZ()]): scale_widget.setRange(-1000, 1000) scale_widget.setSingleStep(0.1) scale_widget.setValue(scale_value) self.scale_x.valueChanged.connect(lambda value: self._onScaleValueChanged(self.scale_x, value)) self.scale_y.valueChanged.connect(lambda value: self._onScaleValueChanged(self.scale_y, value)) self.scale_z.valueChanged.connect(lambda value: self._onScaleValueChanged(self.scale_z, value)) self.scale_x.valueChanged.connect(lambda value: self._updateXScale(model, value)) self.scale_y.valueChanged.connect(lambda value: self._updateYScale(model, value)) self.scale_z.valueChanged.connect(lambda value: self._updateZScale(model, value)) for axis_label, widget in (("X:", self.scale_x), ("Y:", self.scale_y), ("Z:", self.scale_z)): axis = QLabel(axis_label) axis.setAlignment(Qt.AlignVCenter) axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) scale_row.addWidget(axis) scale_row.addWidget(widget) transform_layout.addLayout(scale_row, 3, 1, 1, 3) self.transform_group.setLayout(transform_layout) self._propertyLayout.addWidget(self.transform_group) # 碰撞检测面板 self._addCollisionPanel(model) # 动画和太阳方位角面板 self._addAnimationPanel(model) self._addSunAzimuthPanel() # # 材质属性组 self._updateModelMaterialPanel(model) def _onScaleValueChanged(self, scale_widget, value): pass def _updateXScale(self, model, value): """更新X轴缩放值""" # 确保值不为0 if value == 0: sender = None # 通过遍历找到发出信号的控件 for widget in [self.scale_x, self.scale_y, self.scale_z]: if widget.value() == value: sender = widget break if sender: self._onScaleValueChanged(sender, value) return # 更新模型的X轴缩放 current_scale = model.getScale() model.setScale(value, current_scale.getY(), current_scale.getZ()) self.refreshModelValues(model) def _updateYScale(self, model, value): """更新Y轴缩放值""" # 确保值不为0 if value == 0: sender = None # 通过遍历找到发出信号的控件 for widget in [self.scale_x, self.scale_y, self.scale_z]: if widget.value() == value: sender = widget break if sender: self._onScaleValueChanged(sender, value) return # 更新模型的Y轴缩放 current_scale = model.getScale() model.setScale(current_scale.getX(), value, current_scale.getZ()) self.refreshModelValues(model) def _updateZScale(self, model, value): """更新Z轴缩放值""" # 确保值不为0 if value == 0: sender = None # 通过遍历找到发出信号的控件 for widget in [self.scale_x, self.scale_y, self.scale_z]: if widget.value() == value: sender = widget break if sender: self._onScaleValueChanged(sender, value) return # 更新模型的Z轴缩放 current_scale = model.getScale() model.setScale(current_scale.getX(), current_scale.getY(), value) self.refreshModelValues(model) def updateGUIPropertyPanel(self, gui_element, item): """更新GUI元素属性面板""" self.clearPropertyPanel() itemText = gui_element.getTag("name") or "未命名GUI元素" user_visible = True user_visible = gui_element.getPythonTag("user_visible") if user_visible is None: user_visible = True gui_element.setPythonTag("user_visible", True) self.name_group = QGroupBox("物体名称") name_layout = QGridLayout() self.active_check = QCheckBox() # 根据元素的实际可见性状态设置复选框 self.active_check.setChecked(user_visible) self.name_input = QLineEdit(itemText) # 注意:对于GUI元素,我们需要特殊处理名称更新 def updateGUIName(text): # 更新GUI元素的标签 gui_element.setTag("name", text) self.world.treeWidget.update_item_name(self.name_input.text(), item) # gui_element.setName(text) # 如果有场景管理器,也需要更新场景树 # if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'updateSceneTree'): # self.world.scene_manager.updateSceneTree() self.name_input.returnPressed.connect(lambda: updateGUIName(self.name_input.text())) # 如果失去焦点也更新名称 self.name_input.editingFinished.connect(lambda: updateGUIName(self.name_input.text())) name_layout.addWidget(self.active_check, 0, 0) name_layout.setColumnMinimumWidth(0, self.column_minimum_width) name_layout.addWidget(self.name_input, 0, 1, 1, 3) self.name_group.setLayout(name_layout) self._propertyLayout.addWidget(self.name_group) if gui_element: try: self.active_check.stateChanged.disconnect() except TypeError: pass self.active_check.stateChanged.connect( lambda state, elem=gui_element: self._setUserVisible(elem, state == Qt.Checked) ) 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_info_layout.setColumnMinimumWidth(0, self.column_minimum_width) # 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, 1, 3) # 修改 updateGUIPropertyPanel 中的文本属性部分 # 文本属性(如果适用) 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 = textEdit.text() success = self.world.gui_manager.editGUIElement(gui_element, "text", text) # if success: # # 更新场景树显示的名称 # if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'updateSceneTree'): # self.world.scene_manager.updateSceneTree() # 只在按下回车键或失去焦点时更新 textEdit.returnPressed.connect(updateText) textEdit.editingFinished.connect(updateText) gui_info_layout.addWidget(textEdit, 1, 1, 1, 3) 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", "2d_image", "2d_video_screen"]: transform_group = QGroupBox("变换 Rect Transform") else: transform_group = QGroupBox("变换 Transform") transform_layout = QGridLayout() transform_layout.setColumnMinimumWidth(0, self.column_minimum_width) pos = gui_element.getPos() # 根据GUI类型决定位置编辑方式 if gui_type in ["button", "label", "entry", "2d_image", "2d_video_screen"]: # 2D GUI组件使用屏幕坐标 logical_x = pos.getX() / 0.1 # 反向转换为逻辑坐标 logical_z = pos.getZ() / 0.1 position_row = QHBoxLayout() position_row.setContentsMargins(0, 0, 0, 0) position_row.setSpacing(4) screen_title = QLabel("屏幕位置") transform_layout.addWidget(screen_title, 0, 0) self.pos_x = QDoubleSpinBox() self.pos_x.setRange(-1000, 1000) self.pos_x.setValue(logical_x) self.pos_z = QDoubleSpinBox() self.pos_z.setRange(-1000, 1000) self.pos_z.setValue(logical_z) axis_labels = [] for col, (axis_label, widget) in enumerate((("X:", self.pos_x), ("Z:", self.pos_z)), start=1): axis = QLabel(axis_label) axis.setAlignment(Qt.AlignRight | Qt.AlignVCenter) axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) axis_labels.append(axis) position_row.addWidget(axis) position_row.addWidget(widget) transform_layout.addLayout(position_row, 0, 1, 1, 3) # axis_width = max(label.sizeHint().width() for label in axis_labels) # for label in axis_labels: # label.setMinimumWidth(axis_width) # screen_position_row.setColumnStretch(2, 1) # screen_position_row.setColumnStretch(4, 1) for i in range(4): transform_layout.setColumnStretch(i, 1) actual_position_row = QGridLayout() actual_row = QHBoxLayout() actual_row.setContentsMargins(0, 0, 0, 0) actual_row.setSpacing(4) actual_title = QLabel("实际坐标") # actual_row.addWidget(actual_title) transform_layout.addWidget(actual_title, 1, 0) self.actual_pos_x_label = QLabel(f"{pos.getX():.3f}") self.actual_pos_x_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) self.actual_pos_x_label.setStyleSheet("color: gray; font-size: 10px;") self.actual_pos_z_label = QLabel(f"{pos.getZ():.3f}") self.actual_pos_z_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) self.actual_pos_z_label.setStyleSheet("color: gray; font-size: 10px;") for col, (axis_label, widget) in enumerate((("X:", self.actual_pos_x_label), ("Z:", self.actual_pos_z_label)), start=1): axis = QLabel(axis_label) axis.setAlignment(Qt.AlignRight | Qt.AlignVCenter) axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) # axis.setMinimumWidth(axis_width) # actual_position_row.addWidget(axis, 0, col * 2 - 1, Qt.AlignLeft | Qt.AlignVCenter) # actual_position_row.addWidget(widget, 0, col * 2, Qt.AlignLeft | Qt.AlignVCenter) actual_row.addWidget(axis) actual_row.addWidget(widget) # actual_position_row.setColumnStretch(2, 1) # actual_position_row.setColumnStretch(4, 1) transform_layout.addLayout(actual_row, 1, 1, 1, 3) def _update_actual_position_labels(): current_pos = gui_element.getPos() self.actual_pos_x_label.setText(f"{current_pos.getX():.3f}") self.actual_pos_z_label.setText(f"{current_pos.getZ():.3f}") _update_actual_position_labels() def _on_gui_pos_x_changed(value): self.world.gui_manager.editGUI2DPosition(gui_element, "x", value) _update_actual_position_labels() def _on_gui_pos_z_changed(value): self.world.gui_manager.editGUI2DPosition(gui_element, "z", value) _update_actual_position_labels() self.pos_x.valueChanged.connect(_on_gui_pos_x_changed) self.pos_z.valueChanged.connect(_on_gui_pos_z_changed) if gui_type in ["2d_image", "2d_video_screen"]: scale = gui_element.getScale() width = scale.getX() if hasattr(scale, 'getX') else scale[0] if isinstance(scale, (tuple, list)) else scale height = scale.getZ() if hasattr(scale, 'getZ') else scale[1] if isinstance(scale, (tuple, list)) and len( scale) > 1 else scale scale_row = QHBoxLayout() scale_row.setContentsMargins(0, 0, 0, 0) scale_row.setSpacing(4) transform_layout.addWidget(QLabel("缩放"), 2, 0) self.scale_x = QDoubleSpinBox() self.scale_x.setRange(0.1, 100) self.scale_x.setSingleStep(0.01) self.scale_x.setValue(width) self.scale_x.valueChanged.connect(lambda v: self.editGUIScale(gui_element, "x", v)) self.scale_z = QDoubleSpinBox() self.scale_z.setRange(0.1, 10) self.scale_z.setSingleStep(0.01) self.scale_z.setValue(height) self.scale_z.valueChanged.connect(lambda v: self.editGUIScale(gui_element, "z", v)) for axis_label, widget in (("X:", self.scale_x), ("Z:", self.scale_z)): axis = QLabel(axis_label) axis.setAlignment(Qt.AlignVCenter) axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) scale_row.addWidget(axis) scale_row.addWidget(widget) transform_layout.addLayout(scale_row, 2, 1, 1, 3) else: scale_row = QHBoxLayout() scale_row.setContentsMargins(0, 0, 0, 0) scale_row.setSpacing(4) transform_layout.addWidget(QLabel("缩放"), 2, 0) scaleSpinBox = QDoubleSpinBox() scaleSpinBox.setRange(0.01, 100) scaleSpinBox.setSingleStep(0.01) scaleSpinBox.setValue(gui_element.getScale().getX() * 2) scaleSpinBox.valueChanged.connect(lambda v: self._update2DImageScale(gui_element, v)) axis = QLabel("X:") axis.setAlignment(Qt.AlignVCenter) axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) scale_row.addWidget(axis) scale_row.addWidget(scaleSpinBox) transform_layout.addLayout(scale_row, 2, 1, 1, 3) else: # 3D GUI组件使用世界坐标 position_row = QHBoxLayout() position_row.setContentsMargins(0, 0, 0, 0) position_row.setSpacing(4) # position_row.addWidget(QLabel("位置")) position_label = QLabel("位置") transform_layout.addWidget(position_label, 0, 0) self.pos_x = QDoubleSpinBox() self.pos_x.setRange(-100, 100) self.pos_x.setValue(pos.getX()) self.pos_x.setSingleStep(0.01) self.pos_x.valueChanged.connect(lambda v: self.world.gui_manager.editGUI3DPosition(gui_element, "x", v)) self.pos_y = QDoubleSpinBox() self.pos_y.setRange(-100, 100) self.pos_y.setValue(pos.getY()) self.pos_y.setSingleStep(0.01) self.pos_y.valueChanged.connect(lambda v: self.world.gui_manager.editGUI3DPosition(gui_element, "y", v)) self.pos_z = QDoubleSpinBox() self.pos_z.setRange(-100, 100) self.pos_z.setValue(pos.getZ()) self.pos_z.setSingleStep(0.01) self.pos_z.valueChanged.connect(lambda v: self.world.gui_manager.editGUI3DPosition(gui_element, "z", v)) for axis_label, widget in (("X:", self.pos_x), ("Y:", self.pos_y), ("Z:", self.pos_z)): axis = QLabel(axis_label) axis.setAlignment(Qt.AlignVCenter) axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) position_row.addWidget(axis) position_row.addWidget(widget) transform_layout.addLayout(position_row, 0, 1, 1, 3) scale = gui_element.getScale() scale_row = QHBoxLayout() scale_row.setContentsMargins(0, 0, 0, 0) scale_row.setSpacing(4) # scale_row.addWidget(QLabel("缩放")) scale_label = QLabel("缩放") transform_layout.addWidget(scale_label, 1, 0) self.scale_x = QDoubleSpinBox() self.scale_x.setRange(0.01, 100) self.scale_x.setSingleStep(0.01) self.scale_x.setValue( scale.getX() if hasattr(scale, 'getX') else scale[0] if isinstance(scale, (tuple, list)) else scale) self.scale_x.valueChanged.connect(lambda v: self.world.gui_manager.editGUIScale(gui_element, "x", v)) self.scale_y = QDoubleSpinBox() self.scale_y.setRange(0.01, 100) self.scale_y.setSingleStep(0.01) self.scale_y.setValue( scale.getY() if hasattr(scale, 'getY') else scale[1] if isinstance(scale, (tuple, list)) and len( scale) > 1 else scale) self.scale_y.valueChanged.connect(lambda v: self.world.gui_manager.editGUIScale(gui_element, "y", v)) self.scale_z = QDoubleSpinBox() self.scale_z.setRange(0.01, 100) self.scale_z.setSingleStep(0.01) self.scale_z.setValue( scale.getZ() if hasattr(scale, 'getZ') else scale[2] if isinstance(scale, (tuple, list)) and len( scale) > 2 else scale) self.scale_z.valueChanged.connect(lambda v: self.world.gui_manager.editGUIScale(gui_element, "z", v)) for axis_label, widget in (("X:", self.scale_x), ("Y:", self.scale_y), ("Z:", self.scale_z)): axis = QLabel(axis_label) axis.setAlignment(Qt.AlignVCenter) axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) scale_row.addWidget(axis) scale_row.addWidget(widget) transform_layout.addLayout(scale_row, 1, 1, 1, 3) transform_group.setLayout(transform_layout) self._propertyLayout.addWidget(transform_group) # 为2D图像和视频屏幕添加Sort属性 if gui_type in ["2d_image", "2d_video_screen", "info_panel"]: sort_group = QGroupBox("显示顺序") sort_layout = QGridLayout() sort_layout.setColumnMinimumWidth(0, self.column_minimum_width) # 获取当前sort值,如果没有设置则默认为0 current_sort = int(gui_element.getTag("sort") or "0") sort_layout.addWidget(QLabel("显示优先度:"), 0, 0) sort_spin = QSpinBox() sort_spin.setRange(-1000, 1000) sort_spin.setValue(current_sort) def updateSort(value): try: # 设置标签 gui_element.setTag("sort", str(value)) # 应用sort到节点 gui_element.setBin("fixed", value) print(f"✓ 更新{gui_type}渲染顺序: {value}") except Exception as e: print(f"✗ 更新{gui_type}渲染顺序失败: {e}") sort_spin.valueChanged.connect(updateSort) sort_layout.addWidget(sort_spin, 0, 1, 1, 3) sort_group.setLayout(sort_layout) self._propertyLayout.addWidget(sort_group) # 外观属性组 - 添加字体颜色选择 if gui_type in ["button", "label", "3d_text"]: appearance_group = QGroupBox("外观属性") appearance_layout = QGridLayout() appearance_layout.setColumnMinimumWidth(0, self.column_minimum_width) # 字体颜色选择 appearance_layout.addWidget(QLabel("字体颜色:"), 0, 0) colorButton = QPushButton("选择颜色") colorButton.clicked.connect(lambda checked, elem=gui_element: self._selectGUIColor(elem)) appearance_layout.addWidget(colorButton, 0, 1, 1, 3) appearance_group.setLayout(appearance_layout) self._propertyLayout.addWidget(appearance_group) # 外观属性组 if gui_type in ["button", "label"]: appearance_group = QGroupBox("外观属性") appearance_layout = QGridLayout() appearance_layout.setColumnMinimumWidth(0, self.column_minimum_width) 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, 1, 3) appearance_group.setLayout(appearance_layout) self._propertyLayout.addWidget(appearance_group) if gui_type == "3d_image": image_group = QGroupBox("图片设置") image_layout = QGridLayout() image_layout.setColumnMinimumWidth(0, self.column_minimum_width) # 当前图片路径标签和 current_image_label = QLabel("当前图片:") image_layout.addWidget(current_image_label, 0, 0) # 显示当前贴图路径(简化显示) current_texture_path = gui_element.getTag("texture_path") or "未设置" if current_texture_path != "未设置": display_path = current_texture_path if len(current_texture_path) <= 10 else current_texture_path[ :7] + "..." else: display_path = current_texture_path texture_label = QLabel(display_path) texture_label.setWordWrap(True) texture_label.setToolTip(current_texture_path if current_texture_path != "未设置" else "") image_layout.addWidget(texture_label, 0, 1, 1, 3) # 选择图片按钮 if has_icon("property_select_image"): select_texture_button = QPushButton(get_icon("property_select_image"), "选择图片...") else: select_texture_button = QPushButton("选择图片...") image_layout.addWidget(select_texture_button, 1, 0, 1, 4) def onSelectTexture(): from PyQt5.QtWidgets import QFileDialog file_path, _ = QFileDialog.getOpenFileName( None, "选择图片", "", "图像文件 (*.png *.jpg *.jpeg *.bmp *.tga *.dds)" ) if file_path: # 应用新纹理到 3D Image file_path = util.normalize_model_path(file_path) success = self.world.gui_manager.update3DImageTexture(gui_element, file_path) if success: # 保存路径到 Tag gui_element.setTag("texture_path", file_path) gui_element.setTag("gui_image_path", file_path) # 更新显示 texture_label.setText(file_path) # 可选:刷新场景树或其他 UI # self.world.scene_manager.updateSceneTree() select_texture_button.clicked.connect(onSelectTexture) image_group.setLayout(image_layout) self._propertyLayout.addWidget(image_group) if gui_type in ["2d_image"]: image_group = QGroupBox("2D图片设置") image_layout = QGridLayout() image_layout.setColumnMinimumWidth(0, self.column_minimum_width) # 当前图片路径标签 current_image_label = QLabel("当前图片:") image_layout.addWidget(current_image_label, 0, 0) # 显示当前贴图路径(简化显示) current_texture_path = gui_element.getTag("texture_path") or gui_element.getTag("image_path") or "未设置" if current_texture_path != "未设置": display_path = current_texture_path if len(current_texture_path) <= 10 else current_texture_path[ :7] + "..." else: display_path = current_texture_path texture_label = QLabel(display_path) texture_label.setWordWrap(True) image_layout.addWidget(texture_label, 0, 1, 1, 3) # 选择图片按钮 if has_icon("property_select_image"): select_texture_button = QPushButton(get_icon("property_select_image"), "选择图片...") else: select_texture_button = QPushButton("选择图片...") # select_texture_button = QPushButton("选择图片...") image_layout.addWidget(select_texture_button, 1, 0, 1, 4) def onSelect2DTexture(): from PyQt5.QtWidgets import QFileDialog file_path, _ = QFileDialog.getOpenFileName( None, "选择图片", "", "图像文件 (*.png *.jpg *.jpeg *.bmp *.tga *.dds)" ) if file_path: # 应用新纹理到 2D Image file_path = util.normalize_model_path(file_path) success = self.update2DImageTexture(gui_element, file_path) if success: # 保存路径到 Tag gui_element.setTag("texture_path", file_path) gui_element.setTag("image_path", file_path) # 更新显示 texture_label.setText(file_path) # 可选:刷新场景树或其他 UI # self.world.scene_manager.updateSceneTree() select_texture_button.clicked.connect(onSelect2DTexture) image_group.setLayout(image_layout) self._propertyLayout.addWidget(image_group) # 添加弹性空间 if gui_type == "video_screen": self._addVideoScreenProperties(gui_element, item) elif gui_type == "2d_video_screen": self._add2DVideoScreenProperties(gui_element, item) elif gui_type == "spherical_video": self._addSphericalVideoProperties(gui_element) elif gui_type in ['info_panel', 'info_panel_3d']: self._addInfoPanelProperties(gui_element) self._propertyLayout.addStretch() # 强制更新布局 if self._propertyLayout: self._propertyLayout.update() propertyWidget = self._propertyLayout.parentWidget() if propertyWidget: propertyWidget.update() def _addInfoPanelProperties(self, info_panel): """为信息面板添加属性控制面板 - 同时适配2D和3D信息面板""" try: from PyQt5.QtWidgets import (QGroupBox, QGridLayout, QPushButton, QLabel, QDoubleSpinBox, QColorDialog, QSpinBox, QTextEdit) from PyQt5.QtCore import Qt, QTimer from PyQt5.QtGui import QColor import os panel_id = info_panel.getTag("panel_id") if not panel_id: print("无法找到信息面板ID") return # 检测面板类型 (2D或3D) gui_type = info_panel.getTag("gui_type") is_3d_panel = gui_type == "info_panel_3d" # 获取面板当前属性 current_size = (1.0, 0.6) current_position = (0, 0) if not is_3d_panel else (0, 0, 0) current_bg_color = (0.15, 0.15, 0.15, 0.9) current_border_color = (0.3, 0.3, 0.3, 1.0) current_title_color = (1.0, 1.0, 0.0, 1.0) current_content_color = (0.9, 0.9, 0.9, 1.0) current_title_size = 0.06 # 默认值 current_content_size = 0.045 # 默认值 current_title_text = "信息面板" current_content_text = "这是一个信息面板" current_bg_image = "" current_sort = 0 # 标题和内容位置默认值 current_title_pos = (0, 0) current_content_pos = (-0.45, 0) # 从info_panel_manager获取当前属性 if hasattr(self.world, 'info_panel_manager'): panel_data = self.world.info_panel_manager.panels.get(panel_id) if panel_data: # 获取基本属性 if 'properties' in panel_data: props = panel_data['properties'] current_size = props.get('size', current_size) current_position = props.get('position', current_position) current_bg_color = props.get('bg_color', current_bg_color) current_border_color = props.get('border_color', current_border_color) current_title_color = props.get('title_color', current_title_color) current_content_color = props.get('content_color', current_content_color) current_bg_image = props.get('bg_image', current_bg_image) # 获取文本内容和位置(根据面板类型区分处理) if not is_3d_panel: # 2D面板 if 'title_label' in panel_data and panel_data['title_label']: current_title_text = panel_data['title_label']['text'] # 获取标题位置 title_pos = panel_data['title_label'].getPos() current_title_pos = (title_pos.getX(), title_pos.getZ()) if 'content_label' in panel_data and panel_data['content_label']: current_content_text = panel_data['content_label']['text'] # 获取内容位置 content_pos = panel_data['content_label'].getPos() current_content_pos = (content_pos.getX(), content_pos.getZ()) # 获取字体大小 if 'title_label' in panel_data and panel_data['title_label']: try: title_scale = panel_data['title_label']['text_scale'] # 如果是元组,取第一个值 if isinstance(title_scale, (tuple, list)): current_title_size = title_scale[0] if len(title_scale) > 0 else 0.06 else: current_title_size = title_scale if isinstance(title_scale, (int, float)) else 0.06 except: current_title_size = 0.06 if 'content_label' in panel_data and panel_data['content_label']: try: content_scale = panel_data['content_label']['text_scale'] # 如果是元组,取第一个值 if isinstance(content_scale, (tuple, list)): current_content_size = content_scale[0] if len(content_scale) > 0 else 0.045 else: current_content_size = content_scale if isinstance(content_scale, (int, float)) else 0.045 except: current_content_size = 0.045 else: # 3D面板 if 'title_node' in panel_data and panel_data['title_node']: current_title_text = panel_data['title_node'].getText() if 'content_node' in panel_data and panel_data['content_node']: current_content_text = panel_data['content_node'].getText() if 'title_text' in panel_data and panel_data['title_text']: current_title_size = panel_data['title_text'].getScale().getX() if 'content_text' in panel_data and panel_data['content_text']: current_content_size = panel_data['content_text'].getScale().getX() # 获取sort值 if info_panel.hasTag("sort"): try: current_sort = int(info_panel.getTag("sort")) except: current_sort = 0 # 背景图片组 image_group = QGroupBox("背景图片设置") image_layout = QGridLayout() image_layout.setColumnMinimumWidth(0, self.column_minimum_width) # 当前背景图片路径标签 current_image_label = QLabel("当前背景图片:") image_layout.addWidget(current_image_label, 0, 0) # 显示当前背景图片路径(简化显示) if current_bg_image: display_path = current_bg_image if len(current_bg_image) <= 12 else current_bg_image[:9] + "..." else: display_path = "未设置" texture_label = QLabel(display_path) texture_label.setWordWrap(True) texture_label.setToolTip(current_bg_image if current_bg_image else "") image_layout.addWidget(texture_label, 0, 1, 1, 3) # 选择背景图片按钮 select_texture_button = QPushButton("选择背景图片...") image_layout.addWidget(select_texture_button, 1, 0, 1, 4) # 清除背景图片按钮 clear_texture_button = QPushButton("清除背景图片") clear_texture_button.setStyleSheet("color: red;") image_layout.addWidget(clear_texture_button, 2, 0, 1, 4) def onSelectBackgroundImage(): """选择背景图片""" from PyQt5.QtWidgets import QFileDialog file_path, _ = QFileDialog.getOpenFileName( None, "选择背景图片", "", "图像文件 (*.png *.jpg *.jpeg *.bmp *.tga *.dds)" ) if file_path: # 应用新背景图片到信息面板 file_path = util.normalize_model_path(file_path) success = self.updateInfoPanelBackgroundImage(info_panel, file_path) if success: # 保存路径到 Tag info_panel.setTag("bg_image_path", file_path) # 更新显示 display_path = file_path if len(file_path) <= 12 else file_path[:9] + "..." texture_label.setText(display_path) texture_label.setToolTip(file_path) def onClearBackgroundImage(): """清除背景图片""" success = self.updateInfoPanelBackgroundImage(info_panel, None) if success: # 清除路径标签 info_panel.clearTag("bg_image_path") texture_label.setText("未设置") texture_label.setToolTip("") select_texture_button.clicked.connect(onSelectBackgroundImage) clear_texture_button.clicked.connect(onClearBackgroundImage) image_group.setLayout(image_layout) self._propertyLayout.addWidget(image_group) # 显示顺序组 sort_group = QGroupBox("显示顺序") sort_layout = QGridLayout() sort_layout.setColumnMinimumWidth(0, self.column_minimum_width) sort_layout.addWidget(QLabel("显示优先度:"), 0, 0) sort_spin = QSpinBox() sort_spin.setRange(-1000, 1000) sort_spin.setValue(current_sort) def updateSort(value): try: # 设置标签 info_panel.setTag("sort", str(value)) # 应用sort到节点 info_panel.setBin("fixed", value) print(f"✓ 更新信息面板渲染顺序: {value}") except Exception as e: print(f"✗ 更新信息面板渲染顺序失败: {e}") sort_spin.valueChanged.connect(updateSort) sort_layout.addWidget(sort_spin, 0, 1, 1, 3) sort_group.setLayout(sort_layout) self._propertyLayout.addWidget(sort_group) # 面板大小设置组 size_group = QGroupBox() size_group.setTitle("面板大小设置") size_layout = QGridLayout() size_layout.setColumnMinimumWidth(0, self.column_minimum_width) size_row = QHBoxLayout() size_row.setContentsMargins(0, 0, 0, 0) size_row.setSpacing(4) size_title = QLabel("面板大小设置:") size_layout.addWidget(size_title, 0, 0) width_spin = QDoubleSpinBox() width_spin.setRange(0.1, 5.0) width_spin.setSingleStep(0.1) width_spin.setValue(current_size[0]) height_spin = QDoubleSpinBox() height_spin.setRange(0.1, 5.0) height_spin.setSingleStep(0.1) height_spin.setValue(current_size[1]) for label_text, spinbox in (("宽度:", width_spin), ("高度:", height_spin)): label = QLabel(label_text) label.setAlignment(Qt.AlignVCenter) label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) size_row.addWidget(label) size_row.addWidget(spinbox) size_layout.addLayout(size_row, 0, 1, 1, 3) # 连接大小变化信号 def onSizeChanged(): if hasattr(self.world, 'info_panel_manager'): panel_width = width_spin.value() panel_height = height_spin.value() # 根据面板类型选择更新方法 if is_3d_panel: self.world.info_panel_manager.update3DPanelProperties( panel_id, size=(panel_width, panel_height)) else: self.world.info_panel_manager.updatePanelProperties( panel_id, size=(panel_width, panel_height)) # 同步更新内容的换行设置 self._adjustContentWordwrap(panel_id, panel_width, content_size_spin.value()) width_spin.valueChanged.connect(onSizeChanged) height_spin.valueChanged.connect(onSizeChanged) size_group.setLayout(size_layout) self._propertyLayout.addWidget(size_group) # 位置设置:X:[box] Y:[box]/Z:[box],右对齐可伸缩 position_group = QGroupBox() position_group.setTitle("位置设置") position_layout = QGridLayout() position_layout.setColumnMinimumWidth(0, self.column_minimum_width) position_row = QHBoxLayout() position_row.setContentsMargins(0, 0, 0, 0) position_row.setSpacing(4) position_title = QLabel("位置设置:") position_layout.addWidget(position_title, 0, 0) if is_3d_panel: # 3D 面板位置:X、Y、Z pos_x_spin = QDoubleSpinBox() pos_x_spin.setRange(-1000, 1000) pos_x_spin.setSingleStep(0.1) pos_x_spin.setValue(current_position[0] if len(current_position) > 0 else 0) pos_y_spin = QDoubleSpinBox() pos_y_spin.setRange(-1000, 1000) pos_y_spin.setSingleStep(0.1) pos_y_spin.setValue(current_position[1] if len(current_position) > 1 else 0) pos_z_spin = QDoubleSpinBox() pos_z_spin.setRange(-1000, 1000) pos_z_spin.setSingleStep(0.1) pos_z_spin.setValue(current_position[2] if len(current_position) > 2 else 0) axis_widgets = (("X:", pos_x_spin), ("Y:", pos_y_spin), ("Z:", pos_z_spin)) def on3DPositionChanged(): if hasattr(self.world, "info_panel_manager"): self.world.info_panel_manager.update3DPanelProperties( panel_id, position=(pos_x_spin.value(), pos_y_spin.value(), pos_z_spin.value())) pos_x_spin.valueChanged.connect(on3DPositionChanged) pos_y_spin.valueChanged.connect(on3DPositionChanged) pos_z_spin.valueChanged.connect(on3DPositionChanged) else: # 2D 面板位置:X、Z pos_x_spin = QDoubleSpinBox() pos_x_spin.setRange(-1000, 1000) pos_x_spin.setSingleStep(0.1) pos_x_spin.setValue(current_position[0] if len(current_position) > 0 else 0) pos_z_spin = QDoubleSpinBox() pos_z_spin.setRange(-1000, 1000) pos_z_spin.setSingleStep(0.1) pos_z_spin.setValue(current_position[1] if len(current_position) > 1 else 0) axis_widgets = (("X:", pos_x_spin), ("Z:", pos_z_spin)) def on2DPositionChanged(): if hasattr(self.world, "info_panel_manager"): self.world.info_panel_manager.updatePanelProperties( panel_id, position=(pos_x_spin.value(), pos_z_spin.value())) pos_x_spin.valueChanged.connect(on2DPositionChanged) pos_z_spin.valueChanged.connect(on2DPositionChanged) for axis_label, spinbox in axis_widgets: axis = QLabel(axis_label) axis.setAlignment(Qt.AlignVCenter) axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) position_row.addWidget(axis) position_row.addWidget(spinbox) position_layout.addLayout(position_row, 0, 1, 1, 3) position_group.setLayout(position_layout) self._propertyLayout.addWidget(position_group) # 边框属性组 border_group = QGroupBox("边框属性") border_layout = QGridLayout() border_layout.setColumnMinimumWidth(0, self.column_minimum_width) border_layout.addWidget(QLabel("边框颜色:"), 0, 0) # border_layout.addWidget(border_color_label, 0, 0) border_row_top = QHBoxLayout() border_row_top.setContentsMargins(0, 0, 0, 0) border_row_top.setSpacing(4) border_r_spin = QDoubleSpinBox() border_r_spin.setRange(0.0, 1.0) border_r_spin.setSingleStep(0.01) border_r_spin.setValue(current_border_color[0]) color_r_bg = QLabel("R:") color_r_bg.setAlignment(Qt.AlignVCenter) color_r_bg.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) border_row_top.addWidget(color_r_bg) border_row_top.addWidget(border_r_spin) border_g_spin = QDoubleSpinBox() border_g_spin.setRange(0.0, 1.0) border_g_spin.setSingleStep(0.01) border_g_spin.setValue(current_border_color[1]) color_g = QLabel("G:") color_g.setAlignment(Qt.AlignVCenter) color_g.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) border_row_top.addWidget(color_g) border_row_top.addWidget(border_g_spin) border_layout.addLayout(border_row_top, 0, 1, 1, 3) indent_placeholder = QLabel("") border_layout.addWidget(indent_placeholder, 1, 0) # indent_placeholder.setFixedWidth(border_color_label.sizeHint().width()) # border_layout.addWidget(indent_placeholder, 1, 0) border_row_bottom = QHBoxLayout() border_row_bottom.setContentsMargins(0, 0, 0, 0) border_row_bottom.setSpacing(4) border_b_spin = QDoubleSpinBox() border_b_spin.setRange(0.0, 1.0) border_b_spin.setSingleStep(0.01) border_b_spin.setValue(current_border_color[2]) color_b = QLabel("B:") color_b.setAlignment(Qt.AlignVCenter) color_b.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) border_row_bottom.addWidget(color_b) border_row_bottom.addWidget(border_b_spin) border_a_spin = QDoubleSpinBox() border_a_spin.setRange(0.0, 1.0) border_a_spin.setSingleStep(0.01) border_a_spin.setValue(current_border_color[3]) color_a = QLabel("A:") color_a.setAlignment(Qt.AlignVCenter) color_a.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) border_row_bottom.addWidget(color_a) border_row_bottom.addWidget(border_a_spin) border_layout.addLayout(border_row_bottom, 1, 1, 1, 3) border_color_button = QPushButton("选择边框颜色") border_color_button.clicked.connect(lambda: self._selectInfoPanelBorderColor( info_panel, border_r_spin, border_g_spin, border_b_spin, border_a_spin)) border_layout.addWidget(border_color_button, 2, 0, 1, 4) # 连接边框颜色变化信号 def onBorderColorChanged(): if hasattr(self.world, 'info_panel_manager'): border_color = ( border_r_spin.value(), border_g_spin.value(), border_b_spin.value(), border_a_spin.value() ) # 根据面板类型选择更新方法 if is_3d_panel: self.world.info_panel_manager.update3DPanelProperties( panel_id, border_color=border_color) else: self.world.info_panel_manager.updatePanelProperties( panel_id, border_color=border_color) border_r_spin.valueChanged.connect(onBorderColorChanged) border_g_spin.valueChanged.connect(onBorderColorChanged) border_b_spin.valueChanged.connect(onBorderColorChanged) border_a_spin.valueChanged.connect(onBorderColorChanged) border_group.setLayout(border_layout) self._propertyLayout.addWidget(border_group) bg_color_group = QGroupBox("背景颜色设置") bg_color_layout = QGridLayout() bg_color_layout.setColumnMinimumWidth(0, self.column_minimum_width) bg_color_label = QLabel("背景颜色:") bg_color_layout.addWidget(bg_color_label, 0, 0) bg_color_row_top = QHBoxLayout() bg_color_row_top.setContentsMargins(0, 0, 0, 0) bg_color_row_top.setSpacing(4) # R分量 color_r_bg = QLabel("R:") color_r_bg.setAlignment(Qt.AlignVCenter) color_r_bg.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) r_spin = QDoubleSpinBox() r_spin.setRange(0.0, 1.0) r_spin.setSingleStep(0.01) r_spin.setValue(current_bg_color[0]) bg_color_row_top.addWidget(color_r_bg) bg_color_row_top.addWidget(r_spin) # G分量 color_g_bg = QLabel("G:") color_g_bg.setAlignment(Qt.AlignVCenter) color_g_bg.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) g_spin = QDoubleSpinBox() g_spin.setRange(0.0, 1.0) g_spin.setSingleStep(0.01) g_spin.setValue(current_bg_color[1]) bg_color_row_top.addWidget(color_g_bg) bg_color_row_top.addWidget(g_spin) bg_color_layout.addLayout(bg_color_row_top, 0, 1, 1, 3) indent_placeholder = QLabel("") bg_color_layout.addWidget(indent_placeholder, 1, 0) bg_color_row_bottom = QHBoxLayout() bg_color_row_bottom.setContentsMargins(0, 0, 0, 0) bg_color_row_bottom.setSpacing(4) # B分量 color_b_bg = QLabel("B:") color_b_bg.setAlignment(Qt.AlignVCenter) color_b_bg.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) b_spin = QDoubleSpinBox() b_spin.setRange(0.0, 1.0) b_spin.setSingleStep(0.01) b_spin.setValue(current_bg_color[2]) bg_color_row_bottom.addWidget(color_b_bg) bg_color_row_bottom.addWidget(b_spin) # A分量 color_a_bg = QLabel("A:") color_a_bg.setAlignment(Qt.AlignVCenter) color_a_bg.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) a_spin = QDoubleSpinBox() a_spin.setRange(0.0, 1.0) a_spin.setSingleStep(0.01) a_spin.setValue(current_bg_color[3]) bg_color_row_bottom.addWidget(color_a_bg) bg_color_row_bottom.addWidget(a_spin) bg_color_layout.addLayout(bg_color_row_bottom, 1, 1, 1, 3) # 颜色选择按钮 color_button = QPushButton("选择颜色") color_button.clicked.connect(lambda: self._selectInfoPanelBackgroundColor( info_panel, r_spin, g_spin, b_spin, a_spin)) bg_color_layout.addWidget(color_button, 2, 0, 1, 4) # 连接颜色数值变化信号,自动更新背景颜色 def onBackgroundColorChanged(): self._applyInfoPanelBackgroundColor( info_panel, r_spin.value(), g_spin.value(), b_spin.value(), a_spin.value()) r_spin.valueChanged.connect(onBackgroundColorChanged) g_spin.valueChanged.connect(onBackgroundColorChanged) b_spin.valueChanged.connect(onBackgroundColorChanged) a_spin.valueChanged.connect(onBackgroundColorChanged) bg_color_group.setLayout(bg_color_layout) self._propertyLayout.addWidget(bg_color_group) # 标题属性组 title_group = QGroupBox("标题属性") title_layout = QGridLayout() title_layout.setColumnMinimumWidth(0, self.column_minimum_width) # 标题文本 title_layout.addWidget(QLabel("标题文本:"), 0, 0) title_text_edit = QTextEdit() title_text_edit.setMaximumHeight(60) title_text_edit.setPlainText(current_title_text) title_layout.addWidget(title_text_edit, 0, 1, 1, 3) # 标题颜色 title_layout.addWidget(QLabel("标题颜色:"), 1, 0) title_color_row = QHBoxLayout() # 占位,保持对齐 title_color_row.setContentsMargins(0, 0, 0, 0) title_color_row.setSpacing(4) # 标题R分量 title_color_label_r = QLabel("R:") title_color_label_r.setAlignment(Qt.AlignVCenter) title_color_label_r.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) title_color_row.addWidget(title_color_label_r) title_r_spin = QDoubleSpinBox() title_r_spin.setRange(0.0, 1.0) title_r_spin.setSingleStep(0.01) title_r_spin.setValue(current_title_color[0]) title_color_row.addWidget(title_r_spin) # 标题G分量 title_color_label_g = QLabel("G:") title_color_label_g.setAlignment(Qt.AlignVCenter) title_color_label_g.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) title_color_row.addWidget(title_color_label_g) title_g_spin = QDoubleSpinBox() title_g_spin.setRange(0.0, 1.0) title_g_spin.setSingleStep(0.01) title_g_spin.setValue(current_title_color[1]) title_color_row.addWidget(title_g_spin) title_layout.addLayout(title_color_row, 1, 1, 1, 3) title_layout.addWidget(QLabel(""), 2, 0) # 占位 title_color_row_ = QHBoxLayout() # 占位,保持对齐 title_color_row_.setContentsMargins(0, 0, 0, 0) title_color_row_.setSpacing(4) # 标题B分量 title_color_label_b = QLabel("B:") title_color_label_b.setAlignment(Qt.AlignVCenter) title_color_label_b.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) title_color_row_.addWidget(title_color_label_b) title_b_spin = QDoubleSpinBox() title_b_spin.setRange(0.0, 1.0) title_b_spin.setSingleStep(0.01) title_b_spin.setValue(current_title_color[2]) title_color_row_.addWidget(title_b_spin) # 标题A分量 title_color_label_a = QLabel("A:") title_color_label_a.setAlignment(Qt.AlignVCenter) title_color_label_a.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) title_color_row_.addWidget(title_color_label_a) title_a_spin = QDoubleSpinBox() title_a_spin.setRange(0.0, 1.0) title_a_spin.setSingleStep(0.01) title_a_spin.setValue(current_title_color[3]) title_color_row_.addWidget(title_a_spin) title_layout.addLayout(title_color_row_, 2, 1, 1, 3) # 标题颜色选择按钮 title_color_button = QPushButton("选择标题颜色") title_color_button.clicked.connect(lambda: self._selectInfoPanelTitleColor( info_panel, title_r_spin, title_g_spin, title_b_spin, title_a_spin)) title_layout.addWidget(title_color_button, 4, 0, 1, 4) # 标题字体大小 title_layout.addWidget(QLabel("标题字体大小:"), 5, 0) title_size_spin = QDoubleSpinBox() title_size_spin.setRange(0.01, 1.0) title_size_spin.setSingleStep(0.01) title_size_spin.setValue(current_title_size) title_layout.addWidget(title_size_spin, 5, 1, 1, 3) # 标题位置控制 (仅对2D面板) if not is_3d_panel: title_position_row = QHBoxLayout() title_position_row.setContentsMargins(0, 0, 0, 0) title_position_row.setSpacing(4) title_position_label = QLabel("标题位置") title_layout.addWidget(title_position_label, 6, 0) title_x_spin = QDoubleSpinBox() title_x_spin.setRange(-1.0, 1.0) title_x_spin.setSingleStep(0.01) title_x_spin.setValue(current_title_pos[0]) title_y_spin = QDoubleSpinBox() title_y_spin.setRange(-1.0, 1.0) title_y_spin.setSingleStep(0.01) title_y_spin.setValue(current_title_pos[1]) for axis_label, spinbox in (("X:", title_x_spin), ("Y:", title_y_spin)): label = QLabel(axis_label) label.setAlignment(Qt.AlignVCenter) label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) title_position_row.addWidget(label) title_position_row.addWidget(spinbox) title_layout.addLayout(title_position_row, 6, 1, 1, 3) # 连接标题属性变化信号 def onTitleTextChanged(): if hasattr(self.world, 'info_panel_manager'): # 根据面板类型选择更新方法 if is_3d_panel: self.world.info_panel_manager.update3DPanelContent( panel_id, title=title_text_edit.toPlainText()) else: self.world.info_panel_manager.updatePanelContent( panel_id, title=title_text_edit.toPlainText()) def onTitlePropertyChanged(): # 根据面板类型选择更新方法 if is_3d_panel: self._applyInfoPanel3DTitleProperties( info_panel, title_r_spin.value(), title_g_spin.value(), title_b_spin.value(), title_a_spin.value(), title_size_spin.value()) else: self._applyInfoPanelTitleProperties( info_panel, title_r_spin.value(), title_g_spin.value(), title_b_spin.value(), title_a_spin.value(), title_size_spin.value()) def onTitlePositionChanged(): # 仅对2D面板更新标题位置 if not is_3d_panel and hasattr(self.world, 'info_panel_manager'): panel_data = self.world.info_panel_manager.panels.get(panel_id) if panel_data and 'title_label' in panel_data: # 设置标题位置 panel_data['title_label'].setPos(title_x_spin.value(), 0, title_y_spin.value()) title_text_edit.textChanged.connect(onTitleTextChanged) title_r_spin.valueChanged.connect(onTitlePropertyChanged) title_g_spin.valueChanged.connect(onTitlePropertyChanged) title_b_spin.valueChanged.connect(onTitlePropertyChanged) title_a_spin.valueChanged.connect(onTitlePropertyChanged) title_size_spin.valueChanged.connect(onTitlePropertyChanged) if not is_3d_panel: # 仅对2D面板连接位置变化信号 title_x_spin.valueChanged.connect(onTitlePositionChanged) title_y_spin.valueChanged.connect(onTitlePositionChanged) title_group.setLayout(title_layout) self._propertyLayout.addWidget(title_group) # 内容属性组 content_group = QGroupBox("内容属性") content_layout = QGridLayout() content_layout.setColumnMinimumWidth(0, self.column_minimum_width) # 内容文本 content_layout.addWidget(QLabel("内容文本:"), 0, 0) content_text_edit = QTextEdit() content_text_edit.setMaximumHeight(100) content_text_edit.setPlainText(current_content_text) content_layout.addWidget(content_text_edit, 0, 1, 1, 3) # 内容颜色 content_layout.addWidget(QLabel("内容颜色:"), 1, 0) content_color_row = QHBoxLayout() content_color_row.setContentsMargins(0, 0, 0, 0) content_color_row.setSpacing(4) # 内容R分量 content_r = QLabel("R:") content_r.setAlignment(Qt.AlignVCenter) content_r.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) content_color_row.addWidget(content_r) content_r_spin = QDoubleSpinBox() content_r_spin.setRange(0.0, 1.0) content_r_spin.setSingleStep(0.01) content_r_spin.setValue(current_content_color[0]) content_color_row.addWidget(content_r_spin) # 内容G分量 content_g = QLabel("G:") content_g.setAlignment(Qt.AlignVCenter) content_g.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) content_color_row.addWidget(content_g) content_g_spin = QDoubleSpinBox() content_g_spin.setRange(0.0, 1.0) content_g_spin.setSingleStep(0.01) content_g_spin.setValue(current_content_color[1]) content_color_row.addWidget(content_g_spin) content_layout.addLayout(content_color_row, 1, 1, 1, 3) content_color_row_ = QHBoxLayout() content_color_row_.setContentsMargins(0, 0, 0, 0) content_color_row_.setSpacing(4) content_layout.addWidget(QLabel(":"), 2, 0) # 内容B分量 content_b = QLabel("B:") content_b.setAlignment(Qt.AlignVCenter) content_b.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) content_color_row_.addWidget(content_b) content_b_spin = QDoubleSpinBox() content_b_spin.setRange(0.0, 1.0) content_b_spin.setSingleStep(0.01) content_b_spin.setValue(current_content_color[2]) content_color_row_.addWidget(content_b_spin) # 内容A分量 content_a = QLabel("A:") content_a.setAlignment(Qt.AlignVCenter) content_a.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) content_color_row_.addWidget(content_a) content_a_spin = QDoubleSpinBox() content_a_spin.setRange(0.0, 1.0) content_a_spin.setSingleStep(0.01) content_a_spin.setValue(current_content_color[3]) content_color_row_.addWidget(content_a_spin) content_layout.addLayout(content_color_row_, 2, 1, 1, 3) # 内容颜色选择按钮 content_color_button = QPushButton("选择内容颜色") content_color_button.clicked.connect(lambda: self._selectInfoPanelContentColor( info_panel, content_r_spin, content_g_spin, content_b_spin, content_a_spin)) content_layout.addWidget(content_color_button, 4, 0, 1, 4) # 内容字体大小 content_layout.addWidget(QLabel("内容字体大小:"), 5, 0) content_size_spin = QDoubleSpinBox() content_size_spin.setRange(0.01, 1.0) content_size_spin.setSingleStep(0.01) content_size_spin.setValue(current_content_size) content_layout.addWidget(content_size_spin, 5, 1, 1, 3) # 内容位置控制 (仅对2D面板) if not is_3d_panel: content_layout.addLayout(QLabel("内容位置:"), 6, 0) content_pos_layout = QHBoxLayout() content_pos_layout.setContentsMargins(0, 0, 0, 0) content_pos_layout.setSpacing(4) content_x = QLabel("X:") content_x.setAlignment(Qt.AlignVCenter) content_x.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) content_pos_layout.addWidget(content_x) content_x_spin = QDoubleSpinBox() content_x_spin.setRange(-1.0, 1.0) content_x_spin.setSingleStep(0.01) content_x_spin.setValue(current_content_pos[0]) content_pos_layout.addWidget(content_x_spin) content_y = QLabel("Y:") content_y.setAlignment(Qt.AlignVCenter) content_y.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) content_pos_layout.addWidget(content_y) content_y_spin = QDoubleSpinBox() content_y_spin.setRange(-1.0, 1.0) content_y_spin.setSingleStep(0.01) content_y_spin.setValue(current_content_pos[1]) content_pos_layout.addWidget(content_y_spin) content_layout.addLayout(content_pos_layout, 6, 1, 1, 3) # 连接内容属性变化信号 def onContentTextChanged(): if hasattr(self.world, 'info_panel_manager'): # 根据面板类型选择更新方法 if is_3d_panel: self.world.info_panel_manager.update3DPanelContent( panel_id, content=content_text_edit.toPlainText()) else: self.world.info_panel_manager.updatePanelContent( panel_id, content=content_text_edit.toPlainText()) def onContentPropertyChanged(): # 根据面板类型选择更新方法 if is_3d_panel: self._applyInfoPanel3DContentProperties( info_panel, content_r_spin.value(), content_g_spin.value(), content_b_spin.value(), content_a_spin.value(), content_size_spin.value()) else: self._applyInfoPanelContentProperties( info_panel, content_r_spin.value(), content_g_spin.value(), content_b_spin.value(), content_a_spin.value(), content_size_spin.value()) def onContentPositionChanged(): # 仅对2D面板更新内容位置 if not is_3d_panel and hasattr(self.world, 'info_panel_manager'): panel_data = self.world.info_panel_manager.panels.get(panel_id) if panel_data and 'content_label' in panel_data: # 设置内容位置 panel_data['content_label'].setPos(content_x_spin.value(), 0, content_y_spin.value()) def adjustContentWordwrap(): """根据面板宽度和字体大小调整内容换行""" # 仅对2D面板调整换行 if not is_3d_panel: panel_width = width_spin.value() font_size = content_size_spin.value() self._adjustContentWordwrap(panel_id, panel_width, font_size) # 连接相关信号 content_r_spin.valueChanged.connect(onContentPropertyChanged) content_g_spin.valueChanged.connect(onContentPropertyChanged) content_b_spin.valueChanged.connect(onContentPropertyChanged) content_a_spin.valueChanged.connect(onContentPropertyChanged) # 内容字体大小变化信号连接 content_size_spin.valueChanged.connect(onContentPropertyChanged) # 内容文本变化信号连接 content_text_edit.textChanged.connect(onContentTextChanged) # 标题相关信号连接 title_text_edit.textChanged.connect(onTitleTextChanged) title_r_spin.valueChanged.connect(onTitlePropertyChanged) title_g_spin.valueChanged.connect(onTitlePropertyChanged) title_b_spin.valueChanged.connect(onTitlePropertyChanged) title_a_spin.valueChanged.connect(onTitlePropertyChanged) title_size_spin.valueChanged.connect(onTitlePropertyChanged) if not is_3d_panel: # 仅对2D面板连接位置变化信号 title_x_spin.valueChanged.connect(onTitlePositionChanged) title_y_spin.valueChanged.connect(onTitlePositionChanged) content_x_spin.valueChanged.connect(onContentPositionChanged) content_y_spin.valueChanged.connect(onContentPositionChanged) content_group.setLayout(content_layout) self._propertyLayout.addWidget(content_group) # 存储控件引用,用于后续更新 self._current_info_panel = info_panel self._info_panel_controls = { 'title_text_edit': title_text_edit, 'content_text_edit': content_text_edit, 'title_r_spin': title_r_spin, 'title_g_spin': title_g_spin, 'title_b_spin': title_b_spin, 'title_a_spin': title_a_spin, 'content_r_spin': content_r_spin, 'content_g_spin': content_g_spin, 'content_b_spin': content_b_spin, 'content_a_spin': content_a_spin, 'title_size_spin': title_size_spin, 'content_size_spin': content_size_spin, 'width_spin': width_spin, 'height_spin': height_spin } # 对于2D面板,添加位置控件引用 if not is_3d_panel: self._info_panel_controls.update({ 'title_x_spin': title_x_spin, 'title_y_spin': title_y_spin, 'content_x_spin': content_x_spin, 'content_y_spin': content_y_spin }) # 启动定时器定期检查信息面板内容变化 self._startInfoPanelMonitoring(info_panel, panel_id) # 初始调整内容换行 (仅对2D面板) if not is_3d_panel: adjustContentWordwrap() print(f"✅ 信息面板属性面板已添加 (类型: {'3D' if is_3d_panel else '2D'})") except Exception as e: print(f"❌ 添加信息面板属性面板失败: {e}") import traceback traceback.print_exc() def _applyInfoPanel3DTitleProperties(self, info_panel, r, g, b, a, size): """应用3D信息面板标题属性""" try: panel_id = info_panel.getTag("panel_id") if not panel_id or not hasattr(self.world, 'info_panel_manager'): return # 更新3D面板标题属性 self.world.info_panel_manager.update3DPanelProperties( panel_id, title_color=(r, g, b, a), title_size=size ) except Exception as e: print(f"应用3D信息面板标题属性失败: {e}") def _applyInfoPanel3DContentProperties(self, info_panel, r, g, b, a, size): """应用3D信息面板内容属性""" try: panel_id = info_panel.getTag("panel_id") if not panel_id or not hasattr(self.world, 'info_panel_manager'): return # 更新3D面板内容属性 self.world.info_panel_manager.update3DPanelProperties( panel_id, content_color=(r, g, b, a), content_size=size ) except Exception as e: print(f"应用3D信息面板内容属性失败: {e}") def _adjustContentWordwrap(self, panel_id, panel_width, font_size): """调整信息面板内容的换行设置""" try: if hasattr(self.world, 'info_panel_manager'): panel_data = self.world.info_panel_manager.panels.get(panel_id) if panel_data and 'content_label' in panel_data: # 计算合适的换行值 - 根据面板宽度和字体大小动态调整 base_wordwrap = panel_width * 10 # 基础换行值 # 根据字体大小调整换行值(字体越大,每行字符越少) size_factor = 0.045 / font_size if font_size > 0 else 1.0 adjusted_wordwrap = base_wordwrap * size_factor # 确保换行值在合理范围内 wordwrap_value = max(5.0, min(100.0, adjusted_wordwrap)) # 应用换行设置 panel_data['content_label']['text_wordwrap'] = wordwrap_value print( f"调整内容换行: 面板宽度={panel_width:.2f}, 字体大小={font_size:.3f}, 换行值={wordwrap_value:.2f}") except Exception as e: print(f"调整内容换行失败: {e}") def _startInfoPanelMonitoring(self, info_panel, panel_id): """启动信息面板内容监控""" try: # 使用 QTimer 定期检查信息面板内容变化 if not hasattr(self, '_info_panel_timer'): from PyQt5.QtCore import QTimer self._info_panel_timer = QTimer() self._info_panel_timer.timeout.connect(lambda: self._checkInfoPanelUpdates(panel_id)) self._info_panel_timer.start(500) # 每500毫秒检查一次 except Exception as e: print(f"❌ 启动信息面板监控失败: {e}") def _checkInfoPanelUpdates(self, panel_id): """检查信息面板内容更新""" try: # 检查是否仍有有效的控件引用 if not hasattr(self, '_info_panel_controls') or not self._info_panel_controls: return # 检查是否仍有有效的info_panel_manager if not hasattr(self.world, 'info_panel_manager'): return # 获取当前面板数据 panel_data = self.world.info_panel_manager.panels.get(panel_id) if not panel_data: return # 获取控件引用 controls = self._info_panel_controls # 检查并更新标题文本 if 'title_label' in panel_data and panel_data['title_label']: current_title_text = panel_data['title_label']['text'] title_text_edit = controls.get('title_text_edit') if title_text_edit and title_text_edit.toPlainText() != current_title_text: title_text_edit.blockSignals(True) title_text_edit.setPlainText(current_title_text) title_text_edit.blockSignals(False) # 检查并更新内容文本 if 'content_label' in panel_data and panel_data['content_label']: current_content_text = panel_data['content_label']['text'] content_text_edit = controls.get('content_text_edit') if content_text_edit and content_text_edit.toPlainText() != current_content_text: content_text_edit.blockSignals(True) content_text_edit.setPlainText(current_content_text) content_text_edit.blockSignals(False) # 检查并更新标题位置 if 'title_label' in panel_data and panel_data['title_label']: title_pos = panel_data['title_label'].getPos() current_title_pos = (title_pos.getX(), title_pos.getZ()) title_x_spin = controls.get('title_x_spin') title_y_spin = controls.get('title_y_spin') if title_x_spin and abs(title_x_spin.value() - current_title_pos[0]) > 0.001: title_x_spin.blockSignals(True) title_x_spin.setValue(current_title_pos[0]) title_x_spin.blockSignals(False) if title_y_spin and abs(title_y_spin.value() - current_title_pos[1]) > 0.001: title_y_spin.blockSignals(True) title_y_spin.setValue(current_title_pos[1]) title_y_spin.blockSignals(False) # 检查并更新内容位置 if 'content_label' in panel_data and panel_data['content_label']: content_pos = panel_data['content_label'].getPos() current_content_pos = (content_pos.getX(), content_pos.getZ()) content_x_spin = controls.get('content_x_spin') content_y_spin = controls.get('content_y_spin') if content_x_spin and abs(content_x_spin.value() - current_content_pos[0]) > 0.001: content_x_spin.blockSignals(True) content_x_spin.setValue(current_content_pos[0]) content_x_spin.blockSignals(False) if content_y_spin and abs(content_y_spin.value() - current_content_pos[1]) > 0.001: content_y_spin.blockSignals(True) content_y_spin.setValue(current_content_pos[1]) content_y_spin.blockSignals(False) except Exception as e: print(f"❌ 检查信息面板更新失败: {e}") def _selectInfoPanelBorderColor(self, info_panel, r_spin, g_spin, b_spin, a_spin): """选择信息面板边框颜色""" try: from PyQt5.QtWidgets import QColorDialog from PyQt5.QtGui import QColor # 获取当前颜色值 current_color = QColor( int(r_spin.value() * 255), int(g_spin.value() * 255), int(b_spin.value() * 255), int(a_spin.value() * 255) ) # 打开颜色选择对话框 color = QColorDialog.getColor(current_color, None, "选择边框颜色") if color.isValid(): # 更新数值框 r_spin.setValue(color.redF()) g_spin.setValue(color.greenF()) b_spin.setValue(color.blueF()) a_spin.setValue(color.alphaF()) except Exception as e: print(f"❌ 选择边框颜色失败: {e}") def updateInfoPanelBackgroundImage(self, info_panel, image_path): """更新信息面板背景图片 - 修复版""" try: panel_id = info_panel.getTag("panel_id") if not panel_id: print("❌ 无法找到信息面板ID") return False # 通过 info_panel_manager 更新背景图片 if hasattr(self.world, 'info_panel_manager'): info_panel_manager = self.world.info_panel_manager # 检查是否是3D信息面板 gui_type = info_panel.getTag("gui_type") is_3d_panel = gui_type == "info_panel_3d" if is_3d_panel: # 对于3D信息面板,使用专门的方法 return self._update3DInfoPanelBackgroundImage(panel_id, info_panel, image_path) else: # 对于2D信息面板,使用原有方法 if image_path: # 设置新的背景图片 success = info_panel_manager.setPanelBackgroundImage(panel_id, image_path) if success: print(f"✅ 成功设置信息面板背景图片: {image_path}") return True else: print(f"❌ 设置信息面板背景图片失败: {image_path}") return False else: # 清除背景图片 success = info_panel_manager.setPanelBackgroundImage(panel_id, None) if success: print("✅ 成功清除信息面板背景图片") return True else: print("❌ 清除信息面板背景图片失败") return False else: print("❌ 未找到 info_panel_manager") return False except Exception as e: print(f"❌ 更新信息面板背景图片失败: {e}") import traceback traceback.print_exc() return False def _applyInfoPanelBackgroundImage(self, panel_node, image_path, panel_data): """应用信息面板背景图片 - 改进版""" try: from panda3d.core import CardMaker, TransparencyAttrib, Vec4 # 如果已有背景图片,先移除 if 'bg_image' in panel_data and panel_data['bg_image']: try: panel_data['bg_image'].removeNode() except: pass panel_data['bg_image'] = None # 加载纹理 texture = self.world.loader.loadTexture(image_path) if not texture: print(f"❌ 无法加载背景图片: {image_path}") return False # 设置纹理属性 texture.setMagfilter(texture.FTLinear) texture.setMinfilter(texture.FTLinearMipmapLinear) # 获取面板尺寸 size = panel_data.get('size', (1.0, 0.6)) # 创建背景卡片 cm = CardMaker('info_panel_background') cm.setFrame(-size[0] / 2, size[0] / 2, -size[1] / 2, size[1] / 2) # 生成几何体并创建节点 bg_node = panel_node.attachNewNode(cm.generate()) # 应用纹理 bg_node.setTexture(texture, 1) # 设置渲染属性 bg_node.setTransparency(TransparencyAttrib.MAlpha) bg_node.setBin("background", -10) bg_node.setDepthWrite(False) bg_node.setDepthTest(False) bg_node.setLightOff() bg_node.setTwoSided(True) # 确保在最底层 bg_node.setZ(-0.1) # 保存引用 panel_data['bg_image'] = bg_node print(f"✅ 成功应用信息面板背景图片: {image_path}") return True except Exception as e: print(f"❌ 应用信息面板背景图片失败: {e}") import traceback traceback.print_exc() return False def _update3DInfoPanelBackgroundImage(self, panel_id, info_panel, image_path): """更新3D信息面板背景图片 - 完整修复版""" try: # 从info_panel_manager获取面板数据 if not hasattr(self.world, 'info_panel_manager'): print("❌ 未找到 info_panel_manager") return False panel_data = self.world.info_panel_manager.panels.get(panel_id) if not panel_data: print(f"❌ 无法找到面板数据: {panel_id}") return False # 获取面板节点 panel_node = panel_data.get('node') if not panel_node: print("❌ 无法找到面板节点") return False # 如果已有背景图片,先销毁它 if 'bg_image' in panel_data and panel_data['bg_image']: try: # 如果是NodePath对象,使用removeNode方法 if hasattr(panel_data['bg_image'], 'removeNode'): panel_data['bg_image'].removeNode() # 如果是DirectGUI对象,使用destroy方法 elif hasattr(panel_data['bg_image'], 'destroy'): panel_data['bg_image'].destroy() except Exception as e: print(f"⚠️ 清理旧背景图片时出错: {e}") panel_data['bg_image'] = None # 如果image_path为None,只清除背景图片 if not image_path: # 清除节点标签 if info_panel.hasTag("bg_image_path"): info_panel.clearTag("bg_image_path") print("✅ 成功清除3D信息面板背景图片") return True # 使用 world.loader 加载新图片 texture = self.world.loader.loadTexture(image_path) if not texture: print(f"❌ 无法加载图片: {image_path}") return False # 设置纹理过滤 texture.setMagfilter(texture.FTLinear) texture.setMinfilter(texture.FTLinearMipmapLinear) # 获取面板大小 size = panel_data['properties'].get('size', (1.0, 0.6)) # 使用CardMaker创建卡片几何体 from panda3d.core import CardMaker, TransparencyAttrib cm = CardMaker('info_panel_bg') # 设置卡片大小,居中放置 cm.setFrame(-size[0] / 2, size[0] / 2, -size[1] / 2, size[1] / 2) bg_geom = cm.generate() # 创建节点并附加几何体,直接附加到面板节点 bg_node = panel_node.attachNewNode(bg_geom) # 应用纹理 bg_node.setTexture(texture, 1) # 设置正确的渲染属性 bg_node.setTransparency(TransparencyAttrib.MAlpha) bg_node.setBin("background", -10) # 确保在最底层 bg_node.setDepthWrite(False) bg_node.setDepthTest(False) bg_node.setLightOff() bg_node.setTwoSided(True) # 双面渲染 # 调整位置确保在面板内容后面 bg_node.setPos(0, 0.1, 0) # 稍微向后一点避免z-fighting # 保存引用 panel_data['bg_image'] = bg_node panel_data['properties']['bg_image'] = image_path # 更新节点标签 info_panel.setTag("bg_image_path", image_path) print(f"✅ 成功设置3D信息面板背景图片: {image_path}") return True except Exception as e: print(f"❌ 更新3D信息面板背景图片失败: {e}") import traceback traceback.print_exc() return False def _selectInfoPanelBackgroundColor(self, info_panel, r_spin, g_spin, b_spin, a_spin): """选择信息面板背景颜色""" try: from PyQt5.QtWidgets import QColorDialog from PyQt5.QtGui import QColor # 获取当前背景颜色 current_color = QColor( int(r_spin.value() * 255), int(g_spin.value() * 255), int(b_spin.value() * 255), int(a_spin.value() * 255) ) # 显示颜色选择对话框 color = QColorDialog.getColor(current_color, None, "选择背景颜色") if color.isValid(): # 更新数值框 r_spin.setValue(color.red() / 255.0) g_spin.setValue(color.green() / 255.0) b_spin.setValue(color.blue() / 255.0) a_spin.setValue(color.alpha() / 255.0) except Exception as e: print(f"选择背景颜色失败: {e}") def _applyInfoPanelBackgroundColor(self, info_panel, r, g, b, a): """应用信息面板背景颜色""" try: panel_id = info_panel.getTag("panel_id") if not panel_id: return # 更新InfoPanelManager中的背景颜色 if hasattr(self.world, 'info_panel_manager'): bg_color = (r, g, b, a) # 根据面板类型选择更新方法 gui_type = info_panel.getTag("gui_type") if gui_type == "info_panel_3d": self.world.info_panel_manager.update3DPanelProperties( panel_id, bg_color=bg_color) else: self.world.info_panel_manager.updatePanelProperties( panel_id, bg_color=bg_color) print(f"已更新信息面板 {panel_id} 的背景颜色为 ({r:.2f}, {g:.2f}, {b:.2f}, {a:.2f})") except Exception as e: print(f"应用背景颜色失败: {e}") def _selectInfoPanelTitleColor(self, info_panel, r_spin, g_spin, b_spin, a_spin): """选择信息面板标题颜色""" try: from PyQt5.QtWidgets import QColorDialog from PyQt5.QtGui import QColor # 获取当前颜色值 current_color = QColor( int(r_spin.value() * 255), int(g_spin.value() * 255), int(b_spin.value() * 255), int(a_spin.value() * 255) ) # 打开颜色选择对话框 color = QColorDialog.getColor(current_color, None, "选择标题颜色") if color.isValid(): # 更新数值框 r_spin.setValue(color.redF()) g_spin.setValue(color.greenF()) b_spin.setValue(color.blueF()) a_spin.setValue(color.alphaF()) except Exception as e: print(f"❌ 选择标题颜色失败: {e}") def _selectInfoPanelContentColor(self, info_panel, r_spin, g_spin, b_spin, a_spin): """选择信息面板内容颜色""" try: from PyQt5.QtWidgets import QColorDialog from PyQt5.QtGui import QColor # 获取当前颜色值 current_color = QColor( int(r_spin.value() * 255), int(g_spin.value() * 255), int(b_spin.value() * 255), int(a_spin.value() * 255) ) # 打开颜色选择对话框 color = QColorDialog.getColor(current_color, None, "选择内容颜色") if color.isValid(): # 更新数值框 r_spin.setValue(color.redF()) g_spin.setValue(color.greenF()) b_spin.setValue(color.blueF()) a_spin.setValue(color.alphaF()) except Exception as e: print(f"❌ 选择内容颜色失败: {e}") def _applyInfoPanelTitleProperties(self, info_panel, r, g, b, a, size): """应用信息面板标题属性""" try: panel_id = info_panel.getTag("panel_id") if not panel_id: print("❌ 无法找到信息面板ID") return False # 通过 info_panel_manager 更新标题属性 if hasattr(self.world, 'info_panel_manager'): info_panel_manager = self.world.info_panel_manager # 根据面板类型选择不同的更新方法 gui_type = info_panel.getTag("gui_type") if gui_type == "info_panel_3d": # 3D面板使用update3DPanelProperties info_panel_manager.update3DPanelProperties( panel_id, title_color=(r, g, b, a), title_size=size ) else: # 2D面板使用updatePanelProperties info_panel_manager.updatePanelProperties( panel_id, title_color=(r, g, b, a), title_size=size ) print(f"✓ 信息面板标题属性已更新: 颜色RGBA({r:.2f}, {g:.2f}, {b:.2f}, {a:.2f}), 大小{size:.3f}") return True else: print("❌ 无法找到 info_panel_manager") return False except Exception as e: print(f"❌ 应用信息面板标题属性失败: {e}") return False def _applyInfoPanelContentProperties(self, info_panel, r, g, b, a, size): """应用信息面板内容属性""" try: panel_id = info_panel.getTag("panel_id") if not panel_id: print("❌ 无法找到信息面板ID") return False # 通过 info_panel_manager 更新内容属性 if hasattr(self.world, 'info_panel_manager'): info_panel_manager = self.world.info_panel_manager # 根据面板类型选择不同的更新方法 gui_type = info_panel.getTag("gui_type") if gui_type == "info_panel_3d": # 3D面板使用update3DPanelProperties info_panel_manager.update3DPanelProperties( panel_id, content_color=(r, g, b, a), content_size=size ) else: # 2D面板使用updatePanelProperties info_panel_manager.updatePanelProperties( panel_id, content_color=(r, g, b, a), content_size=size ) # print(f"✓ 信息面板内容属性已更新: 颜色RGBA({r:.2f}, {g:.2f}, {b:.2f}, {a:.2f}), 大小{size:.3f}") return True else: print("❌ 无法找到 info_panel_manager") return False except Exception as e: print(f"❌ 应用信息面板内容属性失败: {e}") return False def _addSphericalVideoProperties(self, spherical_video): """为球形视频添加属性控制面板""" try: from PyQt5.QtWidgets import (QGroupBox, QGridLayout, QPushButton, QLabel, QDoubleSpinBox, QFileDialog, QHBoxLayout, QSlider) from PyQt5.QtCore import Qt import os # 视频控制组 video_group = QGroupBox("视频控制") video_layout = QGridLayout() # 播放控制按钮 control_layout = QHBoxLayout() self.play_button = QPushButton("▶️ 播放") self.play_button.clicked.connect(lambda: self._toggleSphericalVideoPlayback(spherical_video)) self.stop_button = QPushButton("⏹️ 停止") self.stop_button.clicked.connect(lambda: self._stopSphericalVideo(spherical_video)) control_layout.addWidget(self.play_button) control_layout.addWidget(self.stop_button) video_layout.addLayout(control_layout, 0, 0, 1, 2) # 视频进度控制 progress_layout = QHBoxLayout() self.progress_slider = QSlider(Qt.Horizontal) self.progress_slider.setRange(0, 100) self.progress_slider.setValue(0) self.progress_slider.sliderMoved.connect(lambda value: self._seekSphericalVideo(spherical_video, value)) self.time_label = QLabel("00:00 / 00:00") self.time_label.setStyleSheet("font-size: 10px; color: gray;") progress_layout.addWidget(self.progress_slider) progress_layout.addWidget(self.time_label) video_layout.addLayout(progress_layout, 1, 0, 1, 2) # 视频文件选择 file_layout = QHBoxLayout() self.select_video_button = QPushButton("选择视频文件") self.select_video_button.clicked.connect(lambda: self._selectSphericalVideoFile(spherical_video)) # 显示当前视频文件 current_video = spherical_video.getTag("video_path") if spherical_video.hasTag("video_path") else "" self.video_file_label = QLabel(os.path.basename(current_video) if current_video else "未选择视频") self.video_file_label.setStyleSheet("font-size: 9px; color: gray;") self.video_file_label.setWordWrap(True) file_layout.addWidget(self.select_video_button) file_layout.addWidget(self.video_file_label) video_layout.addLayout(file_layout, 2, 0, 1, 2) video_group.setLayout(video_layout) self._propertyLayout.addWidget(video_group) # 球体属性组 sphere_group = QGroupBox("球体属性") sphere_layout = QGridLayout() # 半径控制 sphere_layout.addWidget(QLabel("半径:"), 0, 0) radius_spin = QDoubleSpinBox() radius_spin.setRange(0.1, 100) radius_spin.setSingleStep(0.1) current_radius = float(spherical_video.getTag("original_radius") or "5.0") radius_spin.setValue(current_radius) radius_spin.valueChanged.connect(lambda value: self._updateSphericalVideoRadius(spherical_video, value)) sphere_layout.addWidget(radius_spin, 0, 1, 1, 3) sphere_group.setLayout(sphere_layout) self._propertyLayout.addWidget(sphere_group) # 存储控件引用 self._spherical_video_controls = { 'play_button': self.play_button, 'stop_button': self.stop_button, 'progress_slider': self.progress_slider, 'time_label': self.time_label, 'select_button': self.select_video_button, 'file_label': self.video_file_label } print("✅ 球形视频属性面板已添加") except Exception as e: print(f"❌ 添加球形视频属性面板失败: {e}") import traceback traceback.print_exc() def _toggleSphericalVideoPlayback(self, spherical_video): """切换球形视频播放状态""" try: # 获取视频纹理 movie_texture = spherical_video.getPythonTag("movie_texture") if not movie_texture: # 尝试从节点获取纹理 texture = spherical_video.getTexture() if texture: movie_texture = texture if not movie_texture: print("❌ 未找到视频纹理") return # 检查当前播放状态 if not hasattr(self, '_video_playing'): self._video_playing = {} video_id = id(spherical_video) is_playing = self._video_playing.get(video_id, False) if is_playing: if hasattr(movie_texture, 'stop'): movie_texture.stop() if hasattr(self, '_spherical_video_controls') and 'play_button' in self._spherical_video_controls: play_button = self._spherical_video_controls['play_button'] play_button.setText("▶️ 播放") print("⏸️ 球形视频已暂停") else: if hasattr(movie_texture, 'play'): movie_texture.play() if hasattr(self, '_spherical_video_controls') and 'play_button' in self._spherical_video_controls: play_button = self._spherical_video_controls['play_button'] play_button.setText("⏸️ 暂停") print("▶️ 球形视频开始播放") self._video_playing[video_id] = not is_playing except Exception as e: print(f"❌ 切换球形视频播放状态失败: {e}") def _stopSphericalVideo(self, spherical_video): """停止球形视频""" try: # 获取视频纹理 movie_texture = spherical_video.getPythonTag("movie_texture") if not movie_texture: # 尝试从节点获取纹理 texture = spherical_video.getTexture() if texture: movie_texture = texture if movie_texture: if hasattr(movie_texture, 'stop'): movie_texture.stop() # 重置到开头 if hasattr(movie_texture, 'setTime'): movie_texture.setTime(0.0) if hasattr(self, '_spherical_video_controls') and 'play_button' in self._spherical_video_controls: play_button = self._spherical_video_controls['play_button'] play_button.setText("▶️ 播放") if hasattr(self, '_video_playing'): video_id = id(spherical_video) self._video_playing[video_id] = False print("⏹️ 球形视频已停止") else: print("❌ 视频纹理不支持停止操作") else: print("❌ 未找到视频纹理") except Exception as e: print(f"❌ 停止球形视频失败: {e}") def _seekSphericalVideo(self, spherical_video, slider_value): """跳转到指定时间位置""" try: # 获取视频纹理 movie_texture = spherical_video.getPythonTag("movie_texture") if not movie_texture: # 尝试从节点获取纹理 texture = spherical_video.getTexture() if texture: movie_texture = texture if movie_texture and hasattr(movie_texture, 'setTime'): # 假设视频总时长为100秒(实际应该根据视频时长计算) time_seconds = slider_value movie_texture.setTime(time_seconds) print(f"⏰ 跳转到时间: {time_seconds}秒") # 更新时间标签 if hasattr(self, '_spherical_video_controls') and 'time_label' in self._spherical_video_controls: time_label = self._spherical_video_controls['time_label'] current_time = f"{int(time_seconds // 60):02d}:{int(time_seconds % 60):02d}" total_time = "01:40" # 假设总时长100秒 time_label.setText(f"{current_time} / {total_time}") else: print("❌ 视频纹理不支持时间控制") except Exception as e: print(f"❌ 球形视频跳转失败: {e}") def _updateSphericalVideoRadius(self, spherical_video, new_radius): """更新球形视频半径""" try: from panda3d.core import GeomNode # 移除旧的几何体 spherical_video.node().removeAllGeoms() # 创建新的球形几何体 new_geom = self.world.gui_manager._createSphereGeometry(new_radius, segments=32) spherical_video.node().addGeom(new_geom) # 更新标签 spherical_video.setTag("original_radius", str(new_radius)) print(f"🔄 球形视频半径已更新为: {new_radius}") except Exception as e: print(f"❌ 更新球形视频半径失败: {e}") def loadSphericalVideoFile(self, spherical_video, video_path): """为球形视频加载新的视频文件""" try: from panda3d.core import TextureStage, Texture import os if not os.path.exists(video_path): print(f"❌ 球形视频文件不存在: {video_path}") return False # 加载新的视频纹理 movie_texture = self.world.gui_manager._loadMovieTexture(video_path) if movie_texture: # 清除现有的纹理 spherical_video.clearTexture() # 设置视频纹理属性 movie_texture.setWrapU(Texture.WM_clamp) movie_texture.setWrapV(Texture.WM_clamp) movie_texture.setMinfilter(Texture.FT_linear) movie_texture.setMagfilter(Texture.FT_linear) # 为视频纹理创建专用的纹理阶段 texture_stage = TextureStage("spherical_video") texture_stage.setMode(TextureStage.MModulate) # 应用纹理到球体 spherical_video.setTexture(texture_stage, movie_texture) # 保存视频纹理引用 spherical_video.setPythonTag("movie_texture", movie_texture) spherical_video.setTag("video_path", video_path) # 设置为白色以正确显示视频 spherical_video.setColor(1, 1, 1, 1) print(f"✅ 成功加载新球形视频: {video_path}") return True else: print(f"❌ 无法加载球形视频文件: {video_path}") return False except Exception as e: print(f"❌ 加载球形视频文件失败: {e}") import traceback traceback.print_exc() return False def _selectSphericalVideoFile(self, spherical_video): """选择球形视频文件""" try: from PyQt5.QtWidgets import QFileDialog import os # 打开文件选择对话框 file_path, _ = QFileDialog.getOpenFileName( None, "选择360度视频文件", "", "视频文件 (*.mp4 *.avi *.mov *.wmv *.flv *.mkv *.webm);;所有文件 (*)" ) if file_path and os.path.exists(file_path): # 加载新视频文件 - 修复方法调用 success = self.loadSphericalVideoFile(spherical_video, file_path) if success: # 更新文件标签显示 if hasattr(self, '_spherical_video_controls') and 'file_label' in self._spherical_video_controls: file_label = self._spherical_video_controls['file_label'] file_label.setText(os.path.basename(file_path)) print(f"✅ 球形视频文件已更新为: {file_path}") else: print(f"❌ 无法加载球形视频文件: {file_path}") else: print("❌ 未选择有效的视频文件") except Exception as e: print(f"❌ 选择球形视频文件失败: {e}") import traceback traceback.print_exc() def _addVideoScreenProperties(self, video_screen, item): """添加视频屏幕属性面板""" try: from PyQt5.QtWidgets import (QGroupBox, QGridLayout, QPushButton, QLabel, QFileDialog, QHBoxLayout, QLineEdit) from PyQt5.QtCore import Qt import os video_info_group = QGroupBox("视频信息") video_info_layout = QGridLayout() video_info_layout.setColumnMinimumWidth(0, self.column_minimum_width) # 显示当前视频文件路径 - 改进版本 video_path = video_screen.getTag("video_path") if video_screen.hasTag("video_path") else "" if video_path: if video_path.startswith("http://") or video_path.startswith("https://"): # 显示URL信息 video_info_layout.addWidget(QLabel("视频流URL:"), 0, 0) path_label = QLabel(video_path) path_label.setWordWrap(True) if len(video_path)>30: display_path = video_path[:27]+"..." path_label.setToolTip(video_path) else: display_path = video_path path_label.setText(display_path) path_label.setStyleSheet("color: #00AAFF;") video_info_layout.addWidget(path_label, 0, 1, 1, 3) elif os.path.exists(video_path): # 显示本地文件信息 video_info_layout.addWidget(QLabel("视频文件:"), 0, 0) path_label = QLabel(os.path.basename(video_path)) path_label.setWordWrap(True) path_label.setStyleSheet("color: #00AAFF;") video_info_layout.addWidget(path_label, 0, 1, 1, 3) else: # 文件不存在 video_info_layout.addWidget(QLabel("状态:"), 0, 0) status_label = QLabel("文件不存在") status_label.setStyleSheet("color: red;") video_info_layout.addWidget(status_label, 0, 1, 1, 3) else: # 无视频 video_info_layout.addWidget(QLabel("状态:"), 0, 0) status_label = QLabel("无视频文件") status_label.setStyleSheet("color: red;") video_info_layout.addWidget(status_label, 0, 1, 1, 3) video_info_group.setLayout(video_info_layout) self._propertyLayout.addWidget(video_info_group) video_control_group = QGroupBox("视频控制") video_control_layout = QGridLayout() video_control_layout.setColumnMinimumWidth(0, self.column_minimum_width) video_control_h_layout = QHBoxLayout() play_btn = QPushButton("▶️ 播放") play_btn.clicked.connect(lambda: self.world.gui_manager.playVideo(video_screen)) video_control_h_layout.addWidget(play_btn) # 暂停按钮 pause_btn = QPushButton("⏸️ 暂停") pause_btn.clicked.connect(lambda: self.world.gui_manager.pauseVideo(video_screen)) video_control_h_layout.addWidget(pause_btn) # 停止按钮 stop_btn = QPushButton("⏹️ 停止") stop_btn.clicked.connect(lambda: self.world.gui_manager.stopVideo(video_screen)) video_control_h_layout.addWidget(stop_btn) video_control_layout.addLayout(video_control_h_layout, 0, 0, 1, 4) # 加载新视频按钮 load_btn = QPushButton("📁 加载新视频...") load_btn.clicked.connect(lambda: self._loadNewVideo(video_screen, item)) video_control_layout.addWidget(load_btn, 1, 0, 1, 4) video_control_group.setLayout(video_control_layout) self._propertyLayout.addWidget(video_control_group) # 添加URL输入区域 url_group = QGroupBox("网络视频") url_layout = QGridLayout() url_layout.setColumnMinimumWidth(0, self.column_minimum_width) self.url_input = QLineEdit() self.url_input.setPlaceholderText("输入视频流URL") url_layout.addWidget(self.url_input, 0, 0, 1, 3) load_url_btn = QPushButton("加载URL") self._current_load_url_btn_3d = load_url_btn load_url_btn.clicked.connect( lambda: self._loadVideoFromURLWithOpenCV_3D(video_screen, self.url_input.text().strip())) url_layout.addWidget(load_url_btn, 0, 3) url_group.setLayout(url_layout) self._propertyLayout.addWidget(url_group) except Exception as e: print(f"添加视频屏幕属性失败: {e}") def _add2DVideoScreenProperties(self, video_screen, item): """为2D视频屏幕添加属性控制面板""" try: from PyQt5.QtWidgets import (QGroupBox, QGridLayout, QPushButton, QLabel, QFileDialog, QHBoxLayout, QLineEdit) import os # 视频信息组 video_info_group = QGroupBox("2D视频信息") video_info_layout = QGridLayout() video_info_layout.setColumnMinimumWidth(0, self.column_minimum_width) # 显示当前视频文件路径 # 在显示视频信息时区分本地文件和网络URL video_path = video_screen.getTag("video_path") if video_path: if video_path.startswith("http://") or video_path.startswith("https://"): # 显示URL信息 video_info_layout.addWidget(QLabel("视频流URL:"), 0, 0) display_path = video_path if len(video_path)<=50 else video_path[:47]+"..." path_label = QLabel(display_path) path_label.setWordWrap(True) path_label.setStyleSheet("color: #00AAFF;") video_info_layout.addWidget(path_label, 0, 1, 1, 3) elif os.path.exists(video_path): # 显示本地文件信息 video_info_layout.addWidget(QLabel("视频文件:"), 0, 0) path_label = QLabel(os.path.basename(video_path)) path_label.setWordWrap(True) path_label.setStyleSheet("color: #00AAFF;") video_info_layout.addWidget(path_label, 0, 1, 1, 3) else: # 文件不存在 video_info_layout.addWidget(QLabel("状态:"), 0, 0) status_label = QLabel("文件不存在") status_label.setStyleSheet("color: red;") video_info_layout.addWidget(status_label, 0, 1, 1, 3) else: # 无视频 video_info_layout.addWidget(QLabel("状态:"), 0, 0) status_label = QLabel("无视频文件") status_label.setStyleSheet("color: red;") video_info_layout.addWidget(status_label, 0, 1, 1, 3) video_screen.setBin("fixed", 0) video_info_group.setLayout(video_info_layout) self._propertyLayout.addWidget(video_info_group) # 视频控制组 video_control_group = QGroupBox("2D视频控制") video_control_layout = QGridLayout() video_control_h_layout = QHBoxLayout() # 播放按钮 play_btn = QPushButton("▶️ 播放") play_btn.clicked.connect(lambda: self.play2DVideo(video_screen)) video_control_h_layout.addWidget(play_btn) # 暂停按钮 pause_btn = QPushButton("⏸️ 暂停") pause_btn.clicked.connect(lambda: self.pause2DVideo(video_screen)) video_control_h_layout.addWidget(pause_btn) # 停止按钮 stop_btn = QPushButton("⏹️ 停止") stop_btn.clicked.connect(lambda: self._stop2DVideo(video_screen)) video_control_h_layout.addWidget(stop_btn) video_control_layout.addLayout(video_control_h_layout, 0, 0, 1, 4) video_control_group.setLayout(video_control_layout) # 加载新视频按钮 load_btn = QPushButton("📁 加载新视频...") load_btn.clicked.connect(lambda: self._loadNew2DVideo(video_screen, item)) video_control_layout.addWidget(load_btn, 1, 0, 1, 4) self._propertyLayout.addWidget(video_control_group) # 添加URL输入区域 url_group = QGroupBox("网络视频") url_layout = QGridLayout() self.url_input = QLineEdit() self.url_input.setPlaceholderText("输入视频流URL") url_layout.addWidget(self.url_input, 0, 0, 1, 3) load_url_btn = QPushButton("加载URL") self._current_load_url_btn_2d = load_url_btn load_url_btn.clicked.connect( lambda: self._loadVideoFromURLWithOpenCV(video_screen, self.url_input.text().strip())) url_layout.addWidget(load_url_btn, 0, 3) url_group.setLayout(url_layout) self._propertyLayout.addWidget(url_group) except Exception as e: print(f"添加2D视频屏幕属性失败: {e}") def _loadVideoFromURLWithOpenCV(self, video_screen, url): """使用OpenCV从URL加载视频流并在2D视频屏幕上显示(推荐版)""" try: import cv2 import threading from panda3d.core import Texture, PNMImage import numpy as np import time from PyQt5.QtWidgets import QMessageBox load_url_btn = None if hasattr(self, '_current_load_url_btn_2d'): load_url_btn = self._current_load_url_btn_2d # 安全地更新按钮状态 def update_button_text(text): try: if load_url_btn and load_url_btn.isVisible(): load_url_btn.setText(text) return True except RuntimeError: # 按钮已被删除,忽略错误 return False def update_button_enabled(enabled): try: if load_url_btn and load_url_btn.isVisible(): load_url_btn.setEnabled(enabled) return True except RuntimeError: # 按钮已被删除,忽略错误 return False if load_url_btn: load_url_btn.setText("⏳ 加载中...") load_url_btn.setEnabled(False) from PyQt5.QtWidgets import QApplication QApplication.processEvents() # 停止之前可能正在播放的视频 self._stop2DVideo(video_screen) # 使用 OpenCV 打开视频流 cap = cv2.VideoCapture(url) if not cap.isOpened(): error_msg = f"无法打开视频流:\n{url}\n\n可能的原因:\n1. 网络连接问题\n2. 视频流地址无效\n3. 视频服务器不可用\n4. 防火墙阻止访问" QMessageBox.critical(None, "视频流错误", error_msg) print(f"❌ 无法打开视频流: {url}") # 恢复按钮状态 if load_url_btn: load_url_btn.setText("加载URL") load_url_btn.setEnabled(True) return False # 设置视频参数以提高性能 cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # 减少缓冲以降低延迟 # 创建纹理对象 texture = Texture("video_texture") texture.setMagfilter(Texture.FTLinear) texture.setMinfilter(Texture.FTLinear) texture.setWrapU(Texture.WMClamp) texture.setWrapV(Texture.WMClamp) # 保存视频信息 video_info = { 'capture': cap, 'texture': texture, 'playing': True } # 应用纹理到视频屏幕 video_screen.setTexture(texture, 1) video_screen.setPythonTag("video_info", video_info) video_screen.setTag("video_path", url) # 启动视频播放线程 def update_video_texture(): target_fps = 60 # 降低目标帧率以减少CPU使用 frame_time = 1.0 / target_fps frame_count = 0 while True: try: # 检查是否应该继续播放 if not (video_screen.hasPythonTag("video_info") and video_screen.getPythonTag("video_info").get('playing', False)): break video_info = video_screen.getPythonTag("video_info") cap = video_info['capture'] ret, frame = cap.read() if not ret: # 视频结束,重新开始播放 cap.set(cv2.CAP_PROP_POS_FRAMES, 0) continue frame_count += 1 # 调整帧大小以降低处理负担 frame_height, frame_width = frame.shape[:2] if frame_width > 256: # 限制最大宽度 scale = 256.0 / frame_width new_width = int(frame_width * scale) new_height = int(frame_height * scale) frame = cv2.resize(frame, (new_width, new_height)) # 转换颜色格式 (BGR to RGB) frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) # 获取帧尺寸 height, width = frame_rgb.shape[:2] # 创建 PNMImage 并设置数据 img = PNMImage(width, height, 3) img.set_maxval(255) # 使用逐行设置方法(稳定且兼容性好) for y in range(height): for x in range(width): r, g, b = frame_rgb[y, x] img.setXelVal(x, y, r, g, b) # 更新纹理 texture = video_info['texture'] if texture: texture.load(img) # 控制帧率 time.sleep(frame_time) except Exception as e: print(f"视频帧更新错误: {e}") import traceback traceback.print_exc() break print("视频播放线程结束") # 在单独线程中处理视频流 thread = threading.Thread(target=update_video_texture, daemon=True) thread.start() print(f"✅ 开始在2D视频屏幕上播放网络视频流: {url}") if load_url_btn: load_url_btn.setText("加载URL") load_url_btn.setEnabled(True) return True except ImportError: error_msg = "缺少必要的库,请安装: pip install opencv-python" QMessageBox.critical(None, "依赖库缺失", error_msg) print("❌ 缺少必要的库,请安装: pip install opencv-python") # 恢复按钮状态 try: if load_url_btn: load_url_btn.setText("加载URL") load_url_btn.setEnabled(True) except RuntimeError: pass # 按钮已被删除,忽略 return False except Exception as e: error_msg = f"加载网络视频失败:\n{str(e)}" QMessageBox.critical(None, "视频加载错误", error_msg) print(f"❌ 加载网络视频失败: {e}") import traceback traceback.print_exc() # 恢复按钮状态 try: if load_url_btn: load_url_btn.setText("加载URL") load_url_btn.setEnabled(True) except RuntimeError: pass # 按钮已被删除,忽略 return False def play2DVideo(self, video_screen): """播放2D视频""" try: # 检查是否已经有视频信息(OpenCV方式) if video_screen.hasPythonTag("video_info"): video_info = video_screen.getPythonTag("video_info") if video_info: video_info['playing'] = True print("▶️ 视频已恢复播放") return True # 检查是否是 MovieTexture 方式 movie_texture = video_screen.getPythonTag("movie_texture") if movie_texture and hasattr(movie_texture, 'play'): try: movie_texture.play() print("▶️ MovieTexture 视频开始播放") return True except Exception as play_error: print(f"⚠️ MovieTexture 播放视频时出错: {play_error}") # 如果没有视频信息,尝试从URL加载 video_path = video_screen.getTag("video_path") if video_path: # 根据路径判断是本地文件还是URL if video_path.startswith("http://") or video_path.startswith("https://"): return self._loadVideoFromURLWithOpenCV(video_screen, video_path) else: # 本地文件使用 MovieTexture 方式 return self.world.gui_manager.load2DVideoFile(video_screen, video_path) else: print("❌ 没有找到视频源") return False except Exception as e: print(f"❌ 播放视频失败: {e}") import traceback traceback.print_exc() return False def pause2DVideo(self, video_screen): """暂停2D视频""" try: if video_screen.hasPythonTag("video_info"): video_info = video_screen.getPythonTag("video_info") if video_info: video_info['playing'] = False print("⏸️ 视频已暂停") return True print("没有正在播放的视频") return False except Exception as e: print(f"❌ 暂停视频失败: {e}") return False def _stop2DVideo(self, video_screen): """停止2D视频(内部方法)""" try: # 停止视频播放 if video_screen.hasPythonTag("video_info"): video_info = video_screen.getPythonTag("video_info") if video_info: # 停止播放 video_info['playing'] = False # 释放视频捕获资源 if 'capture' in video_info and video_info['capture']: try: video_info['capture'].release() except: pass # 清理纹理 try: video_screen.clearTexture() except: pass video_screen.clearPythonTag("video_info") # 清理视频路径标签 if video_screen.hasTag("video_path"): video_screen.clearTag("video_path") print("⏹️ 视频已停止") return True except Exception as e: print(f"❌ 停止视频失败: {e}") return False def _loadNew2DVideo(self, video_screen, item): """为2D视频屏幕加载新视频文件""" try: file_path, _ = QFileDialog.getOpenFileName( None, "选择视频文件", "", "视频文件 (*.mp4 *.avi *.mov *.mkv *.webm *.ogg)" ) if file_path: success = self.world.gui_manager.load2DVideoFile(video_screen, file_path) if success: print(f"成功加载新视频: {file_path}") # 刷新属性面板以显示新视频信息 self.updateGUIPropertyPanel(video_screen, item) #self._stop2DVideo(video_screen) return True except Exception as e: print(f"加载新视频失败: {e}") return False def _loadVideoFromURLWithOpenCV_3D(self, video_screen, url): """使用OpenCV从URL加载视频流并在3D视频屏幕上显示""" try: import cv2 import threading from panda3d.core import Texture, PNMImage, TextureStage import time from PyQt5.QtWidgets import QMessageBox is_first_load = not video_screen.hasTag("video_path") or not video_screen.getTag("video_path") # 确保之前的视频流已停止 self._stop3DVideo(video_screen) load_url_btn = None if hasattr(self, '_current_load_url_btn_3d'): load_url_btn = self._current_load_url_btn_3d # 安全地更新按钮状态 def update_button_text(text): try: if load_url_btn and load_url_btn.isVisible(): load_url_btn.setText(text) return True except RuntimeError: # 按钮已被删除,忽略错误 return False def update_button_enabled(enabled): try: if load_url_btn and load_url_btn.isVisible(): load_url_btn.setEnabled(enabled) return True except RuntimeError: # 按钮已被删除,忽略错误 return False # 如果有按钮引用,改变按钮状态为加载中 if load_url_btn: update_button_text("⏳ 加载中...") update_button_enabled(False) from PyQt5.QtWidgets import QApplication QApplication.processEvents() # 使用 OpenCV 打开视频流 cap = cv2.VideoCapture(url) if not cap.isOpened(): error_msg = f"无法打开视频流:\n{url}\n\n可能的原因:\n1. 网络连接问题\n2. 视频流地址无效\n3. 视频服务器不可用\n4. 防火墙阻止访问" QMessageBox.critical(None, "视频流错误", error_msg) print(f"❌ 无法打开视频流: {url}") # 恢复按钮状态 update_button_text("加载URL") update_button_enabled(True) return False # 设置视频参数以提高性能 cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # 减少缓冲以降低延迟 # 创建纹理对象 texture = Texture("video_texture_3d") texture.setMagfilter(Texture.FTLinear) texture.setMinfilter(Texture.FTLinear) texture.setWrapU(Texture.WMClamp) texture.setWrapV(Texture.WMClamp) # 保存视频信息 video_info = { 'capture': cap, 'texture': texture, 'playing': True } # 创建纹理阶段并应用到3D视频屏幕 texture_stage = TextureStage("video_3d") texture_stage.setSort(0) texture_stage.setMode(TextureStage.MModulate) video_screen.setTexture(texture_stage, texture) # 设置为白色以正确显示视频 video_screen.setColor(1, 1, 1, 1) # 保存视频信息到PythonTag video_screen.setPythonTag("video_info", video_info) video_screen.setTag("video_path", url) # 启动视频播放线程 def update_video_texture(): target_fps = 60 # 降低目标帧率以减少CPU使用 frame_time = 1.0 / target_fps frame_count = 0 while True: try: # 检查是否应该继续播放 if not (video_screen.hasPythonTag("video_info") and video_screen.getPythonTag("video_info").get('playing', False)): break video_info = video_screen.getPythonTag("video_info") cap = video_info['capture'] ret, frame = cap.read() if not ret: # 视频结束,重新开始播放 cap.set(cv2.CAP_PROP_POS_FRAMES, 0) continue frame_count += 1 # 调整帧大小以降低处理负担 frame_height, frame_width = frame.shape[:2] if frame_width > 256: # 限制最大宽度 scale = 256.0 / frame_width new_width = int(frame_width * scale) new_height = int(frame_height * scale) frame = cv2.resize(frame, (new_width, new_height)) # 转换颜色格式 (BGR to RGB) frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) # 获取帧尺寸 height, width = frame_rgb.shape[:2] # 创建 PNMImage 并设置数据 img = PNMImage(width, height, 3) img.set_maxval(255) # 使用逐行设置方法(稳定且兼容性好) for y in range(height): for x in range(width): r, g, b = frame_rgb[y, x] img.setXelVal(x, y, r, g, b) # 更新纹理 texture = video_info['texture'] if texture: texture.load(img) # 控制帧率 time.sleep(frame_time) except Exception as e: print(f"3D视频帧更新错误: {e}") import traceback traceback.print_exc() break print("3D视频播放线程结束") # 在单独线程中处理视频流 thread = threading.Thread(target=update_video_texture, daemon=True) thread.start() print(f"✅ 开始在3D视频屏幕上播放网络视频流: {url}") # 恢复按钮状态 update_button_text("加载URL") update_button_enabled(True) return True except ImportError: error_msg = "缺少必要的库,请安装: pip install opencv-python" QMessageBox.critical(None, "依赖库缺失", error_msg) print("❌ 缺少必要的库,请安装: pip install opencv-python") # 恢复按钮状态 try: if load_url_btn: load_url_btn.setText("加载URL") load_url_btn.setEnabled(True) except RuntimeError: pass # 按钮已被删除,忽略 return False except Exception as e: error_msg = f"加载3D网络视频失败:\n{str(e)}" QMessageBox.critical(None, "视频加载错误", error_msg) print(f"❌ 加载3D网络视频失败: {e}") import traceback traceback.print_exc() # 恢复按钮状态 try: if load_url_btn: load_url_btn.setText("加载URL") load_url_btn.setEnabled(True) except RuntimeError: pass # 按钮已被删除,忽略 return False def _verifyVideoDisplay(self, video_screen, texture): try: applied_texture = video_screen.getTexture() if applied_texture and applied_texture.getName() == texture.getName(): print("视频纹理已正确应用到3d模型") return True else: print("视频纹理未正确应用到3d模型") return False except Exception as e: print(f"验证视频显示时出错:{e}") return False def _retryLoadVideoFromURLWithOpenCV_3D(self, video_screen, url): print("重新加载3D视频流") return self._loadVideoFromURLWithOpenCV_3D(video_screen, url) def _stop3DVideo(self, video_screen): """停止3D视频(内部方法)""" try: # 停止视频播放 if video_screen.hasPythonTag("video_info"): video_info = video_screen.getPythonTag("video_info") if video_info: # 停止播放 video_info['playing'] = False # 释放视频捕获资源 if 'capture' in video_info and video_info['capture']: try: video_info['capture'].release() except: pass # 清理纹理 try: video_screen.clearTexture() except: pass video_screen.clearPythonTag("video_info") # 清理视频路径标签 if video_screen.hasTag("video_path"): video_screen.clearTag("video_path") print("⏹️ 3D视频已停止") return True except Exception as e: print(f"❌ 停止3D视频失败: {e}") return False def _loadNewVideo(self, video_screen, item): try: file_path, _ = QFileDialog.getOpenFileName( None, "选择视频文件", "", "视频文件(*.mp4 *.avi *.mov *.mkv *.webm *.ogg)" ) if file_path: success = self.world.gui_manager.loadVideoFile(video_screen, file_path) if success: self.updateGUIPropertyPanel(video_screen, item) return True except Exception as e: print(f"加载新视频失败{e}") return False def editGUI2DPosition(self, gui_element, axis, value): """编辑2D GUI元素位置""" try: gui_type = gui_element.getTag("gui_type") if gui_type in ["button", "label", "entry", "2d_image"]: # 2D元素使用屏幕坐标,需要转换 current_pos = gui_element.getPos() if axis == "x": # 转换逻辑坐标到屏幕坐标 screen_x = value * 0.1 new_pos = (screen_x, current_pos.getY(), current_pos.getZ()) elif axis == "z": screen_z = value * 0.1 new_pos = (current_pos.getX(), current_pos.getY(), screen_z) else: return False self.refreshModelValues(gui_element) gui_element.setPos(*new_pos) print(f"✓ 更新2D GUI元素位置: {axis}={value}") return True else: print(f"✗ 不支持的GUI类型进行2D位置编辑: {gui_type}") return False except Exception as e: print(f"✗ 更新2D GUI元素位置失败: {e}") import traceback traceback.print_exc() return False def editGUIScale(self, gui_element, axis, value): """编辑GUI元素缩放""" try: gui_type = gui_element.getTag("gui_type") current_scale = gui_element.getScale() # 确保缩放值不为0 if value == 0: value = 0.01 if gui_type in ["3d_text", "3d_image", "2d_image", "video_screen", "2d_video_screen"]: # 3D元素处理 if axis == "x": new_scale = (value, current_scale.getY(), current_scale.getZ()) elif axis == "y": new_scale = (current_scale.getX(), value, current_scale.getZ()) elif axis == "z": new_scale = (current_scale.getX(), current_scale.getY(), value) else: return False gui_element.setScale(*new_scale) else: # 2D元素处理 if axis in ["x", "z"]: # 对于2D图像,x和z分别代表宽度和高度 # 保持原有缩放比例,仅调整指定轴 if axis == "x": gui_element.setScale(value, current_scale.getZ() if hasattr(current_scale, 'getZ') else current_scale[ 1] if isinstance(current_scale, (list, tuple)) else current_scale) elif axis == "z": gui_element.setScale( current_scale.getX() if hasattr(current_scale, 'getX') else current_scale[0] if isinstance( current_scale, (list, tuple)) else current_scale, value) else: # 对于其他2D元素,使用统一缩放 gui_element.setScale(value) print(f"✓ 更新GUI元素缩放: {axis}={value}") return True except Exception as e: print(f"✗ 更新GUI元素缩放失败: {e}") import traceback traceback.print_exc() return False def _update2DImageWidth(self, gui_element, width): try: current_scale = gui_element.getScale() width_scaled = width / 2 height_scaled = current_scale.getZ() gui_element.setScale(width_scaled, current_scale.getY(), height_scaled) if hasattr(gui_element, '_height_spinbox'): gui_element._height_spinbox.blockSignals(True) gui_element._height_spinbox.setValue(height_scaled * 2) gui_element._height_spinbox.blockSignals(False) print(f"✓ 更新2D图片宽度: {width}") except Exception as e: print(f"✗ 更新2D图片宽度失败: {e}") def _update2DImageHeight(self, gui_element, height): """更新2D图片高度""" try: # 获取当前缩放 current_scale = gui_element.getScale() # 保持宽度不变,只更新高度(Z轴) height_scaled = height / 2 # 转换为内部缩放值 width_scaled = current_scale.getX() gui_element.setScale(width_scaled, current_scale.getY(), height_scaled) # 更新宽度控件的值 if hasattr(gui_element, '_width_spinbox'): gui_element._width_spinbox.blockSignals(True) gui_element._width_spinbox.setValue(width_scaled * 2) gui_element._width_spinbox.blockSignals(False) print(f"✓ 更新2D图片高度: {height}") except Exception as e: print(f"✗ 更新2D图片高度失败: {e}") def _update2DImageScale(self, gui_element, scale): try: scaled_value = scale / 2 gui_element.setScale(scaled_value, 0, scaled_value) print(f"✓ 更新2D图片缩放: {scale}") except Exception as e: print(f"✗ 更新2D图片缩放失败: {e}") def _update3DImageScale(self, gui_element, axis, scale): """更新3D元素缩放""" try: current_scale = gui_element.getScale() if axis == 'x': new_scale = (scale, current_scale.getY(), current_scale.getZ()) # elif axis == 'y': # new_scale = (current_scale.getX(), scale, current_scale.getZ()) elif axis == 'z': new_scale = (current_scale.getX(), current_scale.getY(), scale) else: return gui_element.setScale(*new_scale) print(f"✓ 更新3D元素轴缩放 {axis}: {scale}") except Exception as e: print(f"✗ 更新3D元素轴缩放失败: {e}") def _selectGUIColor(self, gui_element): """选择GUI元素的字体颜色""" from PyQt5.QtWidgets import QColorDialog from PyQt5.QtGui import QColor from panda3d.core import Vec4 # 获取当前颜色(如果已设置) current_color = QColor(255, 255, 255) # 默认白色 # 尝试获取当前设置的颜色 gui_type = gui_element.getTag("gui_type") try: if gui_type == "3d_text": if gui_element.hasMaterial(): material = gui_element.getMaterial() base_color = material.getBaseColor() current_color = QColor( int(base_color.getX() * 255), int(base_color.getY() * 255), int(base_color.getZ() * 255), int(base_color.getW() * 255) ) else: # 从节点颜色获取 node_color = gui_element.getColor() current_color = QColor( int(node_color.getX() * 255), int(node_color.getY() * 255), int(node_color.getZ() * 255), int(node_color.getW() * 255) ) # current_color_obj = gui_element.getColor() # current_color = QColor( # int(current_color_obj[0] * 255), # int(current_color_obj[1] * 255), # int(current_color_obj[2] * 255) # ) # 对于其他类型的元素,可以添加类似的获取当前颜色的逻辑 except: pass # 使用默认颜色 color = QColorDialog.getColor(current_color, None, "选择字体颜色") if color.isValid(): r, g, b = color.red() / 255.0, color.green() / 255.0, color.blue() / 255.0 self._updateGUITextColor(gui_element, (r, g, b, 1.0)) def _updateGUITextColor(self, gui_element, color): """更新GUI元素的字体颜色""" try: gui_type = gui_element.getTag("gui_type") if gui_type in ["button", "label", "entry"]: # 对于DirectGUI元素,使用text_fg属性 gui_element['text_fg'] = color print(f"✓ 更新DirectGUI元素字体颜色: {gui_type}") elif gui_type == "3d_text": # # 对于3D文本元素,直接设置颜色 # gui_element.setColor(*color) # print(f"✓ 更新3D文本字体颜色: {gui_type}") from panda3d.core import Material, Vec4 if not gui_element.hasMaterial(): material = Material(f"text-material-{gui_element.getName()}") material.setBaseColor(Vec4(color[0], color[1], color[2], color[3])) material.setDiffuse(Vec4(color[0], color[1], color[2], color[3])) material.setAmbient(Vec4(color[0] * 0.5, color[1] * 0.5, color[2] * 0.5, color[3])) material.setSpecular(Vec4(0.1, 0.1, 0.1, 1.0)) material.setShininess(10.0) gui_element.setMaterial(material, 1) else: # 更新现有材质 material = gui_element.getMaterial() material.setBaseColor(Vec4(color[0], color[1], color[2], color[3])) material.setDiffuse(Vec4(color[0], color[1], color[2], color[3])) gui_element.setMaterial(material, 1) print(f"✓ 更新3D文本材质颜色: {color}") gui_element.setColor(*color) elif gui_type == "3d_image": # 对于3D图片,如果有文本标签的话 # 这里可以根据需要添加特定处理 pass print(f"✓ 更新GUI元素字体颜色: {gui_type}, 颜色: {color}") except Exception as e: print(f"✗ 更新GUI元素字体颜色失败: {e}") import traceback traceback.print_exc() def _updateGUIScaleY(self, gui_element, scale_y): """更新GUI元素Y轴缩放""" try: gui_type = gui_element.getTag("gui_type") current_scale = gui_element.getScale() # 对于不同的GUI类型使用不同的缩放方法 if gui_type in ["3d_text", "3d_image"]: # 对于3D元素,直接设置缩放 new_scale = (current_scale.getX(), scale_y, current_scale.getZ()) gui_element.setScale(*new_scale) else: # 对于2D元素,保持原有的缩放方法 gui_element.setScale(scale_y) print(f"✓ 更新GUI元素Y轴缩放: {scale_y}") except Exception as e: print(f"✗ 更新GUI元素Y轴缩放失败: {e}") def _updateGUIScaleZ(self, gui_element, scale_z): """更新GUI元素Z轴缩放""" try: gui_type = gui_element.getTag("gui_type") current_scale = gui_element.getScale() # 对于不同的GUI类型使用不同的缩放方法 if gui_type in ["3d_text", "3d_image"]: # 对于3D元素,直接设置缩放 new_scale = (current_scale.getX(), current_scale.getY(), scale_z) gui_element.setScale(*new_scale) else: # 对于2D元素,使用单一缩放值(保持宽高比) gui_element.setScale(scale_z) print(f"✓ 更新GUI元素Z轴缩放: {scale_z}") except Exception as e: print(f"✗ 更新GUI元素Z轴缩放失败: {e}") def update2DImageTexture(self, image_element, texture_path): """更新2D图片纹理 - 修复版""" try: from panda3d.core import TextureStage, TransparencyAttrib # 加载纹理 texture = self.world.loader.loadTexture(texture_path) if not texture: print(f"❌ 无法加载纹理: {texture_path}") return False # 设置纹理过滤 texture.setMagfilter(texture.FTLinear) texture.setMinfilter(texture.FTLinearMipmapLinear) # 应用纹理到元素 image_element.setTexture(texture, 1) # 设置正确的渲染属性以避免黑色显示 image_element.setTransparency(TransparencyAttrib.MAlpha) image_element.setBin("fixed", 20) # 确保在正确层级渲染 image_element.setDepthWrite(False) image_element.setDepthTest(False) image_element.setLightOff() image_element.setTwoSided(True) print(f"✅ 2D图片纹理已更新: {texture_path}") return True except Exception as e: print(f"❌ 更新2D图片纹理失败: {e}") import traceback traceback.print_exc() return False 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() transform_layout.setColumnMinimumWidth(0, self.column_minimum_width) transform_layout.addWidget(QLabel("位置"), 0, 0) # 位置属性 current_pos = light_object.pos position_row = QHBoxLayout() position_row.setContentsMargins(0, 0, 0, 0) position_row.setSpacing(4) xPos = QDoubleSpinBox() xPos.setRange(-1000, 1000) xPos.setValue(current_pos.getX()) xPos.valueChanged.connect(lambda v: self._updateLightPosition(light_object, model, 'x', v)) yPos = QDoubleSpinBox() yPos.setRange(-1000, 1000) yPos.setValue(current_pos.getY()) yPos.valueChanged.connect(lambda v: self._updateLightPosition(light_object, model, 'y', v)) zPos = QDoubleSpinBox() zPos.setRange(-1000, 1000) zPos.setValue(current_pos.getZ()) zPos.valueChanged.connect(lambda v: self._updateLightPosition(light_object, model, 'z', v)) for axis_label, widget in (("X:", xPos), ("Y:", yPos), ("Z:", zPos)): axis = QLabel(axis_label) axis.setAlignment(Qt.AlignVCenter) axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) position_row.addWidget(axis) position_row.addWidget(widget) transform_layout.addLayout(position_row, 0, 1, 1, 3) worldPos = model.getPos(self.world.render) world_position_row = QHBoxLayout() world_position_row.setContentsMargins(0, 0, 0, 0) world_position_row.setSpacing(4) transform_layout.addWidget(QLabel("世界位置"), 1, 0) worldXPos = QDoubleSpinBox() worldXPos.setRange(-1000, 1000) worldXPos.setValue(worldPos.getX()) worldXPos.setReadOnly(True) worldYPos = QDoubleSpinBox() worldYPos.setRange(-1000, 1000) worldYPos.setValue(worldPos.getY()) worldYPos.setReadOnly(True) worldZPos = QDoubleSpinBox() worldZPos.setRange(-1000, 1000) worldZPos.setValue(worldPos.getZ()) worldZPos.setReadOnly(True) for axis_label, widget in (("X:", worldXPos), ("Y:", worldYPos), ("Z:", worldZPos)): axis = QLabel(axis_label) axis.setAlignment(Qt.AlignVCenter) axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) world_position_row.addWidget(axis) world_position_row.addWidget(widget) transform_layout.addLayout(world_position_row, 1, 1, 1, 3) # 旋转属性(仅聚光灯) next_row = 2 if hasattr(light_object, 'direction'): current_hpr = model.getHpr() rotation_row = QHBoxLayout() rotation_row.setContentsMargins(0, 0, 0, 0) rotation_row.setSpacing(4) transform_layout.addWidget(QLabel("旋转"), next_row, 0) hRot = QDoubleSpinBox() hRot.setRange(-180, 180) hRot.setValue(current_hpr.getX()) hRot.valueChanged.connect(lambda v: self._updateLightRotation(light_object, model, 'h', v)) pRot = QDoubleSpinBox() pRot.setRange(-180, 180) pRot.setValue(current_hpr.getY()) pRot.valueChanged.connect(lambda v: self._updateLightRotation(light_object, model, 'p', v)) rRot = QDoubleSpinBox() rRot.setRange(-180, 180) rRot.setValue(current_hpr.getZ()) rRot.valueChanged.connect(lambda v: self._updateLightRotation(light_object, model, 'r', v)) for axis_label, widget in (("H:", hRot), ("P:", pRot), ("R:", rRot)): axis = QLabel(axis_label) axis.setAlignment(Qt.AlignVCenter) axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) rotation_row.addWidget(axis) rotation_row.addWidget(widget) transform_layout.addLayout(rotation_row, next_row, 1, 1, 3) next_row += 1 # 缩放属性 current_scale = model.getScale() scale_row = QHBoxLayout() scale_row.setContentsMargins(0, 0, 0, 0) scale_row.setSpacing(4) transform_layout.addWidget(QLabel("缩放"), next_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)) 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)) 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)) for axis_label, widget in (("X:", xScaleSpinBox), ("Y:", yScaleSpinBox), ("Z:", zScaleSpinBox)): axis = QLabel(axis_label) axis.setAlignment(Qt.AlignVCenter) axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) scale_row.addWidget(axis) scale_row.addWidget(widget) transform_layout.addLayout(scale_row, next_row, 1, 1, 3) transform_group.setLayout(transform_layout) self._propertyLayout.addWidget(transform_group) # 光源属性组 light_group = QGroupBox("光源属性") light_layout = QGridLayout() current_energy = light_object.energy stored_energy = None if current_energy == 0.0 and model.hasTag("stored_energy"): try: stored_energy = float(model.getTag("stored_energy")) except ValueError: pass display_energy = stored_energy if stored_energy is not None and stored_energy > 0 else current_energy # 能量 light_layout.addWidget(QLabel("能量:"), 0, 0) energySpinBox = QDoubleSpinBox() energySpinBox.setRange(0, 10000) energySpinBox.setValue(display_energy) energySpinBox.valueChanged.connect(lambda v: self._updateLightEnergy(light_object, v)) light_layout.addWidget(energySpinBox, 0, 1, 1, 3) # 半径 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, 1, 3) # 视野角度(仅聚光灯) 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, 1, 3) 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.setColumnMinimumWidth(0, self.column_minimum_width) material_layout.addWidget(QLabel("名称:"), 0, 0) limited_length = 25 if len(display_name) > limited_length: display_name = f"{display_name[:limited_length - 3]}..." 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, 3) # 基础颜色编辑 base_color = self._getOrCreateMaterialBaseColor(material) if base_color is not None: print(f"材质基础颜色: {base_color}") # 基础颜色编辑 color_row = 2 if material_status != "标准PBR材质" else 1 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)) 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)) 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)) title_label = QLabel("基础颜色") material_layout.addWidget(title_label, color_row, 0) base_color_layout = QHBoxLayout() base_color_layout.setContentsMargins(0, 0, 0, 0) base_color_layout.setSpacing(4) for label_text, spinbox in (("R:", r_spinbox), ("G:", g_spinbox), ("B:", b_spinbox)): channel_label = QLabel(label_text) channel_label.setAlignment(Qt.AlignVCenter) channel_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) base_color_layout.addWidget(channel_label) spinbox.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) base_color_layout.addWidget(spinbox) # base_color_layout.addStretch(1) material_layout.addLayout(base_color_layout, color_row, 1, 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 + 1 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, 1, 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, 1, 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, 1, 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, 1, 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, 1, 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, 1, 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, 1, 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, 1, 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, 1, 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) 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, TransparencyAttrib # 加载纹理 texture = RPLoader.load_texture(texture_path) if texture: # 获取材质所属的节点 material, node = self._findMaterialAndNodeByTitle(material_title) if node and material: print(f"正在为节点 {node.getName()} 应用漫反射贴图") # 检查纹理是否包含透明度信息 has_alpha = False # 检查纹理格式 format_name = str(texture.getFormat()) if 'alpha' in format_name.lower() or 'rgba' in format_name.lower(): has_alpha = True print(f"纹理格式: {texture.getFormat()}, 包含透明通道: {has_alpha}") # 检查是否有金属性贴图,选择合适的PBR效果 print("🔧 检查金属性贴图并选择合适的PBR效果...") has_metallic = self._hasMetallicTexture(node) needs_alpha = self._needsAlphaTesting(node) or has_alpha 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槽") # 如果纹理包含透明度,启用透明度渲染 if has_alpha: print("检测到透明纹理,启用透明度渲染") node.setTransparency(TransparencyAttrib.MAlpha) node.setDepthWrite(False) # 透明物体通常不写入深度缓冲区 else: # 确保关闭透明度(避免之前设置的影响) node.setTransparency(TransparencyAttrib.MNone) # 调试信息:显示当前纹理阶段 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 _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() animation_layout.setColumnMinimumWidth(0, 115) # 首先检测骨骼动画 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 print(f"[信息] 检测到骨骼动画: {actor.getAnimNames()}") except Exception as actor_error: # 忽略 Actor 加载错误,很多模型都不是角色动画 print(f"[信息] 该模型不包含骨骼动画: {actor_error}") # 如果都没有动画 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() 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.addLayout(btn_lay, 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'): pass #return self._createFBXActor(origin_model, filepath) # 其他格式使用标准 Actor 加载 try: import gltf print(f"[GLTF加载] 尝试加载: {filepath}") # test_actor=Actor(NodePath(gltf._loader.GltfLoader.load_file(filepath,None))) test_actor = Actor(NodePath(gltf.load_model(filepath, None))) anims = test_actor.getAnimNames() test_actor.reparentTo(self.world.render) self._actor_cache[origin_model] = test_actor print(f"[Actor加载] 标准加载检测到动画: {anims}") if not anims: test_actor.cleanup() test_actor.removeNode() return None return test_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 original_world_pos = origin_model.getPos(self.world.render) original_world_hpr = origin_model.getHpr(self.world.render) original_world_scale = origin_model.getScale(self.world.render) actor.setPos(origin_model.getPos()) actor.setHpr(origin_model.getHpr()) actor.setScale(origin_model.getScale()) origin_model.hide() actor.show() #创建人物来维持世界坐标不变 def maintainWorldPosition(task): try: if not actor.isEmpty(): actor.setPos(self.world.render,original_world_pos) actor.setHpr(self.world.render,original_world_hpr) actor.setScale(self.world.render,original_world_scale) return task.cont else: return task.done except: return task.done taskMgr.add(maintainWorldPosition,f"maintain_anim_pos_{id(actor)}") 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() / 100) 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() def _addCollisionPanel(self, model): """添加碰撞检测面板""" try: # 创建碰撞检测组 collision_group = QGroupBox("碰撞检测") collision_layout = QGridLayout() collision_layout.setColumnMinimumWidth(0, self.column_minimum_width) # collision_layout.setColumnStretch(0, 0) # collision_layout.setColumnStretch(1, 1) # collision_layout.setColumnStretch(2, 1) # collision_layout.setColumnStretch(3, 1) # 检查模型是否已有碰撞 has_collision = self._hasCollision(model) # 创建主容器 main_container = QWidget() main_layout = QVBoxLayout(main_container) main_layout.setContentsMargins(8, 8, 8, 8) main_layout.setSpacing(12) # 状态和形状选择区域 header_container = QWidget() header_layout = QGridLayout(header_container) header_layout.setContentsMargins(0, 0, 0, 0) header_layout.setSpacing(8) # 调整列宽,保证控件对齐 # collision_layout.setColumnStretch(0, 0) # collision_layout.setColumnStretch(1, 0) # collision_layout.setColumnStretch(2, 0) # collision_layout.setColumnStretch(3, 0) # 碰撞状态行 status_layout = QHBoxLayout() status_label = QLabel("状态:") status_layout.addWidget(status_label) # 状态徽章(使用固定宽度样式) if has_collision: self.collision_status_badge = self.createFixedStatusBadge("已启用", "green") else: self.collision_status_badge = self.createFixedStatusBadge("未启用", "red") status_layout.addWidget(self.collision_status_badge) collision_layout.addLayout(status_layout, 0, 0, 1, 1) # 形状选择标签(始终显示) self.collision_shape_label = QLabel("碰撞形状:") collision_layout.addWidget(self.collision_shape_label, 1, 0) # 形状选择下拉框(始终显示) self.collision_shape_combo = QComboBox() self.collision_shape_combo.addItems([ "球形 (Sphere)", "盒型 (Box)", "胶囊体 (Capsule)", "平面 (Plane)", "自动选择 (Auto)" ]) collision_layout.addWidget(self.collision_shape_combo, 1, 1, 1, 3) # 保存布局引用,用于动态添加/移除控件 self.collision_layout = collision_layout self.collision_group = collision_group current_row = 2 # 下一行的索引 # 显示/隐藏切换按钮(只有有碰撞时才显示) if has_collision: # 检查碰撞的当前可见性 is_collision_visible = self._isCollisionVisible(model) # 显示当前碰撞类型并设置为只读 current_shape = self._getCurrentCollisionShape(model) self._setComboToShape(current_shape) self.collision_shape_combo.setEnabled(False) # 添加碰撞参数调整控件 current_row = self._addCollisionParameterControls(model, collision_layout, current_row, current_shape) # 显示/隐藏切换按钮(使用现代化样式) visibility_text = "隐藏碰撞" if is_collision_visible else "显示碰撞" self.collision_visibility_button = self.createModernButton(visibility_text) self.collision_visibility_button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.collision_visibility_button.setStyleSheet(self._collision_button_style()) self.collision_visibility_button.clicked.connect(lambda: self._toggleCollisionVisibility(model)) collision_layout.addWidget(self.collision_visibility_button, current_row, 0, 1, 4) current_row += 1 # 移除碰撞按钮(使用现代化样式) self.collision_button = self.createModernButton("移除碰撞") self.collision_button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.collision_button.setStyleSheet(self._collision_button_style()) self.collision_button.clicked.connect(lambda: self._removeCollisionAndUpdate(model)) collision_layout.addWidget(self.collision_button, current_row, 0, 1, 4) else: # 如果没有碰撞,设置默认选择并允许编辑 self.collision_shape_combo.setCurrentText("球形 (Sphere)") self.collision_shape_combo.setEnabled(True) # 清理之前的参数控件 self._clearCollisionParameterControls() # 隐藏显示/隐藏按钮(如果存在) if hasattr(self, 'collision_visibility_button'): self.collision_visibility_button.setVisible(False) # 添加碰撞按钮(使用现代化样式) self.collision_button = self.createModernButton("添加碰撞") self.collision_button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.collision_button.setStyleSheet(self._collision_button_style()) self.collision_button.clicked.connect(lambda: self._addCollisionAndUpdate(model)) collision_layout.addWidget(self.collision_button, current_row, 0, 1, 4) collision_group.setLayout(collision_layout) self._propertyLayout.addWidget(collision_group) # 同步一次状态,确保主按钮与可见性按钮齐全 try: self._updateCollisionPanelState(model) except Exception: pass except Exception as e: print(f"创建碰撞面板失败: {e}") import traceback traceback.print_exc() def _addCollisionParameterControls(self, model, layout, start_row, shape_type): """添加碰撞参数调整控件""" try: current_row = start_row # 位置调整控件(所有类型都有) pos_label = QLabel("位置偏移:") pos_label.setAlignment(Qt.AlignLeft | Qt.AlignTop) pos_label.setVisible(True) # 确保可见 # X, Y, Z 位置调整 self.collision_pos_x = self._createCollisionSpinBox(-100, 100, 2) self.collision_pos_y = self._createCollisionSpinBox(-100, 100, 2) self.collision_pos_z = self._createCollisionSpinBox(-100, 100, 2) for spinbox in (self.collision_pos_x, self.collision_pos_y, self.collision_pos_z): spinbox.setMinimumWidth(140) spinbox.setVisible(True) axis_controls = [ ("X:", self.collision_pos_x), ("Y:", self.collision_pos_y), ("Z:", self.collision_pos_z), ] for offset, (axis_text, spinbox) in enumerate(axis_controls): row = current_row + offset axis_layout = QHBoxLayout() if offset == 0: layout.addWidget(pos_label, row, 0) # axis_label.addWidget(pos_label) else: spacer = QLabel("") spacer.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) spacer.setVisible(True) layout.addWidget(spacer, row, 0) # axis_layout.addWidget(spacer) axis_label = QLabel(axis_text) axis_label.setAlignment(Qt.AlignVCenter) axis_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) axis_layout.addWidget(axis_label) axis_layout.addWidget(spinbox) # layout.addWidget(axis_label, row, 1) # layout.addWidget(spinbox, row, 2, 1, 2) layout.addLayout(axis_layout, row, 1, 1, 3) current_row += len(axis_controls) # 连接位置变化信号 self.collision_pos_x.valueChanged.connect(lambda v: self._updateCollisionPosition(model, 'x', v)) self.collision_pos_y.valueChanged.connect(lambda v: self._updateCollisionPosition(model, 'y', v)) self.collision_pos_z.valueChanged.connect(lambda v: self._updateCollisionPosition(model, 'z', v)) # 根据形状类型添加特定参数 if shape_type == 'sphere': current_row = self._addSphereParameters(model, layout, current_row) elif shape_type == 'box': current_row = self._addBoxParameters(model, layout, current_row) elif shape_type == 'capsule': current_row = self._addCapsuleParameters(model, layout, current_row) elif shape_type == 'plane': current_row = self._addPlaneParameters(model, layout, current_row) # 获取并设置当前参数值 self._loadCurrentCollisionParameters(model, shape_type) return current_row except Exception as e: print(f"添加碰撞参数控件失败: {e}") return start_row def _createCollisionSpinBox(self, min_val, max_val, decimals=2): """创建碰撞参数调整用的SpinBox""" spinbox = QDoubleSpinBox() spinbox.setRange(min_val, max_val) spinbox.setDecimals(decimals) spinbox.setSingleStep(0.01) return spinbox def _addSphereParameters(self, model, layout, start_row): """添加球形碰撞参数""" current_row = start_row # 半径调整 radius_label = QLabel("半径:") layout.addWidget(radius_label, current_row, 0) # layout.addWidget(QLabel(""), current_row, 1) self.collision_radius = self._createCollisionSpinBox(0.01, 100000, 2) # 只在没有现有碰撞时设置默认值,否则由_loadCurrentCollisionParameters加载实际值 if not self._hasCollision(model): # 设置基于模型变换后尺寸的默认值 if hasattr(self.world, 'collision_manager'): transformed_info = self.world.collision_manager._getTransformedModelInfo(model) if transformed_info: default_radius = transformed_info['radius'] self.collision_radius.setValue(default_radius) else: # 回退到原始包围盒 bounds = model.getBounds() if not bounds.isEmpty(): default_radius = bounds.getRadius() self.collision_radius.setValue(default_radius) self.collision_radius.valueChanged.connect(lambda v: self._updateSphereRadius(model, v)) layout.addWidget(self.collision_radius, current_row, 1, 1, 3) current_row += 1 return current_row def _addBoxParameters(self, model, layout, start_row): """添加盒型碰撞参数""" current_row = start_row size_label = QLabel("尺寸:") layout.addWidget(size_label, current_row, 0) # layout.addWidget(QLabel(""), current_row, 1) # current_row += 1 # 宽度、长度、高度 self.collision_width = self._createCollisionSpinBox(0.001, 100000, 2) self.collision_length = self._createCollisionSpinBox(0.001, 100000, 2) self.collision_height = self._createCollisionSpinBox(0.001, 100000, 2) # 只在没有现有碰撞时设置默认值,否则由_loadCurrentCollisionParameters加载实际值 if not self._hasCollision(model): # 设置基于模型变换后尺寸的默认值 if hasattr(self.world, 'collision_manager'): transformed_info = self.world.collision_manager._getTransformedModelInfo(model) if transformed_info: actual_size = transformed_info['size'] self.collision_width.setValue(actual_size.x) self.collision_length.setValue(actual_size.y) self.collision_height.setValue(actual_size.z) else: # 回退到原始包围盒 bounds = model.getBounds() if not bounds.isEmpty(): model_size = bounds.getMax() - bounds.getMin() self.collision_width.setValue(model_size.x) self.collision_length.setValue(model_size.y) self.collision_height.setValue(model_size.z) box_controls = [ ("宽度:", self.collision_width), ("长度:", self.collision_length), ("高度:", self.collision_height), ] for offset, (text, spinbox) in enumerate(box_controls): text_layout = QHBoxLayout() if offset == 0: layout.addWidget(size_label, current_row, 0) else: spacer = QLabel("") spacer.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) spacer.setVisible(True) layout.addWidget(spacer, current_row, 0) # placeholder = QLabel("") # layout.addWidget(placeholder, current_row, 0) axis_label = QLabel(text) axis_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) axis_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) text_layout.addWidget(axis_label) text_layout.addWidget(spinbox) layout.addLayout(text_layout, current_row, 1, 1, 3) # layout.addWidget(axis_label, current_row, 1) # layout.addWidget(spinbox, current_row, 2) current_row += 1 # 连接信号 self.collision_width.valueChanged.connect(lambda v: self._updateBoxSize(model, 'width', v)) self.collision_length.valueChanged.connect(lambda v: self._updateBoxSize(model, 'length', v)) self.collision_height.valueChanged.connect(lambda v: self._updateBoxSize(model, 'height', v)) return current_row def _addCapsuleParameters(self, model, layout, start_row): """添加胶囊体碰撞参数""" current_row = start_row # 半径和高度 radius_label = QLabel("半径:") layout.addWidget(radius_label, current_row, 0) # layout.addWidget(QLabel(""), current_row, 1) self.collision_capsule_radius = self._createCollisionSpinBox(0.01, 100000, 2) # 只在没有现有碰撞时设置默认值,否则由_loadCurrentCollisionParameters加载实际值 if not self._hasCollision(model): # 设置基于模型变换后尺寸的默认值 if hasattr(self.world, 'collision_manager'): transformed_info = self.world.collision_manager._getTransformedModelInfo(model) if transformed_info: actual_size = transformed_info['size'] # 更合理的默认半径:基于变换后模型宽度的平均值 default_radius = min(actual_size.x, actual_size.y) / 2.5 self.collision_capsule_radius.setValue(default_radius) else: # 回退到原始包围盒 bounds = model.getBounds() if not bounds.isEmpty(): model_size = bounds.getMax() - bounds.getMin() # 更合理的默认半径:基于模型宽度的平均值 default_radius = min(model_size.x, model_size.y) / 2.5 self.collision_capsule_radius.setValue(default_radius) self.collision_capsule_radius.valueChanged.connect(lambda v: self._updateCapsuleRadius(model, v)) layout.addWidget(self.collision_capsule_radius, current_row, 1, 1, 3) current_row += 1 height_label = QLabel("高度:") layout.addWidget(height_label, current_row, 0) # layout.addWidget(QLabel(""), current_row, 1) self.collision_capsule_height = self._createCollisionSpinBox(0.01, 10000, 2) # 只在没有现有碰撞时设置默认值,否则由_loadCurrentCollisionParameters加载实际值 if not self._hasCollision(model): # 设置基于模型变换后高度的默认值 if hasattr(self.world, 'collision_manager'): transformed_info = self.world.collision_manager._getTransformedModelInfo(model) if transformed_info: actual_size = transformed_info['size'] self.collision_capsule_height.setValue(actual_size.z) else: # 回退到原始包围盒 bounds = model.getBounds() if not bounds.isEmpty(): model_size = bounds.getMax() - bounds.getMin() self.collision_capsule_height.setValue(model_size.z) self.collision_capsule_height.valueChanged.connect(lambda v: self._updateCapsuleHeight(model, v)) layout.addWidget(self.collision_capsule_height, current_row, 1, 1, 3) current_row += 1 return current_row def _addPlaneParameters(self, model, layout, start_row): """添加平面碰撞参数""" current_row = start_row # 法向量 normal_label = QLabel("法向量:") layout.addWidget(normal_label, current_row, 0) # layout.addWidget(QLabel(""), current_row, 1) # current_row += 1 self.collision_normal_x = self._createCollisionSpinBox(-1, 1, 2) self.collision_normal_y = self._createCollisionSpinBox(-1, 1, 2) self.collision_normal_z = self._createCollisionSpinBox(-1, 1, 2) # 只在没有现有碰撞时设置默认值,否则由_loadCurrentCollisionParameters加载实际值 if not self._hasCollision(model): # 设置默认法向量(向上) self.collision_normal_x.setValue(0.0) self.collision_normal_y.setValue(0.0) self.collision_normal_z.setValue(1.0) # placeholder_nx = QLabel("") # layout.addWidget(placeholder_nx, current_row, 0) hbox_nx = QHBoxLayout() axis_label_nx = QLabel("Nx:") axis_label_nx.setAlignment(Qt.AlignRight | Qt.AlignVCenter) axis_label_nx.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) hbox_nx.addWidget(axis_label_nx) hbox_nx.addWidget(self.collision_normal_x) layout.addLayout(hbox_nx, current_row, 1, 1, 3) current_row += 1 placeholder_ny = QLabel("") layout.addWidget(placeholder_ny, current_row, 0) hbox_ny = QHBoxLayout() axis_label_ny = QLabel("Ny:") axis_label_ny.setAlignment(Qt.AlignRight | Qt.AlignVCenter) axis_label_ny.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) hbox_ny.addWidget(axis_label_ny) hbox_ny.addWidget(self.collision_normal_y) layout.addLayout(hbox_ny, current_row, 1, 1, 3) current_row += 1 placeholder_nz = QLabel("") layout.addWidget(placeholder_nz, current_row, 0) hbox_nz = QHBoxLayout() axis_label_nz = QLabel("Nz:") axis_label_nz.setAlignment(Qt.AlignRight | Qt.AlignVCenter) axis_label_nz.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) hbox_nz.addWidget(axis_label_nz) hbox_nz.addWidget(self.collision_normal_z) layout.addLayout(hbox_nz, current_row, 1, 1, 3) current_row += 1 # 连接信号 self.collision_normal_x.valueChanged.connect(lambda v: self._updatePlaneNormal(model, 'x', v)) self.collision_normal_y.valueChanged.connect(lambda v: self._updatePlaneNormal(model, 'y', v)) self.collision_normal_z.valueChanged.connect(lambda v: self._updatePlaneNormal(model, 'z', v)) return current_row def _hasCollision(self, model): """检查模型是否已有碰撞体""" try: from panda3d.core import CollisionNode # 检查模型及其子节点是否有碰撞节点 collision_nodes = model.findAllMatches("**/+CollisionNode") has_collision = collision_nodes.getNumPaths() > 0 print( f"碰撞检查:模型 {model.getName()} - {'有' if has_collision else '无'}碰撞 (找到{collision_nodes.getNumPaths()}个碰撞节点)") return has_collision except Exception as e: print(f"检查碰撞失败: {e}") return False def _isCollisionVisible(self, model): """检查碰撞是否可见""" try: collision_nodes = model.findAllMatches("**/+CollisionNode") for collision_np in collision_nodes: # 检查碰撞节点是否隐藏 return not collision_np.isHidden() return False except Exception as e: print(f"检查碰撞可见性失败: {e}") return False def _toggleCollisionVisibility(self, model): """切换碰撞可见性""" try: collision_nodes = model.findAllMatches("**/+CollisionNode") is_visible = self._isCollisionVisible(model) for collision_np in collision_nodes: if is_visible: collision_np.hide() print(f"隐藏碰撞:{model.getName()}") else: collision_np.show() print(f"显示碰撞:{model.getName()}") # 立即更新按钮状态,并同步更新面板(确保缺失主按钮时补建) self._updateCollisionVisibilityButton(model) self._updateCollisionPanelState(model) except Exception as e: print(f"切换碰撞可见性失败: {e}") def _updateCollisionVisibilityButton(self, model): """更新碰撞可见性按钮状态""" try: if hasattr(self, 'collision_visibility_button'): is_visible = self._isCollisionVisible(model) button_text = "隐藏碰撞" if is_visible else "显示碰撞" self.collision_visibility_button.setText(button_text) # 应用统一按钮样式 self.collision_visibility_button.setStyleSheet(self._collision_button_style()) print(f"更新可见性按钮:{model.getName()} - {'可见' if is_visible else '隐藏'}") except Exception as e: print(f"更新碰撞可见性按钮失败: {e}") def _getCurrentCollisionShape(self, model): """获取当前模型的碰撞形状类型""" try: from panda3d.core import CollisionNode, CollisionSphere, CollisionBox, CollisionCapsule, CollisionPlane collision_nodes = model.findAllMatches("**/+CollisionNode") for collision_np in collision_nodes: collision_node = collision_np.node() if collision_node.getNumSolids() > 0: solid = collision_node.getSolid(0) solid_type = type(solid).__name__ if solid_type == "CollisionSphere": return "sphere" elif solid_type == "CollisionBox": return "box" elif solid_type == "CollisionCapsule": return "capsule" elif solid_type == "CollisionPlane": return "plane" return "sphere" # 默认返回球形 except Exception as e: print(f"获取碰撞形状失败: {e}") return "sphere" def _setComboToShape(self, shape_type): """根据形状类型设置下拉框选择""" shape_map = { "sphere": "球形 (Sphere)", "box": "盒型 (Box)", "capsule": "胶囊体 (Capsule)", "plane": "平面 (Plane)", "auto": "自动选择 (Auto)" } if shape_type in shape_map: self.collision_shape_combo.setCurrentText(shape_map[shape_type]) else: self.collision_shape_combo.setCurrentText("球形 (Sphere)") def _getSelectedCollisionShape(self): """获取选中的碰撞形状类型""" if hasattr(self, 'collision_shape_combo'): shape_text = self.collision_shape_combo.currentText() # 从显示文本中提取形状类型 if "球形" in shape_text: return 'sphere' elif "盒型" in shape_text: return 'box' elif "胶囊体" in shape_text: return 'capsule' elif "平面" in shape_text: return 'plane' elif "自动选择" in shape_text: return 'auto' return 'sphere' # 默认返回球形 def _addCollisionAndUpdate(self, model): """添加指定形状的碰撞体并更新界面""" try: # 防止重复调用 if getattr(self, '_adding_collision', False): print("正在添加碰撞,跳过重复调用") return self._adding_collision = True # 初始化加载参数标志位 if not hasattr(self, '_loading_collision_params'): self._loading_collision_params = False if hasattr(self.world, 'scene_manager'): # 获取选中的碰撞形状 shape_type = self._getSelectedCollisionShape() # 参考scene_manager的setupCollision方法 from panda3d.core import CollisionNode, BitMask32 # 创建碰撞节点 cNode = CollisionNode(f'modelCollision_{model.getName()}') # 设置碰撞掩码 cNode.setIntoCollideMask(BitMask32.bit(2)) # 用于鼠标选择 # 如果启用了模型间碰撞检测,添加额外的掩码 if (hasattr(self.world, 'collision_manager') and hasattr(self.world.collision_manager, 'model_collision_enabled') and self.world.collision_manager.model_collision_enabled): current_mask = cNode.getIntoCollideMask() model_collision_mask = BitMask32.bit(6) # MODEL_COLLISION cNode.setIntoCollideMask(current_mask | model_collision_mask) # 创建指定形状的碰撞体 if hasattr(self.world, 'collision_manager'): collision_shape = self.world.collision_manager.createCollisionShape(model, shape_type) else: # 回退方案:创建简单球体 from panda3d.core import CollisionSphere, Point3 bounds = model.getBounds() if bounds.isEmpty(): collision_shape = CollisionSphere(Point3(0, 0, 0), 1.0) else: center = bounds.getCenter() radius = bounds.getRadius() if radius <= 0: radius = 1.0 collision_shape = CollisionSphere(center, radius) cNode.addSolid(collision_shape) # 将碰撞节点附加到模型上 cNodePath = model.attachNewNode(cNode) # 根据调试设置决定是否显示碰撞体 if hasattr(self.world, 'debug_collision') and self.world.debug_collision: cNodePath.show() else: cNodePath.hide() # 为模型添加碰撞相关标签 model.setTag("has_collision", "true") model.setTag("collision_shape", shape_type) if 'radius' in locals(): model.setTag("collision_radius", str(radius)) print(f"✅ 为模型 {model.getName()} 添加了 {shape_type} 碰撞体") # 重置更新标志,确保状态能够更新 self._updating_collision_panel = False # 更新面板状态 - 添加参数控件和显示/隐藏按钮 self._updateCollisionPanelState(model) else: print("场景管理器未初始化") except Exception as e: print(f"添加碰撞失败: {e}") import traceback traceback.print_exc() finally: # 确保标志被重置 self._adding_collision = False def _removeCollisionAndUpdate(self, model): """移除模型的碰撞体并更新界面""" try: # 查找并移除碰撞节点 collision_nodes = model.findAllMatches("**/+CollisionNode") for collision_np in collision_nodes: collision_np.removeNode() print(f"移除了模型 {model.getName()} 的碰撞体") # 重置状态并更新界面 self._previous_collision_state = True # 强制刷新 self._updating_collision_panel = False # 确保状态能够更新 self._updateCollisionPanelState(model) except Exception as e: print(f"移除碰撞失败: {e}") def _findButtonRow(self, layout): """查找按钮应该在的行数""" # 简单返回一个合适的行数,基于布局中现有的项目数 row_count = 0 for i in range(layout.count()): item = layout.itemAt(i) if item and item.widget(): row, col, rowspan, colspan = layout.getItemPosition(i) row_count = max(row_count, row + 1) return row_count def _updateCollisionPanelState(self, model): """更新碰撞面板状态""" try: # 防止重复调用 if getattr(self, '_updating_collision_panel', False): return self._updating_collision_panel = True print("-------------------------------------") if hasattr(self, 'collision_button') and hasattr(self, 'collision_status_badge') and hasattr(self, 'collision_shape_combo'): has_collision = self._hasCollision(model) print(f"模型 {model.getName()} 是否有碰撞体: {has_collision}-------------------------------------------------") # 更新状态徽章(直接修改现有控件,避免布局冲突) if hasattr(self, 'collision_status_badge') and self.collision_status_badge: if has_collision: self.collision_status_badge.setText("已启用") # 使用固定宽度的绿色样式,保持尺寸一致 self.collision_status_badge.setStyleSheet(self.badge_style_green_fixed) else: self.collision_status_badge.setText("未启用") # 使用固定宽度的红色样式,保持尺寸一致 self.collision_status_badge.setStyleSheet(self.badge_style_red_fixed) # 触发重绘,确保立即可见 self.collision_status_badge.update() if has_collision: # 有碰撞:显示移除按钮,下拉框变为只读并显示当前类型 if hasattr(self, 'collision_button'): # 更新按钮文本和样式(简单方法:直接设置文本和样式) self.collision_button.setText("移除碰撞") # 应用统一按钮样式 self.collision_button.setStyleSheet(self._collision_button_style()) # 重新连接信号 try: self.collision_button.clicked.disconnect() except: pass self.collision_button.clicked.connect(lambda: self._removeCollisionAndUpdate(model)) else: # 若不存在主按钮,则创建“移除碰撞”按钮并加入布局 try: self.collision_button = self.createModernButton("移除碰撞") self.collision_button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.collision_button.setStyleSheet(self._collision_button_style()) self.collision_button.clicked.connect(lambda: self._removeCollisionAndUpdate(model)) if hasattr(self, 'collision_layout') and self.collision_layout is not None: row = self.collision_layout.rowCount() self.collision_layout.addWidget(self.collision_button, row, 0, 1, 4) except Exception as _e: print(f"创建移除碰撞按钮失败: {_e}") # 获取并显示当前碰撞类型,设置为只读 current_shape = self._getCurrentCollisionShape(model) self._setComboToShape(current_shape) self.collision_shape_combo.setEnabled(False) # 确保参数控件存在 - 只在没有参数控件时才添加 if not hasattr(self, 'collision_pos_x'): print("添加碰撞参数控件") self._addParameterControlsToExistingPanel(model, current_shape) # 显示/隐藏按钮状态更新 if not hasattr(self, 'collision_visibility_button'): # 创建可见性按钮 self._addVisibilityButtonToExistingPanel(model) else: self.collision_visibility_button.setVisible(True) self._updateCollisionVisibilityButton(model) # 确保按钮顺序:可见性按钮在上,主按钮在下 if hasattr(self, 'collision_layout'): try: self._repositionButtons(self.collision_layout.rowCount()) except Exception: pass print(f"碰撞面板状态更新:有碰撞 - {current_shape}") else: # 无碰撞:显示添加按钮,下拉框变为可编辑 if hasattr(self, 'collision_button'): self.collision_button.setText("添加碰撞") # 应用统一按钮样式 self.collision_button.setStyleSheet(self._collision_button_style()) # 先断开所有连接,再重新连接 try: self.collision_button.clicked.disconnect() except: pass self.collision_button.clicked.connect(lambda: self._addCollisionAndUpdate(model)) else: # 若不存在主按钮,则创建“添加碰撞”按钮并加入布局 try: self.collision_button = self.createModernButton("添加碰撞") self.collision_button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.collision_button.setStyleSheet(self._collision_button_style()) self.collision_button.clicked.connect(lambda: self._addCollisionAndUpdate(model)) if hasattr(self, 'collision_layout') and self.collision_layout is not None: row = self.collision_layout.rowCount() self.collision_layout.addWidget(self.collision_button, row, 0, 1, 4) except Exception as _e: print(f"创建添加碰撞按钮失败: {_e}") # 恢复为可编辑状态 self.collision_shape_combo.setEnabled(True) # 隐藏并清理参数控件 - 只在有参数控件时才清理 if hasattr(self, 'collision_pos_x'): print("清理碰撞参数控件") self._hideCollisionParameterControls() # 隐藏显示/隐藏按钮 if hasattr(self, 'collision_visibility_button'): self.collision_visibility_button.setVisible(False) # 确保按钮顺序:主按钮位于底部 if hasattr(self, 'collision_layout'): try: self._repositionButtons(self.collision_layout.rowCount()) except Exception: pass print(f"碰撞面板状态更新:无碰撞 - 可编辑") except Exception as e: print(f"更新碰撞面板状态失败: {e}") import traceback traceback.print_exc() finally: # 确保标志被重置 self._updating_collision_panel = False def _loadCurrentCollisionParameters(self, model, shape_type): """加载当前碰撞参数到界面""" try: # 设置标志位,防止在加载参数时触发更新 self._loading_collision_params = True collision_nodes = model.findAllMatches("**/+CollisionNode") for collision_np in collision_nodes: collision_node = collision_np.node() if collision_node.getNumSolids() > 0: solid = collision_node.getSolid(0) # 从碰撞体形状中提取位置偏移 if hasattr(self, 'collision_pos_x'): # 获取模型的实际中心(考虑变换) if hasattr(self.world, 'collision_manager'): transformed_info = self.world.collision_manager._getTransformedModelInfo(model) if transformed_info: model_center = transformed_info['center'] else: model_center = model.getBounds().getCenter() if not model.getBounds().isEmpty() else Point3(0, 0, 0) else: model_center = model.getBounds().getCenter() if not model.getBounds().isEmpty() else Point3(0, 0, 0) # 获取碰撞体的中心 collision_center = self._getCollisionShapeCenter(solid) if collision_center: # 计算偏移:碰撞体中心 - 模型中心 offset_x = collision_center.x - model_center.x offset_y = collision_center.y - model_center.y offset_z = collision_center.z - model_center.z self.collision_pos_x.setValue(offset_x) self.collision_pos_y.setValue(offset_y) self.collision_pos_z.setValue(offset_z) print(f"加载位置偏移: ({offset_x:.2f}, {offset_y:.2f}, {offset_z:.2f})") else: # 如果无法计算偏移,设置为0 self.collision_pos_x.setValue(0.0) self.collision_pos_y.setValue(0.0) self.collision_pos_z.setValue(0.0) print("无法计算位置偏移,设置为0") if shape_type == 'sphere': self._loadSphereParameters(solid) elif shape_type == 'box': self._loadBoxParameters(solid) elif shape_type == 'capsule': self._loadCapsuleParameters(solid) elif shape_type == 'plane': self._loadPlaneParameters(solid) break except Exception as e: print(f"加载碰撞参数失败: {e}") finally: # 重置标志位 self._loading_collision_params = False def _getCollisionShapeCenter(self, solid): """从碰撞体形状中获取中心点""" try: from panda3d.core import CollisionSphere, CollisionBox, CollisionCapsule, CollisionPlane, Point3 if isinstance(solid, CollisionSphere): return solid.getCenter() elif isinstance(solid, CollisionBox): # 盒子的中心是最小点和最大点的中点 min_pt = solid.getMin() max_pt = solid.getMax() return Point3((min_pt.x + max_pt.x) * 0.5, (min_pt.y + max_pt.y) * 0.5, (min_pt.z + max_pt.z) * 0.5) elif isinstance(solid, CollisionCapsule): # 胶囊体的中心是两个端点的中点 point_a = solid.getPointA() point_b = solid.getPointB() return Point3((point_a.x + point_b.x) * 0.5, (point_a.y + point_b.y) * 0.5, (point_a.z + point_b.z) * 0.5) elif isinstance(solid, CollisionPlane): # 平面没有明确的中心,返回平面上的一个点 plane = solid.getPlane() return plane.getPoint() else: return None except Exception as e: print(f"获取碰撞体中心失败: {e}") return None def _loadSphereParameters(self, solid): """加载球形参数""" try: from panda3d.core import CollisionSphere if isinstance(solid, CollisionSphere): radius = solid.getRadius() self.collision_radius.setValue(radius) except Exception as e: print(f"加载球形参数失败: {e}") def _loadBoxParameters(self, solid): """加载盒型参数""" try: from panda3d.core import CollisionBox if isinstance(solid, CollisionBox): min_point = solid.getMin() max_point = solid.getMax() width = max_point.x - min_point.x length = max_point.y - min_point.y height = max_point.z - min_point.z self.collision_width.setValue(width) self.collision_length.setValue(length) self.collision_height.setValue(height) except Exception as e: print(f"加载盒型参数失败: {e}") def _loadCapsuleParameters(self, solid): """加载胶囊体参数""" try: from panda3d.core import CollisionCapsule if isinstance(solid, CollisionCapsule): radius = solid.getRadius() point_a = solid.getPointA() point_b = solid.getPointB() height = (point_b - point_a).length() self.collision_capsule_radius.setValue(radius) self.collision_capsule_height.setValue(height) except Exception as e: print(f"加载胶囊体参数失败: {e}") def _loadPlaneParameters(self, solid): """加载平面参数""" try: from panda3d.core import CollisionPlane if isinstance(solid, CollisionPlane): plane = solid.getPlane() normal = plane.getNormal() self.collision_normal_x.setValue(normal.x) self.collision_normal_y.setValue(normal.y) self.collision_normal_z.setValue(normal.z) except Exception as e: print(f"加载平面参数失败: {e}") def _updateCollisionPosition(self, model, axis, value): """更新碰撞位置偏移""" try: # 防止重复调用导致无限循环,以及在加载参数时防止更新 if getattr(self, '_updating_collision_position', False) or getattr(self, '_loading_collision_params', False): return self._updating_collision_position = True # 获取当前所有位置偏移值 pos_x = self.collision_pos_x.value() if hasattr(self, 'collision_pos_x') else 0.0 pos_y = self.collision_pos_y.value() if hasattr(self, 'collision_pos_y') else 0.0 pos_z = self.collision_pos_z.value() if hasattr(self, 'collision_pos_z') else 0.0 # 获取当前碰撞形状类型 current_shape = self._getCurrentCollisionShape(model) if not current_shape: return # 重新创建碰撞体,传入位置偏移 from panda3d.core import Vec3 position_offset = Vec3(pos_x, pos_y, pos_z) # 根据形状类型收集其他参数 if current_shape == 'sphere': radius = self.collision_radius.value() if hasattr(self, 'collision_radius') else 1.0 self._recreateCollisionShape(model, current_shape, radius=radius, position_offset=position_offset) elif current_shape == 'box': width = self.collision_width.value() if hasattr(self, 'collision_width') else 2.0 length = self.collision_length.value() if hasattr(self, 'collision_length') else 2.0 height = self.collision_height.value() if hasattr(self, 'collision_height') else 2.0 self._recreateCollisionShape(model, current_shape, width=width, length=length, height=height, position_offset=position_offset) elif current_shape == 'capsule': cap_radius = self.collision_capsule_radius.value() if hasattr(self, 'collision_capsule_radius') else 0.5 cap_height = self.collision_capsule_height.value() if hasattr(self, 'collision_capsule_height') else 2.0 self._recreateCollisionShape(model, current_shape, radius=cap_radius, height=cap_height, position_offset=position_offset) elif current_shape == 'plane': normal_x = self.collision_normal_x.value() if hasattr(self, 'collision_normal_x') else 0 normal_y = self.collision_normal_y.value() if hasattr(self, 'collision_normal_y') else 0 normal_z = self.collision_normal_z.value() if hasattr(self, 'collision_normal_z') else 1 self._recreateCollisionShape(model, current_shape, normal=(normal_x, normal_y, normal_z), position_offset=position_offset) print(f"更新碰撞位置偏移 {axis}: {value}") except Exception as e: print(f"更新碰撞位置失败: {e}") finally: self._updating_collision_position = False def _updateSphereRadius(self, model, radius): """更新球形半径""" try: # 防止重复调用导致无限循环,以及在加载参数时防止更新 if getattr(self, '_updating_sphere_radius', False) or getattr(self, '_loading_collision_params', False): return self._updating_sphere_radius = True # 获取当前位置偏移 from panda3d.core import Vec3 pos_x = self.collision_pos_x.value() if hasattr(self, 'collision_pos_x') else 0.0 pos_y = self.collision_pos_y.value() if hasattr(self, 'collision_pos_y') else 0.0 pos_z = self.collision_pos_z.value() if hasattr(self, 'collision_pos_z') else 0.0 position_offset = Vec3(pos_x, pos_y, pos_z) self._recreateCollisionShape(model, 'sphere', radius=radius, position_offset=position_offset) print(f"更新球形半径: {radius}") except Exception as e: print(f"更新球形半径失败: {e}") finally: self._updating_sphere_radius = False def _updateBoxSize(self, model, dimension, value): """更新盒型尺寸""" try: # 防止重复调用导致无限循环,以及在加载参数时防止更新 if getattr(self, '_updating_box_size', False) or getattr(self, '_loading_collision_params', False): return self._updating_box_size = True # 获取当前所有尺寸 width = self.collision_width.value() if hasattr(self, 'collision_width') else value length = self.collision_length.value() if hasattr(self, 'collision_length') else value height = self.collision_height.value() if hasattr(self, 'collision_height') else value # 获取当前位置偏移 from panda3d.core import Vec3 pos_x = self.collision_pos_x.value() if hasattr(self, 'collision_pos_x') else 0.0 pos_y = self.collision_pos_y.value() if hasattr(self, 'collision_pos_y') else 0.0 pos_z = self.collision_pos_z.value() if hasattr(self, 'collision_pos_z') else 0.0 position_offset = Vec3(pos_x, pos_y, pos_z) self._recreateCollisionShape(model, 'box', width=width, length=length, height=height, position_offset=position_offset) print(f"更新盒型{dimension}: {value}") except Exception as e: print(f"更新盒型尺寸失败: {e}") finally: self._updating_box_size = False def _updateCapsuleRadius(self, model, radius): """更新胶囊体半径""" try: # 防止重复调用导致无限循环,以及在加载参数时防止更新 if getattr(self, '_updating_capsule_radius', False) or getattr(self, '_loading_collision_params', False): return self._updating_capsule_radius = True height = self.collision_capsule_height.value() if hasattr(self, 'collision_capsule_height') else 2.0 # 获取当前位置偏移 from panda3d.core import Vec3 pos_x = self.collision_pos_x.value() if hasattr(self, 'collision_pos_x') else 0.0 pos_y = self.collision_pos_y.value() if hasattr(self, 'collision_pos_y') else 0.0 pos_z = self.collision_pos_z.value() if hasattr(self, 'collision_pos_z') else 0.0 position_offset = Vec3(pos_x, pos_y, pos_z) self._recreateCollisionShape(model, 'capsule', radius=radius, height=height, position_offset=position_offset) print(f"更新胶囊体半径: {radius}") except Exception as e: print(f"更新胶囊体半径失败: {e}") finally: self._updating_capsule_radius = False def _updateCapsuleHeight(self, model, height): """更新胶囊体高度""" try: # 防止重复调用导致无限循环,以及在加载参数时防止更新 if getattr(self, '_updating_capsule_height', False) or getattr(self, '_loading_collision_params', False): return self._updating_capsule_height = True radius = self.collision_capsule_radius.value() if hasattr(self, 'collision_capsule_radius') else 0.5 # 获取当前位置偏移 from panda3d.core import Vec3 pos_x = self.collision_pos_x.value() if hasattr(self, 'collision_pos_x') else 0.0 pos_y = self.collision_pos_y.value() if hasattr(self, 'collision_pos_y') else 0.0 pos_z = self.collision_pos_z.value() if hasattr(self, 'collision_pos_z') else 0.0 position_offset = Vec3(pos_x, pos_y, pos_z) self._recreateCollisionShape(model, 'capsule', radius=radius, height=height, position_offset=position_offset) print(f"更新胶囊体高度: {height}") except Exception as e: print(f"更新胶囊体高度失败: {e}") finally: self._updating_capsule_height = False def _updatePlaneNormal(self, model, axis, value): """更新平面法向量""" try: # 防止重复调用导致无限循环,以及在加载参数时防止更新 if getattr(self, '_updating_plane_normal', False) or getattr(self, '_loading_collision_params', False): return self._updating_plane_normal = True # 获取当前法向量 normal_x = self.collision_normal_x.value() if hasattr(self, 'collision_normal_x') else 0 normal_y = self.collision_normal_y.value() if hasattr(self, 'collision_normal_y') else 0 normal_z = self.collision_normal_z.value() if hasattr(self, 'collision_normal_z') else 1 # 获取当前位置偏移 from panda3d.core import Vec3 pos_x = self.collision_pos_x.value() if hasattr(self, 'collision_pos_x') else 0.0 pos_y = self.collision_pos_y.value() if hasattr(self, 'collision_pos_y') else 0.0 pos_z = self.collision_pos_z.value() if hasattr(self, 'collision_pos_z') else 0.0 position_offset = Vec3(pos_x, pos_y, pos_z) self._recreateCollisionShape(model, 'plane', normal=(normal_x, normal_y, normal_z), position_offset=position_offset) print(f"更新平面法向量 {axis}: {value}") except Exception as e: print(f"更新平面法向量失败: {e}") finally: self._updating_plane_normal = False def _recreateCollisionShape(self, model, shape_type, **kwargs): """重新创建碰撞形状(保持可见性)""" try: # 保存当前状态 collision_nodes = model.findAllMatches("**/+CollisionNode") if not collision_nodes: return collision_np = collision_nodes[0] is_visible = not collision_np.isHidden() # 移除旧的碰撞体 collision_np.removeNode() # 创建新的碰撞体 from panda3d.core import CollisionNode, BitMask32 cNode = CollisionNode(f'modelCollision_{model.getName()}') cNode.setIntoCollideMask(BitMask32.bit(2)) # 创建新形状(位置偏移已经烘焙在形状中) if hasattr(self.world, 'collision_manager'): collision_shape = self.world.collision_manager.createCollisionShape(model, shape_type, **kwargs) else: # 回退方案 from panda3d.core import CollisionSphere, Point3 collision_shape = CollisionSphere(Point3(0, 0, 0), kwargs.get('radius', 1.0)) cNode.addSolid(collision_shape) # 重新附加(不设置额外位置,因为位置偏移已经在形状中) new_collision_np = model.attachNewNode(cNode) # 碰撞节点默认位置为 (0, 0, 0),位置偏移通过形状几何体处理 new_collision_np.setPos(0, 0, 0) if is_visible: new_collision_np.show() else: new_collision_np.hide() except Exception as e: print(f"重新创建碰撞形状失败: {e}") import traceback traceback.print_exc() def _clearCollisionParameterControls(self): """清理碰撞参数控件""" try: # 位置控件 for attr in ['collision_pos_x', 'collision_pos_y', 'collision_pos_z']: if hasattr(self, attr): widget = getattr(self, attr) if widget and widget.parent(): widget.setParent(None) widget.deleteLater() delattr(self, attr) # 球形参数控件 if hasattr(self, 'collision_radius'): if self.collision_radius and self.collision_radius.parent(): self.collision_radius.setParent(None) self.collision_radius.deleteLater() delattr(self, 'collision_radius') # 盒型参数控件 for attr in ['collision_width', 'collision_length', 'collision_height']: if hasattr(self, attr): widget = getattr(self, attr) if widget and widget.parent(): widget.setParent(None) widget.deleteLater() delattr(self, attr) # 胶囊体参数控件 for attr in ['collision_capsule_radius', 'collision_capsule_height']: if hasattr(self, attr): widget = getattr(self, attr) if widget and widget.parent(): widget.setParent(None) widget.deleteLater() delattr(self, attr) # 平面参数控件 for attr in ['collision_normal_x', 'collision_normal_y', 'collision_normal_z']: if hasattr(self, attr): widget = getattr(self, attr) if widget and widget.parent(): widget.setParent(None) widget.deleteLater() delattr(self, attr) print("清理碰撞参数控件完成") except Exception as e: print(f"清理碰撞参数控件失败: {e}") def _refreshCollisionPanel(self, model): """刷新整个碰撞面板(简化版)""" try: print("使用简化的面板刷新") # 直接调用状态更新,不删除面板 self._updateCollisionPanelState(model) except Exception as e: print(f"刷新碰撞面板失败: {e}") import traceback traceback.print_exc() def _addParameterControlsToExistingPanel(self, model, shape_type): """向现有面板添加参数控件""" try: if not hasattr(self, 'collision_layout'): return layout = self.collision_layout # 首先清理可能存在的旧控件 self._hideCollisionParameterControls() # 找到插入位置(在按钮之前) current_row = 2 # 位置调整控件 pos_label = QLabel("位置偏移:") pos_label.setAlignment(Qt.AlignLeft | Qt.AlignTop) pos_label.setVisible(True) # 确保可见 # X, Y, Z 位置调整 self.collision_pos_x = self._createCollisionSpinBox(-1000000, 1000000, 2) self.collision_pos_y = self._createCollisionSpinBox(-1000000, 1000000, 2) self.collision_pos_z = self._createCollisionSpinBox(-1000000, 1000000, 2) for spinbox in (self.collision_pos_x, self.collision_pos_y, self.collision_pos_z): spinbox.setMinimumWidth(140) spinbox.setVisible(True) axis_controls = [ ("X:", self.collision_pos_x), ("Y:", self.collision_pos_y), ("Z:", self.collision_pos_z), ] for offset, (axis_text, spinbox) in enumerate(axis_controls): row = current_row + offset axis_layout = QHBoxLayout() if offset == 0: layout.addWidget(pos_label, row, 0) # axis_label.addWidget(pos_label) else: spacer = QLabel("") spacer.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) spacer.setVisible(True) layout.addWidget(spacer, row, 0) # axis_layout.addWidget(spacer) axis_label = QLabel(axis_text) axis_label.setAlignment(Qt.AlignVCenter) axis_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) axis_layout.addWidget(axis_label) axis_layout.addWidget(spinbox) # layout.addWidget(axis_label, row, 1) # layout.addWidget(spinbox, row, 2, 1, 2) layout.addLayout(axis_layout, row, 1, 1, 3) current_row += len(axis_controls) # 连接位置变化信号 self.collision_pos_x.valueChanged.connect(lambda v: self._updateCollisionPosition(model, 'x', v)) self.collision_pos_y.valueChanged.connect(lambda v: self._updateCollisionPosition(model, 'y', v)) self.collision_pos_z.valueChanged.connect(lambda v: self._updateCollisionPosition(model, 'z', v)) # 根据形状类型添加特定参数 if shape_type == 'sphere': current_row = self._addSphereParametersToExisting(model, layout, current_row) elif shape_type == 'box': current_row = self._addBoxParametersToExisting(model, layout, current_row) elif shape_type == 'capsule': current_row = self._addCapsuleParametersToExisting(model, layout, current_row) elif shape_type == 'plane': current_row = self._addPlaneParametersToExisting(model, layout, current_row) # 重新定位按钮 self._repositionButtons(current_row) # 加载参数值 self._loadCurrentCollisionParameters(model, shape_type) except Exception as e: print(f"添加参数控件到现有面板失败: {e}") import traceback traceback.print_exc() def _addSphereParametersToExisting(self, model, layout, start_row): """向现有面板添加球形参数""" current_row = start_row radius_label = QLabel("半径:") radius_label.setVisible(True) layout.addWidget(radius_label, current_row, 0) layout.addWidget(QLabel(""), current_row, 1) self.collision_radius = self._createCollisionSpinBox(0.01, 10000, 2) self.collision_radius.setVisible(True) self.collision_radius.valueChanged.connect(lambda v: self._updateSphereRadius(model, v)) layout.addWidget(self.collision_radius, current_row, 1, 1, 3) current_row += 1 return current_row def _addBoxParametersToExisting(self, model, layout, start_row): """向现有面板添加盒型参数""" current_row = start_row size_label = QLabel("尺寸:") size_label.setVisible(True) layout.addWidget(size_label, current_row, 0) # layout.addWidget(QLabel(""), current_row, 1) # current_row += 1 self.collision_width = self._createCollisionSpinBox(0.01, 10000, 2) self.collision_length = self._createCollisionSpinBox(0.01, 10000, 2) self.collision_height = self._createCollisionSpinBox(0.01, 10000, 2) box_controls = [ ("宽度:", self.collision_width), ("长度:", self.collision_length), ("高度:", self.collision_height), ] for offset,(text, spinbox) in enumerate(box_controls): axis_layout = QHBoxLayout() if offset == 0: layout.addWidget(size_label, current_row, 0) else: spacer = QLabel("") spacer.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) spacer.setVisible(True) layout.addWidget(spacer, current_row, 0) axis_label = QLabel(text) axis_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) axis_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) axis_label.setVisible(True) axis_layout.addWidget(axis_label) spinbox.setVisible(True) axis_layout.addWidget(spinbox) layout.addLayout(axis_layout, current_row, 1, 1, 3) current_row += 1 # 连接信号 self.collision_width.valueChanged.connect(lambda v: self._updateBoxSize(model, 'width', v)) self.collision_length.valueChanged.connect(lambda v: self._updateBoxSize(model, 'length', v)) self.collision_height.valueChanged.connect(lambda v: self._updateBoxSize(model, 'height', v)) return current_row def _addCapsuleParametersToExisting(self, model, layout, start_row): """向现有面板添加胶囊体参数""" current_row = start_row radius_label = QLabel("半径:") layout.addWidget(radius_label, current_row, 0) self.collision_capsule_radius = self._createCollisionSpinBox(0.01, 10000, 2) self.collision_capsule_radius.valueChanged.connect(lambda v: self._updateCapsuleRadius(model, v)) layout.addWidget(self.collision_capsule_radius, current_row, 1, 1 , 3) current_row += 1 height_label = QLabel("高度:") layout.addWidget(height_label, current_row, 0) self.collision_capsule_height = self._createCollisionSpinBox(0.01, 10000, 2) self.collision_capsule_height.valueChanged.connect(lambda v: self._updateCapsuleHeight(model, v)) layout.addWidget(self.collision_capsule_height, current_row, 1, 1, 3) current_row += 1 return current_row def _addPlaneParametersToExisting(self, model, layout, start_row): """向现有面板添加平面参数""" current_row = start_row normal_label = QLabel("法向量:") layout.addWidget(normal_label, current_row, 0) self.collision_normal_x = self._createCollisionSpinBox(-1, 1, 2) self.collision_normal_y = self._createCollisionSpinBox(-1, 1, 2) self.collision_normal_z = self._createCollisionSpinBox(-1, 1, 2) nx_layout = QHBoxLayout() axis_label = QLabel("Nx:") axis_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) axis_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) nx_layout.addWidget(axis_label) nx_layout.addWidget(self.collision_normal_x) layout.addLayout(nx_layout, current_row, 1, 1, 3) current_row += 1 ny_layout = QHBoxLayout() placeholder = QLabel("") layout.addWidget(placeholder, current_row, 0) axis_label = QLabel("Ny:") axis_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) axis_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) ny_layout.addWidget(axis_label) ny_layout.addWidget(self.collision_normal_y) layout.addLayout(ny_layout, current_row, 1, 1, 3) current_row += 1 nz_layout = QHBoxLayout() placeholder = QLabel("") layout.addWidget(placeholder, current_row, 0) axis_label = QLabel("Nz:") axis_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) axis_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) nz_layout.addWidget(axis_label) nz_layout.addWidget(self.collision_normal_z) layout.addLayout(nz_layout, current_row, 1, 1, 3) current_row += 1 # 连接信号 self.collision_normal_x.valueChanged.connect(lambda v: self._updatePlaneNormal(model, 'x', v)) self.collision_normal_y.valueChanged.connect(lambda v: self._updatePlaneNormal(model, 'y', v)) self.collision_normal_z.valueChanged.connect(lambda v: self._updatePlaneNormal(model, 'z', v)) return current_row def _addVisibilityButtonToExistingPanel(self, model): """向现有面板添加可见性按钮""" try: if hasattr(self, 'collision_layout'): layout = self.collision_layout is_collision_visible = self._isCollisionVisible(model) # 放在当前最后一行 current_row = layout.rowCount() self.collision_visibility_button = QPushButton("隐藏碰撞" if is_collision_visible else "显示碰撞") self.collision_visibility_button.setStyleSheet(self._collision_button_style()) self.collision_visibility_button.clicked.connect(lambda: self._toggleCollisionVisibility(model)) layout.addWidget(self.collision_visibility_button, current_row, 0, 1, 4) # 确保存在主按钮(有碰撞时应显示移除碰撞) if self._hasCollision(model) and not hasattr(self, 'collision_button'): try: self.collision_button = self.createModernButton("移除碰撞") self.collision_button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.collision_button.setStyleSheet(self._collision_button_style()) self.collision_button.clicked.connect(lambda: self._removeCollisionAndUpdate(model)) layout.addWidget(self.collision_button, current_row + 1, 0, 1, 4) except Exception as _e: print(f"在可见性按钮后创建移除碰撞按钮失败: {_e}") except Exception as e: print(f"添加可见性按钮失败: {e}") def _repositionButtons(self, new_row): """重新定位按钮位置""" try: if hasattr(self, 'collision_layout'): layout = self.collision_layout def _remove_if_present(w): try: if not w: return # 从布局中移除该控件(如果已存在) for i in range(layout.count()): item = layout.itemAt(i) if item and item.widget() is w: layout.removeWidget(w) break except Exception: pass # 计算底部行 bottom_row = layout.rowCount() # 按固定顺序重新放置:先可见性按钮,再主按钮 if hasattr(self, 'collision_visibility_button') and self.collision_visibility_button: _remove_if_present(self.collision_visibility_button) layout.addWidget(self.collision_visibility_button, bottom_row, 0, 1, 4) bottom_row += 1 if hasattr(self, 'collision_button') and self.collision_button: _remove_if_present(self.collision_button) layout.addWidget(self.collision_button, bottom_row, 0, 1, 4) except Exception as e: print(f"重新定位按钮失败: {e}") def _hideCollisionParameterControls(self): """隐藏并移除碰撞参数控件与其所在的行(保留状态/形状/按钮行)""" try: # 1) 先清理对象属性引用,避免残留信号 param_attrs = [ 'collision_pos_x', 'collision_pos_y', 'collision_pos_z', 'collision_radius', 'collision_width', 'collision_length', 'collision_height', 'collision_capsule_radius', 'collision_capsule_height', 'collision_normal_x', 'collision_normal_y', 'collision_normal_z' ] for attr in param_attrs: if hasattr(self, attr): widget = getattr(self, attr) try: if widget: widget.setParent(None) widget.deleteLater() except Exception: pass delattr(self, attr) # 2) 深度移除布局中第2行(索引>=2)之后的所有参数相关项,保留按钮 if hasattr(self, 'collision_layout') and self.collision_layout is not None: layout = self.collision_layout def _remove_layout_recursive(q_layout): try: while q_layout.count(): child = q_layout.takeAt(0) if child.widget(): w = child.widget() w.setParent(None) w.deleteLater() elif child.layout(): _remove_layout_recursive(child.layout()) except Exception: pass row_count = layout.rowCount() col_count = layout.columnCount() # 从参数区域开始清理(行索引2及以后),但跳过我们要保留的按钮 for i in range(2, row_count): for j in range(0, col_count): item = layout.itemAtPosition(i, j) if not item: continue # 若是按钮,且是保留的按钮,则跳过 w = item.widget() if w is not None: try: if (hasattr(self, 'collision_button') and w == self.collision_button) or (hasattr(self, 'collision_visibility_button') and w == self.collision_visibility_button): continue except Exception: pass layout.removeWidget(w) w.setParent(None) w.deleteLater() elif item.layout(): # 移除嵌套布局(包含标签与输入框) nested = item.layout() _remove_layout_recursive(nested) layout.removeItem(nested) # 尝试把保留的按钮移动到参数区域第一行(行2)下方,保持整洁 try: next_row = 2 if hasattr(self, 'collision_visibility_button'): self.collision_visibility_button.setVisible(False) if hasattr(self, 'collision_button') and self.collision_button is not None: layout.addWidget(self.collision_button, next_row, 0, 1, 4) except Exception: pass print("隐藏碰撞参数控件完成(仅保留状态、形状与按钮)") except Exception as e: print(f"隐藏碰撞参数控件失败: {e}") import traceback traceback.print_exc()