This commit is contained in:
2025-04-17 04:52:48 +08:00
commit 9985b73dc1
3708 changed files with 2387532 additions and 0 deletions

View 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

View 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
"""

View 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

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

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