1
0
forked from Rowland/EG
EG/ui/property_panel.py
Hector afb4a4bb50 3d_image和2d_image
根据高度图创建地形
2025-08-27 17:17:54 +08:00

5976 lines
250 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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