This commit is contained in:
2025-05-02 00:14:28 +08:00
commit 6f27dc11e3
132 changed files with 28609 additions and 0 deletions

284
scripts/Main.py Normal file
View File

@@ -0,0 +1,284 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Main module for Metahuman customize plugin
主模块 - 负责初始化UI和功能集成
功能: 从ui模块加载子模块显示
作者: Virtuos Games
版本: Alpha v1.0.0
"""
#===================================== IMPORT MODULES =====================================
from Qt import QtWidgets, QtCore, QtGui
from Qt.QtCompat import wrapInstance
from maya import OpenMayaUI as omui
import maya.cmds as cmds
import maya.mel as mel
import maya.utils as utils
import webbrowser
import subprocess
import importlib
import traceback
import locale
import sys
import os
#===================================== IMPORT UI MODULES ===================================
from scripts.ui import toolbar
from scripts.ui import geometry
from scripts.ui import rigging
from scripts.ui import behaviour
from scripts.ui import definition
#========================================== CONFIG ========================================
import config
TOOL_NAME = config.TOOL_NAME
TOOL_VERSION = config.TOOL_VERSION
TOOL_AUTHOR = config.TOOL_AUTHOR
TOOL_YEAR = config.TOOL_VERSION
TOOL_MOD_FILENAME = config.TOOL_MOD_FILENAME
TOOL_LANG = config.TOOL_LANG
TOOL_WSCL_NAME = config.TOOL_WSCL_NAME
TOOL_HELP_URL = config.TOOL_HELP_URL
TOOL_PATH = config.TOOL_PATH
SCRIPTS_PATH = config.SCRIPTS_PATH
TOOL_MAIN_SCRIPT = config.TOOL_MAIN_SCRIPT
UI_PATH = config.UI_PATH
STYLE_FILE = config.STYLE_FILE
ICONS_PATH = config.ICONS_PATH
TOOL_ICON = config.TOOL_ICON
ASSETS_PATH = config.ASSETS_PATH
DNA_FILE_PATH = config.DNA_FILE_PATH
DNA_IMG_PATH = config.DNA_IMG_PATH
TOOL_COMMAND_ICON = config.TOOL_COMMAND_ICON
#====================================== LOCALIZATION ==================================
from scripts.ui import localization
LANG = localization.LANG
#========================================= INIT =======================================
def get_system_encoding():
encoding = sys.getdefaultencoding()
if encoding.lower() == 'ascii':
encoding = locale.getpreferredencoding()
return encoding
def maya_main_window():
main_window_ptr = omui.MQtUtil.mainWindow()
return wrapInstance(int(main_window_ptr), QtWidgets.QWidget)
#===================================== MAIN FUNCTION ===================================
class MainWindow(QtWidgets.QWidget):
def __init__(self, parent=maya_main_window()):
super(MainWindow, self).__init__(parent)
self.setWindowTitle(TOOL_NAME)
self.setObjectName(f"{TOOL_NAME}MainWindow")
self.setWindowFlags(QtCore.Qt.Window)
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
# 设置自适应大小策略
self.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
self.setMinimumSize(300, 600) # 减小最小高度,让窗口更灵活
self.create_widgets()
self.create_layouts()
self.create_connections()
if os.path.exists(TOOL_ICON):
self.setWindowIcon(QtGui.QIcon(TOOL_ICON))
else:
print("WARNING: Icon file not found: {}".format(TOOL_ICON))
def dock_to_maya(self):
if cmds.workspaceControl(TOOL_WSCL_NAME, exists=True):
cmds.deleteUI(TOOL_WSCL_NAME)
def create_control():
try:
workspace_control = cmds.workspaceControl(
TOOL_WSCL_NAME,
label=TOOL_NAME,
floating=True,
retain=True,
resizeWidth=True,
initialWidth=300,
minimumWidth=300
)
cmds.workspaceControl(TOOL_WSCL_NAME, e=True, resizeWidth=True)
cmds.control(self.objectName(), e=True, p=workspace_control)
cmds.evalDeferred(lambda: cmds.workspaceControl(TOOL_WSCL_NAME, e=True, resizeWidth=True))
except Exception as e:
print("Error creating workspace control: {}".format(e))
cmds.evalDeferred(create_control)
#===================================== UI COMPONENTS =====================================
def create_widgets(self):
# 创建滚动区域
self.scroll_area = QtWidgets.QScrollArea()
self.scroll_area.setObjectName("main_scroll_area")
self.scroll_area.setWidgetResizable(True)
self.scroll_area.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.scroll_area.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) # 改回按需显示
self.scroll_area.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
# 创建滚动区域内容控件
self.scroll_content = QtWidgets.QWidget()
self.scroll_content.setObjectName("scroll_content")
self.scroll_content.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
# 创建主标签页
self.main_tab = QtWidgets.QTabWidget()
self.main_tab.setObjectName("main_tab")
self.main_tab.setTabPosition(QtWidgets.QTabWidget.North)
self.main_tab.setTabShape(QtWidgets.QTabWidget.Rounded)
self.main_tab.setDocumentMode(True)
self.main_tab.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
self.main_tab.setMinimumHeight(400)
# 创建各功能模块标签页
self.geometry_tab = QtWidgets.QWidget()
self.geometry_tab.setObjectName("geometry_tab")
self.geometry_tab.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
self.rigging_tab = QtWidgets.QWidget()
self.rigging_tab.setObjectName("rigging_tab")
self.rigging_tab.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
self.behaviour_tab = QtWidgets.QWidget()
self.behaviour_tab.setObjectName("behaviour_tab")
self.behaviour_tab.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
self.definition_tab = QtWidgets.QWidget()
self.definition_tab.setObjectName("definition_tab")
self.definition_tab.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
# 创建工具栏
self.toolbar_frame = QtWidgets.QFrame()
self.toolbar_frame.setObjectName("toolbar_frame")
self.toolbar_frame.setFrameShape(QtWidgets.QFrame.StyledPanel)
self.toolbar_frame.setFrameShadow(QtWidgets.QFrame.Raised)
self.toolbar_frame.setMaximumHeight(40)
self.toolbar_frame.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
# 创建状态栏
self.status_bar = QtWidgets.QStatusBar()
self.status_bar.setObjectName("status_bar")
self.status_bar.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
self.status_bar.showMessage(f"{TOOL_NAME} {TOOL_VERSION}")
# 初始化各模块UI组件
toolbar.widgets()
geometry.widgets()
rigging.widgets()
behaviour.widgets()
definition.widgets()
def create_layouts(self):
# 主布局
self.main_layout = QtWidgets.QVBoxLayout(self)
self.main_layout.setContentsMargins(2, 2, 2, 2)
self.main_layout.setSpacing(2)
# 滚动区域内容布局
self.scroll_content_layout = QtWidgets.QVBoxLayout(self.scroll_content)
self.scroll_content_layout.setContentsMargins(2, 2, 2, 2)
self.scroll_content_layout.setSpacing(2)
# 添加工具栏
self.toolbar_layout = QtWidgets.QVBoxLayout(self.toolbar_frame)
self.toolbar_layout.setContentsMargins(0, 0, 0, 0)
toolbar.layouts(parent_frame=self.toolbar_frame)
# 设置各标签页布局
self.geometry_layout = QtWidgets.QVBoxLayout(self.geometry_tab)
self.geometry_layout.setContentsMargins(4, 4, 4, 4)
geometry.layouts(parent_tab=self.geometry_tab)
self.rigging_layout = QtWidgets.QVBoxLayout(self.rigging_tab)
self.rigging_layout.setContentsMargins(4, 4, 4, 4)
rigging.layouts(parent_tab=self.rigging_tab)
self.behaviour_layout = QtWidgets.QVBoxLayout(self.behaviour_tab)
self.behaviour_layout.setContentsMargins(4, 4, 4, 4)
behaviour.layouts(parent_tab=self.behaviour_tab)
self.definition_layout = QtWidgets.QVBoxLayout(self.definition_tab)
self.definition_layout.setContentsMargins(4, 4, 4, 4)
definition.layouts(parent_tab=self.definition_tab)
# 添加标签页到主标签控件
self.main_tab.addTab(self.geometry_tab, "几何模型")
self.main_tab.addTab(self.rigging_tab, "绑定系统")
self.main_tab.addTab(self.behaviour_tab, "行为系统")
self.main_tab.addTab(self.definition_tab, "定义系统")
# 将组件添加到滚动区域内容布局
self.scroll_content_layout.addWidget(self.toolbar_frame)
self.scroll_content_layout.addWidget(self.main_tab)
# 设置滚动区域的内容控件
self.scroll_area.setWidget(self.scroll_content)
# 将滚动区域和状态栏添加到主布局
self.main_layout.addWidget(self.scroll_area)
self.main_layout.addWidget(self.status_bar)
# 加载样式表
if os.path.exists(STYLE_FILE):
try:
with open(STYLE_FILE, "r", encoding="utf-8") as f:
style = f.read()
self.setStyleSheet(style)
except UnicodeDecodeError:
# 尝试使用系统默认编码
encoding = get_system_encoding()
try:
with open(STYLE_FILE, "r", encoding=encoding) as f:
style = f.read()
self.setStyleSheet(style)
except Exception as e:
print(f"警告: 无法加载样式表文件: {e}")
else:
print(f"警告: 样式表文件不存在: {STYLE_FILE}")
def create_connections(self):
# 连接各模块的信号和槽
toolbar.connections()
geometry.connections()
rigging.connections()
behaviour.connections()
definition.connections()
# 标签页切换信号
self.main_tab.currentChanged.connect(self.on_tab_changed)
def on_tab_changed(self, index):
tab_name = self.main_tab.tabText(index)
self.status_bar.showMessage(f"当前模块: {tab_name}")
print(f"切换到模块: {tab_name}")
def main():
"""
Main function to initialize and show the Plugin UI
"""
try:
# Initialize UI modules with placeholder functions
# Each module only contains UI framework without actual functionality
toolbar.toolbar_temp_function()
geometry.geometry_temp_function()
rigging.rigging_temp_function()
behaviour.behaviour_temp_function()
definition.definition_temp_function()
# Create and show main window
global tool_window
tool_window = MainWindow()
tool_window.dock_to_maya()
print(f"{TOOL_NAME} plugin initialized successfully!")
return tool_window
except Exception as e:
error_msg = f"Error initializing {TOOL_NAME} plugin: {str(e)}"
print(error_msg)
traceback.print_exc()
cmds.warning(error_msg)
return None
# Auto-run when imported
if __name__ == "__main__":
main()

741
scripts/ReloadModules.py Normal file
View File

@@ -0,0 +1,741 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Tool - Module Reload Tool
Provides module reloading functionality for the plugin, supporting hot updates
用法说明:
1. 在Maya中直接运行:
import scripts.ReloadModules
scripts.ReloadModules.main()
2. 从命令行重新加载所有模块:
import scripts.ReloadModules
scripts.ReloadModules.reload_all()
3. 重新加载特定模块:
import scripts.ReloadModules
scripts.ReloadModules.ModuleReloader.reload_module('module_name')
4. 在开发过程中使用:
- 当修改了代码后,运行此模块可以热更新所有更改
- 无需重启Maya即可测试新功能
- 支持UI和命令行两种方式
注意事项:
- 某些深度依赖的模块可能需要手动重启Maya
- 如果遇到导入错误,请检查模块路径是否正确
- 重新加载可能不会影响已经创建的对象实例
"""
import sys
import importlib
import os
import traceback
import shutil
from maya import cmds
# 设置工具路径
TOOL_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if TOOL_PATH not in sys.path:
sys.path.insert(0, TOOL_PATH)
# 设置脚本路径
SCRIPTS_PATH = os.path.dirname(os.path.abspath(__file__))
if SCRIPTS_PATH not in sys.path:
sys.path.insert(0, SCRIPTS_PATH)
# 导入主配置 - 多级尝试导入策略
def import_main_config():
"""尝试多种方式导入主配置文件"""
global TOOL_NAME, TOOL_VERSION, TOOL_AUTHOR, TOOL_PATH, SCRIPTS_PATH
global UI_PATH, STYLE_FILE, ICONS_PATH, ASSETS_PATH, DNA_FILE_PATH, Config
# 尝试直接导入
try:
import config
importlib.reload(config) # 确保获取最新配置
# 从主配置文件导入路径和设置
TOOL_NAME = config.TOOL_NAME
TOOL_VERSION = config.TOOL_VERSION
TOOL_AUTHOR = config.TOOL_AUTHOR
TOOL_PATH = config.TOOL_PATH
SCRIPTS_PATH = config.SCRIPTS_PATH
UI_PATH = config.UI_PATH
STYLE_FILE = config.STYLE_FILE
ICONS_PATH = config.ICONS_PATH
ASSETS_PATH = config.ASSETS_PATH
DNA_FILE_PATH = config.DNA_FILE_PATH
print(f"成功从主配置文件导入配置")
return True
except ImportError as e:
print(f"直接导入主配置文件失败: {str(e)}")
# 尝试从上级目录导入
parent_dir = os.path.dirname(TOOL_PATH)
if parent_dir not in sys.path:
sys.path.insert(0, parent_dir)
try:
import config
importlib.reload(config) # 确保获取最新配置
# 从主配置文件导入路径和设置
TOOL_NAME = config.TOOL_NAME
TOOL_VERSION = config.TOOL_VERSION
TOOL_AUTHOR = config.TOOL_AUTHOR
TOOL_PATH = config.TOOL_PATH
SCRIPTS_PATH = config.SCRIPTS_PATH
UI_PATH = config.UI_PATH
STYLE_FILE = config.STYLE_FILE
ICONS_PATH = config.ICONS_PATH
ASSETS_PATH = config.ASSETS_PATH
DNA_FILE_PATH = config.DNA_FILE_PATH
print(f"成功从上级目录导入主配置文件")
return True
except ImportError as e:
print(f"从上级目录导入主配置文件失败: {str(e)}")
if parent_dir in sys.path:
sys.path.remove(parent_dir)
return False
# 尝试导入主配置
if not import_main_config():
print("警告: 无法导入主配置文件,使用默认配置")
# 使用默认路径
TOOL_NAME = "Delos"
TOOL_VERSION = "Alpha v1.0.0"
TOOL_AUTHOR = "Virtuos Games"
TOOL_PATH = TOOL_PATH
UI_PATH = os.path.join(SCRIPTS_PATH, 'ui')
STYLE_FILE = os.path.join(SCRIPTS_PATH, 'ui', 'style.qss')
ICONS_PATH = os.path.join(TOOL_PATH, 'icons')
ASSETS_PATH = os.path.join(TOOL_PATH, 'assets')
DNA_FILE_PATH = os.path.join(TOOL_PATH, 'assets', 'dna')
# 尝试导入Config模块
try:
from scripts.utils import Config
except ImportError:
try:
from utils import Config
except ImportError:
try:
import Config
except ImportError:
print("警告: 无法导入Config模块")
Config = None
# 创建默认Config对象
class DefaultConfig:
# 使用TOOL_PATH而不是ROOT_DIR
DNA_CONFIG = {
'DNA_PATH': os.path.join(TOOL_PATH, 'assets', 'dna'),
'DNA_FILE_PATH': os.path.join(TOOL_PATH, 'assets', 'dna', 'default.dna'),
'DNA_VERSION': 'DNAv4',
'LOD_LEVELS': [0, 1, 2, 3, 4],
'DEFAULT_MESH_INDICES': [0],
'GUI_PATH': os.path.join(TOOL_PATH, 'ui'),
'ASSEMBLE_SCRIPT': os.path.join(TOOL_PATH, 'scripts', 'assemble.py')
}
DNA_LIB_PATHS = [
os.path.join(TOOL_PATH, 'lib'),
os.path.join(TOOL_PATH, 'lib', 'windows' if sys.platform == 'win32' else 'linux')
]
Config = DefaultConfig()
# 尝试导入 Qt 模块
# 首先尝试使用项目中的 Qt.py 兼容层
try:
# 添加父目录到路径中以确保可以导入 Qt.py
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if parent_dir not in sys.path:
sys.path.append(parent_dir)
# 尝试导入 Qt.py
from Qt import QtWidgets, QtCore, QtGui
from Qt.QtCompat import wrapInstance
# 获取 Maya 主窗口
def getMayaMainWindow():
from maya import OpenMayaUI as omui
mainWindowPtr = omui.MQtUtil.mainWindow()
return wrapInstance(int(mainWindowPtr), QtWidgets.QWidget)
HAS_QT = True
print("Successfully imported Qt modules using Qt.py compatibility layer")
except ImportError:
# 如果无法导入 Qt.py尝试直接导入 PySide2/PySide6
try:
from PySide2 import QtWidgets, QtCore, QtGui
from shiboken2 import wrapInstance
from maya import OpenMayaUI as omui
def getMayaMainWindow():
mainWindowPtr = omui.MQtUtil.mainWindow()
return wrapInstance(int(mainWindowPtr), QtWidgets.QWidget)
HAS_QT = True
print("Successfully imported Qt modules using PySide2")
except ImportError:
try:
from PySide6 import QtWidgets, QtCore, QtGui
from shiboken6 import wrapInstance
from maya import OpenMayaUI as omui
def getMayaMainWindow():
mainWindowPtr = omui.MQtUtil.mainWindow()
return wrapInstance(int(mainWindowPtr), QtWidgets.QWidget)
HAS_QT = True
print("Successfully imported Qt modules using PySide6")
except ImportError:
# 如果所有导入尝试都失败,设置标志并显示警告
HAS_QT = False
cmds.warning("Failed to import Qt modules. UI functionality will be limited to command line only.")
def clean_pycache(root_dir):
"""Delete all __pycache__ directories and .pyc files under the given root directory
Args:
root_dir (str): Root directory to search for __pycache__ folders and .pyc files
Returns:
int: Number of __pycache__ directories and .pyc files removed
"""
count = 0
try:
# 首先确保目录存在
if not os.path.exists(root_dir) or not os.path.isdir(root_dir):
print(f"Warning: Directory does not exist or is not a directory: {root_dir}")
return count
print(f"Cleaning __pycache__ in directory: {root_dir}")
# 收集所有需要删除的路径,避免在遍历时修改目录结构
pycache_dirs = []
pyc_files = []
# 首先收集所有 __pycache__ 目录和 .pyc 文件
for root, dirs, files in os.walk(root_dir):
# 收集 __pycache__ 目录
if "__pycache__" in dirs:
pycache_path = os.path.join(root, "__pycache__")
pycache_dirs.append(pycache_path)
# 收集 .pyc 文件
for file in files:
if file.endswith(".pyc"):
file_path = os.path.join(root, file)
pyc_files.append(file_path)
# 优先使用批处理命令删除 __pycache__ 目录(更可靠)
if os.name == 'nt': # Windows
# 创建临时批处理文件
temp_bat = os.path.join(os.environ.get('TEMP', '.'), 'clean_pycache_temp.bat')
with open(temp_bat, 'w') as f:
f.write('@echo off\n')
# 添加删除 __pycache__ 目录的命令
for pycache_path in pycache_dirs:
f.write(f'if exist "{pycache_path}" rd /s /q "{pycache_path}"\n')
# 添加删除 .pyc 文件的命令
for file_path in pyc_files:
f.write(f'if exist "{file_path}" del /f /q "{file_path}"\n')
# 执行批处理文件
os.system(f'"{temp_bat}"')
# 删除临时批处理文件
try:
os.remove(temp_bat)
except:
pass
# 验证是否删除成功
remaining_pycache = 0
for pycache_path in pycache_dirs:
if not os.path.exists(pycache_path):
count += 1
else:
remaining_pycache += 1
remaining_pyc = 0
for file_path in pyc_files:
if not os.path.exists(file_path):
count += 1
else:
remaining_pyc += 1
if remaining_pycache > 0 or remaining_pyc > 0:
print(f"Warning: {remaining_pycache} __pycache__ directories and {remaining_pyc} .pyc files could not be removed")
else: # Unix/Linux
# 使用系统命令删除 __pycache__ 目录
for pycache_path in pycache_dirs:
if os.system(f'rm -rf "{pycache_path}"') == 0:
print(f"Removed __pycache__ directory: {pycache_path}")
count += 1
else:
print(f"Failed to remove {pycache_path}")
# 使用系统命令删除 .pyc 文件
for file_path in pyc_files:
if os.system(f'rm -f "{file_path}"') == 0:
print(f"Removed .pyc file: {file_path}")
count += 1
else:
print(f"Failed to remove {file_path}")
except Exception as error:
print(f"Error cleaning __pycache__ directories: {str(error)}")
print(f"Removed {count} files/directories: {len(pycache_dirs)} __pycache__ directories and {len(pyc_files)} .pyc files")
traceback.print_exc()
return count
class ModuleReloader(object):
"""Class for reloading modules in the Plugin"""
@staticmethod
def get_package_modules(package_name):
"""
Get all modules in a package
Args:
package_name (str): Name of the package
Returns:
list: List of module names
"""
package_modules = []
try:
# 尝试导入包
package = importlib.import_module(package_name)
package_path = os.path.dirname(package.__file__)
# 遍历包目录查找模块
for root, dirs, files in os.walk(package_path):
for file in files:
if file.endswith('.py') and file != '__init__.py':
# 计算相对路径
rel_path = os.path.relpath(os.path.join(root, file), package_path)
# 转换为模块路径格式
module_path = os.path.splitext(rel_path)[0].replace(os.sep, '.')
# 构建完整模块名
module_name = f"{package_name}.{module_path}"
package_modules.append(module_name)
# 添加子包
for dir_name in dirs:
init_file = os.path.join(root, dir_name, '__init__.py')
if os.path.exists(init_file):
rel_path = os.path.relpath(os.path.join(root, dir_name), package_path)
module_name = f"{package_name}.{rel_path.replace(os.sep, '.')}"
package_modules.append(module_name)
return package_modules
except Exception as e:
print(f"错误: 无法导入包 {package_name}: {str(e)}")
traceback.print_exc()
return []
@staticmethod
def reload_module(module_name):
"""Reload a specific module
Args:
module_name (str): Name of the module to reload
Returns:
bool: True if successful, False otherwise
"""
try:
# 尝试导入模块
try:
module = __import__(module_name, fromlist=["*"])
except ImportError:
# 如果失败,尝试将点替换为斜杠
module_path = module_name.replace('.', '/')
if os.path.exists(os.path.join(TOOL_PATH, module_path + '.py')):
sys_path_modified = False
if TOOL_PATH not in sys.path:
sys.path.insert(0, TOOL_PATH)
sys_path_modified = True
module = __import__(module_name, fromlist=["*"])
if sys_path_modified:
sys.path.remove(TOOL_PATH)
else:
raise ImportError(f"Module {module_name} not found")
# 重新加载模块
importlib.reload(module)
print(f"成功重新加载模块: {module_name}")
return True
except Exception as e:
print(f"重新加载模块 {module_name} 时出错: {str(e)}")
traceback.print_exc()
return False
@classmethod
def reload_all_modules(cls):
"""
Reload all Plugin modules
Returns:
dict: Results of reloading each module
"""
# 首先声明全局变量
global TOOL_NAME, TOOL_VERSION, TOOL_AUTHOR, TOOL_PATH, SCRIPTS_PATH
global UI_PATH, STYLE_FILE, ICONS_PATH, ASSETS_PATH, DNA_FILE_PATH
results = {}
# 清理缓存文件
clean_pycache(TOOL_PATH)
# 首先尝试重新加载主配置文件
try:
# 尝试直接导入主配置
if 'config' in sys.modules:
config_module = sys.modules['config']
importlib.reload(config_module)
print("成功重新加载主配置文件")
results['config'] = True
TOOL_NAME = config_module.TOOL_NAME
TOOL_VERSION = config_module.TOOL_VERSION
TOOL_AUTHOR = config_module.TOOL_AUTHOR
TOOL_PATH = config_module.TOOL_PATH
SCRIPTS_PATH = config_module.SCRIPTS_PATH
UI_PATH = config_module.UI_PATH
STYLE_FILE = config_module.STYLE_FILE
ICONS_PATH = config_module.ICONS_PATH
ASSETS_PATH = config_module.ASSETS_PATH
DNA_FILE_PATH = config_module.DNA_FILE_PATH
else:
# 尝试重新导入
import_main_config()
results['config'] = True
except Exception as e:
print(f"重新加载主配置文件时出错: {str(e)}")
results['config'] = False
# 定义要重新加载的模块
modules_to_reload = [
"scripts.utils.Config", # 首先重新加载配置
"scripts.utils",
"scripts.ui",
"scripts.Main"
]
# 获取所有子模块
all_modules = []
for module in modules_to_reload:
all_modules.append(module)
try:
submodules = cls.get_package_modules(module)
if submodules:
all_modules.extend(submodules)
except Exception as e:
print(f"获取模块 {module} 的子模块时出错: {str(e)}")
# 去除重复项并排序
all_modules = sorted(list(set(all_modules)))
# 按照依赖关系对模块进行排序
# 确保配置模块先重新加载
priority_modules = []
normal_modules = []
for module in all_modules:
if "Config" in module or module.endswith(".config"):
priority_modules.append(module)
else:
normal_modules.append(module)
# 先重新加载优先级模块,然后是普通模块
for module in priority_modules + normal_modules:
results[module] = cls.reload_module(module)
return results
def reload_all():
"""Reload all Plugin modules and display results"""
print("\n" + "-"*50)
print("Reloading Plugin modules...")
print("-"*50)
# 获取插件根目录 - 使用config中定义的TOOL_PATH
try:
import config
TOOL_PATH = config.TOOL_PATH
except ImportError:
# 如果无法导入config则使用相对路径
TOOL_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
print(f"Plugin root directory: {TOOL_PATH}")
# 清理 __pycache__ 目录
print("Cleaning __pycache__ directories...")
pycache_count = clean_pycache(TOOL_PATH)
print(f"Removed {pycache_count} __pycache__ directories and .pyc files")
# Reload modules
results = ModuleReloader.reload_all_modules()
success_count = sum(1 for success in results.values() if success)
total_count = len(results)
print("\nReload Summary:")
print(f"Successfully reloaded {success_count} of {total_count} modules")
if success_count < total_count:
print("\nFailed modules:")
for module, success in results.items():
if not success:
print(f" - {module}")
print("-"*50)
return results
def show_reload_ui():
"""Show a UI for reloading modules"""
# 检查是否有Qt模块可用
if not 'HAS_QT' in globals() or not HAS_QT:
cmds.warning("Qt modules not available. Falling back to command line reload.")
return reload_all()
try:
# 创建对话框
dialog = QtWidgets.QDialog(getMayaMainWindow())
dialog.setWindowTitle("Plugin Reload UI")
dialog.setMinimumWidth(450)
dialog.setWindowFlags(dialog.windowFlags() ^ QtCore.Qt.WindowContextHelpButtonHint)
# 创建布局
layout = QtWidgets.QVBoxLayout(dialog)
# 创建信息标签
info_label = QtWidgets.QLabel("Reload Plugin modules, no need to restart Maya.")
layout.addWidget(info_label)
# 创建模块选择区域
module_group = QtWidgets.QGroupBox("Select Modules to Reload")
module_layout = QtWidgets.QVBoxLayout(module_group)
# 创建全选复选框
all_modules_cb = QtWidgets.QCheckBox("All Modules")
all_modules_cb.setChecked(True)
module_layout.addWidget(all_modules_cb)
# 创建模块类别复选框
module_cbs = {}
module_categories = [
("Core Modules", [
"scripts",
"scripts.Main",
"scripts.ReloadModules"
]
),
("UI Modules", [
"scripts.ui",
"scripts.ui.toolbar",
"scripts.ui.geometry",
"scripts.ui.rigging",
"scripts.ui.behaviour",
"scripts.ui.definition"
]
),
("Utility Modules", [
"scripts.utils",
"scripts.utils.utils_toolbar",
"scripts.utils.utils_geometry",
"scripts.utils.utils_rigging",
"scripts.utils.utils_behaviour",
"scripts.utils.utils_definition"
]
),
("Config", ["config"])
]
for category, modules in module_categories:
category_cb = QtWidgets.QCheckBox(category)
category_cb.setChecked(True)
category_cb.setEnabled(False)
module_layout.addWidget(category_cb)
module_cbs[category] = (category_cb, modules)
# 全选/取消全选逻辑
def toggle_all_modules():
checked = all_modules_cb.isChecked()
for category, (cb, _) in module_cbs.items():
cb.setChecked(checked)
cb.setEnabled(not checked)
all_modules_cb.toggled.connect(toggle_all_modules)
layout.addWidget(module_group)
# 创建操作按钮区域
button_layout = QtWidgets.QHBoxLayout()
# 创建重载按钮
reload_button = QtWidgets.QPushButton("Reload Selected Modules")
reload_button.setMinimumHeight(30)
button_layout.addWidget(reload_button)
# 创建清理缓存按钮
clean_button = QtWidgets.QPushButton("Clean Caches Only")
clean_button.setMinimumHeight(30)
button_layout.addWidget(clean_button)
layout.addLayout(button_layout)
# 创建结果文本区域
results_text = QtWidgets.QTextEdit()
results_text.setReadOnly(True)
results_text.setMinimumHeight(300)
layout.addWidget(results_text)
# 创建关闭按钮
close_button = QtWidgets.QPushButton("Close")
close_button.setMinimumHeight(30)
close_button.clicked.connect(dialog.close)
layout.addWidget(close_button)
# 重载并更新UI的函数
def reload_and_update_ui(text_widget):
text_widget.clear()
text_widget.append("Reloading modules...\n")
# 获取插件根目录 - 使用config中定义的TOOL_PATH
try:
import config
TOOL_PATH = config.TOOL_PATH
except ImportError:
# 如果无法导入config则使用相对路径
TOOL_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
text_widget.append(f"Plugin root directory: {TOOL_PATH}")
# 清理 __pycache__ 目录
text_widget.append("Cleaning __pycache__ directories...")
pycache_count = clean_pycache(TOOL_PATH)
text_widget.append(f"Removed {pycache_count} __pycache__ directories and .pyc files\n")
# 确定要重载的模块
results = {}
if all_modules_cb.isChecked():
# 重载所有模块
results = ModuleReloader.reload_all_modules()
else:
# 重载选定的模块类别
selected_categories = []
for category, (cb, modules) in module_cbs.items():
if cb.isChecked():
selected_categories.extend(modules)
if not selected_categories:
text_widget.append("No module was selected, the operation was canceled.")
return
# 获取所有模块
all_modules = []
for package in selected_categories:
if package.startswith("scripts."):
all_modules.extend(ModuleReloader.get_package_modules(package))
else:
all_modules.append(package)
# 重载选定的模块
results = {}
for module in all_modules:
results[module] = ModuleReloader.reload_module(module)
# 更新UI显示结果
success_count = sum(1 for success in results.values() if success)
total_count = len(results)
text_widget.append(f"Successfully reloaded {success_count}/{total_count} modules\n")
# 显示成功重载的模块
text_widget.append("Successfully reloaded modules:")
for module, success in results.items():
if success:
text_widget.append(f" - {module}")
# 显示失败的模块(如果有)
if success_count < total_count:
text_widget.append("\nFailed to reload modules:")
for module, success in results.items():
if not success:
text_widget.append(f" - {module}")
# 提示用户重载完成
text_widget.append("\nReload operation completed!")
# 仅清理缓存的函数
def clean_cache_only(text_widget):
text_widget.clear()
text_widget.append("Cleaning caches only...\n")
# 获取插件根目录
try:
import config
TOOL_PATH = config.TOOL_PATH
except ImportError:
TOOL_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
text_widget.append(f"Plugin root directory: {TOOL_PATH}")
# 清理 __pycache__ 目录
pycache_count = clean_pycache(TOOL_PATH)
text_widget.append(f"Removed {pycache_count} __pycache__ directories and .pyc files")
text_widget.append("\nCache cleaning operation completed!")
# 连接按钮信号
reload_button.clicked.connect(lambda: reload_and_update_ui(results_text))
clean_button.clicked.connect(lambda: clean_cache_only(results_text))
# 显示对话框
dialog.show()
except Exception as e:
traceback.print_exc()
cmds.warning(f"Failed to show UI: {str(e)}. Falling back to command line reload.")
return reload_all()
# Main function to be called from Maya
def main():
"""Main function to be called from Maya"""
try:
if HAS_QT:
show_reload_ui()
else:
reload_all()
print("Modules reloaded successfully!")
return True
except Exception as e:
print(f"Error during module reload: {str(e)}")
traceback.print_exc()
return False
# Allow running this script directly
if __name__ == "__main__":
main()

