503 lines
20 KiB
Python
503 lines
20 KiB
Python
# Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
import copy
|
|
|
|
from maya import cmds
|
|
from maya.api import OpenMaya as om
|
|
|
|
from epic_pose_wrangler.log import LOG
|
|
from epic_pose_wrangler.v2.model import exceptions, utils
|
|
|
|
|
|
class UEPoseBlenderNode(object):
|
|
"""
|
|
Class wrapper for UEPoseBlenderNode
|
|
"""
|
|
node_type = 'UEPoseBlenderNode'
|
|
|
|
@classmethod
|
|
def create(cls, driven_transform=None):
|
|
"""
|
|
Create Pose Blender Node
|
|
>>> new_node = UEPoseBlenderNode.create()
|
|
"""
|
|
name = driven_transform
|
|
# If no name is specified, generate unique name
|
|
if name is None:
|
|
name = '{name}#'.format(name=cls.node_type)
|
|
name += "_{class_name}".format(class_name=cls.__name__)
|
|
# Create node
|
|
node = cmds.createNode(cls.node_type, name=name)
|
|
node_ref = cls(node)
|
|
# If a driving transform is specified, connect it via a message attribute and set the base pose
|
|
if driven_transform is not None:
|
|
utils.message_connect(
|
|
from_attribute="{node}.drivenTransform".format(node=node),
|
|
to_attribute="{transform}.poseBlender".format(transform=driven_transform)
|
|
)
|
|
# Set the base pose
|
|
node_ref.base_pose = cmds.xform(driven_transform, query=True, objectSpace=True, matrix=True)
|
|
# Return the new UEPoseBlenderNode reference
|
|
return node_ref
|
|
|
|
@classmethod
|
|
def find_all(cls):
|
|
"""
|
|
Returns all PoseBlender nodes in scene
|
|
"""
|
|
return [cls(node) for node in cmds.ls(type=cls.node_type)]
|
|
|
|
@classmethod
|
|
def find_by_name(cls, name):
|
|
"""
|
|
Find a UEPoseBlenderNode class with the specified name
|
|
:param name :type str: name of the node
|
|
:return :type UEPoseBlenderNode or None:
|
|
"""
|
|
if not name.endswith(cls.node_type):
|
|
name += "_{node_type}".format(node_type=cls.node_type)
|
|
match = cmds.ls(name, type=cls.node_type)
|
|
return cls(match[0]) if match else None
|
|
|
|
@classmethod
|
|
def find_by_transform(cls, transform):
|
|
"""
|
|
Finds a UEPoseBlenderNode class connected to the specified joint
|
|
:param transform :type str: name of the transform
|
|
:return :type UEPoseBlenderNode or None
|
|
"""
|
|
if not cmds.attributeQuery('poseBlender', node=transform, exists=True):
|
|
return
|
|
connections = cmds.listConnections('{transform}.poseBlender'.format(transform=transform))
|
|
if not connections:
|
|
return
|
|
return cls(connections[0])
|
|
|
|
def __init__(self, node):
|
|
# If the node doesn't exist, raise an exception
|
|
if not cmds.objectType(node, isAType=self.node_type):
|
|
raise exceptions.InvalidNodeType(
|
|
'Invalid "{node_type}" node: "{node_name}"'.format(
|
|
node_type=self.node_type,
|
|
node_name=node
|
|
)
|
|
)
|
|
# Store a reference ot the MObject in case the name of the node is changed whilst this class is still in use
|
|
self._node = om.MFnDependencyNode(om.MGlobal.getSelectionListByName(node).getDependNode(0))
|
|
|
|
def __repr__(self):
|
|
"""
|
|
Returns class string representation
|
|
"""
|
|
return '<{node_type}>: {object}'.format(node_type=self.node_type, object=self)
|
|
|
|
def __str__(self):
|
|
"""
|
|
Returns class as string
|
|
"""
|
|
return str(self.node)
|
|
|
|
def __eq__(self, other):
|
|
"""
|
|
Returns if two objects are the same, allows for comparing two different UEPoseBlenderNode references that wrap
|
|
the same MObject
|
|
"""
|
|
return str(self) == str(other)
|
|
|
|
def __enter__(self):
|
|
"""
|
|
Override the __enter__ to allow for this class to be used as a context manager to toggle edit mode
|
|
>>> pose_blender = UEPoseBlenderNode('TestPoseBlenderNode')
|
|
>>> with pose_blender:
|
|
>>> # Edit mode enabled so changes can be made
|
|
>>> pass
|
|
>>> # Edit mode is now disabled
|
|
"""
|
|
self.edit = True
|
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
"""
|
|
On exit, disable edit mode
|
|
"""
|
|
self.edit = not self.edit
|
|
|
|
@property
|
|
def node(self):
|
|
return self._node.name()
|
|
|
|
@property
|
|
def rbf_solver_attr(self):
|
|
return '{node}.rbfSolver'.format(node=self.node)
|
|
|
|
@property
|
|
def base_pose_attr(self):
|
|
return '{node}.basePose'.format(node=self.node)
|
|
|
|
@property
|
|
def envelope_attr(self):
|
|
return '{node}.envelope'.format(node=self.node)
|
|
|
|
@property
|
|
def in_matrix_attr(self):
|
|
return '{node}.inMatrix'.format(node=self.node)
|
|
|
|
@property
|
|
def out_matrix_attr(self):
|
|
return '{node}.outMatrix'.format(node=self.node)
|
|
|
|
@property
|
|
def poses_attr(self):
|
|
return '{node}.poses'.format(node=self.node)
|
|
|
|
@property
|
|
def weights_attr(self):
|
|
return '{node}.weights'.format(node=self.node)
|
|
|
|
@property
|
|
def driven_transform(self):
|
|
"""
|
|
Get the current driven transform
|
|
:return :type str: transform node name
|
|
"""
|
|
if cmds.attributeQuery('drivenTransform', node=self.node, exists=True):
|
|
transforms = cmds.listConnections("{node}.drivenTransform".format(node=self.node))
|
|
if transforms:
|
|
return transforms[0]
|
|
|
|
@property
|
|
def edit(self):
|
|
"""
|
|
:return :type bool: are the driven transforms connected to this node editable?
|
|
"""
|
|
transform = self.driven_transform
|
|
# If no transform, we can edit!
|
|
if not transform:
|
|
return True
|
|
# False if we have any matrix connections, otherwise we have no connections and can edit
|
|
return False if cmds.listConnections(self.out_matrix_attr) else True
|
|
|
|
@edit.setter
|
|
def edit(self, value):
|
|
"""
|
|
Allow the driven transforms to be edited (or not)
|
|
:param value :type bool: edit mode enabled True/False
|
|
"""
|
|
# If we don't have a driven transform we can't do anything
|
|
transform = self.driven_transform
|
|
if not transform:
|
|
return
|
|
# If we are in edit mode, disable out connections
|
|
if bool(value):
|
|
self.out_matrix = None
|
|
# Otherwise connect up the transform
|
|
else:
|
|
self.out_matrix = transform
|
|
|
|
@property
|
|
def rbf_solver(self):
|
|
"""
|
|
:return:The connected rbf solver node if it exists
|
|
"""
|
|
from epic_pose_wrangler.v2.model import api
|
|
# Check the attr exists
|
|
exists = cmds.attributeQuery(self.rbf_solver_attr.split('.')[-1], node=self.node, exists=True)
|
|
# Query the connections
|
|
connections = cmds.listConnections(self.rbf_solver_attr)
|
|
# Return the node if the attr exists and a connection is found
|
|
return api.RBFNode(connections[0]) if exists and connections else None
|
|
|
|
@rbf_solver.setter
|
|
def rbf_solver(self, rbf_solver_attr):
|
|
"""
|
|
Connects up this node to an rbf solver node
|
|
:param rbf_solver_attr: the attribute on the rbf solver to connect this to i.e my_rbf_solver.poseBlend_back_120
|
|
"""
|
|
utils.message_connect(rbf_solver_attr, self.rbf_solver_attr)
|
|
|
|
@property
|
|
def base_pose(self):
|
|
"""
|
|
:return :type list: matrix
|
|
"""
|
|
return self.get_pose(index=0)
|
|
|
|
@base_pose.setter
|
|
def base_pose(self, matrix):
|
|
"""
|
|
Set the base pose to a specific pose.PoseNode
|
|
:param matrix :type list: Matrix to set as base pose
|
|
"""
|
|
# Connect up the pose's outputLocalMatrix to the basePose plug
|
|
utils.set_attr_or_connect(source_attr_name=self.base_pose_attr, value=matrix, attr_type='matrix')
|
|
# Set the inMatrix to the basePose plug
|
|
self.in_matrix = matrix
|
|
# Set the first pose in the pose list to the new base pose
|
|
self.set_pose(index=0, overwrite=True, matrix=matrix)
|
|
|
|
@property
|
|
def envelope(self):
|
|
"""
|
|
Get the value of the envelope attr from this node
|
|
"""
|
|
return utils.get_attr(self.envelope_attr, as_value=True)
|
|
|
|
@envelope.setter
|
|
def envelope(self, value):
|
|
"""
|
|
Sets the envelope
|
|
:param value: float, int or string (i.e node.attributeName). Float/Int will set the value, whilst
|
|
passing an attribute will connect the plugs
|
|
"""
|
|
if isinstance(value, float) or isinstance(value, int) and 0 > value > 1:
|
|
value = min(max(0.0, value), 1.0)
|
|
utils.set_attr_or_connect(source_attr_name=self.envelope_attr, value=value)
|
|
|
|
@property
|
|
def in_matrix(self):
|
|
"""
|
|
Get the inMatrix value
|
|
:return: matrix or attributeName
|
|
"""
|
|
# Get the current value of the attribute
|
|
return utils.get_attr(self.in_matrix_attr, as_value=True)
|
|
|
|
@in_matrix.setter
|
|
def in_matrix(self, value):
|
|
"""
|
|
Sets the inMatrix attribute to a value or connects it to another matrix plug
|
|
:param value: matrix (list) or string node.attributeName
|
|
"""
|
|
utils.set_attr_or_connect(source_attr_name=self.in_matrix_attr, value=value, attr_type='matrix')
|
|
|
|
@property
|
|
def out_matrix(self):
|
|
"""
|
|
:return:
|
|
"""
|
|
return cmds.getAttr(self.out_matrix_attr)
|
|
|
|
@out_matrix.setter
|
|
def out_matrix(self, transform_name=None):
|
|
"""
|
|
Connect the output matrix up to a given transform node
|
|
:param transform_name: name of a transform node to connect to
|
|
"""
|
|
# If a transform is specified, connect it
|
|
current_selection = cmds.ls(selection=True)
|
|
if transform_name:
|
|
# Check that the node type is a transform
|
|
if not cmds.ls(transform_name, type='transform'):
|
|
# If its not a transform raise an error
|
|
raise exceptions.InvalidNodeType(
|
|
"Invalid node type. Expected 'transform', received '{node_type}'".format(
|
|
node_type=cmds.objectType(transform_name)
|
|
)
|
|
)
|
|
# Create a new decomposeMatrix node to convert the outMatrix to T,R,S
|
|
mx_decompose_node = cmds.createNode('decomposeMatrix', name="{node}_mx_decompose#".format(node=self.node))
|
|
# Connect the outMatrix up to the decomposeMatrix's input
|
|
cmds.connectAttr(self.out_matrix_attr, '{node}.inputMatrix'.format(node=mx_decompose_node))
|
|
# Create an iterator with TRS attributes
|
|
attributes = ('translate', 'rotate', 'scale')
|
|
# Iterate through each attr and connect it
|
|
for attr in attributes:
|
|
out_attr = '{mx_decompose_node}.output{attr}'.format(
|
|
mx_decompose_node=mx_decompose_node,
|
|
attr=attr.capitalize()
|
|
)
|
|
in_attr = '{node_name}.{attr}'.format(node_name=transform_name, attr=attr)
|
|
cmds.connectAttr(out_attr, in_attr, force=True)
|
|
else:
|
|
# No transform was specified, disconnect all existing connections
|
|
connections = cmds.listConnections(self.out_matrix_attr, type='decomposeMatrix')
|
|
current_matrix = cmds.xform(self.driven_transform, query=True, matrix=True, objectSpace=True)
|
|
if connections:
|
|
cmds.delete(connections)
|
|
for connection in cmds.listConnections(self.out_matrix_attr, plugs=True) or []:
|
|
cmds.disconnectAttr(self.out_matrix_attr, connection)
|
|
cmds.xform(self.driven_transform, matrix=current_matrix, objectSpace=True)
|
|
cmds.select(current_selection, replace=True)
|
|
|
|
def get_pose(self, index=-1):
|
|
"""
|
|
Get the pose at the specified index
|
|
:param index :type int: index to query
|
|
:return :type list matrix or None
|
|
"""
|
|
# Get the value of the pose_attr at the specified index
|
|
return utils.get_attr(
|
|
'{poses_attr}[{index}]'.format(poses_attr=self.poses_attr, index=index),
|
|
as_value=True
|
|
)
|
|
|
|
def get_poses(self):
|
|
"""
|
|
:return :type list of matrices
|
|
"""
|
|
# TODO update this when the weight + name get added
|
|
return utils.get_attr_array(attr_name=self.poses_attr, as_value=True)
|
|
|
|
def add_pose_from_current(self, pose_name, index=-1):
|
|
"""
|
|
Add a pose from the current transforms positions
|
|
:param pose_name :type str: name of the pose
|
|
:param index :type int: target index for the pose
|
|
"""
|
|
# Find the next index if no index is specified
|
|
if index < 0:
|
|
# If the index is less than 0 find the next available index
|
|
index = len(cmds.getAttr(self.poses_attr, multiIndices=True) or [0]) - 1
|
|
# Store driven transform in var to reduce number of cmds calls
|
|
driven_transform = self.driven_transform
|
|
if not driven_transform:
|
|
raise exceptions.PoseBlenderPoseError(
|
|
"No driven transform associated with this node, "
|
|
"unable to get the current matrix"
|
|
)
|
|
# Get the local matrix for the driven transform
|
|
local_matrix = cmds.xform(driven_transform, query=True, objectSpace=True, matrix=True)
|
|
# Set the pose
|
|
self.set_pose(index=index, pose_name=pose_name, matrix=local_matrix)
|
|
|
|
def set_pose(self, index=-1, pose_name="", overwrite=True, matrix=None):
|
|
"""
|
|
Set a pose for the specified index
|
|
:param index: int value of the index to set
|
|
:param pose_name: name of the pose as a string
|
|
:param overwrite: should it overwrite any existing pose at the index or insert
|
|
:param matrix: matrix value to set
|
|
"""
|
|
# Find the next index if no index is specified
|
|
if index < 0:
|
|
if pose_name:
|
|
LOG.warning("Set Pose has not been implemented to support a pose name. FIXME")
|
|
return
|
|
else:
|
|
# If the index is less than 0 find the next available index
|
|
index = len(cmds.getAttr(self.poses_attr, multiIndices=True) or [0]) - 1
|
|
|
|
# If no matrix is specified, grab the matrix for the driven transform
|
|
if matrix is None:
|
|
matrix = cmds.xform(self.driven_transform, query=True, matrix=True, objectSpace=True)
|
|
|
|
# TODO add in support for inserting into the pose list
|
|
if not overwrite:
|
|
pose_count = len(cmds.getAttr(self.poses_attr, multiIndices=True) or [])
|
|
if pose_count - 1 > index:
|
|
pass
|
|
|
|
# Generate the source attribute (the attribute on this node) from the default attr and given index
|
|
source_attr_name = '{poses_attr}[{index}]'.format(poses_attr=self.poses_attr, index=index)
|
|
# Set the pose
|
|
utils.set_attr_or_connect(source_attr_name=source_attr_name, value=matrix, attr_type='matrix')
|
|
|
|
def go_to_pose(self, index=-1):
|
|
"""
|
|
Move the driven transform to the matrix stored in the pose list at the specified index
|
|
:param index :type index: index to go to
|
|
"""
|
|
# Get the matrix at the specified index
|
|
pose_matrix = self.get_pose(index=index)
|
|
# Assume the position
|
|
cmds.xform(self.driven_transform, matrix=pose_matrix)
|
|
|
|
def delete_pose(self, index=-1, pose_name=""):
|
|
"""
|
|
Delete a pose at the specified index or with the given name
|
|
:param index :type int: index to delete or -1 to use pose_name
|
|
:param pose_name :type str: pose name to delete
|
|
"""
|
|
# TODO Delete pose will need to remember enabled state + reconnect up pose_name connection when the changes are
|
|
# made to the solver. Currently can't delete by pose name because there is nothing tying an index to a pose
|
|
# name
|
|
# If no index and no pose name, raise exception
|
|
if index < 0 and not pose_name:
|
|
raise exceptions.InvalidPoseIndex("Unable to delete pose")
|
|
|
|
# Copy a list of the poses
|
|
poses = copy.deepcopy(self.get_poses())
|
|
|
|
# Iterate through the existing poses in reverse
|
|
for i in reversed(range(len(poses))):
|
|
# Remove the pose from the list of targets
|
|
cmds.removeMultiInstance('{blender}.poses[{index}]'.format(blender=self, index=i), b=True)
|
|
|
|
# Remove the pose at the given index
|
|
poses.pop(index)
|
|
|
|
# Re-add all the deleted poses (minus the one we popped)
|
|
for pose_index, matrix in enumerate(poses):
|
|
self.set_pose(index=pose_index, matrix=matrix)
|
|
|
|
def get_weight(self, index, as_float=True):
|
|
"""
|
|
Get the current weight for the specified index
|
|
:param index :type int
|
|
:param as_float :type bool: return as float or as attribute name of the connected plug
|
|
:return: float or plug
|
|
"""
|
|
return utils.get_attr('{weights}[{index}]'.format(weights=self.weights_attr, index=index), as_value=as_float)
|
|
|
|
def get_weights(self, as_float=True):
|
|
"""
|
|
Get all the weights associated with this node
|
|
:param as_float :type bool: as list of floats or list of plugs
|
|
:return: list of floats or plugs
|
|
"""
|
|
return utils.get_attr_array(attr_name=self.weights_attr, as_value=as_float)
|
|
|
|
def set_weight(self, index=-1, in_float_attr="", float_value=0.0):
|
|
"""
|
|
Set the weight at a given index either by connecting an attribute or specifying a float value
|
|
:param index :type int: index to set
|
|
:param in_float_attr :type str: node.attributeName to connect to this plug
|
|
:param float_value :type float: float value to set
|
|
"""
|
|
if index < 0:
|
|
# If the index is less than 0 find the next available index
|
|
index = len(cmds.getAttr(self.weights_attr, multiIndices=True) or [0]) - 1
|
|
# Generate the correct source attr name for the index specified
|
|
source_attr_name = '{weights}[{index}]'.format(weights=self.weights_attr, index=index)
|
|
# Set the array element to either the attr or the float value
|
|
utils.set_attr_or_connect(source_attr_name=source_attr_name, value=in_float_attr or float_value)
|
|
|
|
def set_weights(self, in_float_array_attr="", floats=None):
|
|
"""
|
|
Set multiple weights either by an attribute array string or a float list.Will overwrite existing values
|
|
:param in_float_array_attr :type str: node.attributeName array attribute to connect all indices with
|
|
:param floats :type list: list of float values to set for each corresponding index
|
|
"""
|
|
# Prioritize plugs over setting floats
|
|
if in_float_array_attr:
|
|
# Iterate through all the indices in the array
|
|
for i in range(len(cmds.getAttr(in_float_array_attr, multiIndices=True) or [0])):
|
|
# Generate the source attr name
|
|
in_float_attr = "{array_attr}[{index}]".format(array_attr=in_float_array_attr, index=i)
|
|
# Set the weight for the current index
|
|
self.set_weight(index=i, in_float_attr=in_float_attr)
|
|
elif floats:
|
|
# Iterate through the floats
|
|
for index, float_value in enumerate(floats):
|
|
# Set the weight for the current index
|
|
self.set_weight(index=index, float_value=float_value)
|
|
|
|
def delete(self):
|
|
"""
|
|
Delete the node associated with this wrapper
|
|
"""
|
|
# If we aren't in edit mode we still have connections made to decomposeMatrix nodes that we want to delete
|
|
if not self.edit:
|
|
# Enable edit mode to delete those decomposeMatrix nodes
|
|
self.edit = True
|
|
# Disconnect all attrs
|
|
destination_conns = cmds.listConnections(self, 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(self, 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])
|
|
# Delete the node
|
|
cmds.delete(self.node)
|