Updated
This commit is contained in:
13
Scripts/Animation/epic_pose_wrangler/model/api.py
Normal file
13
Scripts/Animation/epic_pose_wrangler/model/api.py
Normal file
@ -0,0 +1,13 @@
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
class RBFAPI(object):
|
||||
"""
|
||||
Base class for creating RBF API classes
|
||||
"""
|
||||
UPGRADE_AVAILABLE = False
|
||||
VERSION = "0.0.0"
|
||||
|
||||
def __init__(self, view=False, parent=None, file_path=None):
|
||||
super(RBFAPI, self).__init__()
|
||||
self._view = view
|
||||
self._parent = parent
|
||||
self._file_path = file_path
|
37
Scripts/Animation/epic_pose_wrangler/model/exceptions.py
Normal file
37
Scripts/Animation/epic_pose_wrangler/model/exceptions.py
Normal file
@ -0,0 +1,37 @@
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
from epic_pose_wrangler.log import LOG
|
||||
|
||||
class PoseWranglerException(Exception):
|
||||
"""
|
||||
Base Exception for PoseWrangler related errors
|
||||
"""
|
||||
|
||||
def __init__(self, message):
|
||||
super(PoseWranglerException, self).__init__(message)
|
||||
# Log the message as an error
|
||||
LOG.error(message)
|
||||
|
||||
class InvalidPoseWranglerPlugin(PoseWranglerException, RuntimeError):
|
||||
"""
|
||||
Exception raised when no valid plugins could be loaded
|
||||
"""
|
||||
|
||||
class PoseWranglerSettingsError(PoseWranglerException):
|
||||
"""
|
||||
Raised when a setting is invalid
|
||||
"""
|
||||
|
||||
class InvalidMirrorMapping(PoseWranglerSettingsError):
|
||||
"""
|
||||
Raised when the mirror mapping is incorrect
|
||||
"""
|
||||
|
||||
class PoseWranglerIOError(PoseWranglerException):
|
||||
"""
|
||||
Raised when issues with serialization/deserialization arise
|
||||
"""
|
||||
|
||||
class PoseWranglerFunctionalityNotImplemented(PoseWranglerException):
|
||||
"""
|
||||
Raised when pose wrangler functionality hasn't been implemented yet
|
||||
"""
|
125
Scripts/Animation/epic_pose_wrangler/model/mirror_mapping.py
Normal file
125
Scripts/Animation/epic_pose_wrangler/model/mirror_mapping.py
Normal file
@ -0,0 +1,125 @@
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
"""
|
||||
Example Mapping (MetaHuman)
|
||||
{
|
||||
# Regular expression to validate that the solver follows the correct naming convention for mirroring
|
||||
"solver_expression": "(?P<prefix>[a-zA-Z0-9]+)?(?P<side>_[lr]{1}_)(?P<suffix>[a-zA-Z0-9]+)",
|
||||
# Regular expression to validate that the joint follows the correct naming convention for mirroring
|
||||
"transform_expression": "(?P<prefix>[a-zA-Z0-9]+)?(?P<side>_[lr]{1}_)(?P<suffix>[a-zA-Z0-9]+)",
|
||||
"left": {
|
||||
# Left side syntax for the solver
|
||||
"solver_syntax": "_l_",
|
||||
# Left side syntax for the joint
|
||||
"transform_syntax": "_l_"
|
||||
},
|
||||
"right": {
|
||||
# Right side syntax for the solver
|
||||
"solver_syntax": "_r_",
|
||||
# Right side syntax for the joint
|
||||
"transform_syntax": "_r_"
|
||||
}
|
||||
}
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
|
||||
from epic_pose_wrangler.log import LOG
|
||||
|
||||
class MirrorMapping(object):
|
||||
"""
|
||||
Class for managing mirror settings
|
||||
"""
|
||||
LEFT = "left"
|
||||
RIGHT = "right"
|
||||
|
||||
def __init__(self, file_path=None, source_side="left"):
|
||||
# Make a list of valid mappings that should exist in the mirror mapping file
|
||||
self._valid_mappings = [MirrorMapping.LEFT, MirrorMapping.RIGHT]
|
||||
# If no file path is specified, use the MetaHuman config as the fallback
|
||||
if file_path is None:
|
||||
LOG.debug("No mirror mapping specified, using default MetaHuman conventions")
|
||||
file_path = os.path.join(
|
||||
os.path.dirname(os.path.dirname(__file__)),
|
||||
'resources',
|
||||
'mirror_mappings',
|
||||
'metahuman.json'
|
||||
)
|
||||
self._file_path = file_path
|
||||
# Load the json mapping data
|
||||
with open(file_path, 'r') as f:
|
||||
self._mapping_data = json.loads(f.read())
|
||||
|
||||
# Set the solver expression from the file
|
||||
self._solver_expression = self._mapping_data['solver_expression']
|
||||
# Set the transform expression from the file
|
||||
self._transform_expression = self._mapping_data['transform_expression']
|
||||
|
||||
# Set the source side and create defaults
|
||||
self._source_side = source_side
|
||||
self._source_mapping_data = {}
|
||||
self._source_solver_syntax = ""
|
||||
self._source_transform_syntax = ""
|
||||
|
||||
self._target_mapping_data = {}
|
||||
self._target_solver_syntax = ""
|
||||
self._target_transform_syntax = ""
|
||||
# Set the source side property to trigger the default values to be updated
|
||||
self.source_side = source_side
|
||||
|
||||
@property
|
||||
def file_path(self):
|
||||
return self._file_path
|
||||
|
||||
@property
|
||||
def solver_expression(self):
|
||||
return self._solver_expression
|
||||
|
||||
@property
|
||||
def transform_expression(self):
|
||||
return self._transform_expression
|
||||
|
||||
@property
|
||||
def source_side(self):
|
||||
return self._source_side
|
||||
|
||||
@source_side.setter
|
||||
def source_side(self, side):
|
||||
"""
|
||||
Sets the source side and updates the source/target values accordingly
|
||||
:param side: MirrorMapping.LEFT or MirrorMapping.RIGHT
|
||||
"""
|
||||
if side not in self._valid_mappings:
|
||||
raise ValueError("Invalid side specified, options are: {}".format(", ".join(self._valid_mappings)))
|
||||
self._source_side = side
|
||||
self._source_mapping_data = self._mapping_data[self._source_side]
|
||||
self._source_solver_syntax = self._source_mapping_data['solver_syntax']
|
||||
self._source_transform_syntax = self._source_mapping_data['transform_syntax']
|
||||
|
||||
self._target_mapping_data = self._mapping_data[
|
||||
MirrorMapping.RIGHT if self._source_side == MirrorMapping.LEFT else MirrorMapping.LEFT]
|
||||
self._target_solver_syntax = self._target_mapping_data['solver_syntax']
|
||||
self._target_transform_syntax = self._target_mapping_data['transform_syntax']
|
||||
|
||||
@property
|
||||
def source_solver_syntax(self):
|
||||
return self._source_solver_syntax
|
||||
|
||||
@property
|
||||
def source_transform_syntax(self):
|
||||
return self._source_transform_syntax
|
||||
|
||||
@property
|
||||
def target_solver_syntax(self):
|
||||
return self._target_solver_syntax
|
||||
|
||||
@property
|
||||
def target_transform_syntax(self):
|
||||
return self._target_transform_syntax
|
||||
|
||||
def swap_sides(self):
|
||||
"""
|
||||
Swap the source side to the opposite of the current side.
|
||||
"""
|
||||
new_target = MirrorMapping.LEFT if self.source_side == MirrorMapping.RIGHT else MirrorMapping.RIGHT
|
||||
self.source_side = new_target
|
110
Scripts/Animation/epic_pose_wrangler/model/plugin_manager.py
Normal file
110
Scripts/Animation/epic_pose_wrangler/model/plugin_manager.py
Normal file
@ -0,0 +1,110 @@
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
from collections import OrderedDict
|
||||
|
||||
from maya import cmds
|
||||
|
||||
from epic_pose_wrangler.log import LOG
|
||||
from epic_pose_wrangler.model import exceptions
|
||||
|
||||
class PluginManager:
|
||||
"""
|
||||
Class for loading latest available plugin and managing pose_wrangler versions
|
||||
"""
|
||||
# The name of the recommended solver
|
||||
RECOMMENDED_SOLVER = "UERBFSolverNode"
|
||||
# Empty list to keep track of the loaded solver nodes
|
||||
LOADED_NODES = []
|
||||
# Generate an ordered manifest of known plugin name variants with the newest plugins
|
||||
__PLUGIN_VERSIONS = OrderedDict(
|
||||
{
|
||||
"MayaUERBFPlugin_{}".format(cmds.about(version=True)): "UERBFSolverNode",
|
||||
"MayaUERBFPlugin{}".format(cmds.about(version=True)): "UERBFSolverNode",
|
||||
"MayaUERBFPlugin": "UERBFSolverNode",
|
||||
"MayaUE4RBFPlugin_{}".format(cmds.about(version=True)): "UE4RBFSolverNode",
|
||||
"MayaUE4RBFPlugin{}".format(cmds.about(version=True)): "UE4RBFSolverNode",
|
||||
"MayaUE4RBFPlugin": "UE4RBFSolverNode"}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def load_plugin():
|
||||
"""
|
||||
Load any valid RBF plugins
|
||||
:return :type list: node names loaded
|
||||
"""
|
||||
|
||||
PluginManager.LOADED_NODES = []
|
||||
# Iterate through all of the valid plugin versions and attempt to load
|
||||
for plugin_name, solver_name in PluginManager.__PLUGIN_VERSIONS.items():
|
||||
# If the plugin is already loaded, add the solver name to the list of loaded nodes
|
||||
if cmds.pluginInfo(plugin_name, q=True, loaded=True) and solver_name not in PluginManager.LOADED_NODES:
|
||||
PluginManager.LOADED_NODES.append(solver_name)
|
||||
else:
|
||||
try:
|
||||
# Attempt to load the plugin
|
||||
cmds.loadPlugin(plugin_name)
|
||||
# If the solver name is not already in the list, add it
|
||||
if solver_name not in PluginManager.LOADED_NODES:
|
||||
PluginManager.LOADED_NODES.append(solver_name)
|
||||
# Ignore errors
|
||||
except RuntimeError as e:
|
||||
pass
|
||||
# If we have no loaded nodes no plugin loaded correctly
|
||||
if not PluginManager.LOADED_NODES:
|
||||
raise exceptions.InvalidPoseWranglerPlugin("Unable to load valid RBF plugin version.")
|
||||
|
||||
return PluginManager.LOADED_NODES
|
||||
|
||||
@staticmethod
|
||||
def is_scene_using_recommended_solver():
|
||||
"""
|
||||
Scan the current scene to find which version of the solver is being used
|
||||
:return :type bool: is the recommended solver being used for all RBF nodes
|
||||
"""
|
||||
solvers = []
|
||||
# Get a list of the solver names
|
||||
for solver_node_name in list(PluginManager.__PLUGIN_VERSIONS.values()):
|
||||
if solver_node_name not in solvers:
|
||||
solvers.append(solver_node_name)
|
||||
# Iterate through the solver names
|
||||
for solver_node_name in solvers:
|
||||
# Check if any solvers exist in the scene of the specified type and check if the solver name is the
|
||||
# recommended name. If we have old solvers in the scene, we aren't using the latest version.
|
||||
if cmds.ls(type=solver_node_name) and solver_node_name != PluginManager.RECOMMENDED_SOLVER:
|
||||
return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def get_pose_wrangler(view=True, parent=None, file_path=None):
|
||||
"""
|
||||
Get the correct version of the pose wrangler tool depending on the available plugins and nodes in the scene
|
||||
:param view :type bool: Should we be displaying a UI to the user?
|
||||
:param parent :type main.PoseWrangler: reference to the main entry point for the tool, used for
|
||||
restarting/upgrading the tool
|
||||
:param file_path :type str: (optional) path to a json file containing serialized solver data
|
||||
:return :type object: reference to the currently loaded version of pose wrangler
|
||||
"""
|
||||
# Load the RBF plugin
|
||||
loaded_nodes = PluginManager.load_plugin()
|
||||
# If the recommended solver is not loaded, fall back to the original pose wrangler implementation
|
||||
if PluginManager.RECOMMENDED_SOLVER not in loaded_nodes:
|
||||
LOG.warning("You are currently using an outdated plugin. Certain functionality may be limited.")
|
||||
from epic_pose_wrangler.v1 import main
|
||||
return main.UE4RBFAPI(view=view, parent=parent)
|
||||
# Bool to keep track of importing the newest api version
|
||||
import_failed = False
|
||||
# Check if the scene uses the latest solver
|
||||
if PluginManager.is_scene_using_recommended_solver():
|
||||
# Try and import the latest tool version
|
||||
try:
|
||||
from epic_pose_wrangler.v2 import main
|
||||
return main.UERBFAPI(view=view, parent=parent, file_path=file_path)
|
||||
except ImportError as e:
|
||||
LOG.error("Unable to import API v2, falling back to API v1 - {exception}".format(exception=e))
|
||||
import_failed = True
|
||||
# Fall back to API v1
|
||||
from epic_pose_wrangler.v1 import main
|
||||
# If the recommended solver is available but finds old nodes in the scene and imports correctly, provide
|
||||
# the option to upgrade to the latest version
|
||||
if not import_failed:
|
||||
main.UE4RBFAPI.UPGRADE_AVAILABLE = True
|
||||
return main.UE4RBFAPI(view=view, parent=parent)
|
47
Scripts/Animation/epic_pose_wrangler/model/settings.py
Normal file
47
Scripts/Animation/epic_pose_wrangler/model/settings.py
Normal file
@ -0,0 +1,47 @@
|
||||
import os
|
||||
|
||||
from Qt import QtCore
|
||||
|
||||
from epic_pose_wrangler.log import LOG
|
||||
from epic_pose_wrangler.model import exceptions
|
||||
|
||||
class SettingsManager(object):
|
||||
"""
|
||||
Settings Manager for reading/writing to PoseWrangler settings ini file
|
||||
"""
|
||||
QSETTINGS = None
|
||||
|
||||
def __init__(self):
|
||||
# Initialize the QSettings
|
||||
QtCore.QSettings.setPath(QtCore.QSettings.IniFormat, QtCore.QSettings.UserScope, os.environ['LOCALAPPDATA'])
|
||||
# Store the QSettings
|
||||
self.__class__.QSETTINGS = QtCore.QSettings(
|
||||
QtCore.QSettings.IniFormat,
|
||||
QtCore.QSettings.UserScope,
|
||||
"Epic Games",
|
||||
"PoseWrangler"
|
||||
)
|
||||
self.__class__.QSETTINGS.setFallbacksEnabled(False)
|
||||
LOG.debug("Successfully initialized SettingsManager")
|
||||
|
||||
@classmethod
|
||||
def get_setting(cls, name):
|
||||
"""
|
||||
Get the setting with the specified name
|
||||
:param name :type str: setting name
|
||||
:return :type str or None: setting value
|
||||
"""
|
||||
# If the settings haven't been initialized, raise exception
|
||||
if cls.QSETTINGS is None:
|
||||
raise exceptions.PoseWranglerSettingsError("Unable to load settings, "
|
||||
"{cls} must be initialized first".format(cls=cls))
|
||||
return cls.QSETTINGS.value(name, None)
|
||||
|
||||
@classmethod
|
||||
def set_setting(cls, name, value):
|
||||
"""
|
||||
Add/Overwrite the setting with the specified name and value
|
||||
:param name :type str: setting name
|
||||
:param value :type any: setting value
|
||||
"""
|
||||
cls.QSETTINGS.setValue(name, value)
|
Reference in New Issue
Block a user