32
scripts/__init__.py Normal file
View File

@@ -0,0 +1,32 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
MetaHuman DNA工具包
这个包包含了用于处理MetaHuman DNA文件的工具和实用程序。
"""
import os
import sys
import config
# 将主配置中的变量导入到当前模块的命名空间
TOOL_NAME = config.TOOL_NAME
TOOL_VERSION = config.TOOL_VERSION
TOOL_AUTHOR = config.TOOL_AUTHOR
TOOL_PATH = config.TOOL_PATH
SCRIPTS_PATH = config.SCRIPTS_PATH
UI_PATH = config.UI_PATH
STYLE_FILE = config.STYLE_FILE
ICONS_PATH = config.ICONS_PATH
ASSETS_PATH = config.ASSETS_PATH
DNA_FILE_PATH = config.DNA_FILE_PATH
# 确保项目路径在sys.path中
if TOOL_PATH not in sys.path:
sys.path.insert(0, TOOL_PATH)
# 确保scripts路径在sys.path中
if SCRIPTS_PATH not in sys.path:
sys.path.insert(0, SCRIPTS_PATH)

View File

@@ -0,0 +1,4 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from . import *

436
scripts/builder/builder.py Normal file
View File

@@ -0,0 +1,436 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import logging
import traceback
from dataclasses import dataclass, field
from pathlib import Path
from typing import Dict, List, Optional
from maya import cmds, mel
from ..builder.maya.util import Maya
from ..common import DNAViewerError
from ..dnalib.dnalib import DNA
from ..model import Joint as JointModel
from .config import AngleUnit, Config, LinearUnit
from .joint import Joint as JointBuilder
from .mesh import Mesh
@dataclass
class BuildResult:
"""
A class used for returning data after finishing the build process
Attributes
----------
@type meshes_per_lod: Dict[int, List[str]]
@param meshes_per_lod: The list of mesh names created group by LOD number
"""
meshes_per_lod: Dict[int, List[str]] = field(default_factory=dict)
def get_all_meshes(self) -> List[str]:
"""
Flatten meshes to single list.
@rtype: List[str]
@returns: The list of all mesh names.
"""
all_meshes = []
for meshes_per_lod in self.meshes_per_lod.values():
all_meshes.extend(meshes_per_lod)
return all_meshes
class Builder:
"""
A builder class used for building the character
Attributes
----------
@type config: Config
@param config: The configuration options used for building the character
@type dna: DNA
@param dna: The DNA object read from the DNA file
@type meshes: Dict[int, List[str]]
@param meshes: A list of meshes created grouped by lod
"""
def __init__(self, dna: DNA, config: Optional[Config] = None) -> None:
self.config = config or Config()
self.dna = dna
self.meshes: Dict[int, List[str]] = {}
self.all_loaded_meshes: List[int] = []
def _build(self) -> bool:
self.new_scene()
self.set_filtered_meshes()
if not self.all_loaded_meshes:
logging.error("No mashes has been loaded.")
return False
self.create_groups()
self.set_units()
self.add_joints()
self.build_meshes()
self.add_ctrl_attributes_on_root_joint()
self.add_animated_map_attributes_on_root_joint()
self.add_key_frames()
return True
def build(self) -> BuildResult:
"""Builds the character"""
self.meshes = {}
try:
filename = Path(self.dna.path).stem
logging.info("******************************")
logging.info(f"{filename} started building")
logging.info("******************************")
self._build()
logging.info(f"{filename} built successfully!")
except DNAViewerError as e:
traceback.print_exc()
raise e
except Exception as e:
traceback.print_exc()
logging.error(f"Unhandled exception, {e}")
raise DNAViewerError(f"Scene creation failed! Reason: {e}") from e
return BuildResult(meshes_per_lod=self.meshes)
def new_scene(self) -> None:
cmds.file(new=True, force=True)
def add_mesh_to_display_layer(self, mesh_name: str, lod: int) -> None:
"""
Add the mesh with the given name to an already created display layer.
@type mesh_name: str
@param mesh_name: The name of the mesh that should be added to a display layer.
@type lod: int
@param lod: The lod value, this is needed for determining the name of the display layer that the mesh should be added to.
"""
if self.config.create_display_layers:
cmds.editDisplayLayerMembers(
f"{self.config.top_level_group}_lod{lod}_layer", mesh_name
)
def _add_joints(self) -> List[JointModel]:
"""
Reads and adds the joints to the scene, also returns a list model objects of joints that were added.
@rtype: List[JointModel]
@returns: The list containing model objects representing the joints that were added to the scene.
"""
joints: List[JointModel] = self.dna.read_all_neutral_joints()
builder = JointBuilder(
joints,
)
builder.process()
return joints
def add_joints(self) -> None:
"""
Starts adding the joints the character, if the character configuration options have add_joints set to False,
this step will be skipped.
"""
if self.config.add_joints:
logging.info("adding joints to character...")
joints = self._add_joints()
if self.config.group_by_lod and joints:
cmds.parent(joints[0].name, self.config.get_top_level_group())
def create_groups(self) -> None:
"""
Creates a Maya transform which will hold the character, if the character configuration options have
create_character_node set to False, this step will be skipped.
"""
if self.config.group_by_lod:
logging.info("building character node...")
cmds.group(world=True, empty=True, name=self.config.get_top_level_group())
cmds.group(
parent=self.config.get_top_level_group(),
empty=True,
name=self.config.get_geometry_group(),
)
cmds.group(
parent=self.config.get_top_level_group(),
empty=True,
name=self.config.get_rig_group(),
)
for lod in self.get_display_layers():
name = f"{self.config.top_level_group}_lod{lod}_layer"
if not cmds.objExists(name):
if self.config.group_by_lod:
cmds.group(
parent=self.config.get_geometry_group(),
empty=True,
name=f"{self.config.top_level_group}_lod{lod}_grp",
)
cmds.select(
f"{self.config.top_level_group}_lod{lod}_grp",
replace=True,
)
if self.config.create_display_layers:
cmds.createDisplayLayer(name=name, noRecurse=True)
def attach_mesh_to_lod(self, mesh_name: str, lod: int) -> None:
"""
Attaches the mesh called mesh_name to a given lod.
@type mesh_name: str
@param mesh_name: The mesh that needs to be attached to a lod holder object.
@type lod: str
@param lod: The name of the mesh that should be added to a display layer.
"""
if self.config.group_by_lod:
parent_node = f"{self.config.get_top_level_group()}|{self.config.get_geometry_group()}|{self.config.top_level_group}_lod{lod}_grp"
cmds.parent(
self.get_mesh_node_fullpath_on_root(mesh_name=mesh_name), parent_node
)
def get_mesh_node_fullpath_on_root(self, mesh_name: str) -> str:
"""
Gets the full path in the scene of a mesh.
@type mesh_name: str
@param mesh_name: The mesh thats path is needed.
@rtype: str
@returns: The full path of the mesh object in the scene
"""
return str(Maya.get_element(f"|{mesh_name}").fullPathName())
def add_ctrl_attributes_on_root_joint(self) -> None:
"""
Adds and sets the raw gui control attributes on root joint.
"""
if self.config.add_ctrl_attributes_on_root_joint and self.config.add_joints:
gui_control_names = self.dna.get_raw_control_names()
for name in gui_control_names:
ctrl_and_attr_names = name.split(".")
self.add_attribute(
control_name=self.config.facial_root_joint_name,
long_name=ctrl_and_attr_names[1],
)
def add_animated_map_attributes_on_root_joint(self) -> None:
"""
Adds and sets the animated map attributes on root joint.
"""
if (
self.config.add_animated_map_attributes_on_root_joint
and self.config.add_joints
):
names = self.dna.get_animated_map_names()
for name in names:
long_name = name.replace(".", "_")
self.add_attribute(
control_name=self.config.facial_root_joint_name, long_name=long_name
)
def add_attribute(self, control_name: str, long_name: str) -> None:
"""
Adds attributes wrapper for internal usage.
"""
cmds.addAttr(
control_name,
longName=long_name,
keyable=True,
attributeType="float",
minValue=0.0,
maxValue=1.0,
)
def add_key_frames(self) -> None:
"""
Adds a starting key frame to the facial root joint if joints are added and the add_key_frames option is set
to True.
"""
if self.config.add_key_frames and self.config.add_joints:
logging.info("setting keyframe on the root joint...")
cmds.currentTime(0)
if cmds.objExists(self.config.facial_root_joint_name):
cmds.select(self.config.facial_root_joint_name, replace=True)
cmds.setKeyframe(inTangentType="linear", outTangentType="linear")
def set_filtered_meshes(self) -> None:
self.all_loaded_meshes = self.get_filtered_meshes()
def get_mesh_indices_filter(self) -> List[int]:
indices = []
for index in range(self.dna.get_mesh_count()):
mesh_name = self.dna.get_mesh_name(index)
for cur_filter in self.config.mesh_filter:
if cur_filter in mesh_name:
indices.append(index)
return indices
def get_filtered_meshes(self) -> List[int]:
if not self.config.mesh_filter and not self.config.lod_filter:
if self.config.meshes:
return self.config.meshes
return list(range(self.dna.get_mesh_count()))
meshes: List[int] = []
meshes_by_lod = self.dna.get_all_meshes_grouped_by_lod()
all_meshes = [mesh_index for meshes in meshes_by_lod for mesh_index in meshes]
mesh_indices_filter = self.get_mesh_indices_filter()
if self.config.lod_filter:
for lod in self.config.lod_filter:
if 0 <= lod < len(meshes_by_lod):
meshes.extend(meshes_by_lod[lod])
if mesh_indices_filter:
return list(set(meshes) & set(mesh_indices_filter))
return meshes
if self.config.mesh_filter:
return list(set(all_meshes) & set(mesh_indices_filter))
return all_meshes
def build_meshes(self) -> None:
"""
Builds the meshes. If specified in the config they get parented to a created
character node transform, otherwise the meshes get put to the root level of the scene.
"""
logging.info("adding character meshes...")
self.meshes = {}
for lod, meshes_per_lod in enumerate(
self.dna.get_meshes_by_lods(self.all_loaded_meshes)
):
self.meshes[lod] = self.build_meshes_by_lod(
lod=lod, meshes_per_lod=meshes_per_lod
)
def build_meshes_by_lod(self, lod: int, meshes_per_lod: List[int]) -> List[str]:
"""
Builds the meshes from the provided mesh ids and then attaches them to a given lod if specified in the
character configuration.
@type lod: int
@param lod: The lod number representing the display layer the meshes to the display layer.
@type meshes_per_lod: List[int]
@param meshes_per_lod: List of mesh indices that are being built.
@rtype: List[MObject]
@returns: The list of maya objects that represent the meshes added to the scene.
"""
meshes: List[str] = []
for mesh_index in meshes_per_lod:
builder = Mesh(
config=self.config,
dna=self.dna,
mesh_index=mesh_index,
)
builder.build()
mesh_name = self.dna.get_mesh_name(index=mesh_index)
meshes.append(mesh_name)
self.add_mesh_to_display_layer(mesh_name, lod)
self.attach_mesh_to_lod(mesh_name, lod)
self.default_lambert_shader(mesh_name)
return meshes
def default_lambert_shader(self, mesh_name: str) -> None:
try:
if self.config.group_by_lod:
names = cmds.ls(f"*|{mesh_name}", l=True)
for item in names:
if item.startswith(f"|{self.config.get_top_level_group()}"):
cmds.select(item, r=True)
break
else:
cmds.select(mesh_name, r=True)
mel.eval("sets -e -forceElement initialShadingGroup")
except Exception as e:
logging.error(
f"Couldn't set lambert shader for mesh {mesh_name}. Reason: {e}"
)
raise DNAViewerError(e) from e
def set_units(self) -> None:
"""Sets the translation and rotation units of the scene from @config"""
linear_unit = self.get_linear_unit()
angle_unit = self.get_angle_unit()
cmds.currentUnit(linear=linear_unit.name, angle=angle_unit.name)
def get_linear_unit(self) -> LinearUnit:
return self.get_linear_unit_from_int(self.dna.get_translation_unit())
def get_angle_unit(self) -> AngleUnit:
return self.get_angle_unit_from_int(self.dna.get_rotation_unit())
def get_linear_unit_from_int(self, value: int) -> LinearUnit:
"""
Returns an enum from an int value.
0 -> cm
1 -> m
@type value: int
@param value: The value that the enum is mapped to.
@rtype: LinearUnit
@returns: LinearUnit.cm or LinearUnit.m
"""
if value == 0:
return LinearUnit.cm
if value == 1:
return LinearUnit.m
raise DNAViewerError(f"Unknown linear unit set in DNA file! value {value}")
def get_angle_unit_from_int(self, value: int) -> AngleUnit:
"""
Returns an enum from an int value.
0 -> degree
1 -> radian
@type value: int
@param value: The value that the enum is mapped to.
@rtype: AngleUnit
@returns: AngleUnit.degree or AngleUnit.radian
"""
if value == 0:
return AngleUnit.degree
if value == 1:
return AngleUnit.radian
raise DNAViewerError(f"Unknown angle unit set in DNA file! value {value}")
def get_display_layers(self) -> List[int]:
"""Gets a lod id list that need to be created for the meshes from @config"""
meshes: List[int] = []
for idx, meshes_per_lod in enumerate(
self.dna.get_meshes_by_lods(self.all_loaded_meshes)
):
if meshes_per_lod:
meshes.append(idx)
return list(set(meshes))

260
scripts/builder/config.py Normal file
View File

@@ -0,0 +1,260 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Dict, List, Optional
class LinearUnit(Enum):
"""
An enum used to represent the unit used for linear representation.
Attributes
----------
@cm: using cm as unit
@m: using m as unit
"""
cm = 0
m = 1
class AngleUnit(Enum):
"""
An enum used to represent the unit used for angle representation.
Attributes
----------
@degree: using degree as unit
@radian: using radian as unit
"""
degree = 0
radian = 1
@dataclass
class Config:
"""
A class used to represent the config for @Builder
Attributes
----------
@type mesh_filter: List[str]
@param mesh_filter: List of mesh names that should be filtered. Mash names can be just substrings. ["head"] will find all meshes that contins string "head" in its mash name.
@type lod_filter: List[int]
@param lod_filter: List of lods that should be filtered.
@type group_by_lod: bool
@param group_by_lod: A flag representing whether the character should be parented to a character transform node in the scene hierarchy
@type group_by_lod: bool
@param group_by_lod: A flag representing whether the character should be parented to a character transform node in rig hierarchy
@type top_level_group: str
@param top_level_group: Value that is going to be used when creating root group
@type geometry_group: str
@param geometry_group: Value that is going to be used when creating group that contains geometry
@type facial_root_joint_name: str
@param facial_root_joint_name: The name of the facial root joint
@type blend_shape_group_prefix: str
@param blend_shape_group_prefix: prefix string for blend shape group
@type blend_shape_name_postfix: str
@param blend_shape_name_postfix: postfix string for blend shape name
@type skin_cluster_suffix: str
@param skin_cluster_suffix: postfix string for skin cluster name
@type animated_map_attribute_multipliers_name: str
@param animated_map_attribute_multipliers_name: string for frame animated map attribute name
@type create_display_layers: bool
@param create_display_layers: A flag representing whether the created meshes should be assigned to a display layer
@type add_joints: bool
@param add_joints: A flag representing whether joints should be added
@type add_blend_shapes: bool
@param add_blend_shapes: A flag representing whether blend shapes should be added
@type add_skin_cluster: bool
@param add_skin_cluster: A flag representing whether skin should be added
@type add_ctrl_attributes_on_root_joint: bool
@param add_ctrl_attributes_on_root_joint: A flag representing whether control attributes should be added to the root joint
@type add_animated_map_attributes_on_root_joint: bool
@param add_animated_map_attributes_on_root_joint: A flag representing whether animated map attributes should be added to the root joint
@type add_key_frames: bool
@param add_key_frames: A flag representing whether key frames should be added
@type add_mesh_name_to_blend_shape_channel_name: bool
@param add_mesh_name_to_blend_shape_channel_name: A flag representing whether mesh name of blend shape channel is added to name when creating it
"""
meshes: List[int] = field(default_factory=list)
mesh_filter: List[str] = field(default_factory=list)
lod_filter: List[int] = field(default_factory=list)
group_by_lod: bool = field(default=True)
top_level_group: str = "head"
geometry_group: str = "geometry"
facial_root_joint_name: str = "FACIAL_C_FacialRoot"
blend_shape_group_prefix: str = "BlendshapeGroup_"
blend_shape_name_postfix: str = "_blendShapes"
skin_cluster_suffix: str = "skinCluster"
animated_map_attribute_multipliers_name = "FRM_WMmultipliers"
create_display_layers: bool = field(default=True)
add_joints: bool = field(default=True)
add_blend_shapes: bool = field(default=True)
add_skin_cluster: bool = field(default=True)
add_ctrl_attributes_on_root_joint: bool = field(default=True)
add_animated_map_attributes_on_root_joint: bool = field(default=True)
add_key_frames: bool = field(default=True)
add_mesh_name_to_blend_shape_channel_name: bool = field(default=True)
def get_top_level_group(self) -> str:
return f"{self.top_level_group}_grp"
def get_geometry_group(self) -> str:
return f"{self.geometry_group}_grp"
def get_rig_group(self) -> str:
return f"{self.top_level_group}Rig_grp"
@dataclass
class RigConfig(Config):
"""
A class used to represent the config for @RigBuilder
@type add_rig_logic: bool
@param add_rig_logic: A flag representing whether normals should be added
@type rig_logic_command: str
@param rig_logic_command: The command used to start creating the rig logic using the plugin
@type rig_logic_name: str
@param rig_logic_name: The name of the rig logic node
@type control_naming: str
@param control_naming: The naming pattern of controls
@type joint_naming: str
@param joint_naming: The naming pattern of joints
@type blend_shape_naming: str
@param blend_shape_naming: The naming pattern of blend shapes
@type animated_map_naming: str
@param animated_map_naming: The naming pattern of animated maps
@type gui_path: str
@param gui_path: The location of the gui file
@type left_eye_joint_name: str
@param left_eye_joint_name: The name of the left eye joint
@type eye_gui_name: str
@param eye_gui_name: The name of the control in the gui
@type gui_translate_x: float
@param gui_translate_x: Represents the value that the gui should be additionally translated on the X axis
@type analog_gui_path: str
@param analog_gui_path: The location of the analog gui file
@type left_eye_joint_name: str
@param left_eye_joint_name: The name of the left eye joint
@type right_eye_joint_name: str
@param right_eye_joint_name: The name of the right eye joint
@type central_driver_name: str
@param central_driver_name: The name of the central driver
@type left_eye_driver_name: str
@param left_eye_driver_name: The name of the left eye driver
@type right_eye_driver_name: str
@param right_eye_driver_name: The name of the right eye driver
@type central_aim: str
@param central_aim: The name of the central aim
@type le_aim: str
@param le_aim: The name of the left eye aim
@type re_aim: str
@param re_aim: The name of the right eye aim
@type aas_path: Optional[str]
@param aas_path: The location of the script file
@type aas_method: str
@param aas_method: The method that should be called
@type aas_parameter: Dict[Any, Any]
@param aas_parameter: The parameters that will be passed as the method arguments
"""
add_rig_logic: bool = field(default=True)
rig_logic_command: str = field(default="createEmbeddedNodeRL4")
rig_logic_name: str = field(default="")
control_naming: str = field(default="<objName>.<attrName>")
joint_naming: str = field(default="<objName>.<attrName>")
blend_shape_naming: str = field(default="")
animated_map_naming: str = field(default="")
gui_path: str = field(default=None)
eye_gui_name: str = "CTRL_C_eye"
gui_translate_x: float = 10
analog_gui_path: str = field(default=None)
left_eye_joint_name: str = "FACIAL_L_Eye"
right_eye_joint_name: str = "FACIAL_R_Eye"
central_driver_name: str = "LOC_C_eyeDriver"
left_eye_driver_name: str = "LOC_L_eyeDriver"
right_eye_driver_name: str = "LOC_R_eyeDriver"
left_eye_aim_up_name: str = "LOC_L_eyeAimUp"
right_eye_aim_up_name: str = "LOC_R_eyeAimUp"
central_aim: str = "GRP_C_eyesAim"
le_aim: str = "GRP_L_eyeAim"
re_aim: str = "GRP_R_eyeAim"
aas_path: Optional[str] = field(default=None)
aas_method: str = "run_after_assemble"
aas_parameter: Dict[Any, Any] = field(default_factory=dict)
def __post_init__(self) -> None:
if self.add_mesh_name_to_blend_shape_channel_name:
self.blend_shape_naming = (
f"<objName>{self.blend_shape_name_postfix}.<objName>__<attrName>"
)
else:
self.blend_shape_naming = (
f"<objName>{self.blend_shape_name_postfix}.<attrName>"
)
self.animated_map_naming = (
f"{self.animated_map_attribute_multipliers_name}.<objName>_<attrName>"
)

81
scripts/builder/joint.py Normal file
View File

@@ -0,0 +1,81 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from typing import Dict, List
from maya import cmds
from ..model import Joint as JointModel
class Joint:
"""
A builder class used for adding joints to the scene
Attributes
----------
@type joints: List[JointModel]
@param joints: data representing the joints
@type joint_flags: Dict[str, bool]
@param joint_flags: A mapping used for setting flags that are used to avoid adding the same joint multiple times
"""
def __init__(self, joints: List[JointModel]) -> None:
self.joints = joints
self.joint_flags: Dict[str, bool] = {}
for joint in self.joints:
self.joint_flags[joint.name] = False
def add_joint_to_scene(self, joint: JointModel) -> None:
"""
Adds the given joint to the scene
@type joint: JointModel
@param joint: The joint to be added to the scene
"""
if self.joint_flags[joint.name]:
return
in_parent_space = True
if cmds.objExists(joint.parent_name):
cmds.select(joint.parent_name)
else:
if joint.name != joint.parent_name:
parent_joint = next(
j for j in self.joints if j.name == joint.parent_name
)
self.add_joint_to_scene(parent_joint)
else:
# this is the first node
cmds.select(d=True)
in_parent_space = False
position = (
joint.translation.x,
joint.translation.y,
joint.translation.z,
)
orientation = (
joint.orientation.x,
joint.orientation.y,
joint.orientation.z,
)
cmds.joint(
p=position,
o=orientation,
n=joint.name,
r=in_parent_space,
a=not in_parent_space,
scaleCompensate=False,
)
self.joint_flags[joint.name] = True
def process(self) -> None:
"""Starts adding all the provided joints to the scene"""
for joint in self.joints:
self.add_joint_to_scene(joint)

View File

@@ -0,0 +1,4 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from . import *

View File

@@ -0,0 +1,424 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import logging
from dataclasses import dataclass, field
from typing import List, Tuple
from maya import cmds
from maya.api.OpenMaya import MDagModifier, MFnDagNode, MFnMesh, MObject, MPoint
from ...builder.maya.util import Maya
from ...common import SKIN_WEIGHT_PRINT_RANGE
from ...dnalib.dnalib import DNA
from ...model import Point3
@dataclass
class Mesh:
"""
A model class for holding data needed in the mesh building process
Attributes
----------
@type dna_vertex_positions: List[Point3]
@param dna_vertex_positions: Data representing the positions of the vertices
@type dna_vertex_layout_positions: List[int]
@param dna_vertex_layout_positions: Data representing layout position indices of vertices
@type polygon_faces: List[int]
@param polygon_faces: List of lengths of vertex layout indices
@type polygon_connects: List[int]
@param polygon_connects: List of vertex layout position indices
@type derived_mesh_names: List[str]
@param derived_mesh_names: List of mesh names
"""
dna_vertex_positions: List[Point3] = field(default_factory=list)
dna_vertex_layout_positions: List[int] = field(default_factory=list)
polygon_faces: List[int] = field(default_factory=list)
polygon_connects: List[int] = field(default_factory=list)
derived_mesh_names: List[str] = field(default_factory=list)
class MayaMesh:
"""
A builder class used for adding joints to the scene
Attributes
----------
@type mesh_index: int
@param mesh_index: The index of the mesh
@type dna: DNA
@param dna: Instance of DNA
@type blend_shape_group_prefix: str
@param blend_shape_group_prefix: prefix string for blend shape group
@type blend_shape_name_postfix: str
@param blend_shape_name_postfix: postfix string for blend shape name
@type skin_cluster_suffix: str
@param skin_cluster_suffix: postfix string for skin cluster name
@type data: Mesh
@param data: mesh data used in the mesh creation process
@type fn_mesh: om.MFnMesh
@param fn_mesh: OpenMaya class used for creating the mesh
@type mesh_object: om.MObject
@param mesh_object: the object representing the mesh
@type dag_modifier: om.MDagModifier
@param dag_modifier: OpenMaya class used for naming the mesh
"""
def __init__(
self,
mesh_index: int,
dna: DNA,
blend_shape_group_prefix: str,
blend_shape_name_postfix: str,
skin_cluster_suffix: str,
) -> None:
self.mesh_index = mesh_index
self.data: Mesh = Mesh()
self.fn_mesh = MFnMesh()
self.mesh_object: MObject = None
self.dag_modifier: MDagModifier = None
self.dna = dna
self.blend_shape_group_prefix = blend_shape_group_prefix
self.blend_shape_name_postfix = blend_shape_name_postfix
self.skin_cluster_suffix = skin_cluster_suffix
def create_neutral_mesh(self) -> MObject:
"""
Creates the neutral mesh using the config provided for this builder class object
@rtype: om.MObject
@returns: the instance of the created mesh object
"""
self.prepare_mesh()
self.mesh_object = self.create_mesh_object()
self.dag_modifier = self.rename_mesh()
self.add_texture_coordinates()
return self.mesh_object
def create_mesh_object(self) -> MObject:
"""
Gets a list of points that represent the vertex positions.
@rtype: MObject
@returns: Maya objects representing maya mesh functions and the created maya mesh object.
"""
mesh_object = self.fn_mesh.create(
self.get_vertex_positions_from_dna_vertex_positions(),
self.data.polygon_faces,
self.data.polygon_connects,
)
return mesh_object
def get_vertex_positions_from_dna_vertex_positions(self) -> List[MPoint]:
"""
Gets a list of points that represent the vertex positions.
@rtype: List[MPoint]
@returns: List of maya point objects.
"""
vertex_positions = []
for position in self.data.dna_vertex_positions:
vertex_positions.append(
MPoint(
position.x,
position.y,
position.z,
)
)
return vertex_positions
def rename_mesh(self) -> MDagModifier:
"""
Renames the initial mesh object that was created to the name from the configuration.
@rtype: Tuple[MDagModifier]
@returns: Maya object representing the dag modifier.
"""
mesh_name = self.dna.get_mesh_name(self.mesh_index)
dag_modifier = MDagModifier()
dag_modifier.renameNode(self.mesh_object, mesh_name)
dag_modifier.doIt()
return dag_modifier
def prepare_mesh(self) -> None:
"""
Gets a list of points that represent the vertex positions.
"""
logging.info("==============================")
mesh_name = self.dna.get_mesh_name(self.mesh_index)
logging.info(f"adding mesh: {mesh_name}")
self.data.dna_vertex_positions = self.dna.get_vertex_positions_for_mesh_index(
self.mesh_index
)
self.data.dna_vertex_layout_positions = (
self.dna.get_vertex_layout_positions_for_mesh_index(self.mesh_index)
)
(
self.data.polygon_faces,
self.data.polygon_connects,
) = self.dna.get_polygon_faces_and_connects(self.mesh_index)
def add_texture_coordinates(self) -> None:
"""
Method for adding texture coordinates.
"""
logging.info("adding texture coordinates...")
(
texture_coordinate_us,
texture_coordinate_vs,
texture_coordinate_indices,
) = self.get_texture_data()
self.fn_mesh.setUVs(texture_coordinate_us, texture_coordinate_vs)
self.fn_mesh.assignUVs(self.data.polygon_faces, texture_coordinate_indices)
mesh_name = self.dna.get_mesh_name(self.mesh_index)
cmds.select(mesh_name, replace=True)
cmds.polyMergeUV(mesh_name, distance=0.01, constructionHistory=False)
def get_texture_data(self) -> Tuple[List[float], List[float], List[int]]:
"""
Gets the data needed for the creation of textures.
@rtype: Tuple[List[float], List[float], List[int]] @returns: The tuple containing the list of texture
coordinate Us, the list of texture coordinate Vs and the list of texture coordinate indices.
"""
texture_coordinates = self.dna.get_vertex_texture_coordinates_for_mesh(
self.mesh_index
)
dna_faces = self.dna.get_faces(self.mesh_index)
coordinate_indices = []
for layout_id in range(
len(self.dna.get_layouts_for_mesh_index(self.mesh_index))
):
coordinate_indices.append(
self.dna.get_texture_coordinate_index(self.mesh_index, layout_id)
)
texture_coordinate_us = []
texture_coordinate_vs = []
texture_coordinate_indices = []
index_counter = 0
for vertices_layout_index_array in dna_faces:
for vertex_layout_index_array in vertices_layout_index_array:
texture_coordinate = texture_coordinates[
coordinate_indices[vertex_layout_index_array]
]
texture_coordinate_us.append(texture_coordinate.u)
texture_coordinate_vs.append(texture_coordinate.v)
texture_coordinate_indices.append(index_counter)
index_counter += 1
return texture_coordinate_us, texture_coordinate_vs, texture_coordinate_indices
def add_blend_shapes(self, add_mesh_name_to_blend_shape_channel_name: bool) -> None:
"""Adds blend shapes to the mesh"""
if self.dna.has_blend_shapes(self.mesh_index):
self.create_blend_shapes(add_mesh_name_to_blend_shape_channel_name)
self.create_blend_shape_node()
def create_blend_shape_node(self) -> None:
"""
Creates a blend shape node.
"""
mesh_name = self.dna.get_mesh_name(self.mesh_index)
nodes = []
for derived_mesh_name in self.data.derived_mesh_names:
nodes.append(derived_mesh_name)
cmds.select(nodes, replace=True)
cmds.select(mesh_name, add=True)
cmds.blendShape(name=f"{mesh_name}{self.blend_shape_name_postfix}")
cmds.delete(f"{self.blend_shape_group_prefix}{mesh_name}")
def create_blend_shapes(
self, add_mesh_name_to_blend_shape_channel_name: bool
) -> None:
"""
Builds all the derived meshes using the provided mesh and the blend shapes data of the DNA.
@type add_mesh_name_to_blend_shape_channel_name: bool
@param add_mesh_name_to_blend_shape_channel_name: A flag representing whether mesh name of blend shape channel is added to name when creating it
"""
logging.info("adding derived meshes...")
group: str = cmds.group(
empty=True,
name=f"{self.blend_shape_group_prefix}{self.dna.get_mesh_name(self.mesh_index)}",
)
self.data.derived_mesh_names = []
blend_shapes = self.dna.get_blend_shapes(self.mesh_index)
for blend_shape_target_index, blend_shape in enumerate(blend_shapes):
self.create_blend_shape(
blend_shape_target_index,
blend_shape.channel,
group,
add_mesh_name_to_blend_shape_channel_name,
)
cmds.setAttr(f"{group}.visibility", 0)
def create_blend_shape(
self,
blend_shape_target_index: int,
blend_shape_channel: int,
group: str,
add_mesh_name_to_blend_shape_channel_name: bool,
) -> None:
"""
Builds a single derived mesh using the provided mesh and the blend shape data of the DNA.
@type blend_shape_target_index: int
@param blend_shape_target_index: Used for getting a delta value representing the value change concerning the blend shape.
@type blend_shape_channel: int
@param blend_shape_channel: Used for getting the blend shape name from the DNA.
@type group: str
@param group: The transform the new meshes will be added to.
@type add_mesh_name_to_blend_shape_channel_name: bool
@param add_mesh_name_to_blend_shape_channel_name: A flag representing whether mesh name of blend shape channel is added to name when creating it
"""
new_vert_layout = self.get_vertex_positions_from_dna_vertex_positions()
zipped_deltas = self.dna.get_blend_shape_target_deltas_with_vertex_id(
self.mesh_index, blend_shape_target_index
)
for zipped_delta in zipped_deltas:
delta: Point3 = zipped_delta[1]
new_vert_layout[zipped_delta[0]] += MPoint(
delta.x,
delta.y,
delta.z,
)
new_mesh = self.fn_mesh.create(
new_vert_layout, self.data.polygon_faces, self.data.polygon_connects
)
derived_name = self.dna.get_blend_shape_channel_name(blend_shape_channel)
name = (
f"{self.dna.geometry_meshes[self.mesh_index].name}__{derived_name}"
if add_mesh_name_to_blend_shape_channel_name
else derived_name
)
self.dag_modifier.renameNode(new_mesh, name)
self.dag_modifier.doIt()
dag = MFnDagNode(Maya.get_element(group))
dag.addChild(new_mesh)
self.data.derived_mesh_names.append(name)
def add_skin_cluster(self, joint_names: List[str], joint_ids: List[int]) -> None:
"""
Adds skin cluster to the mesh
@type joint_names: List[str]
@param joint_names: Joint names needed for adding the skin cluster
@type joint_ids: List[int]
@param joint_ids: Joint indices needed for setting skin weights
"""
mesh_name = self.dna.get_mesh_name(self.mesh_index)
self._add_skin_cluster(mesh_name, joint_names)
self.set_skin_weights(mesh_name, joint_ids)
def _add_skin_cluster(self, mesh_name: str, joint_names: List[str]) -> None:
"""
Creates a skin cluster object.
@type mesh_name: str
@param mesh_name: The mesh name that is used for skin cluster naming.
@type joints: List[Joint]
@param joints: List of joints used for adding the skin cluster.
"""
logging.info("adding skin cluster...")
maximum_influences = self.dna.get_maximum_influence_per_vertex(self.mesh_index)
cmds.select(joint_names[0], replace=True)
cmds.select(mesh_name, add=True)
skin_cluster = cmds.skinCluster(
toSelectedBones=True,
name=f"{mesh_name}_{self.skin_cluster_suffix}",
maximumInfluences=maximum_influences,
skinMethod=0,
obeyMaxInfluences=True,
)
cmds.skinCluster(
skin_cluster, edit=True, addInfluence=joint_names[1:], weight=0
)
def set_skin_weights(self, mesh_name: str, joint_ids: List[int]) -> None:
"""
Sets the skin weights attributes.
@type mesh_name: str
@param mesh_name: The mesh name that is used for getting the skin cluster name.
@type joint_ids: List[int]
@param joint_ids: List of joint indices used for setting the skin weight attribute.
"""
logging.info("adding skin weights...")
skin_weights = self.dna.get_skin_weight_matrix_for_mesh(self.mesh_index)
# import skin weights
temp_str = f"{mesh_name}_{self.skin_cluster_suffix}.wl["
for vertex_id, skin_weight in enumerate(skin_weights):
if not (vertex_id + 1) % SKIN_WEIGHT_PRINT_RANGE:
logging.info(f"\t{vertex_id + 1} / {len(skin_weights)}")
vertex_infos = skin_weight
# set all skin weights to zero
vertex_string = f"{temp_str}{str(vertex_id)}].w["
cmds.setAttr(f"{vertex_string}0]", 0.0)
# import skin weights
for vertex_info in vertex_infos:
cmds.setAttr(
f"{vertex_string}{str(joint_ids.index(vertex_info[0]))}]",
float(vertex_info[1]),
)
if len(skin_weights) % SKIN_WEIGHT_PRINT_RANGE != 0:
logging.info(f"\t{len(skin_weights)} / {len(skin_weights)}")

View File

@@ -0,0 +1,204 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import logging
from typing import List, Tuple, Union
from maya import cmds, mel
from maya.api.OpenMaya import MFnMesh, MGlobal
from maya.api.OpenMayaAnim import MFnSkinCluster
from ...builder.maya.util import Maya
from ...common import DNAViewerError
class MayaSkinWeights:
"""
A class used for reading and storing skin weight related data needed for adding skin clusters
"""
no_of_influences: int
skinning_method: int
joints: List[str]
vertices_info: List[List[Union[int, float]]]
def __init__(self, skin_cluster: MFnSkinCluster, mesh_name: str) -> None:
self.no_of_influences = cmds.skinCluster(skin_cluster.name(), q=True, mi=True)
self.skinning_method = cmds.skinCluster(skin_cluster.name(), q=True, sm=True)
self.joints = self.get_skin_cluster_influence(skin_cluster)
self.vertices_info = self.get_skin_weights_for_mesh_name(
skin_cluster, mesh_name
)
def get_skin_cluster_influence(self, skin_cluster: MFnSkinCluster) -> List[str]:
"""
Gets a list of joint names that are influences to the skin cluster.
@type skin_cluster: MFnSkinCluster
@param skin_cluster: The functionalities of a maya skin cluster object
@rtype: List[str]
@returns: The list if names of the joints that influence the skin cluster
"""
influences: List[str] = cmds.skinCluster(skin_cluster.name(), q=True, inf=True)
if influences and not isinstance(influences[0], str):
influences = [obj.name() for obj in influences]
return influences
def get_skin_weights_for_mesh_name(
self,
skin_cluster: MFnSkinCluster,
mesh_name: str,
) -> List[List[Union[int, float]]]:
"""
Gets the skin weights concerning the given mesh.
@type skin_cluster: MFnSkinCluster
@param skin_cluster: The functionalities of a maya skin cluster object
@type mesh_name: str
@param mesh_name: The name of the mesh
@rtype: List[List[Union[int, float]]]
@returns: A list of list of weight indices and the weight values
"""
mesh = Maya.get_element(mesh_name)
components = MGlobal.getSelectionListByName(f"{mesh_name}.vtx[*]").getComponent(
0
)[1]
weights_data, chunk = skin_cluster.getWeights(mesh, components)
iterator = [
weights_data[i : i + chunk] for i in range(0, len(weights_data), chunk)
]
vertices_info = []
for weights in iterator:
vertex_weights: List[float] = []
vertices_info.append(vertex_weights)
for i, weight in enumerate(weights):
if weight:
vertex_weights.append(i)
vertex_weights.append(weight)
return vertices_info
def get_skin_weights_data(mesh_name: str) -> Tuple[MFnMesh, MFnSkinCluster]:
"""
Gets the maya objects that manipulate the mesh node and the skin cluster for a given mesh name.
@type mesh_name: str
@param mesh_name: The name of the mesh
@rtype: Tuple[MFnMesh, MFnSkinCluster]
@returns: The maya object that manipulate the mesh node and the skin cluster for a given mesh name.
"""
skin_cluster_name = mel.eval(f"findRelatedSkinCluster {mesh_name}")
if skin_cluster_name:
skin_cluster = MFnSkinCluster(Maya.get_element(skin_cluster_name))
mesh_node = MFnMesh(Maya.get_element(mesh_name))
return mesh_node, skin_cluster
raise DNAViewerError(f"Unable to find skin for given mesh: {mesh_name}")
def get_skin_weights_from_scene(mesh_name: str) -> MayaSkinWeights:
"""
Gets the instance of this class filled with data from the scene for a given mesh name.
@type mesh_name: str
@param mesh_name: The mesh name
@rtype: MayaSkinWeights
@returns: An instance of this class with the data from the scene
"""
_, skin_cluster = get_skin_weights_data(mesh_name)
return MayaSkinWeights(skin_cluster, mesh_name)
def get_file_joint_mappings(
skin_weights: MayaSkinWeights, skin_cluster: MFnSkinCluster
) -> List[int]:
"""
Returns a list of object indices representing the influences concerning the joint names specified in the skin weight model.
@type skin_weights: MayaSkinWeights
@param skin_weights: The instance of the model storing data about skin weights
@type skin_cluster: MFnSkinCluster
@param skin_cluster: An object for working with functions concerning a skin cluster in maya
@rtype: List[int]
@returns: a list of indices representing the influences concerning the given joints
"""
file_joint_mapping: List[int] = []
for joint_name in skin_weights.joints:
file_joint_mapping.append(
skin_cluster.indexForInfluenceObject(Maya.get_element(joint_name))
)
return file_joint_mapping
def set_skin_weights_to_scene(mesh_name: str, skin_weights: MayaSkinWeights) -> None:
"""
Sets the skin weights to the scene.
@type mesh_name: str
@param mesh_name: The mesh name
@type skin_weights: MayaSkinWeights
@param skin_weights: The object containing data that need to be set to the scene.
"""
mesh_node, skin_cluster = get_skin_weights_data(mesh_name)
file_joint_mapping = get_file_joint_mappings(skin_weights, skin_cluster)
import_skin_weights(skin_cluster, mesh_node, skin_weights, file_joint_mapping)
logging.info("Set skin weights ended.")
def import_skin_weights(
skin_cluster: MFnSkinCluster,
mesh_node: MFnMesh,
skin_weights: MayaSkinWeights,
file_joint_mapping: List[int],
) -> None:
"""
Imports the skin weights to the scene using the joint mapping and the data provided in the model containing the weights.
@type skin_cluster: MFnSkinCluster
@param skin_cluster: An object for working with functions concerning a skin cluster in maya
@type mesh_node: MFnMesh
@param mesh_node: An object for working with functions concerning meshes in maya
@type skin_weights: MayaSkinWeights
@param skin_weights: The instance of the model storing data about skin weights
@type file_joint_mapping: List[int]
@param file_joint_mapping: a list of indices representing the influences concerning joints
"""
temp_str = f"{skin_cluster.name()}.wl["
for vtx_id in range(cmds.polyEvaluate(mesh_node.name(), vertex=True)):
vtx_info = skin_weights.vertices_info[vtx_id]
vtx_str = f"{temp_str}{str(vtx_id)}].w["
cmds.setAttr(f"{vtx_str}0]", 0.0)
for i in range(0, len(vtx_info), 2):
cmds.setAttr(
f"{vtx_str}{str(file_joint_mapping[int(vtx_info[i])])}]",
vtx_info[i + 1],
)

View File

@@ -0,0 +1,84 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from typing import Union
from maya.api.OpenMaya import (
MDagPath,
MFnDagNode,
MFnTransform,
MGlobal,
MSpace,
MVector,
)
from ...common import DNAViewerError
class Maya:
"""A utility class used for interfacing with maya transforms"""
@staticmethod
def get_element(name: str) -> Union[MDagPath, MFnDagNode]:
"""gets the Union[MDagPath, MFnDagNode] object of the element with the given name
@type name: str
@param name: The name of the element to be retrieved
@rtype: Union[MDagPath, MFnDagNode]
@returns: A OpenMaya object representing the given element
"""
try:
sellist = MGlobal.getSelectionListByName(name)
except Exception as exception:
raise DNAViewerError(f"Element with name:{name} not found!") from exception
try:
return sellist.getDagPath(0)
except Exception:
return sellist.getDependNode(0)
@staticmethod
def get_transform(name: str) -> MFnTransform:
"""gets the transform of the element with the given name
@type element: str
@param element: The element name that we want the transform of
@rtype: MFnTransform
@returns: A MFnTransform object representing the given elements transform
"""
return MFnTransform(Maya.get_element(name))
@staticmethod
def get_translation(element: str, space: int = MSpace.kObject) -> MVector:
"""gets the translation of the element with the given name
@type element: str
@param element: The element name that we want the translation of
@type space: str
@param space: A string value representing the translation space (default is "world")
@rtype: MVector
@returns: A MVector object representing the given elements translation
"""
return MFnTransform(Maya.get_element(element)).translation(space)
@staticmethod
def set_translation(
element: str, translation: MVector, space: int = MSpace.kObject
) -> None:
"""sets the translation of the element with the given name
@type element: str
@param element: The element name that we want to set the translation of
@type translation: MVector
@param translation: The new translation value
@type space: str
@param space: A string value representing the translation space (default is "object")
"""
element_obj = Maya.get_transform(element)
element_obj.setTranslation(translation, space)

117
scripts/builder/mesh.py Normal file
View File

@@ -0,0 +1,117 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import logging
from typing import List
from ..builder.maya.mesh import MayaMesh
from ..dnalib.dnalib import DNA
from .config import Config
class Mesh:
"""
A builder class used for adding joints to the scene
Attributes
----------
@type dna: DNA
@param dna: The location of the DNA file
@type mesh_index: int
@param mesh_index: The mesh index we are working with
@type joint_ids: List[int]
@param joint_ids: The joint indices used for adding skin
@type joint_names: List[str]
@param joint_names: The joint names used for adding skin
@type config: Config
@param config: The build options that will be applied when creating the mesh
@type mesh: MayaMesh
@param mesh: The builder class object for creating the meshes
@type dna: DNA
@param dna: The DNA object that was loaded in
"""
def __init__(
self,
config: Config,
dna: DNA,
mesh_index: int,
) -> None:
self.mesh_index: int = mesh_index
self.joint_ids: List[int] = []
self.joint_names: List[str] = []
self.config = config
self.dna = dna
self.mesh = MayaMesh(
self.mesh_index,
self.dna,
blend_shape_group_prefix=self.config.blend_shape_group_prefix,
blend_shape_name_postfix=self.config.blend_shape_name_postfix,
skin_cluster_suffix=self.config.skin_cluster_suffix,
)
def build(self) -> None:
"""Starts the build process, creates the neutral mesh, then adds normals, blends shapes and skin if needed"""
self.create_neutral_mesh()
self.add_blend_shapes()
self.add_skin_cluster()
def create_neutral_mesh(self) -> None:
"""Creates the neutral mesh"""
self.mesh.create_neutral_mesh()
def add_blend_shapes(self) -> None:
"""Reads in the blend shapes, then adds them to the mesh if it is set in the build options"""
if self.config.add_blend_shapes:
logging.info("adding blend shapes...")
self.mesh.add_blend_shapes(
self.config.add_mesh_name_to_blend_shape_channel_name
)
def add_skin_cluster(self) -> None:
"""Adds skin cluster to the mesh if it is set in the build options"""
if self.config.add_skin_cluster and self.config.add_joints:
self.prepare_joints()
if self.joint_names:
self.mesh.add_skin_cluster(self.joint_names, self.joint_ids)
def prepare_joints(self) -> None:
"""
Gets the joint indices and names needed for the given mesh.
"""
self.prepare_joint_ids()
joints = self.dna.read_all_neutral_joints()
self.joint_names = []
for joint_id in self.joint_ids:
self.joint_names.append(joints[joint_id].name)
def prepare_joint_ids(self) -> None:
joints_temp: List[int] = []
joint_indices = self.dna.get_all_skin_weights_joint_indices_for_mesh(
self.mesh_index
)
self.joint_ids = []
if any(joint_indices):
for row in joint_indices:
for column in row:
joints_temp.append(column)
self.joint_ids = list(set(joints_temp))
self.joint_ids.sort()
else:
lod = self.dna.get_lowest_lod_containing_meshes([self.mesh_index])
if lod:
self.joint_ids = self.dna.get_joint_indices_for_lod(lod)

View File

@@ -0,0 +1,293 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import logging
from importlib.machinery import SourceFileLoader
from importlib.util import module_from_spec, spec_from_loader
from pathlib import Path
from types import ModuleType
from typing import Optional
from maya import cmds, mel
from maya.api.OpenMaya import MSpace, MVector
from ..builder.maya.util import Maya
from ..common import ANALOG_GUI_HOLDER, GUI_HOLDER, RIG_LOGIC_PREFIX, DNAViewerError
from ..dnalib.dnalib import DNA
from .builder import Builder
from .config import RigConfig
class RigBuilder(Builder):
"""
A builder class used for building meshes
"""
def __init__(self, dna: DNA, config: Optional[RigConfig] = None) -> None:
super().__init__(dna=dna, config=config)
self.config: Optional[RigConfig]
self.eye_l_pos: MVector
self.eye_r_pos: MVector
def _build(self) -> None:
if super()._build():
self.add_gui()
self.add_analog_gui()
self.add_rig_logic()
self.run_additional_assemble_script()
def run_additional_assemble_script(self) -> None:
"""
Runs an additional assemble script if specified in the character configuration.
"""
if self.config.aas_path:
logging.info("running additional assemble script...")
try:
module_name = Path(self.config.aas_path).stem
script = self.source_py_file(module_name, self.config.aas_path)
script_method = getattr(script, self.config.aas_method)
script_method(
self.config.get_top_level_group(),
self.config.get_rig_group(),
self.config.aas_parameter,
)
except Exception as e:
raise DNAViewerError(f"Can't run aas script. Reason: {e}") from e
def add_rig_logic(self) -> None:
"""
Creates and adds a rig logic node specified in the character configuration.
"""
if (
self.config.add_rig_logic
and self.config.add_joints
and self.config.add_skin_cluster
and self.config.add_blend_shapes
and self.config.aas_path
and self.config.analog_gui_path
and self.config.gui_path
):
logging.info("adding rig logic...")
try:
cmds.loadPlugin("embeddedRL4.mll")
self.config.rig_logic_name = f"{RIG_LOGIC_PREFIX}{self.dna.name}"
dna = self.dna.path.replace("\\", "/")
mel_command = self.config.rig_logic_command
mel_command += f' -n "{self.config.rig_logic_name}"'
mel_command += f' -dfp "{dna}"'
mel_command += f' -cn "{self.config.control_naming}"'
mel_command += f' -jn "{self.config.joint_naming}"'
mel_command += f' -bsn "{self.config.blend_shape_naming}"'
mel_command += f' -amn "{self.config.animated_map_naming}"; '
logging.info(f"mel command: {mel_command}")
mel.eval(mel_command)
except Exception as e:
logging.error(
"The procedure needed for assembling the rig logic was not found, the plugin needed for this might not be loaded."
)
raise DNAViewerError(
f"Something went wrong, skipping adding the rig logic... Reason: {e}"
) from e
def add_gui(self) -> None:
"""
Adds a gui according to the specified gui options. If none is specified no gui will be added.
"""
if self.config.gui_path:
logging.info("adding gui...")
self.import_gui(
gui_path=self.config.gui_path,
group_name=GUI_HOLDER,
)
self.position_gui(GUI_HOLDER)
self.add_ctrl_attributes()
self.add_animated_map_attributes()
def add_ctrl_attributes(self) -> None:
"""
Adds and sets the raw gui control attributes.
"""
gui_control_names = self.dna.get_raw_control_names()
for name in gui_control_names:
ctrl_and_attr_names = name.split(".")
self.add_attribute(
control_name=ctrl_and_attr_names[0],
long_name=ctrl_and_attr_names[1],
)
def add_animated_map_attributes(self) -> None:
"""
Adds and sets the animated map attributes.
"""
names = self.dna.get_animated_map_names()
for name in names:
long_name = name.replace(".", "_")
if self.config.gui_path:
self.add_attribute(
control_name=self.config.animated_map_attribute_multipliers_name,
long_name=long_name,
)
def position_gui(self, group_name: str) -> None:
"""Sets the gui position to align with the character eyes"""
if not cmds.objExists(self.config.eye_gui_name) or not cmds.objExists(
self.config.left_eye_joint_name
):
logging.warning(
"could not find joints needed for positioning the gui, leaving it at its default position..."
)
return
gui_y = (
Maya.get_transform(self.config.eye_gui_name).translation(MSpace.kObject).y
)
eyes_y = (
Maya.get_transform(self.config.left_eye_joint_name)
.translation(MSpace.kObject)
.y
)
delta_y = eyes_y - gui_y
if isinstance(self.config.gui_translate_x, str):
try:
logging.warning(
"gui_translate_x should be a float, trying to cast the value to float..."
)
self.config.gui_translate_x = float(self.config.gui_translate_x)
except ValueError:
logging.error("could not cast string value to float")
return
Maya.get_transform(group_name).translateBy(
MVector(self.config.gui_translate_x, delta_y, 0), MSpace.kObject
)
def add_analog_gui(self) -> None:
"""
Adds an analog gui according to the specified analog gui options. If none is specified no analog gui will be
added.
"""
if self.config.analog_gui_path and self.config.add_joints:
logging.info("adding analog gui...")
self.import_gui(
gui_path=self.config.analog_gui_path,
group_name=ANALOG_GUI_HOLDER,
)
if self.dna.joints.names:
self.add_eyes()
self.add_eye_locators()
def add_eyes(self) -> None:
"""Add eyes to the analog gui"""
self.eye_l_pos = Maya.get_translation(self.config.left_eye_joint_name)
self.eye_r_pos = Maya.get_translation(self.config.right_eye_joint_name)
Maya.set_translation(
self.config.central_driver_name,
Maya.get_translation(self.config.facial_root_joint_name),
)
delta_l = Maya.get_translation(
self.config.left_eye_aim_up_name
) - Maya.get_translation(self.config.left_eye_driver_name)
delta_r = Maya.get_translation(
self.config.right_eye_aim_up_name
) - Maya.get_translation(self.config.right_eye_driver_name)
Maya.set_translation(self.config.left_eye_driver_name, self.eye_l_pos)
Maya.set_translation(
self.config.right_eye_driver_name,
self.eye_r_pos,
)
Maya.set_translation(
self.config.left_eye_aim_up_name,
MVector(
self.eye_l_pos[0] + delta_l[0],
self.eye_l_pos[1] + delta_l[1],
self.eye_l_pos[2] + delta_l[2],
),
)
Maya.set_translation(
self.config.right_eye_aim_up_name,
MVector(
self.eye_r_pos[0] + delta_r[0],
self.eye_r_pos[1] + delta_r[1],
self.eye_r_pos[2] + delta_r[2],
),
)
def add_eye_locators(self) -> None:
"""Add eye locators to the analog gui"""
eye_l_locator_pos = Maya.get_translation(self.config.le_aim)
eye_r_locator_pos = Maya.get_translation(self.config.re_aim)
central_aim_pos = Maya.get_translation(self.config.central_aim)
eye_middle_delta = (self.eye_l_pos - self.eye_r_pos) / 2
eye_middle = self.eye_r_pos + eye_middle_delta
Maya.set_translation(
self.config.central_aim,
MVector(eye_middle[0], eye_middle[1], central_aim_pos[2]),
)
Maya.set_translation(
self.config.le_aim,
MVector(self.eye_l_pos[0], self.eye_l_pos[1], eye_l_locator_pos[2]),
)
Maya.set_translation(
self.config.re_aim,
MVector(self.eye_r_pos[0], self.eye_r_pos[1], eye_r_locator_pos[2]),
)
def source_py_file(self, name: str, path: str) -> Optional[ModuleType]:
"""
Used for loading a python file, used for additional assemble script.
@type name: str
@param name: The name of the module.
@type path: str
@param path: The path of the python file.
@rtype: Optional[ModuleType]
@returns: The loaded module.
"""
path_obj = Path(path.strip())
if (
path
and path_obj.exists()
and path_obj.is_file()
and path_obj.suffix == ".py"
):
spec = spec_from_loader(name, SourceFileLoader(name, path))
module = module_from_spec(spec)
spec.loader.exec_module(module)
return module
raise DNAViewerError(f"File {path} is not found!")
def import_gui(self, gui_path: str, group_name: str) -> None:
"""
Imports a gui using the provided parameters.
@type gui_path: str
@param gui_path: The path of the gui file that needs to be imported.
@type group_name: str
@param group_name: The name of the transform that holds the imported asset.
"""
cmds.file(gui_path, i=True, groupReference=True, groupName=group_name)

View File

371
scripts/dnalib/behavior.py Normal file
View File

@@ -0,0 +1,371 @@
from dataclasses import dataclass, field
from typing import List, Optional, cast
from dna import BinaryStreamReader as DNAReader
from .definition import Definition
from .layer import Layer
class Behavior(Definition):
"""
@type reader: BinaryStreamReader
@param reader: The binary stream reader being used
@type gui_to_raw: ConditionalTable
@param gui_to_raw: Mapping data about gui to raw values
@type psd: PSDMatrix
@param psd: The data representing Pose Space Deformation
@type blend_shapes: BlendShapesData
@param blend_shapes: The data representing blend shapes
@type animated_maps: AnimatedMapsConditionalTable
@param animated_maps: The data representing animated maps
@type joints: JointGroups
@param joints: The data representing joints
"""
def __init__(self, reader: DNAReader, layers: Optional[List[Layer]]) -> None:
super().__init__(reader, layers)
self.gui_to_raw = ConditionalTable()
self.psd = PSDMatrix()
self.blend_shapes = BlendShapesData()
self.animated_maps_conditional_table = AnimatedMapsConditionalTable()
self.joint_groups = JointGroups()
self.behavior_read = False
def start_read(self) -> None:
super().start_read()
self.behavior_read = False
def is_read(self) -> bool:
return super().is_read() and self.behavior_read
def read(self) -> None:
"""
Starts reading in the behavior part of the DNA
"""
super().read()
if not self.behavior_read and self.layer_enabled(Layer.behavior):
self.behavior_read = True
self.add_gui_to_raw()
self.add_psd()
self.add_joint_groups()
self.add_blend_shapes()
self.add_animated_maps_conditional_table()
def get_animated_map_lods(self) -> List[int]:
return cast(List[int], self.reader.getAnimatedMapLODs())
def get_animated_map_from_values(self) -> List[float]:
return cast(List[float], self.reader.getAnimatedMapFromValues())
def get_animated_map_to_values(self) -> List[float]:
return cast(List[float], self.reader.getAnimatedMapToValues())
def get_animated_map_slope_values(self) -> List[float]:
return cast(List[float], self.reader.getAnimatedMapSlopeValues())
def get_animated_map_cut_values(self) -> List[float]:
return cast(List[float], self.reader.getAnimatedMapCutValues())
def get_animated_map_input_indices(self) -> List[int]:
return cast(List[int], self.reader.getAnimatedMapInputIndices())
def get_animated_map_output_indices(self) -> List[int]:
return cast(List[int], self.reader.getAnimatedMapOutputIndices())
def get_gui_to_raw_from_values(self) -> List[float]:
return cast(List[float], self.reader.getGUIToRawFromValues())
def get_gui_to_raw_to_values(self) -> List[float]:
return cast(List[float], self.reader.getGUIToRawToValues())
def gget_gui_to_raw_slope_values(self) -> List[float]:
return cast(List[float], self.reader.getGUIToRawSlopeValues())
def get_gui_to_raw_cut_values(self) -> List[float]:
return cast(List[float], self.reader.getGUIToRawCutValues())
def get_gui_to_raw_input_indices(self) -> List[int]:
return cast(List[int], self.reader.getGUIToRawInputIndices())
def get_gui_to_raw_output_indices(self) -> List[int]:
return cast(List[int], self.reader.getGUIToRawOutputIndices())
def get_psd_count(self) -> int:
return cast(int, self.reader.getPSDCount())
def get_psd_row_indices(self) -> List[int]:
return cast(List[int], self.reader.getPSDRowIndices())
def get_psd_column_indices(self) -> List[int]:
return cast(List[int], self.reader.getPSDColumnIndices())
def get_psd_values(self) -> List[float]:
return cast(List[float], self.reader.getPSDValues())
def get_blend_shape_channel_lods(self) -> List[int]:
return cast(List[int], self.reader.getBlendShapeChannelLODs())
def get_blend_shape_channel_input_indices(self) -> List[int]:
return cast(List[int], self.reader.getBlendShapeChannelInputIndices())
def get_blend_shape_channel_output_indices(self) -> List[int]:
return cast(List[int], self.reader.getBlendShapeChannelOutputIndices())
def get_joint_row_count(self) -> int:
return cast(int, self.reader.getJointRowCount())
def get_joint_column_count(self) -> int:
return cast(int, self.reader.getJointColumnCount())
def get_joint_variable_attribute_indices(self) -> int:
return cast(int, self.reader.getJointVariableAttributeIndices())
def get_joint_group_count(self) -> int:
return cast(int, self.reader.getJointGroupCount())
def get_joint_group_logs(self, joint_group_index: int) -> List[int]:
return cast(List[int], self.reader.getJointGroupLODs(joint_group_index))
def get_joint_group_input_indices(self, joint_group_index: int) -> List[int]:
return cast(List[int], self.reader.getJointGroupInputIndices(joint_group_index))
def get_joint_group_output_indices(self, joint_group_index: int) -> List[int]:
return cast(
List[int], self.reader.getJointGroupOutputIndices(joint_group_index)
)
def get_joint_group_values(self, joint_group_index: int) -> List[float]:
return cast(List[float], self.reader.getJointGroupValues(joint_group_index))
def get_joint_group_joint_indices(self, joint_group_index: int) -> List[int]:
return cast(List[int], self.reader.getJointGroupJointIndices(joint_group_index))
def add_gui_to_raw(self) -> None:
"""Reads in the gui to raw mapping"""
self.reader.gui_to_raw = ConditionalTable(
inputs=self.get_gui_to_raw_input_indices(),
outputs=self.get_gui_to_raw_output_indices(),
from_values=self.get_gui_to_raw_from_values(),
to_values=self.get_gui_to_raw_to_values(),
slope_values=self.gget_gui_to_raw_slope_values(),
cut_values=self.get_gui_to_raw_cut_values(),
)
def add_psd(self) -> None:
"""Reads in the PSD part of the behavior"""
self.psd = PSDMatrix(
count=self.get_psd_count(),
rows=self.get_psd_row_indices(),
columns=self.get_psd_column_indices(),
values=self.get_psd_values(),
)
def add_joint_groups(self) -> None:
"""Reads in the joints part of the behavior"""
self.joint_groups.joint_row_count = self.reader.getJointRowCount()
self.joint_groups.joint_column_count = self.reader.getJointColumnCount()
for lod in range(self.get_lod_count()):
self.joint_groups.joint_variable_attribute_indices.append(
self.reader.getJointVariableAttributeIndices(lod)
)
for joint_group_index in range(self.get_joint_group_count()):
self.joint_groups.joint_groups.append(
JointGroup(
lods=self.get_joint_group_logs(joint_group_index),
inputs=self.get_joint_group_input_indices(joint_group_index),
outputs=self.get_joint_group_output_indices(joint_group_index),
values=self.get_joint_group_values(joint_group_index),
joints=self.get_joint_group_joint_indices(joint_group_index),
)
)
def add_blend_shapes(self) -> None:
"""Reads in the blend shapes part of the behavior"""
self.blend_shapes = BlendShapesData(
lods=self.get_blend_shape_channel_lods(),
inputs=self.get_blend_shape_channel_input_indices(),
outputs=self.get_blend_shape_channel_output_indices(),
)
def add_animated_maps_conditional_table(self) -> None:
"""Reads in the animated maps part of the behavior"""
self.reader.animated_maps_conditional_table = AnimatedMapsConditionalTable(
lods=self.get_animated_map_lods(),
conditional_table=ConditionalTable(
from_values=self.get_animated_map_from_values(),
to_values=self.get_animated_map_to_values(),
slope_values=self.get_animated_map_slope_values(),
cut_values=self.get_animated_map_cut_values(),
inputs=self.get_animated_map_input_indices(),
outputs=self.get_animated_map_output_indices(),
),
)
@dataclass
class ConditionalTable:
"""
A model class for holding various values
Attributes
----------
@type from_values: List[float]
@param from_values: The list of values
@type to_values: List[float]
@param to_values: The list of values
@type slope_values: List[float]
@param slope_values: The list of slope values
@type cut_values: List[float]
@param cut_values: The list of cut values
@type inputs: List[int]
@param inputs: The indices of inputs
@type outputs: List[int]
@param outputs: The indices of outputs
"""
from_values: List[float] = field(default_factory=list)
to_values: List[float] = field(default_factory=list)
slope_values: List[float] = field(default_factory=list)
cut_values: List[float] = field(default_factory=list)
inputs: List[int] = field(default_factory=list)
outputs: List[int] = field(default_factory=list)
@dataclass
class PSDMatrix:
"""
A model class for holding data about Pose Space Deformation
Attributes
----------
@type count: int
@param count: The list of values
@type rows: List[int]
@param rows: List of row indices used for storing values
@type columns: List[int]
@param columns: List of row indices used for storing values
@type values: List[float]
@param values: The list of values, that can be accessed from the row and column index
"""
count: Optional[int] = field(default=None)
rows: List[int] = field(default_factory=list)
columns: List[int] = field(default_factory=list)
values: List[float] = field(default_factory=list)
@dataclass
class JointGroup:
"""
A model class for holding data about joint groups
Attributes
----------
@type lods: List[int]
@param lods: A list of lod indices that the joint group is contained within
@type values: List[float]
@param values: A list of values
@type joints: List[int]
@param joints: A list of joint indices
@type inputs: List[int]
@param inputs: The indices of inputs
@type outputs: List[int]
@param outputs: The indices of outputs
"""
lods: List[int] = field(default_factory=list)
values: List[float] = field(default_factory=list)
joints: List[int] = field(default_factory=list)
inputs: List[int] = field(default_factory=list)
outputs: List[int] = field(default_factory=list)
@dataclass
class BlendShapesData:
"""
A model class for holding data about blend shapes
Attributes
----------
@type lods: List[int]
@param lods: A list of lod indices that the blend shapes are contained within
@type inputs: List[int]
@param inputs: The indices of inputs
@type outputs: List[int]
@param outputs: The indices of outputs
"""
lods: List[int] = field(default_factory=list)
inputs: List[int] = field(default_factory=list)
outputs: List[int] = field(default_factory=list)
@dataclass
class AnimatedMapsConditionalTable:
"""
A model class for holding data about animated maps
Attributes
----------
@type lods: List[int]
@param lods: A list of lod indices that the blend shapes are contained within
@type conditional_table: ConditionalTable
@param conditional_table: Data needed for animated maps
"""
lods: List[int] = field(default_factory=list)
conditional_table: ConditionalTable = field(default_factory=ConditionalTable)
@dataclass
class JointGroups:
"""
A model class for storing data about joints
Attributes
----------
@type joint_row_count: int
@param joint_row_count: The row count of the matrix that stores the joints data
@type joint_column_count: int
@param joint_column_count: The column count of the matrix that stores the joints data
@type joint_variable_attribute_indices: List[List[int]]
@param joint_variable_attribute_indices: List of joint variable attribute indices per LOD
@type joint_groups: List[JointGroup]
@param joint_groups: The list of joint groups
"""
joint_row_count: Optional[int] = field(default=None)
joint_column_count: Optional[int] = field(default=None)
joint_variable_attribute_indices: List[List[int]] = field(default_factory=list)
joint_groups: List[JointGroup] = field(default_factory=list)

View File

@@ -0,0 +1,333 @@
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Tuple, cast
from dna import BinaryStreamReader as DNAReader
from dna import MeshBlendShapeChannelMapping
from ..model import Point3
from .descriptor import Descriptor
from .layer import Layer
class Definition(Descriptor):
"""
A class used for reading and accessing the definition part of the DNA file
Attributes
----------
@type reader: BinaryStreamReader
@param reader: The binary stream reader being used
@type definition: DefinitionModel
@param definition: The object that holds the definition data read from the DNA file
@type joints: Joints
@param joints: The data about joints
@type blend_shape_channels: GeometryEntity
@param blend_shape_channels: The names and indices of blend shape channels
@type animated_maps: GeometryEntity
@param animated_maps: The names and indices of animated maps
@type meshes: GeometryEntity
@param meshes: The names and indices of the meshes
@type gui_control_names: List[str]
@param gui_control_names: The list of gui control names
@type raw_control_names: List[str]
@param raw_control_names: The list of raw control names
@type mesh_blend_shape_channel_mapping: List[Tuple[int, int]]
@param mesh_blend_shape_channel_mapping: Mapping of mesh index to the blend shape channel index
@type mesh_blend_shape_channel_mapping_indices_for_lod: List[List[int]]
@param mesh_blend_shape_channel_mapping_indices_for_lod: The list of blend shape channel mapping indices by lod
@type neutral_joint_translations: List[Point3]
@param neutral_joint_translations: The list of neutral joint translations
@type neutral_joint_rotations: List[Point3]
@param neutral_joint_rotations: The list of neutral joint rotations
"""
def __init__(self, reader: DNAReader, layers: Optional[List[Layer]]) -> None:
super().__init__(reader, layers)
self.joints = Joints()
self.blend_shape_channels = GeometryEntity()
self.animated_maps = GeometryEntity()
self.meshes = GeometryEntity()
self.meshes_mapping: Dict[str, int] = {}
self.gui_control_names: List[str] = []
self.raw_control_names: List[str] = []
self.mesh_blend_shape_channel_mapping: List[Tuple[int, int]] = []
self.mesh_blend_shape_channel_mapping_indices_for_lod: List[List[int]] = []
self.neutral_joint_translations: List[Point3] = []
self.neutral_joint_rotations: List[Point3] = []
self.definition_read = False
def start_read(self) -> None:
super().start_read()
self.definition_read = False
def is_read(self) -> bool:
return super().is_read() and self.definition_read
def read(self) -> None:
"""
Starts reading in the definition part of the DNA
@rtype: DefinitionModel
@returns: the instance of the created definition model
"""
super().read()
if not self.definition_read and self.layer_enabled(Layer.definition):
self.definition_read = True
self.add_controls()
self.add_joints()
self.add_blend_shape_channels()
self.add_animated_maps()
self.add_meshes()
self.add_mesh_blend_shape_channel_mapping()
self.add_neutral_joints()
def get_lod_count(self) -> int:
return cast(int, self.reader.getLODCount())
def get_gui_control_count(self) -> int:
return cast(int, self.reader.getGUIControlCount())
def get_gui_control_name(self, index: int) -> str:
return cast(str, self.reader.getGUIControlName(index))
def get_raw_control_count(self) -> int:
return cast(int, self.reader.getRawControlCount())
def get_raw_control_name(self, index: int) -> str:
return cast(str, self.reader.getRawControlName(index))
def get_raw_control_names(self) -> List[str]:
names = []
for i in range(self.get_raw_control_count()):
names.append(self.get_raw_control_name(i))
return names
def get_neutral_joint_translation(self, index: int) -> Point3:
translation = cast(List[float], self.reader.getNeutralJointTranslation(index))
return Point3(translation[0], translation[1], translation[2])
def get_neutral_joint_translation_xs(self) -> List[float]:
return cast(List[float], self.reader.getNeutralJointTranslationXs())
def get_neutral_joint_translation_ys(self) -> List[float]:
return cast(List[float], self.reader.getNeutralJointTranslationYs())
def get_neutral_joint_translation_zs(self) -> List[float]:
return cast(List[float], self.reader.getNeutralJointTranslationZs())
def get_neutral_joint_rotation(self, index: int) -> Point3:
translation = cast(List[float], self.reader.getNeutralJointRotation(index))
return Point3(translation[0], translation[1], translation[2])
def get_neutral_joint_rotation_xs(self) -> List[float]:
return cast(List[float], self.reader.getNeutralJointRotationXs())
def get_neutral_joint_rotation_ys(self) -> List[float]:
return cast(List[float], self.reader.getNeutralJointRotationYs())
def get_neutral_joint_rotation_zs(self) -> List[float]:
return cast(List[float], self.reader.getNeutralJointRotationZs())
def get_mesh_blend_shape_channel_mapping_count(self) -> int:
return cast(int, self.reader.getMeshBlendShapeChannelMappingCount())
def get_mesh_blend_shape_channel_mapping(
self, index: int
) -> MeshBlendShapeChannelMapping:
return cast(
MeshBlendShapeChannelMapping,
self.reader.getMeshBlendShapeChannelMapping(index),
)
def get_mesh_blend_shape_channel_mapping_for_lod(self, lod: int) -> List[int]:
return cast(
List[int], self.reader.getMeshBlendShapeChannelMappingIndicesForLOD(lod)
)
def get_joint_count(self) -> int:
return cast(int, self.reader.getJointCount())
def get_joint_name(self, index: int) -> str:
return cast(str, self.reader.getJointName(index))
def get_joint_parent_index(self, index: int) -> int:
return cast(int, self.reader.getJointParentIndex(index))
def get_joint_indices_for_lod(self, index: int) -> List[int]:
return cast(List[int], self.reader.getJointIndicesForLOD(index))
def get_blend_shape_channel_count(self) -> int:
return cast(int, self.reader.getBlendShapeChannelCount())
def get_blend_shape_channel_name(self, index: int) -> str:
return cast(str, self.reader.getBlendShapeChannelName(index))
def get_mesh_count(self) -> int:
return cast(int, self.reader.getMeshCount())
def get_mesh_name(self, index: int) -> str:
return cast(str, self.reader.getMeshName(index))
def get_mesh_indices_for_lod(self, index: int) -> List[int]:
return cast(List[int], self.reader.getMeshIndicesForLOD(index))
def get_blend_shape_channel_indices_for_lod(self, index: int) -> List[int]:
return cast(List[int], self.reader.getBlendShapeChannelIndicesForLOD(index))
def get_animated_map_count(self) -> int:
return cast(int, self.reader.getAnimatedMapCount())
def get_animated_map_name(self, index: int) -> str:
return cast(str, self.reader.getAnimatedMapName(index))
def get_animated_map_names(self) -> List[str]:
names = []
for i in range(self.get_animated_map_count()):
names.append(self.get_animated_map_name(i))
return names
def get_animated_map_indices_for_lod(self, index: int) -> List[int]:
return cast(List[int], self.reader.getAnimatedMapIndicesForLOD(index))
def get_translation_unit(self) -> int:
return cast(int, self.reader.getTranslationUnit())
def get_rotation_unit(self) -> int:
return cast(int, self.reader.getRotationUnit())
def add_neutral_joints(self) -> None:
"""Reads in the neutral joints part of the definition"""
neutral_joint_translation_xs = self.get_neutral_joint_translation_xs()
neutral_joint_translation_ys = self.get_neutral_joint_translation_ys()
neutral_joint_translation_zs = self.get_neutral_joint_translation_zs()
neutral_joint_translation_count_x = len(neutral_joint_translation_xs)
for index in range(neutral_joint_translation_count_x):
self.neutral_joint_translations.append(
Point3(
x=neutral_joint_translation_xs[index],
y=neutral_joint_translation_ys[index],
z=neutral_joint_translation_zs[index],
)
)
neutral_joint_rotation_xs = self.get_neutral_joint_rotation_xs()
neutral_joint_rotation_ys = self.get_neutral_joint_rotation_ys()
neutral_joint_rotation_zs = self.get_neutral_joint_rotation_zs()
neutral_joint_rotation_count_x = len(neutral_joint_rotation_xs)
for index in range(neutral_joint_rotation_count_x):
self.neutral_joint_rotations.append(
Point3(
x=neutral_joint_rotation_xs[index],
y=neutral_joint_rotation_ys[index],
z=neutral_joint_rotation_zs[index],
)
)
def add_mesh_blend_shape_channel_mapping(self) -> None:
"""Reads in the mesh blend shape channel mapping"""
for index in range(self.get_mesh_blend_shape_channel_mapping_count()):
mapping = self.get_mesh_blend_shape_channel_mapping(index)
self.mesh_blend_shape_channel_mapping.append(
(mapping.meshIndex, mapping.blendShapeChannelIndex)
)
for lod in range(self.get_lod_count()):
self.mesh_blend_shape_channel_mapping_indices_for_lod.append(
self.get_mesh_blend_shape_channel_mapping_for_lod(lod)
)
def add_meshes(self) -> None:
"""Reads in the meshes of the definition"""
for index in range(self.get_mesh_count()):
mesh_name = self.get_mesh_name(index)
self.meshes.names.append(mesh_name)
self.meshes_mapping[mesh_name] = index
for index in range(self.get_lod_count()):
self.meshes.lod_indices.append(self.get_mesh_indices_for_lod(index))
def add_animated_maps(self) -> None:
"""Reads in the animated maps of the definition"""
for index in range(self.get_animated_map_count()):
self.animated_maps.names.append(self.get_animated_map_name(index))
for index in range(self.get_lod_count()):
self.animated_maps.lod_indices.append(
self.get_animated_map_indices_for_lod(index)
)
def add_blend_shape_channels(self) -> None:
"""Reads in the neutral joints part of the definition"""
for index in range(self.get_blend_shape_channel_count()):
self.blend_shape_channels.names.append(
self.get_blend_shape_channel_name(index)
)
for index in range(self.get_lod_count()):
self.blend_shape_channels.lod_indices.append(
self.get_blend_shape_channel_indices_for_lod(index)
)
def add_joints(self) -> None:
"""Reads in the joints of the definition"""
for index in range(self.get_joint_count()):
self.joints.names.append(self.get_joint_name(index))
self.joints.parent_index.append(self.get_joint_parent_index(index))
for index in range(self.get_lod_count()):
self.joints.lod_indices.append(self.get_joint_indices_for_lod(index))
def add_controls(self) -> None:
"""Reads in the gui and raw controls of the definition"""
for index in range(self.get_gui_control_count()):
self.gui_control_names.append(self.get_gui_control_name(index))
for index in range(self.get_raw_control_count()):
self.raw_control_names.append(self.get_raw_control_name(index))
@dataclass
class GeometryEntity:
"""
A model class for holding names and indices
Attributes
----------
@type names: List[str]
@param names: List of names
@type lod_indices: List[List[int]]
@param lod_indices: List of indices per lod
"""
names: List[str] = field(default_factory=list)
lod_indices: List[List[int]] = field(default_factory=list)
@dataclass
class Joints(GeometryEntity):
"""
A model class for holding data about the joints
Attributes
----------
@type parent_index: List[int]
@param parent_index: List of parent indices for each joint index
"""
parent_index: List[int] = field(default_factory=list)

View File

@@ -0,0 +1,129 @@
from typing import Dict, List, Optional, Tuple
from dna import BinaryStreamReader as DNAReader
from ..dnalib.layer import Layer
class Descriptor:
"""
A class used for reading and accessing the descriptor part of the DNA file
Attributes
----------
@type name: str
@param name: The name of the character
@type archetype: int
@param archetype: A value that represents the archetype of the character
@type gender: int
@param gender: A value that represents the gender of the character
@type age: int
@param age: The age of the character
@type metadata: Dict[str, str]
@param metadata: Metadata stored for the character
@type translation_unit: int
@param translation_unit: The translation unit that was used for creating the character
@type rotation_unit: int
@param rotation_unit: The translation unit that was used for creating the character
@type coordinate_system: Tuple[int, int, int]
@param coordinate_system: A tuple representing the coordinate system
@type lod_count: int
@param lod_count: The number of LODs for the characters
@type db_max_lod:int
@param db_max_lod: A LOD constraint representing the greatest LOD we wish wish to produce (ie. if the value is n, the potential LODs are 0, 1, .. n-1)
@type db_complexity: str
@param db_complexity: Will be used in future
@type db_name: str
@param db_name: DB identifier
"""
def __init__(self, reader: DNAReader, layers: Optional[List[Layer]]) -> None:
self.reader = reader
self.layers = layers
self.name: Optional[str] = None
self.archetype: Optional[int] = None
self.gender: Optional[int] = None
self.age: Optional[int] = None
self.metadata: Dict[str, str] = {}
self.translation_unit: Optional[int] = None
self.rotation_unit: Optional[int] = None
self.coordinate_system: Optional[Tuple[int, int, int]] = None
self.lod_count: Optional[int] = None
self.db_max_lod: Optional[int] = None
self.db_complexity: Optional[str] = None
self.db_name: Optional[str] = None
self.descriptor_read = False
def start_read(self) -> None:
self.descriptor_read = False
def is_read(self) -> bool:
return self.descriptor_read
def layer_enabled(self, layer: Layer) -> bool:
return layer in self.layers or Layer.all in self.layers
def read(self) -> None:
"""
Starts reading in the descriptor part of the DNA
@rtype: DescriptorModel
@returns: the instance of the created descriptor model
"""
if not self.descriptor_read and self.layer_enabled(Layer.descriptor):
self.descriptor_read = True
self.add_basic_data()
self.add_metadata()
self.add_geometry_data()
self.add_db_data()
def add_basic_data(self) -> None:
"""Reads in the character name, archetype, gender and age"""
self.name = self.reader.getName()
self.archetype = self.reader.getArchetype()
self.gender = self.reader.getGender()
self.age = self.reader.getAge()
def add_metadata(self) -> None:
"""Reads in the metadata provided from the DNA file"""
for i in range(self.reader.getMetaDataCount()):
key = self.reader.getMetaDataKey(i)
self.metadata[key] = self.reader.getMetaDataValue(key)
def add_geometry_data(self) -> None:
"""Sets the translation unit, rotation unit, and coordinate system from the DNA file"""
self.translation_unit = self.reader.getTranslationUnit()
self.rotation_unit = self.reader.getRotationUnit()
coordinate_system = self.reader.getCoordinateSystem()
self.coordinate_system = (
coordinate_system.xAxis,
coordinate_system.yAxis,
coordinate_system.zAxis,
)
def add_db_data(self) -> None:
"""Reads in the db data from the DNA file"""
self.lod_count = self.reader.getLODCount()
self.db_max_lod = self.reader.getDBMaxLOD()
self.db_complexity = self.reader.getDBComplexity()
self.db_name = self.reader.getDBName()

250
scripts/dnalib/dnalib.py Normal file
View File

@@ -0,0 +1,250 @@
from typing import List, Optional, Tuple
from dna import BinaryStreamReader as DNAReader
from dna import DataLayer_All, FileStream, Status
from ..common import DNAViewerError
from ..model import UV, BlendShape, Joint, Layout, Point3
from .behavior import Behavior
from .geometry import Geometry
from .layer import Layer
class DNA(Behavior, Geometry):
"""
A class used for accessing data in DNA file.
@type dna_path: str
@param dna_path: The path of the DNA file
@type layers: Optional[List[Layer]]
@param layers: List of parts of DNA to be loaded. If noting is passed, whole DNA is going to be loaded. Same as
passing Layer.all.
"""
def __init__(self, dna_path: str, layers: Optional[List[Layer]] = None) -> None:
self.path = dna_path
self.reader = self.create_reader(dna_path)
layers = layers or [Layer.all]
Behavior.__init__(self, self.reader, layers)
Geometry.__init__(self, self.reader, layers)
self.read()
def create_reader(self, dna_path: str) -> DNAReader:
"""
Creates a stream reader needed for reading values from the DNA file.
@type dna_path: str
@param dna_path: The path of the DNA file
@rtype: DNA
@returns: The reader needed for reading values from the DNA file
"""
stream = FileStream(
dna_path, FileStream.AccessMode_Read, FileStream.OpenMode_Binary
)
reader = DNAReader(stream, DataLayer_All)
reader.read()
if not Status.isOk():
status = Status.get()
raise RuntimeError(f"Error loading DNA: {status.message}")
return reader
def is_read(self) -> bool:
return Behavior.is_read(self) and Geometry.is_read(self)
def read(self) -> None:
if not self.is_read():
self.start_read()
Behavior.read(self)
Geometry.read(self)
def read_all_neutral_joints(self) -> List[Joint]:
joints = []
for i in range(self.get_joint_count()):
name = self.get_joint_name(i)
translation = self.get_neutral_joint_translation(i)
orientation = self.get_neutral_joint_rotation(i)
parent_name = self.get_joint_name(self.get_joint_parent_index(i))
joint = Joint(
name=name,
translation=translation,
orientation=orientation,
parent_name=parent_name,
)
joints.append(joint)
return joints
def get_all_skin_weights_joint_indices_for_mesh(
self, mesh_index: int
) -> List[List[int]]:
return self.geometry_meshes[mesh_index].skin_weights.joint_indices
def get_blend_shape_target_deltas_with_vertex_id(
self, mesh_index: int, blend_shape_target_index: int
) -> List[Tuple[int, Point3]]:
blend_shape = self.geometry_meshes[mesh_index].blend_shapes[
blend_shape_target_index
]
indices = list(blend_shape.deltas.keys())
deltas: List[Point3] = []
for i in indices:
deltas.append(blend_shape.deltas[i])
if not deltas:
return []
return list(zip(indices, deltas))
def get_all_skin_weights_values_for_mesh(
self, mesh_index: int
) -> List[List[float]]:
skin_weight_values = []
mesh = self.geometry_meshes[mesh_index]
for i in range(len(mesh.topology.positions)):
skin_weight_values.append(mesh.skin_weights.values[i])
return skin_weight_values
def get_skin_weight_matrix_for_mesh(
self, mesh_index: int
) -> List[List[Tuple[int, float]]]:
vertex_position_count = len(self.geometry_meshes[mesh_index].topology.positions)
joint_indices = self.get_all_skin_weights_joint_indices_for_mesh(mesh_index)
if len(joint_indices) != vertex_position_count:
raise DNAViewerError(
"Number of joint indices and vertex count don't match!"
)
skin_weight_values = self.get_all_skin_weights_values_for_mesh(mesh_index)
if len(skin_weight_values) != vertex_position_count:
raise DNAViewerError(
"Number of skin weight values and vertex count don't match!"
)
if len(joint_indices) != len(skin_weight_values):
raise DNAViewerError(
"Number of skin weight values and joint indices count don't match for vertex!"
)
weight_matrix = []
for indices, values in zip(joint_indices, skin_weight_values):
if not indices:
raise DNAViewerError(
"JointIndexArray for vertex can't be less than one!"
)
vertex_weights = []
for joint_index, skin_weight_value in zip(indices, values):
vertex_weights.append((joint_index, skin_weight_value))
weight_matrix.append(vertex_weights)
return weight_matrix
def get_vertex_texture_coordinates_for_mesh(self, mesh_index: int) -> List[UV]:
return self.geometry_meshes[mesh_index].topology.texture_coordinates
def get_vertex_positions_for_mesh_index(self, mesh_index: int) -> List[Point3]:
return self.geometry_meshes[mesh_index].topology.positions
def get_vertex_layout_positions_for_mesh_index(self, mesh_index: int) -> List[int]:
return [
item.position_index
for item in self.geometry_meshes[mesh_index].topology.layouts
]
def get_faces(self, mesh_index: int) -> List[List[int]]:
return self.geometry_meshes[mesh_index].topology.face_vertex_layouts
def get_polygon_faces_and_connects(
self,
mesh_index: int = None,
dna_faces: List[List[int]] = None,
dna_vertex_layout_positions: List[int] = None,
) -> Tuple[List[int], List[int]]:
if mesh_index is None:
if None in (dna_faces, dna_vertex_layout_positions):
raise DNAViewerError(
"get_polygon_faces_and_connects -> Must provide either mesh_index or dna_faces and dna_vertex_layout_positions"
)
if dna_faces is None:
dna_faces = self.get_faces(mesh_index)
if dna_vertex_layout_positions is None:
dna_vertex_layout_positions = (
self.get_vertex_layout_positions_for_mesh_index(mesh_index)
)
polygon_faces = []
polygon_connects = []
for vertices_layout_index_array in dna_faces:
polygon_faces.append(len(vertices_layout_index_array))
for vertex_layout_index_array in vertices_layout_index_array:
polygon_connects.append(
dna_vertex_layout_positions[vertex_layout_index_array]
)
return polygon_faces, polygon_connects
def get_layouts_for_mesh_index(self, mesh_index: int) -> List[Layout]:
return self.geometry_meshes[mesh_index].topology.layouts
def get_texture_coordinate_index(self, mesh_index: int, layout_id: int) -> int:
return (
self.geometry_meshes[mesh_index]
.topology.layouts[layout_id]
.texture_coordinate_index
)
def has_blend_shapes(self, mesh_index: int) -> bool:
return (
len([bs.channel for bs in self.geometry_meshes[mesh_index].blend_shapes])
> 0
)
def get_lowest_lod_containing_meshes(
self, mesh_indices: List[int]
) -> Optional[int]:
unique_mesh_indices = set(mesh_indices)
for lod in range(self.get_lod_count()):
if any(list(unique_mesh_indices & set(self.get_mesh_indices_for_lod(lod)))):
return lod
return None
def get_meshes_by_lods(self, mesh_indices: List[int]) -> List[List[int]]:
result_list = []
for lod in range(self.get_lod_count()):
temp = list(set(mesh_indices) & set(self.get_mesh_indices_for_lod(lod)))
result_list.append(temp)
return result_list
def get_all_meshes_grouped_by_lod(self) -> List[List[int]]:
"""
Gets the list of list of mesh indices grouped by the lod number.
@type dna: DNA
@param dna: Instance of DNA.
@rtype: List[List[int]]
@returns: The list of list of mesh indices grouped by the lod number
"""
result: List[List[int]] = []
for lod in range(self.get_lod_count()):
mesh_indices = []
for mesh_index in self.get_mesh_indices_for_lod(lod):
mesh_indices.append(mesh_index)
result.append(mesh_indices)
return result
def get_blend_shapes(self, mesh_index: int) -> List[BlendShape]:
return self.geometry_meshes[mesh_index].blend_shapes
def get_mesh_id_from_mesh_name(self, mesh_name: str) -> Optional[int]:
return self.meshes_mapping.get(mesh_name, None)

283
scripts/dnalib/geometry.py Normal file
View File

@@ -0,0 +1,283 @@
from typing import Dict, List, Optional, Tuple, cast
from dna import BinaryStreamReader as DNAReader
from ..model import UV, BlendShape, Layout, Mesh, Point3, SkinWeightsData, Topology
from .definition import Definition
from .layer import Layer
class Geometry(Definition):
def __init__(self, reader: DNAReader, layers: Optional[List[Layer]]) -> None:
super().__init__(reader, layers)
self.geometry_meshes: List[Mesh] = []
self.geometry_read = False
def start_read(self) -> None:
super().start_read()
self.geometry_read = False
def is_read(self) -> bool:
return super().is_read() and self.geometry_read
def read(self) -> None:
"""
Starts reading in the mesh from the geometry part of the DNA
"""
super().read()
if not self.geometry_read and self.layer_enabled(Layer.geometry):
self.geometry_read = True
self.geometry_meshes = []
for lod in range(self.get_lod_count()):
for mesh_index in self.get_mesh_indices_for_lod(lod):
self.geometry_meshes.append(self.add_mesh(mesh_index))
def get_maximum_influence_per_vertex(self, mesh_index: int) -> int:
return cast(int, self.reader.getMaximumInfluencePerVertex(meshIndex=mesh_index))
def get_vertex_position_count(self, mesh_index: int) -> int:
return cast(int, self.reader.getVertexPositionCount(mesh_index))
def get_skin_weights_values(
self, mesh_index: int, vertex_index: int
) -> List[float]:
return cast(
List[float],
self.reader.getSkinWeightsValues(
meshIndex=mesh_index, vertexIndex=vertex_index
),
)
def get_skin_weights_joint_indices(
self, mesh_index: int, vertex_index: int
) -> List[int]:
return cast(
List[int],
self.reader.getSkinWeightsJointIndices(
meshIndex=mesh_index, vertexIndex=vertex_index
),
)
def get_vertex_texture_coordinate_count(self, mesh_index: int) -> int:
return cast(
int, self.reader.getVertexTextureCoordinateCount(meshIndex=mesh_index)
)
def get_vertex_texture_coordinate(
self, mesh_index: int, texture_coordinate_index: int
) -> Tuple[float, float]:
return cast(
Tuple[float, float],
self.reader.getVertexTextureCoordinate(
meshIndex=mesh_index, textureCoordinateIndex=texture_coordinate_index
),
)
def get_face_count(self, mesh_index: int) -> int:
return cast(int, self.reader.getFaceCount(meshIndex=mesh_index))
def get_face_vertex_layout_indices(
self, mesh_index: int, face_index: int
) -> List[int]:
return cast(
List[int],
self.reader.getFaceVertexLayoutIndices(
meshIndex=mesh_index, faceIndex=face_index
),
)
def get_vertex_layout(
self, mesh_index: int, layout_index: int
) -> Tuple[int, int, int]:
return cast(
Tuple[int, int, int],
self.reader.getVertexLayout(meshIndex=mesh_index, layoutIndex=layout_index),
)
def get_vertex_layout_count(self, mesh_index: int) -> int:
return cast(int, self.reader.getVertexLayoutCount(meshIndex=mesh_index))
def get_vertex_position(
self, mesh_index: int, vertex_index: int
) -> Tuple[float, float, float]:
return cast(
Tuple[float, float, float],
self.reader.getVertexPosition(
meshIndex=mesh_index, vertexIndex=vertex_index
),
)
def get_blend_shape_target_vertex_indices(
self, mesh_index: int, blend_shape_target_index: int
) -> List[int]:
return cast(
List[int],
self.reader.getBlendShapeTargetVertexIndices(
meshIndex=mesh_index, blendShapeTargetIndex=blend_shape_target_index
),
)
def get_blend_shape_target_delta_count(
self, mesh_index: int, blend_shape_target_index: int
) -> int:
return cast(
int,
self.reader.getBlendShapeTargetDeltaCount(
meshIndex=mesh_index, blendShapeTargetIndex=blend_shape_target_index
),
)
def get_blend_shape_target_delta(
self, mesh_index: int, blend_shape_target_index: int, delta_index: int
) -> Tuple[int, int, int]:
return cast(
Tuple[int, int, int],
self.reader.getBlendShapeTargetDelta(
meshIndex=mesh_index,
blendShapeTargetIndex=blend_shape_target_index,
deltaIndex=delta_index,
),
)
def get_blend_shape_target_count(self, mesh_index: int) -> int:
return cast(int, self.reader.getBlendShapeTargetCount(meshIndex=mesh_index))
def get_blend_shape_channel_index(
self, mesh_index: int, blend_shape_target_index: int
) -> int:
return cast(
int,
self.reader.getBlendShapeChannelIndex(
meshIndex=mesh_index, blendShapeTargetIndex=blend_shape_target_index
),
)
def add_mesh(self, mesh_index: int) -> Mesh:
mesh = Mesh()
mesh.name = self.get_mesh_name(mesh_index)
mesh.topology = self.add_mesh_topology(mesh_index)
mesh.skin_weights = self.add_mesh_skin_weights(mesh_index)
mesh.blend_shapes = self.add_mesh_blend_shapes(mesh_index)
return mesh
def add_mesh_skin_weights(self, mesh_index: int) -> SkinWeightsData:
"""Reads in the skin weights"""
skin_weights = SkinWeightsData()
for vertex_index in range(self.get_vertex_position_count(mesh_index)):
skin_weights.values.append(
self.get_skin_weights_values(mesh_index, vertex_index)
)
skin_weights.joint_indices.append(
self.get_skin_weights_joint_indices(mesh_index, vertex_index)
)
return skin_weights
def add_mesh_topology(self, mesh_index: int) -> Topology:
"""Reads in the positions, texture coordinates, normals, layouts and face vertex layouts"""
topology = Topology()
topology.positions = self.add_positions(mesh_index)
topology.texture_coordinates = self.add_texture_coordinates(mesh_index)
topology.layouts = self.add_layouts(mesh_index)
topology.face_vertex_layouts = self.add_face_vertex_layouts(mesh_index)
return topology
def add_face_vertex_layouts(self, mesh_index: int) -> List[List[int]]:
"""Reads in the face vertex layouts"""
face_vertex_layouts = []
for face_index in range(self.get_face_count(mesh_index)):
face_vertex_layouts.append(
self.get_face_vertex_layout_indices(mesh_index, face_index)
)
return face_vertex_layouts
def add_layouts(self, mesh_index: int) -> List[Layout]:
"""Reads in the vertex layouts"""
layouts = []
for layout_index in range(self.get_vertex_layout_count(mesh_index)):
(
position_id,
texture_coordinate_id,
_,
) = self.get_vertex_layout(mesh_index, layout_index)
layouts.append(
Layout(
position_index=position_id,
texture_coordinate_index=texture_coordinate_id,
)
)
return layouts
def add_texture_coordinates(self, mesh_index: int) -> List[UV]:
"""Reads in the texture coordinates"""
texture_coordinates = []
for texture_coordinate_index in range(
self.get_vertex_texture_coordinate_count(mesh_index)
):
u, v = self.get_vertex_texture_coordinate(
mesh_index, texture_coordinate_index
)
texture_coordinates.append(UV(u=u, v=v))
return texture_coordinates
def add_positions(self, mesh_index: int) -> List[Point3]:
"""Reads in the vertex positions"""
positions = []
for vertex_index in range(self.get_vertex_position_count(mesh_index)):
x, y, z = self.get_vertex_position(mesh_index, vertex_index)
positions.append(Point3(x=x, y=y, z=z))
return positions
def read_target_deltas(
self, mesh_index: int, blend_shape_target_index: int
) -> Dict[int, Point3]:
"""
Reads in the target deltas
@rtype: Dict[int, Point3]
@returns: Mapping of vertex indices to positions
"""
result: Dict[int, Point3] = {}
vertices = self.get_blend_shape_target_vertex_indices(
mesh_index, blend_shape_target_index
)
blend_shape_target_delta_count = self.get_blend_shape_target_delta_count(
mesh_index, blend_shape_target_index
)
for delta_index in range(blend_shape_target_delta_count):
x, y, z = self.get_blend_shape_target_delta(
mesh_index, blend_shape_target_index, delta_index
)
result[vertices[delta_index]] = Point3(x=x, y=y, z=z)
return result
def add_mesh_blend_shapes(self, mesh_index: int) -> List[BlendShape]:
"""
Reads in the blend shapes
@type mesh_index: int
@param mesh_index: The mesh index
"""
blend_shape_target_count = self.get_blend_shape_target_count(mesh_index)
blend_shapes = []
for blend_shape_target_index in range(blend_shape_target_count):
blend_shapes.append(
BlendShape(
channel=self.get_blend_shape_channel_index(
mesh_index, blend_shape_target_index
),
deltas=self.read_target_deltas(
mesh_index, blend_shape_target_index
),
)
)
return blend_shapes

9
scripts/dnalib/layer.py Normal file
View File

@@ -0,0 +1,9 @@
from enum import Enum
class Layer(Enum):
descriptor = 1
definition = 2
behavior = 3
geometry = 4
all = 5

2156
scripts/ui/Qt.py Normal file

File diff suppressed because it is too large Load Diff

4
scripts/ui/__init__.py Normal file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from . import *

233
scripts/ui/behaviour.py Normal file
View File

@@ -0,0 +1,233 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Behaviour UI Module for Plugin
行为系统UI模块 - 负责显示角色行为编辑界面和基础操作
基本功能:
- Blendshape自动加载刷新筛选
- 次级Blendshape自动加载刷新筛选
- Blendshape批量导出和导入
- Blendshape范围编辑
- Blendshape镜像
- Blendshape查找翻转目标
- Blendshape重建
- 表情控制器还原默认表情
- 查找选择表情
- 控制面板查找
- 选择关联关节
- 写入当前表情
- 写入镜像表情
"""
#===================================== IMPORT MODULES =====================================
from Qt import QtWidgets, QtCore, QtGui
from Qt.QtCompat import wrapInstance
from maya import OpenMayaUI as omui
import sys
import os
#===================================== IMPORT FUNCTIONS ===================================
from scripts.utils import utils_behaviour as utils_behaviour
from scripts.ui import ui_utils
#========================================== WIDGETS ==========================================
# 全局变量存储UI控件
blendshape_tree = None
sub_blendshape_tree = None
blendshape_slider = None
blendshape_preview = None
behaviour_buttons = {}
def widgets():
"""
创建行为系统UI控件
"""
global blendshape_tree, sub_blendshape_tree, blendshape_slider, blendshape_preview, behaviour_buttons
# 混合变形树形视图
blendshape_tree = QtWidgets.QTreeWidget()
blendshape_tree.setHeaderLabels(["混合变形名称", "权重", "类型"])
blendshape_tree.setMinimumHeight(200)
blendshape_tree.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
blendshape_tree.header().setSectionResizeMode(QtWidgets.QHeaderView.Stretch) # 列自动拉伸
# 次级混合变形树形视图
sub_blendshape_tree = QtWidgets.QTreeWidget()
sub_blendshape_tree.setHeaderLabels(["次级混合变形", "权重", "目标"])
sub_blendshape_tree.setMinimumHeight(150)
sub_blendshape_tree.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
sub_blendshape_tree.header().setSectionResizeMode(QtWidgets.QHeaderView.Stretch) # 列自动拉伸
# 混合变形滑块
blendshape_slider = QtWidgets.QSlider(QtCore.Qt.Horizontal)
blendshape_slider.setMinimum(0)
blendshape_slider.setMaximum(100)
blendshape_slider.setValue(0)
blendshape_slider.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
# 预览区域
blendshape_preview = QtWidgets.QLabel("预览区域")
blendshape_preview.setAlignment(QtCore.Qt.AlignCenter)
blendshape_preview.setStyleSheet("background-color: #333333; color: white;")
blendshape_preview.setMinimumHeight(150)
blendshape_preview.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
# 功能按钮 - 设置统一的大小策略
button_names = [
"refresh_blendshapes", "refresh_sub_blendshapes", "export_blendshapes", "import_blendshapes",
"edit_range", "mirror_blendshape", "find_flip_target", "rebuild_blendshape",
"reset_controller", "find_expression", "find_control_panel", "select_related_joints",
"write_current_expression", "write_mirror_expression"
]
button_texts = {
"refresh_blendshapes": "刷新混合变形",
"refresh_sub_blendshapes": "刷新次级混合变形",
"export_blendshapes": "导出混合变形",
"import_blendshapes": "导入混合变形",
"edit_range": "范围编辑",
"mirror_blendshape": "混合变形镜像",
"find_flip_target": "查找翻转目标",
"rebuild_blendshape": "重建混合变形",
"reset_controller": "还原默认表情",
"find_expression": "查找选择表情",
"find_control_panel": "控制面板查找",
"select_related_joints": "选择关联关节",
"write_current_expression": "写入当前表情",
"write_mirror_expression": "写入镜像表情"
}
for button_name in button_names:
behaviour_buttons[button_name] = QtWidgets.QPushButton(button_texts[button_name])
behaviour_buttons[button_name].setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
behaviour_buttons[button_name].setMinimumWidth(120)
#========================================== LAYOUTS ==========================================
def layouts(parent_tab=None):
"""
创建行为系统UI布局
Args:
parent_tab: 父容器控件由Main.py传入
"""
# 获取父容器在Main.py中创建的behaviour_tab
if not parent_tab:
parent_tab = ui_utils.get_parent_widget("behaviour_tab")
if not parent_tab:
print("无法获取父容器,布局创建失败")
return
# 创建主布局
main_layout = parent_tab.layout()
if not main_layout:
main_layout = QtWidgets.QVBoxLayout(parent_tab)
main_layout.setContentsMargins(4, 4, 4, 4)
main_layout.setSpacing(4)
main_layout.setSizeConstraint(QtWidgets.QLayout.SetDefaultConstraint) # 设置布局约束为默认,允许自适应
# 创建主分割控件
main_splitter = QtWidgets.QSplitter(QtCore.Qt.Horizontal)
# 左侧区域 - 混合变形列表
left_widget = QtWidgets.QWidget()
left_layout = QtWidgets.QVBoxLayout(left_widget)
left_layout.setContentsMargins(2, 2, 2, 2)
# 混合变形列表区域
blendshape_group = QtWidgets.QGroupBox("混合变形列表")
blendshape_layout = QtWidgets.QVBoxLayout(blendshape_group)
# 添加刷新按钮
refresh_layout = QtWidgets.QHBoxLayout()
refresh_layout.addWidget(behaviour_buttons["refresh_blendshapes"])
refresh_layout.addWidget(behaviour_buttons["refresh_sub_blendshapes"])
blendshape_layout.addLayout(refresh_layout)
# 创建列表分割控件
list_splitter = QtWidgets.QSplitter(QtCore.Qt.Vertical)
list_splitter.addWidget(blendshape_tree)
list_splitter.addWidget(sub_blendshape_tree)
blendshape_layout.addWidget(list_splitter)
# 添加滑块
slider_layout = QtWidgets.QHBoxLayout()
slider_layout.addWidget(QtWidgets.QLabel("权重:"))
slider_layout.addWidget(blendshape_slider)
slider_layout.addWidget(QtWidgets.QLabel("0"))
blendshape_layout.addLayout(slider_layout)
left_layout.addWidget(blendshape_group)
# 左侧按钮组
left_buttons_layout = QtWidgets.QGridLayout()
left_buttons_layout.addWidget(behaviour_buttons["export_blendshapes"], 0, 0)
left_buttons_layout.addWidget(behaviour_buttons["import_blendshapes"], 0, 1)
left_buttons_layout.addWidget(behaviour_buttons["edit_range"], 1, 0)
left_buttons_layout.addWidget(behaviour_buttons["mirror_blendshape"], 1, 1)
left_buttons_layout.addWidget(behaviour_buttons["find_flip_target"], 2, 0)
left_buttons_layout.addWidget(behaviour_buttons["rebuild_blendshape"], 2, 1)
left_layout.addLayout(left_buttons_layout)
# 右侧区域 - 预览和工具
right_widget = QtWidgets.QWidget()
right_layout = QtWidgets.QVBoxLayout(right_widget)
right_layout.setContentsMargins(2, 2, 2, 2)
# 预览区域
preview_group = QtWidgets.QGroupBox("预览")
preview_layout = QtWidgets.QVBoxLayout(preview_group)
preview_layout.addWidget(blendshape_preview)
right_layout.addWidget(preview_group)
# 右侧按钮组
right_buttons_layout = QtWidgets.QGridLayout()
right_buttons_layout.addWidget(behaviour_buttons["reset_controller"], 0, 0)
right_buttons_layout.addWidget(behaviour_buttons["find_expression"], 0, 1)
right_buttons_layout.addWidget(behaviour_buttons["find_control_panel"], 1, 0)
right_buttons_layout.addWidget(behaviour_buttons["select_related_joints"], 1, 1)
right_buttons_layout.addWidget(behaviour_buttons["write_current_expression"], 2, 0)
right_buttons_layout.addWidget(behaviour_buttons["write_mirror_expression"], 2, 1)
right_layout.addLayout(right_buttons_layout)
# 添加到主分割控件
main_splitter.addWidget(left_widget)
main_splitter.addWidget(right_widget)
# 设置分割比例
main_splitter.setSizes([int(parent_tab.width() * 0.6), int(parent_tab.width() * 0.4)])
# 添加到主布局
main_layout.addWidget(main_splitter)
#========================================== CONNECTIONS ==========================================
def connections():
"""
连接行为系统UI信号和槽
"""
# 连接按钮点击事件到占位函数
behaviour_buttons["refresh_blendshapes"].clicked.connect(lambda: print("刷新混合变形功能待实现"))
behaviour_buttons["refresh_sub_blendshapes"].clicked.connect(lambda: print("刷新次级混合变形功能待实现"))
behaviour_buttons["export_blendshapes"].clicked.connect(lambda: print("导出混合变形功能待实现"))
behaviour_buttons["import_blendshapes"].clicked.connect(lambda: print("导入混合变形功能待实现"))
behaviour_buttons["edit_range"].clicked.connect(lambda: print("范围编辑功能待实现"))
behaviour_buttons["mirror_blendshape"].clicked.connect(lambda: print("混合变形镜像功能待实现"))
behaviour_buttons["find_flip_target"].clicked.connect(lambda: print("查找翻转目标功能待实现"))
behaviour_buttons["rebuild_blendshape"].clicked.connect(lambda: print("重建混合变形功能待实现"))
behaviour_buttons["reset_controller"].clicked.connect(lambda: print("还原默认表情功能待实现"))
behaviour_buttons["find_expression"].clicked.connect(lambda: print("查找选择表情功能待实现"))
behaviour_buttons["find_control_panel"].clicked.connect(lambda: print("控制面板查找功能待实现"))
behaviour_buttons["select_related_joints"].clicked.connect(lambda: print("选择关联关节功能待实现"))
behaviour_buttons["write_current_expression"].clicked.connect(lambda: print("写入当前表情功能待实现"))
behaviour_buttons["write_mirror_expression"].clicked.connect(lambda: print("写入镜像表情功能待实现"))
# 连接树形视图选择事件
blendshape_tree.itemSelectionChanged.connect(lambda: print("混合变形选择已更改"))
sub_blendshape_tree.itemSelectionChanged.connect(lambda: print("次级混合变形选择已更改"))
# 连接滑块值变化事件
blendshape_slider.valueChanged.connect(lambda value: print(f"混合变形权重已更改为: {value/100.0}"))
#===================================== PLACEHOLDER FUNCTION ===================================
def behaviour_temp_function():
return utils_behaviour.behaviour_temp_utils_function()

