This commit is contained in:
Jeffreytsai1004 2025-01-13 23:39:54 +08:00
parent 691a4ffe69
commit dd2318e54b
27 changed files with 1 additions and 5153 deletions

View File

@ -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()


View File

@ -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__",
]

View File

@ -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()

View File

@ -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))

View File

@ -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>"
)

View File

@ -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)

View File

@ -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)}")

View File

@ -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],
)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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()

View File

@ -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)

View File

@ -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

View File

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

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -1 +0,0 @@
__version__ = "2.1.1"