Base
This commit is contained in:
284
scripts/Main.py
Normal file
284
scripts/Main.py
Normal 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
741
scripts/ReloadModules.py
Normal 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
32
scripts/__init__.py
Normal 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)
|
||||
4
scripts/builder/__init__.py
Normal file
4
scripts/builder/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import *
|
||||
436
scripts/builder/builder.py
Normal file
436
scripts/builder/builder.py
Normal 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
260
scripts/builder/config.py
Normal 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
81
scripts/builder/joint.py
Normal 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)
|
||||
4
scripts/builder/maya/__init__.py
Normal file
4
scripts/builder/maya/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import *
|
||||
424
scripts/builder/maya/mesh.py
Normal file
424
scripts/builder/maya/mesh.py
Normal 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)}")
|
||||
204
scripts/builder/maya/skin_weights.py
Normal file
204
scripts/builder/maya/skin_weights.py
Normal 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],
|
||||
)
|
||||
84
scripts/builder/maya/util.py
Normal file
84
scripts/builder/maya/util.py
Normal 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
117
scripts/builder/mesh.py
Normal 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)
|
||||
293
scripts/builder/rig_builder.py
Normal file
293
scripts/builder/rig_builder.py
Normal 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)
|
||||
0
scripts/dnalib/__init__.py
Normal file
0
scripts/dnalib/__init__.py
Normal file
371
scripts/dnalib/behavior.py
Normal file
371
scripts/dnalib/behavior.py
Normal 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)
|
||||
333
scripts/dnalib/definition.py
Normal file
333
scripts/dnalib/definition.py
Normal 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)
|
||||
129
scripts/dnalib/descriptor.py
Normal file
129
scripts/dnalib/descriptor.py
Normal 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
250
scripts/dnalib/dnalib.py
Normal 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
283
scripts/dnalib/geometry.py
Normal 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
9
scripts/dnalib/layer.py
Normal 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
2156
scripts/ui/Qt.py
Normal file
File diff suppressed because it is too large
Load Diff
4
scripts/ui/__init__.py
Normal file
4
scripts/ui/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import *
|
||||
233
scripts/ui/behaviour.py
Normal file
233
scripts/ui/behaviour.py
Normal 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
197
scripts/ui/definition.py
Normal 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
169
scripts/ui/geometry.py
Normal 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()
|
||||
12
scripts/ui/localization.py
Normal file
12
scripts/ui/localization.py
Normal 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
147
scripts/ui/rigging.py
Normal 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
321
scripts/ui/style.qss
Normal 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
138
scripts/ui/toolbar.py
Normal 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
62
scripts/ui/ui_utils.py
Normal 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)
|
||||
4
scripts/utils/__init__.py
Normal file
4
scripts/utils/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import *
|
||||
23
scripts/utils/utils_behaviour.py
Normal file
23
scripts/utils/utils_behaviour.py
Normal 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
|
||||
23
scripts/utils/utils_definition.py
Normal file
23
scripts/utils/utils_definition.py
Normal 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
|
||||
23
scripts/utils/utils_geometry.py
Normal file
23
scripts/utils/utils_geometry.py
Normal 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
|
||||
23
scripts/utils/utils_rigging.py
Normal file
23
scripts/utils/utils_rigging.py
Normal 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
|
||||
19
scripts/utils/utils_toolbar.py
Normal file
19
scripts/utils/utils_toolbar.py
Normal 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
|
||||
Reference in New Issue
Block a user