197
scripts/ui/definition.py Normal file
View File

@@ -0,0 +1,197 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Definition UI Module for Plugin
定义系统UI模块 - 负责显示DNA定义编辑界面和基础操作
基本功能:
- LOD, Meshes, Joints, Blendshape, AnimatedMap 加载和刷新
- 写入: 写入关节默认位置,写入几何体,写入蒙皮,写入混合变形目标
- 创建:创建混合变形,绑定蒙皮,取消蒙皮
- 工具:重新定位头部关节,重新定位身体关节,重新定位全身关节,快速创建预设
"""
#===================================== IMPORT MODULES =====================================
from Qt import QtWidgets, QtCore, QtGui
from Qt.QtCompat import wrapInstance
from maya import OpenMayaUI as omui
import sys
import os
#===================================== IMPORT FUNCTIONS ===================================
from scripts.utils import utils_definition as utils_definition
from scripts.ui import ui_utils
#========================================== WIDGETS ==========================================
# 全局变量存储UI控件
definition_tabs = None
dna_elements = {}
definition_buttons = {}
def widgets():
"""
创建定义系统UI控件
"""
global definition_tabs, dna_elements, definition_buttons
# 创建子标签页
definition_tabs = QtWidgets.QTabWidget()
# 创建各类元素列表
dna_elements["lod_list"] = QtWidgets.QListWidget()
dna_elements["mesh_list"] = QtWidgets.QListWidget()
dna_elements["joint_list"] = QtWidgets.QListWidget()
dna_elements["blendshape_list"] = QtWidgets.QListWidget()
dna_elements["animmap_list"] = QtWidgets.QListWidget()
# 创建元素信息面板
dna_elements["element_info"] = QtWidgets.QTextEdit()
dna_elements["element_info"].setReadOnly(True)
# 功能按钮
definition_buttons["write_joint_defaults"] = QtWidgets.QPushButton("写入关节默认位置")
definition_buttons["write_geometry"] = QtWidgets.QPushButton("写入几何体")
definition_buttons["write_skinning"] = QtWidgets.QPushButton("写入蒙皮")
definition_buttons["write_blendshapes"] = QtWidgets.QPushButton("写入混合变形目标")
definition_buttons["create_blendshapes"] = QtWidgets.QPushButton("创建混合变形")
definition_buttons["bind_skin"] = QtWidgets.QPushButton("绑定蒙皮")
definition_buttons["unbind_skin"] = QtWidgets.QPushButton("取消蒙皮")
definition_buttons["reposition_head"] = QtWidgets.QPushButton("重新定位头部关节")
definition_buttons["reposition_body"] = QtWidgets.QPushButton("重新定位身体关节")
definition_buttons["reposition_all"] = QtWidgets.QPushButton("重新定位全身关节")
definition_buttons["quick_preset"] = QtWidgets.QPushButton("快速创建预设")
#========================================== LAYOUTS ==========================================
def layouts(parent_tab=None):
"""
创建定义系统UI布局
Args:
parent_tab: 父容器控件由Main.py传入
"""
# 获取父容器在Main.py中创建的definition_tab
if not parent_tab:
parent_tab = ui_utils.get_parent_widget("definition_tab")
if not parent_tab:
print("无法获取父容器,布局创建失败")
return
# 创建主布局
main_layout = parent_tab.layout()
if not main_layout:
print("父容器没有布局,布局创建失败")
return
# 创建元素标签页
elements_tab = QtWidgets.QWidget()
elements_layout = QtWidgets.QVBoxLayout(elements_tab)
# 创建分割控件
elements_splitter = QtWidgets.QSplitter(QtCore.Qt.Vertical)
# 创建元素列表标签页
lists_tabs = QtWidgets.QTabWidget()
# 创建各类元素列表标签页
lod_tab = QtWidgets.QWidget()
lod_layout = QtWidgets.QVBoxLayout(lod_tab)
lod_layout.addWidget(dna_elements["lod_list"])
lists_tabs.addTab(lod_tab, "LOD")
mesh_tab = QtWidgets.QWidget()
mesh_layout = QtWidgets.QVBoxLayout(mesh_tab)
mesh_layout.addWidget(dna_elements["mesh_list"])
lists_tabs.addTab(mesh_tab, "Meshes")
joint_tab = QtWidgets.QWidget()
joint_layout = QtWidgets.QVBoxLayout(joint_tab)
joint_layout.addWidget(dna_elements["joint_list"])
lists_tabs.addTab(joint_tab, "Joints")
blendshape_tab = QtWidgets.QWidget()
blendshape_layout = QtWidgets.QVBoxLayout(blendshape_tab)
blendshape_layout.addWidget(dna_elements["blendshape_list"])
lists_tabs.addTab(blendshape_tab, "Blendshapes")
animmap_tab = QtWidgets.QWidget()
animmap_layout = QtWidgets.QVBoxLayout(animmap_tab)
animmap_layout.addWidget(dna_elements["animmap_list"])
lists_tabs.addTab(animmap_tab, "AnimatedMaps")
# 信息面板
info_group = QtWidgets.QGroupBox("元素信息")
info_layout = QtWidgets.QVBoxLayout(info_group)
info_layout.addWidget(dna_elements["element_info"])
# 添加到分割控件
elements_splitter.addWidget(lists_tabs)
elements_splitter.addWidget(info_group)
elements_layout.addWidget(elements_splitter)
# 创建工具标签页
tools_tab = QtWidgets.QWidget()
tools_layout = QtWidgets.QVBoxLayout(tools_tab)
# 创建工具分组
write_group = QtWidgets.QGroupBox("写入")
write_layout = QtWidgets.QVBoxLayout(write_group)
write_layout.addWidget(definition_buttons["write_joint_defaults"])
write_layout.addWidget(definition_buttons["write_geometry"])
write_layout.addWidget(definition_buttons["write_skinning"])
write_layout.addWidget(definition_buttons["write_blendshapes"])
create_group = QtWidgets.QGroupBox("创建")
create_layout = QtWidgets.QVBoxLayout(create_group)
create_layout.addWidget(definition_buttons["create_blendshapes"])
create_layout.addWidget(definition_buttons["bind_skin"])
create_layout.addWidget(definition_buttons["unbind_skin"])
tools_group = QtWidgets.QGroupBox("工具")
tools_layout2 = QtWidgets.QVBoxLayout(tools_group)
tools_layout2.addWidget(definition_buttons["reposition_head"])
tools_layout2.addWidget(definition_buttons["reposition_body"])
tools_layout2.addWidget(definition_buttons["reposition_all"])
tools_layout2.addWidget(definition_buttons["quick_preset"])
# 添加到工具标签页
tools_layout.addWidget(write_group)
tools_layout.addWidget(create_group)
tools_layout.addWidget(tools_group)
tools_layout.addStretch()
# 添加标签页到主标签控件
definition_tabs.addTab(elements_tab, "元素")
definition_tabs.addTab(tools_tab, "工具")
# 添加到主布局
main_layout.addWidget(definition_tabs)
#========================================== CONNECTIONS ==========================================
def connections():
"""
连接定义系统UI信号和槽
"""
# 连接按钮点击事件到占位函数
definition_buttons["write_joint_defaults"].clicked.connect(lambda: print("写入关节默认位置功能待实现"))
definition_buttons["write_geometry"].clicked.connect(lambda: print("写入几何体功能待实现"))
definition_buttons["write_skinning"].clicked.connect(lambda: print("写入蒙皮功能待实现"))
definition_buttons["write_blendshapes"].clicked.connect(lambda: print("写入混合变形目标功能待实现"))
definition_buttons["create_blendshapes"].clicked.connect(lambda: print("创建混合变形功能待实现"))
definition_buttons["bind_skin"].clicked.connect(lambda: print("绑定蒙皮功能待实现"))
definition_buttons["unbind_skin"].clicked.connect(lambda: print("取消蒙皮功能待实现"))
definition_buttons["reposition_head"].clicked.connect(lambda: print("重新定位头部关节功能待实现"))
definition_buttons["reposition_body"].clicked.connect(lambda: print("重新定位身体关节功能待实现"))
definition_buttons["reposition_all"].clicked.connect(lambda: print("重新定位全身关节功能待实现"))
definition_buttons["quick_preset"].clicked.connect(lambda: print("快速创建预设功能待实现"))
# 连接列表选择事件
for key, widget in dna_elements.items():
if isinstance(widget, QtWidgets.QListWidget):
widget.itemSelectionChanged.connect(lambda k=key: print(f"{k} 选择已更改"))
# 连接标签页切换事件
definition_tabs.currentChanged.connect(lambda index: print(f"切换到标签页: {definition_tabs.tabText(index)}"))
#===================================== PLACEHOLDER FUNCTION ===================================
def definition_temp_function():
utils_definition.definition_temp_utils_function

169
scripts/ui/geometry.py Normal file
View File

@@ -0,0 +1,169 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Geometry UI Module for Plugin
几何模型UI模块 - 负责显示几何模型编辑界面和基础操作
基本功能:
- 模型拾取以及加载
- LOD模型分级过滤
- LOD模型创建
- 自动加载模型
- 标准化命名
- 自动分组
- 生成面部配件(睫毛,舌头,泪腺 等)
- 修复接缝(修复法线)
- 修复点序
"""
#===================================== IMPORT MODULES =====================================
from Qt import QtWidgets, QtCore, QtGui
from Qt.QtCompat import wrapInstance
from maya import OpenMayaUI as omui
import sys
import os
#===================================== IMPORT FUNCTIONS ===================================
from scripts.utils import utils_geometry as utils_geometry
from scripts.ui import ui_utils
#========================================== WIDGETS ==========================================
# 全局变量存储UI控件
model_tree = None
lod_combo = None
model_buttons = {}
model_info_panel = None
def widgets():
"""
创建几何模型UI控件
"""
global model_tree, lod_combo, model_buttons, model_info_panel
# LOD选择下拉菜单
lod_combo = QtWidgets.QComboBox()
lod_combo.addItems(["LOD0", "LOD1", "LOD2", "LOD3", "LOD4"])
lod_combo.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
lod_combo.setMinimumWidth(100)
# 模型树形视图
model_tree = QtWidgets.QTreeWidget()
model_tree.setHeaderLabels(["模型名称", "顶点数", "面数"])
model_tree.setMinimumHeight(250)
model_tree.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
model_tree.header().setSectionResizeMode(QtWidgets.QHeaderView.Stretch) # 列自动拉伸
# 模型信息面板
model_info_panel = QtWidgets.QTextEdit()
model_info_panel.setReadOnly(True)
model_info_panel.setMinimumHeight(100)
model_info_panel.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
# 功能按钮 - 设置统一的大小策略
for button_name in ["load_model", "create_lod", "auto_load", "standardize_name",
"auto_group", "generate_accessories", "fix_seams", "fix_vertex_order"]:
model_buttons[button_name] = QtWidgets.QPushButton({
"load_model": "加载模型",
"create_lod": "创建LOD",
"auto_load": "自动加载",
"standardize_name": "标准化命名",
"auto_group": "自动分组",
"generate_accessories": "生成面部配件",
"fix_seams": "修复接缝",
"fix_vertex_order": "修复点序"
}[button_name])
model_buttons[button_name].setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
model_buttons[button_name].setMinimumWidth(120)
#========================================== LAYOUTS ==========================================
def layouts(parent_tab=None):
"""
创建几何模型UI布局
Args:
parent_tab: 父容器控件由Main.py传入
"""
# 获取父容器在Main.py中创建的geometry_tab
if not parent_tab:
parent_tab = ui_utils.get_parent_widget("geometry_tab")
if not parent_tab:
print("无法获取父容器,布局创建失败")
return
# 创建主布局
main_layout = parent_tab.layout()
if not main_layout:
main_layout = QtWidgets.QVBoxLayout(parent_tab)
main_layout.setContentsMargins(4, 4, 4, 4)
main_layout.setSpacing(4)
main_layout.setSizeConstraint(QtWidgets.QLayout.SetDefaultConstraint) # 设置布局约束为默认,允许自适应
# 创建分割控件
splitter = QtWidgets.QSplitter(QtCore.Qt.Vertical)
# 上部区域 - LOD选择和模型树
top_widget = QtWidgets.QWidget()
top_layout = QtWidgets.QVBoxLayout(top_widget)
top_layout.setContentsMargins(0, 0, 0, 0)
# LOD选择区域
lod_layout = QtWidgets.QHBoxLayout()
lod_layout.addWidget(QtWidgets.QLabel("LOD级别:"))
lod_layout.addWidget(lod_combo)
lod_layout.addStretch()
top_layout.addLayout(lod_layout)
# 模型树区域
model_group = QtWidgets.QGroupBox("模型列表")
model_layout = QtWidgets.QVBoxLayout(model_group)
model_layout.addWidget(model_tree)
top_layout.addWidget(model_group)
# 下部区域 - 模型信息
model_info_group = QtWidgets.QGroupBox("模型信息")
model_info_layout = QtWidgets.QVBoxLayout(model_info_group)
model_info_layout.addWidget(model_info_panel)
# 添加到分割控件
splitter.addWidget(top_widget)
splitter.addWidget(model_info_group)
# 按钮区域
button_layout = QtWidgets.QGridLayout()
button_layout.addWidget(model_buttons["load_model"], 0, 0)
button_layout.addWidget(model_buttons["create_lod"], 0, 1)
button_layout.addWidget(model_buttons["auto_load"], 1, 0)
button_layout.addWidget(model_buttons["standardize_name"], 1, 1)
button_layout.addWidget(model_buttons["auto_group"], 2, 0)
button_layout.addWidget(model_buttons["generate_accessories"], 2, 1)
button_layout.addWidget(model_buttons["fix_seams"], 3, 0)
button_layout.addWidget(model_buttons["fix_vertex_order"], 3, 1)
# 添加到主布局
main_layout.addWidget(splitter)
main_layout.addLayout(button_layout)
#========================================== CONNECTIONS ==========================================
def connections():
"""
连接几何模型UI信号和槽
"""
# 连接按钮点击事件到占位函数
model_buttons["load_model"].clicked.connect(lambda: print("加载模型功能待实现"))
model_buttons["create_lod"].clicked.connect(lambda: print("创建LOD功能待实现"))
model_buttons["auto_load"].clicked.connect(lambda: print("自动加载功能待实现"))
model_buttons["standardize_name"].clicked.connect(lambda: print("标准化命名功能待实现"))
model_buttons["auto_group"].clicked.connect(lambda: print("自动分组功能待实现"))
model_buttons["generate_accessories"].clicked.connect(lambda: print("生成面部配件功能待实现"))
model_buttons["fix_seams"].clicked.connect(lambda: print("修复接缝功能待实现"))
model_buttons["fix_vertex_order"].clicked.connect(lambda: print("修复点序功能待实现"))
# 连接LOD选择事件
lod_combo.currentIndexChanged.connect(lambda index: print(f"选择的LOD级别: {lod_combo.currentText()}"))
# 连接模型树选择事件
model_tree.itemSelectionChanged.connect(lambda: print("模型选择已更改"))
#===================================== PLACEHOLDER FUNCTION ===================================
def geometry_temp_function():
return utils_geometry.geometry_temp_utils_function()

