1966 lines
87 KiB
Python
1966 lines
87 KiB
Python
|
# 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
|