1635 lines
54 KiB
Python
1635 lines
54 KiB
Python
import os
|
|
import sys
|
|
import maya.cmds as cmds
|
|
|
|
try:
|
|
from PySide import QtCore, QtGui, QtWidgets
|
|
print(f"从PySide加载Qt")
|
|
except ImportError as e:
|
|
try:
|
|
from PySide2 import QtCore, QtGui, QtWidgets
|
|
print(f"从PySide2加载Qt")
|
|
except ImportError as e:
|
|
try:
|
|
from PySide6 import QtCore, QtGui, QtWidgets
|
|
print(f"从PySide6加载Qt")
|
|
except ImportError as e:
|
|
print(f"PySide6加载失败: {str(e)}")
|
|
|
|
try:
|
|
from shiboken import wrapInstance
|
|
print(f"从shiboken加载wrapInstance")
|
|
except ImportError as e:
|
|
cmds.warning(f"shiboken加载失败: {str(e)}")
|
|
try:
|
|
from shiboken2 import wrapInstance
|
|
print(f"从shiboken2加载wrapInstance")
|
|
except ImportError as e:
|
|
cmds.warning(f"shiboken2加载失败: {str(e)}")
|
|
try:
|
|
from shiboken6 import wrapInstance
|
|
print(f"从shiboken6加载wrapInstance")
|
|
except ImportError as e:
|
|
cmds.warning(f"shiboken6加载失败: {str(e)}")
|
|
|
|
from scripts.config.data import (
|
|
ROOT_PATH,
|
|
TOOL_NAME,
|
|
TOOL_VERSION,
|
|
TOOL_AUTHOR,
|
|
TOOL_LANG,
|
|
TOOL_WSCL_NAME,
|
|
TOOL_HELP_URL,
|
|
SCRIPTS_PATH,
|
|
ICONS_PATH,
|
|
STYLES_PATH,
|
|
DNA_FILE_PATH,
|
|
DNA_IMG_PATH,
|
|
PLUGIN_PATH,
|
|
PYDNA_PATH,
|
|
DNACALIB_PATH,
|
|
BUILDER_PATH,
|
|
DNALIB_PATH,
|
|
UI_PATH,
|
|
UTILS_PATH,
|
|
TOOL_MAIN_SCRIPT,
|
|
TOOL_STYLE_FILE,
|
|
TOOL_ICON,
|
|
TOOL_COMMAND_ICON,
|
|
TOOL_MOD_FILENAME,
|
|
STYLE_FILE
|
|
)
|
|
|
|
if {ROOT_PATH,
|
|
SCRIPTS_PATH,
|
|
ICONS_PATH,
|
|
STYLES_PATH,
|
|
DNA_FILE_PATH,
|
|
DNA_IMG_PATH,
|
|
PLUGIN_PATH,
|
|
PYDNA_PATH,
|
|
DNACALIB_PATH,
|
|
BUILDER_PATH,
|
|
DNALIB_PATH,
|
|
UI_PATH,
|
|
UTILS_PATH
|
|
} not in sys.path:
|
|
for path in [
|
|
ROOT_PATH,
|
|
SCRIPTS_PATH,
|
|
ICONS_PATH,
|
|
STYLES_PATH,
|
|
DNA_FILE_PATH,
|
|
DNA_IMG_PATH,
|
|
PLUGIN_PATH,
|
|
PYDNA_PATH,
|
|
DNACALIB_PATH,
|
|
BUILDER_PATH,
|
|
DNALIB_PATH,
|
|
UI_PATH,
|
|
UTILS_PATH
|
|
]:
|
|
if path not in sys.path:
|
|
sys.path.append(path)
|
|
|
|
class BaseWidget(QtWidgets.QWidget):
|
|
"""基础控件类"""
|
|
def __init__(self, parent=None):
|
|
super(BaseWidget, self).__init__(parent)
|
|
self.setup_ui()
|
|
|
|
def setup_ui(self):
|
|
"""设置UI"""
|
|
pass
|
|
|
|
class SearchLineEdit(QtWidgets.QLineEdit):
|
|
"""搜索输入框"""
|
|
def __init__(self, parent=None):
|
|
super(SearchLineEdit, self).__init__(parent)
|
|
self.setup_ui()
|
|
|
|
def setup_ui(self):
|
|
self.setPlaceholderText("搜索...")
|
|
self.setClearButtonEnabled(True)
|
|
self.setMinimumWidth(200)
|
|
|
|
class LoadButton(QtWidgets.QPushButton):
|
|
"""加载按钮"""
|
|
def __init__(self, text="", icon_name="target.png", parent=None):
|
|
super(LoadButton, self).__init__(parent)
|
|
self.setup_ui(text, icon_name)
|
|
|
|
def setup_ui(self, text, icon_name):
|
|
if text:
|
|
self.setText(text)
|
|
icon_path = os.path.join(ICONS_PATH, icon_name)
|
|
if os.path.exists(icon_path):
|
|
self.setIcon(QtGui.QIcon(icon_path))
|
|
self.setFixedSize(25, 25)
|
|
|
|
class ModelInput(QtWidgets.QWidget):
|
|
"""模型输入组件"""
|
|
def __init__(self, label="", parent=None):
|
|
super(ModelInput, self).__init__(parent)
|
|
self.label = label
|
|
self.setup_ui()
|
|
|
|
def setup_ui(self):
|
|
layout = QtWidgets.QHBoxLayout(self)
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
layout.setSpacing(2)
|
|
|
|
# 标签
|
|
if self.label:
|
|
label = QtWidgets.QLabel(self.label)
|
|
layout.addWidget(label)
|
|
|
|
# 输入框
|
|
self.line_edit = QtWidgets.QLineEdit()
|
|
layout.addWidget(self.line_edit)
|
|
|
|
# 加载按钮
|
|
self.load_btn = LoadButton()
|
|
layout.addWidget(self.load_btn)
|
|
|
|
class LODGroup(QtWidgets.QGroupBox):
|
|
"""LOD分组"""
|
|
def __init__(self, lod_index, parent=None):
|
|
super(LODGroup, self).__init__(parent)
|
|
self.lod_index = lod_index
|
|
self.setup_ui()
|
|
|
|
def setup_ui(self):
|
|
self.setTitle(f"LOD{self.lod_index}")
|
|
|
|
# 主布局
|
|
main_layout = QtWidgets.QVBoxLayout(self)
|
|
|
|
# 顶部工具栏
|
|
toolbar = QtWidgets.QHBoxLayout()
|
|
toolbar.addStretch()
|
|
|
|
# 删除按钮
|
|
delete_btn = LoadButton(icon_name="delete.png")
|
|
delete_btn.clicked.connect(lambda: self.delete_lod())
|
|
toolbar.addWidget(delete_btn)
|
|
|
|
main_layout.addLayout(toolbar)
|
|
|
|
# 模型输入列表
|
|
self.model_list = QtWidgets.QVBoxLayout()
|
|
self.add_model_inputs()
|
|
main_layout.addLayout(self.model_list)
|
|
|
|
def add_model_inputs(self):
|
|
"""添加模型输入组件"""
|
|
# 根据LOD级别添加不同的输入组件
|
|
model_types = self.get_model_types()
|
|
for model_type, label in model_types:
|
|
input_widget = ModelInput(label)
|
|
input_widget.load_btn.clicked.connect(
|
|
lambda checked, t=model_type: self.load_model(t))
|
|
self.model_list.addWidget(input_widget)
|
|
|
|
def get_model_types(self):
|
|
"""获取当前LOD级别需要的模型类型"""
|
|
# 基础类型
|
|
types = [
|
|
("head", "头部"),
|
|
("teeth", "牙齿")
|
|
]
|
|
|
|
# 根据LOD级别添加额外类型
|
|
if self.lod_index <= 3:
|
|
types.extend([
|
|
("eye_l", "左眼"),
|
|
("eye_r", "右眼"),
|
|
("iris", "虹膜"),
|
|
("eyelash", "睫毛"),
|
|
("eyelid", "眼睑")
|
|
])
|
|
|
|
if self.lod_index <= 2:
|
|
types.append(("gums", "牙龈"))
|
|
|
|
if self.lod_index <= 1:
|
|
types.append(("cartilage", "软骨"))
|
|
|
|
if self.lod_index <= 2:
|
|
types.append(("body", "身体"))
|
|
|
|
return types
|
|
|
|
def load_model(self, model_type):
|
|
"""加载模型"""
|
|
from scripts.utils import model_utils
|
|
model_utils.load_model(self.lod_index, model_type)
|
|
|
|
def delete_lod(self):
|
|
"""删除当前LOD"""
|
|
from scripts.utils import model_utils
|
|
model_utils.delete_lod(self.lod_index)
|
|
|
|
class IconButton(QtWidgets.QPushButton):
|
|
"""图标按钮"""
|
|
def __init__(self, icon_name="", tooltip="", parent=None):
|
|
super(IconButton, self).__init__(parent)
|
|
self.setup_ui(icon_name, tooltip)
|
|
|
|
def setup_ui(self, icon_name, tooltip):
|
|
if icon_name:
|
|
icon_path = os.path.join(ICONS_PATH, icon_name)
|
|
if os.path.exists(icon_path):
|
|
self.setIcon(QtGui.QIcon(icon_path))
|
|
if tooltip:
|
|
self.setToolTip(tooltip)
|
|
self.setFixedSize(30, 30)
|
|
|
|
class SliderWithValue(QtWidgets.QWidget):
|
|
"""带数值显示的滑块"""
|
|
valueChanged = QtCore.Signal(float)
|
|
|
|
def __init__(self, min_val=0.0, max_val=1.0, default=0.0, parent=None):
|
|
super(SliderWithValue, self).__init__(parent)
|
|
self.min_val = min_val
|
|
self.max_val = max_val
|
|
self.default = default
|
|
self.setup_ui()
|
|
|
|
def setup_ui(self):
|
|
layout = QtWidgets.QHBoxLayout(self)
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
# 滑块
|
|
self.slider = QtWidgets.QSlider(QtCore.Qt.Horizontal)
|
|
self.slider.setRange(0, 100)
|
|
self.slider.setValue(self.to_slider_value(self.default))
|
|
self.slider.valueChanged.connect(self.on_slider_changed)
|
|
layout.addWidget(self.slider)
|
|
|
|
# 数值显示
|
|
self.value_spin = QtWidgets.QDoubleSpinBox()
|
|
self.value_spin.setRange(self.min_val, self.max_val)
|
|
self.value_spin.setValue(self.default)
|
|
self.value_spin.setSingleStep(0.01)
|
|
self.value_spin.valueChanged.connect(self.on_spin_changed)
|
|
layout.addWidget(self.value_spin)
|
|
|
|
def to_slider_value(self, value):
|
|
"""转换为滑块值"""
|
|
return int((value - self.min_val) * 100 / (self.max_val - self.min_val))
|
|
|
|
def to_real_value(self, slider_value):
|
|
"""转换为实际值"""
|
|
return self.min_val + slider_value * (self.max_val - self.min_val) / 100
|
|
|
|
def on_slider_changed(self, value):
|
|
real_value = self.to_real_value(value)
|
|
self.value_spin.setValue(real_value)
|
|
self.valueChanged.emit(real_value)
|
|
|
|
def on_spin_changed(self, value):
|
|
self.slider.setValue(self.to_slider_value(value))
|
|
self.valueChanged.emit(value)
|
|
|
|
class DNABrowser(QtWidgets.QWidget):
|
|
"""DNA浏览器"""
|
|
dnaSelected = QtCore.Signal(str) # 发送选中的DNA文件路径
|
|
|
|
def __init__(self, parent=None):
|
|
super(DNABrowser, self).__init__(parent)
|
|
self.setup_ui()
|
|
self.load_dna_files()
|
|
|
|
def setup_ui(self):
|
|
layout = QtWidgets.QVBoxLayout(self)
|
|
|
|
# 工具栏
|
|
toolbar = QtWidgets.QHBoxLayout()
|
|
|
|
# 缩放滑块
|
|
self.zoom_slider = QtWidgets.QSlider(QtCore.Qt.Horizontal)
|
|
self.zoom_slider.setRange(50, 200)
|
|
self.zoom_slider.setValue(100)
|
|
self.zoom_slider.valueChanged.connect(self.on_zoom_changed)
|
|
toolbar.addWidget(QtWidgets.QLabel("缩放:"))
|
|
toolbar.addWidget(self.zoom_slider)
|
|
|
|
layout.addLayout(toolbar)
|
|
|
|
# DNA网格视图
|
|
self.grid_view = QtWidgets.QListView()
|
|
self.grid_view.setViewMode(QtWidgets.QListView.IconMode)
|
|
self.grid_view.setIconSize(QtCore.QSize(100, 100))
|
|
self.grid_view.setSpacing(10)
|
|
self.grid_view.setResizeMode(QtWidgets.QListView.Adjust)
|
|
self.grid_view.clicked.connect(self.on_dna_selected)
|
|
|
|
self.model = QtGui.QStandardItemModel()
|
|
self.grid_view.setModel(self.model)
|
|
|
|
layout.addWidget(self.grid_view)
|
|
|
|
def load_dna_files(self):
|
|
"""加载DNA文件"""
|
|
self.model.clear()
|
|
|
|
# 从DNA目录加载文件
|
|
dna_path = DNA_FILE_PATH
|
|
img_path = DNA_IMG_PATH
|
|
|
|
if os.path.exists(dna_path):
|
|
for file in os.listdir(dna_path):
|
|
if file.endswith(".dna"):
|
|
# 创建项
|
|
item = QtGui.QStandardItem()
|
|
item.setText(os.path.splitext(file)[0])
|
|
item.setData(os.path.join(dna_path, file), QtCore.Qt.UserRole)
|
|
|
|
# 设置图标
|
|
img_file = os.path.join(img_path, f"{os.path.splitext(file)[0]}.png")
|
|
if os.path.exists(img_file):
|
|
item.setIcon(QtGui.QIcon(img_file))
|
|
|
|
self.model.appendRow(item)
|
|
|
|
def on_zoom_changed(self, value):
|
|
"""缩放改变"""
|
|
size = value
|
|
self.grid_view.setIconSize(QtCore.QSize(size, size))
|
|
|
|
def on_dna_selected(self, index):
|
|
"""DNA选中"""
|
|
item = self.model.itemFromIndex(index)
|
|
dna_path = item.data(QtCore.Qt.UserRole)
|
|
self.dnaSelected.emit(dna_path)
|
|
|
|
class DescriptionWidget(QtWidgets.QGroupBox):
|
|
"""描述信息组件"""
|
|
def __init__(self, parent=None):
|
|
super(DescriptionWidget, self).__init__("描述", parent)
|
|
self.setup_ui()
|
|
|
|
def setup_ui(self):
|
|
layout = QtWidgets.QFormLayout(self)
|
|
|
|
# 名称
|
|
self.name_edit = QtWidgets.QLineEdit()
|
|
layout.addRow("名称:", self.name_edit)
|
|
|
|
# 原型
|
|
self.prototype_combo = QtWidgets.QComboBox()
|
|
self.prototype_combo.addItems(["亚洲人", "黑人", "高加素人", "拉美裔", "外星人", "其他"])
|
|
layout.addRow("原型:", self.prototype_combo)
|
|
|
|
# 性别
|
|
self.gender_combo = QtWidgets.QComboBox()
|
|
self.gender_combo.addItems(["男性", "女性", "其他"])
|
|
layout.addRow("性别:", self.gender_combo)
|
|
|
|
# 年龄
|
|
self.age_spin = QtWidgets.QSpinBox()
|
|
self.age_spin.setRange(0, 100)
|
|
self.age_spin.setValue(24)
|
|
layout.addRow("年龄:", self.age_spin)
|
|
|
|
# 变换单位
|
|
self.unit_combo = QtWidgets.QComboBox()
|
|
self.unit_combo.addItems(["厘米", "米"])
|
|
layout.addRow("变换单位:", self.unit_combo)
|
|
|
|
# 旋转单位
|
|
self.rotation_combo = QtWidgets.QComboBox()
|
|
self.rotation_combo.addItems(["角度", "弧度"])
|
|
layout.addRow("旋转单位:", self.rotation_combo)
|
|
|
|
# 坐标向上
|
|
self.up_combo = QtWidgets.QComboBox()
|
|
self.up_combo.addItems(["Y轴向上", "Z轴向上"])
|
|
layout.addRow("坐标向上:", self.up_combo)
|
|
|
|
# LOD计数
|
|
self.lod_spin = QtWidgets.QSpinBox()
|
|
self.lod_spin.setRange(1, 8)
|
|
self.lod_spin.setValue(8)
|
|
layout.addRow("LOD计数:", self.lod_spin)
|
|
|
|
class BlendShapeList(QtWidgets.QWidget):
|
|
"""BlendShape列表组件"""
|
|
def __init__(self, title="BlendShapes", parent=None):
|
|
super(BlendShapeList, self).__init__(parent)
|
|
self.title = title
|
|
self.setup_ui()
|
|
|
|
def setup_ui(self):
|
|
layout = QtWidgets.QVBoxLayout(self)
|
|
|
|
# 标题栏
|
|
title_layout = QtWidgets.QHBoxLayout()
|
|
title_label = QtWidgets.QLabel(self.title)
|
|
self.count_label = QtWidgets.QLabel("[0/0]")
|
|
title_layout.addWidget(title_label)
|
|
title_layout.addWidget(self.count_label)
|
|
title_layout.addStretch()
|
|
layout.addLayout(title_layout)
|
|
|
|
# 搜索栏
|
|
self.search_edit = SearchLineEdit()
|
|
self.search_edit.textChanged.connect(self.filter_items)
|
|
layout.addWidget(self.search_edit)
|
|
|
|
# BS列表
|
|
self.list_widget = QtWidgets.QListWidget()
|
|
self.list_widget.setSelectionMode(QtWidgets.QListWidget.ExtendedSelection)
|
|
layout.addWidget(self.list_widget)
|
|
|
|
def filter_items(self, text):
|
|
"""过滤列表项"""
|
|
for i in range(self.list_widget.count()):
|
|
item = self.list_widget.item(i)
|
|
item.setHidden(text.lower() not in item.text().lower())
|
|
|
|
def update_count(self):
|
|
"""更新计数"""
|
|
visible = sum(1 for i in range(self.list_widget.count())
|
|
if not self.list_widget.item(i).isHidden())
|
|
total = self.list_widget.count()
|
|
self.count_label.setText(f"[{visible}/{total}]")
|
|
|
|
class BlendShapeControls(QtWidgets.QWidget):
|
|
"""BlendShape控制组件"""
|
|
def __init__(self, parent=None):
|
|
super(BlendShapeControls, self).__init__(parent)
|
|
self.setup_ui()
|
|
|
|
def setup_ui(self):
|
|
layout = QtWidgets.QVBoxLayout(self)
|
|
|
|
# 工具栏
|
|
toolbar = QtWidgets.QHBoxLayout()
|
|
|
|
# 恢复表情按钮
|
|
reset_btn = IconButton("reset.png", "恢复表情")
|
|
reset_btn.clicked.connect(self.reset_blendshapes)
|
|
toolbar.addWidget(reset_btn)
|
|
|
|
# 混合筛选按钮
|
|
filter_btn = IconButton("blendRaw.png", "混合筛选")
|
|
filter_btn.clicked.connect(self.filter_blendshapes)
|
|
toolbar.addWidget(filter_btn)
|
|
|
|
# 分类按钮组
|
|
for i in range(1, 7):
|
|
btn = QtWidgets.QPushButton(str(i) if i > 1 else "全部")
|
|
btn.setCheckable(True)
|
|
btn.setFixedWidth(40)
|
|
toolbar.addWidget(btn)
|
|
|
|
layout.addLayout(toolbar)
|
|
|
|
# 数值控制
|
|
value_layout = QtWidgets.QHBoxLayout()
|
|
|
|
self.value_slider = SliderWithValue(min_val=0.0, max_val=1.0, default=0.01)
|
|
value_layout.addWidget(self.value_slider)
|
|
|
|
self.all_check = QtWidgets.QCheckBox("全部")
|
|
value_layout.addWidget(self.all_check)
|
|
|
|
layout.addLayout(value_layout)
|
|
|
|
# 范围控制
|
|
range_layout = QtWidgets.QHBoxLayout()
|
|
|
|
range_plus = QtWidgets.QPushButton("范围 +")
|
|
range_minus = QtWidgets.QPushButton("范围 -")
|
|
range_layout.addWidget(range_plus)
|
|
range_layout.addWidget(range_minus)
|
|
|
|
layout.addLayout(range_layout)
|
|
|
|
def reset_blendshapes(self):
|
|
"""重置所有BlendShape"""
|
|
from scripts.utils import adjust_utils
|
|
adjust_utils.reset_blendshapes()
|
|
|
|
def filter_blendshapes(self):
|
|
"""过滤BlendShape"""
|
|
from scripts.utils import adjust_utils
|
|
adjust_utils.filter_blendshapes()
|
|
|
|
class BlendShapeTools(QtWidgets.QWidget):
|
|
"""BlendShape工具组件"""
|
|
def __init__(self, parent=None):
|
|
super(BlendShapeTools, self).__init__(parent)
|
|
self.setup_ui()
|
|
|
|
def setup_ui(self):
|
|
layout = QtWidgets.QVBoxLayout(self)
|
|
|
|
# 工具按钮
|
|
tools_layout = QtWidgets.QHBoxLayout()
|
|
|
|
# 翻转
|
|
flip_btn = IconButton("mirrorL.png", "翻转")
|
|
tools_layout.addWidget(flip_btn)
|
|
|
|
# 镜像目标
|
|
mirror_btn = IconButton("symmetry.png", "镜像目标")
|
|
tools_layout.addWidget(mirror_btn)
|
|
|
|
# 查找翻转目标
|
|
find_flip_btn = IconButton("mirrorR.png", "查找翻转目标")
|
|
tools_layout.addWidget(find_flip_btn)
|
|
|
|
# 添加混合目标
|
|
add_bs_btn = IconButton("blendShape.png", "添加混合目标")
|
|
tools_layout.addWidget(add_bs_btn)
|
|
|
|
# 删除混合目标
|
|
del_bs_btn = IconButton("blendShape.png", "删除混合目标")
|
|
tools_layout.addWidget(del_bs_btn)
|
|
|
|
# 批量混合目标
|
|
batch_bs_btn = IconButton("blendShape.png", "批量混合目标")
|
|
tools_layout.addWidget(batch_bs_btn)
|
|
|
|
layout.addLayout(tools_layout)
|
|
|
|
# 功能开关
|
|
toggle_layout = QtWidgets.QHBoxLayout()
|
|
|
|
toggles = [
|
|
("PSD", "psd.png"),
|
|
("BSE", "blendShape.png"),
|
|
("KEY", "centerCurrentTime.png"),
|
|
("MIR", "mirrorR.png"),
|
|
("ARK", "ARKit52.png"),
|
|
("CTR", "ctrl_hide.png")
|
|
]
|
|
|
|
for text, icon in toggles:
|
|
btn = IconButton(icon, text)
|
|
btn.setCheckable(True)
|
|
toggle_layout.addWidget(btn)
|
|
|
|
layout.addLayout(toggle_layout)
|
|
|
|
# 表情控制
|
|
expr_layout = QtWidgets.QHBoxLayout()
|
|
|
|
expr_btns = [
|
|
("还原默认表情", "reset.png"),
|
|
("选择选择表情", "expressions_current.png"),
|
|
("写入当前表情", "expression.png"),
|
|
("控制面板查找", "controller.png"),
|
|
("选择关联关节", "kinJoint.png"),
|
|
("写入镜像表情", "ctrl_hide.png")
|
|
]
|
|
|
|
for text, icon in expr_btns:
|
|
btn = IconButton(icon, text)
|
|
expr_layout.addWidget(btn)
|
|
|
|
layout.addLayout(expr_layout)
|
|
|
|
class WeightEditorWidget(QtWidgets.QWidget):
|
|
"""权重编辑器界面"""
|
|
|
|
weightChanged = QtCore.Signal(str, float) # 权重变化信号(目标名称, 权重值)
|
|
|
|
def __init__(self, parent=None):
|
|
super(WeightEditorWidget, self).__init__(parent)
|
|
self.bs_node = None
|
|
self.setup_ui()
|
|
|
|
def setup_ui(self):
|
|
"""创建界面"""
|
|
layout = QtWidgets.QVBoxLayout(self)
|
|
|
|
# 顶部工具栏
|
|
toolbar = QtWidgets.QHBoxLayout()
|
|
|
|
# BlendShape节点选择
|
|
self.bs_combo = QtWidgets.QComboBox()
|
|
self.bs_combo.currentTextChanged.connect(self.on_bs_changed)
|
|
toolbar.addWidget(self.bs_combo)
|
|
|
|
# 刷新按钮
|
|
refresh_btn = IconButton("refresh.png", "刷新")
|
|
refresh_btn.clicked.connect(self.refresh_weights)
|
|
toolbar.addWidget(refresh_btn)
|
|
|
|
# 优化按钮
|
|
optimize_btn = IconButton("optimize.png", "优化权重")
|
|
optimize_btn.clicked.connect(self.optimize_weights)
|
|
toolbar.addWidget(optimize_btn)
|
|
|
|
# 分析按钮
|
|
analyze_btn = IconButton("analyze.png", "分析权重")
|
|
analyze_btn.clicked.connect(self.analyze_weights)
|
|
toolbar.addWidget(analyze_btn)
|
|
|
|
layout.addLayout(toolbar)
|
|
|
|
# 搜索栏
|
|
self.search_edit = SearchLineEdit()
|
|
self.search_edit.textChanged.connect(self.filter_targets)
|
|
layout.addWidget(self.search_edit)
|
|
|
|
# 权重列表
|
|
self.weight_list = QtWidgets.QTreeWidget()
|
|
self.weight_list.setHeaderLabels(["目标", "权重"])
|
|
self.weight_list.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
|
|
self.weight_list.itemChanged.connect(self.on_item_changed)
|
|
layout.addWidget(self.weight_list)
|
|
|
|
# 底部工具栏
|
|
bottom_toolbar = QtWidgets.QHBoxLayout()
|
|
|
|
# 批量设置
|
|
bottom_toolbar.addWidget(QtWidgets.QLabel("批量设置:"))
|
|
self.batch_spin = QtWidgets.QDoubleSpinBox()
|
|
self.batch_spin.setRange(0, 1)
|
|
self.batch_spin.setSingleStep(0.1)
|
|
bottom_toolbar.addWidget(self.batch_spin)
|
|
|
|
apply_btn = IconButton("apply.png", "应用")
|
|
apply_btn.clicked.connect(self.apply_batch_weight)
|
|
bottom_toolbar.addWidget(apply_btn)
|
|
|
|
bottom_toolbar.addStretch()
|
|
|
|
# 快捷按钮
|
|
for value in [0, 0.5, 1.0]:
|
|
btn = QtWidgets.QPushButton(str(value))
|
|
btn.clicked.connect(lambda x, v=value: self.quick_set_weight(v))
|
|
bottom_toolbar.addWidget(btn)
|
|
|
|
layout.addLayout(bottom_toolbar)
|
|
|
|
def set_blendshape(self, bs_node):
|
|
"""设置BlendShape节点"""
|
|
self.bs_node = bs_node
|
|
self.refresh_weights()
|
|
|
|
def refresh_weights(self):
|
|
"""刷新权重列表"""
|
|
from scripts.utils import adjust_utils
|
|
|
|
self.weight_list.clear()
|
|
|
|
if not self.bs_node:
|
|
return
|
|
|
|
# 获取所有目标
|
|
targets = adjust_utils.bs_manager.get_all_targets(self.bs_node)
|
|
|
|
# 添加目标项
|
|
for target in targets:
|
|
weight = adjust_utils.bs_manager.get_weight(self.bs_node, target)
|
|
item = QtWidgets.QTreeWidgetItem([target, str(weight)])
|
|
item.setFlags(item.flags() | QtCore.Qt.ItemIsEditable)
|
|
self.weight_list.addTopLevelItem(item)
|
|
|
|
def filter_targets(self, text):
|
|
"""过滤目标"""
|
|
for i in range(self.weight_list.topLevelItemCount()):
|
|
item = self.weight_list.topLevelItem(i)
|
|
item.setHidden(text.lower() not in item.text(0).lower())
|
|
|
|
def on_item_changed(self, item, column):
|
|
"""项目变化"""
|
|
if column == 1: # 权重列
|
|
try:
|
|
target = item.text(0)
|
|
weight = float(item.text(1))
|
|
self.weightChanged.emit(target, weight)
|
|
except ValueError:
|
|
pass
|
|
|
|
def on_bs_changed(self, bs_node):
|
|
"""BlendShape节点变化"""
|
|
self.set_blendshape(bs_node)
|
|
|
|
def optimize_weights(self):
|
|
"""优化权重"""
|
|
from scripts.utils import adjust_utils
|
|
|
|
if self.bs_node:
|
|
adjust_utils.optimize_blendshape_weights(self.bs_node)
|
|
self.refresh_weights()
|
|
|
|
def analyze_weights(self):
|
|
"""分析权重"""
|
|
from scripts.utils import adjust_utils
|
|
|
|
if self.bs_node:
|
|
results = adjust_utils.analyze_blendshape_weights(self.bs_node)
|
|
if results:
|
|
# 显示分析结果
|
|
msg = (
|
|
f"总目标数: {results['total_targets']}\n"
|
|
f"活动目标: {results['active_targets']}\n"
|
|
f"未使用目标: {results['unused_targets']}\n"
|
|
f"全权重目标: {results['full_weight_targets']}\n"
|
|
f"部分权重目标: {results['partial_weight_targets']}\n\n"
|
|
"权重分布:\n"
|
|
)
|
|
|
|
for weight, count in sorted(results["weight_distribution"].items()):
|
|
msg += f" {weight:.1f}: {count}\n"
|
|
|
|
QtWidgets.QMessageBox.information(self, "权重分析", msg)
|
|
|
|
def apply_batch_weight(self):
|
|
"""应用批量权重"""
|
|
from scripts.utils import adjust_utils
|
|
|
|
weight = self.batch_spin.value()
|
|
|
|
# 获取选中项
|
|
items = self.weight_list.selectedItems()
|
|
if not items:
|
|
return
|
|
|
|
# 设置权重
|
|
for item in items:
|
|
target = item.text(0)
|
|
adjust_utils.bs_manager.set_weight(self.bs_node, target, weight)
|
|
item.setText(1, str(weight))
|
|
|
|
def quick_set_weight(self, weight):
|
|
"""快速设置权重"""
|
|
self.batch_spin.setValue(weight)
|
|
self.apply_batch_weight()
|
|
|
|
class ExpressionPreviewWidget(QtWidgets.QWidget):
|
|
"""表情预览组件"""
|
|
def __init__(self, parent=None):
|
|
super(ExpressionPreviewWidget, self).__init__(parent)
|
|
self.expr_set = None
|
|
self.setup_ui()
|
|
|
|
def setup_ui(self):
|
|
"""创建界面"""
|
|
layout = QtWidgets.QVBoxLayout(self)
|
|
|
|
# 顶部工具栏
|
|
toolbar = QtWidgets.QHBoxLayout()
|
|
|
|
# 表情集选择
|
|
self.expr_combo = QtWidgets.QComboBox()
|
|
self.expr_combo.currentTextChanged.connect(self.on_expr_changed)
|
|
toolbar.addWidget(self.expr_combo)
|
|
|
|
# 刷新按钮
|
|
refresh_btn = IconButton("refresh.png", "刷新")
|
|
refresh_btn.clicked.connect(self.refresh_expressions)
|
|
toolbar.addWidget(refresh_btn)
|
|
|
|
# 播放控制
|
|
self.play_btn = IconButton("play.png", "播放")
|
|
self.play_btn.setCheckable(True)
|
|
self.play_btn.clicked.connect(self.toggle_play)
|
|
toolbar.addWidget(self.play_btn)
|
|
|
|
# 循环播放
|
|
self.loop_btn = IconButton("loop.png", "循环")
|
|
self.loop_btn.setCheckable(True)
|
|
toolbar.addWidget(self.loop_btn)
|
|
|
|
layout.addLayout(toolbar)
|
|
|
|
# 预览区域
|
|
preview_group = QtWidgets.QGroupBox("预览")
|
|
preview_layout = QtWidgets.QVBoxLayout(preview_group)
|
|
|
|
# 时间控制
|
|
time_layout = QtWidgets.QHBoxLayout()
|
|
|
|
self.time_slider = QtWidgets.QSlider(QtCore.Qt.Horizontal)
|
|
self.time_slider.setRange(0, 100)
|
|
self.time_slider.valueChanged.connect(self.on_time_changed)
|
|
time_layout.addWidget(self.time_slider)
|
|
|
|
self.time_spin = QtWidgets.QSpinBox()
|
|
self.time_spin.setRange(0, 100)
|
|
self.time_spin.valueChanged.connect(self.time_slider.setValue)
|
|
time_layout.addWidget(self.time_spin)
|
|
|
|
preview_layout.addLayout(time_layout)
|
|
|
|
# 权重控制
|
|
weight_layout = QtWidgets.QHBoxLayout()
|
|
|
|
self.weight_slider = SliderWithValue(min_val=0.0, max_val=1.0, default=1.0)
|
|
self.weight_slider.valueChanged.connect(self.on_weight_changed)
|
|
weight_layout.addWidget(self.weight_slider)
|
|
|
|
preview_layout.addLayout(weight_layout)
|
|
|
|
layout.addWidget(preview_group)
|
|
|
|
# 表情列表
|
|
list_group = QtWidgets.QGroupBox("表情列表")
|
|
list_layout = QtWidgets.QVBoxLayout(list_group)
|
|
|
|
# 搜索栏
|
|
self.search_edit = SearchLineEdit()
|
|
self.search_edit.textChanged.connect(self.filter_expressions)
|
|
list_layout.addWidget(self.search_edit)
|
|
|
|
# 表情树
|
|
self.expr_tree = QtWidgets.QTreeWidget()
|
|
self.expr_tree.setHeaderLabels(["表情", "权重"])
|
|
self.expr_tree.itemChanged.connect(self.on_item_changed)
|
|
list_layout.addWidget(self.expr_tree)
|
|
|
|
layout.addWidget(list_group)
|
|
|
|
# 创建定时器
|
|
self.play_timer = QtCore.QTimer()
|
|
self.play_timer.timeout.connect(self.update_time)
|
|
|
|
def set_expression(self, expr_set):
|
|
"""设置表情集"""
|
|
self.expr_set = expr_set
|
|
self.refresh_expressions()
|
|
|
|
def refresh_expressions(self):
|
|
"""刷新表情列表"""
|
|
from scripts.utils import adjust_utils
|
|
|
|
self.expr_tree.clear()
|
|
|
|
if not self.expr_set:
|
|
return
|
|
|
|
# 获取表情数据
|
|
target_data = cmds.getAttr(f"{self.expr_set}.targets")
|
|
for item in target_split(";"):
|
|
name, value = item.split(":")
|
|
tree_item = QtWidgets.QTreeWidgetItem([name, value])
|
|
tree_item.setFlags(tree_item.flags() | QtCore.Qt.ItemIsEditable)
|
|
self.expr_tree.addTopLevelItem(tree_item)
|
|
|
|
def filter_expressions(self, text):
|
|
"""过滤表情"""
|
|
for i in range(self.expr_tree.topLevelItemCount()):
|
|
item = self.expr_tree.topLevelItem(i)
|
|
item.setHidden(text.lower() not in item.text(0).lower())
|
|
|
|
def on_expr_changed(self, expr_set):
|
|
"""表情集变化"""
|
|
self.set_expression(expr_set)
|
|
|
|
def on_item_changed(self, item, column):
|
|
"""项目变化"""
|
|
if column == 1: # 权重列
|
|
try:
|
|
target = item.text(0)
|
|
weight = float(item.text(1))
|
|
self.apply_expression_weight(target, weight)
|
|
except ValueError:
|
|
pass
|
|
|
|
def on_time_changed(self, time):
|
|
"""时间变化"""
|
|
self.time_spin.setValue(time)
|
|
self.update_preview()
|
|
|
|
def on_weight_changed(self, weight):
|
|
"""权重变化"""
|
|
self.update_preview()
|
|
|
|
def toggle_play(self, checked):
|
|
"""切换播放状态"""
|
|
if checked:
|
|
self.play_timer.start(33) # ~30fps
|
|
self.play_btn.setIcon(QtGui.QIcon(os.path.join(ICONS_PATH, "pause.png")))
|
|
else:
|
|
self.play_timer.stop()
|
|
self.play_btn.setIcon(QtGui.QIcon(os.path.join(ICONS_PATH, "play.png")))
|
|
|
|
def update_time(self):
|
|
"""更新时间"""
|
|
current = self.time_slider.value()
|
|
if current >= self.time_slider.maximum():
|
|
if self.loop_btn.isChecked():
|
|
self.time_slider.setValue(0)
|
|
else:
|
|
self.play_btn.setChecked(False)
|
|
else:
|
|
self.time_slider.setValue(current + 1)
|
|
|
|
def update_preview(self):
|
|
"""更新预览"""
|
|
from scripts.utils import adjust_utils
|
|
|
|
if not self.expr_set:
|
|
return
|
|
|
|
# 获取当前权重
|
|
weight = self.weight_slider.value_spin.value()
|
|
|
|
# 应用表情
|
|
adjust_utils.apply_expression_set(self.expr_set, weight)
|
|
|
|
def apply_expression_weight(self, target, weight):
|
|
"""应用表情权重"""
|
|
from scripts.utils import adjust_utils
|
|
|
|
if not self.expr_set:
|
|
return
|
|
|
|
# 更新表情集数据
|
|
target_data = cmds.getAttr(f"{self.expr_set}.targets")
|
|
targets = []
|
|
for item in target_split(";"):
|
|
name, value = item.split(":")
|
|
if name == target:
|
|
targets.append(f"{name}:{weight}")
|
|
else:
|
|
targets.append(item)
|
|
|
|
cmds.setAttr(f"{self.expr_set}.targets", ";".join(targets), type="string")
|
|
|
|
# 更新预览
|
|
self.update_preview()
|
|
|
|
class ExpressionCombinationPreview(QtWidgets.QWidget):
|
|
"""表情组合预览组件"""
|
|
def __init__(self, parent=None):
|
|
super(ExpressionCombinationPreview, self).__init__(parent)
|
|
self.combo = None
|
|
self.setup_ui()
|
|
|
|
def setup_ui(self):
|
|
"""创建界面"""
|
|
layout = QtWidgets.QVBoxLayout(self)
|
|
|
|
# 顶部工具栏
|
|
toolbar = QtWidgets.QHBoxLayout()
|
|
|
|
# 组合选择
|
|
self.combo_combo = QtWidgets.QComboBox()
|
|
self.combo_combo.currentTextChanged.connect(self.on_combo_changed)
|
|
toolbar.addWidget(self.combo_combo)
|
|
|
|
# 刷新按钮
|
|
refresh_btn = IconButton("refresh.png", "刷新")
|
|
refresh_btn.clicked.connect(self.refresh_combinations)
|
|
toolbar.addWidget(refresh_btn)
|
|
|
|
# 创建组合
|
|
create_btn = IconButton("create.png", "创建组合")
|
|
create_btn.clicked.connect(self.create_combination)
|
|
toolbar.addWidget(create_btn)
|
|
|
|
# 删除组合
|
|
delete_btn = IconButton("delete.png", "删除组合")
|
|
delete_btn.clicked.connect(self.delete_combination)
|
|
toolbar.addWidget(delete_btn)
|
|
|
|
layout.addLayout(toolbar)
|
|
|
|
# 预览区域
|
|
preview_group = QtWidgets.QGroupBox("预览")
|
|
preview_layout = QtWidgets.QVBoxLayout(preview_group)
|
|
|
|
# 总权重控制
|
|
weight_layout = QtWidgets.QHBoxLayout()
|
|
weight_layout.addWidget(QtWidgets.QLabel("总权重:"))
|
|
|
|
self.weight_slider = SliderWithValue(min_val=0.0, max_val=1.0, default=1.0)
|
|
self.weight_slider.valueChanged.connect(self.on_weight_changed)
|
|
weight_layout.addWidget(self.weight_slider)
|
|
|
|
preview_layout.addLayout(weight_layout)
|
|
|
|
layout.addWidget(preview_group)
|
|
|
|
# 表情列表
|
|
list_group = QtWidgets.QGroupBox("组合表情")
|
|
list_layout = QtWidgets.QVBoxLayout(list_group)
|
|
|
|
# 搜索栏
|
|
self.search_edit = SearchLineEdit()
|
|
self.search_edit.textChanged.connect(self.filter_expressions)
|
|
list_layout.addWidget(self.search_edit)
|
|
|
|
# 表情树
|
|
self.expr_tree = QtWidgets.QTreeWidget()
|
|
self.expr_tree.setHeaderLabels(["表情", "权重"])
|
|
self.expr_tree.itemChanged.connect(self.on_item_changed)
|
|
list_layout.addWidget(self.expr_tree)
|
|
|
|
# 表情工具栏
|
|
expr_toolbar = QtWidgets.QHBoxLayout()
|
|
|
|
# 添加表情
|
|
add_btn = IconButton("add.png", "添加表情")
|
|
add_btn.clicked.connect(self.add_expression)
|
|
expr_toolbar.addWidget(add_btn)
|
|
|
|
# 移除表情
|
|
remove_btn = IconButton("remove.png", "移除表情")
|
|
remove_btn.clicked.connect(self.remove_expression)
|
|
expr_toolbar.addWidget(remove_btn)
|
|
|
|
# 上移
|
|
up_btn = IconButton("up.png", "上移")
|
|
up_btn.clicked.connect(self.move_expression_up)
|
|
expr_toolbar.addWidget(up_btn)
|
|
|
|
# 下移
|
|
down_btn = IconButton("down.png", "下移")
|
|
down_btn.clicked.connect(self.move_expression_down)
|
|
expr_toolbar.addWidget(down_btn)
|
|
|
|
list_layout.addLayout(expr_toolbar)
|
|
|
|
layout.addWidget(list_group)
|
|
|
|
def set_combination(self, combo):
|
|
"""设置表情组合"""
|
|
self.combo = combo
|
|
self.refresh_combinations()
|
|
|
|
def refresh_combinations(self):
|
|
"""刷新组合列表"""
|
|
from scripts.utils import adjust_utils
|
|
|
|
self.expr_tree.clear()
|
|
|
|
if not self.combo:
|
|
return
|
|
|
|
# 获取组合数据
|
|
expr_data = cmds.getAttr(f"{self.combo}.expressions")
|
|
for item in expr_split(";"):
|
|
expr_set, weight = item.split(":")
|
|
tree_item = QtWidgets.QTreeWidgetItem([expr_set, weight])
|
|
tree_item.setFlags(tree_item.flags() | QtCore.Qt.ItemIsEditable)
|
|
self.expr_tree.addTopLevelItem(tree_item)
|
|
|
|
def filter_expressions(self, text):
|
|
"""过滤表情"""
|
|
for i in range(self.expr_tree.topLevelItemCount()):
|
|
item = self.expr_tree.topLevelItem(i)
|
|
item.setHidden(text.lower() not in item.text(0).lower())
|
|
|
|
def on_combo_changed(self, combo):
|
|
"""组合变化"""
|
|
self.set_combination(combo)
|
|
|
|
def on_item_changed(self, item, column):
|
|
"""项目变化"""
|
|
if column == 1: # 权重列
|
|
try:
|
|
expr_set = item.text(0)
|
|
weight = float(item.text(1))
|
|
self.update_expression_weight(expr_set, weight)
|
|
except ValueError:
|
|
pass
|
|
|
|
def on_weight_changed(self, weight):
|
|
"""总权重变化"""
|
|
from scripts.utils import adjust_utils
|
|
|
|
if self.combo:
|
|
adjust_utils.apply_expression_combination(self.combo, weight)
|
|
|
|
def create_combination(self):
|
|
"""创建组合"""
|
|
name, ok = QtWidgets.QInputDialog.getText(
|
|
self, "创建组合", "请输入组合名称:")
|
|
|
|
if ok and name:
|
|
from scripts.utils import adjust_utils
|
|
combo = adjust_utils.create_expression_combination(name, [])
|
|
if combo:
|
|
self.combo_combo.addItem(combo)
|
|
self.combo_combo.setCurrentText(combo)
|
|
|
|
def delete_combination(self):
|
|
"""删除组合"""
|
|
if self.combo:
|
|
reply = QtWidgets.QMessageBox.question(
|
|
self, "删除组合",
|
|
f"确定要删除组合 {self.combo} 吗?",
|
|
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No
|
|
)
|
|
|
|
if reply == QtWidgets.QMessageBox.Yes:
|
|
cmds.delete(self.combo)
|
|
self.combo_combo.removeItem(
|
|
self.combo_combo.findText(self.combo))
|
|
self.combo = None
|
|
self.refresh_combinations()
|
|
|
|
def add_expression(self):
|
|
"""添加表情"""
|
|
from scripts.utils import adjust_utils
|
|
|
|
if not self.combo:
|
|
return
|
|
|
|
# 获取场景中的表情集
|
|
expr_sets = cmds.ls(type="objectSet", long=True)
|
|
expr_sets = [s for s in expr_sets if "_exprSet" in s]
|
|
|
|
if not expr_sets:
|
|
QtWidgets.QMessageBox.warning(
|
|
self, "添加表情", "场景中没有可用的表情集")
|
|
return
|
|
|
|
# 选择表情集
|
|
expr_set, ok = QtWidgets.QInputDialog.getItem(
|
|
self, "添加表情", "选择表情集:", expr_sets, 0, False)
|
|
|
|
if ok and expr_set:
|
|
# 添加到组合
|
|
expr_data = cmds.getAttr(f"{self.combo}.expressions")
|
|
if expr_data:
|
|
expr_data += f";{expr_set}:1.0"
|
|
else:
|
|
expr_data = f"{expr_set}:1.0"
|
|
cmds.setAttr(f"{self.combo}.expressions", expr_data, type="string")
|
|
|
|
# 刷新列表
|
|
self.refresh_combinations()
|
|
|
|
def remove_expression(self):
|
|
"""移除表情"""
|
|
items = self.expr_tree.selectedItems()
|
|
if not items:
|
|
return
|
|
|
|
# 更新组合数据
|
|
expr_data = cmds.getAttr(f"{self.combo}.expressions")
|
|
exprs = []
|
|
for item in expr_split(";"):
|
|
expr_set, weight = item.split(":")
|
|
if expr_set not in [i.text(0) for i in items]:
|
|
exprs.append(item)
|
|
|
|
cmds.setAttr(f"{self.combo}.expressions",
|
|
";".join(exprs), type="string")
|
|
|
|
# 刷新列表
|
|
self.refresh_combinations()
|
|
|
|
def move_expression_up(self):
|
|
"""上移表情"""
|
|
item = self.expr_tree.currentItem()
|
|
if not item:
|
|
return
|
|
|
|
index = self.expr_tree.indexOfTopLevelItem(item)
|
|
if index <= 0:
|
|
return
|
|
|
|
# 移动项目
|
|
self.expr_tree.takeTopLevelItem(index)
|
|
self.expr_tree.insertTopLevelItem(index - 1, item)
|
|
self.expr_tree.setCurrentItem(item)
|
|
|
|
# 更新组合数据
|
|
self.update_expression_order()
|
|
|
|
def move_expression_down(self):
|
|
"""下移表情"""
|
|
item = self.expr_tree.currentItem()
|
|
if not item:
|
|
return
|
|
|
|
index = self.expr_tree.indexOfTopLevelItem(item)
|
|
if index >= self.expr_tree.topLevelItemCount() - 1:
|
|
return
|
|
|
|
# 移动项目
|
|
self.expr_tree.takeTopLevelItem(index)
|
|
self.expr_tree.insertTopLevelItem(index + 1, item)
|
|
self.expr_tree.setCurrentItem(item)
|
|
|
|
# 更新组合数据
|
|
self.update_expression_order()
|
|
|
|
def update_expression_order(self):
|
|
"""更新表情顺序"""
|
|
exprs = []
|
|
for i in range(self.expr_tree.topLevelItemCount()):
|
|
item = self.expr_tree.topLevelItem(i)
|
|
exprs.append(f"{item.text(0)}:{item.text(1)}")
|
|
|
|
cmds.setAttr(f"{self.combo}.expressions",
|
|
";".join(exprs), type="string")
|
|
|
|
def update_expression_weight(self, expr_set, weight):
|
|
"""更新表情权重"""
|
|
# 更新组合数据
|
|
expr_data = cmds.getAttr(f"{self.combo}.expressions")
|
|
exprs = []
|
|
for item in expr_split(";"):
|
|
name, value = item.split(":")
|
|
if name == expr_set:
|
|
exprs.append(f"{name}:{weight}")
|
|
else:
|
|
exprs.append(item)
|
|
|
|
cmds.setAttr(f"{self.combo}.expressions",
|
|
";".join(exprs), type="string")
|
|
|
|
# 更新预览
|
|
self.on_weight_changed(self.weight_slider.value_spin.value())
|
|
|
|
class WeightCurveEditor(QtWidgets.QWidget):
|
|
"""权重曲线编辑器"""
|
|
def __init__(self, parent=None):
|
|
super(WeightCurveEditor, self).__init__(parent)
|
|
self.curve = None
|
|
self.setup_ui()
|
|
|
|
def setup_ui(self):
|
|
"""创建界面"""
|
|
layout = QtWidgets.QVBoxLayout(self)
|
|
|
|
# 顶部工具栏
|
|
toolbar = QtWidgets.QHBoxLayout()
|
|
|
|
# 曲线选择
|
|
self.curve_combo = QtWidgets.QComboBox()
|
|
self.curve_combo.currentTextChanged.connect(self.on_curve_changed)
|
|
toolbar.addWidget(self.curve_combo)
|
|
|
|
# 创建曲线
|
|
create_btn = IconButton("create.png", "创建曲线")
|
|
create_btn.clicked.connect(self.create_curve)
|
|
toolbar.addWidget(create_btn)
|
|
|
|
# 删除曲线
|
|
delete_btn = IconButton("delete.png", "删除曲线")
|
|
delete_btn.clicked.connect(self.delete_curve)
|
|
toolbar.addWidget(delete_btn)
|
|
|
|
# 重置曲线
|
|
reset_btn = IconButton("reset.png", "重置曲线")
|
|
reset_btn.clicked.connect(self.reset_curve)
|
|
toolbar.addWidget(reset_btn)
|
|
|
|
layout.addLayout(toolbar)
|
|
|
|
# 曲线编辑区域
|
|
curve_group = QtWidgets.QGroupBox("曲线编辑")
|
|
curve_layout = QtWidgets.QVBoxLayout(curve_group)
|
|
|
|
# 添加曲线视图
|
|
curve_view = self.setup_curve_view()
|
|
curve_layout.addWidget(curve_view)
|
|
|
|
# 添加关键帧控制
|
|
key_controls = self.setup_key_controls()
|
|
curve_layout.addWidget(key_controls)
|
|
|
|
layout.addWidget(curve_group)
|
|
|
|
def set_curve(self, curve):
|
|
"""设置曲线"""
|
|
self.curve = curve
|
|
self.refresh_view()
|
|
|
|
def refresh_view(self):
|
|
"""刷新视图"""
|
|
self.curve_scene.clear()
|
|
|
|
if not self.curve:
|
|
return
|
|
|
|
# 绘制网格
|
|
self.draw_grid()
|
|
|
|
# 绘制曲线
|
|
self.draw_curve()
|
|
|
|
# 绘制关键帧
|
|
self.draw_keyframes()
|
|
|
|
def draw_grid(self):
|
|
"""绘制网格"""
|
|
# 获取视图大小
|
|
width = self.curve_view.width()
|
|
height = self.curve_view.height()
|
|
|
|
# 绘制水平线
|
|
for i in range(11):
|
|
y = height * i / 10
|
|
self.curve_scene.addLine(0, y, width, y,
|
|
QtGui.QPen(QtGui.QColor(100, 100, 100)))
|
|
|
|
# 绘制垂直线
|
|
for i in range(11):
|
|
x = width * i / 10
|
|
self.curve_scene.addLine(x, 0, x, height,
|
|
QtGui.QPen(QtGui.QColor(100, 100, 100)))
|
|
|
|
def draw_curve(self):
|
|
"""绘制曲线"""
|
|
if not self.curve:
|
|
return
|
|
|
|
# 获取曲线数据
|
|
times = cmds.keyframe(self.curve, q=True, timeChange=True)
|
|
values = cmds.keyframe(self.curve, q=True, valueChange=True)
|
|
|
|
if not times or not values:
|
|
return
|
|
|
|
# 创建路径
|
|
path = QtGui.QPainterPath()
|
|
path.moveTo(times[0], values[0])
|
|
|
|
for i in range(1, len(times)):
|
|
path.lineTo(times[i], values[i])
|
|
|
|
# 添加到场景
|
|
self.curve_scene.addPath(path,
|
|
QtGui.QPen(QtGui.QColor(0, 255, 0), 2))
|
|
|
|
def draw_keyframes(self):
|
|
"""绘制关键帧"""
|
|
if not self.curve:
|
|
return
|
|
|
|
# 获取关键帧
|
|
times = cmds.keyframe(self.curve, q=True, timeChange=True)
|
|
values = cmds.keyframe(self.curve, q=True, valueChange=True)
|
|
|
|
if not times or not values:
|
|
return
|
|
|
|
# 绘制关键帧点
|
|
for t, v in zip(times, values):
|
|
self.curve_scene.addEllipse(t-3, v-3, 6, 6,
|
|
QtGui.QPen(QtGui.QColor(255, 255, 0)),
|
|
QtGui.QBrush(QtGui.QColor(255, 255, 0)))
|
|
|
|
def create_curve(self):
|
|
"""创建曲线"""
|
|
from scripts.utils import adjust_utils
|
|
|
|
name, ok = QtWidgets.QInputDialog.getText(
|
|
self, "创建曲线", "请输入曲线名称:")
|
|
|
|
if ok and name:
|
|
curve = adjust_utils.create_weight_curve(name)
|
|
if curve:
|
|
self.curve_combo.addItem(curve)
|
|
self.curve_combo.setCurrentText(curve)
|
|
|
|
def delete_curve(self):
|
|
"""删除曲线"""
|
|
if self.curve:
|
|
reply = QtWidgets.QMessageBox.question(
|
|
self, "删除曲线",
|
|
f"确定要删除曲线 {self.curve} 吗?",
|
|
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No
|
|
)
|
|
|
|
if reply == QtWidgets.QMessageBox.Yes:
|
|
cmds.delete(self.curve)
|
|
self.curve_combo.removeItem(
|
|
self.curve_combo.findText(self.curve))
|
|
self.curve = None
|
|
self.refresh_view()
|
|
|
|
def reset_curve(self):
|
|
"""重置曲线"""
|
|
if not self.curve:
|
|
return
|
|
|
|
# 删除所有关键帧
|
|
cmds.cutKey(self.curve)
|
|
|
|
# 添加默认关键帧
|
|
start = self.start_spin.value()
|
|
end = self.end_spin.value()
|
|
|
|
cmds.setKeyframe(self.curve, time=start, value=0)
|
|
cmds.setKeyframe(self.curve, time=end, value=0)
|
|
|
|
self.refresh_view()
|
|
|
|
def on_curve_changed(self, curve):
|
|
"""曲线变化"""
|
|
self.set_curve(curve)
|
|
|
|
def setup_curve_view(self):
|
|
"""设置曲线视图"""
|
|
# 创建视图容器
|
|
view_container = QtWidgets.QWidget()
|
|
view_layout = QtWidgets.QVBoxLayout(view_container)
|
|
view_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
# 添加标尺
|
|
self.h_ruler = TimeRuler(QtCore.Qt.Horizontal)
|
|
self.v_ruler = ValueRuler(QtCore.Qt.Vertical)
|
|
|
|
# 创建视图区域
|
|
view_area = QtWidgets.QWidget()
|
|
grid_layout = QtWidgets.QGridLayout(view_area)
|
|
grid_layout.setSpacing(0)
|
|
grid_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
# 添加标尺和视图
|
|
grid_layout.addWidget(self.v_ruler, 1, 0)
|
|
grid_layout.addWidget(self.curve_view, 1, 1)
|
|
grid_layout.addWidget(self.h_ruler, 0, 1)
|
|
|
|
view_layout.addWidget(view_area)
|
|
|
|
# 添加缩放控制
|
|
zoom_layout = QtWidgets.QHBoxLayout()
|
|
|
|
# 水平缩放
|
|
zoom_layout.addWidget(QtWidgets.QLabel("水平缩放:"))
|
|
self.h_zoom = QtWidgets.QSlider(QtCore.Qt.Horizontal)
|
|
self.h_zoom.setRange(10, 200)
|
|
self.h_zoom.setValue(100)
|
|
self.h_zoom.valueChanged.connect(self.update_view_scale)
|
|
zoom_layout.addWidget(self.h_zoom)
|
|
|
|
# 垂直缩放
|
|
zoom_layout.addWidget(QtWidgets.QLabel("垂直缩放:"))
|
|
self.v_zoom = QtWidgets.QSlider(QtCore.Qt.Horizontal)
|
|
self.v_zoom.setRange(10, 200)
|
|
self.v_zoom.setValue(100)
|
|
self.v_zoom.valueChanged.connect(self.update_view_scale)
|
|
zoom_layout.addWidget(self.v_zoom)
|
|
|
|
view_layout.addLayout(zoom_layout)
|
|
|
|
return view_container
|
|
|
|
def update_view_scale(self):
|
|
"""更新视图缩放"""
|
|
# 获取缩放值
|
|
h_scale = self.h_zoom.value() / 100.0
|
|
v_scale = self.v_zoom.value() / 100.0
|
|
|
|
# 更新变换
|
|
self.curve_view.setTransform(
|
|
QtGui.QTransform().scale(h_scale, v_scale))
|
|
|
|
# 更新标尺
|
|
self.h_ruler.setScale(h_scale)
|
|
self.v_ruler.setScale(v_scale)
|
|
|
|
# 刷新视图
|
|
self.refresh_view()
|
|
|
|
def setup_key_controls(self):
|
|
"""设置关键帧控制"""
|
|
controls = QtWidgets.QWidget()
|
|
layout = QtWidgets.QHBoxLayout(controls)
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
# 关键帧导航
|
|
nav_group = QtWidgets.QGroupBox("关键帧导航")
|
|
nav_layout = QtWidgets.QHBoxLayout(nav_group)
|
|
|
|
prev_key = IconButton("prevKey.png", "上一帧")
|
|
prev_key.clicked.connect(self.goto_prev_key)
|
|
nav_layout.addWidget(prev_key)
|
|
|
|
next_key = IconButton("nextKey.png", "下一帧")
|
|
next_key.clicked.connect(self.goto_next_key)
|
|
nav_layout.addWidget(next_key)
|
|
|
|
layout.addWidget(nav_group)
|
|
|
|
# 关键帧编辑
|
|
edit_group = QtWidgets.QGroupBox("关键帧编辑")
|
|
edit_layout = QtWidgets.QHBoxLayout(edit_group)
|
|
|
|
# 时间输入
|
|
edit_layout.addWidget(QtWidgets.QLabel("时间:"))
|
|
self.time_edit = QtWidgets.QSpinBox()
|
|
self.time_edit.setRange(-9999, 9999)
|
|
self.time_edit.valueChanged.connect(self.on_time_changed)
|
|
edit_layout.addWidget(self.time_edit)
|
|
|
|
# 值输入
|
|
edit_layout.addWidget(QtWidgets.QLabel("值:"))
|
|
self.value_edit = QtWidgets.QDoubleSpinBox()
|
|
self.value_edit.setRange(-9999.0, 9999.0)
|
|
self.value_edit.setDecimals(3)
|
|
self.value_edit.valueChanged.connect(self.on_value_changed)
|
|
edit_layout.addWidget(self.value_edit)
|
|
|
|
layout.addWidget(edit_group)
|
|
|
|
return controls
|
|
|
|
def goto_prev_key(self):
|
|
"""跳转到上一个关键帧"""
|
|
if not self.curve:
|
|
return
|
|
|
|
current = cmds.currentTime(q=True)
|
|
times = cmds.keyframe(self.curve, q=True, timeChange=True)
|
|
|
|
if not times:
|
|
return
|
|
|
|
# 查找上一帧
|
|
prev_time = max([t for t in times if t < current], default=None)
|
|
if prev_time is not None:
|
|
cmds.currentTime(prev_time)
|
|
self.time_edit.setValue(prev_time)
|
|
|
|
def goto_next_key(self):
|
|
"""跳转到下一个关键帧"""
|
|
if not self.curve:
|
|
return
|
|
|
|
current = cmds.currentTime(q=True)
|
|
times = cmds.keyframe(self.curve, q=True, timeChange=True)
|
|
|
|
if not times:
|
|
return
|
|
|
|
# 查找下一帧
|
|
next_time = min([t for t in times if t > current], default=None)
|
|
if next_time is not None:
|
|
cmds.currentTime(next_time)
|
|
self.time_edit.setValue(next_time)
|
|
|
|
def on_time_changed(self, time):
|
|
"""时间变化"""
|
|
if not self.curve:
|
|
return
|
|
|
|
# 更新当前时间
|
|
cmds.currentTime(time)
|
|
|
|
# 更新值显示
|
|
value = cmds.getValue(self.curve, time=time)
|
|
self.value_edit.setValue(value)
|
|
|
|
# 刷新视图
|
|
self.refresh_view()
|
|
|
|
def on_value_changed(self, value):
|
|
"""值变化"""
|
|
if not self.curve:
|
|
return
|
|
|
|
# 获取当前时间
|
|
time = self.time_edit.value()
|
|
|
|
# 设置关键帧
|
|
cmds.setKeyframe(self.curve, time=time, value=value)
|
|
|
|
# 刷新视图
|
|
self.refresh_view()
|
|
|
|
def apply_curve_to_target(self):
|
|
"""应用曲线到目标"""
|
|
if not self.curve:
|
|
return
|
|
|
|
# 选择目标
|
|
sel = cmds.ls(sl=True)
|
|
if not sel:
|
|
QtWidgets.QMessageBox.warning(
|
|
self, "应用曲线", "请先选择目标对象")
|
|
return
|
|
|
|
# 获取目标属性
|
|
attrs = []
|
|
for obj in sel:
|
|
# 获取可设置关键帧的属性
|
|
keyable_attrs = cmds.listAttr(obj, keyable=True) or []
|
|
for attr in keyable_attrs:
|
|
attrs.append(f"{obj}.{attr}")
|
|
|
|
if not attrs:
|
|
QtWidgets.QMessageBox.warning(
|
|
self, "应用曲线", "选中的对象没有可设置关键帧的属性")
|
|
return
|
|
|
|
# 选择属性
|
|
attr, ok = QtWidgets.QInputDialog.getItem(
|
|
self, "应用曲线", "选择目标属性:", attrs, 0, False)
|
|
|
|
if ok and attr:
|
|
# 连接曲线到属性
|
|
try:
|
|
cmds.connectAttr(f"{self.curve}.output", attr)
|
|
QtWidgets.QMessageBox.information(
|
|
self, "应用曲线", f"已将曲线应用到 {attr}")
|
|
except Exception as e:
|
|
QtWidgets.QMessageBox.warning(
|
|
self, "应用曲线", f"应用曲线失败: {str(e)}") |