View File

@@ -0,0 +1,12 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Localization module
"""
LANG = {
"en_US": {
},
"zh_CN": {
}
}

147
scripts/ui/rigging.py Normal file
View File

@@ -0,0 +1,147 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Rigging UI Module for Plugin
绑定系统UI模块 - 负责显示骨骼绑定编辑界面和基础操作
基本功能:
- DNA浏览器
- 根据DNA导入骨骼
- 根据DNA生成身体
- DNA校准
- 骨骼位置校准
- 创建绑定
- 复制蒙皮
"""
#===================================== IMPORT MODULES =====================================
from Qt import QtWidgets, QtCore, QtGui
from Qt.QtCompat import wrapInstance
from maya import OpenMayaUI as omui
import sys
import os
#===================================== IMPORT FUNCTIONS ===================================
from scripts.utils import utils_rigging as utils_rigging
from scripts.ui import ui_utils
#========================================== WIDGETS ==========================================
# 全局变量存储UI控件
dna_browser_tree = None
joint_list = None
dna_info_panel = None
rig_buttons = {}
def widgets():
"""
创建绑定系统UI控件
"""
global dna_browser_tree, joint_list, dna_info_panel, rig_buttons
# DNA浏览器树形视图
dna_browser_tree = QtWidgets.QTreeWidget()
dna_browser_tree.setHeaderLabels(["DNA文件", "版本", "修改日期"])
dna_browser_tree.setMinimumHeight(200)
# 关节列表
joint_list = QtWidgets.QListWidget()
joint_list.setMinimumHeight(150)
# DNA信息面板
dna_info_panel = QtWidgets.QTextEdit()
dna_info_panel.setReadOnly(True)
dna_info_panel.setMinimumHeight(100)
# 功能按钮
rig_buttons["load_dna"] = QtWidgets.QPushButton("加载DNA")
rig_buttons["import_joints"] = QtWidgets.QPushButton("导入骨骼")
rig_buttons["generate_body"] = QtWidgets.QPushButton("生成身体")
rig_buttons["calibrate_dna"] = QtWidgets.QPushButton("DNA校准")
rig_buttons["calibrate_joints"] = QtWidgets.QPushButton("骨骼位置校准")
rig_buttons["create_binding"] = QtWidgets.QPushButton("创建绑定")
rig_buttons["copy_skinning"] = QtWidgets.QPushButton("复制蒙皮")
rig_buttons["save_dna"] = QtWidgets.QPushButton("保存DNA")
#========================================== LAYOUTS ==========================================
def layouts(parent_tab=None):
"""
创建绑定系统UI布局
Args:
parent_tab: 父容器控件由Main.py传入
"""
# 获取父容器在Main.py中创建的rigging_tab
if not parent_tab:
parent_tab = ui_utils.get_parent_widget("rigging_tab")
if not parent_tab:
print("无法获取父容器,布局创建失败")
return
# 创建主布局
main_layout = parent_tab.layout()
if not main_layout:
print("父容器没有布局,布局创建失败")
return
# 创建分割控件
splitter = QtWidgets.QSplitter(QtCore.Qt.Vertical)
# 上部区域 - DNA浏览器
dna_browser_group = QtWidgets.QGroupBox("DNA浏览器")
dna_browser_layout = QtWidgets.QVBoxLayout(dna_browser_group)
dna_browser_layout.addWidget(dna_browser_tree)
# 中部区域 - 关节列表
joint_list_group = QtWidgets.QGroupBox("骨骼列表")
joint_list_layout = QtWidgets.QVBoxLayout(joint_list_group)
joint_list_layout.addWidget(joint_list)
# 下部区域 - DNA信息
dna_info_group = QtWidgets.QGroupBox("DNA信息")
dna_info_layout = QtWidgets.QVBoxLayout(dna_info_group)
dna_info_layout.addWidget(dna_info_panel)
# 添加到分割控件
splitter.addWidget(dna_browser_group)
splitter.addWidget(joint_list_group)
splitter.addWidget(dna_info_group)
# 按钮区域
button_layout = QtWidgets.QGridLayout()
button_layout.addWidget(rig_buttons["load_dna"], 0, 0)
button_layout.addWidget(rig_buttons["import_joints"], 0, 1)
button_layout.addWidget(rig_buttons["generate_body"], 1, 0)
button_layout.addWidget(rig_buttons["calibrate_dna"], 1, 1)
button_layout.addWidget(rig_buttons["calibrate_joints"], 2, 0)
button_layout.addWidget(rig_buttons["create_binding"], 2, 1)
button_layout.addWidget(rig_buttons["copy_skinning"], 3, 0)
button_layout.addWidget(rig_buttons["save_dna"], 3, 1)
# 添加到主布局
main_layout.addWidget(splitter)
main_layout.addLayout(button_layout)
#========================================== CONNECTIONS ==========================================
def connections():
"""
连接绑定系统UI信号和槽
"""
# 连接按钮点击事件到占位函数
rig_buttons["load_dna"].clicked.connect(lambda: print("加载DNA功能待实现"))
rig_buttons["import_joints"].clicked.connect(lambda: print("导入骨骼功能待实现"))
rig_buttons["generate_body"].clicked.connect(lambda: print("生成身体功能待实现"))
rig_buttons["calibrate_dna"].clicked.connect(lambda: print("DNA校准功能待实现"))
rig_buttons["calibrate_joints"].clicked.connect(lambda: print("骨骼位置校准功能待实现"))
rig_buttons["create_binding"].clicked.connect(lambda: print("创建绑定功能待实现"))
rig_buttons["copy_skinning"].clicked.connect(lambda: print("复制蒙皮功能待实现"))
rig_buttons["save_dna"].clicked.connect(lambda: print("保存DNA功能待实现"))
# 连接树形视图选择事件
dna_browser_tree.itemSelectionChanged.connect(lambda: print("DNA文件选择已更改"))
# 连接关节列表选择事件
joint_list.itemSelectionChanged.connect(lambda: print("骨骼选择已更改"))
#===================================== PLACEHOLDER FUNCTION ===================================
def rigging_temp_function():
return utils_rigging.rigging_temp_utils_function()

