Update
This commit is contained in:
parent
691a4ffe69
commit
dd2318e54b
@ -1,440 +1 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
#===================================== IMPORTS =====================================
|
|
||||||
# Standard library imports
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import webbrowser
|
|
||||||
import locale
|
|
||||||
|
|
||||||
# Qt imports
|
|
||||||
from PySide2 import QtWidgets, QtCore, QtGui
|
|
||||||
from shiboken2 import wrapInstance
|
|
||||||
|
|
||||||
# Maya imports
|
|
||||||
from maya import OpenMayaUI as omui
|
|
||||||
import maya.cmds as cmds
|
|
||||||
import maya.mel as mel
|
|
||||||
|
|
||||||
#===================================== IMPORT MODULES =====================================
|
|
||||||
import BodyPrep
|
|
||||||
import BatchImport
|
|
||||||
import DNA_Viewer
|
|
||||||
<<<<<<< Updated upstream
|
|
||||||
=======
|
|
||||||
|
|
||||||
>>>>>>> Stashed changes
|
|
||||||
#===================================== CONSTANTS =====================================
|
|
||||||
# Tool info
|
|
||||||
TOOL_NAME = "MetaFusion"
|
|
||||||
TOOL_VERSION = "Beta v1.0.0"
|
|
||||||
TOOL_AUTHOR = "CGNICO"
|
|
||||||
TOOL_LANG = 'en_US'
|
|
||||||
# UI Constants
|
|
||||||
TOOL_WSCL_NAME = "MetaFusionWorkSpaceControl"
|
|
||||||
<<<<<<< Updated upstream
|
|
||||||
TOOL_HELP_URL = f"http://10.72.61.59:3000/ArtGroup/{TOOL_NAME}/wiki"
|
|
||||||
=======
|
|
||||||
TOOL_HELP_URL = f"https://gitea.cgnico.com/CGNICO/MetaFusion/wiki"
|
|
||||||
>>>>>>> Stashed changes
|
|
||||||
DEFAULT_WINDOW_SIZE = (500, 800)
|
|
||||||
|
|
||||||
# Paths
|
|
||||||
TOOL_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__))).replace("\\", "/")
|
|
||||||
SCRIPTS_PATH = os.path.join(TOOL_PATH, "scripts").replace("\\", "/")
|
|
||||||
ICONS_PATH = os.path.join(TOOL_PATH, "icons").replace("\\", "/")
|
|
||||||
TOOL_ICON = os.path.join(ICONS_PATH, "logo.png").replace("\\", "/")
|
|
||||||
|
|
||||||
# Metahuman paths
|
|
||||||
DATA_PATH = os.path.join(TOOL_PATH, "data").replace("\\", "/")
|
|
||||||
DNA_PATH = os.path.join(DATA_PATH, "dna").replace("\\", "/")
|
|
||||||
BODY_PATH = os.path.join(DATA_PATH, "body").replace("\\", "/")
|
|
||||||
IMG_PATH = os.path.join(DATA_PATH, "img").replace("\\", "/")
|
|
||||||
MAP_PATH = os.path.join(DATA_PATH, "map").replace("\\", "/")
|
|
||||||
MASKS_PATH = os.path.join(DATA_PATH, "masks").replace("\\", "/")
|
|
||||||
SHADERS_PATH = os.path.join(DATA_PATH, "shaders").replace("\\", "/")
|
|
||||||
MH4_PATH = os.path.join(DATA_PATH, "mh4").replace("\\", "/")
|
|
||||||
MH4_DNA_PATH = os.path.join(MH4_PATH, "dna").replace("\\", "/")
|
|
||||||
OUT_PATH = os.path.join(DATA_PATH, "out").replace("\\", "/")
|
|
||||||
SAVE_PATH = os.path.join(DATA_PATH, "save").replace("\\", "/")
|
|
||||||
MAYA_VERSION = cmds.about(version=True)
|
|
||||||
PLUGIN_PATH = os.path.join(TOOL_PATH, "plugins", f"{MAYA_VERSION}").replace("\\", "/")
|
|
||||||
if not os.path.exists(PLUGIN_PATH):
|
|
||||||
cmds.warning(f"Plugin path not found: {PLUGIN_PATH}")
|
|
||||||
|
|
||||||
print(f"TOOL_PATH: {TOOL_PATH}")
|
|
||||||
print(f"SCRIPTS_PATH: {SCRIPTS_PATH}")
|
|
||||||
print(f"ICONS_PATH: {ICONS_PATH}")
|
|
||||||
print(f"TOOL_ICON: {TOOL_ICON}")
|
|
||||||
print(f"DATA_PATH: {DATA_PATH}")
|
|
||||||
print(f"DNA_PATH: {DNA_PATH}")
|
|
||||||
print(f"BODY_PATH: {BODY_PATH}")
|
|
||||||
print(f"IMG_PATH: {IMG_PATH}")
|
|
||||||
print(f"MAP_PATH: {MAP_PATH}")
|
|
||||||
print(f"MASKS_PATH: {MASKS_PATH}")
|
|
||||||
print(f"SHADERS_PATH: {SHADERS_PATH}")
|
|
||||||
|
|
||||||
|
|
||||||
#===================================== LANGUAGE SETTINGS =====================================
|
|
||||||
TOOL_LANG = 'en_US'
|
|
||||||
SUPPORTED_LANGUAGES = ['en_US', 'zh_CN']
|
|
||||||
|
|
||||||
LANG = {
|
|
||||||
"en_US": {
|
|
||||||
"MetaFusion": "MetaFusion",
|
|
||||||
"Prepare": "Prepare",
|
|
||||||
"Body Prepare": "Body Prepare",
|
|
||||||
"DNA Edit": "DNA Edit",
|
|
||||||
"Open DNA Viewer": "Open DNA Viewer",
|
|
||||||
"Import": "Import",
|
|
||||||
"Batch Import": "Batch Import",
|
|
||||||
"Help": "Help",
|
|
||||||
"Switch Language": "Switch Language",
|
|
||||||
"Language switched": "Language switched",
|
|
||||||
"EN": "EN",
|
|
||||||
"ZH": "ZH",
|
|
||||||
"English": "English",
|
|
||||||
"Chinese": "Chinese"
|
|
||||||
},
|
|
||||||
"zh_CN": {
|
|
||||||
"MetaFusion": "MetaFusion",
|
|
||||||
"Prepare": "准备",
|
|
||||||
"Body Prepare": "身体准备",
|
|
||||||
"DNA Edit": "DNA 编辑",
|
|
||||||
"Open DNA Viewer": "打开 DNA 查看器",
|
|
||||||
"Import": "导入",
|
|
||||||
"Batch Import": "批量导入",
|
|
||||||
"Help": "帮助",
|
|
||||||
"Switch Language": "切换语言",
|
|
||||||
"Language switched": "语言已切换",
|
|
||||||
"EN": "英文",
|
|
||||||
"ZH": "中文",
|
|
||||||
"English": "英语",
|
|
||||||
"Chinese": "中文"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#===================================== UTILITY FUNCTIONS =====================================
|
|
||||||
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)
|
|
||||||
|
|
||||||
#===================================== UI COMPONENTS =====================================
|
|
||||||
class MainButton(QtWidgets.QPushButton):
|
|
||||||
DEFAULT_COLORS = {"normal": "#D0D0D0", "hover": "#E0E0E0", "pressed": "#C0C0C0"}
|
|
||||||
|
|
||||||
def __init__(self, text="", icon=None, color=None, hover_color=None, pressed_color=None):
|
|
||||||
super().__init__(text)
|
|
||||||
if icon:
|
|
||||||
self.setIcon(icon)
|
|
||||||
self.setIconSize(QtCore.QSize(24, 24))
|
|
||||||
self.setMinimumHeight(30)
|
|
||||||
colors = {
|
|
||||||
"normal": color or self.DEFAULT_COLORS["normal"],
|
|
||||||
"hover": hover_color or self.DEFAULT_COLORS["hover"],
|
|
||||||
"pressed": pressed_color or self.DEFAULT_COLORS["pressed"]
|
|
||||||
}
|
|
||||||
self.setStyleSheet(self._generate_style_sheet(**colors))
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _generate_style_sheet(normal, hover, pressed):
|
|
||||||
return f"""
|
|
||||||
QPushButton {{
|
|
||||||
background-color: {normal};
|
|
||||||
color: #303030;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 5px;
|
|
||||||
font-weight: bold;
|
|
||||||
text-align: center;
|
|
||||||
}}
|
|
||||||
QPushButton:hover {{
|
|
||||||
background-color: {hover};
|
|
||||||
}}
|
|
||||||
QPushButton:pressed {{
|
|
||||||
background-color: {pressed};
|
|
||||||
}}
|
|
||||||
"""
|
|
||||||
|
|
||||||
class BottomButton(QtWidgets.QPushButton):
|
|
||||||
def __init__(self, text="", icon=None):
|
|
||||||
super().__init__(text)
|
|
||||||
self.setMinimumHeight(20)
|
|
||||||
self.setStyleSheet(self._generate_style_sheet())
|
|
||||||
self.setFont(QtGui.QFont("Microsoft YaHei", 10))
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _generate_style_sheet():
|
|
||||||
return """
|
|
||||||
QPushButton {
|
|
||||||
background-color: transparent;
|
|
||||||
border: none;
|
|
||||||
color: gray;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
QPushButton:hover {
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
#===================================== MAIN WINDOW =====================================
|
|
||||||
class MainWindow(QtWidgets.QWidget):
|
|
||||||
|
|
||||||
instance = None
|
|
||||||
|
|
||||||
def __init__(self, parent=maya_main_window()):
|
|
||||||
self.load_required_plugins()
|
|
||||||
|
|
||||||
super(MainWindow, self).__init__(parent)
|
|
||||||
self.setWindowTitle(f"{TOOL_NAME} - {TOOL_VERSION}")
|
|
||||||
self.setObjectName(TOOL_PATH)
|
|
||||||
self.setWindowFlags(QtCore.Qt.Window)
|
|
||||||
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
|
|
||||||
self.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Preferred)
|
|
||||||
self.setMinimumSize(300, 800)
|
|
||||||
|
|
||||||
self.create_widgets()
|
|
||||||
self.create_layouts()
|
|
||||||
self.create_connections()
|
|
||||||
|
|
||||||
if os.path.exists(TOOL_ICON):
|
|
||||||
self.setWindowIcon(QtGui.QIcon(TOOL_ICON))
|
|
||||||
else:
|
|
||||||
print(f"WARNING: Icon file not found: {TOOL_ICON}")
|
|
||||||
|
|
||||||
def load_required_plugins(self):
|
|
||||||
try:
|
|
||||||
if PLUGIN_PATH not in os.environ.get('MAYA_PLUG_IN_PATH', ''):
|
|
||||||
if 'MAYA_PLUG_IN_PATH' in os.environ:
|
|
||||||
os.environ['MAYA_PLUG_IN_PATH'] = f"{PLUGIN_PATH};{os.environ['MAYA_PLUG_IN_PATH']}"
|
|
||||||
else:
|
|
||||||
os.environ['MAYA_PLUG_IN_PATH'] = PLUGIN_PATH
|
|
||||||
|
|
||||||
required_plugins = ['embeddedRL4.mll']
|
|
||||||
for plugin in required_plugins:
|
|
||||||
plugin_path = os.path.join(PLUGIN_PATH, plugin)
|
|
||||||
if os.path.exists(plugin_path):
|
|
||||||
try:
|
|
||||||
if not cmds.pluginInfo(plugin, query=True, loaded=True):
|
|
||||||
cmds.loadPlugin(plugin_path)
|
|
||||||
print(f"Successfully loaded plugin: {plugin}")
|
|
||||||
except Exception as e:
|
|
||||||
cmds.warning(f"Failed to load plugin {plugin}: {str(e)}")
|
|
||||||
else:
|
|
||||||
cmds.warning(f"Plugin not found: {plugin_path}")
|
|
||||||
except Exception as e:
|
|
||||||
cmds.warning(f"Error loading plugins: {str(e)}")
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def show_window(cls):
|
|
||||||
try:
|
|
||||||
if cmds.workspaceControl(TOOL_WSCL_NAME, exists=True):
|
|
||||||
cmds.deleteUI(TOOL_WSCL_NAME, control=True)
|
|
||||||
|
|
||||||
if cls.instance is not None:
|
|
||||||
try:
|
|
||||||
cls.instance.close()
|
|
||||||
cls.instance.deleteLater()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
cls.instance = cls()
|
|
||||||
cls.instance.dock_to_maya()
|
|
||||||
return cls.instance
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error showing {TOOL_NAME} window: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def dock_to_maya(self):
|
|
||||||
if cmds.workspaceControl(TOOL_WSCL_NAME, exists=True):
|
|
||||||
cmds.deleteUI(TOOL_WSCL_NAME)
|
|
||||||
try:
|
|
||||||
workspace_control = cmds.workspaceControl(
|
|
||||||
TOOL_WSCL_NAME,
|
|
||||||
label=TOOL_NAME,
|
|
||||||
floating=True,
|
|
||||||
retain=True,
|
|
||||||
resizeWidth=True,
|
|
||||||
initialWidth=500,
|
|
||||||
minimumWidth=500
|
|
||||||
)
|
|
||||||
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(f"Error creating workspace control: {e}")
|
|
||||||
|
|
||||||
#===================================== UI COMPONENTS =====================================
|
|
||||||
def create_widgets(self):
|
|
||||||
<<<<<<< Updated upstream
|
|
||||||
# DNA Edit group
|
|
||||||
self.dna_edit_btn = MainButton(LANG[TOOL_LANG]["DNA Edit"])
|
|
||||||
self.dna_viewer_btn = MainButton(LANG[TOOL_LANG]["Open DNA Viewer"], color="#B8E6B3", hover_color="#C4F2BF", pressed_color="#A3D99E")
|
|
||||||
|
|
||||||
=======
|
|
||||||
|
|
||||||
>>>>>>> Stashed changes
|
|
||||||
# Prepare group
|
|
||||||
self.prepare_btn = MainButton(LANG[TOOL_LANG]["Prepare"])
|
|
||||||
self.body_prepare_btn = MainButton(LANG[TOOL_LANG]["Body Prepare"], color="#FFEBA1", hover_color="#FFF5B3", pressed_color="#FFE68A")
|
|
||||||
|
|
||||||
<<<<<<< Updated upstream
|
|
||||||
=======
|
|
||||||
# DNA Edit group
|
|
||||||
self.dna_edit_btn = MainButton(LANG[TOOL_LANG]["DNA Edit"])
|
|
||||||
self.dna_viewer_btn = MainButton(LANG[TOOL_LANG]["Open DNA Viewer"], color="#B8E6B3", hover_color="#C4F2BF", pressed_color="#A3D99E")
|
|
||||||
|
|
||||||
# Import group
|
|
||||||
self.import_btn = MainButton(LANG[TOOL_LANG]["Import"])
|
|
||||||
self.batch_import_btn = MainButton(LANG[TOOL_LANG]["Batch Import"], color="#A7C6ED", hover_color="#B2D3F0", pressed_color="#8BB8E0")
|
|
||||||
|
|
||||||
>>>>>>> Stashed changes
|
|
||||||
# Bottom buttons (existing code)
|
|
||||||
self.help_btn = BottomButton(LANG[TOOL_LANG]["Help"])
|
|
||||||
self.help_btn.setToolTip(LANG[TOOL_LANG]["Help"])
|
|
||||||
self.help_btn.setFixedSize(100, 20)
|
|
||||||
|
|
||||||
self.lang_btn = BottomButton(LANG[TOOL_LANG]["ZH" if TOOL_LANG == 'en_US' else "EN"])
|
|
||||||
self.lang_btn.setToolTip(LANG[TOOL_LANG]["Switch Language"])
|
|
||||||
self.lang_btn.setFixedSize(30, 20)
|
|
||||||
|
|
||||||
for button in [self.help_btn, self.lang_btn]:
|
|
||||||
button.setFont(QtGui.QFont("Microsoft YaHei", 10))
|
|
||||||
|
|
||||||
def create_layouts(self):
|
|
||||||
main_layout = QtWidgets.QVBoxLayout(self)
|
|
||||||
main_layout.setContentsMargins(2, 2, 2, 2)
|
|
||||||
content_layout = QtWidgets.QVBoxLayout()
|
|
||||||
content_layout.setContentsMargins(5, 5, 5, 5)
|
|
||||||
bottom_layout = QtWidgets.QHBoxLayout()
|
|
||||||
bottom_layout.setContentsMargins(5, 0, 5, 5)
|
|
||||||
|
|
||||||
prepare_group = QtWidgets.QGroupBox("Prepare")
|
|
||||||
prepare_layout = QtWidgets.QVBoxLayout(prepare_group)
|
|
||||||
prepare_layout.addWidget(self.body_prepare_btn)
|
|
||||||
content_layout.addWidget(prepare_group)
|
|
||||||
|
|
||||||
import_group = QtWidgets.QGroupBox("Import")
|
|
||||||
import_layout = QtWidgets.QVBoxLayout(import_group)
|
|
||||||
import_layout.addWidget(self.batch_import_btn)
|
|
||||||
content_layout.addWidget(import_group)
|
|
||||||
|
|
||||||
<<<<<<< Updated upstream
|
|
||||||
main_layout.addLayout(content_layout)
|
|
||||||
main_layout.addStretch()
|
|
||||||
|
|
||||||
# Bottom layout (existing code)
|
|
||||||
bottom_layout = QtWidgets.QHBoxLayout()
|
|
||||||
bottom_layout.setContentsMargins(5, 0, 5, 5)
|
|
||||||
|
|
||||||
=======
|
|
||||||
dna_edit_group = QtWidgets.QGroupBox("DNA Edit")
|
|
||||||
dna_edit_layout = QtWidgets.QVBoxLayout(dna_edit_group)
|
|
||||||
dna_edit_layout.addWidget(self.dna_viewer_btn)
|
|
||||||
content_layout.addWidget(dna_edit_group)
|
|
||||||
|
|
||||||
>>>>>>> Stashed changes
|
|
||||||
icon_label = QtWidgets.QLabel()
|
|
||||||
if os.path.exists(TOOL_ICON):
|
|
||||||
icon = QtGui.QPixmap(TOOL_ICON).scaled(24, 24, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
|
|
||||||
icon_label.setPixmap(icon)
|
|
||||||
|
|
||||||
version_label = QtWidgets.QLabel(f"{TOOL_VERSION}")
|
|
||||||
version_label.setStyleSheet("color: gray; font-size: 12px;")
|
|
||||||
|
|
||||||
bottom_layout.addWidget(icon_label)
|
|
||||||
bottom_layout.addWidget(version_label)
|
|
||||||
bottom_layout.addWidget(self.help_btn)
|
|
||||||
bottom_layout.addWidget(self.lang_btn)
|
|
||||||
|
|
||||||
main_layout.addLayout(content_layout)
|
|
||||||
main_layout.addLayout(bottom_layout)
|
|
||||||
main_layout.addStretch()
|
|
||||||
|
|
||||||
def create_connections(self):
|
|
||||||
self.body_prepare_btn.clicked.connect(self.run_body_prepare)
|
|
||||||
self.dna_viewer_btn.clicked.connect(self.run_dna_viewer)
|
|
||||||
<<<<<<< Updated upstream
|
|
||||||
|
|
||||||
# Existing connections
|
|
||||||
=======
|
|
||||||
self.batch_import_btn.clicked.connect(self.run_batch_import)
|
|
||||||
>>>>>>> Stashed changes
|
|
||||||
self.help_btn.clicked.connect(self.help)
|
|
||||||
self.lang_btn.clicked.connect(self.switch_language)
|
|
||||||
|
|
||||||
#===================================== FUNCTIONS =====================================
|
|
||||||
#===================================== MAIN FUNCTIONS =====================================
|
|
||||||
def run_body_prepare(self):
|
|
||||||
BodyPrep.run()
|
|
||||||
|
|
||||||
def run_batch_import(self):
|
|
||||||
BatchImport.run()
|
|
||||||
|
|
||||||
def run_dna_viewer(self):
|
|
||||||
<<<<<<< Updated upstream
|
|
||||||
import DNA_Viewer
|
|
||||||
DNA_Viewer.show()
|
|
||||||
|
|
||||||
=======
|
|
||||||
DNA_Viewer.show()
|
|
||||||
>>>>>>> Stashed changes
|
|
||||||
|
|
||||||
#===================================== BOTTOM LAYOUT =====================================
|
|
||||||
def help(self):
|
|
||||||
webbrowser.open(TOOL_HELP_URL)
|
|
||||||
|
|
||||||
def switch_language(self):
|
|
||||||
global TOOL_LANG
|
|
||||||
TOOL_LANG = 'en_US' if TOOL_LANG == 'zh_CN' else 'zh_CN'
|
|
||||||
self.lang_btn.setText("ZH" if TOOL_LANG == 'en_US' else "EN")
|
|
||||||
self.retranslate_ui()
|
|
||||||
QtWidgets.QToolTip.showText(
|
|
||||||
self.lang_btn.mapToGlobal(QtCore.QPoint(0, -30)),
|
|
||||||
"Language switched" if TOOL_LANG == 'en_US' else "语言已切换",
|
|
||||||
self.lang_btn
|
|
||||||
)
|
|
||||||
|
|
||||||
def retranslate_ui(self):
|
|
||||||
<<<<<<< Updated upstream
|
|
||||||
|
|
||||||
# Update function button translations
|
|
||||||
=======
|
|
||||||
>>>>>>> Stashed changes
|
|
||||||
self.load_dna_btn.setText(LANG[TOOL_LANG]["Load DNA"])
|
|
||||||
self.body_prepare_btn.setText(LANG[TOOL_LANG]["Body Prepare"])
|
|
||||||
self.dna_viewer_btn.setText(LANG[TOOL_LANG]["Open DNA Viewer"])
|
|
||||||
self.batch_import_btn.setText(LANG[TOOL_LANG]["Batch Import"])
|
|
||||||
|
|
||||||
self.help_btn.setText(LANG[TOOL_LANG]["Help"])
|
|
||||||
self.lang_btn.setText("ZH" if TOOL_LANG == 'en_US' else "EN")
|
|
||||||
|
|
||||||
for button in [self.body_prepare_btn, self.dna_viewer_btn, self.batch_import_btn]:
|
|
||||||
button.setFont(QtGui.QFont("Microsoft YaHei", 10))
|
|
||||||
|
|
||||||
for button in [self.help_btn, self.lang_btn]:
|
|
||||||
button.setFont(QtGui.QFont("Microsoft YaHei", 10))
|
|
||||||
|
|
||||||
self.dna_file_label.setText(LANG[TOOL_LANG]["DNA File:"])
|
|
||||||
|
|
||||||
def on_dna_selected(self, dna_path):
|
|
||||||
"""当DNA被选中时"""
|
|
||||||
global DNA_File
|
|
||||||
DNA_File = dna_path
|
|
||||||
self.dna_file_input.setText(DNA_File)
|
|
||||||
print(f"Selected DNA file: {DNA_File}")
|
|
||||||
|
|
||||||
def on_dna_file_changed(self):
|
|
||||||
"""当DNA文件输入框内容改变时"""
|
|
||||||
global DNA_File
|
|
||||||
DNA_File = self.dna_file_input.text()
|
|
||||||
print(f"DNA file path updated: {DNA_File}")
|
|
||||||
|
|
||||||
#===================================== LAUNCH FUNCTIONS =====================================
|
|
||||||
def show():
|
|
||||||
return MainWindow.show_window()
|
|
@ -1,23 +0,0 @@
|
|||||||
from .api import build_meshes, build_rig
|
|
||||||
from .builder.config import Config, RigConfig
|
|
||||||
from .builder.maya.skin_weights import (
|
|
||||||
get_skin_weights_from_scene,
|
|
||||||
set_skin_weights_to_scene,
|
|
||||||
)
|
|
||||||
from .dnalib.dnalib import DNA
|
|
||||||
from .dnalib.layer import Layer
|
|
||||||
from .ui.app import show
|
|
||||||
from .version import __version__
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"DNA",
|
|
||||||
"build_rig",
|
|
||||||
"build_meshes",
|
|
||||||
"show",
|
|
||||||
"get_skin_weights_from_scene",
|
|
||||||
"set_skin_weights_to_scene",
|
|
||||||
"Config",
|
|
||||||
"RigConfig",
|
|
||||||
"Layer",
|
|
||||||
"__version__",
|
|
||||||
]
|
|
@ -1,42 +0,0 @@
|
|||||||
from typing import Optional
|
|
||||||
|
|
||||||
from .builder.builder import Builder, BuildResult
|
|
||||||
from .builder.config import Config, RigConfig
|
|
||||||
from .builder.rig_builder import RigBuilder
|
|
||||||
from .dnalib.dnalib import DNA
|
|
||||||
|
|
||||||
|
|
||||||
def build_rig(dna: DNA, config: RigConfig) -> BuildResult:
|
|
||||||
"""
|
|
||||||
Used for assembling the rig with provided configuration.
|
|
||||||
|
|
||||||
@type config: DNA
|
|
||||||
@param config: Instance of DNA
|
|
||||||
|
|
||||||
@type config: Config
|
|
||||||
@param config: Instance of configuration
|
|
||||||
|
|
||||||
@rtype: BuildResult
|
|
||||||
@returns: The object representing result of build
|
|
||||||
"""
|
|
||||||
|
|
||||||
return RigBuilder(dna, config).build()
|
|
||||||
|
|
||||||
|
|
||||||
def build_meshes(dna: DNA, config: Optional[Config] = None) -> BuildResult:
|
|
||||||
"""
|
|
||||||
Starts the mesh building process with the provided configuration.
|
|
||||||
|
|
||||||
@type config: DNA
|
|
||||||
@param config: Instance of DNA
|
|
||||||
|
|
||||||
@type config: Config
|
|
||||||
@param config: Instance of configuration
|
|
||||||
|
|
||||||
@rtype: BuildResult
|
|
||||||
@returns: The object representing result of build
|
|
||||||
"""
|
|
||||||
if config is None:
|
|
||||||
config = Config()
|
|
||||||
|
|
||||||
return Builder(dna, config).build()
|
|
@ -1,433 +0,0 @@
|
|||||||
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))
|
|
@ -1,257 +0,0 @@
|
|||||||
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>"
|
|
||||||
)
|
|
@ -1,78 +0,0 @@
|
|||||||
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)
|
|
@ -1,421 +0,0 @@
|
|||||||
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)}")
|
|
@ -1,201 +0,0 @@
|
|||||||
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],
|
|
||||||
)
|
|
@ -1,81 +0,0 @@
|
|||||||
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)
|
|
@ -1,114 +0,0 @@
|
|||||||
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)
|
|
@ -1,290 +0,0 @@
|
|||||||
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)
|
|
@ -1,11 +0,0 @@
|
|||||||
ANALOG_GUI_HOLDER = "analog_gui"
|
|
||||||
|
|
||||||
GUI_HOLDER = "gui"
|
|
||||||
|
|
||||||
RIG_LOGIC_PREFIX = "rl4Embedded_"
|
|
||||||
|
|
||||||
SKIN_WEIGHT_PRINT_RANGE = 2000
|
|
||||||
|
|
||||||
|
|
||||||
class DNAViewerError(Exception):
|
|
||||||
pass
|
|
@ -1,371 +0,0 @@
|
|||||||
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)
|
|
@ -1,333 +0,0 @@
|
|||||||
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)
|
|
@ -1,129 +0,0 @@
|
|||||||
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()
|
|
@ -1,250 +0,0 @@
|
|||||||
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)
|
|
@ -1,283 +0,0 @@
|
|||||||
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
|
|
@ -1,9 +0,0 @@
|
|||||||
from enum import Enum
|
|
||||||
|
|
||||||
|
|
||||||
class Layer(Enum):
|
|
||||||
descriptor = 1
|
|
||||||
definition = 2
|
|
||||||
behavior = 3
|
|
||||||
geometry = 4
|
|
||||||
all = 5
|
|
@ -1,175 +0,0 @@
|
|||||||
from dataclasses import dataclass, field
|
|
||||||
from typing import Dict, List
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Point3:
|
|
||||||
"""
|
|
||||||
A model class for representing a 3 dimensional point
|
|
||||||
|
|
||||||
Attributes
|
|
||||||
----------
|
|
||||||
@type x: float
|
|
||||||
@param x: The value of x
|
|
||||||
|
|
||||||
@type y: float
|
|
||||||
@param y: The value of y
|
|
||||||
|
|
||||||
@type z: float
|
|
||||||
@param z: The value of z
|
|
||||||
"""
|
|
||||||
|
|
||||||
x: float = field(default=0.0)
|
|
||||||
y: float = field(default=0.0)
|
|
||||||
z: float = field(default=0.0)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class UV:
|
|
||||||
"""
|
|
||||||
A model class for holding data about the UV
|
|
||||||
|
|
||||||
Attributes
|
|
||||||
----------
|
|
||||||
@type u: float
|
|
||||||
@param u: The value of u
|
|
||||||
|
|
||||||
@type v: float
|
|
||||||
@param v: The value of v
|
|
||||||
"""
|
|
||||||
|
|
||||||
u: float = field(default=0.0)
|
|
||||||
v: float = field(default=0.0)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Layout:
|
|
||||||
"""
|
|
||||||
A model class for holding data about a single layout
|
|
||||||
|
|
||||||
Attributes
|
|
||||||
----------
|
|
||||||
@type position_index: int
|
|
||||||
@param position_index: An index representing position
|
|
||||||
|
|
||||||
@type texture_coordinate_index: int
|
|
||||||
@param texture_coordinate_index: A value representing the texture coordinate index
|
|
||||||
"""
|
|
||||||
|
|
||||||
position_index: int = field(default=0)
|
|
||||||
texture_coordinate_index: int = field(default=0)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Topology:
|
|
||||||
"""
|
|
||||||
A model class for holding data about the topology
|
|
||||||
|
|
||||||
Attributes
|
|
||||||
----------
|
|
||||||
@type positions: List[Point3]
|
|
||||||
@param positions: List of points in space representing the positions
|
|
||||||
|
|
||||||
@type texture_coordinates: List[UV]
|
|
||||||
@param texture_coordinates: List of UVs representing the positions
|
|
||||||
|
|
||||||
@type layouts: List[Layout]
|
|
||||||
@param layouts: The list of Layout mappings
|
|
||||||
|
|
||||||
@type face_vertex_layouts: List[List[int]]
|
|
||||||
@param face_vertex_layouts: List of face vertex layout indices by face index
|
|
||||||
"""
|
|
||||||
|
|
||||||
positions: List[Point3] = field(default_factory=list)
|
|
||||||
texture_coordinates: List[UV] = field(default_factory=list)
|
|
||||||
layouts: List[Layout] = field(default_factory=list)
|
|
||||||
face_vertex_layouts: List[List[int]] = field(default_factory=list)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class BlendShape:
|
|
||||||
"""
|
|
||||||
A model class for holding data about the blend shape
|
|
||||||
|
|
||||||
Attributes
|
|
||||||
----------
|
|
||||||
@type channel: int
|
|
||||||
@param channel: The index pointing to the blend shape name
|
|
||||||
|
|
||||||
@type deltas: Dict[int, Point3]
|
|
||||||
@param deltas: A mapping of blend shape indices to the coordinate differences that are made by the blend shape
|
|
||||||
"""
|
|
||||||
|
|
||||||
channel: int = field(default=None)
|
|
||||||
deltas: Dict[int, Point3] = field(default_factory=dict)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class SkinWeightsData:
|
|
||||||
"""
|
|
||||||
A model class for holding data about the skin weights
|
|
||||||
|
|
||||||
Attributes
|
|
||||||
----------
|
|
||||||
|
|
||||||
@type values: List[List[float]]
|
|
||||||
@param values: The skin weight values per vertex index
|
|
||||||
|
|
||||||
@type joint_indices: List[List[int]]
|
|
||||||
@param joint_indices: The joint indces per vertex index
|
|
||||||
"""
|
|
||||||
|
|
||||||
values: List[List[float]] = field(default_factory=list)
|
|
||||||
joint_indices: List[List[int]] = field(default_factory=list)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Mesh:
|
|
||||||
"""
|
|
||||||
A model class for holding data about the mesh
|
|
||||||
|
|
||||||
Attributes
|
|
||||||
----------
|
|
||||||
@type name: str
|
|
||||||
@param name: The name of the mesh
|
|
||||||
|
|
||||||
@type topology: Topology
|
|
||||||
@param topology: Data containing the topology of the mesh
|
|
||||||
|
|
||||||
@type skin_weights: SkinWeightsData
|
|
||||||
@param skin_weights: Data representing skin weights
|
|
||||||
|
|
||||||
@type blend_shapes: List[BlendShape]
|
|
||||||
@param blend_shapes: The list of blend shapes for the mesh
|
|
||||||
"""
|
|
||||||
|
|
||||||
name: str = field(default=None)
|
|
||||||
topology: Topology = field(default_factory=Topology)
|
|
||||||
skin_weights: SkinWeightsData = field(default_factory=SkinWeightsData)
|
|
||||||
blend_shapes: List[BlendShape] = field(default_factory=list)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Joint:
|
|
||||||
"""
|
|
||||||
A model class for holding data about a single joint
|
|
||||||
|
|
||||||
Attributes
|
|
||||||
----------
|
|
||||||
@type name: str
|
|
||||||
@param name: The name of the joint
|
|
||||||
|
|
||||||
@type translation: Point3
|
|
||||||
@param translation: A point in 3 dimensional space which represents the translation of the joint
|
|
||||||
|
|
||||||
@type orientation: Point3
|
|
||||||
@param orientation: A point in 3 dimensional space which represents the orientation of the joint
|
|
||||||
|
|
||||||
@type parent_name: str
|
|
||||||
@param parent_name: The name of the parent joint
|
|
||||||
"""
|
|
||||||
|
|
||||||
name: str = field(default=None)
|
|
||||||
translation: Point3 = field(default_factory=Point3)
|
|
||||||
orientation: Point3 = field(default_factory=Point3)
|
|
||||||
parent_name: str = field(default=None)
|
|
@ -1,66 +0,0 @@
|
|||||||
/* Global CSS */
|
|
||||||
|
|
||||||
* {
|
|
||||||
font-size: 9pt;
|
|
||||||
font-family: "Open Sans";
|
|
||||||
}
|
|
||||||
|
|
||||||
/* General CSS */
|
|
||||||
|
|
||||||
QWidget {
|
|
||||||
color: #DDD;
|
|
||||||
}
|
|
||||||
|
|
||||||
QWidget:disabled {
|
|
||||||
color: #888;
|
|
||||||
}
|
|
||||||
|
|
||||||
QMainWindow {
|
|
||||||
background-color: #444;
|
|
||||||
}
|
|
||||||
|
|
||||||
QDialog {
|
|
||||||
background-color: #444;
|
|
||||||
}
|
|
||||||
|
|
||||||
QToolTip {
|
|
||||||
color: #DDD;
|
|
||||||
border: #DDD solid 1px;
|
|
||||||
background-color: #111;
|
|
||||||
}
|
|
||||||
|
|
||||||
QToolButton {
|
|
||||||
width: 27px;
|
|
||||||
height: 27px;
|
|
||||||
background-color: #555;
|
|
||||||
border: 1px solid #333;
|
|
||||||
border-radius: 2px;
|
|
||||||
font-size: 12pt;
|
|
||||||
font-family: "Material Icons";
|
|
||||||
}
|
|
||||||
|
|
||||||
QPushButton {
|
|
||||||
height: 27px;
|
|
||||||
min-width: 60px;
|
|
||||||
background-color: #555;
|
|
||||||
border: 1px solid #333;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QToolButton:disabled,
|
|
||||||
QPushButton:disabled {
|
|
||||||
color: #888;
|
|
||||||
}
|
|
||||||
|
|
||||||
QToolButton:hover:!pressed,
|
|
||||||
QPushButton:hover:!pressed {
|
|
||||||
background-color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
QTextEdit,
|
|
||||||
QLineEdit {
|
|
||||||
background-color: #333;
|
|
||||||
border: 1px solid #222;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
@ -1,111 +0,0 @@
|
|||||||
from pathlib import Path
|
|
||||||
from typing import Callable, Optional
|
|
||||||
|
|
||||||
from PySide2.QtCore import Qt
|
|
||||||
from PySide2.QtWidgets import (
|
|
||||||
QFileDialog,
|
|
||||||
QFrame,
|
|
||||||
QHBoxLayout,
|
|
||||||
QLabel,
|
|
||||||
QLineEdit,
|
|
||||||
QPushButton,
|
|
||||||
QWidget,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class QLine(QFrame):
|
|
||||||
"""A widget for creating a horizontal line"""
|
|
||||||
|
|
||||||
def __init__(self, line: QFrame.Shape) -> None:
|
|
||||||
super().__init__()
|
|
||||||
self.setFrameShape(line)
|
|
||||||
self.setFrameShadow(QFrame.Sunken)
|
|
||||||
|
|
||||||
|
|
||||||
class QHLine(QLine):
|
|
||||||
"""A widget for creating a horizontal line"""
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
super().__init__(QFrame.HLine)
|
|
||||||
|
|
||||||
|
|
||||||
class FileChooser(QWidget):
|
|
||||||
"""
|
|
||||||
A custom widget used for selecting a file path using a FileDialog and an input field
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
label_text: str,
|
|
||||||
hint: str,
|
|
||||||
parent: Optional[QWidget] = None,
|
|
||||||
placeholder: str = "",
|
|
||||||
dialog_caption: str = "Select a file",
|
|
||||||
dialog_filter: str = "All files (*.*)",
|
|
||||||
button_text: str = "...",
|
|
||||||
dir_selector: bool = False,
|
|
||||||
on_changed: Callable[[int], None] = None,
|
|
||||||
) -> None:
|
|
||||||
super().__init__(parent=parent)
|
|
||||||
|
|
||||||
self._dialog_caption = dialog_caption
|
|
||||||
self._dialog_filter = dialog_filter
|
|
||||||
self._dir_selector = dir_selector
|
|
||||||
|
|
||||||
layout = QHBoxLayout()
|
|
||||||
layout.setMargin(0)
|
|
||||||
|
|
||||||
fc_label = QLabel(label_text)
|
|
||||||
fc_label.setMinimumHeight(32)
|
|
||||||
fc_label.setToolTip(hint)
|
|
||||||
|
|
||||||
self.fc_text_field = QLineEdit()
|
|
||||||
self.fc_text_field.setAlignment(Qt.AlignLeft)
|
|
||||||
self.fc_text_field.setPlaceholderText(placeholder)
|
|
||||||
self.fc_text_field.textChanged.connect(on_changed)
|
|
||||||
self.fc_text_field.setToolTip(hint)
|
|
||||||
|
|
||||||
fc_btn = QPushButton(button_text)
|
|
||||||
fc_btn.setToolTip(hint)
|
|
||||||
|
|
||||||
layout.addWidget(fc_label)
|
|
||||||
layout.addWidget(self.fc_text_field)
|
|
||||||
layout.addWidget(fc_btn)
|
|
||||||
|
|
||||||
fc_btn.clicked.connect(
|
|
||||||
self.open_dialog,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.setLayout(layout)
|
|
||||||
|
|
||||||
def get_file_path(self) -> str:
|
|
||||||
"""
|
|
||||||
Gets the file path from the text field
|
|
||||||
|
|
||||||
@rtype: str
|
|
||||||
@returns: The file path contained in the text field
|
|
||||||
"""
|
|
||||||
|
|
||||||
path = str(self.fc_text_field.text())
|
|
||||||
if path and Path(path.strip()).exists():
|
|
||||||
return path
|
|
||||||
return None
|
|
||||||
|
|
||||||
def open_dialog(self) -> None:
|
|
||||||
"""Opens a file dialog, when a path is chosen, the text field gets filled with its value"""
|
|
||||||
|
|
||||||
if self._dir_selector:
|
|
||||||
file_name, _ = QFileDialog.getExistingDirectory(
|
|
||||||
self,
|
|
||||||
self._dialog_caption,
|
|
||||||
"",
|
|
||||||
QFileDialog.Option.ShowDirsOnly,
|
|
||||||
)
|
|
||||||
if file_name:
|
|
||||||
self.fc_text_field.setText(file_name)
|
|
||||||
else:
|
|
||||||
file_name, _ = QFileDialog.getOpenFileName(
|
|
||||||
self, self._dialog_caption, "", self._dialog_filter
|
|
||||||
)
|
|
||||||
if file_name:
|
|
||||||
self.fc_text_field.setText(file_name)
|
|
@ -1 +0,0 @@
|
|||||||
__version__ = "2.1.1"
|
|
Loading…
Reference in New Issue
Block a user