diff --git a/Install.py b/Install.py index fe0367b..15f575f 100644 --- a/Install.py +++ b/Install.py @@ -6,56 +6,83 @@ import os import sys import webbrowser - -# Maya imports import maya.mel as mel import maya.cmds as cmds import maya.OpenMayaUI as omui -# Qt imports -from PySide2 import QtWidgets, QtGui, QtCore -from shiboken2 import wrapInstance - -# Custom imports -from scripts.config import data -QtCore, QtGui, QtWidgets = data.Qt() - -#===================================== 2. Global Variables ===================================== try: - ROOT_PATH = data.ROOT_PATH -except NameError: - ROOT_PATH = os.path.abspath(os.path.dirname(__file__)).replace("\\", "/") -TOOL_NAME = data.TOOL_NAME -TOOL_VERSION = data.TOOL_VERSION -TOOL_AUTHOR = data.TOOL_AUTHOR -TOOL_LANG = data.TOOL_LANG -TOOL_WSCL_NAME = data.TOOL_WSCL_NAME -TOOL_HELP_URL = data.TOOL_HELP_URL -SCRIPTS_PATH = data.SCRIPTS_PATH -ICONS_PATH = data.ICONS_PATH + from PySide2 import QtCore, QtGui, QtWidgets + from shiboken2 import wrapInstance + print("从PySide2加载Qt和shiboken2") +except ImportError: + try: + from PySide6 import QtCore, QtGui, QtWidgets + from shiboken6 import wrapInstance + print("从PySide6加载Qt和shiboken6") + except ImportError: + try: + from PySide import QtCore, QtGui, QtWidgets + from shiboken import wrapInstance + print("从PySide加载Qt和shiboken") + except ImportError as e: + print(f"Qt加载失败: {str(e)}") + QtCore = QtGui = QtWidgets = None + wrapInstance = None + +from scripts.config.data import ( + TOOL_NAME, TOOL_VERSION, TOOL_AUTHOR, TOOL_LANG, + TOOL_WSCL_NAME, TOOL_HELP_URL, 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, TOOL_MAIN_SCRIPT, + TOOL_STYLE_FILE, TOOL_ICON, TOOL_COMMAND_ICON, + TOOL_MOD_FILENAME +) + +def get_script_path(): + try: + maya_script = mel.eval('getenv("MAYA_SCRIPT_PATH")') + if maya_script: + paths = maya_script.split(os.pathsep) + for path in paths: + install_path = os.path.join(path, "Install.py") + if os.path.exists(install_path): + return os.path.dirname(install_path) + except: + pass + + try: + return os.path.dirname(os.path.abspath(__file__)) + except: + return os.getcwd() + +ROOT_PATH = get_script_path() +if ROOT_PATH not in sys.path: + sys.path.insert(0, ROOT_PATH) -TOOL_MAIN_SCRIPT = data.TOOL_MAIN_SCRIPT -TOOL_MOD_FILENAME = data.TOOL_MOD_FILENAME -TOOL_ICON = data.TOOL_ICON -TOOL_COMMAND_ICON = data.TOOL_COMMAND_ICON #===================================== 3. Utility Functions ===================================== def maya_main_window(): - """Get Maya main window as QWidget""" + """获取Maya主窗口""" main_window_ptr = omui.MQtUtil.mainWindow() - return wrapInstance(int(main_window_ptr), QtWidgets.QWidget) + if main_window_ptr: + return wrapInstance(int(main_window_ptr), QtWidgets.QWidget) + return None def ensure_directory(directory_path): - """Ensure directory exists, create if not""" - if not os.path.exists(directory_path): - os.makedirs(directory_path) - print(f"Created directory: {directory_path}") + """确保目录存在""" + if directory_path and isinstance(directory_path, str): + if not os.path.exists(directory_path): + os.makedirs(directory_path) + print(f"Created directory: {directory_path}") return directory_path def get_maya_modules_dir(): - """Get Maya modules directory path""" + """获取Maya模块目录""" maya_app_dir = cmds.internalVar(userAppDir=True) - return ensure_directory(os.path.join(maya_app_dir, "modules")) + if maya_app_dir and isinstance(maya_app_dir, str): + return ensure_directory(os.path.join(maya_app_dir, "modules")) + return None #===================================== 4. UI Component Classes ===================================== class SetButton(QtWidgets.QPushButton): @@ -71,16 +98,10 @@ class InstallDialog(QtWidgets.QDialog): self.setup_ui() def load_stylesheet(self): - """加载 QSS 样式文件""" - try: - style_file = data.TOOL_STYLE_FILE - if os.path.exists(style_file): - with open(style_file, 'r') as f: - self.setStyleSheet(f.read()) - else: - print(f"Warning: Style file not found: {style_file}") - except Exception as e: - print(f"Error loading stylesheet: {e}") + with open(TOOL_STYLE_FILE, 'r', encoding='utf-8') as f: + style = f.read() + self.setStyleSheet(style) + print(f"已加载样式文件: {TOOL_STYLE_FILE}") def setup_ui(self): """Initialize and setup UI components""" @@ -161,7 +182,7 @@ class InstallDialog(QtWidgets.QDialog): webbrowser.open(TOOL_HELP_URL) QtWidgets.QApplication.restoreOverrideCursor() - def get_script_path(self): + def get_script_path(): maya_script = mel.eval('getenv("MAYA_SCRIPT_NAME")') if maya_script and os.path.exists(maya_script): return os.path.dirname(maya_script) @@ -322,7 +343,7 @@ except ImportError as e: print("sys.path:", sys.path) print("Contents of Scripts folder:", os.listdir(SCRIPTS_PATH)) """ - + def uninstall_tool(self): """Uninstall the tool from Maya""" window_name = f"{TOOL_NAME}Window" diff --git a/__pycache__/Install.cpython-39.pyc b/__pycache__/Install.cpython-39.pyc new file mode 100644 index 0000000..866665a Binary files /dev/null and b/__pycache__/Install.cpython-39.pyc differ diff --git a/scripts/MetaFusion.py b/scripts/MetaFusion.py index 48df25f..f30e5fd 100644 --- a/scripts/MetaFusion.py +++ b/scripts/MetaFusion.py @@ -5,111 +5,49 @@ import os import sys import maya.cmds as cmds import maya.OpenMayaUI as omui -from shiboken2 import wrapInstance import traceback - -from scripts.config import data -QtCore, QtGui, QtWidgets = data.Qt() - -#===================================== 2. Global Variables ===================================== +import os +import sys +import webbrowser +import maya.mel as mel +import maya.cmds as cmds +import maya.OpenMayaUI as omui try: - ROOT_PATH = data.ROOT_PATH -except NameError: - ROOT_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__))).replace("\\", "/") -TOOL_NAME = data.TOOL_NAME -TOOL_VERSION = data.TOOL_VERSION -TOOL_AUTHOR = data.TOOL_AUTHOR -TOOL_LANG = data.TOOL_LANG -TOOL_WSCL_NAME = data.TOOL_WSCL_NAME -TOOL_HELP_URL = data.TOOL_HELP_URL -SCRIPTS_PATH = data.SCRIPTS_PATH -ICONS_PATH = data.ICONS_PATH -TOOL_MAIN_SCRIPT = data.TOOL_MAIN_SCRIPT -TOOL_MOD_FILENAME = data.TOOL_MOD_FILENAME -TOOL_ICON = data.TOOL_ICON -TOOL_COMMAND_ICON = data.TOOL_COMMAND_ICON -DNA_PATH = data.DNA_PATH -DNA_IMG_PATH = data.DNA_IMG_PATH -DNALIB_PATH = data.DNALIB_PATH -BUILDER_PATH = data.BUILDER_PATH -UI_PATH = data.UI_PATH -UTILS_PATH = data.UTILS_PATH + from PySide2 import QtCore, QtGui, QtWidgets + from shiboken2 import wrapInstance + print("从PySide2加载Qt和shiboken2") +except ImportError: + try: + from PySide6 import QtCore, QtGui, QtWidgets + from shiboken6 import wrapInstance + print("从PySide6加载Qt和shiboken6") + except ImportError: + try: + from PySide import QtCore, QtGui, QtWidgets + from shiboken import wrapInstance + print("从PySide加载Qt和shiboken") + except ImportError as e: + print(f"Qt加载失败: {str(e)}") + QtCore = QtGui = QtWidgets = None + wrapInstance = None -main_window = None +from scripts.config.data import ( + TOOL_NAME, TOOL_VERSION, TOOL_AUTHOR, TOOL_LANG, + TOOL_WSCL_NAME, TOOL_HELP_URL, 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, TOOL_MAIN_SCRIPT, + TOOL_STYLE_FILE, TOOL_ICON, TOOL_COMMAND_ICON, + TOOL_MOD_FILENAME +) -class ModelTab(QtWidgets.QWidget): - def __init__(self): - super().__init__() - self.init_ui() - - def init_ui(self): - layout = QtWidgets.QVBoxLayout(self) - - # LOD导航栏 - self.lod_tabs = QtWidgets.QTabWidget() - for i in range(1, 7): - tab = QtWidgets.QWidget() - self.lod_tabs.addTab(tab, f"LOD{i-1}") - layout.addWidget(self.lod_tabs) - - # 模型操作工具栏 - tool_bar = QtWidgets.QHBoxLayout() - self.add_button(tool_bar, "导入模型", "import.png", self.import_model) - self.add_button(tool_bar, "导出模型", "export.png", self.export_model) - layout.addLayout(tool_bar) - - def add_button(self, layout, text, icon, callback): - btn = QtWidgets.QPushButton(QtGui.QIcon(os.path.join(data.ICONS_PATH, icon)), text) - btn.clicked.connect(callback) - layout.addWidget(btn) - - def import_model(self): pass - def export_model(self): pass - -class RigTab(QtWidgets.QWidget): - def __init__(self): - super().__init__() - self.init_ui() - - def init_ui(self): - splitter = QtWidgets.QSplitter(QtCore.Qt.Horizontal) - - # 左侧控制器列表 - left_panel = QtWidgets.QWidget() - left_layout = QtWidgets.QVBoxLayout(left_panel) - self.controller_list = QtWidgets.QListWidget() - left_layout.addWidget(self.controller_list) - - # 右侧视口区域 - right_panel = QtWidgets.QWidget() - right_layout = QtWidgets.QVBoxLayout(right_panel) - self.viewport_label = QtWidgets.QLabel("3D视口区域") - right_layout.addWidget(self.viewport_label) - - splitter.addWidget(left_panel) - splitter.addWidget(right_panel) - - main_layout = QtWidgets.QHBoxLayout(self) - main_layout.addWidget(splitter) - -class AdjustTab(QtWidgets.QWidget): - def __init__(self): - super().__init__() - self.init_ui() - - def init_ui(self): - layout = QtWidgets.QVBoxLayout(self) - layout.addWidget(QtWidgets.QLabel("调整功能开发中...")) - -class DefineTab(QtWidgets.QWidget): - def __init__(self): - super().__init__() - self.init_ui() - - def init_ui(self): - layout = QtWidgets.QVBoxLayout(self) - layout.addWidget(QtWidgets.QLabel("定义功能开发中...")) +# 导入UI模块 +from scripts.ui.menu import MenuManager +from scripts.ui.models import ModelTab +from scripts.ui.rigging import RigTab +from scripts.ui.adjust import AdjustTab +from scripts.ui.define import DefineTab class MainWindow(QtWidgets.QMainWindow): def __init__(self, parent=None): @@ -120,21 +58,19 @@ class MainWindow(QtWidgets.QMainWindow): """初始化核心组件""" self.setup_paths() self.init_ui() - self.setup_connections() def setup_paths(self): """配置系统路径""" - # 添加插件路径 - if data.PLUGIN_PATH not in sys.path: - sys.path.insert(0, data.PLUGIN_PATH) + if PLUGIN_PATH not in sys.path: + sys.path.insert(0, PLUGIN_PATH) - # 添加PyDNA二进制路径到环境变量 - pydna_bin = os.path.join(data.PYDNA_PATH, "bin") + # 添加PyDNA路径 + pydna_bin = os.path.join(PYDNA_PATH, "bin") os.environ["PATH"] = f"{pydna_bin}{os.pathsep}{os.environ['PATH']}" def init_ui(self): """初始化UI框架""" - self.setWindowTitle(f"{data.TOOL_NAME} {data.TOOL_VERSION}") + self.setWindowTitle(f"{TOOL_NAME} {TOOL_VERSION}") self.setMinimumSize(1200, 800) # 主窗口布局 @@ -142,9 +78,8 @@ class MainWindow(QtWidgets.QMainWindow): self.setCentralWidget(main_widget) main_layout = QtWidgets.QVBoxLayout(main_widget) - # 创建核心组件 - self.create_menu_bar() - self.create_toolbar() + # 创建菜单和工具栏 + self.menu_manager = MenuManager(self) self.create_tab_widget() # 加载样式 @@ -152,108 +87,12 @@ class MainWindow(QtWidgets.QMainWindow): def load_styles(self): """加载样式表""" - style_path = os.path.join(data.STYLES_PATH, "style.qss") - if os.path.exists(style_path): - with open(style_path, "r", encoding="utf-8") as f: + if os.path.exists(TOOL_STYLE_FILE): + with open(TOOL_STYLE_FILE, "r", encoding="utf-8") as f: self.setStyleSheet(f.read()) - def create_menu_bar(self): - """创建菜单栏""" - menubar = self.menuBar() - - # 文件菜单 - file_menu = menubar.addMenu("文件") - self.add_menu_action(file_menu, "打开DNA", "open.png", lambda: self.load_dna()) - self.add_menu_action(file_menu, "保存DNA", "save.png", lambda: self.save_dna()) - self.add_menu_action(file_menu, "加载当前项目的DNA", "open.png", self.on_load_project_dna) - self.add_menu_action(file_menu, "修改混合目标名称", "rename.png", self.on_rename_blend_target) - self.add_menu_action(file_menu, "重置混合目标名称", "resetname.png", self.on_reset_blend_target) - self.add_menu_action(file_menu, "导出FBX", "export.png", self.on_export_fbx) - file_menu.addSeparator() - self.add_menu_action(file_menu, "退出", "exit.png", self.close) - - # 编辑菜单 - edit_menu = menubar.addMenu("编辑") - self.add_menu_action(edit_menu, "创建RL4节点", "connect.png", self.create_rl4_node) - self.add_menu_action(edit_menu, "删除RL4节点", "disconnect.png", self.delete_rl4_node) - self.add_menu_action(edit_menu, "镜像左至右", "mirrorL.png", self.mirror_left_to_right) - self.add_menu_action(edit_menu, "镜像右至左", "mirrorR.png", self.mirror_right_to_left) - self.add_menu_action(edit_menu, "姿势由A型转T型", "pose_A_To_T.png", self.pose_A_to_T) - self.add_menu_action(edit_menu, "姿势由T型转A型", "pose_T_To_A.png", self.pose_T_to_A) - self.add_menu_action(edit_menu, "传输LOD贴图", "locator.png", self.transfer_lod_texture) - self.add_menu_action(edit_menu, "设置关节颜色", "color.png", self.set_joint_color) - self.add_menu_action(edit_menu, "取消全部标记", "unmark_all.png", self.unmark_all) - self.add_menu_action(edit_menu, "重建所有目标", "rebuildTargets.png", self.rebuild_targets) - self.add_menu_action(edit_menu, "为所有表情设置关键帧", "bakeAnimation.png", self.bake_all_animations) - self.add_menu_action(edit_menu, "烘焙所有表情的关键帧", "centerCurrentTime.png", self.bake_all_keyframes) - - # 工具菜单 - tool_menu = menubar.addMenu("工具") - self.add_menu_action(tool_menu, "导出蒙皮", "export_skin.png", self.export_skin) - self.add_menu_action(tool_menu, "导入蒙皮", "import_skin.png", self.import_skin) - self.add_menu_action(tool_menu, "拷贝装皮", "copy_skin.png", self.copy_skin) - self.add_menu_action(tool_menu, "RBF变形器", "blendShape.png", self.create_blend_shape) - self.add_menu_action(tool_menu, "快速绑定服装", "clothing_weight.png", self.quick_bind_clothing) - self.add_menu_action(tool_menu, "克隆混合变形", "blendShape.png", self.clone_blend_shape) - self.add_menu_action(tool_menu, "UV传递点序", "repair_vertex_order.png", self.repair_vertex_order) - self.add_menu_action(tool_menu, "面部生成控制器", "controller.png", self.create_face_controller) - self.add_menu_action(tool_menu, "提取52BS", "ARKit52.png", self.extract_52BS) - self.add_menu_action(tool_menu, "关节轴向修复", "joint.png", self.repair_joint_axis) - self.add_menu_action(tool_menu, "生成身体控制器", "create_body_ctrl.png", self.create_body_controller) - self.add_menu_action(tool_menu, "导入面部动画", "import_face_anim.png", self.import_face_animation) - self.add_menu_action(tool_menu, "导入身体动画", "import_body_anim.png", self.import_body_animation) - - # 语言菜单 - lang_menu = menubar.addMenu("语言") - self.add_menu_action(lang_menu, "中文", "chinese.png", self.set_chinese) - self.add_menu_action(lang_menu, "English", "english.png", self.set_english) - - # 帮助菜单 - help_menu = menubar.addMenu("帮助") - self.add_menu_action(help_menu, "帮助文档", "help.png", self.show_help_document) - self.add_menu_action(help_menu, "关于", "warning.png", self.show_about_dialog) - - def add_menu_action(self, menu, text, icon, callback): - """通用菜单项添加方法""" - action = QtWidgets.QAction(text, self) - if icon: - # 优先使用Maya内置图标 - maya_icon = self.get_maya_icon(icon) - if maya_icon: - action.setIcon(maya_icon) - else: - # 使用自定义图标 - icon_path = os.path.join(data.ICONS_PATH, icon) - if os.path.exists(icon_path): - action.setIcon(QtGui.QIcon(icon_path)) - else: - cmds.warning(f"图标文件不存在: {icon_path}") - action.triggered.connect(callback) - menu.addAction(action) - - def get_maya_icon(self, icon_name): - """获取Maya内置图标""" - maya_icons = { - "bakeAnimation.png": "BakeSimulation.png", - "centerCurrentTime.png": "timeCurrentFrame.png", - "export_skin.png": "kinReroot.png", - # 添加更多图标映射... - } - if icon_name in maya_icons: - return QtGui.QIcon(f":/{maya_icons[icon_name]}") - return None - - def create_toolbar(self): - """创建工具栏""" - toolbar = self.addToolBar("主工具栏") - toolbar.setMovable(False) - - # 添加工具栏按钮 - toolbar.addAction(QtGui.QIcon(os.path.join(ICONS_PATH, "save.png")), "保存DNA") - toolbar.addAction(QtGui.QIcon(os.path.join(ICONS_PATH, "open.png")), "加载当前项目DNA") - def create_tab_widget(self): - """创建主标签页""" + """创建标签页""" self.tab_widget = QtWidgets.QTabWidget() self.centralWidget().layout().addWidget(self.tab_widget) @@ -268,305 +107,68 @@ class MainWindow(QtWidgets.QMainWindow): self.tab_widget.addTab(self.rig_tab, "绑定") self.tab_widget.addTab(self.adjust_tab, "调整") self.tab_widget.addTab(self.define_tab, "定义") - - def setup_connections(self): - """建立信号连接""" - # 示例:连接标签页切换信号 - self.tab_widget.currentChanged.connect(self.on_tab_changed) - - def on_tab_changed(self, index): - """标签页切换回调""" - current_tab = self.tab_widget.widget(index) - print(f"切换到标签页: {current_tab.objectName()}") - - def setup_model_tab(self): - """设置模型标签页内容""" - layout = QtWidgets.QVBoxLayout(self.model_tab) - - # 创建 LOD 分栏 - self.lod_tabs = QtWidgets.QTabWidget() - layout.addWidget(self.lod_tabs) - - # 添加 LOD 分页 - for i in range(8): - lod_tab = QtWidgets.QWidget() - lod_layout = QtWidgets.QVBoxLayout(lod_tab) - - # 添加删除按钮 - delete_btn = QtWidgets.QPushButton(QtGui.QIcon(os.path.join(ICONS_PATH, "delete.png")), "删除") - lod_layout.addWidget(delete_btn) - - # 添加模型输入框和加载按钮 - for part in ["头部", "牙齿", "牙龈", "左眼", "右眼", "虹膜", "睫毛", "眼睑", "软骨", "身体"]: - part_layout = QtWidgets.QHBoxLayout() - part_input = QtWidgets.QLineEdit() - load_btn = QtWidgets.QPushButton(QtGui.QIcon(os.path.join(ICONS_PATH, "target.png")), "加载") - part_layout.addWidget(part_input) - part_layout.addWidget(load_btn) - lod_layout.addLayout(part_layout) - - self.lod_tabs.addTab(lod_tab, f"LOD{i}") - - # 添加 LOD 功能按钮 - lod_func_layout = QtWidgets.QHBoxLayout() - lod_func_layout.addWidget(QtWidgets.QPushButton(QtGui.QIcon(os.path.join(ICONS_PATH, "load_meshes.png")), "自定加载模型")) - lod_func_layout.addWidget(QtWidgets.QPushButton(QtGui.QIcon(os.path.join(ICONS_PATH, "standardized_naming.png")), "标准化命名")) - lod_func_layout.addWidget(QtWidgets.QPushButton(QtGui.QIcon(os.path.join(ICONS_PATH, "automatic_groupingg.png")), "自动分组")) - layout.addLayout(lod_func_layout) - - # 添加模型工具 - model_tool_layout = QtWidgets.QHBoxLayout() - model_tool_layout.addWidget(QtWidgets.QComboBox()) # 拓扑结构下拉菜单 - model_tool_layout.addWidget(QtWidgets.QComboBox()) # 选择LOD下拉菜单 - model_tool_layout.addWidget(QtWidgets.QPushButton(QtGui.QIcon(os.path.join(ICONS_PATH, "polySplitVertex.png")), "模型分离")) - model_tool_layout.addWidget(QtWidgets.QPushButton(QtGui.QIcon(os.path.join(ICONS_PATH, "supplement_meshes.png")), "生成面部配件")) - model_tool_layout.addWidget(QtWidgets.QPushButton(QtGui.QIcon(os.path.join(ICONS_PATH, "repair_normals.png")), "修复法线")) - model_tool_layout.addWidget(QtWidgets.QPushButton(QtGui.QIcon(os.path.join(ICONS_PATH, "repair_vertex_order.png")), "修复点序")) - model_tool_layout.addWidget(QtWidgets.QPushButton(QtGui.QIcon(os.path.join(ICONS_PATH, "polyChipOff.png")), "修复接缝")) - layout.addLayout(model_tool_layout) - - def setup_rig_tab(self): - """设置绑定标签页内容""" - layout = QtWidgets.QVBoxLayout(self.rig_tab) - - # 添加 DNA 浏览器 - self.dna_browser = QtWidgets.QListWidget() - self.dna_browser.setIconSize(QtCore.QSize(64, 64)) - layout.addWidget(self.dna_browser) - - # 添加 DNA 图标缩放滑块 - self.dna_scale_slider = QtWidgets.QSlider(QtCore.Qt.Horizontal) - self.dna_scale_slider.setMinimum(50) - self.dna_scale_slider.setMaximum(200) - self.dna_scale_slider.setValue(100) - self.dna_scale_slider.valueChanged.connect(self.on_dna_scale_changed) - layout.addWidget(self.dna_scale_slider) - - # 添加导入/导出按钮 - button_layout = QtWidgets.QHBoxLayout() - import_btn = QtWidgets.QPushButton("导入设置") - export_btn = QtWidgets.QPushButton("导出设置") - import_btn.clicked.connect(self.on_import_settings) - export_btn.clicked.connect(self.on_export_settings) - button_layout.addWidget(import_btn) - button_layout.addWidget(export_btn) - layout.addLayout(button_layout) - - # 初始化 DNA 浏览器 - self.init_dna_browser() - - def init_dna_browser(self): - """初始化 DNA 浏览器""" - # 清空现有内容 - self.dna_browser.clear() - - # 获取 DNA 文件列表 - dna_files = os.listdir(DNA_PATH) - img_files = os.listdir(DNA_IMG_PATH) - - # 添加 DNA 项目 - for dna_file in dna_files: - item = QtWidgets.QListWidgetItem(dna_file) - # 查找对应的图片 - img_name = os.path.splitext(dna_file)[0] + ".png" - if img_name in img_files: - item.setIcon(QtGui.QIcon(os.path.join(data.DNA_IMG_PATH, img_name))) - self.dna_browser.addItem(item) - - def on_dna_scale_changed(self, value): - """处理 DNA 图标缩放""" - size = int(64 * (value / 100)) - self.dna_browser.setIconSize(QtCore.QSize(size, size)) - - def on_import_settings(self): - pass - - def on_export_settings(self): - """导出设置""" - # TODO: 实现导出设置功能 - pass - - def setup_adjust_tab(self): - """设置调整标签页内容""" - layout = QtWidgets.QVBoxLayout(self.adjust_tab) - - # 添加BlendShape列表 - blend_list = QtWidgets.QListWidget() - layout.addWidget(blend_list) - - # 添加滑块组 - slider_layout = QtWidgets.QVBoxLayout() - for i in range(5): - slider = QtWidgets.QSlider(QtCore.Qt.Horizontal) - slider_layout.addWidget(slider) - layout.addLayout(slider_layout) - - # 添加按钮组 - button_layout = QtWidgets.QHBoxLayout() - edit_btn = QtWidgets.QPushButton("编辑BlendShape") - save_btn = QtWidgets.QPushButton("保存设置") - button_layout.addWidget(edit_btn) - button_layout.addWidget(save_btn) - layout.addLayout(button_layout) - - def setup_define_tab(self): - """设置定义标签页内容""" - layout = QtWidgets.QVBoxLayout(self.define_tab) - - # 添加DNA编辑区域 - self.dna_edit = QtWidgets.QTextEdit() - layout.addWidget(self.dna_edit) - - # 添加按钮组 - button_layout = QtWidgets.QHBoxLayout() - self.load_btn = QtWidgets.QPushButton("载入DNA") - self.save_btn = QtWidgets.QPushButton("保存DNA") - self.export_btn = QtWidgets.QPushButton("导出FBX") - - # 连接信号槽 - self.load_btn.clicked.connect(self.on_load_dna) - self.save_btn.clicked.connect(self.on_save_dna) - self.export_btn.clicked.connect(self.on_export_fbx) - - button_layout.addWidget(self.load_btn) - button_layout.addWidget(self.save_btn) - button_layout.addWidget(self.export_btn) - layout.addLayout(button_layout) - - # 创建DNA管理器 - self.dna_manager = DNAManager() - - def on_load_dna(self): pass - def on_save_dna(self): pass - def on_export_fbx(self): pass - def on_open_dna(self): pass - def on_load_project_dna(self): pass - def on_rename_blend_target(self): pass - def on_reset_blend_target(self): pass - def create_rl4_node(self): pass - def delete_rl4_node(self): pass - def mirror_left_to_right(self): pass - def mirror_right_to_left(self): pass - def pose_A_to_T(self): pass - def pose_T_to_A(self): pass - def transfer_lod_texture(self): pass - def set_joint_color(self): pass - def unmark_all(self): pass - def rebuild_targets(self): pass - def bake_all_animations(self): pass - def bake_all_keyframes(self): pass - def safe_shutdown(self): pass - def export_skin(self): pass - def import_skin(self): pass - def copy_skin(self): pass - def create_blend_shape(self): pass - def quick_bind_clothing(self): pass - def clone_blend_shape(self): pass - def repair_vertex_order(self): pass - def create_face_controller(self): pass - def extract_52BS(self): pass - def repair_joint_axis(self): pass - def create_body_controller(self): pass - def import_face_animation(self): pass - def import_body_animation(self): pass - - # 添加语言支持方法 - def set_chinese(self): - """设置中文界面""" - print("切换中文界面(待实现)") - # TODO: 实现国际化切换 - - def set_english(self): - """设置英文界面""" - print("切换英文界面(待实现)") - # TODO: 实现国际化切换 - - # 添加帮助相关方法 - def show_help_document(self): - """显示帮助文档""" - print("打开帮助文档(待实现)") - # TODO: 实现帮助文档打开逻辑 - - def show_about_dialog(self): - """显示关于对话框""" - print("显示关于信息(待实现)") - # TODO: 实现关于对话框 # ===================================== 显示主窗口 ===================================== def get_maya_window(): - """获取 Maya 主窗口""" - import maya.OpenMayaUI as omui - from shiboken2 import wrapInstance - + """获取Maya主窗口""" maya_main_window_ptr = omui.MQtUtil.mainWindow() - return wrapInstance(int(maya_main_window_ptr), QtWidgets.QWidget) + if maya_main_window_ptr is not None: + # 确保指针转换为整数 + ptr = int(maya_main_window_ptr) + return wrapInstance(ptr, QtWidgets.QWidget) + return None def dock_to_maya(): - """将窗口嵌入到 Maya 的 Dock 面板""" + """嵌入Maya Dock面板""" + global main_window try: - # 先清理可能存在的旧控件 if cmds.workspaceControl(TOOL_WSCL_NAME, exists=True): cmds.deleteUI(TOOL_WSCL_NAME) - # 创建新的Dock控件 dock_control = cmds.workspaceControl( TOOL_WSCL_NAME, label=TOOL_NAME, tabToControl=["AttributeEditor", -1], initialWidth=1400, minimumWidth=1000, - minimumHeight=800, - widthProperty="free", - heightProperty="free" + minimumHeight=800 ) - # 获取Dock控件指针 maya_dock_ptr = omui.MQtUtil.findControl(dock_control) if not maya_dock_ptr: raise RuntimeError("无法获取Dock控件指针") - # 获取Maya主窗口并创建实例 maya_main_window = get_maya_window() main_window = MainWindow(parent=maya_main_window) - # 嵌入到Dock + # 确保指针转换为整数 maya_dock_widget = wrapInstance(int(maya_dock_ptr), QtWidgets.QWidget) maya_dock_widget.layout().addWidget(main_window) return main_window except Exception as e: - error_msg = f"Dock嵌入失败: {str(e)}\n{''.join(traceback.format_exc())}" - cmds.warning(error_msg) + cmds.warning(f"Dock嵌入失败: {str(e)}") return None def show(): """显示主窗口""" global main_window - try: - # 清理旧实例 if main_window: main_window.close() cmds.deleteUI(TOOL_WSCL_NAME) except: pass - # 尝试嵌入 Dock main_window = dock_to_maya() - - # 备用方案:独立窗口模式 if not main_window: - cmds.warning("Dock 模式失败,使用独立窗口模式") + cmds.warning("使用独立窗口模式") main_window = MainWindow() main_window.show() return main_window -# ===================================== 主函数 ===================================== - if __name__ == "__main__": - app = QtWidgets.QApplication([]) - window = show() - app.exec_() + show() diff --git a/scripts/__pycache__/__init__.cpython-39.pyc b/scripts/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000..214e193 Binary files /dev/null and b/scripts/__pycache__/__init__.cpython-39.pyc differ diff --git a/scripts/config/__pycache__/__init__.cpython-39.pyc b/scripts/config/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000..748d792 Binary files /dev/null and b/scripts/config/__pycache__/__init__.cpython-39.pyc differ diff --git a/scripts/config/__pycache__/data.cpython-39.pyc b/scripts/config/__pycache__/data.cpython-39.pyc new file mode 100644 index 0000000..75dbc30 Binary files /dev/null and b/scripts/config/__pycache__/data.cpython-39.pyc differ diff --git a/scripts/config/data.py b/scripts/config/data.py index 344eb68..bc70370 100644 --- a/scripts/config/data.py +++ b/scripts/config/data.py @@ -6,72 +6,110 @@ import sys import maya.cmds as cmds # Base Information -TOOL_NAME = "MetaFusion" -TOOL_VERSION = "Beta v1.0.0" -TOOL_AUTHOR = "CGNICO" -TOOL_LANG = 'en_US' -TOOL_WSCL_NAME = f"{TOOL_NAME}WorkSpaceControl" -TOOL_HELP_URL = f"https://gitea.cgnico.com/CGNICO/{TOOL_NAME}/wiki" - +TOOL_NAME = str("MetaFusion") +TOOL_VERSION = str("Beta v1.0.0") +TOOL_AUTHOR = str("CGNICO") +TOOL_LANG = str('en_US') +TOOL_WSCL_NAME = str(f"{TOOL_NAME}WorkSpaceControl") +TOOL_HELP_URL = str(f"https://gitea.cgnico.com/CGNICO/{TOOL_NAME}/wiki") # BASE_PATH -ROOT_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__))).replace("\\", "/") -SCRIPTS_PATH = os.path.join(ROOT_PATH, "scripts").replace("\\", "/") -ICONS_PATH = os.path.join(ROOT_PATH, "resources", "icons").replace("\\", "/") -STYLES_PATH = os.path.join(ROOT_PATH, "resources", "styles").replace("\\", "/") -DNA_FILE_PATH = os.path.join(ROOT_PATH, "resources", "dna").replace("\\", "/") -DNA_IMG_PATH = os.path.join(ROOT_PATH, "resources", "img").replace("\\", "/") +ROOT_PATH = str(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))).replace("\\", "/")) +SCRIPTS_PATH = str(os.path.join(ROOT_PATH, "scripts").replace("\\", "/")) -# PYDNA_PATH & PLUGIN_PATH -SYSTEM_OS = "Windows" if cmds.about(os=True).lower().startswith("win") else "Linux" -MAYA_VERSION = int(cmds.about(version=True).split('.')[0]) -PYTHON_VERSION = sys.version.replace(".", "") -PYTHON_VERSION_DIR_MAPPING = {"3108": "python3108", "311": "python311", "397": "python397"} -PYTHON_VERSION_DIR = PYTHON_VERSION_DIR_MAPPING.get(PYTHON_VERSION, "python3") -PLUGIN_PATH = os.path.join(ROOT_PATH, "plugins", SYSTEM_OS, MAYA_VERSION).replace("\\", "/") -PYDNA_PATH = os.path.join(ROOT_PATH, "plugins", SYSTEM_OS, "pydna", PYTHON_VERSION_DIR).replace("\\", "/") +# 资源路径 +RESOURCES_PATH = str(os.path.join(ROOT_PATH, "resources").replace("\\", "/")) +ICONS_PATH = str(os.path.join(RESOURCES_PATH, "icons").replace("\\", "/")) +STYLES_PATH = str(os.path.join(RESOURCES_PATH, "styles").replace("\\", "/")) +DNA_FILE_PATH = str(os.path.join(RESOURCES_PATH, "dna").replace("\\", "/")) +DNA_IMG_PATH = str(os.path.join(RESOURCES_PATH, "img").replace("\\", "/")) + +# SYSTEM_INFO +SYSTEM_OS = str("Windows" if cmds.about(os=True).lower().startswith("win") else "Linux") +MAYA_VERSION = str(int(cmds.about(version=True).split('.')[0])) + +# PYTHON_VERSION +PYTHON_VERSION = str(sys.version.replace(".", "")) +major_version = int(PYTHON_VERSION[0]) +minor_version = int(PYTHON_VERSION[1:3]) if len(PYTHON_VERSION) > 1 else None +version_tuple = (major_version,) if minor_version is None else (major_version, minor_version) + +# 版本映射表 +PYTHON_VERSION_MAP = { + (3,): "python3", # 所有Python3主版本 + (3, 9): "python397", # 3.9.x → python397 + (3, 10): "python3108", # 3.10.x → python3108 + (3, 11): "python311" # 3.11.x → python311 +} + +# 获取Python版本目录 +PYTHON_VERSION_DIR = str(PYTHON_VERSION_MAP.get(version_tuple, "python3")) + +# PATHS +PLUGIN_PATH = str(os.path.join(ROOT_PATH, "plugins", SYSTEM_OS, MAYA_VERSION).replace("\\", "/")) +PYDNA_PATH = str(os.path.join(ROOT_PATH, "plugins", SYSTEM_OS, "pydna", PYTHON_VERSION_DIR).replace("\\", "/")) # TOOLS_PATH -DNACALIB_PATH = os.path.join(ROOT_PATH, "dnacalib").replace("\\", "/") -BUILDER_PATH = os.path.join(SCRIPTS_PATH, "builder").replace("\\", "/") -DNALIB_PATH = os.path.join(SCRIPTS_PATH, "dnalib").replace("\\", "/") -UI_PATH = os.path.join(SCRIPTS_PATH, "ui").replace("\\", "/") -UTILS_PATH = os.path.join(SCRIPTS_PATH, "utils").replace("\\", "/") +DNACALIB_PATH = str(os.path.join(ROOT_PATH, "dnacalib").replace("\\", "/")) +BUILDER_PATH = str(os.path.join(SCRIPTS_PATH, "builder").replace("\\", "/")) +DNALIB_PATH = str(os.path.join(SCRIPTS_PATH, "dnalib").replace("\\", "/")) +UI_PATH = str(os.path.join(SCRIPTS_PATH, "ui").replace("\\", "/")) +UTILS_PATH = str(os.path.join(SCRIPTS_PATH, "utils").replace("\\", "/")) -#FILES -TOOL_MAIN_SCRIPT = os.path.join(SCRIPTS_PATH, f"{TOOL_NAME}.py").replace("\\", "/") -TOOL_STYLE_FILE = os.path.join(STYLES_PATH, "style.qss").replace("\\", "/") -TOOL_ICON = os.path.join(ICONS_PATH, f"{TOOL_NAME}Logo.png").replace("\\", "/") -TOOL_COMMAND_ICON = os.path.join(ICONS_PATH, "CommandButton.png").replace("\\", "/") -TOOL_MOD_FILENAME = f"{TOOL_NAME}.mod" +# FILES +TOOL_MAIN_SCRIPT = str(os.path.join(SCRIPTS_PATH, f"{TOOL_NAME}.py").replace("\\", "/")) +TOOL_STYLE_FILE = str(os.path.join(UI_PATH, "style.qss").replace("\\", "/")) +TOOL_ICON = str(os.path.join(ICONS_PATH, f"{TOOL_NAME}Logo.png").replace("\\", "/")) +TOOL_COMMAND_ICON = str(os.path.join(ICONS_PATH, "CommandButton.png").replace("\\", "/")) +TOOL_MOD_FILENAME = str(f"{TOOL_NAME}.mod") +if __name__ == "__main__": + validate_paths = { + 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 + } + for i in validate_paths: + if not i in sys.path: + sys.path.append(i) + + print("============================================") + print(f"TOOL_NAME: {TOOL_NAME}") + print(f"TOOL_VERSION: {TOOL_VERSION}") + print(f"TOOL_AUTHOR: {TOOL_AUTHOR}") + print(f"TOOL_LANG: {TOOL_LANG}") + print(f"TOOL_WSCL_NAME: {TOOL_WSCL_NAME}") + print(f"TOOL_HELP_URL: {TOOL_HELP_URL}") -print("TOOL_NAME",TOOL_NAME) -print("TOOL_VERSION",TOOL_VERSION) -print("TOOL_AUTHOR",TOOL_AUTHOR) -print("TOOL_LANG",TOOL_LANG) -print("TOOL_WSCL_NAME",TOOL_WSCL_NAME) -print("TOOL_HELP_URL",TOOL_HELP_URL) + print(f"ROOT_PATH: {ROOT_PATH}") + print(f"SCRIPTS_PATH: {SCRIPTS_PATH}") + print(f"ICONS_PATH: {ICONS_PATH}") + print(f"STYLES_PATH: {STYLES_PATH}") + print(f"DNA_FILE_PATH: {DNA_FILE_PATH}") + print(f"DNA_IMG_PATH: {DNA_IMG_PATH}") -print("ROOT_PATH",ROOT_PATH) -print("SCRIPTS_PATH",SCRIPTS_PATH) -print("ICONS_PATH",ICONS_PATH) -print("STYLES_PATH",STYLES_PATH) -print("DNA_FILE_PATH",DNA_FILE_PATH) -print("DNA_IMG_PATH",DNA_IMG_PATH) + print(f"PLUGIN_PATH: {PLUGIN_PATH}") + print(f"PYDNA_PATH: {PYDNA_PATH}") -print("PLUGIN_PATH",PLUGIN_PATH) -print("PYDNA_PATH",PYDNA_PATH) + print(f"DNACALIB_PATH: {DNACALIB_PATH}") + print(f"BUILDER_PATH: {BUILDER_PATH}") + print(f"DNALIB_PATH: {DNALIB_PATH}") + print(f"UI_PATH: {UI_PATH}") + print(f"UTILS_PATH: {UTILS_PATH}") -print("DNACALIB_PATH",DNACALIB_PATH) -print("BUILDER_PATH",BUILDER_PATH) -print("DNALIB_PATH",DNALIB_PATH) -print("UI_PATH",UI_PATH) -print("UTILS_PATH",UTILS_PATH) - -print("TOOL_MAIN_SCRIPT",TOOL_MAIN_SCRIPT) -print("TOOL_STYLE_FILE",TOOL_STYLE_FILE) -print("TOOL_ICON",TOOL_ICON) -print("TOOL_COMMAND_ICON",TOOL_COMMAND_ICON) -print("TOOL_MOD_FILENAME",TOOL_MOD_FILENAME) + print(f"TOOL_MAIN_SCRIPT: {TOOL_MAIN_SCRIPT}") + print(f"TOOL_STYLE_FILE: {TOOL_STYLE_FILE}") + print(f"TOOL_ICON: {TOOL_ICON}") + print(f"TOOL_COMMAND_ICON: {TOOL_COMMAND_ICON}") + print(f"TOOL_MOD_FILENAME: {TOOL_MOD_FILENAME}") diff --git a/scripts/ui/Qt.py b/scripts/ui/Qt.py deleted file mode 100644 index 0f9d1c9..0000000 --- a/scripts/ui/Qt.py +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -根据Maya版本加载对应的Qt库的对应模块 -""" - -import os -import sys -import maya.cmds as cmds - -def Qt(): - try: - from PySide import QtCore, QtGui, QtWidgets - return QtCore, QtGui, QtWidgets - except ImportError as e: - try: - from PySide2 import QtCore, QtGui, QtWidgets - QtWidgets = QtGui - return QtCore, QtGui, QtWidgets - except ImportError as e: - try: - from PySide6 import QtCore, QtGui, QtWidgets - return QtCore, QtGui, QtWidgets - except ImportError as e: - return None, None, None - -QtCore, QtGui, QtWidgets = Qt() -if QtCore is None: - cmds.warning("QtCore加载失败") -if QtGui is None: - cmds.warning("QtGui加载失败") -if QtWidgets is None: - cmds.warning("QtWidgets加载失败") - - -def get_wrapInstance(): - try: - from shiboken import wrapInstance - print("从shiboken加载wrapInstance") - return wrapInstance - except ImportError as e: - cmds.warning(f"shiboken加载失败: {str(e)}") - try: - from shiboken2 import wrapInstance - print("从shiboken2加载wrapInstance") - return wrapInstance - except ImportError as e: - cmds.warning(f"shiboken2加载失败: {str(e)}") - try: - from shiboken6 import wrapInstance - print("从shiboken6加载wrapInstance") - return wrapInstance - except ImportError as e: - cmds.warning(f"shiboken6加载失败: {str(e)}") - return None - -wrapInstance = get_wrapInstance() -if wrapInstance is None: - cmds.warning("wrapInstance加载失败") diff --git a/scripts/ui/adjust.py b/scripts/ui/adjust.py new file mode 100644 index 0000000..d16e969 --- /dev/null +++ b/scripts/ui/adjust.py @@ -0,0 +1,182 @@ +import os +from scripts.config import data +from scripts.ui.widgets import ( + BaseWidget, BlendShapeList, BlendShapeControls, + BlendShapeTools, IconButton, SliderWithValue +) +QtCore, QtGui, QtWidgets = data.Qt() + +class AdjustTab(BaseWidget): + """调整标签页""" + def __init__(self, parent=None): + super(AdjustTab, self).__init__(parent) + + def setup_ui(self): + layout = QtWidgets.QVBoxLayout(self) + + # 创建分割器 + splitter = QtWidgets.QSplitter(QtCore.Qt.Vertical) + layout.addWidget(splitter) + + # 上部分 - 主要BlendShape列表 + top_widget = QtWidgets.QWidget() + top_layout = QtWidgets.QVBoxLayout(top_widget) + + # RowControl BlendShape列表 + self.main_bs_list = BlendShapeList("RowControl") + top_layout.addWidget(self.main_bs_list) + + # BlendShape控制 + self.bs_controls = BlendShapeControls() + top_layout.addWidget(self.bs_controls) + + splitter.addWidget(top_widget) + + # 下部分 - 相关BlendShape和工具 + bottom_widget = QtWidgets.QWidget() + bottom_layout = QtWidgets.QVBoxLayout(bottom_widget) + + # Related BlendShape列表 + self.related_bs_list = BlendShapeList("Related Blend Shapes") + bottom_layout.addWidget(self.related_bs_list) + + # BlendShape工具 + self.bs_tools = BlendShapeTools() + bottom_layout.addWidget(self.bs_tools) + + # 表情控制工具栏 + expression_tools = self.create_expression_tools() + bottom_layout.addWidget(expression_tools) + + splitter.addWidget(bottom_widget) + + # 设置分割器比例 + splitter.setStretchFactor(0, 2) + splitter.setStretchFactor(1, 1) + + # 连接信号 + self.connect_signals() + + def create_expression_tools(self): + """创建表情控制工具栏""" + group = QtWidgets.QGroupBox("表情控制") + layout = QtWidgets.QVBoxLayout(group) + + # 功能开关 + 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) + + # 数值控制 + value_layout = QtWidgets.QHBoxLayout() + + self.expr_slider = SliderWithValue(min_val=0.0, max_val=1.0, default=0.0) + value_layout.addWidget(self.expr_slider) + + self.expr_all_check = QtWidgets.QCheckBox("全部") + value_layout.addWidget(self.expr_all_check) + + layout.addLayout(value_layout) + + # 表情控制按钮 + expr_layout = QtWidgets.QHBoxLayout() + + expr_btns = [ + ("还原默认表情", "reset.png", self.reset_expression), + ("选择选择表情", "expressions_current.png", self.select_expression), + ("写入当前表情", "expression.png", self.write_expression), + ("控制面板查找", "controller.png", self.find_controller), + ("选择关联关节", "kinJoint.png", self.select_joints), + ("写入镜像表情", "ctrl_hide.png", self.write_mirror_expression) + ] + + for text, icon, callback in expr_btns: + btn = IconButton(icon, text) + btn.clicked.connect(callback) + expr_layout.addWidget(btn) + + layout.addLayout(expr_layout) + + return group + + def connect_signals(self): + """连接信号""" + # BlendShape列表选择变化 + self.main_bs_list.list_widget.itemSelectionChanged.connect( + self.on_main_selection_changed) + self.related_bs_list.list_widget.itemSelectionChanged.connect( + self.on_related_selection_changed) + + # 数值变化 + self.bs_controls.value_slider.valueChanged.connect( + self.on_bs_value_changed) + self.expr_slider.valueChanged.connect( + self.on_expr_value_changed) + + # 回调函数 + def on_main_selection_changed(self): + """主BlendShape列表选择变化""" + from scripts.utils import adjust_utils + items = self.main_bs_list.list_widget.selectedItems() + adjust_utils.on_main_bs_selected([item.text() for item in items]) + + def on_related_selection_changed(self): + """相关BlendShape列表选择变化""" + from scripts.utils import adjust_utils + items = self.related_bs_list.list_widget.selectedItems() + adjust_utils.on_related_bs_selected([item.text() for item in items]) + + def on_bs_value_changed(self, value): + """BlendShape权重变化""" + from scripts.utils import adjust_utils + adjust_utils.set_bs_value(value) + + def on_expr_value_changed(self, value): + """表情权重变化""" + from scripts.utils import adjust_utils + adjust_utils.set_expr_value(value) + + # 表情控制回调 + def reset_expression(self): + """还原默认表情""" + from scripts.utils import adjust_utils + adjust_utils.reset_expression() + + def select_expression(self): + """选择表情""" + from scripts.utils import adjust_utils + adjust_utils.select_expression() + + def write_expression(self): + """写入当前表情""" + from scripts.utils import adjust_utils + adjust_utils.write_expression() + + def find_controller(self): + """查找控制器""" + from scripts.utils import adjust_utils + adjust_utils.find_controller() + + def select_joints(self): + """选择关联关节""" + from scripts.utils import adjust_utils + adjust_utils.select_joints() + + def write_mirror_expression(self): + """写入镜像表情""" + from scripts.utils import adjust_utils + adjust_utils.write_mirror_expression() \ No newline at end of file diff --git a/scripts/ui/define.py b/scripts/ui/define.py new file mode 100644 index 0000000..c1c8f04 --- /dev/null +++ b/scripts/ui/define.py @@ -0,0 +1,171 @@ +import os +from scripts.config import data +from scripts.ui.widgets import ( + BaseWidget, IconButton, SearchLineEdit +) +QtCore, QtGui, QtWidgets = data.Qt() + +class DefineTab(BaseWidget): + """定义标签页""" + def __init__(self, parent=None): + super(DefineTab, self).__init__(parent) + + def setup_ui(self): + layout = QtWidgets.QVBoxLayout(self) + + # 创建滚动区域 + scroll = QtWidgets.QScrollArea() + scroll.setWidgetResizable(True) + scroll.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + layout.addWidget(scroll) + + # 创建内容控件 + content = QtWidgets.QWidget() + content_layout = QtWidgets.QVBoxLayout(content) + scroll.setWidget(content) + + # DNA定义部分 + dna_group = self.create_dna_definition() + content_layout.addWidget(dna_group) + + # 骨骼定义部分 + joint_group = self.create_joint_definition() + content_layout.addWidget(joint_group) + + # BlendShape定义部分 + bs_group = self.create_blendshape_definition() + content_layout.addWidget(bs_group) + + content_layout.addStretch() + + def create_dna_definition(self): + """创建DNA定义组""" + group = QtWidgets.QGroupBox("DNA定义") + layout = QtWidgets.QVBoxLayout(group) + + # DNA文件选择 + file_layout = QtWidgets.QHBoxLayout() + file_layout.addWidget(QtWidgets.QLabel("DNA文件:")) + self.dna_file_input = QtWidgets.QLineEdit() + file_layout.addWidget(self.dna_file_input) + + browse_btn = IconButton("target.png", "浏览DNA文件") + browse_btn.clicked.connect(self.browse_dna_file) + file_layout.addWidget(browse_btn) + + layout.addLayout(file_layout) + + # DNA预览 + preview_group = QtWidgets.QGroupBox("DNA预览") + preview_layout = QtWidgets.QVBoxLayout(preview_group) + self.dna_preview = QtWidgets.QTextEdit() + self.dna_preview.setReadOnly(True) + preview_layout.addWidget(self.dna_preview) + + layout.addWidget(preview_group) + + return group + + def create_joint_definition(self): + """创建骨骼定义组""" + group = QtWidgets.QGroupBox("骨骼定义") + layout = QtWidgets.QVBoxLayout(group) + + # 骨骼列表 + self.joint_list = QtWidgets.QTreeWidget() + self.joint_list.setHeaderLabels(["骨骼名称", "位置", "旋转", "缩放"]) + layout.addWidget(self.joint_list) + + # 骨骼工具栏 + tools_layout = QtWidgets.QHBoxLayout() + + # 添加骨骼 + add_btn = IconButton("joint.png", "添加骨骼") + add_btn.clicked.connect(self.add_joint) + tools_layout.addWidget(add_btn) + + # 删除骨骼 + del_btn = IconButton("delete.png", "删除骨骼") + del_btn.clicked.connect(self.delete_joint) + tools_layout.addWidget(del_btn) + + # 修改骨骼 + mod_btn = IconButton("modify.png", "修改骨骼") + mod_btn.clicked.connect(self.modify_joint) + tools_layout.addWidget(mod_btn) + + tools_layout.addStretch() + layout.addLayout(tools_layout) + + return group + + def create_blendshape_definition(self): + """创建BlendShape定义组""" + group = QtWidgets.QGroupBox("BlendShape定义") + layout = QtWidgets.QVBoxLayout(group) + + # BlendShape列表 + self.bs_list = QtWidgets.QTreeWidget() + self.bs_list.setHeaderLabels(["名称", "目标", "权重"]) + layout.addWidget(self.bs_list) + + # BlendShape工具栏 + tools_layout = QtWidgets.QHBoxLayout() + + # 添加BlendShape + add_btn = IconButton("blendShape.png", "添加BlendShape") + add_btn.clicked.connect(self.add_blendshape) + tools_layout.addWidget(add_btn) + + # 删除BlendShape + del_btn = IconButton("delete.png", "删除BlendShape") + del_btn.clicked.connect(self.delete_blendshape) + tools_layout.addWidget(del_btn) + + # 修改BlendShape + mod_btn = IconButton("modify.png", "修改BlendShape") + mod_btn.clicked.connect(self.modify_blendshape) + tools_layout.addWidget(mod_btn) + + tools_layout.addStretch() + layout.addLayout(tools_layout) + + return group + + # DNA定义回调 + def browse_dna_file(self): + """浏览DNA文件""" + from scripts.utils import define_utils + define_utils.browse_dna_file() + + # 骨骼定义回调 + def add_joint(self): + """添加骨骼""" + from scripts.utils import define_utils + define_utils.add_joint() + + def delete_joint(self): + """删除骨骼""" + from scripts.utils import define_utils + define_utils.delete_joint() + + def modify_joint(self): + """修改骨骼""" + from scripts.utils import define_utils + define_utils.modify_joint() + + # BlendShape定义回调 + def add_blendshape(self): + """添加BlendShape""" + from scripts.utils import define_utils + define_utils.add_blendshape() + + def delete_blendshape(self): + """删除BlendShape""" + from scripts.utils import define_utils + define_utils.delete_blendshape() + + def modify_blendshape(self): + """修改BlendShape""" + from scripts.utils import define_utils + define_utils.modify_blendshape() \ No newline at end of file diff --git a/scripts/ui/menu.py b/scripts/ui/menu.py new file mode 100644 index 0000000..b003813 --- /dev/null +++ b/scripts/ui/menu.py @@ -0,0 +1,108 @@ +import os +from scripts.config import data +from scripts.utils import menu_utils +QtCore, QtGui, QtWidgets = data.Qt() + +class MenuManager: + """菜单管理器""" + def __init__(self, parent): + self.parent = parent + self.menu_bar = parent.menuBar() + self.create_menus() + self.create_toolbar() + + def create_menus(self): + """创建菜单""" + # 文件菜单 + file_menu = self.menu_bar.addMenu("文件") + self.add_menu_item(file_menu, "打开DNA", "open.png", menu_utils.load_dna) + self.add_menu_item(file_menu, "保存DNA", "save.png", menu_utils.save_dna) + self.add_menu_item(file_menu, "加载当前项目的DNA", "open.png", menu_utils.load_project_dna) + file_menu.addSeparator() + self.add_menu_item(file_menu, "修改混合目标名称", "rename.png", menu_utils.rename_blend_target) + self.add_menu_item(file_menu, "重置混合目标名称", "resetname.png", menu_utils.reset_blend_target) + file_menu.addSeparator() + self.add_menu_item(file_menu, "导出FBX", "export.png", menu_utils.export_fbx) + file_menu.addSeparator() + self.add_menu_item(file_menu, "退出", "exit.png", menu_utils.safe_shutdown) + + # 编辑菜单 + edit_menu = self.menu_bar.addMenu("编辑") + self.add_menu_item(edit_menu, "创建RL4节点", "connect.png", menu_utils.create_rl4_node) + self.add_menu_item(edit_menu, "删除RL4节点", "disconnect.png", menu_utils.delete_rl4_node) + edit_menu.addSeparator() + self.add_menu_item(edit_menu, "镜像左至右", "mirrorL.png", menu_utils.mirror_left_to_right) + self.add_menu_item(edit_menu, "镜像右至左", "mirrorR.png", menu_utils.mirror_right_to_left) + edit_menu.addSeparator() + self.add_menu_item(edit_menu, "姿势由A型转T型", "pose_A_To_T.png", menu_utils.pose_a_to_t) + self.add_menu_item(edit_menu, "姿势由T型转A型", "pose_T_To_A.png", menu_utils.pose_t_to_a) + edit_menu.addSeparator() + self.add_menu_item(edit_menu, "传输LOD贴图", "locator.png", menu_utils.transfer_lod_texture) + self.add_menu_item(edit_menu, "设置关节颜色", "color.png", menu_utils.set_joint_color) + edit_menu.addSeparator() + self.add_menu_item(edit_menu, "取消全部标记", "unmark_all.png", menu_utils.unmark_all) + self.add_menu_item(edit_menu, "重建所有目标", "rebuildTargets.png", menu_utils.rebuild_all_targets) + edit_menu.addSeparator() + self.add_menu_item(edit_menu, "为所有表情设置关键帧", "bakeAnimation.png", menu_utils.bake_all_animations) + self.add_menu_item(edit_menu, "烘焙所有表情的关键帧", "centerCurrentTime.png", menu_utils.bake_all_keyframes) + + # 工具菜单 + tools_menu = self.menu_bar.addMenu("工具") + self.add_menu_item(tools_menu, "导出蒙皮", "export_skin.png", menu_utils.export_skin) + self.add_menu_item(tools_menu, "导入蒙皮", "import_skin.png", menu_utils.import_skin) + self.add_menu_item(tools_menu, "拷贝蒙皮", "copy_skin.png", menu_utils.copy_skin) + tools_menu.addSeparator() + self.add_menu_item(tools_menu, "RBF变形器", "blendShape.png", menu_utils.create_rbf_deformer) + self.add_menu_item(tools_menu, "快速绑定服装", "clothing_weight.png", menu_utils.quick_bind_clothing) + self.add_menu_item(tools_menu, "克隆混合变形", "blendShape.png", menu_utils.clone_blendshape) + tools_menu.addSeparator() + self.add_menu_item(tools_menu, "UV传递点序", "repair_vertex_order.png", menu_utils.transfer_uv_order) + self.add_menu_item(tools_menu, "面部生成控制器", "controller.png", menu_utils.create_face_controller) + self.add_menu_item(tools_menu, "提取52BS", "ARKit52.png", menu_utils.extract_52bs) + tools_menu.addSeparator() + self.add_menu_item(tools_menu, "关节轴向修复", "joint.png", menu_utils.fix_joint_orientation) + self.add_menu_item(tools_menu, "生成身体控制器", "create_body_ctrl.png", menu_utils.create_body_controller) + tools_menu.addSeparator() + self.add_menu_item(tools_menu, "导入面部动画", "import_face_anim.png", menu_utils.import_face_animation) + self.add_menu_item(tools_menu, "导入身体动画", "import_body_anim.png", menu_utils.import_body_animation) + + # 语言菜单 + lang_menu = self.menu_bar.addMenu("语言") + self.add_menu_item(lang_menu, "中文", "chinese.png", menu_utils.set_chinese) + self.add_menu_item(lang_menu, "English", "english.png", menu_utils.set_english) + + # 帮助菜单 + help_menu = self.menu_bar.addMenu("帮助") + self.add_menu_item(help_menu, "帮助文档", "help.png", menu_utils.show_help) + self.add_menu_item(help_menu, "关于", "warning.png", menu_utils.show_about) + + def create_toolbar(self): + """创建工具栏""" + toolbar = self.parent.addToolBar("主工具栏") + toolbar.setMovable(False) + + # 添加工具栏按钮 + self.add_toolbar_item(toolbar, "保存DNA", "save.png", menu_utils.save_dna) + self.add_toolbar_item(toolbar, "加载当前项目DNA", "open.png", menu_utils.load_project_dna) + + def add_menu_item(self, menu, text, icon, callback): + """添加菜单项""" + action = QtWidgets.QAction(text, self.parent) + if icon: + icon_path = os.path.join(data.ICONS_PATH, icon) + if os.path.exists(icon_path): + action.setIcon(QtGui.QIcon(icon_path)) + action.triggered.connect(callback) + menu.addAction(action) + return action + + def add_toolbar_item(self, toolbar, text, icon, callback): + """添加工具栏项""" + action = QtWidgets.QAction(text, self.parent) + if icon: + icon_path = os.path.join(data.ICONS_PATH, icon) + if os.path.exists(icon_path): + action.setIcon(QtGui.QIcon(icon_path)) + action.triggered.connect(callback) + toolbar.addAction(action) + return action \ No newline at end of file diff --git a/scripts/ui/models.py b/scripts/ui/models.py new file mode 100644 index 0000000..08d33e3 --- /dev/null +++ b/scripts/ui/models.py @@ -0,0 +1,151 @@ +import os +from scripts.config import data +from scripts.ui.widgets import ( + BaseWidget, LODGroup, IconButton, SearchLineEdit +) +QtCore, QtGui, QtWidgets = data.Qt() + +class ModelTab(BaseWidget): + """模型标签页""" + def __init__(self, parent=None): + super(ModelTab, self).__init__(parent) + + def setup_ui(self): + layout = QtWidgets.QVBoxLayout(self) + + # 创建滚动区域 + scroll = QtWidgets.QScrollArea() + scroll.setWidgetResizable(True) + scroll.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + layout.addWidget(scroll) + + # 创建内容控件 + content = QtWidgets.QWidget() + content_layout = QtWidgets.QVBoxLayout(content) + scroll.setWidget(content) + + # 添加LOD组 + for i in range(8): + lod_group = LODGroup(i) + content_layout.addWidget(lod_group) + + # 添加LOD功能组 + lod_tools = self.create_lod_tools() + content_layout.addWidget(lod_tools) + + # 添加模型工具组 + model_tools = self.create_model_tools() + content_layout.addWidget(model_tools) + + content_layout.addStretch() + + def create_lod_tools(self): + """创建LOD功能组""" + group = QtWidgets.QGroupBox("LOD功能") + layout = QtWidgets.QHBoxLayout(group) + + # 自定加载模型 + load_btn = IconButton("load_meshes.png", "自定加载模型") + load_btn.clicked.connect(self.load_custom_models) + layout.addWidget(load_btn) + + # 标准化命名 + name_btn = IconButton("standardized_naming.png", "标准化命名") + name_btn.clicked.connect(self.standardize_naming) + layout.addWidget(name_btn) + + # 自动分组 + group_btn = IconButton("automatic_grouping.png", "自动分组") + group_btn.clicked.connect(self.auto_group) + layout.addWidget(group_btn) + + layout.addStretch() + return group + + def create_model_tools(self): + """创建模型工具组""" + group = QtWidgets.QGroupBox("模型工具") + layout = QtWidgets.QVBoxLayout(group) + + # 拓扑和LOD选择 + options = QtWidgets.QHBoxLayout() + + # 拓扑结构选择 + options.addWidget(QtWidgets.QLabel("拓扑结构:")) + topo_combo = QtWidgets.QComboBox() + topo_combo.addItem("MetaHuman") + options.addWidget(topo_combo) + + # LOD选择 + options.addWidget(QtWidgets.QLabel("选择LOD:")) + lod_combo = QtWidgets.QComboBox() + lod_combo.addItem("全部") + for i in range(8): + lod_combo.addItem(f"LOD{i}") + options.addWidget(lod_combo) + + options.addStretch() + layout.addLayout(options) + + # 工具按钮 + tools = QtWidgets.QHBoxLayout() + + tool_buttons = [ + ("模型分离", "polySplitVertex.png", self.split_model), + ("生成面部配件", "supplement_meshes.png", self.generate_facial_accessories), + ("修复法线", "repair_normals.png", self.fix_normals), + ("修复点序", "repair_vertex_order.png", self.fix_vertex_order), + ("修复接缝", "polyChipOff.png", self.fix_seams) + ] + + for text, icon, callback in tool_buttons: + btn = IconButton(icon, text) + btn.clicked.connect(callback) + tools.addWidget(btn) + + tools.addStretch() + layout.addLayout(tools) + + return group + + # LOD功能回调 + def load_custom_models(self): + """自定加载模型""" + from scripts.utils import model_utils + model_utils.load_custom_models() + + def standardize_naming(self): + """标准化命名""" + from scripts.utils import model_utils + model_utils.standardize_naming() + + def auto_group(self): + """自动分组""" + from scripts.utils import model_utils + model_utils.auto_group() + + # 模型工具回调 + def split_model(self): + """分离模型""" + from scripts.utils import model_utils + model_utils.split_model() + + def generate_facial_accessories(self): + """生成面部配件""" + from scripts.utils import model_utils + model_utils.generate_facial_accessories() + + def fix_normals(self): + """修复法线""" + from scripts.utils import model_utils + model_utils.fix_normals() + + def fix_vertex_order(self): + """修复点序""" + from scripts.utils import model_utils + model_utils.fix_vertex_order() + + def fix_seams(self): + """修复接缝""" + from scripts.utils import model_utils + model_utils.fix_seams() \ No newline at end of file diff --git a/scripts/ui/rigging.py b/scripts/ui/rigging.py new file mode 100644 index 0000000..f7b05af --- /dev/null +++ b/scripts/ui/rigging.py @@ -0,0 +1,203 @@ +import os +import sys +import maya.cmds as cmds +from scripts.ui.widgets import ( + BaseWidget, DNABrowser, DescriptionWidget, IconButton, SearchLineEdit +) +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)}") + +class RigTab(BaseWidget): + """绑定标签页""" + def __init__(self, parent=None): + super(RigTab, self).__init__(parent) + + def setup_ui(self): + layout = QtWidgets.QVBoxLayout(self) + + # 创建滚动区域 + scroll = QtWidgets.QScrollArea() + scroll.setWidgetResizable(True) + scroll.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + layout.addWidget(scroll) + + # 创建内容控件 + content = QtWidgets.QWidget() + content_layout = QtWidgets.QVBoxLayout(content) + scroll.setWidget(content) + + # DNA部分 + dna_group = self.create_dna_group() + content_layout.addWidget(dna_group) + + # 资产部分 + asset_group = self.create_asset_group() + content_layout.addWidget(asset_group) + + # 描述部分 + self.description_widget = DescriptionWidget() + content_layout.addWidget(self.description_widget) + + # 骨架工具 + skeleton_tools = self.create_skeleton_tools() + content_layout.addWidget(skeleton_tools) + + content_layout.addStretch() + + def create_dna_group(self): + """创建DNA组""" + group = QtWidgets.QGroupBox("DNA") + layout = QtWidgets.QVBoxLayout(group) + + # DNA浏览器 + self.dna_browser = DNABrowser() + self.dna_browser.dnaSelected.connect(self.on_dna_selected) + layout.addWidget(self.dna_browser) + + # 导入导出按钮 + btn_layout = QtWidgets.QHBoxLayout() + + export_btn = IconButton("export.png", "导出设置") + export_btn.clicked.connect(self.export_settings) + btn_layout.addWidget(export_btn) + + import_btn = IconButton("import.png", "导入设置") + import_btn.clicked.connect(self.import_settings) + btn_layout.addWidget(import_btn) + + btn_layout.addStretch() + layout.addLayout(btn_layout) + + return group + + def create_asset_group(self): + """创建资产组""" + group = QtWidgets.QGroupBox("资产") + layout = QtWidgets.QFormLayout(group) + + # 项目路径 + project_layout = QtWidgets.QHBoxLayout() + self.project_edit = QtWidgets.QLineEdit() + project_btn = IconButton("target.png", "选择项目路径") + project_btn.clicked.connect(self.browse_project) + project_layout.addWidget(self.project_edit) + project_layout.addWidget(project_btn) + layout.addRow("项目路径:", project_layout) + + # 预设文件 + preset_layout = QtWidgets.QHBoxLayout() + self.preset_edit = QtWidgets.QLineEdit() + preset_btn = IconButton("target.png", "选择预设文件") + preset_btn.clicked.connect(self.browse_preset) + preset_layout.addWidget(self.preset_edit) + preset_layout.addWidget(preset_btn) + layout.addRow("预设文件:", preset_layout) + + # 数据分层 + layer_layout = QtWidgets.QHBoxLayout() + self.layer_combo = QtWidgets.QComboBox() + self.layer_combo.addItems(["行为"]) + self.override_check = QtWidgets.QCheckBox("覆盖表情") + layer_layout.addWidget(self.layer_combo) + layer_layout.addWidget(self.override_check) + layout.addRow("数据分层:", layer_layout) + + return group + + def create_skeleton_tools(self): + """创建骨架工具""" + group = QtWidgets.QGroupBox("骨架工具") + layout = QtWidgets.QHBoxLayout(group) + + # 清空选项 + clear_btn = IconButton("delete.png", "清空选项") + clear_btn.clicked.connect(self.clear_options) + layout.addWidget(clear_btn) + + # 导入骨架 + import_btn = IconButton("HIKCharacterToolSkeleton.png", "导入骨架") + import_btn.clicked.connect(self.import_skeleton) + layout.addWidget(import_btn) + + # 创建骨架 + create_btn = IconButton("HIKcreateControlRig.png", "创建骨架") + create_btn.clicked.connect(self.create_skeleton) + layout.addWidget(create_btn) + + layout.addStretch() + return group + + # DNA功能回调 + def on_dna_selected(self, dna_path): + """DNA文件选中""" + from scripts.utils import rigging_utils + rigging_utils.load_dna(dna_path) + + def export_settings(self): + """导出设置""" + from scripts.utils import rigging_utils + rigging_utils.export_settings() + + def import_settings(self): + """导入设置""" + from scripts.utils import rigging_utils + rigging_utils.import_settings() + + # 资产功能回调 + def browse_project(self): + """浏览项目路径""" + path = QtWidgets.QFileDialog.getExistingDirectory( + self, "选择项目路径", os.path.expanduser("~")) + if path: + self.project_edit.setText(path) + + def browse_preset(self): + """浏览预设文件""" + file_path, _ = QtWidgets.QFileDialog.getOpenFileName( + self, "选择预设文件", os.path.expanduser("~"), + "预设文件 (*.json *.preset)") + if file_path: + self.preset_edit.setText(file_path) + + # 骨架工具回调 + def clear_options(self): + """清空选项""" + from scripts.utils import rigging_utils + rigging_utils.clear_options() + + def import_skeleton(self): + """导入骨架""" + from scripts.utils import rigging_utils + rigging_utils.import_skeleton() + + def create_skeleton(self): + """创建骨架""" + from scripts.utils import rigging_utils + rigging_utils.create_skeleton() \ No newline at end of file diff --git a/scripts/ui/style.qss b/scripts/ui/style.qss index ff9cc48..3b41b15 100644 --- a/scripts/ui/style.qss +++ b/scripts/ui/style.qss @@ -1,22 +1,19 @@ /* 全局 QPushButton 样式 */ QPushButton { - background-color: #2A2A2A; - color: #CCCCCC; - border-radius: 3px; + background-color: #D0D0D0; + color: #303030; + border-radius: 10px; padding: 5px; font-weight: bold; min-width: 80px; - border: 1px solid #444444; } QPushButton:hover { - background-color: #3A3A3A; - border-color: #555555; + background-color: #E0E0E0; } QPushButton:pressed { - background-color: #1A1A1A; - border-color: #333333; + background-color: #C0C0C0; } QPushButton:disabled { @@ -26,7 +23,7 @@ QPushButton:disabled { } /* 单独的消息按钮样式(可选) */ -.messageButton { +QPushButton.message-button { background-color: #B0B0B0; color: #303030; border-radius: 10px; @@ -35,11 +32,11 @@ QPushButton:disabled { min-width: 80px; } -.messageButton:hover { +QPushButton.message-button:hover { background-color: #C0C0C0; } -.messageButton:pressed { +QPushButton.message-button:pressed { background-color: #A0A0A0; } @@ -277,3 +274,13 @@ QMenu::item:selected { QMenu::item:pressed { background-color: #333333; } + +/* 其他控件样式 */ +QDialog { + background-color: #404040; + color: #E0E0E0; +} + +QLabel { + color: #E0E0E0; +} diff --git a/scripts/ui/widgets.py b/scripts/ui/widgets.py new file mode 100644 index 0000000..a45d809 --- /dev/null +++ b/scripts/ui/widgets.py @@ -0,0 +1,1635 @@ +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)}") \ No newline at end of file diff --git a/scripts/utils/adjust_utils.py b/scripts/utils/adjust_utils.py new file mode 100644 index 0000000..3ea219c --- /dev/null +++ b/scripts/utils/adjust_utils.py @@ -0,0 +1,1187 @@ +import maya.cmds as cmds +from scripts.config import data + +class BlendShapeManager: + """BlendShape管理器""" + def __init__(self): + self.main_bs = None # 主要BlendShape节点 + self.related_bs = [] # 相关BlendShape节点 + self.current_weights = {} # 当前权重缓存 + + def set_main_blendshape(self, bs_node): + """设置主要BlendShape节点""" + if not cmds.objExists(bs_node): + raise ValueError(f"BlendShape节点不存在: {bs_node}") + self.main_bs = bs_node + + def add_related_blendshape(self, bs_node): + """添加相关BlendShape节点""" + if not cmds.objExists(bs_node): + raise ValueError(f"BlendShape节点不存在: {bs_node}") + if bs_node not in self.related_bs: + self.related_bs.append(bs_node) + + def clear_related_blendshapes(self): + """清空相关BlendShape节点""" + self.related_bs = [] + + def get_all_targets(self, bs_node): + """获取BlendShape的所有目标""" + if not cmds.objExists(bs_node): + return [] + + # 获取目标数量 + target_count = cmds.blendShape(bs_node, q=True, weightCount=True) + targets = [] + + # 获取每个目标的名称 + for i in range(target_count): + alias = cmds.aliasAttr(f"{bs_node}.weight[{i}]", q=True) + if alias: + targets.append(alias) + + return targets + + def set_weight(self, bs_node, target, weight): + """设置BlendShape权重""" + if not cmds.objExists(bs_node): + return False + + try: + # 获取权重索引 + weight_index = -1 + target_count = cmds.blendShape(bs_node, q=True, weightCount=True) + + for i in range(target_count): + alias = cmds.aliasAttr(f"{bs_node}.weight[{i}]", q=True) + if alias == target: + weight_index = i + break + + if weight_index >= 0: + cmds.setAttr(f"{bs_node}.weight[{weight_index}]", weight) + return True + + except Exception as e: + cmds.warning(f"设置权重失败: {str(e)}") + + return False + + def get_weight(self, bs_node, target): + """获取BlendShape权重""" + if not cmds.objExists(bs_node): + return 0.0 + + try: + # 获取权重索引 + weight_index = -1 + target_count = cmds.blendShape(bs_node, q=True, weightCount=True) + + for i in range(target_count): + alias = cmds.aliasAttr(f"{bs_node}.weight[{i}]", q=True) + if alias == target: + weight_index = i + break + + if weight_index >= 0: + return cmds.getAttr(f"{bs_node}.weight[{weight_index}]") + + except Exception as e: + cmds.warning(f"获取权重失败: {str(e)}") + + return 0.0 + + def store_weights(self): + """存储当前权重""" + self.current_weights = {} + + # 存储主要BlendShape权重 + if self.main_bs: + self.current_weights[self.main_bs] = {} + for target in self.get_all_targets(self.main_bs): + self.current_weights[self.main_bs][target] = self.get_weight(self.main_bs, target) + + # 存储相关BlendShape权重 + for bs in self.related_bs: + self.current_weights[bs] = {} + for target in self.get_all_targets(bs): + self.current_weights[bs][target] = self.get_weight(bs, target) + + def restore_weights(self): + """还原存储的权重""" + for bs, weights in self.current_weights.items(): + for target, weight in weights.items(): + self.set_weight(bs, target, weight) + +# 全局BlendShape管理器实例 +bs_manager = BlendShapeManager() + +def reset_blendshapes(): + """重置所有BlendShape""" + try: + # 重置主要BlendShape + if bs_manager.main_bs: + for target in bs_manager.get_all_targets(bs_manager.main_bs): + bs_manager.set_weight(bs_manager.main_bs, target, 0) + + # 重置相关BlendShape + for bs in bs_manager.related_bs: + for target in bs_manager.get_all_targets(bs): + bs_manager.set_weight(bs, target, 0) + + print("BlendShape重置完成") + + except Exception as e: + cmds.warning(f"重置BlendShape失败: {str(e)}") + +def filter_blendshapes(): + """过滤BlendShape""" + # TODO: 实现BlendShape过滤功能 + print("BlendShape过滤功能待实现") + +def on_main_bs_selected(targets): + """主要BlendShape选择变化""" + try: + # 存储当前权重 + bs_manager.store_weights() + + # 设置新的权重 + if bs_manager.main_bs: + for target in targets: + bs_manager.set_weight(bs_manager.main_bs, target, 1.0) + + except Exception as e: + cmds.warning(f"设置BlendShape权重失败: {str(e)}") + +def on_related_bs_selected(targets): + """相关BlendShape选择变化""" + try: + # 存储当前权重 + bs_manager.store_weights() + + # 设置新的权重 + for bs in bs_manager.related_bs: + for target in targets: + bs_manager.set_weight(bs, target, 1.0) + + except Exception as e: + cmds.warning(f"设置BlendShape权重失败: {str(e)}") + +def set_bs_value(value): + """设置BlendShape权重值""" + try: + # 设置主要BlendShape权重 + if bs_manager.main_bs: + for target in bs_manager.get_all_targets(bs_manager.main_bs): + bs_manager.set_weight(bs_manager.main_bs, target, value) + + # 设置相关BlendShape权重 + for bs in bs_manager.related_bs: + for target in bs_manager.get_all_targets(bs): + bs_manager.set_weight(bs, target, value) + + except Exception as e: + cmds.warning(f"设置BlendShape权重值失败: {str(e)}") + +def set_expr_value(value): + """设置表情权重值""" + try: + # 获取当前选中的表情控制器 + sel = cmds.ls(sl=True) + if not sel: + return + + # 设置权重 + for ctrl in sel: + if cmds.objExists(f"{ctrl}.weight"): + cmds.setAttr(f"{ctrl}.weight", value) + + except Exception as e: + cmds.warning(f"设置表情权重值失败: {str(e)}") + +# 表情控制功能 +def reset_expression(): + """还原默认表情""" + try: + # 还原存储的权重 + bs_manager.restore_weights() + print("表情已还原") + + except Exception as e: + cmds.warning(f"还原表情失败: {str(e)}") + +def select_expression(): + """选择表情""" + # TODO: 实现表情选择功能 + print("表情选择功能待实现") + +def write_expression(): + """写入当前表情""" + # TODO: 实现表情写入功能 + print("表情写入功能待实现") + +def find_controller(): + """查找控制器""" + # TODO: 实现控制器查找功能 + print("控制器查找功能待实现") + +def select_joints(): + """选择关联关节""" + # TODO: 实现关联关节选择功能 + print("关联关节选择功能待实现") + +def write_mirror_expression(): + """写入镜像表情""" + # TODO: 实现镜像表情写入功能 + print("镜像表情写入功能待实现") + +def reset_expression(): + """恢复表情""" + print("恢复表情功能待实现") + +def blend_filter(): + """混合筛选""" + print("混合筛选功能待实现") + +def flip(): + """翻转""" + print("翻转功能待实现") + +def mirror_target(): + """镜像目标""" + print("镜像目标功能待实现") + +def find_flip_target(): + """查找翻转目标""" + print("查找翻转目标功能待实现") + +def add_blend_target(): + """添加混合目标""" + print("添加混合目标功能待实现") + +def delete_blend_target(): + """删除混合目标""" + print("删除混合目标功能待实现") + +def batch_blend_target(): + """批量混合目标""" + print("批量混合目标功能待实现") + +def rebuild_selected_target(): + """重建选择目标""" + print("重建选择目标功能待实现") + +def blend_selected_target(): + """混合选择目标""" + print("混合选择目标功能待实现") + +# 功能开关 +def toggle_psd(checked): + """PSD开关""" + print(f"PSD功能{'开启' if checked else '关闭'}") + +def toggle_bse(checked): + """BSE开关""" + print(f"BSE功能{'开启' if checked else '关闭'}") + +def toggle_key(checked): + """KEY开关""" + print(f"KEY功能{'开启' if checked else '关闭'}") + +def toggle_mir(checked): + """MIR开关""" + print(f"MIR功能{'开启' if checked else '关闭'}") + +def toggle_ark(checked): + """ARK开关""" + print(f"ARK功能{'开启' if checked else '关闭'}") + +def toggle_ctr(checked): + """CTR开关""" + print(f"CTR功能{'开启' if checked else '关闭'}") + +# 底部功能按钮 +def reset_default_expression(): + """还原默认表情""" + print("还原默认表情功能待实现") + +def find_control_panel(): + """查找控制面板""" + print("查找控制面板功能待实现") + +def select_related_joints(): + """选择关联关节""" + print("选择关联关节功能待实现") + +def write_mirror_expression(): + """写入镜像表情""" + print("写入镜像表情功能待实现") + +def mirror_blendshape(source_bs, target_bs, left_prefix="L_", right_prefix="R_"): + """镜像BlendShape + Args: + source_bs: 源BlendShape节点 + target_bs: 目标BlendShape节点 + left_prefix: 左侧前缀 + right_prefix: 右侧前缀 + """ + try: + # 获取源BlendShape的所有目标 + source_targets = bs_manager.get_all_targets(source_bs) + + for source_target in source_targets: + # 获取镜像目标名称 + if source_target.startswith(left_prefix): + mirror_target = source_target.replace(left_prefix, right_prefix) + elif source_target.startswith(right_prefix): + mirror_target = source_target.replace(right_prefix, left_prefix) + else: + continue + + # 获取源权重 + weight = bs_manager.get_weight(source_bs, source_target) + + # 设置镜像权重 + bs_manager.set_weight(target_bs, mirror_target, weight) + + print(f"BlendShape镜像完成: {source_bs} -> {target_bs}") + + except Exception as e: + cmds.warning(f"BlendShape镜像失败: {str(e)}") + +def find_mirror_target(target_name, left_prefix="L_", right_prefix="R_"): + """查找镜像目标 + Args: + target_name: 目标名称 + left_prefix: 左侧前缀 + right_prefix: 右侧前缀 + Returns: + str: 镜像目标名称 + """ + if target_name.startswith(left_prefix): + return target_name.replace(left_prefix, right_prefix) + elif target_name.startswith(right_prefix): + return target_name.replace(right_prefix, left_prefix) + return "" + +def create_blend_target(base_mesh, target_mesh, target_name): + """创建混合目标 + Args: + base_mesh: 基础模型 + target_mesh: 目标模型 + target_name: 目标名称 + Returns: + str: 创建的BlendShape节点名称 + """ + try: + # 检查模型是否存在 + if not all(cmds.objExists(obj) for obj in [base_mesh, target_mesh]): + raise ValueError("模型不存在") + + # 创建BlendShape节点 + bs_node = cmds.blendShape( + target_mesh, + base_mesh, + name=f"{base_mesh}_blendShape", + frontOfChain=True + )[0] + + # 重命名目标 + target_index = 0 + cmds.aliasAttr(target_name, f"{bs_node}.weight[{target_index}]") + + return bs_node + + except Exception as e: + cmds.warning(f"创建混合目标失败: {str(e)}") + return None + +def batch_create_blend_targets(base_mesh, target_meshes, name_prefix=""): + """批量创建混合目标 + Args: + base_mesh: 基础模型 + target_meshes: 目标模型列表 + name_prefix: 名称前缀 + """ + try: + # 创建BlendShape节点 + bs_node = cmds.blendShape( + base_mesh, + name=f"{base_mesh}_blendShape", + frontOfChain=True + )[0] + + # 添加目标 + for i, target in enumerate(target_meshes): + target_name = f"{name_prefix}target_{i+1}" + cmds.blendShape(bs_node, edit=True, target=(base_mesh, i, target, 1.0)) + cmds.aliasAttr(target_name, f"{bs_node}.weight[{i}]") + + print(f"批量创建混合目标完成: {bs_node}") + return bs_node + + except Exception as e: + cmds.warning(f"批量创建混合目标失败: {str(e)}") + return None + +def rebuild_blend_target(bs_node, target_name): + """重建混合目标 + Args: + bs_node: BlendShape节点 + target_name: 目标名称 + """ + try: + # 获取基础模型 + base_mesh = cmds.blendShape(bs_node, q=True, geometry=True)[0] + + # 获取目标索引 + target_index = -1 + target_count = cmds.blendShape(bs_node, q=True, weightCount=True) + + for i in range(target_count): + alias = cmds.aliasAttr(f"{bs_node}.weight[{i}]", q=True) + if alias == target_name: + target_index = i + break + + if target_index < 0: + raise ValueError(f"找不到目标: {target_name}") + + # 提取目标形状 + target_mesh = cmds.duplicate(base_mesh, name=f"{target_name}_rebuild")[0] + cmds.setAttr(f"{bs_node}.weight[{target_index}]", 1) + cmds.delete(target_mesh, constructionHistory=True) + + # 重建目标 + cmds.blendShape(bs_node, edit=True, remove=True, target=(base_mesh, target_index, target_name, 1.0)) + cmds.blendShape(bs_node, edit=True, target=(base_mesh, target_index, target_mesh, 1.0)) + + # 清理 + cmds.delete(target_mesh) + print(f"重建混合目标完成: {target_name}") + + except Exception as e: + cmds.warning(f"重建混合目标失败: {str(e)}") + +def blend_selected_targets(bs_node, targets, weight=1.0): + """混合选中的目标 + Args: + bs_node: BlendShape节点 + targets: 目标名称列表 + weight: 混合权重 + """ + try: + # 重置所有权重 + all_targets = bs_manager.get_all_targets(bs_node) + for target in all_targets: + bs_manager.set_weight(bs_node, target, 0) + + # 设置选中目标的权重 + for target in targets: + bs_manager.set_weight(bs_node, target, weight) + + print(f"混合目标完成: {targets}") + + except Exception as e: + cmds.warning(f"混合目标失败: {str(e)}") + +def create_corrective_blend(base_mesh, pose_mesh, corrective_mesh, pose_attrs): + """创建修正混合变形 + Args: + base_mesh: 基础模型 + pose_mesh: 姿势模型 + corrective_mesh: 修正模型 + pose_attrs: 姿势属性列表 [(attr, value), ...] + """ + try: + # 创建BlendShape节点 + bs_node = cmds.blendShape( + [pose_mesh, corrective_mesh], + base_mesh, + name=f"{base_mesh}_corrective", + frontOfChain=True + )[0] + + # 设置驱动关系 + for attr, value in pose_attrs: + # 创建条件节点 + cond = cmds.createNode("condition", name=f"{bs_node}_cond") + cmds.setAttr(f"{cond}.operation", 2) # Greater Than + cmds.setAttr(f"{cond}.secondTerm", value) + + # 连接属性 + cmds.connectAttr(attr, f"{cond}.firstTerm") + cmds.connectAttr(f"{cond}.outColorR", f"{bs_node}.weight[1]") + + print(f"创建修正混合变形完成: {bs_node}") + return bs_node + + except Exception as e: + cmds.warning(f"创建修正混合变形失败: {str(e)}") + return None + +def create_expression_set(name, targets, base_mesh=None): + """创建表情集 + Args: + name: 表情集名称 + targets: 目标列表 [(target_name, weight), ...] + base_mesh: 基础模型(可选) + Returns: + str: 创建的表情集节点名称 + """ + try: + # 创建表情集节点 + expr_set = cmds.createNode("objectSet", name=f"{name}_exprSet") + + # 添加自定义属性 + cmds.addAttr(expr_set, ln="weight", at="double", min=0, max=1, dv=0) + cmds.setAttr(f"{expr_set}.weight", e=True, keyable=True) + + # 添加目标信息 + cmds.addAttr(expr_set, ln="targets", dt="string") + target_data = ";".join([f"{t}:{w}" for t, w in targets]) + cmds.setAttr(f"{expr_set}.targets", target_data, type="string") + + # 添加基础模型 + if base_mesh: + cmds.addAttr(expr_set, ln="baseMesh", dt="string") + cmds.setAttr(f"{expr_set}.baseMesh", base_mesh, type="string") + + return expr_set + + except Exception as e: + cmds.warning(f"创建表情集失败: {str(e)}") + return None + +def apply_expression_set(expr_set, weight=None): + """应用表情集 + Args: + expr_set: 表情集节点 + weight: 权重值(可选) + """ + try: + # 获取目标信息 + target_data = cmds.getAttr(f"{expr_set}.targets") + targets = [] + for item in target_data.split(";"): + name, value = item.split(":") + targets.append((name, float(value))) + + # 获取权重 + if weight is None: + weight = cmds.getAttr(f"{expr_set}.weight") + + # 应用权重 + for target_name, target_weight in targets: + bs_node = target_name.split(".")[0] + bs_manager.set_weight(bs_node, target_name, target_weight * weight) + + except Exception as e: + cmds.warning(f"应用表情集失败: {str(e)}") + +def create_mirror_expression(expr_set, left_prefix="L_", right_prefix="R_"): + """创建镜像表情 + Args: + expr_set: 源表情集节点 + left_prefix: 左侧前缀 + right_prefix: 右侧前缀 + Returns: + str: 创建的镜像表情集节点名称 + """ + try: + # 获取源表情信息 + target_data = cmds.getAttr(f"{expr_set}.targets") + source_targets = [] + for item in target_data.split(";"): + name, value = item.split(":") + source_targets.append((name, float(value))) + + # 创建镜像目标 + mirror_targets = [] + for target_name, weight in source_targets: + mirror_name = find_mirror_target(target_name, left_prefix, right_prefix) + if mirror_name: + mirror_targets.append((mirror_name, weight)) + + # 创建镜像表情集 + name = cmds.getAttr(f"{expr_set}.name") + mirror_name = f"{name}_mirror" + if cmds.objExists(f"{expr_set}.baseMesh"): + base_mesh = cmds.getAttr(f"{expr_set}.baseMesh") + else: + base_mesh = None + + return create_expression_set(mirror_name, mirror_targets, base_mesh) + + except Exception as e: + cmds.warning(f"创建镜像表情失败: {str(e)}") + return None + +def optimize_weights(bs_node, threshold=0.001): + """优化权重 + Args: + bs_node: BlendShape节点 + threshold: 权重阈值 + """ + try: + # 获取所有目标 + targets = bs_manager.get_all_targets(bs_node) + + # 检查每个目标的权重 + for target in targets: + weight = bs_manager.get_weight(bs_node, target) + + # 移除小权重 + if abs(weight) < threshold: + bs_manager.set_weight(bs_node, target, 0) + + # 规范化权重 + elif abs(weight - 1.0) < threshold: + bs_manager.set_weight(bs_node, target, 1.0) + + print(f"权重优化完成: {bs_node}") + + except Exception as e: + cmds.warning(f"权重优化失败: {str(e)}") + +def create_weight_driver(bs_node, target, driver_attr): + """创建权重驱动器 + Args: + bs_node: BlendShape节点 + target: 目标名称 + driver_attr: 驱动属性 + """ + try: + # 创建条件节点 + cond = cmds.createNode("condition", name=f"{target}_cond") + cmds.setAttr(f"{cond}.operation", 2) # Greater Than + cmds.setAttr(f"{cond}.secondTerm", 0) + + # 连接驱动属性 + cmds.connectAttr(driver_attr, f"{cond}.firstTerm") + + # 获取目标索引 + target_index = -1 + target_count = cmds.blendShape(bs_node, q=True, weightCount=True) + for i in range(target_count): + alias = cmds.aliasAttr(f"{bs_node}.weight[{i}]", q=True) + if alias == target: + target_index = i + break + + if target_index >= 0: + # 连接权重 + cmds.connectAttr(f"{cond}.outColorR", f"{bs_node}.weight[{target_index}]") + print(f"创建权重驱动器完成: {target}") + return cond + + except Exception as e: + cmds.warning(f"创建权重驱动器失败: {str(e)}") + return None + +def create_expression_group(name, expressions): + """创建表情组 + Args: + name: 组名称 + expressions: 表情集列表 [(expr_name, weight), ...] + Returns: + str: 创建的表情组节点名称 + """ + try: + # 创建组节点 + group = cmds.createNode("objectSet", name=f"{name}_exprGroup") + + # 添加表情集信息 + cmds.addAttr(group, ln="expressions", dt="string") + expr_data = ";".join([f"{e}:{w}" for e, w in expressions]) + cmds.setAttr(f"{group}.expressions", expr_data, type="string") + + return group + + except Exception as e: + cmds.warning(f"创建表情组失败: {str(e)}") + return None + +def create_expression_controller(name, attributes): + """创建表情控制器 + Args: + name: 控制器名称 + attributes: 属性列表 [(attr_name, min_val, max_val, default), ...] + Returns: + str: 创建的控制器名称 + """ + try: + # 创建控制器 + ctrl = cmds.circle(name=f"{name}_ctrl", normal=(0, 1, 0))[0] + + # 添加属性 + for attr_name, min_val, max_val, default in attributes: + cmds.addAttr(ctrl, ln=attr_name, at="double", + min=min_val, max=max_val, dv=default) + cmds.setAttr(f"{ctrl}.{attr_name}", e=True, keyable=True) + + return ctrl + + except Exception as e: + cmds.warning(f"创建表情控制器失败: {str(e)}") + return None + +def create_expression_driver(source_attr, target_attrs, remap=False): + """创建表情驱动器 + Args: + source_attr: 源属性 + target_attrs: 目标属性列表 [(attr, value), ...] + remap: 是否重映射值 + """ + try: + for target_attr, target_value in target_attrs: + if remap: + # 创建重映射节点 + remap_node = cmds.createNode("remapValue") + cmds.connectAttr(source_attr, f"{remap_node}.inputValue") + cmds.connectAttr(f"{remap_node}.outValue", target_attr) + + # 设置映射值 + cmds.setAttr(f"{remap_node}.value[0].value_Position", 0) + cmds.setAttr(f"{remap_node}.value[0].value_FloatValue", 0) + cmds.setAttr(f"{remap_node}.value[1].value_Position", 1) + cmds.setAttr(f"{remap_node}.value[1].value_FloatValue", target_value) + else: + # 直接连接 + cmds.connectAttr(source_attr, target_attr) + + print(f"创建表情驱动器完成: {source_attr}") + + except Exception as e: + cmds.warning(f"创建表情驱动器失败: {str(e)}") + +def create_expression_pose(name, targets, mirror=False): + """创建表情姿势 + Args: + name: 姿势名称 + targets: 目标列表 [(node, attr, value), ...] + mirror: 是否创建镜像姿势 + Returns: + str: 创建的姿势节点名称 + """ + try: + # 创建姿势节点 + pose = cmds.createNode("objectSet", name=f"{name}_pose") + + # 添加目标信息 + cmds.addAttr(pose, ln="targets", dt="string") + target_data = ";".join([f"{n}.{a}:{v}" for n, a, v in targets]) + cmds.setAttr(f"{pose}.targets", target_data, type="string") + + if mirror: + # 创建镜像姿势 + mirror_targets = [] + for node, attr, value in targets: + if "_L_" in node: + mirror_node = node.replace("_L_", "_R_") + elif "_R_" in node: + mirror_node = node.replace("_R_", "_L_") + else: + continue + mirror_targets.append((mirror_node, attr, value)) + + if mirror_targets: + create_expression_pose(f"{name}_mirror", mirror_targets) + + return pose + + except Exception as e: + cmds.warning(f"创建表情姿势失败: {str(e)}") + return None + +def apply_expression_pose(pose, weight=1.0): + """应用表情姿势 + Args: + pose: 姿势节点 + weight: 应用权重 + """ + try: + # 获取目标信息 + target_data = cmds.getAttr(f"{pose}.targets") + for item in target_data.split(";"): + attr_path, value = item.split(":") + node, attr = attr_path.split(".") + + if cmds.objExists(attr_path): + current = cmds.getAttr(attr_path) + target = float(value) + blend = current + (target - current) * weight + cmds.setAttr(attr_path, blend) + + except Exception as e: + cmds.warning(f"应用表情姿势失败: {str(e)}") + +def create_expression_sequence(name, poses, times): + """创建表情序列 + Args: + name: 序列名称 + poses: 姿势列表 + times: 时间列表 + Returns: + str: 创建的序列节点名称 + """ + try: + # 创建序列节点 + sequence = cmds.createNode("objectSet", name=f"{name}_sequence") + + # 添加序列信息 + cmds.addAttr(sequence, ln="poses", dt="string") + pose_data = ";".join([f"{p}:{t}" for p, t in zip(poses, times)]) + cmds.setAttr(f"{sequence}.poses", pose_data, type="string") + + return sequence + + except Exception as e: + cmds.warning(f"创建表情序列失败: {str(e)}") + return None + +def play_expression_sequence(sequence): + """播放表情序列 + Args: + sequence: 序列节点 + """ + try: + # 获取序列信息 + pose_data = cmds.getAttr(f"{sequence}.poses") + for item in pose_data.split(";"): + pose, time = item.split(":") + # 设置时间 + cmds.currentTime(float(time)) + # 应用姿势 + apply_expression_pose(pose) + + except Exception as e: + cmds.warning(f"播放表情序列失败: {str(e)}") + +def create_weight_editor(bs_node): + """创建权重编辑器 + Args: + bs_node: BlendShape节点 + Returns: + str: 创建的编辑器节点名称 + """ + try: + # 创建编辑器节点 + editor = cmds.createNode("objectSet", name=f"{bs_node}_weightEditor") + + # 添加编辑器信息 + cmds.addAttr(editor, ln="blendShape", dt="string") + cmds.setAttr(f"{editor}.blendShape", bs_node, type="string") + + # 获取所有目标 + targets = bs_manager.get_all_targets(bs_node) + + # 添加目标权重属性 + for target in targets: + cmds.addAttr(editor, ln=target, at="double", min=0, max=1, dv=0) + cmds.setAttr(f"{editor}.{target}", e=True, keyable=True) + + # 连接权重 + weight = bs_manager.get_weight(bs_node, target) + cmds.setAttr(f"{editor}.{target}", weight) + + return editor + + except Exception as e: + cmds.warning(f"创建权重编辑器失败: {str(e)}") + return None + +def create_expression_preview(expr_set): + """创建表情预览 + Args: + expr_set: 表情集节点 + Returns: + str: 创建的预览节点名称 + """ + try: + # 创建预览节点 + preview = cmds.createNode("objectSet", name=f"{expr_set}_preview") + + # 添加预览信息 + cmds.addAttr(preview, ln="expression", dt="string") + cmds.setAttr(f"{preview}.expression", expr_set, type="string") + + # 添加预览控制 + cmds.addAttr(preview, ln="weight", at="double", min=0, max=1, dv=0) + cmds.setAttr(f"{preview}.weight", e=True, keyable=True) + + # 添加时间控制 + cmds.addAttr(preview, ln="time", at="time") + cmds.setAttr(f"{preview}.time", e=True, keyable=True) + + # 添加播放控制 + cmds.addAttr(preview, ln="play", at="bool") + cmds.setAttr(f"{preview}.play", e=True, keyable=True) + + return preview + + except Exception as e: + cmds.warning(f"创建表情预览失败: {str(e)}") + return None + +def update_weight_editor(editor): + """更新权重编辑器 + Args: + editor: 编辑器节点 + """ + try: + # 获取BlendShape节点 + bs_node = cmds.getAttr(f"{editor}.blendShape") + + # 获取所有目标 + targets = bs_manager.get_all_targets(bs_node) + + # 更新权重 + for target in targets: + if cmds.objExists(f"{editor}.{target}"): + weight = bs_manager.get_weight(bs_node, target) + cmds.setAttr(f"{editor}.{target}", weight) + + except Exception as e: + cmds.warning(f"更新权重编辑器失败: {str(e)}") + +def update_expression_preview(preview): + """更新表情预览 + Args: + preview: 预览节点 + """ + try: + # 获取表情集 + expr_set = cmds.getAttr(f"{preview}.expression") + + # 获取预览权重 + weight = cmds.getAttr(f"{preview}.weight") + + # 应用表情 + apply_expression_set(expr_set, weight) + + # 检查播放状态 + if cmds.getAttr(f"{preview}.play"): + # 更新时间 + current_time = cmds.getAttr(f"{preview}.time") + cmds.setAttr(f"{preview}.time", current_time + 1) + + # 循环播放 + if current_time >= cmds.playbackOptions(q=True, maxTime=True): + cmds.setAttr(f"{preview}.time", cmds.playbackOptions(q=True, minTime=True)) + + except Exception as e: + cmds.warning(f"更新表情预览失败: {str(e)}") + +def create_weight_curve(bs_node, target): + """创建权重曲线 + Args: + bs_node: BlendShape节点 + target: 目标名称 + Returns: + str: 创建的动画曲线节点名称 + """ + try: + # 创建动画曲线 + curve = cmds.createNode("animCurve", name=f"{target}_weightCurve") + + # 设置曲线类型 + cmds.setAttr(f"{curve}.preInfinity", 1) # Cycle + cmds.setAttr(f"{curve}.postInfinity", 1) # Cycle + + # 添加关键帧 + cmds.setKeyframe(curve, time=0, value=0) + cmds.setKeyframe(curve, time=12, value=1) + cmds.setKeyframe(curve, time=24, value=0) + + # 设置切线类型 + cmds.keyTangent(curve, edit=True, time=(0,24), outTangentType="linear") + cmds.keyTangent(curve, edit=True, time=(0,24), inTangentType="linear") + + return curve + + except Exception as e: + cmds.warning(f"创建权重曲线失败: {str(e)}") + return None + +def apply_weight_curve(curve, bs_node, target): + """应用权重曲线 + Args: + curve: 动画曲线节点 + bs_node: BlendShape节点 + target: 目标名称 + """ + try: + # 获取当前时间 + current_time = cmds.currentTime(q=True) + + # 获取曲线值 + weight = cmds.getValue(curve, time=current_time) + + # 设置权重 + bs_manager.set_weight(bs_node, target, weight) + + except Exception as e: + cmds.warning(f"应用权重曲线失败: {str(e)}") + +def create_expression_combination(name, expressions, weights=None): + """创建表情组合 + Args: + name: 组合名称 + expressions: 表情集列表 + weights: 权重列表(可选) + Returns: + str: 创建的组合节点名称 + """ + try: + # 创建组合节点 + combo = cmds.createNode("objectSet", name=f"{name}_exprCombo") + + # 添加组合信息 + cmds.addAttr(combo, ln="expressions", dt="string") + if weights: + expr_data = ";".join([f"{e}:{w}" for e, w in zip(expressions, weights)]) + else: + expr_data = ";".join([f"{e}:1.0" for e in expressions]) + cmds.setAttr(f"{combo}.expressions", expr_data, type="string") + + # 添加总权重控制 + cmds.addAttr(combo, ln="weight", at="double", min=0, max=1, dv=1) + cmds.setAttr(f"{combo}.weight", e=True, keyable=True) + + return combo + + except Exception as e: + cmds.warning(f"创建表情组合失败: {str(e)}") + return None + +def apply_expression_combination(combo, weight=None): + """应用表情组合 + Args: + combo: 组合节点 + weight: 总权重(可选) + """ + try: + # 获取组合信息 + expr_data = cmds.getAttr(f"{combo}.expressions") + + # 获取总权重 + if weight is None: + weight = cmds.getAttr(f"{combo}.weight") + + # 应用每个表情 + for item in expr_data.split(";"): + expr_set, expr_weight = item.split(":") + apply_expression_set(expr_set, float(expr_weight) * weight) + + except Exception as e: + cmds.warning(f"应用表情组合失败: {str(e)}") + +def optimize_blendshape_weights(bs_node, options=None): + """优化BlendShape权重 + Args: + bs_node: BlendShape节点 + options: 优化选项字典 + """ + try: + if options is None: + options = { + "threshold": 0.001, # 权重阈值 + "normalize": True, # 是否规范化 + "remove_unused": True, # 是否移除未使用的目标 + "clean_history": True # 是否清理历史 + } + + # 获取所有目标 + targets = bs_manager.get_all_targets(bs_node) + + # 移除未使用的目标 + if options["remove_unused"]: + for target in targets: + weight = bs_manager.get_weight(bs_node, target) + if abs(weight) < options["threshold"]: + # 获取目标索引 + target_index = -1 + target_count = cmds.blendShape(bs_node, q=True, weightCount=True) + for i in range(target_count): + alias = cmds.aliasAttr(f"{bs_node}.weight[{i}]", q=True) + if alias == target: + target_index = i + break + + if target_index >= 0: + # 移除目标 + base_mesh = cmds.blendShape(bs_node, q=True, geometry=True)[0] + cmds.blendShape(bs_node, edit=True, remove=True, + target=(base_mesh, target_index, target, 1.0)) + + # 规范化权重 + if options["normalize"]: + for target in targets: + weight = bs_manager.get_weight(bs_node, target) + if abs(weight) > options["threshold"]: + if abs(weight - 1.0) < options["threshold"]: + bs_manager.set_weight(bs_node, target, 1.0) + elif abs(weight) < options["threshold"]: + bs_manager.set_weight(bs_node, target, 0.0) + else: + normalized = round(weight, 3) + bs_manager.set_weight(bs_node, target, normalized) + + # 清理历史 + if options["clean_history"]: + base_mesh = cmds.blendShape(bs_node, q=True, geometry=True)[0] + cmds.delete(base_mesh, constructionHistory=True) + + print(f"权重优化完成: {bs_node}") + + except Exception as e: + cmds.warning(f"权重优化失败: {str(e)}") + +def analyze_blendshape_weights(bs_node): + """分析BlendShape权重 + Args: + bs_node: BlendShape节点 + Returns: + dict: 分析结果 + """ + try: + results = { + "total_targets": 0, + "active_targets": 0, + "unused_targets": 0, + "full_weight_targets": 0, + "partial_weight_targets": 0, + "weight_distribution": {} + } + + # 获取所有目标 + targets = bs_manager.get_all_targets(bs_node) + results["total_targets"] = len(targets) + + # 分析每个目标 + for target in targets: + weight = bs_manager.get_weight(bs_node, target) + + # 统计权重分布 + weight_range = round(weight * 10) / 10 # 取一位小数 + if weight_range in results["weight_distribution"]: + results["weight_distribution"][weight_range] += 1 + else: + results["weight_distribution"][weight_range] = 1 + + # 统计目标类型 + if abs(weight) < 0.001: + results["unused_targets"] += 1 + elif abs(weight - 1.0) < 0.001: + results["full_weight_targets"] += 1 + results["active_targets"] += 1 + elif abs(weight) > 0.001: + results["partial_weight_targets"] += 1 + results["active_targets"] += 1 + + return results + + except Exception as e: + cmds.warning(f"分析权重失败: {str(e)}") + return None \ No newline at end of file diff --git a/scripts/utils/define_utils.py b/scripts/utils/define_utils.py new file mode 100644 index 0000000..5ab97b3 --- /dev/null +++ b/scripts/utils/define_utils.py @@ -0,0 +1,496 @@ +import os +import json +import maya.cmds as cmds +from scripts.config import data + +class DNADefinition: + """DNA定义类""" + def __init__(self): + self.file_path = "" + self.content = {} + + def load(self, file_path): + """加载DNA文件""" + if not os.path.exists(file_path): + raise FileNotFoundError(f"DNA文件不存在: {file_path}") + + try: + with open(file_path, "r") as f: + self.content = json.load(f) + self.file_path = file_path + return True + except Exception as e: + cmds.warning(f"加载DNA文件失败: {str(e)}") + return False + + def save(self, file_path=None): + """保存DNA文件""" + save_path = file_path or self.file_path + if not save_path: + cmds.warning("未指定保存路径") + return False + + try: + with open(save_path, "w") as f: + json.dump(self.content, f, indent=4) + return True + except Exception as e: + cmds.warning(f"保存DNA文件失败: {str(e)}") + return False + + def validate(self): + """验证DNA内容""" + required_keys = ["joints", "blendshapes", "description"] + for key in required_keys: + if key not in self.content: + return False + return True + +# 全局DNA定义实例 +dna_definition = DNADefinition() + +def browse_dna_file(): + """浏览DNA文件""" + file_path, _ = cmds.fileDialog2( + fileFilter="DNA Files (*.dna)", + dialogStyle=2, + fileMode=1 + ) + + if not file_path: + return + + # 加载DNA文件 + if dna_definition.load(file_path[0]): + # 更新UI预览 + update_dna_preview() + # 更新骨骼列表 + update_joint_list() + # 更新BlendShape列表 + update_blendshape_list() + +def update_dna_preview(): + """更新DNA预览""" + # 获取UI实例 + from scripts.MetaFusion import app + if not app: + return + + define_tab = app.window.findChild(QtWidgets.QWidget, "DefineTab") + if not define_tab: + return + + # 更新预览内容 + preview_text = json.dumps(dna_definition.content, indent=4) + define_tab.dna_preview.setText(preview_text) + +def update_joint_list(): + """更新骨骼列表""" + from scripts.MetaFusion import app + if not app: + return + + define_tab = app.window.findChild(QtWidgets.QWidget, "DefineTab") + if not define_tab: + return + + # 清空列表 + define_tab.joint_list.clear() + + # 添加骨骼项 + joints = dna_definition.content.get("joints", []) + for joint in joints: + item = QtWidgets.QTreeWidgetItem([ + joint["name"], + str(joint["position"]), + str(joint["rotation"]), + str(joint["scale"]) + ]) + define_tab.joint_list.addTopLevelItem(item) + +def update_blendshape_list(): + """更新BlendShape列表""" + from scripts.MetaFusion import app + if not app: + return + + define_tab = app.window.findChild(QtWidgets.QWidget, "DefineTab") + if not define_tab: + return + + # 清空列表 + define_tab.bs_list.clear() + + # 添加BlendShape项 + blendshapes = dna_definition.content.get("blendshapes", []) + for bs in blendshapes: + item = QtWidgets.QTreeWidgetItem([ + bs["name"], + bs["target"], + str(bs["weight"]) + ]) + define_tab.bs_list.addTopLevelItem(item) + +# 骨骼功能 +def add_joint(): + """添加骨骼""" + # 获取选中的骨骼 + sel = cmds.ls(sl=True, type="joint") + if not sel: + cmds.warning("请先选择要添加的骨骼") + return + + # 获取骨骼信息 + joint = sel[0] + pos = cmds.xform(joint, q=True, ws=True, t=True) + rot = cmds.xform(joint, q=True, ws=True, ro=True) + scl = cmds.xform(joint, q=True, ws=True, s=True) + + # 添加到DNA定义 + joint_data = { + "name": joint, + "position": pos, + "rotation": rot, + "scale": scl + } + + if "joints" not in dna_definition.content: + dna_definition.content["joints"] = [] + dna_definition.content["joints"].append(joint_data) + + # 更新UI + update_joint_list() + +def delete_joint(): + """删除骨骼""" + from scripts.MetaFusion import app + if not app: + return + + define_tab = app.window.findChild(QtWidgets.QWidget, "DefineTab") + if not define_tab: + return + + # 获取选中项 + items = define_tab.joint_list.selectedItems() + if not items: + cmds.warning("请先选择要删除的骨骼") + return + + # 从DNA定义中删除 + for item in items: + joint_name = item.text(0) + joints = dna_definition.content.get("joints", []) + dna_definition.content["joints"] = [ + j for j in joints if j["name"] != joint_name + ] + + # 更新UI + update_joint_list() + +def modify_joint(): + """修改骨骼""" + from scripts.MetaFusion import app + if not app: + return + + define_tab = app.window.findChild(QtWidgets.QWidget, "DefineTab") + if not define_tab: + return + + # 获取选中项 + items = define_tab.joint_list.selectedItems() + if not items: + cmds.warning("请先选择要修改的骨骼") + return + + # 获取场景中选中的骨骼 + sel = cmds.ls(sl=True, type="joint") + if not sel: + cmds.warning("请先选择要用于更新的骨骼") + return + + # 更新骨骼信息 + joint = sel[0] + pos = cmds.xform(joint, q=True, ws=True, t=True) + rot = cmds.xform(joint, q=True, ws=True, ro=True) + scl = cmds.xform(joint, q=True, ws=True, s=True) + + # 更新DNA定义 + joint_name = items[0].text(0) + joints = dna_definition.content.get("joints", []) + for j in joints: + if j["name"] == joint_name: + j["position"] = pos + j["rotation"] = rot + j["scale"] = scl + break + + # 更新UI + update_joint_list() + +# BlendShape功能 +def add_blendshape(): + """添加BlendShape""" + # 获取选中的BlendShape + sel = cmds.ls(sl=True, type="blendShape") + if not sel: + cmds.warning("请先选择要添加的BlendShape") + return + + # 获取BlendShape信息 + bs = sel[0] + target = cmds.blendShape(bs, q=True, g=True)[0] + weight = cmds.getAttr(f"{bs}.weight[0]") + + # 添加到DNA定义 + bs_data = { + "name": bs, + "target": target, + "weight": weight + } + + if "blendshapes" not in dna_definition.content: + dna_definition.content["blendshapes"] = [] + dna_definition.content["blendshapes"].append(bs_data) + + # 更新UI + update_blendshape_list() + +def delete_blendshape(): + """删除BlendShape""" + from scripts.MetaFusion import app + if not app: + return + + define_tab = app.window.findChild(QtWidgets.QWidget, "DefineTab") + if not define_tab: + return + + # 获取选中项 + items = define_tab.bs_list.selectedItems() + if not items: + cmds.warning("请先选择要删除的BlendShape") + return + + # 从DNA定义中删除 + for item in items: + bs_name = item.text(0) + blendshapes = dna_definition.content.get("blendshapes", []) + dna_definition.content["blendshapes"] = [ + bs for bs in blendshapes if bs["name"] != bs_name + ] + + # 更新UI + update_blendshape_list() + +def modify_blendshape(): + """修改BlendShape""" + from scripts.MetaFusion import app + if not app: + return + + define_tab = app.window.findChild(QtWidgets.QWidget, "DefineTab") + if not define_tab: + return + + # 获取选中项 + items = define_tab.bs_list.selectedItems() + if not items: + cmds.warning("请先选择要修改的BlendShape") + return + + # 获取场景中选中的BlendShape + sel = cmds.ls(sl=True, type="blendShape") + if not sel: + cmds.warning("请先选择要用于更新的BlendShape") + return + + # 更新BlendShape信息 + bs = sel[0] + target = cmds.blendShape(bs, q=True, g=True)[0] + weight = cmds.getAttr(f"{bs}.weight[0]") + + # 更新DNA定义 + bs_name = items[0].text(0) + blendshapes = dna_definition.content.get("blendshapes", []) + for b in blendshapes: + if b["name"] == bs_name: + b["target"] = target + b["weight"] = weight + break + + # 更新UI + update_blendshape_list() + +# DNA相关功能 +def load_dna_preview(dna_file): + """加载DNA预览 + Args: + dna_file: DNA文件路径 + Returns: + bool: 是否加载成功 + """ + try: + # 加载DNA文件 + if not dna_definition.load(dna_file): + return False + + # 更新UI预览 + update_dna_preview() + return True + + except Exception as e: + cmds.warning(f"加载DNA预览失败: {str(e)}") + return False + +def validate_dna(dna_file): + """验证DNA文件 + Args: + dna_file: DNA文件路径 + Returns: + bool: 是否验证通过 + """ + try: + # 加载DNA文件 + if not dna_definition.load(dna_file): + return False + + # 验证必要字段 + if not dna_definition.validate(): + cmds.warning("DNA文件缺少必要字段") + return False + + # 验证骨骼数据 + joints = dna_definition.content.get("joints", []) + for joint in joints: + required = ["name", "position", "rotation", "scale"] + if not all(key in joint for key in required): + cmds.warning(f"骨骼数据不完整: {joint.get('name', 'unknown')}") + return False + + # 验证BlendShape数据 + blendshapes = dna_definition.content.get("blendshapes", []) + for bs in blendshapes: + required = ["name", "target", "weight"] + if not all(key in bs for key in required): + cmds.warning(f"BlendShape数据不完整: {bs.get('name', 'unknown')}") + return False + + return True + + except Exception as e: + cmds.warning(f"验证DNA文件失败: {str(e)}") + return False + +def export_dna_definition(dna_file): + """导出DNA定义 + Args: + dna_file: 导出文件路径 + Returns: + bool: 是否导出成功 + """ + try: + # 收集场景中的骨骼数据 + joints = [] + for joint in cmds.ls(type="joint"): + pos = cmds.xform(joint, q=True, ws=True, t=True) + rot = cmds.xform(joint, q=True, ws=True, ro=True) + scl = cmds.xform(joint, q=True, ws=True, s=True) + + joints.append({ + "name": joint, + "position": pos, + "rotation": rot, + "scale": scl + }) + + # 收集场景中的BlendShape数据 + blendshapes = [] + for bs in cmds.ls(type="blendShape"): + target = cmds.blendShape(bs, q=True, g=True)[0] + weight = cmds.getAttr(f"{bs}.weight[0]") + + blendshapes.append({ + "name": bs, + "target": target, + "weight": weight + }) + + # 更新DNA内容 + dna_definition.content.update({ + "joints": joints, + "blendshapes": blendshapes, + "description": { + "name": cmds.file(q=True, sn=True, shn=True), + "version": data.TOOL_VERSION, + "author": data.TOOL_AUTHOR, + "date": cmds.date(format="YYYY-MM-DD HH:mm:ss") + } + }) + + # 保存文件 + return dna_definition.save(dna_file) + + except Exception as e: + cmds.warning(f"导出DNA定义失败: {str(e)}") + return False + +def import_dna_definition(dna_file): + """导入DNA定义 + Args: + dna_file: DNA文件路径 + Returns: + bool: 是否导入成功 + """ + try: + # 验证DNA文件 + if not validate_dna(dna_file): + return False + + # 导入骨骼 + joints = dna_definition.content.get("joints", []) + for joint_data in joints: + # 检查骨骼是否存在 + joint_name = joint_data["name"] + if not cmds.objExists(joint_name): + # 创建骨骼 + joint = cmds.joint(name=joint_name) + else: + joint = joint_name + + # 设置变换 + cmds.xform(joint, + ws=True, + t=joint_data["position"], + ro=joint_data["rotation"], + s=joint_data["scale"] + ) + + # 导入BlendShape + blendshapes = dna_definition.content.get("blendshapes", []) + for bs_data in blendshapes: + # 检查目标是否存在 + if not cmds.objExists(bs_data["target"]): + cmds.warning(f"BlendShape目标不存在: {bs_data['target']}") + continue + + # 创建或获取BlendShape + bs_name = bs_data["name"] + if not cmds.objExists(bs_name): + bs = cmds.blendShape( + bs_data["target"], + name=bs_name, + frontOfChain=True + )[0] + else: + bs = bs_name + + # 设置权重 + cmds.setAttr(f"{bs}.weight[0]", bs_data["weight"]) + + return True + + except Exception as e: + cmds.warning(f"导入DNA定义失败: {str(e)}") + return False \ No newline at end of file diff --git a/scripts/utils/install_check.py b/scripts/utils/install_check.py new file mode 100644 index 0000000..79c9ae8 --- /dev/null +++ b/scripts/utils/install_check.py @@ -0,0 +1,57 @@ +import os +import maya.cmds as cmds + +def check_installation(): + """检查插件安装状态""" + # 获取当前插件路径 + plugin_path = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) + + issues = [] + + # 检查mod文件 + maya_mod_path = os.path.join(os.getenv('MAYA_APP_DIR'), 'modules') + mod_file = os.path.join(maya_mod_path, 'MetaFusion.mod') + if not os.path.exists(mod_file): + issues.append("缺少mod文件") + else: + # 验证mod文件内容 + with open(mod_file, 'r') as f: + content = f.read() + if plugin_path not in content: + issues.append("mod文件路径不正确") + + # 检查必要目录 + required_dirs = [ + 'ui', + 'utils', + 'config', + 'resources/icons' + ] + for dir_path in required_dirs: + if not os.path.exists(os.path.join(plugin_path, dir_path)): + issues.append(f"缺少必要目录: {dir_path}") + + # 检查必要文件 + required_files = [ + 'MetaFusion.py', + 'ui/__init__.py', + 'ui/menu.py', + 'ui/models.py', + 'ui/rigging.py', + 'ui/define.py', + 'utils/adjust_utils.py', + 'utils/define_utils.py', + 'config/data.py' + ] + for file_path in required_files: + if not os.path.exists(os.path.join(plugin_path, file_path)): + issues.append(f"缺少必要文件: {file_path}") + + # 检查插件加载 + if not cmds.pluginInfo('MetaFusion', q=True, loaded=True): + try: + cmds.loadPlugin('MetaFusion') + except: + issues.append("插件无法加载") + + return issues \ No newline at end of file diff --git a/scripts/utils/menu_utils.py b/scripts/utils/menu_utils.py new file mode 100644 index 0000000..3f03628 --- /dev/null +++ b/scripts/utils/menu_utils.py @@ -0,0 +1,146 @@ +import os +import maya.cmds as cmds +from scripts.config import data +from scripts.utils import model_utils, rigging_utils, adjust_utils, define_utils + +def load_dna(): + """打开DNA文件""" + file_path = cmds.fileDialog2( + fileFilter="DNA Files (*.dna)", + dialogStyle=2, + fileMode=1 + ) + if file_path: + print(f"加载DNA文件: {file_path[0]}") + # TODO: 实现DNA加载逻辑 + +def save_dna(): + """保存DNA文件""" + file_path = cmds.fileDialog2( + fileFilter="DNA Files (*.dna)", + dialogStyle=2, + fileMode=0 + ) + if file_path: + print(f"保存DNA文件: {file_path[0]}") + # TODO: 实现DNA保存逻辑 + +def load_project_dna(): + """加载当前项目的DNA""" + print("加载当前项目DNA功能待实现") + +def rename_blend_target(): + """修改混合目标名称""" + print("修改混合目标名称功能待实现") + +def reset_blend_target(): + """重置混合目标名称""" + print("重置混合目标名称功能待实现") + +def export_fbx(): + """导出FBX""" + file_path = cmds.fileDialog2( + fileFilter="FBX Files (*.fbx)", + dialogStyle=2, + fileMode=0 + ) + if file_path: + print(f"导出FBX文件: {file_path[0]}") + # TODO: 实现FBX导出逻辑 + +def safe_shutdown(): + """安全退出""" + # 保存当前状态 + save_dna() + # 关闭窗口 + cmds.deleteUI(data.TOOL_WSCL_NAME) + +# 编辑菜单功能 +def create_rl4_node(): + """创建RL4节点""" + print("创建RL4节点功能待实现") + +def delete_rl4_node(): + """删除RL4节点""" + print("删除RL4节点功能待实现") + +def mirror_left_to_right(): + """镜像左至右""" + print("镜像左至右功能待实现") + +def mirror_right_to_left(): + """镜像右至左""" + print("镜像右至左功能待实现") + +def pose_a_to_t(): + """姿势由A型转T型""" + print("姿势由A型转T型功能待实现") + +def pose_t_to_a(): + """姿势由T型转A型""" + print("姿势由T型转A型功能待实现") + +def transfer_lod_texture(): + """传输LOD贴图""" + print("传输LOD贴图功能待实现") + +def set_joint_color(): + """设置关节颜色""" + print("设置关节颜色功能待实现") + +def unmark_all(): + """取消全部标记""" + print("取消全部标记功能待实现") + +def rebuild_all_targets(): + """重建所有目标""" + print("重建所有目标功能待实现") + +def bake_all_animations(): + """为所有表情设置关键帧""" + print("为所有表情设置关键帧功能待实现") + +def bake_all_keyframes(): + """烘焙所有表情的关键帧""" + print("烘焙所有表情的关键帧功能待实现") + +# 工具菜单功能 +def export_skin(): + """导出蒙皮""" + print("导出蒙皮功能待实现") + +def import_skin(): + """导入蒙皮""" + print("导入蒙皮功能待实现") + +def copy_skin(): + """拷贝蒙皮""" + print("拷贝蒙皮功能待实现") + +# 语言菜单功能 +def set_chinese(): + """设置中文界面""" + print("设置中文界面功能待实现") + +def set_english(): + """设置英文界面""" + print("设置英文界面功能待实现") + +# 帮助菜单功能 +def show_help(): + """显示帮助文档""" + if os.path.exists(data.TOOL_HELP_URL): + os.startfile(data.TOOL_HELP_URL) + else: + cmds.warning("帮助文档不存在") + +def show_about(): + """显示关于对话框""" + cmds.confirmDialog( + title="关于", + message=f"{data.TOOL_NAME} {data.TOOL_VERSION}\n" + f"作者: {data.TOOL_AUTHOR}\n" + f"帮助: {data.TOOL_HELP_URL}", + button=["确定"], + defaultButton="确定" + ) \ No newline at end of file diff --git a/scripts/utils/model_utils.py b/scripts/utils/model_utils.py new file mode 100644 index 0000000..62aefff --- /dev/null +++ b/scripts/utils/model_utils.py @@ -0,0 +1,595 @@ +import os +import maya.cmds as cmds +from scripts.config import data +import maya.mel as mel + +# LOD模型加载功能 +def load_model(lod_index, model_type, file_path): + """加载模型 + Args: + lod_index: LOD级别(0-7) + model_type: 模型类型(head/teeth/gums/eye_l/eye_r/iris/eyelash/eyelid/cartilage/body) + file_path: 模型文件路径 + """ + try: + # 检查文件是否存在 + if not os.path.exists(file_path): + raise FileNotFoundError(f"模型文件不存在: {file_path}") + + # 导入模型 + imported_nodes = cmds.file( + file_path, + i=True, + returnNewNodes=True, + namespace=f"LOD{lod_index}_{model_type}" + ) + + # 设置模型属性 + for node in imported_nodes: + if cmds.objectType(node) == "transform": + # 添加自定义属性 + cmds.addAttr(node, ln="LOD_INDEX", at="long", dv=lod_index) + cmds.addAttr(node, ln="MODEL_TYPE", dt="string") + cmds.setAttr(f"{node}.MODEL_TYPE", model_type, type="string") + + print(f"成功加载LOD{lod_index}_{model_type}模型") + return imported_nodes + + except Exception as e: + cmds.warning(f"加载模型失败: {str(e)}") + return None + +def delete_lod(lod_index): + """删除指定LOD级别的所有模型""" + try: + # 查找指定LOD的所有模型 + lod_nodes = cmds.ls(f"LOD{lod_index}_*", long=True) + if lod_nodes: + cmds.delete(lod_nodes) + print(f"成功删除LOD{lod_index}的所有模型") + else: + print(f"未找到LOD{lod_index}的模型") + except Exception as e: + cmds.warning(f"删除LOD{lod_index}失败: {str(e)}") + +# 模型工具功能 +def split_model(): + """分离选中的模型""" + try: + selection = cmds.ls(sl=True, type="transform") + if not selection: + raise ValueError("请先选择要分离的模型") + + for obj in selection: + # 获取模型的面数 + mesh = cmds.listRelatives(obj, shapes=True, type="mesh") + if not mesh: + continue + + # 分离每个面为独立的模型 + cmds.polySeparate(obj, ch=False) + + print("模型分离完成") + + except Exception as e: + cmds.warning(f"模型分离失败: {str(e)}") + +def generate_facial_accessories(): + """生成面部配件""" + print("生成面部配件功能待实现") + +def repair_normals(): + """修复法线""" + try: + selection = cmds.ls(sl=True, type="transform") + if not selection: + raise ValueError("请先选择要修复法线的模型") + + for obj in selection: + mesh = cmds.listRelatives(obj, shapes=True, type="mesh") + if not mesh: + continue + + # 统一法线方向 + cmds.polyNormal(obj, normalMode=2, userNormalMode=0) + # 解锁法线 + cmds.polyNormalPerVertex(obj, ufn=True) + + print("法线修复完成") + + except Exception as e: + cmds.warning(f"修复法线失败: {str(e)}") + +def repair_vertex_order(): + """修复点序""" + try: + selection = cmds.ls(sl=True, type="transform") + if not selection: + raise ValueError("请先选择要修复点序的模型") + + for obj in selection: + # 重新排序顶点 + cmds.polyReorder(obj) + + print("点序修复完成") + + except Exception as e: + cmds.warning(f"修复点序失败: {str(e)}") + +def repair_seams(): + """修复接缝""" + try: + selection = cmds.ls(sl=True, type="transform") + if not selection: + raise ValueError("请先选择要修复接缝的模型") + + for obj in selection: + # 合并重叠顶点 + cmds.polyMergeVertex(obj, d=0.001) + # 删除重复面 + cmds.polyRemoveFace(obj, removeDuplicateFaces=True) + + print("接缝修复完成") + + except Exception as e: + cmds.warning(f"修复接缝失败: {str(e)}") + +# LOD功能 +def load_custom_models(): + """自定义加载模型""" + print("自定义加载模型功能待实现") + +def standardize_naming(): + """标准化命名""" + try: + selection = cmds.ls(sl=True, type="transform") + if not selection: + raise ValueError("请先选择要标准化命名的模型") + + for obj in selection: + # 获取LOD信息 + if cmds.attributeQuery("LOD_INDEX", node=obj, exists=True): + lod_index = cmds.getAttr(f"{obj}.LOD_INDEX") + model_type = cmds.getAttr(f"{obj}.MODEL_TYPE") + # 重命名为标准格式 + new_name = f"LOD{lod_index}_{model_type}" + cmds.rename(obj, new_name) + + print("命名标准化完成") + + except Exception as e: + cmds.warning(f"标准化命名失败: {str(e)}") + +def auto_group(): + """自动分组""" + try: + # 创建LOD组 + for i in range(8): + group_name = f"LOD{i}_GROUP" + if not cmds.objExists(group_name): + cmds.group(empty=True, name=group_name) + + # 将模型移动到对应组 + all_models = cmds.ls("LOD*_*", type="transform") + for model in all_models: + if cmds.attributeQuery("LOD_INDEX", node=model, exists=True): + lod_index = cmds.getAttr(f"{model}.LOD_INDEX") + group_name = f"LOD{lod_index}_GROUP" + cmds.parent(model, group_name) + + print("自动分组完成") + + except Exception as e: + cmds.warning(f"自动分组失败: {str(e)}") + +# MetaHuman模型特定功能 +def validate_metahuman_topology(): + """验证模型是否符合MetaHuman拓扑要求""" + try: + selection = cmds.ls(sl=True, type="transform") + if not selection: + raise ValueError("请先选择要验证的模型") + + results = [] + for obj in selection: + mesh = cmds.listRelatives(obj, shapes=True, type="mesh") + if not mesh: + continue + + # 获取模型信息 + vertex_count = cmds.polyEvaluate(obj, vertex=True) + face_count = cmds.polyEvaluate(obj, face=True) + + # 检查是否符合MetaHuman标准 + is_valid = True + messages = [] + + # 获取LOD级别 + lod_index = -1 + if cmds.attributeQuery("LOD_INDEX", node=obj, exists=True): + lod_index = cmds.getAttr(f"{obj}.LOD_INDEX") + + # 根据LOD级别验证顶点数和面数 + if lod_index == 0: + if vertex_count != 7127: + messages.append(f"顶点数不符合LOD0标准(当前:{vertex_count}, 应为:7127)") + is_valid = False + elif lod_index == 1: + if vertex_count != 5127: + messages.append(f"顶点数不符合LOD1标准(当前:{vertex_count}, 应为:5127)") + is_valid = False + # ... 其他LOD级别的验证 + + results.append({ + "object": obj, + "is_valid": is_valid, + "messages": messages + }) + + # 显示验证结果 + for result in results: + status = "✓" if result["is_valid"] else "✗" + print(f"{status} {result['object']}:") + if not result["is_valid"]: + for msg in result["messages"]: + print(f" - {msg}") + + return results + + except Exception as e: + cmds.warning(f"拓扑验证失败: {str(e)}") + return None + +def transfer_uvs(): + """传递UV到其他LOD级别""" + try: + # 获取源模型和目标模型 + selection = cmds.ls(sl=True, type="transform") + if len(selection) != 2: + raise ValueError("请选择一个源模型和一个目标模型") + + source = selection[0] + target = selection[1] + + # 执行UV传递 + cmds.transferAttributes( + source, + target, + transferUVs=2, + transferColors=0, + sampleSpace=4 # World space + ) + + # 删除构建历史 + cmds.delete(target, ch=True) + + print(f"UV传递完成: {source} -> {target}") + + except Exception as e: + cmds.warning(f"UV传递失败: {str(e)}") + +def generate_lod_chain(): + """生成完整的LOD链""" + try: + # 获取选中的LOD0模型 + selection = cmds.ls(sl=True, type="transform") + if not selection: + raise ValueError("请先选择LOD0模型") + + source = selection[0] + if not cmds.attributeQuery("LOD_INDEX", node=source, exists=True): + raise ValueError("请选择带有LOD属性的模型") + + lod_index = cmds.getAttr(f"{source}.LOD_INDEX") + if lod_index != 0: + raise ValueError("请选择LOD0模型") + + # 为每个LOD级别生成简化模型 + for i in range(1, 8): + target_name = source.replace("LOD0", f"LOD{i}") + + # 复制模型 + duplicate = cmds.duplicate(source, name=target_name)[0] + + # 设置LOD属性 + cmds.setAttr(f"{duplicate}.LOD_INDEX", i) + + # 简化模型 + reduction_ratio = 1.0 - (i * 0.1) # 每级减少10% + cmds.polyReduce( + duplicate, + percentage=reduction_ratio * 100, + triangulate=False, + preserveTopology=True, + keepQuadsWeight=1.0, + keepBorder=True + ) + + print(f"生成LOD{i}完成: {target_name}") + + print("LOD链生成完成") + + except Exception as e: + cmds.warning(f"LOD链生成失败: {str(e)}") + +def check_model_symmetry(): + """检查模型对称性""" + try: + selection = cmds.ls(sl=True, type="transform") + if not selection: + raise ValueError("请先选择要检查的模型") + + for obj in selection: + # 获取顶点位置 + vertices = cmds.ls(f"{obj}.vtx[*]", flatten=True) + vertex_positions = {} + + for vtx in vertices: + pos = cmds.pointPosition(vtx, world=True) + # 使用x坐标的绝对值作为键,保存顶点信息 + x_abs = abs(pos[0]) + if x_abs not in vertex_positions: + vertex_positions[x_abs] = [] + vertex_positions[x_abs].append((vtx, pos)) + + # 检查对称性 + asymmetric_vertices = [] + for x_abs, points in vertex_positions.items(): + if len(points) == 2: + vtx1, pos1 = points[0] + vtx2, pos2 = points[1] + # 检查y和z坐标是否对称 + if abs(pos1[1] - pos2[1]) > 0.001 or abs(pos1[2] - pos2[2]) > 0.001: + asymmetric_vertices.extend([vtx1, vtx2]) + elif len(points) == 1 and x_abs > 0.001: # 忽略中心线上的点 + asymmetric_vertices.append(points[0][0]) + + # 显示结果 + if asymmetric_vertices: + cmds.select(asymmetric_vertices) + print(f"发现{len(asymmetric_vertices)}个不对称顶点") + else: + print(f"{obj}模型对称性检查通过") + + except Exception as e: + cmds.warning(f"对称性检查失败: {str(e)}") + +def mirror_geometry(): + """镜像几何体""" + try: + selection = cmds.ls(sl=True, type="transform") + if not selection: + raise ValueError("请先选择要镜像的模型") + + for obj in selection: + # 创建镜像几何体 + mirrored = cmds.duplicate(obj, name=f"{obj}_mirrored")[0] + + # 执行镜像 + cmds.scale(-1, 1, 1, mirrored) + + # 反转法线 + cmds.polyNormal(mirrored, normalMode=0) + + print(f"模型镜像完成: {mirrored}") + + except Exception as e: + cmds.warning(f"模型镜像失败: {str(e)}") + +# 骨骼权重相关功能 +def transfer_skin_weights(): + """传递蒙皮权重""" + try: + selection = cmds.ls(sl=True, type="transform") + if len(selection) < 2: + raise ValueError("请选择源模型和目标模型") + + source = selection[0] + targets = selection[1:] + + # 获取源模型的蒙皮变形器 + skin_cluster = mel.eval(f'findRelatedSkinCluster("{source}")') + if not skin_cluster: + raise ValueError(f"源模型{source}没有蒙皮变形器") + + # 获取骨骼列表 + joints = cmds.skinCluster(skin_cluster, q=True, inf=True) + + # 为每个目标模型创建蒙皮并传递权重 + for target in targets: + # 创建新的蒙皮变形器 + new_skin = cmds.skinCluster( + joints, + target, + toSelectedBones=True, + bindMethod=0, + skinMethod=0, + normalizeWeights=1 + )[0] + + # 复制权重 + cmds.copySkinWeights( + ss=skin_cluster, + ds=new_skin, + noMirror=True, + surfaceAssociation="closestPoint", + influenceAssociation="oneToOne" + ) + + print(f"权重传递完成: {source} -> {target}") + + except Exception as e: + cmds.warning(f"权重传递失败: {str(e)}") + +def mirror_skin_weights(): + """镜像蒙皮权重""" + try: + selection = cmds.ls(sl=True, type="transform") + if not selection: + raise ValueError("请选择要镜像权重的模型") + + for obj in selection: + # 获取蒙皮变形器 + skin_cluster = mel.eval(f'findRelatedSkinCluster("{obj}")') + if not skin_cluster: + continue + + # 执行镜像 + cmds.copySkinWeights( + ss=skin_cluster, + ds=skin_cluster, + mirrorMode="YZ", + surfaceAssociation="closestPoint", + influenceAssociation="closestJoint" + ) + + print(f"权重镜像完成: {obj}") + + except Exception as e: + cmds.warning(f"权重镜像失败: {str(e)}") + +def clean_skin_weights(): + """清理蒙皮权重""" + try: + selection = cmds.ls(sl=True, type="transform") + if not selection: + raise ValueError("请选择要清理权重的模型") + + for obj in selection: + # 获取蒙皮变形器 + skin_cluster = mel.eval(f'findRelatedSkinCluster("{obj}")') + if not skin_cluster: + continue + + # 移除小权重 + cmds.skinPercent( + skin_cluster, + obj, + pruneWeights=0.01 # 移除小于1%的权重 + ) + + # 规范化权重 + cmds.skinPercent( + skin_cluster, + obj, + normalize=True + ) + + print(f"权重清理完成: {obj}") + + except Exception as e: + cmds.warning(f"权重清理失败: {str(e)}") + +# 变形器处理功能 +def transfer_blendshapes(): + """传递变形器""" + try: + selection = cmds.ls(sl=True, type="transform") + if len(selection) < 2: + raise ValueError("请选择源模型和目标模型") + + source = selection[0] + targets = selection[1:] + + # 获取源模型的变形器 + blendshapes = cmds.listConnections(source, type="blendShape") + if not blendshapes: + raise ValueError(f"源模型{source}没有变形器") + + for bs in blendshapes: + # 获取所有目标形状 + targets_count = cmds.blendShape(bs, q=True, target=True) + target_weights = [] + target_names = [] + + # 保存当前权重 + for i in range(targets_count): + weight = cmds.getAttr(f"{bs}.weight[{i}]") + name = cmds.aliasAttr(f"{bs}.weight[{i}]", q=True) + target_weights.append(weight) + target_names.append(name) + + # 为每个目标模型创建变形器 + for target in targets: + new_bs = cmds.blendShape( + target, + frontOfChain=True, + name=f"{target}_blendShape" + )[0] + + # 设置权重 + for i, (weight, name) in enumerate(zip(target_weights, target_names)): + cmds.setAttr(f"{new_bs}.{name}", weight) + + print(f"变形器传递完成: {source} -> {targets}") + + except Exception as e: + cmds.warning(f"变形器传递失败: {str(e)}") + +def optimize_blendshapes(): + """优化变形器""" + try: + selection = cmds.ls(sl=True, type="transform") + if not selection: + raise ValueError("请选择要优化变形器的模型") + + for obj in selection: + # 获取变形器 + blendshapes = cmds.listConnections(obj, type="blendShape") + if not blendshapes: + continue + + for bs in blendshapes: + # 获取所有目标形状 + target_count = cmds.blendShape(bs, q=True, target=True) + + # 删除未使用的目标形状 + for i in range(target_count): + weight = cmds.getAttr(f"{bs}.weight[{i}]") + if abs(weight) < 0.001: # 权重接近0的目标形状 + cmds.removeMultiInstance(f"{bs}.weight[{i}]", b=True) + + # 重新排序索引 + cmds.blendShape(bs, edit=True, resetTargetDelta=True) + + print(f"变形器优化完成: {obj}") + + except Exception as e: + cmds.warning(f"变形器优化失败: {str(e)}") + +def create_corrective_blendshape(): + """创建修正变形器""" + try: + selection = cmds.ls(sl=True, type="transform") + if len(selection) != 2: + raise ValueError("请选择基础模型和目标形状") + + base = selection[0] + target = selection[1] + + # 创建修正变形器 + corrective_bs = cmds.blendShape( + target, + base, + frontOfChain=True, + name=f"{base}_corrective" + )[0] + + # 设置权重驱动 + cmds.setDrivenKeyframe( + f"{corrective_bs}.{target}", + currentDriver="time", + driverValue=0, + value=0 + ) + cmds.setDrivenKeyframe( + f"{corrective_bs}.{target}", + currentDriver="time", + driverValue=1, + value=1 + ) + + print(f"修正变形器创建完成: {corrective_bs}") + + except Exception as e: + cmds.warning(f"创建修正变形器失败: {str(e)}") \ No newline at end of file diff --git a/scripts/utils/rigging_utils.py b/scripts/utils/rigging_utils.py new file mode 100644 index 0000000..c5caf9e --- /dev/null +++ b/scripts/utils/rigging_utils.py @@ -0,0 +1,24 @@ +import maya.cmds as cmds +from scripts.config import data + +def export_settings(): + """导出设置""" + print("导出设置功能待实现") + +def import_settings(): + """导入设置""" + print("导入设置功能待实现") + +def clear_options(): + """清空选项""" + print("清空选项功能待实现") + +def import_skeleton(): + """导入骨架""" + print("导入骨架功能待实现") + +def create_skeleton(): + """创建骨架""" + print("创建骨架功能待实现") + +# 其他功能函数... \ No newline at end of file