321
scripts/ui/style.qss Normal file
View File

@@ -0,0 +1,321 @@
/* Plugin 统一样式表 */
/* 全局样式 */
QWidget {
font-family: "Microsoft YaHei", "SimHei", sans-serif;
font-size: 9pt;
color: #E0E0E0;
background-color: #333333;
}
/* 菜单样式 */
QMenuBar {
background-color: #2D2D30;
color: #E0E0E0;
}
QMenuBar::item {
background-color: transparent;
padding: 5px 10px;
}
QMenuBar::item:selected {
background-color: #007ACC;
color: white;
}
QMenu {
background-color: #1E1E1E;
color: #E0E0E0;
border: 1px solid #3F3F46;
}
QMenu::item {
padding: 5px 30px 5px 20px;
}
QMenu::item:selected {
background-color: #007ACC;
color: white;
}
/* 工具栏样式 */
QToolBar {
background-color: #2D2D30;
border: 1px solid #3F3F46;
spacing: 3px;
}
QToolBar::separator {
background-color: #3F3F46;
width: 1px;
height: 20px;
}
/* 标签页样式 */
QTabWidget::pane {
border: 1px solid #3F3F46;
background-color: #2D2D30;
}
QTabBar::tab {
background-color: #252526;
color: #E0E0E0;
border: 1px solid #3F3F46;
border-bottom: none;
padding: 5px 10px;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
min-width: 80px;
}
QTabBar::tab:selected {
background-color: #007ACC;
color: white;
}
QTabBar::tab:hover:!selected {
background-color: #3E3E40;
}
/* 分组框样式 */
QGroupBox {
border: 1px solid #3F3F46;
border-radius: 3px;
margin-top: 10px;
padding-top: 10px;
font-weight: bold;
}
QGroupBox::title {
subcontrol-origin: margin;
subcontrol-position: top left;
padding: 0 5px;
color: #E0E0E0;
}
/* 下拉框样式 */
QComboBox {
background-color: #1E1E1E;
color: #E0E0E0;
border: 1px solid #3F3F46;
border-radius: 3px;
padding: 3px 18px 3px 3px;
min-width: 6em;
}
QComboBox::drop-down {
subcontrol-origin: padding;
subcontrol-position: top right;
width: 15px;
border-left: 1px solid #3F3F46;
}
QComboBox QAbstractItemView {
background-color: #1E1E1E;
color: #E0E0E0;
border: 1px solid #3F3F46;
selection-background-color: #264F78;
selection-color: white;
}
/* 按钮样式 */
QPushButton {
background-color: #0E639C;
color: white;
border: 1px solid #0E639C;
border-radius: 3px;
padding: 5px 10px;
min-width: 80px;
}
QPushButton:hover {
background-color: #1177BB;
border: 1px solid #1177BB;
}
QPushButton:pressed {
background-color: #00578A;
border: 1px solid #00578A;
}
QPushButton:disabled {
background-color: #3F3F46;
color: #9D9D9D;
border: 1px solid #3F3F46;
}
/* 输入框样式 */
QLineEdit, QTextEdit, QPlainTextEdit {
background-color: #1E1E1E;
color: #E0E0E0;
border: 1px solid #3F3F46;
border-radius: 3px;
padding: 3px;
selection-background-color: #264F78;
}
/* 单选框样式 */
QRadioButton {
color: #E0E0E0;
spacing: 5px;
}
QRadioButton::indicator {
width: 13px;
height: 13px;
}
/* 复选框样式 */
QCheckBox {
color: #E0E0E0;
spacing: 5px;
}
QCheckBox::indicator {
width: 13px;
height: 13px;
}
/* 滑块样式 */
QSlider::groove:horizontal {
border: 1px solid #3F3F46;
height: 8px;
background: #1E1E1E;
margin: 2px 0;
border-radius: 4px;
}
QSlider::handle:horizontal {
background: #007ACC;
border: 1px solid #007ACC;
width: 14px;
height: 14px;
margin: -4px 0;
border-radius: 7px;
}
QSlider::handle:horizontal:hover {
background: #1177BB;
border: 1px solid #1177BB;
}
/* 进度条样式 */
QProgressBar {
border: 1px solid #3F3F46;
border-radius: 3px;
background-color: #1E1E1E;
text-align: center;
color: white;
}
QProgressBar::chunk {
background-color: #007ACC;
width: 10px;
}
/* 列表和树视图样式 */
QTreeView, QListView, QTableView {
background-color: #1E1E1E;
alternate-background-color: #262626;
color: #E0E0E0;
border: 1px solid #3F3F46;
selection-background-color: #264F78;
selection-color: white;
outline: none;
}
QTreeView::item, QListView::item, QTableView::item {
padding: 3px;
}
QTreeView::item:selected, QListView::item:selected, QTableView::item:selected {
background-color: #264F78;
color: white;
}
QTreeView::item:hover, QListView::item:hover, QTableView::item:hover {
background-color: #2A2D2E;
}
QHeaderView::section {
background-color: #252526;
color: #E0E0E0;
padding: 3px;
border: 1px solid #3F3F46;
}
/* 滚动条样式 */
QScrollBar:vertical {
background-color: #1E1E1E;
width: 14px;
margin: 14px 0 14px 0;
border: 1px solid #3F3F46;
}
QScrollBar::handle:vertical {
background-color: #3E3E42;
min-height: 20px;
}
QScrollBar::add-line:vertical {
border: 1px solid #3F3F46;
background-color: #3E3E42;
height: 14px;
subcontrol-position: bottom;
subcontrol-origin: margin;
}
QScrollBar::sub-line:vertical {
border: 1px solid #3F3F46;
background-color: #3E3E42;
height: 14px;
subcontrol-position: top;
subcontrol-origin: margin;
}
QScrollBar:horizontal {
background-color: #1E1E1E;
height: 14px;
margin: 0 14px 0 14px;
border: 1px solid #3F3F46;
}
QScrollBar::handle:horizontal {
background-color: #3E3E42;
min-width: 20px;
}
QScrollBar::add-line:horizontal {
border: 1px solid #3F3F46;
background-color: #3E3E42;
width: 14px;
subcontrol-position: right;
subcontrol-origin: margin;
}
QScrollBar::sub-line:horizontal {
border: 1px solid #3F3F46;
background-color: #3E3E42;
width: 14px;
subcontrol-position: left;
subcontrol-origin: margin;
}
/* 状态栏样式 */
QStatusBar {
background-color: #007ACC;
color: white;
}
QStatusBar::item {
border: none;
}
/* 提示框样式 */
QToolTip {
background-color: #2D2D30;
color: #E0E0E0;
border: 1px solid #3F3F46;
padding: 3px;
}

