MetaBox/Scripts/Animation/epic_pose_wrangler/v2/model/api.py

1966 lines
87 KiB
Python
Raw Permalink Normal View History

2025-01-14 03:06:35 +08:00
# Copyright Epic Games, Inc. All Rights Reserved.
import math
import re
import six
import json
from collections import OrderedDict
from maya import cmds
from maya import OpenMaya
from maya.api import OpenMaya as om
from epic_pose_wrangler.log import LOG
from epic_pose_wrangler.v2.model import exceptions, pose_blender, utils
class RBFNode(object):
node_type = 'UERBFSolverNode'
def __init__(self, node):
"""
Initialize RBFNode on give node
>>> node = cmds.createNode('UERBFSolverNode')
>>> RBFNode(node)
"""
if not cmds.objectType(node, isAType=self.node_type):
raise TypeError('Invalid "{}" node: "{}"'.format(self.node_type, node))
self._node = node
def __repr__(self):
"""
Returns class string representation
"""
return '<{}>: {}'.format(self.node_type, self)
def __str__(self):
"""
Returns class as string
"""
return str(self._node)
def __eq__(self, other):
"""
Overrides equals operator to allow for different RBFNode instances to be matched against each other
"""
return str(self) == str(other)
def set_defaults(self):
"""
Sets node default values
"""
self.set_mode('Interpolative')
self.set_radius(45)
self.set_automatic_radius(False)
# -------------------------------------------------------------------------
@classmethod
def create(cls, name=None):
"""
Create RBF node
>>> rbf_node = RBFNode.create()
"""
if name is None:
name = '{}#'.format(cls.node_type)
active_selection = cmds.ls(selection=True)
# create node
node = cmds.createNode(cls.node_type, name=name)
rbf_node = cls(node)
rbf_node.set_defaults()
cmds.select(active_selection, replace=True)
return rbf_node
@classmethod
def create_from_data(cls, data):
"""
Creates RBF network from dictionary
>>> new_joint = cmds.createNode('joint')
>>> data = {'drivers': [new_joint], 'poses': {'drivers': {'default': [[1,0,0,0,0,1,0,0,0,0,1,0,2,0,0,1]]}}}
>>> RBFNode.create_from_data(data)
"""
if not cmds.objExists(data['solver_name']):
rbf_node = cls.create(name=data['solver_name'])
else:
rbf_node = cls(data['solver_name'])
# add drivers
drivers = data['drivers']
for driver in drivers:
if not rbf_node.has_driver(driver):
rbf_node.add_driver(driver)
# add controllers
controllers = data.get('controllers', [])
for controller in controllers:
if not rbf_node.has_controller(controller):
rbf_node.add_controller(controller)
driven_transforms = data.get('driven_transforms', [])
if driven_transforms:
rbf_node.add_driven_transforms(driven_nodes=driven_transforms, edit=False)
# add poses
for pose_name, pose_data in data['poses'].items():
drivers_matrices = pose_data['drivers']
controllers_matrices = pose_data.get('controllers', [])
driven_matrices = pose_data.get('driven', {})
function_type = pose_data.get('function_type', 'DefaultFunctionType')
distance_method = pose_data.get('distance_method', 'DefaultMethod')
scale_factor = pose_data.get('scale_factor', 1.0)
target_enable = pose_data.get('target_enable', True)
rbf_node.add_pose(
pose_name, drivers=drivers, matrices=drivers_matrices, driven_matrices=driven_matrices,
controller_matrices=controllers_matrices, function_type=function_type,
distance_method=distance_method, scale_factor=scale_factor, target_enable=target_enable
)
# set solver attributes
attributes = ['radius', 'automaticRadius', 'weightThreshold', 'normalizeMethod']
enum_attributes = ['mode', 'distanceMethod', 'normalizeMethod',
'functionType', 'twistAxis', 'inputMode']
for attr in attributes:
if attr in data:
cmds.setAttr('{}.{}'.format(rbf_node, attr), data[attr])
for attr in enum_attributes:
if attr in data:
value = data[attr]
attr = "{node}.{attr}".format(node=rbf_node, attr=attr)
rbf_node._set_enum_attribute(attr, value)
return rbf_node
@classmethod
def find_all(cls):
"""
Returns all RBF nodes in scene
"""
return [cls(node) for node in cmds.ls(type=cls.node_type)]
# ----------------------------------------------------------------------------------------------
# PARAMETERS
# ----------------------------------------------------------------------------------------------
def mode(self, enum_value=True):
return cmds.getAttr('{}.mode'.format(self), asString=enum_value)
def set_mode(self, value):
self._set_enum_attribute('{}.mode'.format(self), value)
def radius(self):
return cmds.getAttr('{}.radius'.format(self))
def set_radius(self, value):
cmds.setAttr('{}.radius'.format(self), value)
def automatic_radius(self):
return cmds.getAttr('{}.automaticRadius'.format(self))
def set_automatic_radius(self, value):
cmds.setAttr('{}.automaticRadius'.format(self), value)
def weight_threshold(self):
return cmds.getAttr('{}.weightThreshold'.format(self))
def set_weight_threshold(self, value):
cmds.setAttr('{}.weightThreshold'.format(self), value)
def distance_method(self, enum_value=True):
return cmds.getAttr('{}.distanceMethod'.format(self), asString=enum_value)
def set_distance_method(self, value):
self._set_enum_attribute('{}.distanceMethod'.format(self), value)
def normalize_method(self, enum_value=True):
return cmds.getAttr('{}.normalizeMethod'.format(self), asString=enum_value)
def set_normalize_method(self, value):
self._set_enum_attribute('{}.normalizeMethod'.format(self), value)
def function_type(self, enum_value=True):
return cmds.getAttr('{}.functionType'.format(self), asString=enum_value)
def set_function_type(self, value):
self._set_enum_attribute('{}.functionType'.format(self), value)
def twist_axis(self, enum_value=True):
return cmds.getAttr('{}.twistAxis'.format(self), asString=enum_value)
def set_twist_axis(self, value):
self._set_enum_attribute('{}.twistAxis'.format(self), value)
def input_mode(self, enum_value=True):
return cmds.getAttr('{}.inputMode'.format(self), asString=enum_value)
def set_input_mode(self, value):
self._set_enum_attribute('{}.inputMode'.format(self), value)
def data(self):
"""
Returns dictionary with the setup
"""
data = OrderedDict()
data['solver_name'] = str(self)
data['drivers'] = self.drivers()
data['driven_transforms'] = self.driven_nodes(pose_blender.UEPoseBlenderNode.node_type)
data['controllers'] = self.controllers()
data['poses'] = self.poses()
data['driven_attrs'] = self.driven_attributes()
data['mode'] = self.mode(enum_value=False)
data['radius'] = self.radius()
data['automaticRadius'] = self.automatic_radius()
data['weightThreshold'] = self.weight_threshold()
data['distanceMethod'] = self.distance_method(enum_value=False)
data['normalizeMethod'] = self.normalize_method(enum_value=False)
data['functionType'] = self.function_type(enum_value=False)
data['twistAxis'] = self.twist_axis(enum_value=False)
data['inputMode'] = self.input_mode(enum_value=False)
return data
def export_data(self, file_path):
"""
Exports data dictionary to disk
"""
with open(file_path, 'w') as outfile:
json.dump(self.data(), outfile, sort_keys=1, indent=4, separators=(",", ":"))
return file_path
def output_weights(self):
"""
Returns output weights
"""
return [cmds.getAttr(attr) for attr in self.output_attributes()]
def output_attributes(self):
"""
Returns output attributes
"""
attributes = list()
indices = cmds.getAttr('{}.outputs'.format(self), multiIndices=True)
if indices:
for pose_index in indices:
attr = '{}.outputs[{}]'.format(self, pose_index)
attributes.append(attr)
return attributes
def driven_attributes(self, type='blendShape'):
"""
Returns output connections
"""
driven_attributes = list()
output_attributes = self.output_attributes()
if output_attributes:
for attr in output_attributes:
con = cmds.listConnections(attr, destination=True, shapes=False, plugs=True, type=type) or []
driven_attributes.append(con)
return driven_attributes
def driven_nodes(self, type='blendShape'):
"""
Returns driven transform nodes
:return:
"""
driven = []
if type == pose_blender.UEPoseBlenderNode.node_type:
if cmds.attributeQuery("poseBlenders", node=self, exists=True):
# Iterate through all the pose blenders
for pose_blender_node_name in cmds.listConnections("{solver}.poseBlenders".format(solver=self)) or []:
# Get the pose blender wrapper from the node name
pose_blender_node = pose_blender.UEPoseBlenderNode(pose_blender_node_name)
driven.append(pose_blender_node.driven_transform)
elif type == 'blendShape':
for pose_index in range(self.num_poses()):
attr = '{}.targets[{}].targetName'.format(self, pose_index)
poses = cmds.listConnections(attr, type='transform') or []
driven.extend(poses)
return driven
# ----------------------------------------------------------------------------------------------
# DRIVERS
# ----------------------------------------------------------------------------------------------
def num_drivers(self):
"""
Returns number of drivers
"""
return cmds.getAttr('{}.inputs'.format(self), size=True)
def has_driver(self, transform_node):
"""
Check if node is using this transform as input
"""
return transform_node in self.drivers()
def drivers(self):
"""
Returns list of drivers
"""
indices = cmds.getAttr('{}.inputs'.format(self), multiIndices=True)
drivers = list()
if indices:
for i in indices:
connections = cmds.listConnections('{}.inputs[{}]'.format(self, i))
if connections:
drivers.append(connections[0])
else:
raise RuntimeError('Unable to get driver at index: {}'.format(i))
return drivers
def add_driver(self, transform_nodes=None, controllers=None):
"""
Adds driver to RBF Node
"""
if transform_nodes is None:
transform_nodes = cmds.ls(selection=True, type='transform')
if self.num_poses() > 1:
raise RuntimeError('Unable to add driver after poses have been added')
if not isinstance(transform_nodes, (list, set, tuple)):
transform_nodes = [transform_nodes]
for transform_node in transform_nodes:
if not cmds.objExists(transform_node):
raise RuntimeError('Node not found: "{}"'.format(transform_node))
if not cmds.objectType(transform_node, isAType='transform'):
raise RuntimeError('Invalid transform node: "{}"'.format(transform_node))
if self.has_driver(transform_node):
raise RuntimeError('Already has driver "{}"'.format(transform_node))
index = self.num_drivers()
cmds.connectAttr('{}.matrix'.format(transform_node), '{}.inputs[{}]'.format(self, index))
# set rest matrix
rest_matrix = cmds.xform(transform_node, q=True, ws=False, matrix=True)
cmds.setAttr('{}.inputsRest[{}]'.format(self, index), rest_matrix, type='matrix')
def remove_drivers(self, transform_nodes):
"""
Removes the specified drivers from this solver
:param transform_nodes :type list: list of transform node names
"""
# Get the valid driver names - converts from MObject to DagPath if MObjects are specified
valid_drivers = []
for driver in transform_nodes:
# Check if its an MObject
if isinstance(driver, om.MObject):
# Get the fullPathName and add to list of valid drivers
valid_drivers.append(om.MDagPath.getAPathTo(driver).fullPathName())
else:
# Check if the driver exists and get the full path name
matching_driver = cmds.ls(driver, long=True)
if matching_driver:
# Add to list of valid drivers
valid_drivers.append(matching_driver[0])
LOG.debug("Removing drivers: '{drivers}' from solver: {solver}".format(drivers=valid_drivers, solver=self))
# Get the existing drivers from the solver
existing_drivers = self.drivers()
# Create an empty dict to store the remaining {driver: pose_node}
remaining_drivers = []
poses_indices = cmds.getAttr('{solver}.targets'.format(solver=self), multiIndices=True) or []
# Iterate through the existing drivers in reverse
for index in reversed(range(len(existing_drivers))):
# Grab the driver from the index
existing_driver = existing_drivers[index]
# Check that the driver exists
matching_driver = cmds.ls(existing_driver, long=True)
# If the driver doesn't exist, warn and continue
if not matching_driver:
LOG.warning(
"Could not find driver: {existing_driver} in the scene".format(
existing_driver=existing_driver
)
)
continue
# Existing driver found
existing_driver = matching_driver[0]
# Remove the drivers connections from the solver
cmds.removeMultiInstance('{solver}.inputs[{index}]'.format(solver=self, index=index), b=True)
cmds.removeMultiInstance('{solver}.inputsRest[{index}]'.format(solver=self, index=index), b=True)
for pose_index in poses_indices:
cmds.removeMultiInstance(
'{solver}.targets[{pose_index}]'.format(
solver=self,
pose_index=pose_index,
),
b=True
)
# If the driver is in the not valid drivers list we want to keep it
if existing_driver not in valid_drivers:
# Add it back to the list of drivers to reconnect
remaining_drivers.append(existing_driver)
# Iterate through the remaining drivers and reconnect them
for index, driver in enumerate(remaining_drivers):
cmds.connectAttr(
'{driver}.matrix'.format(driver=driver), '{solver}.inputs[{index}]'.format(
solver=self,
index=index
)
)
# set rest matrix
rest_matrix = cmds.xform(driver, q=True, ws=False, matrix=True)
cmds.setAttr('{}.inputsRest[{}]'.format(self, index), rest_matrix, type='matrix')
# If we have any drivers left, recreate the default pose
if self.drivers():
self.add_pose_from_current('default')
# ----------------------------------------------------------------------------------------------
# CONTROLLERS
# ----------------------------------------------------------------------------------------------
def num_controllers(self):
"""
Returns number of controllers
"""
return cmds.getAttr('{}.inputsControllers'.format(self), size=True)
def has_controller(self, transform_node):
"""
Check if node is using this transform as controller
"""
return transform_node in self.controllers()
def controllers(self):
"""
Returns list of controllers
"""
indices = cmds.getAttr('{}.inputsControllers'.format(self), multiIndices=True)
controllers = list()
if indices:
for i in indices:
connections = cmds.listConnections('{}.inputsControllers[{}]'.format(self, i))
if connections:
controllers.append(connections[0])
else:
raise RuntimeError('Unable to get controller at index: {}'.format(i))
return controllers
def add_controller(self, transform_nodes):
"""
Adds controller to RBF Node
"""
if self.num_poses():
raise RuntimeError('Unable to add controller after poses have been added')
if not isinstance(transform_nodes, (list, set, tuple)):
transform_nodes = [transform_nodes]
for transform_node in transform_nodes:
if not cmds.objExists(transform_node):
raise RuntimeError('Node not found: "{}"'.format(transform_node))
if not cmds.objectType(transform_node, isAType='transform'):
raise RuntimeError('Invalid transform node: "{}"'.format(transform_node))
if self.has_controller(transform_node):
raise RuntimeError('Already has controller "{}"'.format(transform_node))
index = self.num_controllers()
cmds.connectAttr('{}.message'.format(transform_node), '{}.inputsControllers[{}]'.format(self, index))
# ----------------------------------------------------------------------------------------------
# DRIVEN TRANSFORMS
# ----------------------------------------------------------------------------------------------
def add_driven_transforms(self, driven_nodes=None, edit=False):
"""
Add driven transforms to this node, creating UEPoseBlenderNodes where needed
:param driven_nodes :type list: list of transform_node names
:param edit :type bool: should we be editing the transforms on creation? When set to true, no connection is
made from the output of the UEPoseBlenderNode to the driven transform translate, rotate and scale. When set
to False, a decompose matrix is created and connected between the output of the UEPoseBlenderNode and the
translate, rotate and scale of the driven transform.
"""
# If no driven nodes are specified, grab the current selection
if not driven_nodes:
driven_nodes = cmds.ls(selection=True, type='transform')
# If an iterable wasn't specified, convert it to a list
if not isinstance(driven_nodes, (list, tuple, set)):
driven_nodes = [driven_nodes]
# For each driven node
for node in driven_nodes:
existing_connection = pose_blender.UEPoseBlenderNode.find_by_transform(node)
if existing_connection:
LOG.error(
"Driven transform {node} is already connected to {solver}. "
"Unable to add, skipping.".format(
node=node,
solver=existing_connection.rbf_solver
)
)
continue
# Create a UEPoseBlender node
pose_blender_node = pose_blender.UEPoseBlenderNode.create(driven_transform=node)
# Create a message attr connection to the rbf solver node
pose_blender_node.rbf_solver = "{solver}.poseBlenders".format(solver=self)
# Connect each output attribute from the solver to the corresponding weight
for index, attribute in enumerate(self.output_attributes()):
pose_blender_node.set_weight(index=index, in_float_attr=attribute)
# Set the mode to edit
pose_blender_node.edit = edit
def remove_driven_transforms(self, driven_nodes):
"""
Remove the specified driven transforms from this solver
:param driven_nodes :type list: list of transform names
"""
# Can only remove transforms if we have connected UEPoseBlenderNodes
if cmds.attributeQuery("poseBlenders", node=self, exists=True):
# Iterate through the pose blender nodes
for pose_blender_node_name in cmds.listConnections("{solver}.poseBlenders".format(solver=self)):
# Generate an API wrapper for the UEPoseBlenderNode
pose_blender_node = pose_blender.UEPoseBlenderNode(pose_blender_node_name)
# If the pose blender's driven transform is in the list to remove
if pose_blender_node.driven_transform in driven_nodes:
# Remove it from the list
driven_nodes.pop(driven_nodes.index(pose_blender_node.driven_transform))
# Delete the UEPoseBlenderNode
pose_blender_node.delete()
# Iterate through the driven
for driven_node in driven_nodes:
# See if they are a blendshape and have an associated pose
blendshape_pose = self.get_pose_for_blendshape_mesh(driven_node)
if blendshape_pose:
# Delete the blendshape
self.delete_blendshape(pose_name=blendshape_pose)
# ----------------------------------------------------------------------------------------------
# POSES
# ----------------------------------------------------------------------------------------------
def num_poses(self):
"""
Returns number of poses
"""
return cmds.getAttr('{}.targets'.format(self), size=True)
def has_pose(self, pose_name):
"""
Returns if pose already exists
"""
return pose_name in self.poses()
def pose_index(self, pose_name):
"""
Returns pose index
"""
poses_indices = cmds.getAttr('{}.targets'.format(self), multiIndices=True)
if poses_indices:
for pose_index in poses_indices:
if pose_name == cmds.getAttr('{}.targets[{}].targetName'.format(self, pose_index)):
return pose_index
return -1
def pose(self, pose_name):
"""
Returns pose matrices
"""
pose_index = self.pose_index(pose_name)
if pose_index == -1:
raise exceptions.InvalidPose('Pose not found: "{}"'.format(pose_name))
# ---------------------------------------------------------------------
# get driver indices
driver_indices = cmds.getAttr('{}.targets[{}].targetValues'.format(self, pose_index), multiIndices=True)
if not driver_indices:
raise exceptions.InvalidPose('Unable to get Driver indices')
# just to be sure ...
num_drivers = self.num_drivers()
if len(driver_indices) != num_drivers:
raise exceptions.InvalidPose('Invalid Driver Indices')
# get matrices
matrices = list()
for driver_index in driver_indices:
matrix = cmds.getAttr('{}.targets[{}].targetValues[{}]'.format(self, pose_index, driver_index))
matrices.append(matrix)
# ---------------------------------------------------------------------
# get controller indices
controller_indices = cmds.getAttr(
'{}.targets[{}].targetControllers'.format(self, pose_index),
multiIndices=True
) or []
# just to be sure ...
num_controllers = self.num_controllers()
if len(controller_indices) != num_controllers:
raise RuntimeError('Invalid Controller Indices')
# get controller matrices
controller_matrices = list()
for controller_index in controller_indices:
matrix = cmds.getAttr('{}.targets[{}].targetControllers[{}]'.format(self, pose_index, controller_index))
controller_matrices.append(matrix)
# ---------------------------------------------------------------------
# get properties
function_type = cmds.getAttr('{}.targets[{}].targetFunctionType'.format(self, pose_index), asString=True)
scale_factor = cmds.getAttr('{}.targets[{}].targetScaleFactor'.format(self, pose_index), asString=True)
distance_method = cmds.getAttr('{}.targets[{}].targetDistanceMethod'.format(self, pose_index), asString=True)
pose_blender_data = {}
if cmds.attributeQuery("poseBlenders", node=self, exists=True):
for pose_blender_node_name in cmds.listConnections("{solver}.poseBlenders".format(solver=self)):
# Get the pose blender wrapper from the node name
pose_blender_node = pose_blender.UEPoseBlenderNode(pose_blender_node_name)
pose_matrix = pose_blender_node.get_pose(index=pose_index)
pose_blender_data[pose_blender_node.driven_transform] = pose_matrix
pose_data = OrderedDict()
pose_data['drivers'] = matrices
pose_data['controllers'] = controller_matrices
pose_data['driven'] = pose_blender_data
pose_data['function_type'] = function_type
pose_data['scale_factor'] = scale_factor
pose_data['distance_method'] = distance_method
pose_data['target_enable'] = cmds.getAttr(
'{solver}.targets[{index}].targetEnable'.format(solver=self, index=pose_index)
)
pose_data['blendshape_data'] = self.get_blendshape_data_for_pose(pose_name) or []
return pose_data
def go_to_pose(self, pose_name):
"""
Sets drivers or controllers to current pose
:param pose_name :type str: name of the pose to move this solvers drivers/driven transform nodes to
"""
# get drivers and controllers matrices
pose = self.pose(pose_name)
pose_index = self.pose_index(pose_name)
# if not controllers, set drivers
if not self.num_controllers():
matrices = pose['drivers']
for driver_index, driver in enumerate(self.drivers()):
# TODO: check if translate, rotate and scale are free to change
cmds.xform(driver, matrix=matrices[driver_index])
# if controllers, set controllers
else:
matrices = pose['controllers']
for controller_index, controller in enumerate(self.controllers()):
# TODO: check if translate, rotate and scale are free to change
cmds.xform(controller, matrix=matrices[controller_index])
# If we have poseBlenders connected we need to create a matching pose on each of those
if cmds.attributeQuery("poseBlenders", node=self, exists=True):
for pose_blender_node_name in cmds.listConnections("{solver}.poseBlenders".format(solver=self)):
# Get the pose blender wrapper from the node name
pose_blender_node = pose_blender.UEPoseBlenderNode(pose_blender_node_name)
pose_blender_node.go_to_pose(index=pose_index)
def poses(self):
"""
Returns dictionary with poses and their transformation.
"""
poses = OrderedDict()
poses_indices = cmds.getAttr('{}.targets'.format(self), multiIndices=True)
if poses_indices:
for pose_index in poses_indices:
# get pose name
pose_name = cmds.getAttr('{}.targets[{}].targetName'.format(self, pose_index))
# BUG - sometimes an unnamed pose will appear when selecting the node causing issues with the indexing
if pose_name:
# get pose transforms
poses[pose_name] = self.pose(pose_name)
return poses
def add_pose(
self, pose_name, drivers=None, matrices=None, controller_matrices=None, driven_matrices=None,
function_type='DefaultFunctionType', distance_method='DefaultMethod', scale_factor=1.0, target_enable=True,
blendshape_data=None
):
"""
Adds pose to RBF Node
"""
if drivers and matrices is None:
matrices = [cmds.xform(driver, q=True, matrix=True, objectSpace=True) for driver in drivers]
if not self.num_drivers():
raise exceptions.InvalidPose('You must add a driver first.')
if self.has_pose(pose_name):
raise exceptions.InvalidPose('Already a pose called: "{}"'.format(pose_name))
if len(matrices) != self.num_drivers():
raise exceptions.InvalidPose('Invalid number of matrices. Must match number of drivers.')
if controller_matrices and len(controller_matrices) != self.num_controllers():
raise exceptions.InvalidPose('Invalid number of controller matrices. Must match number of controllers.')
if self.num_controllers() and not controller_matrices:
raise exceptions.InvalidPose('Invalid number of controller matrices. Must match number of controllers.')
# get new index
pose_index = self.num_poses()
# set matrices
for driver_index, matrix in enumerate(matrices):
attr = '{}.targets[{}].targetValues[{}]'.format(self, pose_index, driver_index)
cmds.setAttr(attr, matrix, type='matrix')
# set controller matrices
if controller_matrices:
for controller_index, matrix in enumerate(controller_matrices):
attr = '{}.targets[{}].targetControllers[{}]'.format(self, pose_index, controller_index)
cmds.setAttr(attr, matrix, type='matrix')
# If we have poseBlenders connected we need to create a matching pose on each of those
if cmds.attributeQuery("poseBlenders", node=self, exists=True):
output_attr = "{solver}.outputs[{pose_index}]".format(solver=self, pose_index=pose_index)
# Iterate through all the pose blenders
for pose_blender_node_name in cmds.listConnections("{solver}.poseBlenders".format(solver=self)):
# Get the pose blender wrapper from the node name
pose_blender_node = pose_blender.UEPoseBlenderNode(pose_blender_node_name)
# If we have driven matrices we need to set the pose from the matrix provided
if driven_matrices and driven_matrices.get(pose_blender_node.driven_transform, False):
pose_blender_node.set_pose(
index=pose_index, pose_name=pose_name,
matrix=driven_matrices.get(pose_blender_node.driven_transform)
)
else:
# Create a pose at the current position in the matching index
pose_blender_node.add_pose_from_current(pose_name=pose_name, index=pose_index)
pose_blender_node.set_weight(index=pose_index, in_float_attr=output_attr)
# set pose name
cmds.setAttr('{}.targets[{}].targetName'.format(self, pose_index), pose_name, type='string')
# create output instance
cmds.getAttr('{}.outputs[{}]'.format(self, pose_index), type=True)
# set the enabled status of the pose
cmds.setAttr('{solver}.targets[{index}].targetEnable'.format(solver=self, index=pose_index), target_enable)
if blendshape_data:
for data in blendshape_data:
blendshape_mesh = data['blendshape_mesh']
blendshape_mesh_orig = data['orig_mesh']
self.add_existing_blendshape(pose_name, blendshape_mesh, blendshape_mesh_orig)
def update_pose(
self, pose_name, drivers=None, matrices=None, controller_matrices=None,
function_type='DefaultFunctionType', distance_method='DefaultMethod', scale_factor=1.0
):
"""
Updates an existing pose on the RBF Node
"""
if drivers and matrices is None:
matrices = [cmds.xform(driver, q=True, matrix=True) for driver in drivers]
if not self.has_pose(pose_name):
raise RuntimeError('Pose "{}" does not exist'.format(pose_name))
# get new index
pose_index = self.pose_index(pose_name)
# set matrices
for driver_index, matrix in enumerate(matrices):
attr = '{}.targets[{}].targetValues[{}]'.format(self, pose_index, driver_index)
cmds.setAttr(attr, matrix, type='matrix')
# set controller matrices
if controller_matrices:
for controller_index, matrix in enumerate(controller_matrices):
attr = '{}.targets[{}].targetControllers[{}]'.format(self, pose_index, controller_index)
cmds.setAttr(attr, matrix, type='matrix')
# If we have poseBlenders connected we need to create a matching pose on each of those
if cmds.attributeQuery("poseBlenders", node=self, exists=True):
for pose_blender_node_name in cmds.listConnections("{solver}.poseBlenders".format(solver=self)):
# Get the pose blender wrapper from the node name
pose_blender_node = pose_blender.UEPoseBlenderNode(pose_blender_node_name)
pose_blender_node.set_pose(index=pose_index)
def add_pose_from_current(self, pose_name, update=False):
"""
Adds current pose to RBF Node
"""
# drivers
matrices = list()
drivers = self.drivers()
for driver in drivers:
matrix = cmds.xform(driver, q=True, matrix=True, objectSpace=True)
matrices.append(matrix)
# controllers
controller_matrices = None
if self.num_controllers():
controller_matrices = list()
for controller in self.controllers():
matrix = cmds.xform(controller, q=True, matrix=True, objectSpace=True)
controller_matrices.append(matrix)
if update:
self.update_pose(pose_name, drivers, matrices, controller_matrices)
else:
self.add_pose(
pose_name=pose_name, drivers=drivers, matrices=matrices,
controller_matrices=controller_matrices
)
def mirror_pose(self, pose_name, mirror_mapping, mirror_blendshapes=True):
"""
Mirrors the specified pose using the given mirror mapping to determine the target drivers/driven transforms
:param pose_name :type str: pose name to mirror
:param mirror_mapping :type pose_wrangler.model.mirror_mapping.MirrorMapping: mirror mapping ref
:param mirror_blendshapes :type bool: option to mirror blendshapes, stops infinite loop when pose is created via
mirror_blendshape
"""
# Get the name of the mirrored solver
target_solver_name = self._get_mirrored_solver_name(mirror_mapping=mirror_mapping)
# Check if the solver already exists
match = [s for s in self.find_all() if str(s) == target_solver_name]
# Force the base pose
for solver in self.find_all():
if solver == self:
continue
if solver.has_pose('default'):
solver.go_to_pose('default')
# If it does, use it
if match:
new_solver = match[0]
# Otherwise create a new solver that is a mirror of this one
else:
new_solver = self.mirror(mirror_mapping=mirror_mapping, mirror_poses=False)
# Go to the pose we want to mirror
self.go_to_pose(pose_name=pose_name)
# Store a list of all of the transforms affecting this pose
transforms = self.drivers()
# Generate a list of the mirrored transforms based on the transforms we found
mirrored_transforms = self._get_mirrored_transforms(transforms, mirror_mapping=mirror_mapping)
for index, source_transform in enumerate(transforms):
target_transform = mirrored_transforms[index]
rotate = cmds.getAttr("{source_transform}.rotate".format(source_transform=source_transform))[0]
cmds.setAttr("{target_transform}.rotate".format(target_transform=target_transform), *rotate)
transforms = self.driven_nodes(pose_blender.UEPoseBlenderNode.node_type)
# Generate a list of the mirrored transforms based on the transforms we found
mirrored_transforms = self._get_mirrored_transforms(transforms, mirror_mapping=mirror_mapping)
# If we have poseBlenders connected we need to create a matching pose on each of those
if cmds.attributeQuery("poseBlenders", node=new_solver, exists=True):
for pose_blender_node_name in cmds.listConnections("{solver}.poseBlenders".format(solver=new_solver)):
# Get the pose blender wrapper from the node name
pose_blender_node = pose_blender.UEPoseBlenderNode(pose_blender_node_name)
pose_blender_node.edit = True
# Iterate through all of the source transforms
for index, source_transform in enumerate(transforms):
# Get the target transform at the same index
target_transform = mirrored_transforms[index]
# Need to get each transforms parent so that we can get the relative offset
# Get the sources parent matrix
source_parent_matrix = om.MMatrix(
cmds.getAttr(
'{source_transform}.parentMatrix'.format(
source_transform=source_transform
)
)
)
# Get the targets parent matrix
target_parent_matrix = om.MMatrix(
cmds.getAttr(
'{target_transform}.parentMatrix'.format(
target_transform=target_transform
)
)
)
# Calculate the translation
translate = om.MVector(
*cmds.getAttr(
'{source_transform}.translate'.format(
source_transform=source_transform
)
)
)
driver_mat_fn = om.MTransformationMatrix(om.MMatrix.kIdentity)
driver_mat_fn.setTranslation(translate, om.MSpace.kWorld)
driver_mat = driver_mat_fn.asMatrix()
scale_matrix_fn = om.MTransformationMatrix(om.MMatrix.kIdentity)
scale_matrix_fn.setScale([-1.0, 1.0, 1.0], om.MSpace.kWorld)
scale_matrix = scale_matrix_fn.asMatrix()
pos_matrix = driver_mat * source_parent_matrix
pos_matrix = pos_matrix * scale_matrix
pos_matrix = pos_matrix * target_parent_matrix.inverse()
mat_fn = om.MTransformationMatrix(pos_matrix)
cmds.setAttr(
'{target_transform}.translate'.format(target_transform=target_transform),
*mat_fn.translation(om.MSpace.kWorld)
)
# Calculate the rotation
rotate = om.MVector(
*cmds.getAttr(
'{source_transform}.rotate'.format(
source_transform=source_transform
)
)
)
driver_mat_fn = om.MTransformationMatrix(om.MMatrix.kIdentity)
# set the values to radians
euler = om.MEulerRotation(*[math.radians(i) for i in rotate])
driver_mat_fn.setRotation(euler)
driver_matrix = driver_mat_fn.asMatrix()
world_matrix = driver_matrix * source_parent_matrix
rot_matrix = source_parent_matrix.inverse() * world_matrix
rot_matrix_fn = om.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 = target_parent_matrix * rot_matrix * target_parent_matrix.inverse()
rot_matrix_fn = om.MTransformationMatrix(final_rot_matrix)
rot = rot_matrix_fn.rotation(asQuaternion=False)
m_rot = om.MVector(*[math.degrees(i) for i in rot])
cmds.setAttr('{target_transform}.rotate'.format(target_transform=target_transform), *m_rot)
# Assume scale is the same
cmds.setAttr(
"{target_transform}.scale".format(target_transform=target_transform),
*cmds.getAttr("{source_transform}.scale".format(source_transform=source_transform))[0]
)
# Add a new pose on the mirrored solver with the same name, update if the pose already exists
new_solver.add_pose_from_current(pose_name=pose_name, update=new_solver.has_pose(pose_name=pose_name))
# Mirror blendshapes if option is specified
if mirror_blendshapes:
new_solver.delete_blendshape(pose_name=pose_name)
self.mirror_blendshape(pose_name=pose_name, mirror_mapping=mirror_mapping)
# If we have poseBlenders connected we need to create a matching pose on each of those
if cmds.attributeQuery("poseBlenders", node=new_solver, exists=True):
for pose_blender_node_name in cmds.listConnections("{solver}.poseBlenders".format(solver=new_solver)):
# Get the pose blender wrapper from the node name
pose_blender_node = pose_blender.UEPoseBlenderNode(pose_blender_node_name)
pose_blender_node.edit = False
def delete_pose(self, pose_name):
"""
Delete the specified pose name
:param pose_name :type str: pose name to delete
"""
# If the pose doesn't exist, raise an exception
if not self.has_pose(pose_name):
raise exceptions.InvalidPose("Pose {pose_name} does not exist.".format(pose_name=pose_name))
LOG.debug("Removing pose: '{pose_name}'".format(pose_name=pose_name))
# Get the existing poses from the solver
poses = self.poses()
pose_index = self.pose_index(pose_name)
# Disconnect all of the blendshapes
for pose in poses.keys():
self.delete_blendshape(pose)
# Iterate through the existing drivers in reverse
for index in reversed(range(len(list(poses.keys())))):
# Remove the pose from the list of targets
cmds.removeMultiInstance('{solver}.targets[{index}]'.format(solver=self, index=index), b=True)
# Remove the deleted pose
poses.pop(pose_name)
# If we have poseBlenders connected we need to create a matching pose on each of those
if cmds.attributeQuery("poseBlenders", node=self, exists=True):
for pose_blender_node_name in cmds.listConnections("{solver}.poseBlenders".format(solver=self)):
# Get the pose blender wrapper from the node name
pose_blender_node = pose_blender.UEPoseBlenderNode(pose_blender_node_name)
pose_blender_node.delete_pose(index=pose_index)
# Iterate through the remaining poses and recreate them
for pose_name, pose_data in poses.items():
pose_data['controller_matrices'] = pose_data.pop('controllers')
pose_data['matrices'] = pose_data.pop('drivers')
pose_data['driven_matrices'] = pose_data.pop('driven')
# Add the pose with the specified kwargs
self.add_pose(pose_name=pose_name, **pose_data)
def is_pose_muted(self, pose_name="", pose_index=-1):
"""
Gets the current status of the pose if it is muted or not
:param pose_name :type str: optional pose name
:param pose_index :type index, optional pose index
:return :type bool: True if the pose is muted, else False
"""
# Get the pose index if name is specified and the index hasn't been
if pose_index < 0 and pose_name:
pose_index = self.pose_index(pose_name)
elif not pose_name and pose_index < 0:
raise exceptions.InvalidPoseIndex("Unable to query the mute status, no pose name or index specified")
attr = '{solver}.targets[{index}].targetEnable'.format(solver=self, index=pose_index)
return not cmds.getAttr(attr)
def mute_pose(self, pose_name="", pose_index=-1, mute=True):
"""
Mute or unmute the specified pose, removing all influences of the pose from the solver.
NOTE: This will affect the solver radius if automatic radius is enabled.
:param pose_name :type str: optional name of the pose
:param pose_index :type index, optional pose index
:param mute :type bool: mute or unmute the pose
"""
# Get the pose index if name is specified and the index hasn't been
if pose_index < 0 and pose_name:
pose_index = self.pose_index(pose_name)
elif not pose_name and pose_index < 0:
raise exceptions.InvalidPoseIndex("Unable to query the mute status, no pose name or index specified")
attr = '{solver}.targets[{index}].targetEnable'.format(solver=self, index=pose_index)
if mute is None:
mute = not self.is_pose_muted(pose_index=pose_index)
cmds.setAttr(attr, not mute)
return not mute
def edit_solver(self, edit=True):
"""
Edit or finish editing this solver
:param edit :type bool: set edit mode on or off
"""
# If we have poseBlenders connected we need to toggle the output connection based on the edit param
if cmds.attributeQuery("poseBlenders", node=self, exists=True):
# Iterate through all the pose blenders
for pose_blender_node_name in cmds.listConnections("{solver}.poseBlenders".format(solver=self)):
# Get the pose blender wrapper from the node name
pose_blender_node = pose_blender.UEPoseBlenderNode(pose_blender_node_name)
# Enable or disable edit mode on the blender
pose_blender_node.edit = edit
# Force the base pose
for solver in self.find_all():
if solver != self and solver.has_pose('default'):
solver.go_to_pose('default')
def get_solver_edit_status(self):
"""
Gets this solvers edit status
:return :type bool: True if in edit mode, False if not
"""
# If we have poseBlenders connected we need to toggle the output connection based on the edit param
if cmds.attributeQuery("poseBlenders", node=self, exists=True):
# Iterate through all the pose blenders
for pose_blender_node_name in cmds.listConnections("{solver}.poseBlenders".format(solver=self)):
# Get the pose blender wrapper from the node name
pose_blender_node = pose_blender.UEPoseBlenderNode(pose_blender_node_name)
# If one blender node is in edit mode, then mark the solver as in edit mode
if pose_blender_node.edit:
return True
return False
def get_pose_for_blendshape_mesh(self, mesh_name):
"""
Get the pose name that the specified blendshape mesh corresponds to
:param mesh_name :type str: mesh name
:return :type str: the pose name this mesh is associated with
"""
if not cmds.ls(mesh_name, type='transform'):
return
# If the blendshape doesn't have a poseName attr, it wasn't connected correctly via the API
if not cmds.attributeQuery('poseName', node=mesh_name, exists=True):
raise exceptions.BlendshapeError("Unable to find pose name for current blendshape")
# Return the poseName value
return cmds.getAttr("{mesh_name}.poseName".format(mesh_name=mesh_name))
def create_blendshape(self, pose_name, mesh_name=None):
"""
Create a blendshape for the specified pose name from the given mesh (or current selection if no mesh name is
specified)
:param pose_name :type str: pose name
:param mesh_name :type str: mesh name (or None for current selection)
"""
# If no mesh is specified grab the current selection
if not mesh_name:
mesh_name = cmds.ls(selection=True, type='transform')
# If no mesh, error
if not mesh_name:
raise exceptions.BlendshapeError("Unable to create blendshape. No mesh was found")
mesh_name = mesh_name[0]
# If the mesh_name is not a shape node, get the shape node for the transform
shapes = cmds.listRelatives(mesh_name, shapes=True, noIntermediate=True)
if not shapes:
raise exceptions.BlendshapeError(
"Unable to create blendshape. No shape node was found for selected "
"transform."
)
# Find the skin cluster to validate that we are working with a skinned mesh
if not cmds.listConnections(shapes[0], type='skinCluster'):
raise exceptions.BlendshapeError("Target mesh is not skinned")
# Create the blendshape at the default pose. Once blendshapes have been created at the presumed pose using
# cmds.invertShape() will convert them to the bind pose position, which is where they need to be for
# pre-deformation blendshapes to work. In this case, we create a duplicate of the bind pose so that when we
# go into edit mode it'll be recreated in the desired pose correctly.
self.go_to_pose("default")
self.isolate_blendshape(pose_name=pose_name, isolate=True)
blendshape_mesh = cmds.duplicate(
mesh_name, name="{solver}_{pose_name}_{mesh}".format(
solver=self,
pose_name=pose_name,
mesh=mesh_name
)
)[0]
self.add_existing_blendshape(pose_name, blendshape_mesh, mesh_name)
return blendshape_mesh
def add_existing_blendshape(self, pose_name, blendshape_mesh="", base_mesh=""):
"""
Add an existing blendshape mesh and create a new blendshape for the specified pose
:param pose_name :type str: pose name
:param blendshape_mesh :type str: name of the blendshape mesh to add
:param base_mesh :type str: mesh name
"""
if not blendshape_mesh or not base_mesh:
selection = cmds.ls(selection=True)
if len(selection) != 2:
raise exceptions.BlendshapeError(
"Invalid number of meshes selected, please select the blendshape mesh"
" followed by the base mesh."
)
blendshape_mesh, base_mesh = selection
# Get the current pose index
pose_index = self.pose_index(pose_name)
# Generate the attribute str to get the target name
attr = '{}.targets[{}].targetName'.format(self, pose_index)
# If the blendshape mesh doesn't have a poseName attr, create it so we can link the solver to the blendshape
# mesh
if not cmds.attributeQuery('poseName', node=blendshape_mesh, exists=True):
cmds.addAttr(blendshape_mesh, dt='string', longName='poseName')
# Connect the attrs
utils.connect_attr(attr, '{blendshape_mesh}.poseName'.format(blendshape_mesh=blendshape_mesh))
# Generate the name for the blendshape attribute on the base mesh
blendshape_attr = "{base_mesh}.blendShape".format(base_mesh=base_mesh)
blendshape = None
# Find the blendshape if it already exists
if cmds.attributeQuery('blendShape', node=base_mesh, exists=True):
blendshapes = cmds.listConnections(blendshape_attr, type='blendShape') or []
if blendshapes:
blendshape = blendshapes[0]
# Create the blendshape if it does not
if not blendshape:
# Create a new blendshape from the given mesh
blendshape = cmds.blendShape(
base_mesh,
name="{base_mesh}_blendShape".format(base_mesh=base_mesh),
frontOfChain=True
)[0]
# Connect the blendshape_mesh to the blendshape via message attribute
utils.message_connect(
"{mesh}.blendShape".format(mesh=base_mesh),
"{blendshape}.meshOrig".format(blendshape=blendshape)
)
if not cmds.attributeQuery("meshes", node=blendshape, exists=True):
cmds.addAttr(blendshape, longName="meshes", attributeType='message', multi=True)
target_index = utils.is_connected_to_array(
'{mesh}.blendShape'.format(mesh=blendshape_mesh),
"{blendshape}.meshes".format(blendshape=blendshape)
)
if target_index is None:
target_index = utils.get_next_available_index_in_array("{blendshape}.meshes".format(blendshape=blendshape))
# Connect the blendshape_mesh to the blendshape via message attribute
utils.message_connect(
"{mesh}.blendShape".format(mesh=blendshape_mesh),
"{blendshape}.meshes".format(blendshape=blendshape), out_array=True
)
cmds.blendShape(blendshape, edit=True, target=(base_mesh, target_index, blendshape_mesh, 1.0))
if utils.is_connected_to_array(
"{blendshape_mesh}.meshOrig".format(blendshape_mesh=blendshape_mesh),
"{mesh}.blendShapeMeshes".format(mesh=base_mesh)
) is None:
# Connect the original mesh to the blendshape mesh via message attribute
utils.message_connect(
"{mesh}.blendShapeMeshes".format(mesh=base_mesh),
"{blendshape_mesh}.meshOrig".format(blendshape_mesh=blendshape_mesh), in_array=True
)
self.isolate_blendshape(pose_name=pose_name, isolate=False)
def edit_blendshape(self, pose_name, edit=False):
"""
Edit the blendshape at the given pose. This will disconnect and isolate the specific blendshape
:param pose_name: Pose name to edit
:param edit: enable or disable editing
:return:
"""
# Go to the target pose so that we can create a new mesh to edit
self.go_to_pose(pose_name=pose_name)
# Get the blendshape data associated with this pose
blendshape_data = self.get_blendshape_data_for_pose(pose_name=pose_name)
if not blendshape_data:
return
for data in blendshape_data:
orig_mesh = data['orig_mesh']
blendshape_mesh = data['blendshape_mesh']
blendshape_mesh_shape = data['blendshape_mesh_shape']
blendshape_mesh_orig_index = data['blendshape_mesh_orig_index']
mesh_index = data['mesh_index']
pose_index = data['pose_index']
blendshape = data['blendshape']
pose_attr = data['pose_attr']
mesh_index_attr = '{blendshape}.meshes[{mesh_index}]'.format(blendshape=blendshape, mesh_index=mesh_index)
weight_index_attr = '{blendshape}.weight[{mesh_index}]'.format(blendshape=blendshape, mesh_index=mesh_index)
output_attr = '{solver}.outputs[{pose_index}]'.format(solver=self, pose_index=pose_index)
blendshape_mesh_plug = ('{blendshape_node}.inputTarget[{mesh_index}].inputTargetGroup[0].'
'inputTargetItem[6000].inputGeomTarget'.format(
blendshape_node=blendshape,
mesh_index=mesh_index
))
blendshape_mesh_orig_attr = '{mesh}.blendShapeMeshes[{blendshape_mesh_orig_index}]'.format(
mesh=orig_mesh, blendshape_mesh_orig_index=blendshape_mesh_orig_index
)
if edit:
# Isolate the current blendshape, removing all other blendshape influences so this blendshape can be
# worked on in isolation
self.isolate_blendshape(pose_name=pose_name, isolate=True)
# Generate a new blendshape mesh from the current pose (so that we can edit the shape in place)
new_blendshape_mesh = cmds.duplicate(
orig_mesh, name="{blendshape_mesh}_EDIT".format(
blendshape_mesh=blendshape_mesh
)
)[0]
self.delete_blendshape(pose_name=pose_name)
else:
# Select original mesh then new shape
new_blendshape_mesh = cmds.invertShape(orig_mesh, blendshape_mesh)
# Rename the blendshape mesh
new_blendshape_mesh = cmds.rename(new_blendshape_mesh, blendshape_mesh.replace('_EDIT', ''))
new_blendshape_mesh_shape = cmds.listRelatives(new_blendshape_mesh, type='shape')[0]
# Handle the disconnect and reconnect of new attributes
if cmds.isConnected(
'{blendshape_mesh_shape}.worldMesh[0]'.format(blendshape_mesh_shape=blendshape_mesh_shape),
blendshape_mesh_plug
):
cmds.disconnectAttr(
'{blendshape_mesh_shape}.worldMesh[0]'.format(
blendshape_mesh_shape=blendshape_mesh_shape
), blendshape_mesh_plug
)
# Disconnect the old blendshape mesh from the blendshape
cmds.disconnectAttr(
'{blendshape_mesh}.blendShape'.format(blendshape_mesh=blendshape_mesh),
mesh_index_attr
)
# Disconnect the old blendshape mesh from the orig mesh
cmds.disconnectAttr(
blendshape_mesh_orig_attr,
'{blendshape_mesh}.meshOrig'.format(blendshape_mesh=blendshape_mesh)
)
# Disconnect the old blendshape mesh from the solvers pose
cmds.disconnectAttr(pose_attr, '{blendshape_mesh}.poseName'.format(blendshape_mesh=blendshape_mesh))
# Delete the old blendshape mesh
cmds.delete(blendshape_mesh)
# Connected the new mesh to the blendshape
utils.connect_attr(
'{new_blendshape_mesh}.blendShape'.format(new_blendshape_mesh=new_blendshape_mesh),
mesh_index_attr
)
# Connect the original mesh to the blendshape mesh via message attribute
utils.message_connect(
"{mesh}.blendShapeMeshes".format(mesh=orig_mesh),
"{blendshape_mesh}.meshOrig".format(blendshape_mesh=new_blendshape_mesh),
in_array=True
)
# If the blendshape mesh doesn't have a poseName attr, create it so we can link the solver to the blendshape
# mesh
if not cmds.attributeQuery('poseName', node=new_blendshape_mesh, exists=True):
cmds.addAttr(new_blendshape_mesh, dt='string', longName='poseName')
# Connect the new blendshape mesh up to the solver
utils.connect_attr(pose_attr, '{blendshape_mesh}.poseName'.format(blendshape_mesh=new_blendshape_mesh))
if edit:
cmds.select(new_blendshape_mesh, replace=True)
cmds.hide(orig_mesh)
else:
self.add_existing_blendshape(
pose_name=pose_name, blendshape_mesh=new_blendshape_mesh, base_mesh=orig_mesh
)
self.isolate_blendshape(pose_name=pose_name, isolate=False)
cmds.hide(new_blendshape_mesh)
cmds.showHidden(orig_mesh)
def isolate_blendshape(self, pose_name, isolate=True):
"""
Will disconnect or reconnect all of the blendshapes that aren't directly controlled by this pose
:param pose_name :type str: name of the pose that the blendshape is at
:param isolate :type bool: True will disconnect the blendshapes, False will reconnect
"""
# Get the output attrs
output_attributes = self.output_attributes()
# Get the specified pose's index
pose_index = self.pose_index(pose_name)
# If we don't have any output attributes, there's nothing to isolate
if output_attributes:
# Force default pose on everything bar this solver
for solver in self.find_all():
if solver == self:
continue
if solver.has_pose('default'):
solver.go_to_pose(pose_name='default')
# If we are isolating, we want to disconnect all the other blendshapes connected to this solver
if isolate:
# Iterate through the output attributes
for index, attr in enumerate(output_attributes):
# If the index is the specified pose, skip it because we want to keep this one connected
if index == pose_index:
continue
# List all the blendshape connections and disconnect each blendshape
connections = cmds.listConnections(
attr, destination=True, shapes=False,
plugs=True, type='blendShape'
) or []
for con in connections:
# Disconnect
cmds.disconnectAttr(attr, con)
# Force the disconnected blendshapes weight to 0 so that it doesn't have any influence on the
# final rendered mesh
cmds.setAttr(con, 0.0)
else:
# Undo the isolation, reconnecting all the attributes
for index, attr in enumerate(output_attributes):
# Generate the pose name attr for this index
pose_name_attr = '{}.targets[{}].targetName'.format(self, index)
# List connections to find all the transforms connected
transforms = cmds.listConnections(
pose_name_attr, type='transform', destination=True,
shapes=False
) or []
# For each transform
for transform in transforms:
# Check if it has a blendshape attr, will be missing if blendshapes aren't added via the API
if cmds.attributeQuery("blendShape", node=transform, exists=True):
# Generate the blendshape attribute name
blendshape_attr = "{transform}.blendShape".format(transform=transform)
# Iterate through each connected blendshape
for blendshape in cmds.listConnections(blendshape_attr, type='blendShape') or []:
# Get list of connected blendshape mesh plugs
blendshape_mesh_plugs = cmds.listConnections(
"{blendshape}.meshes".format(blendshape=blendshape),
plugs=True
) or []
if blendshape_attr not in blendshape_mesh_plugs:
continue
target_index = blendshape_mesh_plugs.index(blendshape_attr)
# Connect the output attr to the blendshape
target_attr = "{blendshape}.weight[{index}]".format(
blendshape=blendshape,
index=target_index
)
if not cmds.isConnected(attr, target_attr):
try:
cmds.connectAttr(attr, target_attr)
except RuntimeError as e:
LOG.error(e)
def mirror_blendshape(self, pose_name, mirror_mapping):
"""
Mirrors the blendshape at the specified pose name. Will mirror the solver if it doesn't exist
:param pose_name :type str: name of the pose this blendshape is associated with
:param mirror_mapping :type pose_wrangler.model.mirror_mapping.MirrorMapping: mirror mapping ref
"""
# Get the pose index for this name
pose_index = self.pose_index(pose_name)
# Generate the attribute str to get the target name
attr = '{}.targets[{}].targetName'.format(self, pose_index)
# Find the blendshape transform node connected
blendshape_transforms = cmds.listConnections(attr, type='transform') or []
# If we don't find one, the blendshape wasn't created via the API
if not blendshape_transforms:
LOG.debug("Unable to find blendshape associated with '{pose_name}' pose".format(pose_name=pose_name))
return
# Get the transform
blendshape_transform = blendshape_transforms[0]
# Get the original mesh
original_mesh = cmds.listConnections(
'{blendshape}.meshOrig'.format(blendshape=blendshape_transform),
type='transform'
) or []
if not original_mesh:
raise exceptions.BlendshapeError(
"Unable to find original mesh for blendshape: {blendshape}".format(
blendshape=blendshape_transform
)
)
original_mesh = original_mesh[0]
# Generate the mirrored name for the blendshape
mirrored_blendshape_name = self._get_mirrored_transforms(
[blendshape_transform], mirror_mapping,
ignore_invalid_nodes=True
)[0]
if cmds.objExists(mirrored_blendshape_name):
cmds.delete(mirrored_blendshape_name)
# Duplicate the mesh with the new name
mirrored_blendshape_mesh = cmds.duplicate(blendshape_transform, name=mirrored_blendshape_name)[0]
# Get the name of the mirrored solver
target_solver_name = self._get_mirrored_solver_name(mirror_mapping=mirror_mapping)
# Check if the solver already exists
match = [s for s in self.find_all() if str(s) == target_solver_name]
# If it does, use it
if match:
new_solver = match[0]
# Otherwise create a new solver that is a mirror of this one
else:
new_solver = self.mirror(mirror_mapping=mirror_mapping, mirror_poses=False)
# If the solver doesn't have this pose, mirror it
if not new_solver.has_pose(pose_name):
self.mirror_pose(pose_name=pose_name, mirror_mapping=mirror_mapping)
# Add the new blendshape mesh to the mirrored solver
new_solver.add_existing_blendshape(
pose_name, blendshape_mesh=mirrored_blendshape_mesh,
base_mesh=original_mesh
)
blendshape_attr = "{base_mesh}.blendShape".format(base_mesh=original_mesh)
blendshape = None
if cmds.attributeQuery('blendShape', node=original_mesh, exists=True):
blendshapes = cmds.listConnections(blendshape_attr, type='blendShape') or []
if blendshapes:
blendshape = blendshapes[0]
if not blendshape:
raise exceptions.BlendshapeError(
"Unable to find blendshape for {mesh} pose: {pose}".format(
mesh=original_mesh,
pose=pose_name
)
)
target_plug = "{blendshape_mesh}.blendShape".format(blendshape_mesh=mirrored_blendshape_mesh)
blendshape_mesh_plugs = cmds.listConnections(
"{blendshape}.meshes".format(blendshape=blendshape),
plugs=True
) or []
if target_plug not in blendshape_mesh_plugs:
raise exceptions.BlendshapeError(
"Unable to find {mesh} connection to {blendshape}".format(
mesh=original_mesh,
blendshape=blendshape
)
)
target_index = blendshape_mesh_plugs.index(target_plug)
# Find the mirror axis from the drivers
current_drivers = [cmds.xform(driver, query=True, translation=True, worldSpace=True) for driver in
self.drivers()]
mirrored_drivers = [cmds.xform(driver, query=True, translation=True, worldSpace=True) for driver in
new_solver.drivers()]
# Find the mirrored axis
mirror_axis = self._get_mirrored_axis(source_transforms=current_drivers, mirrored_transforms=mirrored_drivers)
# HACK For some reason flipTarget fails to flip when called a single time
# Mirror the blendshape along the target axis
cmds.blendShape(
blendshape, edit=True, symmetryAxis=mirror_axis, symmetrySpace=1,
flipTarget=[(0, target_index)]
)
# Mirror the blendshape along the target axis
cmds.blendShape(
blendshape, edit=True, symmetryAxis=mirror_axis, symmetrySpace=1,
flipTarget=[(0, target_index)]
)
blendshape_data = new_solver.get_blendshape_data_for_pose(pose_name=pose_name)
parent = cmds.listRelatives(mirrored_blendshape_mesh, parent=True) or []
cmds.delete(mirrored_blendshape_mesh)
# Regenerate the blendshape mesh from the new mirrored blendshape
new_mesh = cmds.sculptTarget(blendshape, edit=True, target=target_index, regenerate=True)[0]
# Rename the new mesh so that it doesn't match the old blendshape name and get deleted
new_mesh = cmds.rename(new_mesh, "{new_mesh}_TEMP".format(new_mesh=new_mesh))
# Delete the original blendshape because the geometry isn't mirrored
new_solver.delete_blendshape(pose_name, blendshape_data=blendshape_data)
# Rename the mirrored mesh to the correct mirrored blendshape mesh name
cmds.rename(new_mesh, mirrored_blendshape_mesh)
cmds.hide(mirrored_blendshape_mesh)
if parent:
cmds.parent(mirrored_blendshape_mesh, parent)
# Add the new mesh as a blendshape
new_solver.add_existing_blendshape(pose_name, blendshape_mesh=mirrored_blendshape_mesh, base_mesh=original_mesh)
def delete_blendshape(self, pose_name, blendshape_data=None, delete_mesh=False):
"""
Delete the blendshape associated with the specified pose
:param pose_name :type str: name of the pose to delete blendshapes for
:param blendshape_data :type dict or None: optional blendshape data
:param delete_mesh :type bool: True = mesh will be deleted, False = connections will be broken
"""
# Delete all the blendshapes associated with the specified pose
for data in blendshape_data or self.get_blendshape_data_for_pose(pose_name=pose_name):
# Get the blendshape, blendshape mesh and target index from the blendshape data
blendshape = data['blendshape']
blendshape_mesh = data['blendshape_mesh']
mesh_index = data['mesh_index']
# Remove the attributes at the target index
cmds.removeMultiInstance(
'{blendshape}.meshes[{index}]'.format(
blendshape=blendshape,
index=mesh_index
), b=True
)
cmds.removeMultiInstance(
'{blendshape}.weight[{index}]'.format(
blendshape=blendshape,
index=mesh_index
), b=True
)
cmds.removeMultiInstance(
'{blendshape}.inputTarget[0].inputTargetGroup[{index}]'.format(
blendshape=blendshape,
index=mesh_index
), b=True
)
cmds.aliasAttr(
'weight{index}'.format(index=mesh_index), '{blendshape}.weight[{index}]'.format(
blendshape=blendshape,
index=mesh_index
)
)
pose_index = self.pose_index(pose_name=pose_name)
from_attr = "{self}.targets[{pose_index}].targetName".format(self=self, pose_index=pose_index)
to_attr = "{blendshape}.poseName".format(blendshape=blendshape_mesh)
cmds.disconnectAttr(from_attr, to_attr)
# Delete the mesh
if cmds.objExists(blendshape_mesh) and delete_mesh:
cmds.select(blendshape_mesh, replace=True)
cmds.delete(blendshape_mesh)
def get_blendshape_data_for_pose(self, pose_name):
"""
Get all the blendshape data associated with the specified pose name
:param pose_name :type str: pose name
:return :type list of dictionaries: blendshape data
"""
blendshape_data = []
# Get the pose index for this name
pose_index = self.pose_index(pose_name)
if pose_index < 0:
return blendshape_data
# Generate the attribute str to get the target name
target_name_attr = '{}.targets[{}].targetName'.format(self, pose_index)
# Find the blendshape transform nodes connected
blendshape_transforms = cmds.listConnections(target_name_attr, type='transform') or []
# If we don't find one, the blendshape wasn't created via the API
if not blendshape_transforms:
return blendshape_data
# Iterate through the transforms associated with the blendshape
for blendshape_mesh in blendshape_transforms:
blendshape_mesh_shape = cmds.listRelatives(blendshape_mesh, type='shape')[0]
# Get the connected blendshapes
blendshape_plug = '{transform}.blendShape'.format(transform=blendshape_mesh)
blendshapes = cmds.listConnections(blendshape_plug, type='blendShape') or []
if len(blendshapes) != 1:
raise exceptions.BlendshapeError(
"Unable to find blendshape associated with {pose_name} for mesh: "
"{blendshape_mesh}".format(
pose_name=pose_name,
blendshape_mesh=blendshape_mesh
)
)
blendshape = blendshapes[0]
# Get the original mesh that the blendshape is driving
orig_mesh = cmds.listConnections(
"{mesh}.meshOrig".format(mesh=blendshape_mesh, type='transform')
)
if not orig_mesh:
raise exceptions.BlendshapeError(
"Unable to find original mesh for blendshape: "
"'{mesh}'".format(mesh=blendshape_mesh)
)
orig_mesh = orig_mesh[0]
# Get the meshes connected to the blendshape
blendshape_mesh_plugs = cmds.listConnections(
"{blendshape}.meshes".format(blendshape=blendshape),
plugs=True
) or []
if blendshape_plug not in blendshape_mesh_plugs:
LOG.warning(
"Could not find connection between mesh: {mesh} and blendshape: {blendshape}".format(
mesh=blendshape_mesh,
blendshape=blendshape
)
)
continue
# Get the index that the blendshape mesh is connected to the blendshape
target_index = blendshape_mesh_plugs.index(blendshape_plug)
orig_mesh_plugs = cmds.listConnections(
"{orig_mesh}.blendShapeMeshes".format(orig_mesh=orig_mesh),
plugs=True
) or []
blendshape_mesh_orig_plug = "{blendshape_mesh}.meshOrig".format(blendshape_mesh=blendshape_mesh)
if blendshape_mesh_orig_plug not in orig_mesh_plugs:
LOG.warning(
"Could not find connection between mesh: {mesh} and blendshape mesh: {blendshape}".format(
mesh=orig_mesh,
blendshape=blendshape_mesh
)
)
continue
blendshape_mesh_orig_index = orig_mesh_plugs.index(blendshape_mesh_orig_plug)
blendshape_data.append(
{
# Blendshape Transform Node
'blendshape_mesh': blendshape_mesh,
# Blendshape Shape Node
'blendshape_mesh_shape': blendshape_mesh_shape,
# Original Mesh Transform Node
'orig_mesh': orig_mesh,
# Blendshape Node
'blendshape': blendshape,
# orig_mesh.blendShapeMeshes index for this blendshape mesh
'blendshape_mesh_orig_index': blendshape_mesh_orig_index,
# Index of the blendshape transform node on the blendshape node
'mesh_index': target_index,
# The attribute name
'pose_attr': target_name_attr,
'pose_index': pose_index
}
)
return blendshape_data
def pose_driven_attributes(self, pose_name):
"""
Returns attributes driven by the pose output
"""
pose_driven_attributes = list()
if self.has_pose(pose_name):
pose_index = self.pose_index(pose_name)
pose_driven_attributes = self.driven_attributes()[pose_index]
return pose_driven_attributes
def pose_output_attribute(self, pose_name):
"""
Returns the pose output attribute
"""
if self.has_pose(pose_name):
pose_index = self.pose_index(pose_name)
return self.output_attributes()[pose_index]
def pose_name(self, pose_index):
"""
Returns pose name from index
"""
poses_indices = cmds.getAttr('{}.targets'.format(self), multiIndices=True)
if poses_indices and pose_index in poses_indices:
return cmds.getAttr('{}.targets[{}].targetName'.format(self, pose_index))
return None
def rename_pose(self, pose_index, pose_name):
"""
Renames a pose given an index
"""
poses_indices = cmds.getAttr('{}.targets'.format(self), multiIndices=True)
if poses_indices:
if pose_index not in poses_indices:
raise RuntimeError('Pose Index "{}" not found'.format(pose_index))
if self.has_pose(pose_name):
raise RuntimeError('Pose "{}" already exists'.format(pose_name))
cmds.setAttr('{}.targets[{}].targetName'.format(self, pose_index), pose_name, type='string')
return pose_name
def unnamed_poses_indices(self):
"""
Returns unnamed poses indices
"""
unnamed_poses = list()
poses_indices = cmds.getAttr('{}.targets'.format(self), multiIndices=True)
for pose_index in poses_indices:
if not self.pose_name(pose_index):
unnamed_poses.append(pose_index)
return unnamed_poses
def name_unnamed_poses(self, basename='pose'):
"""
Names unnamed poses with basename.
NOTE: this is a utility for existing Metahuman scenes where
the pose names are stored in network nodes.
"""
pose_names = list()
for pose_index in self.unnamed_poses_indices():
# add index to basename
pose_name = '{}_{}'.format(basename, pose_index)
# make sure is unique name
while self.has_pose(pose_name):
pose_name = '{}_{}'.format(basename, pose_index + 1)
self.rename_pose(pose_index, pose_name)
pose_names.append(pose_name)
return pose_names
def mirror(self, mirror_mapping, mirror_poses=True):
"""
Mirror this RBF Solver
:param mirror_mapping :type pose_wrangler.model.mirror_mapping.MirrorMapping: mirror mapping ref
:param mirror_poses :type bool: should we mirror poses too?
:return :type RBFNode: mirrored RBF Solver wrapper
"""
# Generate the target solver name from the current solver name
target_rbf_solver_name = self._get_mirrored_solver_name(mirror_mapping=mirror_mapping)
LOG.debug(
"Creating mirrored solver: '{target_pose_driver_name}".format(
target_pose_driver_name=target_rbf_solver_name
)
)
# Serialize the solver data
solver_data = self.data()
# Grab the drivers and driven from the serialized data
mirrored_transform_data = {
'drivers': solver_data.pop('drivers'),
'driven_transforms': solver_data.pop('driven_transforms')
}
# Iterate through the drivers and driven transforms to find their mirrored counterpart
for key, transforms in mirrored_transform_data.items():
# Update the dict with the new transforms
mirrored_transform_data[key] = self._get_mirrored_transforms(transforms, mirror_mapping)
# Update the solver data with the new drivers
solver_data['drivers'] = mirrored_transform_data.pop('drivers')
# Update the solver data with the new driven transforms
solver_data['driven_transforms'] = mirrored_transform_data.pop('driven_transforms')
# Update the solver data with the new solver name
solver_data['solver_name'] = target_rbf_solver_name
# Remove pose data to iterate over later
pose_data = solver_data.pop('poses')
solver_data['poses'] = {}
# Delete any existing solvers:
for solver in [solver for solver in RBFNode.find_all() if str(solver) == target_rbf_solver_name]:
solver.delete()
# Create a new solver from the data
mirrored_solver = RBFNode.create_from_data(solver_data)
# If we are mirroring poses, iterate through each pose and mirror it
if mirror_poses:
for pose_name in pose_data.keys():
self.mirror_pose(pose_name=pose_name, mirror_mapping=mirror_mapping)
# Force the base pose
for solver in self.find_all():
if solver.has_pose('default'):
solver.go_to_pose('default')
# If we aren't mirroring poses, create the default pose
else:
mirrored_solver.add_pose_from_current(pose_name='default')
return mirrored_solver
def delete(self):
"""
Delete the Maya node associated with this class
"""
# If we have blendshapes, disconnect them all
if self.driven_nodes(type='blendShape'):
for pose in self.poses().keys():
self.delete_blendshape(pose)
cmds.delete(self._node)
# ----------------------------------------------------------------------------------------------
# ENUM UTILS
# ----------------------------------------------------------------------------------------------
def _set_enum_attribute(self, attribute, value):
"""
Sets enum attribute either by string or index value
"""
if isinstance(value, six.string_types):
# get enum index
selection_list = OpenMaya.MSelectionList()
selection_list.add(attribute)
plug = OpenMaya.MPlug()
selection_list.getPlug(0, plug)
mfn = OpenMaya.MFnEnumAttribute(plug.attribute())
index = mfn.fieldIndex(value)
cmds.setAttr(attribute, index)
else:
cmds.setAttr(attribute, value)
def _get_mirrored_axis(self, source_transforms, mirrored_transforms):
axis_names = 'xyz'
for index, driver in enumerate(source_transforms):
mirrored_driver = mirrored_transforms[index]
for i, axis in enumerate(driver):
if axis > 0 and mirrored_driver[i] > 0 or axis < 0 and mirrored_driver[i] < 0:
continue
return axis_names[i]
def _get_mirrored_solver_name(self, mirror_mapping):
"""
Generate the mirrored solver name using the specified mirror mapping
:param mirror_mapping :type pose_wrangler.model.mirror_mapping.MirrorMapping: mirror mapping ref
:return :type str: mirrored solver name
"""
# Grab the solver expression from the mirror mapping and check that this solver matches the correct naming
match = re.match(mirror_mapping.solver_expression, str(self))
# If it doesn't match, raise exception
if not match:
raise exceptions.exceptions.InvalidMirrorMapping(
"Unable to mirror solver '{solver}'. The naming conventions do "
"not match the mirror mapping specified: {expression}".format(
solver=self,
expression=mirror_mapping.solver_expression
)
)
# Generate the new pose driver name
target_rbf_solver_name = ""
for group in match.groups():
# If the the group matches the source solver syntax, change the group to be the target syntax.
# I.e if the source is _l_, change it to _r_
if group == mirror_mapping.source_solver_syntax:
group = mirror_mapping.target_solver_syntax
# If the group matches the target, change the group to the source and swap the sides in the config
elif group == mirror_mapping.target_solver_syntax:
group = mirror_mapping.source_solver_syntax
mirror_mapping.swap_sides()
# Add the group name to generate the new name for the solver
target_rbf_solver_name += group
return target_rbf_solver_name
def _get_mirrored_transforms(self, transforms, mirror_mapping, ignore_invalid_nodes=False):
"""
Generate a list of transforms that mirror the list given
:param transforms :type list: list of transforms to get the mirrored name for
:param mirror_mapping :type pose_wrangler.model.mirror_mapping.MirrorMapping: mirror mapping ref
:return :type list: list of mirrored transform names
"""
# Generate empty list to store the newly mapped transforms
new_transforms = []
for transform in transforms:
# Check if the transform matches the target transform expression
match = re.match(mirror_mapping.transform_expression, transform)
# If it doesn't, raise exception. Can't work with incorrectly named transforms
if not match:
raise exceptions.exceptions.InvalidMirrorMapping(
"Unable to mirror transform '{transform}'. The naming conventions do "
"not match the mirror mapping specified: {expression}".format(
transform=transform,
expression=mirror_mapping.transform_expression
)
)
# Generate the new pose transform name
target_transform_name = ""
# Iterate through the groups
for group in match.groups():
# If the the group matches the source transform syntax, change the group to be the target syntax.
if group == mirror_mapping.source_transform_syntax:
group = mirror_mapping.target_transform_syntax
elif group == mirror_mapping.target_transform_syntax:
group = mirror_mapping.source_transform_syntax
# Add the group name to generate the new name for the transform
target_transform_name += group
# If the generated transform name doesn't exist, raise exception
if not cmds.ls(target_transform_name) and not ignore_invalid_nodes:
raise exceptions.exceptions.InvalidMirrorMapping(
"Unable to mirror transform '{transform}'. Target transform does not exist: '{target}'".format(
transform=transform,
target=target_transform_name
)
)
# Add the new transform to the list
new_transforms.append(target_transform_name)
return new_transforms