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