138
scripts/ui/toolbar.py Normal file
View File

@@ -0,0 +1,138 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Toolbar UI Module for Plugin
工具栏UI模块 - 负责显示工具栏界面和基础操作
基本功能:
- 加载预设
- 保存预设
- 导入DNA
- 导出DNA
- 创建RL4节点用于切换DNA编辑的状态
- 删除RL4节点用于切换DNA编辑的状态
"""
#===================================== IMPORT MODULES =====================================
from Qt import QtWidgets, QtCore, QtGui
from Qt.QtCompat import wrapInstance
from maya import OpenMayaUI as omui
import sys
import os
#===================================== IMPORT FUNCTIONS ===================================
from scripts.utils import utils_toolbar as utils_toolbar
from scripts.ui import ui_utils
#========================================== WIDGETS ==========================================
# 全局变量存储UI控件
toolbar_buttons = {}
dna_dropdown = None
status_label = None
def widgets():
"""
创建工具栏UI控件
"""
global toolbar_buttons, dna_dropdown, status_label
# 工具栏按钮
toolbar_buttons["load_preset"] = QtWidgets.QPushButton("加载预设")
toolbar_buttons["load_preset"].setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
toolbar_buttons["load_preset"].setMinimumWidth(80)
toolbar_buttons["save_preset"] = QtWidgets.QPushButton("保存预设")
toolbar_buttons["save_preset"].setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
toolbar_buttons["save_preset"].setMinimumWidth(80)
toolbar_buttons["import_dna"] = QtWidgets.QPushButton("导入DNA")
toolbar_buttons["import_dna"].setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
toolbar_buttons["import_dna"].setMinimumWidth(80)
toolbar_buttons["export_dna"] = QtWidgets.QPushButton("导出DNA")
toolbar_buttons["export_dna"].setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
toolbar_buttons["export_dna"].setMinimumWidth(80)
toolbar_buttons["create_rl4"] = QtWidgets.QPushButton("创建RL4节点")
toolbar_buttons["create_rl4"].setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
toolbar_buttons["create_rl4"].setMinimumWidth(100)
toolbar_buttons["delete_rl4"] = QtWidgets.QPushButton("删除RL4节点")
toolbar_buttons["delete_rl4"].setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
toolbar_buttons["delete_rl4"].setMinimumWidth(100)
# DNA下拉菜单
dna_dropdown = QtWidgets.QComboBox()
dna_dropdown.addItem("选择DNA文件...")
dna_dropdown.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
dna_dropdown.setMinimumWidth(150)
# 状态标签
status_label = QtWidgets.QLabel("就绪")
status_label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
status_label.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
#========================================== LAYOUTS ==========================================
def layouts(parent_frame=None):
"""
创建工具栏UI布局
Args:
parent_frame: 父容器控件由Main.py传入
"""
# 获取父容器在Main.py中创建的toolbar_frame
if not parent_frame:
parent_frame = ui_utils.get_parent_widget("toolbar_frame")
if not parent_frame:
print("无法获取父容器,布局创建失败")
return
# 创建主布局
main_layout = parent_frame.layout()
if not main_layout:
print("父容器没有布局,布局创建失败")
return
# 创建工具栏布局
toolbar_layout = QtWidgets.QHBoxLayout()
toolbar_layout.setContentsMargins(2, 2, 2, 2)
toolbar_layout.setSpacing(4)
toolbar_layout.setSizeConstraint(QtWidgets.QLayout.SetDefaultConstraint) # 设置布局约束为默认,允许自适应
# 添加按钮到工具栏
toolbar_layout.addWidget(toolbar_buttons["load_preset"])
toolbar_layout.addWidget(toolbar_buttons["save_preset"])
toolbar_layout.addWidget(QtWidgets.QLabel("|"))
toolbar_layout.addWidget(toolbar_buttons["import_dna"])
toolbar_layout.addWidget(toolbar_buttons["export_dna"])
toolbar_layout.addWidget(QtWidgets.QLabel("|"))
toolbar_layout.addWidget(toolbar_buttons["create_rl4"])
toolbar_layout.addWidget(toolbar_buttons["delete_rl4"])
toolbar_layout.addWidget(QtWidgets.QLabel("|"))
toolbar_layout.addWidget(QtWidgets.QLabel("DNA文件:"))
toolbar_layout.addWidget(dna_dropdown)
toolbar_layout.addStretch()
toolbar_layout.addWidget(status_label)
# 添加到主布局
main_layout.addLayout(toolbar_layout)
#========================================== CONNECTIONS ==========================================
def connections():
"""
连接工具栏UI信号和槽
"""
# 连接按钮点击事件到占位函数
toolbar_buttons["load_preset"].clicked.connect(lambda: print("加载预设功能待实现"))
toolbar_buttons["save_preset"].clicked.connect(lambda: print("保存预设功能待实现"))
toolbar_buttons["import_dna"].clicked.connect(lambda: print("导入DNA功能待实现"))
toolbar_buttons["export_dna"].clicked.connect(lambda: print("导出DNA功能待实现"))
toolbar_buttons["create_rl4"].clicked.connect(lambda: print("创建RL4节点功能待实现"))
toolbar_buttons["delete_rl4"].clicked.connect(lambda: print("删除RL4节点功能待实现"))
# 连接下拉菜单选择事件
dna_dropdown.currentIndexChanged.connect(lambda index: print(f"选择的DNA文件索引: {index}"))
#===================================== PLACEHOLDER FUNCTION ===================================
def toolbar_temp_function():
return utils_toolbar.toolbar_temp_utils_function()

62
scripts/ui/ui_utils.py Normal file
View File

@@ -0,0 +1,62 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
UI Utilities Module for Plugin
UI工具模块 - 提供UI相关的通用函数
"""
#===================================== IMPORT MODULES =====================================
from Qt import QtWidgets, QtCore, QtGui
from Qt.QtCompat import wrapInstance
from maya import OpenMayaUI as omui
import sys
import os
import config
#===================================== UI UTILITY FUNCTIONS ===================================
TOOL_NAME = config.TOOL_NAME
def get_parent_widget(widget_name):
"""
根据控件名称查找父容器控件
Args:
widget_name (str): 控件名称
Returns:
QWidget: 找到的父容器控件如果未找到则返回None
"""
# 查找主窗口中的所有控件
main_window = None
for widget in QtWidgets.QApplication.topLevelWidgets():
if widget.objectName() == f"{TOOL_NAME}MainWindow" or widget.objectName().endswith("MainWindow"):
main_window = widget
break
if not main_window:
print(f"无法找到主窗口,无法获取父容器: {widget_name}")
return None
# 在主窗口中查找指定名称的控件
found_widget = main_window.findChild(QtWidgets.QWidget, widget_name)
if found_widget:
return found_widget
# 如果未找到精确匹配,尝试模糊匹配
for child in main_window.findChildren(QtWidgets.QWidget):
if widget_name.lower() in child.objectName().lower():
return child
print(f"无法找到控件: {widget_name}")
return None
def get_maya_main_window():
"""
获取Maya主窗口
Returns:
QWidget: Maya主窗口控件
"""
main_window_ptr = omui.MQtUtil.mainWindow()
return wrapInstance(int(main_window_ptr), QtWidgets.QWidget)

