diff --git a/Scripts/Animation/epic_pose_wrangler/plugins/Windows/2020/MayaUERBFPlugin.mll b/Scripts/Animation/epic_pose_wrangler/plugins/Windows/2020/MayaUERBFPlugin.mll new file mode 100644 index 0000000..9187eab Binary files /dev/null and b/Scripts/Animation/epic_pose_wrangler/plugins/Windows/2020/MayaUERBFPlugin.mll differ diff --git a/Scripts/Animation/epic_pose_wrangler/plugins/Windows/2022/MayaUE4RBFPlugin2022.mll b/Scripts/Animation/epic_pose_wrangler/plugins/Windows/2022/MayaUE4RBFPlugin2022.mll new file mode 100644 index 0000000..2a74e29 Binary files /dev/null and b/Scripts/Animation/epic_pose_wrangler/plugins/Windows/2022/MayaUE4RBFPlugin2022.mll differ diff --git a/Scripts/Animation/epic_pose_wrangler/plugins/Windows/2022/MayaUERBFPlugin.mll b/Scripts/Animation/epic_pose_wrangler/plugins/Windows/2022/MayaUERBFPlugin.mll new file mode 100644 index 0000000..e0d83d4 Binary files /dev/null and b/Scripts/Animation/epic_pose_wrangler/plugins/Windows/2022/MayaUERBFPlugin.mll differ diff --git a/Scripts/Animation/epic_pose_wrangler/plugins/Windows/2022/embeddedRL4.mll b/Scripts/Animation/epic_pose_wrangler/plugins/Windows/2022/embeddedRL4.mll new file mode 100644 index 0000000..586a76f Binary files /dev/null and b/Scripts/Animation/epic_pose_wrangler/plugins/Windows/2022/embeddedRL4.mll differ diff --git a/Scripts/Animation/epic_pose_wrangler/plugins/Windows/2023/MayaUE4RBFPlugin2023.mll b/Scripts/Animation/epic_pose_wrangler/plugins/Windows/2023/MayaUE4RBFPlugin2023.mll new file mode 100644 index 0000000..956e91b Binary files /dev/null and b/Scripts/Animation/epic_pose_wrangler/plugins/Windows/2023/MayaUE4RBFPlugin2023.mll differ diff --git a/Scripts/Animation/epic_pose_wrangler/plugins/Windows/2023/MayaUERBFPlugin.mll b/Scripts/Animation/epic_pose_wrangler/plugins/Windows/2023/MayaUERBFPlugin.mll new file mode 100644 index 0000000..8900114 Binary files /dev/null and b/Scripts/Animation/epic_pose_wrangler/plugins/Windows/2023/MayaUERBFPlugin.mll differ diff --git a/Scripts/Animation/epic_pose_wrangler/plugins/Windows/2023/embeddedRL4.mll b/Scripts/Animation/epic_pose_wrangler/plugins/Windows/2023/embeddedRL4.mll new file mode 100644 index 0000000..7ed5124 Binary files /dev/null and b/Scripts/Animation/epic_pose_wrangler/plugins/Windows/2023/embeddedRL4.mll differ diff --git a/Scripts/Animation/epic_pose_wrangler/plugins/Windows/2024/MayaUERBFPlugin.mll b/Scripts/Animation/epic_pose_wrangler/plugins/Windows/2024/MayaUERBFPlugin.mll new file mode 100644 index 0000000..d8e253d Binary files /dev/null and b/Scripts/Animation/epic_pose_wrangler/plugins/Windows/2024/MayaUERBFPlugin.mll differ diff --git a/Scripts/Animation/epic_pose_wrangler/plugins/Windows/2024/embeddedRL4.mll b/Scripts/Animation/epic_pose_wrangler/plugins/Windows/2024/embeddedRL4.mll new file mode 100644 index 0000000..9e1c849 Binary files /dev/null and b/Scripts/Animation/epic_pose_wrangler/plugins/Windows/2024/embeddedRL4.mll differ diff --git a/Scripts/Animation/epic_pose_wrangler/resources/icons/clear.png b/Scripts/Animation/epic_pose_wrangler/resources/icons/clear.png new file mode 100644 index 0000000..134ab12 Binary files /dev/null and b/Scripts/Animation/epic_pose_wrangler/resources/icons/clear.png differ diff --git a/Scripts/Animation/epic_pose_wrangler/resources/icons/debug.png b/Scripts/Animation/epic_pose_wrangler/resources/icons/debug.png new file mode 100644 index 0000000..9e9ea68 Binary files /dev/null and b/Scripts/Animation/epic_pose_wrangler/resources/icons/debug.png differ diff --git a/Scripts/Animation/epic_pose_wrangler/resources/icons/epic.png b/Scripts/Animation/epic_pose_wrangler/resources/icons/epic.png new file mode 100644 index 0000000..e280859 Binary files /dev/null and b/Scripts/Animation/epic_pose_wrangler/resources/icons/epic.png differ diff --git a/Scripts/Animation/epic_pose_wrangler/resources/icons/error.png b/Scripts/Animation/epic_pose_wrangler/resources/icons/error.png new file mode 100644 index 0000000..1d8bcc9 Binary files /dev/null and b/Scripts/Animation/epic_pose_wrangler/resources/icons/error.png differ diff --git a/Scripts/Animation/epic_pose_wrangler/resources/icons/frame_closed.png b/Scripts/Animation/epic_pose_wrangler/resources/icons/frame_closed.png new file mode 100644 index 0000000..48b1a24 Binary files /dev/null and b/Scripts/Animation/epic_pose_wrangler/resources/icons/frame_closed.png differ diff --git a/Scripts/Animation/epic_pose_wrangler/resources/icons/frame_open.png b/Scripts/Animation/epic_pose_wrangler/resources/icons/frame_open.png new file mode 100644 index 0000000..3ffa4b8 Binary files /dev/null and b/Scripts/Animation/epic_pose_wrangler/resources/icons/frame_open.png differ diff --git a/Scripts/Animation/epic_pose_wrangler/resources/icons/info.png b/Scripts/Animation/epic_pose_wrangler/resources/icons/info.png new file mode 100644 index 0000000..a57af1d Binary files /dev/null and b/Scripts/Animation/epic_pose_wrangler/resources/icons/info.png differ diff --git a/Scripts/Animation/epic_pose_wrangler/resources/icons/unreal.png b/Scripts/Animation/epic_pose_wrangler/resources/icons/unreal.png new file mode 100644 index 0000000..cc9cf78 Binary files /dev/null and b/Scripts/Animation/epic_pose_wrangler/resources/icons/unreal.png differ diff --git a/Scripts/Animation/epic_pose_wrangler/resources/icons/warning.png b/Scripts/Animation/epic_pose_wrangler/resources/icons/warning.png new file mode 100644 index 0000000..ac26b7a Binary files /dev/null and b/Scripts/Animation/epic_pose_wrangler/resources/icons/warning.png differ diff --git a/Scripts/Animation/epic_pose_wrangler/resources/mirror_mappings/metahuman.json b/Scripts/Animation/epic_pose_wrangler/resources/mirror_mappings/metahuman.json new file mode 100644 index 0000000..bd5967c --- /dev/null +++ b/Scripts/Animation/epic_pose_wrangler/resources/mirror_mappings/metahuman.json @@ -0,0 +1,12 @@ +{ + "solver_expression": "(?P[a-zA-Z0-9]+)?(?P_[lr]{1}_)(?P[a-zA-Z0-9_]+)", + "transform_expression": "(?P[a-zA-Z0-9_]+)?(?P_[lr]{1}_)(?P[a-zA-Z0-9_]+)", + "left": { + "solver_syntax": "_l_", + "transform_syntax": "_l_" + }, + "right": { + "solver_syntax": "_r_", + "transform_syntax": "_r_" + } +} \ No newline at end of file diff --git a/Scripts/Animation/epic_pose_wrangler/v1/__init__.py b/Scripts/Animation/epic_pose_wrangler/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Scripts/Animation/epic_pose_wrangler/v1/main.py b/Scripts/Animation/epic_pose_wrangler/v1/main.py new file mode 100644 index 0000000..2da5ee7 --- /dev/null +++ b/Scripts/Animation/epic_pose_wrangler/v1/main.py @@ -0,0 +1,30 @@ +# Copyright Epic Games, Inc. All Rights Reserved. +from epic_pose_wrangler.model.api import RBFAPI + +from epic_pose_wrangler.v1 import poseWranglerUI, poseWrangler + + +class UE4RBFAPI(RBFAPI): + VERSION = "1.0.0" + + def __init__(self, view=False, parent=None, file_path=None): + super(UE4RBFAPI, self).__init__(view=view, parent=parent) + if view: + self._view = poseWranglerUI.PoseWrangler() + self._view.event_upgrade_dispatch.upgrade.connect(self._upgrade) + self._view.show(dockable=True) + + @property + def view(self): + return self._view + + @property + def api_module(self): + return poseWrangler + + def _upgrade(self, file_path): + """ + Upgrade the scene to the new version + :param file_path :type str: file path to the exported scene data + """ + self._parent.upgrade(file_path, delete_file=True) diff --git a/Scripts/Animation/epic_pose_wrangler/v1/palette.py b/Scripts/Animation/epic_pose_wrangler/v1/palette.py new file mode 100644 index 0000000..c74a1cd --- /dev/null +++ b/Scripts/Animation/epic_pose_wrangler/v1/palette.py @@ -0,0 +1,147 @@ +# Copyright Epic Games, Inc. All Rights Reserved. +def getPaletteString(): + + + + bgColorA = "rgb(45, 56, 78)" + scrollWidgetBgColor = "(35, 45, 55)" + bgHighlightColorA = "rgb(121, 94, 102)" + + borderDarkColor = "rgb(21, 21, 21)" + + buttonLightColor = "rgb(62, 91, 122)" + buttonDisabledColor = "rgb(52, 81, 112)" + buttonLightPressedColor = "rgb(32, 61, 92)" + + fontDarkColor = "rgb(118, 93, 105)" + fontLightColor = "rgb(201, 201, 201)" + + selectedTabColor = "rgb(253, 251, 212)" + notSelectedTabColor = "rgb(220, 188, 156)" + + borderStr = "border: 1px groove " + borderDarkColor + ";" + + paletteStr = str( + "QWidget {font-family: Trebuchet MS; font: bold 11px;}" + "QMainWindow{" + "color: " + bgColorA + ";" + "background: " + bgColorA + ";" + "border: 0px solid white;" + "font: " + fontLightColor + ";" + "}" + "QLabel{" + "color: " + fontLightColor + ";" + "}" + "QLineEdit{" + + borderStr + + "color: " + fontDarkColor + ";" + "border-radius: 5px;" + "background: " + scrollWidgetBgColor + ";" + "color: " + fontLightColor + ";" + "}" + "QPushButton{" + "background: " + buttonLightColor + ";" + "color: " + fontLightColor + ";" + + "border-radius: 3px;" + "padding: 8px;" + "margin: 1px;" + "}" + "QPushButton:pressed{" + "background: " + buttonLightPressedColor + ";" + "}" + "QPushButton:!pressed{" + "background: " + buttonLightColor + ";" + "}" + "QPushButton:disabled{" + "background: " + buttonDisabledColor + ";" + "color: " + fontDarkColor + ";" + "}" + "QCheckBox{" + "background-color: " + bgColorA + ";" + "}" + "QMenu{" + "background-color: " + bgColorA + ";" + "border: 1px solid " + borderDarkColor + ";" + "color: " + fontDarkColor + ";" + "}" + "QMenu::item{" + "color: " + fontLightColor + ";" + "}" + "QMenu::item:selected {" + "background-color: " + bgHighlightColorA + ";" + "}" + "QTreeWidget{" + "background-color: " + scrollWidgetBgColor + ";" + + borderStr + + "color: " + fontDarkColor + ";" + "border-radius: 5px;" + "margin-top: 3px;" + "padding: 10px;" + "}" + "QTreeWidget::item{" + "color: " + fontLightColor + ";" + "}" + "QTreeWidget::item:selected {" + "background-color: " + bgHighlightColorA + ";" + "}" + "QListWidget{" + "background-color: " + scrollWidgetBgColor + ";" + + borderStr + + "color: " + fontDarkColor + ";" + "border-radius: 5px;" + "margin-top: 3px;" + "padding: 10px;" + "}" + "QGroupBox{" + "background-color: " + bgColorA + ";" + + borderStr + + "color: " + fontDarkColor + ";" + "border-radius: 5px;" + "margin-top: 10px;" + "padding: 10px;" + "}" + "QGroupBox::title{" + "subcontrol-origin: margin;" + "subcontrol-position: top center;" + "background-color: " + bgColorA + ";" + + borderStr + + "color: " + fontLightColor + ";" + "border-radius: 3px;" + "padding: 1px 3px;" + "}" + "QComboBox{" + "background: " + buttonLightColor + ";" + "color: " + fontDarkColor + ";" + "border-radius: 4px;" + "}" + "QComboBox::drop-down {" + "subcontrol-origin: padding;" + "subcontrol-position: top right;" + "border-style: none;" + "}" + "QTabWidget::pane{" + "background: " + bgColorA + ";" + + borderStr + + "color: " + fontDarkColor + ";" + "border-radius: 5px;" + "}" + "QTabWidget::tab-bar {" + "left: 5px;" + "padding:0px;" + "}" + "QTabBar::tab {" + "color: " + fontDarkColor + ";" + "padding:5px;" + "border-top-left-radius: 4px;" + "border-top-right-radius: 4px;" + "border: 1px solid " + borderDarkColor + ";" + "}" + "QTabBar::tab:selected{" + "background: " + selectedTabColor + ";" + "}" + "QTabBar::tab:!selected{" + "background: " + notSelectedTabColor + ";" + "}") + + return paletteStr diff --git a/Scripts/Animation/epic_pose_wrangler/v1/poseWrangler.py b/Scripts/Animation/epic_pose_wrangler/v1/poseWrangler.py new file mode 100644 index 0000000..2c664f5 --- /dev/null +++ b/Scripts/Animation/epic_pose_wrangler/v1/poseWrangler.py @@ -0,0 +1,993 @@ +# Copyright Epic Games, Inc. All Rights Reserved. +import traceback +import math +import json +from PySide2 import QtWidgets +import maya.cmds as cmds +import maya.api.OpenMaya as api + + +class UE4PoseDriver(object): + ''' + TODO: + - add a transform to an existing pose driver + - I was told that the Maya implementation only works with one driver, + not multiple transforms, this reflects that + - properly serialize the UI driving mode when opening the UI based on connections into the drivers + - validate the node network on load, knowing that a driven has driven, etc.. (low prio) + ''' + + def __init__(self, existing_interpolator=None): + + self.name = None + + self.solver = None + self.decompose_mx_list = [] + + self.copied_trs_map = {} + + if existing_interpolator: + if cmds.nodeType(existing_interpolator) == 'UE4RBFSolverNode': + self.name = existing_interpolator + self.solver = existing_interpolator + else: + print('Node', existing_interpolator, 'is not of type UE4RBFSolverNode.') + return + + ## General utils pulled from utils lib + ################################################################################# + def attrExists(self, attr): + if '.' in attr: + node, att = attr.split('.') + return cmds.attributeQuery(att, node=node, ex=1) + else: + cmds.warning('attrExists: No attr passed in: ' + attr) + return False + + def msgConnect(self, attribFrom, attribTo, debug=0): + objFrom, attFrom = attribFrom.split('.') + objTo, attTo = attribTo.split('.') + objTo, attTo = attribTo.split('.') + if debug: print('msgConnect>>> Locals:', locals()) + if not self.attrExists(attribFrom): + cmds.addAttr(objFrom, longName=attFrom, attributeType='message') + if not self.attrExists(attribTo): + cmds.addAttr(objTo, longName=attTo, attributeType='message') + # check that both atts, if existing are msg atts + for a in (attribTo, attribFrom): + if cmds.getAttr(a, type=1) != 'message': + cmds.warning('msgConnect: Attr, ' + a + ' is not a message attribute. CONNECTION ABORTED.') + return False + try: + return cmds.connectAttr(attribFrom, attribTo, f=True) + except Exception as e: + print(traceback.format_exc()) + return False + + def break_all_connections(self, node): + destination_conns = cmds.listConnections(node, plugs=True, connections=True, source=False) or [] + for i in range(0, len(destination_conns), 2): + cmds.disconnectAttr(destination_conns[i], destination_conns[i + 1]) + source_conns = cmds.listConnections(node, plugs=True, connections=True, destination=False) or [] + for i in range(0, len(source_conns), 2): + # we have to flip these because the output is always node centric and not connection centric + cmds.disconnectAttr(source_conns[i + 1], source_conns[i]) + + def get_dag_dict(self, dagpose_node): + dag_dict = {} + if cmds.nodeType(dagpose_node) == 'dagPose': + for idx in range(1, cmds.getAttr(dagpose_node + '.members', size=True)): + conns = cmds.listConnections(dagpose_node + '.members[' + str(idx) + ']') + if conns: + # because it's a message there could be more than one incoming, but Maya doesn't do that + # set the value for the name key to the int socket + # also this check for length is because of the garbage menber plugs for parents + if len(conns) == 1: + dag_dict[conns[0]] = idx + return dag_dict + else: + cmds.error('Requires dagPose node.') + return False + + def get_local_matrix(self, node, invertJointOrient=False): + + local_mx = cmds.xform(node, m=1, q=1) + # ignore code below for now - that code will take joint orient into account + return local_mx + + if invertJointOrient: + # if it's a joint, lets remove the joint orient from the local matrix + if cmds.nodeType(node) == "joint": + selList = api.MGlobal.getSelectionListByName(node) + dagPath = selList.getDagPath(0) + + parentMatrix = dagPath.exclusiveMatrix() + worldMatrix = dagPath.inclusiveMatrix() + + jointOrient = cmds.getAttr(node + ".jointOrient")[0] + x = math.radians(jointOrient[0]) + y = math.radians(jointOrient[1]) + z = math.radians(jointOrient[2]) + joEuler = api.MEulerRotation(x, y, z) + joMatrix = joEuler.asMatrix() + + localMatrix = worldMatrix * parentMatrix.inverse() + orientLocalMatrix = localMatrix * joMatrix.inverse() + + # combine the orient local matrix and the translation which shouldn't be changed based on the orientation + for i in range(12): + localMatrix[i] = orientLocalMatrix[i] + + # set the local matrix to this + local_mx = localMatrix + + return local_mx + + def create_matrix_node(self, node, mx_node_name, invertJointOrient=False): + local_mx = self.get_local_matrix(node, invertJointOrient=invertJointOrient) + world_mx = cmds.xform(node, m=1, ws=1, q=1) + + mx_node = cmds.createNode('network', name=mx_node_name) + cmds.addAttr(mx_node, at='matrix', longName='outputLocalMatrix') + cmds.addAttr(mx_node, at='matrix', longName='outputWorldMatrix') + cmds.addAttr(mx_node, dt='string', longName='matrix_node_ver') + + self.msgConnect(node + '.mx_pose', mx_node + '.driven_transform') + + cmds.setAttr(mx_node + '.outputLocalMatrix', local_mx, type='matrix') + cmds.setAttr(mx_node + '.outputWorldMatrix', world_mx, type='matrix') + + return mx_node + + ## Methods used by class + ################################################################################# + def init_pose_driver_system(self, existing_solver_node): + # check that it's a solver node + if cmds.nodeType(existing_solver_node) == 'UE4RBFSolverNode': + self.solver = existing_solver_node + + def create_pose_driver_system(self, name, input_xform, driven_xforms): + solver = self.create_UE4RBFSolverNode(name, input_xform, driven_xforms) + + def create_UE4RBFSolverNode(self, name, input_xform, driven_xforms): + try: + cmds.undoInfo(openChunk=True, undoName='create pose interpolator') + # create the interpolator (will be your API) + self.solver = cmds.createNode('UE4RBFSolverNode', name=name + '_UE4RBFSolver') + self.name = self.solver + + # Set the default values for the solver node + cmds.setAttr("{}.radius".format(self.solver), 50) + + # connect the world matrix from the driving xform to the interp + cmds.connectAttr(input_xform + '.matrix', self.solver + '.inputs[0]') + + # connect the default pose into the first target (Nate said to do this) + default_mx_node = self.create_matrix_node(input_xform, input_xform + '_base_pose', invertJointOrient=True) + cmds.connectAttr(default_mx_node + '.outputLocalMatrix', self.solver + '.targets[0].targetValues[0]') + + # connect the driver to the interp for the property to query + self.msgConnect(input_xform + '.ue4_rbf_solver', self.solver + '.driver') + + # store the driven nodes for the property query + for node in driven_xforms: + self.msgConnect(self.solver + '.driven_transforms', node + '.ue4_rbf_solver') + + for node in driven_xforms: + + blender_node_name = node + '_ue4PoseBlender' + if not cmds.objExists(blender_node_name): + print("node: " + blender_node_name + " didn't exist!") + + # make matrix node + mx_node = self.create_matrix_node(node, node + '_base_pose', invertJointOrient=True) + print("mx node") + print(mx_node) + blender_node = cmds.createNode('UE4PoseBlenderNode', name=blender_node_name) + + # connect the base pose, it doesn't make much sense copying the same mx into + # all three slots on this node (base_pose, in_matrix, pose) -but I was told to by the author + cmds.connectAttr(mx_node + '.outputLocalMatrix', blender_node + '.basePose') + cmds.connectAttr(mx_node + '.outputLocalMatrix', blender_node + '.inMatrix') + + self.msgConnect(node + '.blenderNode', blender_node + '.drivenTransform') + cmds.connectAttr(mx_node + '.outputLocalMatrix', blender_node + '.poses[0]') + + # connect ourtput, remember that this bone could already be driven + next_pose_weight_idx = cmds.getAttr(blender_node + '.weights', size=True) + cmds.connectAttr( + self.solver + '.outputs[0]', + blender_node + '.weights[' + str(next_pose_weight_idx) + ']' + ) + + # wire up the pose representation + self.msgConnect(self.solver + '.stored_pose_base_pose', mx_node + '.ue4_rbf_solver') + + + else: + print("node: " + blender_node_name + " did exist!") + blender_node = node + '_ue4PoseBlender' + # TODO: check that this node is valid and has a base pose connected + + self.msgConnect(self.solver + '.pose_blenders', blender_node + '.ue4_rbf_solver') + self.msgConnect(self.solver + '.stored_pose_base_pose', default_mx_node + '.ue4_rbf_solver') + + + except Exception as e: + print(traceback.format_exc()) + return False + finally: + cmds.undoInfo(closeChunk=True) + + def add_pose(self, pose_name, debug_mode=0): + ''' + Because the user has selected what xforms are driven on creation of the solver, this function does + not need to know anything but what we will call the pose, everything else is derived from metadata. + ''' + try: + cmds.undoInfo(openChunk=True, undoName='create pose') + self.is_driving(False) + if pose_name: + if debug_mode: + # I was storing transforms as locators for testing + pose_loc = cmds.spaceLocator(name=pose_name)[0] + cmds.delete(cmds.parentConstraint(self.driving_transform, pose_loc)) + + # wire up the world mx of the driver in it's current position to the solver + mx_node_driver = self.create_matrix_node( + self.driving_transform, + self.driving_transform + '_' + pose_name + '_pose', + invertJointOrient=True + ) + + next_target_index = cmds.getAttr(self.solver + '.targets', size=True) + cmds.connectAttr( + mx_node_driver + '.outputLocalMatrix', + self.solver + '.targets[' + str(next_target_index) + '].targetValues[0]' + ) + + # get the next output weight + next_output = cmds.getAttr(self.solver + '.outputs', size=True) + + # hook that up to existing + for node in self.driven_transforms: + # make matrix node + mx_node = self.create_matrix_node(node, pose_name + '_' + node + '_mx_pose', invertJointOrient=True) + + blender_node = cmds.listConnections(node + '.blenderNode')[0] + + next_pose_idx = cmds.getAttr(blender_node + '.poses', size=True) + + cmds.connectAttr( + mx_node + '.outputLocalMatrix', + blender_node + '.poses[' + str(next_pose_idx) + ']' + ) + # connect to weight + next_pose_weight_idx = cmds.getAttr(blender_node + '.weights', size=True) + cmds.connectAttr( + self.solver + '.outputs[' + str(next_output) + ']', + blender_node + '.weights[' + str(next_pose_weight_idx) + ']' + ) + + # wire up the pose representation + self.msgConnect(self.solver + '.stored_pose_' + pose_name, mx_node + '.ue4_rbf_solver') + driving_xform_mx = \ + cmds.listConnections(self.solver + '.targets[' + str(next_target_index) + '].targetValues[0]')[ + 0] + self.msgConnect(self.solver + '.stored_pose_' + pose_name, driving_xform_mx + '.ue4_rbf_solver') + self.is_driving(True) + except Exception as e: + print(traceback.format_exc()) + finally: + cmds.undoInfo(closeChunk=True) + + def update_pose(self, pose_name): + try: + cmds.undoInfo(openChunk=True, undoName='enable pose ' + pose_name) + if pose_name in self.pose_dict: + for mx_node in self.pose_dict[pose_name]: + driven_xform = cmds.listConnections(mx_node + '.driven_transform')[0] + # local_mx = cmds.xform(driven_xform, m=1, q=1) + local_mx = self.get_local_matrix(driven_xform, invertJointOrient=True) + cmds.setAttr(mx_node + '.outputLocalMatrix', local_mx, type='matrix') + except Exception as e: + print(traceback.format_exc()) + finally: + cmds.undoInfo(closeChunk=True) + + def delete_pose(self, pose_name): + try: + cmds.undoInfo(openChunk=True, undoName='enable pose ' + pose_name) + if pose_name in self.pose_dict: + for mx_node in self.pose_dict[pose_name]: + + print("mx node") + print(mx_node) + # this will also remove the pose array instance which keeps the pose computing + conn = cmds.listConnections(mx_node + ".outputLocalMatrix", d=1, s=0, p=1) + print(conn) + if conn: + index = int(conn[0].split("[")[-1].split("]")[0]) + # this means the conn is a poseBlender + if ".poses[" in conn[0]: + cmds.removeMultiInstance(conn[0], b=1) + poseBlender = conn[0].split(".")[0] + cmds.removeMultiInstance(poseBlender + ".weights[" + str(index) + "]", b=1) + print("pose blender:") + print(poseBlender) + + print("solver") + print(self.solver) + cmds.removeMultiInstance(self.solver + ".outputs[" + str(index) + "]", b=1) + cmds.removeMultiInstance(self.solver + ".targets[" + str(index) + "]", b=1) + print("index:") + print(index) + + # driven = cmds.listConnections(mx_node + ".driven_transform")[0] + # blender_node_name = driven + '_ue4PoseBlender' + # cmds.removeMultiInstance(blender_node_name + ".weights[" + str(index) + "]", b=1) + cmds.delete(mx_node) + cmds.deleteAttr(self.name + '.stored_pose_' + pose_name) + except Exception as e: + print(traceback.format_exc()) + finally: + cmds.undoInfo(closeChunk=True) + + def assume_pose(self, pose_name): + try: + cmds.undoInfo(openChunk=True, undoName='enable pose ' + pose_name) + if pose_name in self.pose_dict: + for mx in self.pose_dict[pose_name]: + driven_xform = cmds.listConnections(mx + '.driven_transform')[0] + mx = cmds.getAttr(mx + '.outputLocalMatrix') + """ + matrix = api.MMatrix(mx) + matrixFn = api.MTransformationMatrix(matrix) + euler = matrixFn.rotation() + rotX = math.degrees(euler[0]) + rotY = math.degrees(euler[1]) + rotZ = math.degrees(euler[2]) + translation = matrixFn.translation(api.MSpace.kWorld) + scale = matrixFn.scale(api.MSpace.kWorld) + + try: + cmds.setAttr(driven_xform + ".translate", translation[0], translation[1], translation[2]) + except: + pass + try: + cmds.setAttr(driven_xform + ".rotate", rotX, rotY, rotZ) + except: + pass + try: + cmds.setAttr(driven_xform + ".scale", scale[0], scale[1], scale[2]) + except: + pass + """ + # replace this to take joint orient into account + cmds.xform(driven_xform, m=mx) + + + except Exception as e: + print(traceback.format_exc()) + finally: + cmds.undoInfo(closeChunk=True) + + def bake_poses_to_timeline(self, start_frame=0, suppress=False, anim_layer=None): + """ + Bakes the poses to the timeline and sets the time range to the given animation. + :param start_frame: start frame of he baked animation + :param suppress: A bool to suppress the warning dialogue. Meant to be used outside of the UI + :anim_layer: if given the animations will be baked on that layer.If the layer doesnt exist we will create one. + """ + bake_bool = True + pose_list = [] + if not anim_layer: + anim_layer = "BaseAnimation" + if not cmds.animLayer(anim_layer, query=True, exists=True): + cmds.animLayer(anim_layer) + try: + cmds.autoKeyframe(e=1, st=0) + if not suppress: + ret = QtWidgets.QMessageBox.warning( + None, "WARNING: DESTRUCTIVE FUNCTION", + "This will bake the poses to the timeline, change your time range, and delete inputs on driving and driven transforms.\n" + \ + "Do you want this to happen?", + QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel, + QtWidgets.QMessageBox.Cancel + ) + if ret == QtWidgets.QMessageBox.StandardButton.Ok: + bake_bool = True + cmds.undoInfo(openChunk=True, undoName='Bake poses to timeline') + else: + bake_bool = False + + if bake_bool: + # begin printing UE4 pose list + # print('<- UE4 Pose List ---------------------->') + i = start_frame + for p in self.pose_dict: + driven_xforms = [] + for mx_node in self.pose_dict[p]: + driven_xforms.extend(cmds.listConnections(mx_node + '.driven_transform')) + # let's key it on the previous and next frames before we pose it + cmds.select(driven_xforms) + cmds.animLayer(anim_layer, addSelectedObjects=True, e=True) + cmds.setKeyframe(driven_xforms, t=[(i - 1), (i + 1)], animLayer=anim_layer) + + # assume the pose + self.assume_pose(p) + + cmds.setKeyframe(driven_xforms, t=[i], animLayer=anim_layer) + + # print(out to UE4 pose list) + print(p) + pose_list.append(p) + + # increment to next keyframe + i += 1 + # print('<- UE4 Pose List ---------------------->') + + # set the range to the number of keyframes + cmds.playbackOptions(minTime=0, maxTime=i, animationStartTime=0, animationEndTime=i - 1) + cmds.dgdirty(a=1) + return pose_list + cmds.dgdirty(a=1) + except Exception as e: + print(traceback.format_exc()) + finally: + cmds.undoInfo(closeChunk=True) + + def is_driving(self, state): + try: + cmds.undoInfo(openChunk=True, undoName='Change driving state') + # state is True or False + if state: + for node in self.pose_blenders: + # let's get the driven transform + driven_transform = cmds.listConnections(node + '.drivenTransform')[0] + # we have to decompose the out matrix in order to connect it to the driven transform + mx_decompose = cmds.createNode('decomposeMatrix', name=node + '_mx_decompose') + # we hookup the output mx of the pose driver to the input of the decompose + cmds.connectAttr(node + '.outMatrix', mx_decompose + '.inputMatrix') + # and the TRS out of the decompose back into the original driven xform + cmds.connectAttr(mx_decompose + '.outputTranslate', driven_transform + '.translate', f=True) + cmds.connectAttr(mx_decompose + '.outputRotate', driven_transform + '.rotate', f=True) + cmds.connectAttr(mx_decompose + '.outputScale', driven_transform + '.scale', f=True) + + else: + for node in self.pose_blenders: + print("debugging:") + print(node) + mx_decompose_node = cmds.listConnections(node + '.outMatrix') + print(mx_decompose_node) + while (mx_decompose_node): + self.break_all_connections(mx_decompose_node[0]) + cmds.delete(mx_decompose_node[0]) + mx_decompose_node = cmds.listConnections(node + '.outMatrix') + except Exception as e: + print(traceback.format_exc()) + finally: + cmds.undoInfo(closeChunk=True) + + def delete(self): + """function that deletes the node""" + + for mx_nodes in self.pose_dict.values(): + for mx_node in mx_nodes: + cmds.delete(mx_node) + cmds.delete(self.pose_blenders) + cmds.delete(self.solver) + + def add_driven(self, driven): + + try: + cmds.undoInfo(openChunk=True, undoName='add driven') + + self.msgConnect(self.solver + '.driven_transforms', driven + '.ue4_rbf_solver') + blender_node_name = driven + '_ue4PoseBlender' + mx_node = self.create_matrix_node(driven, driven + '_base_pose', invertJointOrient=True) + blender_node = cmds.createNode('UE4PoseBlenderNode', name=blender_node_name) + + cmds.connectAttr(mx_node + '.outputLocalMatrix', blender_node + '.basePose') + cmds.connectAttr(mx_node + '.outputLocalMatrix', blender_node + '.inMatrix') + + self.msgConnect(driven + '.blenderNode', blender_node + '.drivenTransform') + cmds.connectAttr(mx_node + '.outputLocalMatrix', blender_node + '.poses[0]') + + cmds.connectAttr(self.solver + '.outputs[0]', blender_node + '.weights[0]') + + self.msgConnect(self.solver + '.stored_pose_base_pose', mx_node + '.ue4_rbf_solver') + self.msgConnect(self.solver + '.pose_blenders', blender_node + '.ue4_rbf_solver') + + output_size = cmds.getAttr(self.solver + '.outputs', size=True) + for i in range(output_size): + cmds.connectAttr( + self.solver + ".outputs[" + str(i) + "]", blender_node + ".weights[" + str(i) + "]", + f=1 + ) + + for pose_name in self.pose_dict.keys(): + other_mx_node = self.pose_dict[pose_name][0] + conns = cmds.listConnections(other_mx_node + ".outputLocalMatrix", d=1, s=0, p=1) + index = None + for conn in conns: + if "poses" in conn and "PoseBlender" in conn: + index = int(conn.split("[")[-1].split("]")[0]) + + mx_node = self.create_matrix_node(driven, pose_name + '_' + driven + '_mx_pose', invertJointOrient=True) + cmds.connectAttr(mx_node + '.outputLocalMatrix', blender_node + '.poses[' + str(index) + ']', f=1) + + self.msgConnect(self.solver + '.stored_pose_' + pose_name, mx_node + '.ue4_rbf_solver') + + + except Exception as e: + print(traceback.format_exc()) + return False + finally: + cmds.undoInfo(closeChunk=True) + + def copy_driven_trs(self): + """copies the driven TRS so we can paste it across other poses""" + + for driven in self.driven_transforms: + translate = cmds.getAttr(driven + ".translate")[0] + rotate = cmds.getAttr(driven + ".rotate")[0] + scale = cmds.getAttr(driven + ".scale")[0] + self.copied_trs_map[driven] = {"translate": translate, "rotate": rotate, "scale": scale} + + print("Copied driven TRS successfully.") + + def paste_driven_trs(self, mult=1.0): + """pastes the driven trs and multiplies it""" + + for driven, data in self.copied_trs_map.items(): + translate = data['translate'] + rotate = data['rotate'] + scale = list(data['scale']) + + scale[0] = ((scale[0] - 1.0) * mult) + 1.0 + scale[1] = ((scale[1] - 1.0) * mult) + 1.0 + scale[2] = ((scale[2] - 1.0) * mult) + 1.0 + + try: + cmds.setAttr(driven + ".translate", translate[0] * mult, translate[1] * mult, translate[2] * mult) + except: + traceback.print_exc() + try: + cmds.setAttr(driven + ".rotate", rotate[0] * mult, rotate[1] * mult, rotate[2] * mult) + except: + traceback.print_exc() + try: + cmds.setAttr(driven + ".scale", scale[0], scale[1], scale[2]) + except: + traceback.print_exc() + + print("Pasted driven TRS successfully.") + + def zero_base_pose(self): + """zero's out the base pose""" + self.assume_pose("base_pose") + self.is_driving(False) + + for d in self.driven_transforms: + cmds.setAttr(d + ".translate", 0.0, 0.0, 0.0) + cmds.setAttr(d + ".rotate", 0.0, 0.0, 0.0) + cmds.setAttr(d + ".scale", 1.0, 1.0, 1.0) + + self.update_pose("base_pose") + self.is_driving(True) + + ## Class properties + ################################################################################# + @property + def is_enabled(self): + + found_conns = False + for node in self.pose_blenders: + mx_decompose_node = cmds.listConnections(node + '.outMatrix') + if mx_decompose_node: + found_conns = True + return found_conns + + @property + def driven_transforms(self): + """gets the driven transforms""" + + if cmds.attributeQuery("driven_transforms", n=self.solver, ex=1): + xforms = cmds.listConnections(self.solver + '.driven_transforms') + if xforms: + return xforms + + return [] + + @driven_transforms.setter + def driven_transforms(self, xforms): + pass + + @property + def driving_transform(self): + """gets the driving transform""" + + if cmds.attributeQuery("driver", n=self.solver, ex=1): + transform = cmds.listConnections(self.solver + '.driver')[0] + if transform: + return transform + return [] + + @driving_transform.setter + def driving_transform(self, transform): + self.msgConnect(transform + '.ue4_rbf_solver', self.solver + '.driver') + + @property + def base_dagPose(self): + xforms = cmds.listConnections(self.solver + '.driven_transforms') + if xforms: + return xforms + else: + return [] + + @base_dagPose.setter + def base_dagPose(self, dp): + pass + + @property + def pose_blenders(self): + + if not cmds.attributeQuery("pose_blenders", ex=1, n=self.solver): + return [] + + xforms = cmds.listConnections(self.solver + '.pose_blenders') + if xforms: + return xforms + else: + return [] + + @pose_blenders.setter + def poses(self, pose_list): + # nuke poses connections + for item in pose_list: + self.msgConnect(self.solver + '.pose_blenders', item + '.ue4_rbf_solver') + + @property + def pose_dict(self): + pose_dict = {} + attrs = cmds.listAttr(self.solver, st='stored_pose_*') + if attrs: + for attr in attrs: + pose_name = attr.replace('stored_pose_', '') + pose_matrices = cmds.listConnections(self.solver + '.' + attr) + pose_dict[pose_name] = pose_matrices + return pose_dict + + +def zero_all_base_poses(): + """zero's all driver bases poses""" + + nodes = cmds.ls(type='UE4RBFSolverNode') + for node in nodes: + driver_obj = UE4PoseDriver(existing_interpolator=node) + driver_obj.zero_base_pose() + + +def export_drivers(drivers, file_path): + output_data = {"drivers": {}} + for driver in drivers: + + driver_data = {} + driver_obj = UE4PoseDriver(existing_interpolator=driver) + + solver = driver_obj.solver + driver_name = driver_obj.name.replace("_UE4RBFSolver", "") + driver_data['name'] = driver_name + driver_data['solver_settings'] = {} + solver_attrs = cmds.listAttr(solver, k=1) + for solver_attr in solver_attrs: + try: + value = cmds.getAttr(solver + "." + solver_attr) + except: + continue + driver_data['solver_settings'][solver_attr] = value + + driver_data['driver_transform'] = driver_obj.driving_transform + driver_data['driven_transforms'] = driver_obj.driven_transforms + + """ + driver_data['driven_off_transforms'] = [] + + for transform in driver_obj.driven_transforms: + parent = cmds.listRelatives(transform, p=1) + if parent: + """ + + driver_data['pose_data'] = {} + for pose, mx_list in driver_obj.pose_dict.items(): + pose_data = { + "pose": pose, "local_matrix_map": {}, "world_matrix_map": {}, "driven_trs": {}, + "driving_trs": {} + } + driver_obj.assume_pose(pose) + + driving_transforn = driver_obj.driving_transform + translate = cmds.getAttr(driving_transforn + ".translate")[0] + rotate = cmds.getAttr(driving_transforn + ".rotate")[0] + scale = cmds.getAttr(driving_transforn + ".scale")[0] + pose_data["driving_trs"][driving_transforn] = {"translate": translate, "rotate": rotate, "scale": scale} + + for driven in driver_obj.driven_transforms: + translate = cmds.getAttr(driven + ".translate")[0] + rotate = cmds.getAttr(driven + ".rotate")[0] + scale = cmds.getAttr(driven + ".scale")[0] + pose_data["driven_trs"][driven] = {"translate": translate, "rotate": rotate, "scale": scale} + + for mx in mx_list: + local_matrix = cmds.getAttr(mx + ".outputLocalMatrix") + world_matrix = cmds.getAttr(mx + ".outputWorldMatrix") + pose_data['local_matrix_map'][mx] = local_matrix + pose_data['world_matrix_map'][mx] = world_matrix + driver_data['pose_data'][pose] = pose_data + output_data['drivers'][driver_name] = driver_data + + # zero the pose + driver_obj.assume_pose("base_pose") + + with open(file_path, 'w') as outfile: + + json.dump(output_data, outfile, sort_keys=1, indent=4, separators=(",", ":")) + + print("Successfuly export pose data to : " + file_path) + + +def import_drivers(file_path, driverFilter=None): + """imports the drivers""" + + with open(file_path) as json_file: + data = json.load(json_file) + + for driver, driver_data in data['drivers'].items(): + + if driverFilter: + if not driver in driverFilter: + continue + + print("driver:") + print(driver) + + driving_transform = driver_data['driver_transform'] + driven_transforms = driver_data['driven_transforms'] + + print("driving: ") + print(driving_transform) + print("driven: ") + print(driven_transforms) + + # if the driver exists, then just update it + if (cmds.objExists(driver + "_UE4RBFSolver")): + pose_driver_obj = UE4PoseDriver(existing_interpolator=driver + "_UE4RBFSolver") + else: + print("Solver NOT found!") + pose_driver_obj = UE4PoseDriver() + pose_driver_obj.create_pose_driver_system(driver, driving_transform, driven_transforms) + + # pose_driver_obj.is_driving(True)#delete this one + pose_driver_obj.is_driving(False) + + for attr, value in driver_data['solver_settings'].items(): + cmds.setAttr(pose_driver_obj.solver + "." + attr, value) + + for pose, pose_data in driver_data['pose_data'].items(): + + if pose == "base_pose": + continue + + if not pose in pose_driver_obj.pose_dict.keys(): + pose_driver_obj.add_pose(pose.replace("_pose", "")) + + local_matrix_map = pose_data['local_matrix_map'] + world_matrix_map = pose_data['local_matrix_map'] + for mx_node, local_matrix in local_matrix_map.items(): + if mx_node.startswith(driving_transform): + translate = cmds.getAttr(driving_transform + ".translate")[0] + local_matrix[12] = translate[0] + local_matrix[13] = translate[1] + local_matrix[14] = translate[2] + cmds.setAttr(mx_node + ".outputLocalMatrix", local_matrix, type="matrix") + + for mx_node, world_matrix in world_matrix_map.items(): + if mx_node.startswith(driving_transform): + translate = cmds.getAttr(driving_transform + ".translate")[0] + world_matrix[12] = translate[0] + world_matrix[13] = translate[1] + world_matrix[14] = translate[2] + cmds.setAttr(mx_node + ".outputWorldMatrix", world_matrix, type="matrix") + + pose_driver_obj.is_driving(True) + + +def mirror_all_drivers(): + nodes = cmds.ls(type='UE4RBFSolverNode') + for node in nodes: + if "_l_" not in node: + continue + + mirror_pose_driver(node) + print("-----------------------------------------------------------") + print("--------successfuly mirrored node: " + node + "-------") + print("-------------------------------------------------------------") + + +def mirror_pose_driver(source_pose_driver, pose=None): + source_syntax = "_l_" + target_syntax = "_r_" + target_pose_driver = None + if "_r_" in source_pose_driver: + source_syntax = "_r_" + target_syntax = "_l_" + + target_pose_driver = source_pose_driver.replace(source_syntax, target_syntax) + source_driver_obj = UE4PoseDriver(existing_interpolator=source_pose_driver) + poses = source_driver_obj.pose_dict.keys() + driven = source_driver_obj.driven_transforms + driving = source_driver_obj.driving_transform + target_driving = driving.replace(source_syntax, target_syntax) + target_driver_obj = None + if not cmds.objExists(target_pose_driver): + target_driven = [] + for d in driven: + target_driven.append(d.replace(source_syntax, target_syntax)) + + target_driver_obj = UE4PoseDriver() + target_driver_obj.create_pose_driver_system( + target_pose_driver.replace("_UE4RBFSolver", ""), target_driving, + target_driven + ) + else: + target_driver_obj = UE4PoseDriver(existing_interpolator=target_pose_driver) + + # make sure the solver settings match + solver_attrs = cmds.listAttr(source_driver_obj.solver, k=1) + for solver_attr in solver_attrs: + try: + value = cmds.getAttr(source_driver_obj.solver + "." + solver_attr) + except: + continue + cmds.setAttr(target_driver_obj.solver + "." + solver_attr, value) + + for p in poses: + + if pose and not pose == p: + continue + + # don't mirror the base pose + if "base_pose" in p: + continue + + target_pose = p.replace(source_syntax, target_syntax) + source_driver_obj.assume_pose(p) + target_driver_obj.is_driving(False) + + # if the pose doesn't exist, lets add it + if target_pose not in target_driver_obj.pose_dict.keys(): + print("didn't find pose: " + target_pose) + # mirror_transforms([driving], rotation=True, position=False) + rotate = cmds.getAttr(driving + ".rotate")[0] + cmds.setAttr(target_driving + ".rotate", rotate[0], rotate[1], rotate[2]) + mirror_transforms(driven) + target_driver_obj.add_pose(target_pose) + else: + target_driver_obj.assume_pose(target_pose) + mirror_transforms(driven) + target_driver_obj.update_pose(target_pose) + + target_driver_obj.is_driving(True) + + print("Successfully mirrored pose: " + p + " to " + target_pose) + + # if no pose was specified, then lets default back to base at the end + if not pose: + source_driver_obj.assume_pose("base_pose") + target_driver_obj.assume_pose("base_pose") + + +def mirror_transforms(transforms, position=True, rotation=True, scale=True): + """mirrors the transform""" + + for transform in transforms: + source_transform = transform + + source_syntax = "_l_" + target_syntax = "_r_" + if "_r_" in source_transform: + source_syntax = "_r_" + target_syntax = "_l_" + + target_transform = source_transform.replace(source_syntax, target_syntax) + source_parent_mat = api.MMatrix() + values = cmds.getAttr(source_transform + ".parentMatrix") + index = 0 + for i in range(4): + for j in range(4): + source_parent_mat.setElement(i, j, values[index]) + index += 1 + target_parent_mat = api.MMatrix() + values = cmds.getAttr(target_transform + ".parentMatrix") + index = 0 + for i in range(4): + for j in range(4): + target_parent_mat.setElement(i, j, values[index]) + index += 1 + + if position: + pos = cmds.getAttr(source_transform + ".translate")[0] + pos = api.MVector(pos[0], pos[1], pos[2]) + pos = _mirror_position(parent_matrix=source_parent_mat, m_parent_matrix=target_parent_mat, pos=pos) + try: + cmds.setAttr(target_transform + ".translate", pos[0], pos[1], pos[2]) + except: + pass + if rotation: + rot = cmds.getAttr(source_transform + ".rotate")[0] + rot = api.MVector(rot[0], rot[1], rot[2]) + rot = _mirror_rotation(parent_matrix=source_parent_mat, m_parent_matrix=target_parent_mat, rot=rot) + try: + cmds.setAttr(target_transform + ".rotate", rot[0], rot[1], rot[2]) + except: + pass + + # scale we assume the axis correlate and mirror + if scale: + scale = cmds.getAttr(source_transform + ".scale")[0] + cmds.setAttr(target_transform + ".scale", scale[0], scale[1], scale[2]) + + +def _mirror_position(parent_matrix, m_parent_matrix, pos): + """this mirrors the position""" + + driver_mat_fn = api.MTransformationMatrix(api.MMatrix.kIdentity) + driver_mat_fn.setTranslation(pos, api.MSpace.kWorld) + driver_mat = driver_mat_fn.asMatrix() + + scale_matrix_fn = api.MTransformationMatrix(api.MMatrix.kIdentity) + scale_matrix_fn.setScale([-1.0, 1.0, 1.0], api.MSpace.kWorld) + scale_matrix = scale_matrix_fn.asMatrix() + + pos_matrix = driver_mat * parent_matrix + pos_matrix = pos_matrix * scale_matrix + pos_matrix = pos_matrix * m_parent_matrix.inverse() + mat_fn = api.MTransformationMatrix(pos_matrix) + m_pos = mat_fn.translation(api.MSpace.kWorld) + + return m_pos + + +def _mirror_rotation(parent_matrix, m_parent_matrix, rot): + """this mirrors the rotation""" + + driver_mat_fn = api.MTransformationMatrix(api.MMatrix.kIdentity) + + # set the values to radians + rot[0] = math.radians(rot[0]) + rot[1] = math.radians(rot[1]) + rot[2] = math.radians(rot[2]) + + euler = api.MEulerRotation(rot[0], rot[1], rot[2]) + + driver_mat_fn.setRotation(euler) + driver_matrix = driver_mat_fn.asMatrix() + + world_matrix = driver_matrix * parent_matrix + rot_matrix = parent_matrix.inverse() * world_matrix + rot_matrix_fn = api.MTransformationMatrix(rot_matrix) + rot = rot_matrix_fn.rotation(asQuaternion=True) + rot.x = rot.x * -1.0 + rot.w = rot.w * -1.0 + rot_matrix = rot.asMatrix() + final_rot_matrix = m_parent_matrix * rot_matrix * m_parent_matrix.inverse() + + rot_matrix_fn = api.MTransformationMatrix(final_rot_matrix) + rot = rot_matrix_fn.rotation(asQuaternion=False) + m_rot = api.MVector() + m_rot[0] = math.degrees(rot[0]) + m_rot[1] = math.degrees(rot[1]) + m_rot[2] = math.degrees(rot[2]) + + return m_rot diff --git a/Scripts/Animation/epic_pose_wrangler/v1/poseWranglerUI.py b/Scripts/Animation/epic_pose_wrangler/v1/poseWranglerUI.py new file mode 100644 index 0000000..dcf7711 --- /dev/null +++ b/Scripts/Animation/epic_pose_wrangler/v1/poseWranglerUI.py @@ -0,0 +1,658 @@ +# Copyright Epic Games, Inc. All Rights Reserved. + +from PySide2 import QtWidgets +from PySide2 import QtCore +from PySide2 import QtUiTools + +import os + +import maya.cmds as cmds +import maya.OpenMaya as OpenMaya +from maya.app.general.mayaMixin import MayaQWidgetDockableMixin + +from epic_pose_wrangler.log import LOG +from epic_pose_wrangler.view import log_widget +from epic_pose_wrangler.v1 import poseWrangler, upgrade +from epic_pose_wrangler.v1 import palette + + +class EventUpgrade(QtCore.QObject): + upgrade = QtCore.Signal(str) + + +class PoseWrangler(MayaQWidgetDockableMixin, QtWidgets.QMainWindow): + """ + class for the pose wranglerUI + """ + + ui_name = "poseWranglerWindow" + + def __init__(self, parent=None): + super(PoseWrangler, self).__init__(parent) + self.event_upgrade_dispatch = EventUpgrade() + # Load the dependent plugin + plugin_versions = [{ + "name": "MayaUE4RBFPlugin_{}".format(cmds.about(version=True)), + "node_name": "UE4RBFSolverNode" + }, + { + "name": "MayaUE4RBFPlugin{}".format(cmds.about(version=True)), + "node_name": "UE4RBFSolverNode" + }, + {"name": "MayaUERBFPlugin".format(cmds.about(version=True)), "node_name": "UERBFSolverNode"}] + + QtCore.QSettings.setPath(QtCore.QSettings.IniFormat, QtCore.QSettings.UserScope, os.environ['LOCALAPPDATA']) + self._settings = QtCore.QSettings( + QtCore.QSettings.IniFormat, QtCore.QSettings.UserScope, "Epic Games", + "PoseWrangler" + ) + self._settings.setFallbacksEnabled(False) + + for plugin_version in plugin_versions: + plugin_name = plugin_version['name'] + node_name = plugin_version['node_name'] + if cmds.pluginInfo(plugin_name, q=True, loaded=True): + self._node_name = node_name + break + else: + try: + cmds.loadPlugin(plugin_name) + self._node_name = node_name + break + except RuntimeError as e: + pass + else: + raise RuntimeError("Unable to load valid RBF plugin version") + + # Load the UI file + file_path = os.path.dirname(__file__) + "/poseWranglerUI.ui" + if os.path.exists(file_path): + ui_file = QtCore.QFile(file_path) + # Attempt to open and load the UI + try: + ui_file.open(QtCore.QFile.ReadOnly) + loader = QtUiTools.QUiLoader() + self.win = loader.load(ui_file) + finally: + # Always close the UI file regardless of loader result + ui_file.close() + else: + raise ValueError('UI File does not exist on disk at path: {}'.format(file_path)) + + self.setWindowTitle("Pose Wrangler") + # Embed the UI window inside this widget + self.setCentralWidget(self.win) + + # buttons + self.win.add_pose_BTN.pressed.connect(self.add_pose) + + # refresh the driver cmd list + self.load_drivers() + + # hook up utils UI + self.win.bake_poses_BTN.pressed.connect(self.bake_poses) + + self.win.pose_LIST.itemSelectionChanged.connect(self.pose_changed) + self.win.edit_pose_BTN.pressed.connect(self.edit_pose) + self.win.delete_pose_BTN.pressed.connect(self.delete_pose) + + self.win.driver_LIST.itemSelectionChanged.connect(self.driver_changed) + self.win.driver_LIST.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.win.driver_LIST.customContextMenuRequested.connect(self.driver_popup) + + self.win.driven_transforms_LIST.itemSelectionChanged.connect(self.driven_changed) + + self.win.select_driver_BTN.pressed.connect(self.select_driver) + self.win.add_driven_BTN.pressed.connect(self.add_new_driven) + + self.win.mirror_pose_BTN.pressed.connect(self.mirror_pose) + + self.win.copy_driven_trs_BTN.pressed.connect(self.copy_driven_trs) + self.win.paste_driven_trs_BTN.pressed.connect(self.paste_driven_trs) + + self.win.create_driver_BTN.pressed.connect(self.create_driver) + self.win.delete_driver_BTN.pressed.connect(self.delete_driver) + + # setup the 'driving enabled' check box functionality + self.win.toggle_edit_BTN.clicked.connect(self.toggle_edit) + self.win.refresh_BTN.clicked.connect(self.load_drivers) + + self.win.driver_transform_LINE.setReadOnly(True) + self.win.upgrade_scene_BTN.pressed.connect(self._upgrade_scene) + + self._log_widget = log_widget.LogWidget() + self.addDockWidget(QtCore.Qt.BottomDockWidgetArea, self._log_widget.log_dock) + LOG.addHandler(self._log_widget) + # refresh the tree + self.load_poses() + + self.set_stylesheet() + + self._driver = None + + def set_stylesheet(self): + """set the theming and styling here""" + self.setStyleSheet(palette.getPaletteString()) + + def driver_popup(self, point): + """add right click menu""" + + selected_solvers = self.get_selected_solvers() + solver = None + if selected_solvers: + solver = selected_solvers[-1] + + """--------------------------EDIT----------------------------""" + + edit_action = QtWidgets.QAction("Edit", self) + edit_action.triggered.connect(self.edit_driver) + + enable_action = QtWidgets.QAction("Finish Editing (Enable)", self) + enable_action.triggered.connect(self.enable_driver) + + """--------------------------SELECTION----------------------------""" + + select_driver_action = QtWidgets.QAction("Select Driver Transform", self) + select_driver_action.triggered.connect(self.select_driver) + + select_solver_action = QtWidgets.QAction("Select Solver Node", self) + select_solver_action.triggered.connect(self.select_solver) + + select_driven_action = QtWidgets.QAction("Select Driven Transform(s)", self) + select_driven_action.triggered.connect(self.select_driven) + + """--------------------------MIRROR----------------------------""" + + mirror_driver_action = QtWidgets.QAction("Mirror Selected Drivers", self) + mirror_driver_action.triggered.connect(self.mirror_driver) + + """----------------------IMPORT/EXPORT----------------------------""" + + export_driver_action = QtWidgets.QAction("Export Selected Drivers", self) + export_driver_action.triggered.connect(self.export_driver) + + import_driver_action = QtWidgets.QAction("Import Driver(s)", self) + import_driver_action.triggered.connect(self.import_drivers) + + export_all_action = QtWidgets.QAction("Export All Drivers", self) + export_all_action.triggered.connect(self.export_all) + + """--------------------------ADD----------------------------""" + + add_driven_action = QtWidgets.QAction("Add Driven Transform(s)", self) + add_driven_action.triggered.connect(self.add_new_driven) + + """--------------------------UTILITIES----------------------------""" + + zero_base_pose_action = QtWidgets.QAction("Zero Base Pose Transforms", self) + zero_base_pose_action.triggered.connect(self.zero_base_poses) + + menu = QtWidgets.QMenu("Options:", self.win.driver_LIST) + + select_menu = QtWidgets.QMenu("Select:", menu) + select_menu.addAction(select_driver_action) + select_menu.addAction(select_driven_action) + select_menu.addAction(select_solver_action) + + mirror_menu = QtWidgets.QMenu("Mirror:", menu) + mirror_menu.addAction(mirror_driver_action) + + import_export_menu = QtWidgets.QMenu("Import/Export:", menu) + + if solver: + import_export_menu.addAction(export_driver_action) + import_export_menu.addAction(export_all_action) + import_export_menu.addSeparator() + import_export_menu.addAction(import_driver_action) + + add_menu = QtWidgets.QMenu("Add:", menu) + add_menu.addAction(add_driven_action) + + utilities_menu = QtWidgets.QMenu("Utilities:", menu) + utilities_menu.addAction(zero_base_pose_action) + + if solver: + # if the solver is enabled show the edit and otherwise show the enable + if solver.is_enabled: + menu.addAction(edit_action) + else: + menu.addAction(enable_action) + menu.addSeparator() + menu.addMenu(select_menu) + menu.addMenu(add_menu) + menu.addMenu(mirror_menu) + menu.addMenu(utilities_menu) + + menu.addMenu(import_export_menu) + + menu.popup(self.win.driver_LIST.mapToGlobal(point)) + + def get_selected_solvers(self): + """gets the selected solvers""" + + selected_items = self.win.driver_LIST.selectedItems() + + selected_solvers = [] + if selected_items: + for item in selected_items: + solver = item.data(QtCore.Qt.UserRole) + selected_solvers.append(solver) + return selected_solvers + + def get_selected_poses(self): + """gets the selected poses""" + + selected_items = self.win.pose_LIST.selectedItems() + selected_poses = [] + if selected_items: + for item in selected_items: + pose = item.text() + selected_poses.append(pose) + return selected_poses + + def add_pose(self): + """add a new pose to the driver""" + + selected_solvers = self.get_selected_solvers() + + if not selected_solvers: + OpenMaya.MGlobal.displayError('PoseWrangler: You must have a driver selected!') + return + + solver = selected_solvers[-1] + pose_name, ok = QtWidgets.QInputDialog.getText(self, 'text', 'Pose Name:') + if pose_name: + solver.add_pose(pose_name) + # self.win.edit_pose_BTN.setEnabled(True) + self.load_poses() + else: + cmds.warning('PoseWrangler: You must enter a pose name to add a pose.') + + def edit_pose(self): + """updates the current pose""" + + poses = self.get_selected_poses() + if not poses: + return + pose = poses[-1] + selected_solvers = self.get_selected_solvers() + if not selected_solvers: + OpenMaya.MGlobal.displayError('PoseWrangler: You must have a driver selected!') + return + + solver = selected_solvers[-1] + solver.update_pose(pose) + + def delete_pose(self): + """deletes the selected pose""" + + poses = self.get_selected_poses() + if not poses: + return + pose = poses[-1] + selected_solvers = self.get_selected_solvers() + if not selected_solvers: + OpenMaya.MGlobal.displayError('PoseWrangler: You must have a driver selected!') + return + + solver = selected_solvers[-1] + solver.delete_pose(pose) + self.load_poses() + + def driver_changed(self): + """function that gets called with the driver/solver is clicked""" + + selected_solvers = self.get_selected_solvers() + + self.win.driven_transforms_LIST.clear() + if selected_solvers: + solver = selected_solvers[-1] + driven_transforms = solver.driven_transforms + if driven_transforms: + for transform in driven_transforms: + item = QtWidgets.QListWidgetItem(transform) + self.win.driven_transforms_LIST.addItem(item) + else: + self.win.driver_transform_LINE.setText("") + + self.refresh_ui_state() + self.load_poses() + + def driven_changed(self): + """select the driven transforms when picked in the UI""" + + selected_items = self.win.driven_transforms_LIST.selectedItems() + cmds.select(cl=1) + for item in selected_items: + cmds.select(item.text(), add=1) + + def select_driver(self): + """selects the driver(s)""" + + cmds.select(cl=1) + selected_solvers = self.get_selected_solvers() + for solver in selected_solvers: + cmds.select(solver.driving_transform, add=1) + + def select_driven(self): + """selects the driven""" + cmds.select(cl=1) + selected_solvers = self.get_selected_solvers() + for solver in selected_solvers: + cmds.select(solver.driven_transforms, add=1) + + def select_solver(self): + """selects the solver DG node""" + + cmds.select(cl=1) + selected_solvers = self.get_selected_solvers() + for solver in selected_solvers: + cmds.select(solver.name, add=1) + + def bake_poses(self): + """bakes the poses to the timeline""" + + selected_solvers = self.get_selected_solvers() + if selected_solvers: + for solver in selected_solvers: + solver.bake_poses_to_timeline() + + def add_new_driven(self): + """adds new driven transforms to the selected drivers""" + + selected_items = self.win.driver_LIST.selectedItems() + + sl = cmds.ls(sl=1) + if not sl: + OpenMaya.MGlobal.displayError("No driven object selected, please select a driven object to add") + + selected_solvers = self.get_selected_solvers() + if selected_solvers: + for solver in selected_solvers: + for s in sl: + solver.add_driven(s) + + self.load_drivers(selected_items[-1].text()) + + def edit_driver(self): + """set the drivers into edit mode""" + selected_solvers = self.get_selected_solvers() + if selected_solvers: + for solver in selected_solvers: + solver.is_driving(False) + + self.refresh_ui_state() + + def enable_driver(self): + """enables the drivers into finishes edit mode""" + selected_solvers = self.get_selected_solvers() + if selected_solvers: + for solver in selected_solvers: + solver.is_driving(True) + + self.refresh_ui_state() + + def toggle_edit(self): + """toggle the edit""" + + selected_solvers = self.get_selected_solvers() + if selected_solvers: + solver = selected_solvers[-1] + if solver.is_enabled: + solver.is_driving(False) + else: + solver.is_driving(True) + + self.refresh_ui_state() + + def refresh_ui_state(self): + """refresh the UI state""" + + selected_solvers = self.get_selected_solvers() + if selected_solvers: + solver = selected_solvers[-1] + + self.win.driver_transform_LINE.setText(solver.driving_transform) + + # enable buttons that should be available when the driver is selected + self.win.toggle_edit_BTN.setEnabled(True) + self.win.delete_driver_BTN.setEnabled(True) + + if solver.is_enabled: + self.win.toggle_edit_BTN.setText("EDIT") + else: + self.win.toggle_edit_BTN.setText("FINISH EDITING (ENABLE)") + + self.win.add_pose_BTN.setEnabled(True) + self.win.add_driven_BTN.setEnabled(True) + self.win.select_driver_BTN.setEnabled(True) + + self.win.copy_driven_trs_BTN.setEnabled(True) + self.win.paste_driven_trs_BTN.setEnabled(True) + + selected_poses = self.get_selected_poses() + if selected_poses: + self.win.edit_pose_BTN.setEnabled(True) + self.win.delete_pose_BTN.setEnabled(True) + self.win.mirror_pose_BTN.setEnabled(True) + + else: + self.win.add_pose_BTN.setEnabled(False) + self.win.add_driven_BTN.setEnabled(False) + + self.win.toggle_edit_BTN.setEnabled(False) + self.win.delete_driver_BTN.setEnabled(False) + self.win.driver_transform_LINE.setText("") + self.win.select_driver_BTN.setEnabled(False) + + # pose buttons + self.win.edit_pose_BTN.setEnabled(False) + self.win.delete_pose_BTN.setEnabled(False) + self.win.mirror_pose_BTN.setEnabled(False) + + self.win.copy_driven_trs_BTN.setEnabled(False) + self.win.paste_driven_trs_BTN.setEnabled(False) + + for i in range(self.win.driver_LIST.count()): + item = self.win.driver_LIST.item(i) + solver = item.data(QtCore.Qt.UserRole) + if not solver.is_enabled: + item.setText(solver.name + " (Editing)") + else: + item.setText(solver.name) + + def pose_changed(self): + """called when the pose selection is changed""" + + selected_poses = self.get_selected_poses() + if selected_poses: + selected_pose = selected_poses[-1] + selected_solvers = self.get_selected_solvers() + solver = selected_solvers[-1] + if selected_pose in solver.pose_dict.keys(): + solver.assume_pose(selected_pose) + else: + cmds.warning('Pose ' + selected_pose + ' not found in pose dictionary') + self.refresh_ui_state() + + def load_drivers(self, selected=None): + """loads all the RBF node drivers into the driver list widget""" + + self.win.driver_LIST.clear() + drivers = [node for node in cmds.ls(type=self._node_name)] + selected_item = None + for driver in drivers: + item = QtWidgets.QListWidgetItem(driver) + solver = poseWrangler.UE4PoseDriver(existing_interpolator=driver) + item.setData(QtCore.Qt.UserRole, solver) + if selected and selected == driver: + selected_item = item + + self.win.driver_LIST.addItem(item) + + self.win.driver_LIST.sortItems(QtCore.Qt.AscendingOrder) + if selected_item: + self.win.driver_LIST.setCurrentItem(selected_item) + + self.refresh_ui_state() + self.load_poses() + + def load_poses(self): + """loads the poses for the current driver""" + + self.win.pose_LIST.clear() + selected_solvers = self.get_selected_solvers() + if selected_solvers: + solver = selected_solvers[-1] + for target in solver.pose_dict.keys() or []: + item = QtWidgets.QListWidgetItem() + item.setText(target) + item.setData(QtCore.Qt.UserRole, target) + + self.win.pose_LIST.addItem(item) + + # sort the items + self.win.pose_LIST.sortItems(QtCore.Qt.AscendingOrder) + + def mirror_pose(self): + """mirrors the selected pose for the current driver""" + + selected_solvers = self.get_selected_solvers() + if not selected_solvers: + return + + solver = selected_solvers[-1] + selected_poses = self.get_selected_poses() + if selected_poses: + for selected_pose in selected_poses: + pose_dict = solver.pose_dict + if selected_pose in pose_dict.keys(): + poseWrangler.mirror_pose_driver( + solver.name, pose=selected_pose + ) + else: + cmds.warning('Pose ' + selected_pose + ' not found in pose dictionary') + + def mirror_driver(self): + """mirrors all poses for the selected drivers""" + + selected_solvers = self.get_selected_solvers() + for solver in selected_solvers: + poseWrangler.mirror_pose_driver(solver.name) + + self.load_drivers() + + def import_drivers(self, file_path=""): + """imports the drivers""" + + path = file_path or QtWidgets.QFileDialog.getOpenFileName( + self, "Pose Wrangler Format", + "", "JSON (*.json)" + )[0] + if path == "": + return + + if not os.path.isfile(path): + OpenMaya.MGlobal.displayError(path + " is not a valid file.") + return + + poseWrangler.import_drivers(path) + + self.load_drivers() + self.load_poses() + + def export_driver(self): + """exports the selected drivers""" + + selected_solvers = self.get_selected_solvers() + solver_names = [] + for solver in selected_solvers: + solver_names.append(solver.name) + + self._export(solver_names) + + def export_all(self): + """exports all rbf nodes""" + nodes = cmds.ls(type=self._node_name) + if not nodes: + return + + self._export(nodes) + + def _export(self, drivers): + """exports the drivers passed in""" + + file_path = QtWidgets.QFileDialog.getSaveFileName(None, "Pose Wrangler Format", "", "*.json")[0] + if file_path == "": + return + + # do the export + poseWrangler.export_drivers(drivers, file_path) + + def create_driver(self): + """creates a new driver""" + + sel = cmds.ls(sl=1) + if not sel: + OpenMaya.MGlobal.displayError( + 'PoseWrangler: You must select a driving transform to CREATE a pose interpolator' + ) + return + + # takes all driven and the driving input is last + interp_name, ok = QtWidgets.QInputDialog.getText(self, 'text', 'Driver Name:') + if interp_name: + driver = poseWrangler.UE4PoseDriver() + driver.create_pose_driver_system(interp_name, sel[-1], sel[0:-1]) + self._current_solver = driver + # refresh the combobox and set this interpolator as current + self.load_drivers(selected=self._current_solver.name if self._current_solver else None) + + def delete_driver(self): + """deletes the selected drivers""" + + selected_solvers = self.get_selected_solvers() + if selected_solvers: + for solver in selected_solvers: + solver.delete() + + self.load_drivers() + self.load_poses() + + def copy_driven_trs(self): + """copies the driven TRS for pasting in different poses""" + + selected_solvers = self.get_selected_solvers() + if selected_solvers: + for solver in selected_solvers: + solver.copy_driven_trs() + + def paste_driven_trs(self): + """pastes the driven TRS and lets you multiply it""" + + mult = self.paste_mult_DSPN.value() + selected_solvers = self.get_selected_solvers() + if selected_solvers: + for solver in selected_solvers: + solver.paste_driven_trs(mult=mult) + + def zero_base_poses(self): + """zeros out the selected solver base poses""" + + selected_solvers = self.get_selected_solvers() + if selected_solvers: + for solver in selected_solvers: + solver.zero_base_pose() + + def _upgrade_scene(self): + file_path = upgrade.upgrade_scene(clear_scene=True) + LOG.info("Successfully Exported Current Scene") + self.event_upgrade_dispatch.upgrade.emit(file_path) + self.close() + + +def showUI(): + """show the UI""" + pose_wrangler_widget = PoseWrangler() + # show the UI + pose_wrangler_widget.show(dockable=True) diff --git a/Scripts/Animation/epic_pose_wrangler/v1/poseWranglerUI.ui b/Scripts/Animation/epic_pose_wrangler/v1/poseWranglerUI.ui new file mode 100644 index 0000000..e9d8227 --- /dev/null +++ b/Scripts/Animation/epic_pose_wrangler/v1/poseWranglerUI.ui @@ -0,0 +1,550 @@ + + + MainWindow + + + + 0 + 0 + 970 + 860 + + + + MainWindow + + + + + + + + + + 0 + 60 + + + + + + + true + + + This scene is using an out of date version of PoseWrangler. Please upgrade your scene to use the latest features and functionality. + + + + + + + Upgrade Scene + + + + + + + + + + + + Qt::Vertical + + + + + + + + 0 + 0 + + + + + 10 + 75 + true + + + + RBF POSE DRIVERS: + + + + + + + + 16777215 + 16777215 + + + + QAbstractItemView::ContiguousSelection + + + + + + + RMB Pose Drivers for options + + + Qt::AlignCenter + + + + + + + + 10 + + + + EDIT + + + true + + + + + + + 0 + + + + + CREATE DRIVER + + + + + + + + 8 + 75 + true + + + + DELETE DRIVER(S) + + + + + + + + 75 + true + + + + REFRESH + + + + + + + + + Select Driven joints then the Driver joint for Create Driver + + + + + + + Qt::Horizontal + + + + + + + + + + + + 10 + 75 + true + + + + POSES: + + + + + + + QAbstractItemView::ContiguousSelection + + + + + + + + + true + + + + 10 + 75 + true + + + + ADD POSE + + + + + + + true + + + + 10 + 75 + true + + + + EDIT POSE + + + + + + + true + + + + 10 + 75 + true + + + + DELETE POSE + + + + + + + true + + + + 10 + 75 + true + + + + MIRROR POSE + + + + + + + + + + + + + + + + + + + + + DRIVER TRANSFORM: + + + + + + + 0 + + + + + + + + SELECT + + + + + + + + + DRIVEN TRANSFORM(S) + + + + + + + QAbstractItemView::ContiguousSelection + + + + + + + 0 + + + + + ADD DRIVEN TRANSFORM(S) + + + + + + + + + + + + COPY/PASTE DRIVEN TRS: + + + + + + 0 + + + + + true + + + + 8 + 75 + true + + + + COPY + + + + + + + + 0 + 0 + + + + + 8 + 75 + true + + + + PASTE + + + + + + + + 35 + 16777215 + + + + + 8 + 75 + true + + + + MULT + + + + + + + + 50 + 16777215 + + + + 1 + + + 5.000000000000000 + + + 0.100000000000000 + + + 1.000000000000000 + + + + + + + + + + + + true + + + + 10 + 75 + true + + + + Utilities: + + + + 2 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + + + true + + + + 8 + 75 + true + + + + BAKE POSES TO TIMELINE + + + + + + + true + + + + 8 + 75 + true + + + + COPY UE4 POSE DRIVER + + + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + + Connect Selected Skin Nodes + + + + + Create Geometry From SkinNode + + + + + true + + + true + + + Unbind Geometry When Saving + + + + + + diff --git a/Scripts/Animation/epic_pose_wrangler/v1/upgrade.py b/Scripts/Animation/epic_pose_wrangler/v1/upgrade.py new file mode 100644 index 0000000..d4e95ea --- /dev/null +++ b/Scripts/Animation/epic_pose_wrangler/v1/upgrade.py @@ -0,0 +1,77 @@ +# Copyright Epic Games, Inc. All Rights Reserved. +import collections +import json +import os +import tempfile +import uuid + +from maya import cmds + +from epic_pose_wrangler.log import LOG +from epic_pose_wrangler.v1 import poseWrangler + + +def upgrade_scene(clear_scene=True): + drivers = cmds.ls(type='UE4RBFSolverNode') + output_data = collections.OrderedDict() + for driver in drivers: + + driver_data = collections.OrderedDict() + driver_obj = poseWrangler.UE4PoseDriver(existing_interpolator=driver) + + solver = driver_obj.solver + driver_name = driver_obj.name.replace("_UE4RBFSolver", "_UERBFSolver") + driver_data['solver_name'] = driver_name + solver_attrs = cmds.listAttr(solver, keyable=True) + for solver_attr in solver_attrs: + try: + value = cmds.getAttr(solver + "." + solver_attr) + except: + continue + driver_data[solver_attr] = value + + driver_data['drivers'] = [driver_obj.driving_transform] + driver_data['driven_transforms'] = driver_obj.driven_transforms + driver_data['mode'] = cmds.getAttr("{driver}.mode".format(driver=driver)) + driver_data['radius'] = cmds.getAttr("{driver}.radius".format(driver=driver)) + driver_data['weightThreshold'] = cmds.getAttr("{driver}.weightThreshold".format(driver=driver)) + driver_data['automaticRadius'] = cmds.getAttr("{driver}.automaticRadius".format(driver=driver)) + driver_data['distanceMethod'] = cmds.getAttr("{driver}.distanceMethod".format(driver=driver)) + driver_data['poses'] = collections.OrderedDict() + driver_transform = driver_obj.driving_transform + sorted_poses = sorted([p for p in driver_obj.pose_dict.keys()]) + for pose in sorted_poses: + mx_list = driver_obj.pose_dict[pose] + driver_obj.assume_pose(pose) + pose_data = { + "drivers": [cmds.xform(driver_transform, query=True, matrix=True, objectSpace=True)], + "driven": {transform: cmds.xform(transform, query=True, matrix=True, objectSpace=True) + for transform in driver_obj.driven_transforms}, + "function_type": "DefaultFunctionType", + "scale_factor": 1.0, + "distance_method": "DefaultMethod" + } + # Changing default pose name + if pose == 'base_pose': + pose = 'default' + driver_data['poses'][pose] = pose_data + output_data[driver_name] = driver_data + + # zero the pose + driver_obj.assume_pose("base_pose") + temp_dir = tempfile.gettempdir() + file_path = os.path.join(temp_dir, "pose_wrangler-{uuid}.json".format(uuid=uuid.uuid4())) + with open(file_path, 'w') as outfile: + + json.dump(output_data, outfile, indent=4, separators=(",", ":")) + + LOG.debug("Writing scene to {file_path}".format(file_path=file_path)) + if clear_scene: + cmds.delete(cmds.ls(type='UE4RBFSolverNode')) + cmds.delete(cmds.ls(type='UE4PoseBlenderNode')) + cmds.delete(cmds.ls('*_pose', type='network')) + for joint in cmds.ls(type='joint'): + for attr in ['mx_pose', 'ue4_rbf_solver', 'blenderNode']: + if cmds.attributeQuery(attr, node=joint, exists=True): + cmds.deleteAttr("{joint}.{attr}".format(joint=joint, attr=attr)) + return file_path diff --git a/Scripts/Animation/epic_pose_wrangler/v2/__init__.py b/Scripts/Animation/epic_pose_wrangler/v2/__init__.py new file mode 100644 index 0000000..ce7b5e7 --- /dev/null +++ b/Scripts/Animation/epic_pose_wrangler/v2/__init__.py @@ -0,0 +1,5 @@ +from .extensions import ( + bake_poses, + copy_paste_trs, + generate_inbetweens +)