View File

@@ -0,0 +1,4 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from . import *

View File

@@ -0,0 +1,23 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Behaviour function module
"""
#===================================== IMPORT MODULES =====================================
import maya.cmds as cmds
import pymel.core as pm
import importlib
import sys
import os
#========================================== FUNCTIONS ========================================
def behaviour_temp_utils_function():
"""
Placeholder function for behaviour module
This function will be replaced with actual functionality in future updates
"""
print("Behaviour module initialized with placeholder function")
return True

View File

@@ -0,0 +1,23 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Definition function module
"""
#===================================== IMPORT MODULES =====================================
import maya.cmds as cmds
import pymel.core as pm
import importlib
import sys
import os
#========================================== FUNCTIONS ========================================
def definition_temp_utils_function():
"""
Placeholder function for definition module
This function will be replaced with actual functionality in future updates
"""
print("Definition module initialized with placeholder function")
return True

View File

@@ -0,0 +1,23 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Geometry function module
"""
#===================================== IMPORT MODULES =====================================
import maya.cmds as cmds
import pymel.core as pm
import importlib
import sys
import os
#========================================== FUNCTIONS ========================================
def geometry_temp_utils_function():
"""
Placeholder function for geometry module
This function will be replaced with actual functionality in future updates
"""
print("Geometry module initialized with placeholder function")
return True

View File

@@ -0,0 +1,23 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Rigging function module
"""
#===================================== IMPORT MODULES =====================================
import maya.cmds as cmds
import pymel.core as pm
import importlib
import sys
import os
#========================================== FUNCTIONS ========================================
def rigging_temp_utils_function():
"""
Placeholder function for rigging module
This function will be replaced with actual functionality in future updates
"""
print("Rigging module initialized with placeholder function")
return True

View File

@@ -0,0 +1,19 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#===================================== IMPORT MODULES =====================================
import maya.cmds as cmds
import pymel.core as pm
import importlib
import sys
import os
#========================================== FUNCTIONS ========================================
def toolbar_temp_utils_function():
"""
Placeholder function for toolbar module
This function will be replaced with actual functionality in future updates
"""
print("Toolbar module initialized with placeholder function")
return True