994 lines
41 KiB
Python
994 lines
41 KiB
Python
# Copyright Epic Games, Inc. All Rights Reserved.
|
|
import traceback
|
|
import math
|
|
import json
|
|
from PySide2 import QtWidgets
|
|
import maya.cmds as cmds
|
|
import maya.api.OpenMaya as api
|
|
|
|
|
|
class UE4PoseDriver(object):
|
|
'''
|
|
TODO:
|
|
- add a transform to an existing pose driver
|
|
- I was told that the Maya implementation only works with one driver,
|
|
not multiple transforms, this reflects that
|
|
- properly serialize the UI driving mode when opening the UI based on connections into the drivers
|
|
- validate the node network on load, knowing that a driven has driven, etc.. (low prio)
|
|
'''
|
|
|
|
def __init__(self, existing_interpolator=None):
|
|
|
|
self.name = None
|
|
|
|
self.solver = None
|
|
self.decompose_mx_list = []
|
|
|
|
self.copied_trs_map = {}
|
|
|
|
if existing_interpolator:
|
|
if cmds.nodeType(existing_interpolator) == 'UE4RBFSolverNode':
|
|
self.name = existing_interpolator
|
|
self.solver = existing_interpolator
|
|
else:
|
|
print('Node', existing_interpolator, 'is not of type UE4RBFSolverNode.')
|
|
return
|
|
|
|
## General utils pulled from utils lib
|
|
#################################################################################
|
|
def attrExists(self, attr):
|
|
if '.' in attr:
|
|
node, att = attr.split('.')
|
|
return cmds.attributeQuery(att, node=node, ex=1)
|
|
else:
|
|
cmds.warning('attrExists: No attr passed in: ' + attr)
|
|
return False
|
|
|
|
def msgConnect(self, attribFrom, attribTo, debug=0):
|
|
objFrom, attFrom = attribFrom.split('.')
|
|
objTo, attTo = attribTo.split('.')
|
|
objTo, attTo = attribTo.split('.')
|
|
if debug: print('msgConnect>>> Locals:', locals())
|
|
if not self.attrExists(attribFrom):
|
|
cmds.addAttr(objFrom, longName=attFrom, attributeType='message')
|
|
if not self.attrExists(attribTo):
|
|
cmds.addAttr(objTo, longName=attTo, attributeType='message')
|
|
# check that both atts, if existing are msg atts
|
|
for a in (attribTo, attribFrom):
|
|
if cmds.getAttr(a, type=1) != 'message':
|
|
cmds.warning('msgConnect: Attr, ' + a + ' is not a message attribute. CONNECTION ABORTED.')
|
|
return False
|
|
try:
|
|
return cmds.connectAttr(attribFrom, attribTo, f=True)
|
|
except Exception as e:
|
|
print(traceback.format_exc())
|
|
return False
|
|
|
|
def break_all_connections(self, node):
|
|
destination_conns = cmds.listConnections(node, plugs=True, connections=True, source=False) or []
|
|
for i in range(0, len(destination_conns), 2):
|
|
cmds.disconnectAttr(destination_conns[i], destination_conns[i + 1])
|
|
source_conns = cmds.listConnections(node, plugs=True, connections=True, destination=False) or []
|
|
for i in range(0, len(source_conns), 2):
|
|
# we have to flip these because the output is always node centric and not connection centric
|
|
cmds.disconnectAttr(source_conns[i + 1], source_conns[i])
|
|
|
|
def get_dag_dict(self, dagpose_node):
|
|
dag_dict = {}
|
|
if cmds.nodeType(dagpose_node) == 'dagPose':
|
|
for idx in range(1, cmds.getAttr(dagpose_node + '.members', size=True)):
|
|
conns = cmds.listConnections(dagpose_node + '.members[' + str(idx) + ']')
|
|
if conns:
|
|
# because it's a message there could be more than one incoming, but Maya doesn't do that
|
|
# set the value for the name key to the int socket
|
|
# also this check for length is because of the garbage menber plugs for parents
|
|
if len(conns) == 1:
|
|
dag_dict[conns[0]] = idx
|
|
return dag_dict
|
|
else:
|
|
cmds.error('Requires dagPose node.')
|
|
return False
|
|
|
|
def get_local_matrix(self, node, invertJointOrient=False):
|
|
|
|
local_mx = cmds.xform(node, m=1, q=1)
|
|
# ignore code below for now - that code will take joint orient into account
|
|
return local_mx
|
|
|
|
if invertJointOrient:
|
|
# if it's a joint, lets remove the joint orient from the local matrix
|
|
if cmds.nodeType(node) == "joint":
|
|
selList = api.MGlobal.getSelectionListByName(node)
|
|
dagPath = selList.getDagPath(0)
|
|
|
|
parentMatrix = dagPath.exclusiveMatrix()
|
|
worldMatrix = dagPath.inclusiveMatrix()
|
|
|
|
jointOrient = cmds.getAttr(node + ".jointOrient")[0]
|
|
x = math.radians(jointOrient[0])
|
|
y = math.radians(jointOrient[1])
|
|
z = math.radians(jointOrient[2])
|
|
joEuler = api.MEulerRotation(x, y, z)
|
|
joMatrix = joEuler.asMatrix()
|
|
|
|
localMatrix = worldMatrix * parentMatrix.inverse()
|
|
orientLocalMatrix = localMatrix * joMatrix.inverse()
|
|
|
|
# combine the orient local matrix and the translation which shouldn't be changed based on the orientation
|
|
for i in range(12):
|
|
localMatrix[i] = orientLocalMatrix[i]
|
|
|
|
# set the local matrix to this
|
|
local_mx = localMatrix
|
|
|
|
return local_mx
|
|
|
|
def create_matrix_node(self, node, mx_node_name, invertJointOrient=False):
|
|
local_mx = self.get_local_matrix(node, invertJointOrient=invertJointOrient)
|
|
world_mx = cmds.xform(node, m=1, ws=1, q=1)
|
|
|
|
mx_node = cmds.createNode('network', name=mx_node_name)
|
|
cmds.addAttr(mx_node, at='matrix', longName='outputLocalMatrix')
|
|
cmds.addAttr(mx_node, at='matrix', longName='outputWorldMatrix')
|
|
cmds.addAttr(mx_node, dt='string', longName='matrix_node_ver')
|
|
|
|
self.msgConnect(node + '.mx_pose', mx_node + '.driven_transform')
|
|
|
|
cmds.setAttr(mx_node + '.outputLocalMatrix', local_mx, type='matrix')
|
|
cmds.setAttr(mx_node + '.outputWorldMatrix', world_mx, type='matrix')
|
|
|
|
return mx_node
|
|
|
|
## Methods used by class
|
|
#################################################################################
|
|
def init_pose_driver_system(self, existing_solver_node):
|
|
# check that it's a solver node
|
|
if cmds.nodeType(existing_solver_node) == 'UE4RBFSolverNode':
|
|
self.solver = existing_solver_node
|
|
|
|
def create_pose_driver_system(self, name, input_xform, driven_xforms):
|
|
solver = self.create_UE4RBFSolverNode(name, input_xform, driven_xforms)
|
|
|
|
def create_UE4RBFSolverNode(self, name, input_xform, driven_xforms):
|
|
try:
|
|
cmds.undoInfo(openChunk=True, undoName='create pose interpolator')
|
|
# create the interpolator (will be your API)
|
|
self.solver = cmds.createNode('UE4RBFSolverNode', name=name + '_UE4RBFSolver')
|
|
self.name = self.solver
|
|
|
|
# Set the default values for the solver node
|
|
cmds.setAttr("{}.radius".format(self.solver), 50)
|
|
|
|
# connect the world matrix from the driving xform to the interp
|
|
cmds.connectAttr(input_xform + '.matrix', self.solver + '.inputs[0]')
|
|
|
|
# connect the default pose into the first target (Nate said to do this)
|
|
default_mx_node = self.create_matrix_node(input_xform, input_xform + '_base_pose', invertJointOrient=True)
|
|
cmds.connectAttr(default_mx_node + '.outputLocalMatrix', self.solver + '.targets[0].targetValues[0]')
|
|
|
|
# connect the driver to the interp for the property to query
|
|
self.msgConnect(input_xform + '.ue4_rbf_solver', self.solver + '.driver')
|
|
|
|
# store the driven nodes for the property query
|
|
for node in driven_xforms:
|
|
self.msgConnect(self.solver + '.driven_transforms', node + '.ue4_rbf_solver')
|
|
|
|
for node in driven_xforms:
|
|
|
|
blender_node_name = node + '_ue4PoseBlender'
|
|
if not cmds.objExists(blender_node_name):
|
|
print("node: " + blender_node_name + " didn't exist!")
|
|
|
|
# make matrix node
|
|
mx_node = self.create_matrix_node(node, node + '_base_pose', invertJointOrient=True)
|
|
print("mx node")
|
|
print(mx_node)
|
|
blender_node = cmds.createNode('UE4PoseBlenderNode', name=blender_node_name)
|
|
|
|
# connect the base pose, it doesn't make much sense copying the same mx into
|
|
# all three slots on this node (base_pose, in_matrix, pose) -but I was told to by the author
|
|
cmds.connectAttr(mx_node + '.outputLocalMatrix', blender_node + '.basePose')
|
|
cmds.connectAttr(mx_node + '.outputLocalMatrix', blender_node + '.inMatrix')
|
|
|
|
self.msgConnect(node + '.blenderNode', blender_node + '.drivenTransform')
|
|
cmds.connectAttr(mx_node + '.outputLocalMatrix', blender_node + '.poses[0]')
|
|
|
|
# connect ourtput, remember that this bone could already be driven
|
|
next_pose_weight_idx = cmds.getAttr(blender_node + '.weights', size=True)
|
|
cmds.connectAttr(
|
|
self.solver + '.outputs[0]',
|
|
blender_node + '.weights[' + str(next_pose_weight_idx) + ']'
|
|
)
|
|
|
|
# wire up the pose representation
|
|
self.msgConnect(self.solver + '.stored_pose_base_pose', mx_node + '.ue4_rbf_solver')
|
|
|
|
|
|
else:
|
|
print("node: " + blender_node_name + " did exist!")
|
|
blender_node = node + '_ue4PoseBlender'
|
|
# TODO: check that this node is valid and has a base pose connected
|
|
|
|
self.msgConnect(self.solver + '.pose_blenders', blender_node + '.ue4_rbf_solver')
|
|
self.msgConnect(self.solver + '.stored_pose_base_pose', default_mx_node + '.ue4_rbf_solver')
|
|
|
|
|
|
except Exception as e:
|
|
print(traceback.format_exc())
|
|
return False
|
|
finally:
|
|
cmds.undoInfo(closeChunk=True)
|
|
|
|
def add_pose(self, pose_name, debug_mode=0):
|
|
'''
|
|
Because the user has selected what xforms are driven on creation of the solver, this function does
|
|
not need to know anything but what we will call the pose, everything else is derived from metadata.
|
|
'''
|
|
try:
|
|
cmds.undoInfo(openChunk=True, undoName='create pose')
|
|
self.is_driving(False)
|
|
if pose_name:
|
|
if debug_mode:
|
|
# I was storing transforms as locators for testing
|
|
pose_loc = cmds.spaceLocator(name=pose_name)[0]
|
|
cmds.delete(cmds.parentConstraint(self.driving_transform, pose_loc))
|
|
|
|
# wire up the world mx of the driver in it's current position to the solver
|
|
mx_node_driver = self.create_matrix_node(
|
|
self.driving_transform,
|
|
self.driving_transform + '_' + pose_name + '_pose',
|
|
invertJointOrient=True
|
|
)
|
|
|
|
next_target_index = cmds.getAttr(self.solver + '.targets', size=True)
|
|
cmds.connectAttr(
|
|
mx_node_driver + '.outputLocalMatrix',
|
|
self.solver + '.targets[' + str(next_target_index) + '].targetValues[0]'
|
|
)
|
|
|
|
# get the next output weight
|
|
next_output = cmds.getAttr(self.solver + '.outputs', size=True)
|
|
|
|
# hook that up to existing
|
|
for node in self.driven_transforms:
|
|
# make matrix node
|
|
mx_node = self.create_matrix_node(node, pose_name + '_' + node + '_mx_pose', invertJointOrient=True)
|
|
|
|
blender_node = cmds.listConnections(node + '.blenderNode')[0]
|
|
|
|
next_pose_idx = cmds.getAttr(blender_node + '.poses', size=True)
|
|
|
|
cmds.connectAttr(
|
|
mx_node + '.outputLocalMatrix',
|
|
blender_node + '.poses[' + str(next_pose_idx) + ']'
|
|
)
|
|
# connect to weight
|
|
next_pose_weight_idx = cmds.getAttr(blender_node + '.weights', size=True)
|
|
cmds.connectAttr(
|
|
self.solver + '.outputs[' + str(next_output) + ']',
|
|
blender_node + '.weights[' + str(next_pose_weight_idx) + ']'
|
|
)
|
|
|
|
# wire up the pose representation
|
|
self.msgConnect(self.solver + '.stored_pose_' + pose_name, mx_node + '.ue4_rbf_solver')
|
|
driving_xform_mx = \
|
|
cmds.listConnections(self.solver + '.targets[' + str(next_target_index) + '].targetValues[0]')[
|
|
0]
|
|
self.msgConnect(self.solver + '.stored_pose_' + pose_name, driving_xform_mx + '.ue4_rbf_solver')
|
|
self.is_driving(True)
|
|
except Exception as e:
|
|
print(traceback.format_exc())
|
|
finally:
|
|
cmds.undoInfo(closeChunk=True)
|
|
|
|
def update_pose(self, pose_name):
|
|
try:
|
|
cmds.undoInfo(openChunk=True, undoName='enable pose ' + pose_name)
|
|
if pose_name in self.pose_dict:
|
|
for mx_node in self.pose_dict[pose_name]:
|
|
driven_xform = cmds.listConnections(mx_node + '.driven_transform')[0]
|
|
# local_mx = cmds.xform(driven_xform, m=1, q=1)
|
|
local_mx = self.get_local_matrix(driven_xform, invertJointOrient=True)
|
|
cmds.setAttr(mx_node + '.outputLocalMatrix', local_mx, type='matrix')
|
|
except Exception as e:
|
|
print(traceback.format_exc())
|
|
finally:
|
|
cmds.undoInfo(closeChunk=True)
|
|
|
|
def delete_pose(self, pose_name):
|
|
try:
|
|
cmds.undoInfo(openChunk=True, undoName='enable pose ' + pose_name)
|
|
if pose_name in self.pose_dict:
|
|
for mx_node in self.pose_dict[pose_name]:
|
|
|
|
print("mx node")
|
|
print(mx_node)
|
|
# this will also remove the pose array instance which keeps the pose computing
|
|
conn = cmds.listConnections(mx_node + ".outputLocalMatrix", d=1, s=0, p=1)
|
|
print(conn)
|
|
if conn:
|
|
index = int(conn[0].split("[")[-1].split("]")[0])
|
|
# this means the conn is a poseBlender
|
|
if ".poses[" in conn[0]:
|
|
cmds.removeMultiInstance(conn[0], b=1)
|
|
poseBlender = conn[0].split(".")[0]
|
|
cmds.removeMultiInstance(poseBlender + ".weights[" + str(index) + "]", b=1)
|
|
print("pose blender:")
|
|
print(poseBlender)
|
|
|
|
print("solver")
|
|
print(self.solver)
|
|
cmds.removeMultiInstance(self.solver + ".outputs[" + str(index) + "]", b=1)
|
|
cmds.removeMultiInstance(self.solver + ".targets[" + str(index) + "]", b=1)
|
|
print("index:")
|
|
print(index)
|
|
|
|
# driven = cmds.listConnections(mx_node + ".driven_transform")[0]
|
|
# blender_node_name = driven + '_ue4PoseBlender'
|
|
# cmds.removeMultiInstance(blender_node_name + ".weights[" + str(index) + "]", b=1)
|
|
cmds.delete(mx_node)
|
|
cmds.deleteAttr(self.name + '.stored_pose_' + pose_name)
|
|
except Exception as e:
|
|
print(traceback.format_exc())
|
|
finally:
|
|
cmds.undoInfo(closeChunk=True)
|
|
|
|
def assume_pose(self, pose_name):
|
|
try:
|
|
cmds.undoInfo(openChunk=True, undoName='enable pose ' + pose_name)
|
|
if pose_name in self.pose_dict:
|
|
for mx in self.pose_dict[pose_name]:
|
|
driven_xform = cmds.listConnections(mx + '.driven_transform')[0]
|
|
mx = cmds.getAttr(mx + '.outputLocalMatrix')
|
|
"""
|
|
matrix = api.MMatrix(mx)
|
|
matrixFn = api.MTransformationMatrix(matrix)
|
|
euler = matrixFn.rotation()
|
|
rotX = math.degrees(euler[0])
|
|
rotY = math.degrees(euler[1])
|
|
rotZ = math.degrees(euler[2])
|
|
translation = matrixFn.translation(api.MSpace.kWorld)
|
|
scale = matrixFn.scale(api.MSpace.kWorld)
|
|
|
|
try:
|
|
cmds.setAttr(driven_xform + ".translate", translation[0], translation[1], translation[2])
|
|
except:
|
|
pass
|
|
try:
|
|
cmds.setAttr(driven_xform + ".rotate", rotX, rotY, rotZ)
|
|
except:
|
|
pass
|
|
try:
|
|
cmds.setAttr(driven_xform + ".scale", scale[0], scale[1], scale[2])
|
|
except:
|
|
pass
|
|
"""
|
|
# replace this to take joint orient into account
|
|
cmds.xform(driven_xform, m=mx)
|
|
|
|
|
|
except Exception as e:
|
|
print(traceback.format_exc())
|
|
finally:
|
|
cmds.undoInfo(closeChunk=True)
|
|
|
|
def bake_poses_to_timeline(self, start_frame=0, suppress=False, anim_layer=None):
|
|
"""
|
|
Bakes the poses to the timeline and sets the time range to the given animation.
|
|
:param start_frame: start frame of he baked animation
|
|
:param suppress: A bool to suppress the warning dialogue. Meant to be used outside of the UI
|
|
:anim_layer: if given the animations will be baked on that layer.If the layer doesnt exist we will create one.
|
|
"""
|
|
bake_bool = True
|
|
pose_list = []
|
|
if not anim_layer:
|
|
anim_layer = "BaseAnimation"
|
|
if not cmds.animLayer(anim_layer, query=True, exists=True):
|
|
cmds.animLayer(anim_layer)
|
|
try:
|
|
cmds.autoKeyframe(e=1, st=0)
|
|
if not suppress:
|
|
ret = QtWidgets.QMessageBox.warning(
|
|
None, "WARNING: DESTRUCTIVE FUNCTION",
|
|
"This will bake the poses to the timeline, change your time range, and delete inputs on driving and driven transforms.\n" + \
|
|
"Do you want this to happen?",
|
|
QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel,
|
|
QtWidgets.QMessageBox.Cancel
|
|
)
|
|
if ret == QtWidgets.QMessageBox.StandardButton.Ok:
|
|
bake_bool = True
|
|
cmds.undoInfo(openChunk=True, undoName='Bake poses to timeline')
|
|
else:
|
|
bake_bool = False
|
|
|
|
if bake_bool:
|
|
# begin printing UE4 pose list
|
|
# print('<- UE4 Pose List ---------------------->')
|
|
i = start_frame
|
|
for p in self.pose_dict:
|
|
driven_xforms = []
|
|
for mx_node in self.pose_dict[p]:
|
|
driven_xforms.extend(cmds.listConnections(mx_node + '.driven_transform'))
|
|
# let's key it on the previous and next frames before we pose it
|
|
cmds.select(driven_xforms)
|
|
cmds.animLayer(anim_layer, addSelectedObjects=True, e=True)
|
|
cmds.setKeyframe(driven_xforms, t=[(i - 1), (i + 1)], animLayer=anim_layer)
|
|
|
|
# assume the pose
|
|
self.assume_pose(p)
|
|
|
|
cmds.setKeyframe(driven_xforms, t=[i], animLayer=anim_layer)
|
|
|
|
# print(out to UE4 pose list)
|
|
print(p)
|
|
pose_list.append(p)
|
|
|
|
# increment to next keyframe
|
|
i += 1
|
|
# print('<- UE4 Pose List ---------------------->')
|
|
|
|
# set the range to the number of keyframes
|
|
cmds.playbackOptions(minTime=0, maxTime=i, animationStartTime=0, animationEndTime=i - 1)
|
|
cmds.dgdirty(a=1)
|
|
return pose_list
|
|
cmds.dgdirty(a=1)
|
|
except Exception as e:
|
|
print(traceback.format_exc())
|
|
finally:
|
|
cmds.undoInfo(closeChunk=True)
|
|
|
|
def is_driving(self, state):
|
|
try:
|
|
cmds.undoInfo(openChunk=True, undoName='Change driving state')
|
|
# state is True or False
|
|
if state:
|
|
for node in self.pose_blenders:
|
|
# let's get the driven transform
|
|
driven_transform = cmds.listConnections(node + '.drivenTransform')[0]
|
|
# we have to decompose the out matrix in order to connect it to the driven transform
|
|
mx_decompose = cmds.createNode('decomposeMatrix', name=node + '_mx_decompose')
|
|
# we hookup the output mx of the pose driver to the input of the decompose
|
|
cmds.connectAttr(node + '.outMatrix', mx_decompose + '.inputMatrix')
|
|
# and the TRS out of the decompose back into the original driven xform
|
|
cmds.connectAttr(mx_decompose + '.outputTranslate', driven_transform + '.translate', f=True)
|
|
cmds.connectAttr(mx_decompose + '.outputRotate', driven_transform + '.rotate', f=True)
|
|
cmds.connectAttr(mx_decompose + '.outputScale', driven_transform + '.scale', f=True)
|
|
|
|
else:
|
|
for node in self.pose_blenders:
|
|
print("debugging:")
|
|
print(node)
|
|
mx_decompose_node = cmds.listConnections(node + '.outMatrix')
|
|
print(mx_decompose_node)
|
|
while (mx_decompose_node):
|
|
self.break_all_connections(mx_decompose_node[0])
|
|
cmds.delete(mx_decompose_node[0])
|
|
mx_decompose_node = cmds.listConnections(node + '.outMatrix')
|
|
except Exception as e:
|
|
print(traceback.format_exc())
|
|
finally:
|
|
cmds.undoInfo(closeChunk=True)
|
|
|
|
def delete(self):
|
|
"""function that deletes the node"""
|
|
|
|
for mx_nodes in self.pose_dict.values():
|
|
for mx_node in mx_nodes:
|
|
cmds.delete(mx_node)
|
|
cmds.delete(self.pose_blenders)
|
|
cmds.delete(self.solver)
|
|
|
|
def add_driven(self, driven):
|
|
|
|
try:
|
|
cmds.undoInfo(openChunk=True, undoName='add driven')
|
|
|
|
self.msgConnect(self.solver + '.driven_transforms', driven + '.ue4_rbf_solver')
|
|
blender_node_name = driven + '_ue4PoseBlender'
|
|
mx_node = self.create_matrix_node(driven, driven + '_base_pose', invertJointOrient=True)
|
|
blender_node = cmds.createNode('UE4PoseBlenderNode', name=blender_node_name)
|
|
|
|
cmds.connectAttr(mx_node + '.outputLocalMatrix', blender_node + '.basePose')
|
|
cmds.connectAttr(mx_node + '.outputLocalMatrix', blender_node + '.inMatrix')
|
|
|
|
self.msgConnect(driven + '.blenderNode', blender_node + '.drivenTransform')
|
|
cmds.connectAttr(mx_node + '.outputLocalMatrix', blender_node + '.poses[0]')
|
|
|
|
cmds.connectAttr(self.solver + '.outputs[0]', blender_node + '.weights[0]')
|
|
|
|
self.msgConnect(self.solver + '.stored_pose_base_pose', mx_node + '.ue4_rbf_solver')
|
|
self.msgConnect(self.solver + '.pose_blenders', blender_node + '.ue4_rbf_solver')
|
|
|
|
output_size = cmds.getAttr(self.solver + '.outputs', size=True)
|
|
for i in range(output_size):
|
|
cmds.connectAttr(
|
|
self.solver + ".outputs[" + str(i) + "]", blender_node + ".weights[" + str(i) + "]",
|
|
f=1
|
|
)
|
|
|
|
for pose_name in self.pose_dict.keys():
|
|
other_mx_node = self.pose_dict[pose_name][0]
|
|
conns = cmds.listConnections(other_mx_node + ".outputLocalMatrix", d=1, s=0, p=1)
|
|
index = None
|
|
for conn in conns:
|
|
if "poses" in conn and "PoseBlender" in conn:
|
|
index = int(conn.split("[")[-1].split("]")[0])
|
|
|
|
mx_node = self.create_matrix_node(driven, pose_name + '_' + driven + '_mx_pose', invertJointOrient=True)
|
|
cmds.connectAttr(mx_node + '.outputLocalMatrix', blender_node + '.poses[' + str(index) + ']', f=1)
|
|
|
|
self.msgConnect(self.solver + '.stored_pose_' + pose_name, mx_node + '.ue4_rbf_solver')
|
|
|
|
|
|
except Exception as e:
|
|
print(traceback.format_exc())
|
|
return False
|
|
finally:
|
|
cmds.undoInfo(closeChunk=True)
|
|
|
|
def copy_driven_trs(self):
|
|
"""copies the driven TRS so we can paste it across other poses"""
|
|
|
|
for driven in self.driven_transforms:
|
|
translate = cmds.getAttr(driven + ".translate")[0]
|
|
rotate = cmds.getAttr(driven + ".rotate")[0]
|
|
scale = cmds.getAttr(driven + ".scale")[0]
|
|
self.copied_trs_map[driven] = {"translate": translate, "rotate": rotate, "scale": scale}
|
|
|
|
print("Copied driven TRS successfully.")
|
|
|
|
def paste_driven_trs(self, mult=1.0):
|
|
"""pastes the driven trs and multiplies it"""
|
|
|
|
for driven, data in self.copied_trs_map.items():
|
|
translate = data['translate']
|
|
rotate = data['rotate']
|
|
scale = list(data['scale'])
|
|
|
|
scale[0] = ((scale[0] - 1.0) * mult) + 1.0
|
|
scale[1] = ((scale[1] - 1.0) * mult) + 1.0
|
|
scale[2] = ((scale[2] - 1.0) * mult) + 1.0
|
|
|
|
try:
|
|
cmds.setAttr(driven + ".translate", translate[0] * mult, translate[1] * mult, translate[2] * mult)
|
|
except:
|
|
traceback.print_exc()
|
|
try:
|
|
cmds.setAttr(driven + ".rotate", rotate[0] * mult, rotate[1] * mult, rotate[2] * mult)
|
|
except:
|
|
traceback.print_exc()
|
|
try:
|
|
cmds.setAttr(driven + ".scale", scale[0], scale[1], scale[2])
|
|
except:
|
|
traceback.print_exc()
|
|
|
|
print("Pasted driven TRS successfully.")
|
|
|
|
def zero_base_pose(self):
|
|
"""zero's out the base pose"""
|
|
self.assume_pose("base_pose")
|
|
self.is_driving(False)
|
|
|
|
for d in self.driven_transforms:
|
|
cmds.setAttr(d + ".translate", 0.0, 0.0, 0.0)
|
|
cmds.setAttr(d + ".rotate", 0.0, 0.0, 0.0)
|
|
cmds.setAttr(d + ".scale", 1.0, 1.0, 1.0)
|
|
|
|
self.update_pose("base_pose")
|
|
self.is_driving(True)
|
|
|
|
## Class properties
|
|
#################################################################################
|
|
@property
|
|
def is_enabled(self):
|
|
|
|
found_conns = False
|
|
for node in self.pose_blenders:
|
|
mx_decompose_node = cmds.listConnections(node + '.outMatrix')
|
|
if mx_decompose_node:
|
|
found_conns = True
|
|
return found_conns
|
|
|
|
@property
|
|
def driven_transforms(self):
|
|
"""gets the driven transforms"""
|
|
|
|
if cmds.attributeQuery("driven_transforms", n=self.solver, ex=1):
|
|
xforms = cmds.listConnections(self.solver + '.driven_transforms')
|
|
if xforms:
|
|
return xforms
|
|
|
|
return []
|
|
|
|
@driven_transforms.setter
|
|
def driven_transforms(self, xforms):
|
|
pass
|
|
|
|
@property
|
|
def driving_transform(self):
|
|
"""gets the driving transform"""
|
|
|
|
if cmds.attributeQuery("driver", n=self.solver, ex=1):
|
|
transform = cmds.listConnections(self.solver + '.driver')[0]
|
|
if transform:
|
|
return transform
|
|
return []
|
|
|
|
@driving_transform.setter
|
|
def driving_transform(self, transform):
|
|
self.msgConnect(transform + '.ue4_rbf_solver', self.solver + '.driver')
|
|
|
|
@property
|
|
def base_dagPose(self):
|
|
xforms = cmds.listConnections(self.solver + '.driven_transforms')
|
|
if xforms:
|
|
return xforms
|
|
else:
|
|
return []
|
|
|
|
@base_dagPose.setter
|
|
def base_dagPose(self, dp):
|
|
pass
|
|
|
|
@property
|
|
def pose_blenders(self):
|
|
|
|
if not cmds.attributeQuery("pose_blenders", ex=1, n=self.solver):
|
|
return []
|
|
|
|
xforms = cmds.listConnections(self.solver + '.pose_blenders')
|
|
if xforms:
|
|
return xforms
|
|
else:
|
|
return []
|
|
|
|
@pose_blenders.setter
|
|
def poses(self, pose_list):
|
|
# nuke poses connections
|
|
for item in pose_list:
|
|
self.msgConnect(self.solver + '.pose_blenders', item + '.ue4_rbf_solver')
|
|
|
|
@property
|
|
def pose_dict(self):
|
|
pose_dict = {}
|
|
attrs = cmds.listAttr(self.solver, st='stored_pose_*')
|
|
if attrs:
|
|
for attr in attrs:
|
|
pose_name = attr.replace('stored_pose_', '')
|
|
pose_matrices = cmds.listConnections(self.solver + '.' + attr)
|
|
pose_dict[pose_name] = pose_matrices
|
|
return pose_dict
|
|
|
|
|
|
def zero_all_base_poses():
|
|
"""zero's all driver bases poses"""
|
|
|
|
nodes = cmds.ls(type='UE4RBFSolverNode')
|
|
for node in nodes:
|
|
driver_obj = UE4PoseDriver(existing_interpolator=node)
|
|
driver_obj.zero_base_pose()
|
|
|
|
|
|
def export_drivers(drivers, file_path):
|
|
output_data = {"drivers": {}}
|
|
for driver in drivers:
|
|
|
|
driver_data = {}
|
|
driver_obj = UE4PoseDriver(existing_interpolator=driver)
|
|
|
|
solver = driver_obj.solver
|
|
driver_name = driver_obj.name.replace("_UE4RBFSolver", "")
|
|
driver_data['name'] = driver_name
|
|
driver_data['solver_settings'] = {}
|
|
solver_attrs = cmds.listAttr(solver, k=1)
|
|
for solver_attr in solver_attrs:
|
|
try:
|
|
value = cmds.getAttr(solver + "." + solver_attr)
|
|
except:
|
|
continue
|
|
driver_data['solver_settings'][solver_attr] = value
|
|
|
|
driver_data['driver_transform'] = driver_obj.driving_transform
|
|
driver_data['driven_transforms'] = driver_obj.driven_transforms
|
|
|
|
"""
|
|
driver_data['driven_off_transforms'] = []
|
|
|
|
for transform in driver_obj.driven_transforms:
|
|
parent = cmds.listRelatives(transform, p=1)
|
|
if parent:
|
|
"""
|
|
|
|
driver_data['pose_data'] = {}
|
|
for pose, mx_list in driver_obj.pose_dict.items():
|
|
pose_data = {
|
|
"pose": pose, "local_matrix_map": {}, "world_matrix_map": {}, "driven_trs": {},
|
|
"driving_trs": {}
|
|
}
|
|
driver_obj.assume_pose(pose)
|
|
|
|
driving_transforn = driver_obj.driving_transform
|
|
translate = cmds.getAttr(driving_transforn + ".translate")[0]
|
|
rotate = cmds.getAttr(driving_transforn + ".rotate")[0]
|
|
scale = cmds.getAttr(driving_transforn + ".scale")[0]
|
|
pose_data["driving_trs"][driving_transforn] = {"translate": translate, "rotate": rotate, "scale": scale}
|
|
|
|
for driven in driver_obj.driven_transforms:
|
|
translate = cmds.getAttr(driven + ".translate")[0]
|
|
rotate = cmds.getAttr(driven + ".rotate")[0]
|
|
scale = cmds.getAttr(driven + ".scale")[0]
|
|
pose_data["driven_trs"][driven] = {"translate": translate, "rotate": rotate, "scale": scale}
|
|
|
|
for mx in mx_list:
|
|
local_matrix = cmds.getAttr(mx + ".outputLocalMatrix")
|
|
world_matrix = cmds.getAttr(mx + ".outputWorldMatrix")
|
|
pose_data['local_matrix_map'][mx] = local_matrix
|
|
pose_data['world_matrix_map'][mx] = world_matrix
|
|
driver_data['pose_data'][pose] = pose_data
|
|
output_data['drivers'][driver_name] = driver_data
|
|
|
|
# zero the pose
|
|
driver_obj.assume_pose("base_pose")
|
|
|
|
with open(file_path, 'w') as outfile:
|
|
|
|
json.dump(output_data, outfile, sort_keys=1, indent=4, separators=(",", ":"))
|
|
|
|
print("Successfuly export pose data to : " + file_path)
|
|
|
|
|
|
def import_drivers(file_path, driverFilter=None):
|
|
"""imports the drivers"""
|
|
|
|
with open(file_path) as json_file:
|
|
data = json.load(json_file)
|
|
|
|
for driver, driver_data in data['drivers'].items():
|
|
|
|
if driverFilter:
|
|
if not driver in driverFilter:
|
|
continue
|
|
|
|
print("driver:")
|
|
print(driver)
|
|
|
|
driving_transform = driver_data['driver_transform']
|
|
driven_transforms = driver_data['driven_transforms']
|
|
|
|
print("driving: ")
|
|
print(driving_transform)
|
|
print("driven: ")
|
|
print(driven_transforms)
|
|
|
|
# if the driver exists, then just update it
|
|
if (cmds.objExists(driver + "_UE4RBFSolver")):
|
|
pose_driver_obj = UE4PoseDriver(existing_interpolator=driver + "_UE4RBFSolver")
|
|
else:
|
|
print("Solver NOT found!")
|
|
pose_driver_obj = UE4PoseDriver()
|
|
pose_driver_obj.create_pose_driver_system(driver, driving_transform, driven_transforms)
|
|
|
|
# pose_driver_obj.is_driving(True)#delete this one
|
|
pose_driver_obj.is_driving(False)
|
|
|
|
for attr, value in driver_data['solver_settings'].items():
|
|
cmds.setAttr(pose_driver_obj.solver + "." + attr, value)
|
|
|
|
for pose, pose_data in driver_data['pose_data'].items():
|
|
|
|
if pose == "base_pose":
|
|
continue
|
|
|
|
if not pose in pose_driver_obj.pose_dict.keys():
|
|
pose_driver_obj.add_pose(pose.replace("_pose", ""))
|
|
|
|
local_matrix_map = pose_data['local_matrix_map']
|
|
world_matrix_map = pose_data['local_matrix_map']
|
|
for mx_node, local_matrix in local_matrix_map.items():
|
|
if mx_node.startswith(driving_transform):
|
|
translate = cmds.getAttr(driving_transform + ".translate")[0]
|
|
local_matrix[12] = translate[0]
|
|
local_matrix[13] = translate[1]
|
|
local_matrix[14] = translate[2]
|
|
cmds.setAttr(mx_node + ".outputLocalMatrix", local_matrix, type="matrix")
|
|
|
|
for mx_node, world_matrix in world_matrix_map.items():
|
|
if mx_node.startswith(driving_transform):
|
|
translate = cmds.getAttr(driving_transform + ".translate")[0]
|
|
world_matrix[12] = translate[0]
|
|
world_matrix[13] = translate[1]
|
|
world_matrix[14] = translate[2]
|
|
cmds.setAttr(mx_node + ".outputWorldMatrix", world_matrix, type="matrix")
|
|
|
|
pose_driver_obj.is_driving(True)
|
|
|
|
|
|
def mirror_all_drivers():
|
|
nodes = cmds.ls(type='UE4RBFSolverNode')
|
|
for node in nodes:
|
|
if "_l_" not in node:
|
|
continue
|
|
|
|
mirror_pose_driver(node)
|
|
print("-----------------------------------------------------------")
|
|
print("--------successfuly mirrored node: " + node + "-------")
|
|
print("-------------------------------------------------------------")
|
|
|
|
|
|
def mirror_pose_driver(source_pose_driver, pose=None):
|
|
source_syntax = "_l_"
|
|
target_syntax = "_r_"
|
|
target_pose_driver = None
|
|
if "_r_" in source_pose_driver:
|
|
source_syntax = "_r_"
|
|
target_syntax = "_l_"
|
|
|
|
target_pose_driver = source_pose_driver.replace(source_syntax, target_syntax)
|
|
source_driver_obj = UE4PoseDriver(existing_interpolator=source_pose_driver)
|
|
poses = source_driver_obj.pose_dict.keys()
|
|
driven = source_driver_obj.driven_transforms
|
|
driving = source_driver_obj.driving_transform
|
|
target_driving = driving.replace(source_syntax, target_syntax)
|
|
target_driver_obj = None
|
|
if not cmds.objExists(target_pose_driver):
|
|
target_driven = []
|
|
for d in driven:
|
|
target_driven.append(d.replace(source_syntax, target_syntax))
|
|
|
|
target_driver_obj = UE4PoseDriver()
|
|
target_driver_obj.create_pose_driver_system(
|
|
target_pose_driver.replace("_UE4RBFSolver", ""), target_driving,
|
|
target_driven
|
|
)
|
|
else:
|
|
target_driver_obj = UE4PoseDriver(existing_interpolator=target_pose_driver)
|
|
|
|
# make sure the solver settings match
|
|
solver_attrs = cmds.listAttr(source_driver_obj.solver, k=1)
|
|
for solver_attr in solver_attrs:
|
|
try:
|
|
value = cmds.getAttr(source_driver_obj.solver + "." + solver_attr)
|
|
except:
|
|
continue
|
|
cmds.setAttr(target_driver_obj.solver + "." + solver_attr, value)
|
|
|
|
for p in poses:
|
|
|
|
if pose and not pose == p:
|
|
continue
|
|
|
|
# don't mirror the base pose
|
|
if "base_pose" in p:
|
|
continue
|
|
|
|
target_pose = p.replace(source_syntax, target_syntax)
|
|
source_driver_obj.assume_pose(p)
|
|
target_driver_obj.is_driving(False)
|
|
|
|
# if the pose doesn't exist, lets add it
|
|
if target_pose not in target_driver_obj.pose_dict.keys():
|
|
print("didn't find pose: " + target_pose)
|
|
# mirror_transforms([driving], rotation=True, position=False)
|
|
rotate = cmds.getAttr(driving + ".rotate")[0]
|
|
cmds.setAttr(target_driving + ".rotate", rotate[0], rotate[1], rotate[2])
|
|
mirror_transforms(driven)
|
|
target_driver_obj.add_pose(target_pose)
|
|
else:
|
|
target_driver_obj.assume_pose(target_pose)
|
|
mirror_transforms(driven)
|
|
target_driver_obj.update_pose(target_pose)
|
|
|
|
target_driver_obj.is_driving(True)
|
|
|
|
print("Successfully mirrored pose: " + p + " to " + target_pose)
|
|
|
|
# if no pose was specified, then lets default back to base at the end
|
|
if not pose:
|
|
source_driver_obj.assume_pose("base_pose")
|
|
target_driver_obj.assume_pose("base_pose")
|
|
|
|
|
|
def mirror_transforms(transforms, position=True, rotation=True, scale=True):
|
|
"""mirrors the transform"""
|
|
|
|
for transform in transforms:
|
|
source_transform = transform
|
|
|
|
source_syntax = "_l_"
|
|
target_syntax = "_r_"
|
|
if "_r_" in source_transform:
|
|
source_syntax = "_r_"
|
|
target_syntax = "_l_"
|
|
|
|
target_transform = source_transform.replace(source_syntax, target_syntax)
|
|
source_parent_mat = api.MMatrix()
|
|
values = cmds.getAttr(source_transform + ".parentMatrix")
|
|
index = 0
|
|
for i in range(4):
|
|
for j in range(4):
|
|
source_parent_mat.setElement(i, j, values[index])
|
|
index += 1
|
|
target_parent_mat = api.MMatrix()
|
|
values = cmds.getAttr(target_transform + ".parentMatrix")
|
|
index = 0
|
|
for i in range(4):
|
|
for j in range(4):
|
|
target_parent_mat.setElement(i, j, values[index])
|
|
index += 1
|
|
|
|
if position:
|
|
pos = cmds.getAttr(source_transform + ".translate")[0]
|
|
pos = api.MVector(pos[0], pos[1], pos[2])
|
|
pos = _mirror_position(parent_matrix=source_parent_mat, m_parent_matrix=target_parent_mat, pos=pos)
|
|
try:
|
|
cmds.setAttr(target_transform + ".translate", pos[0], pos[1], pos[2])
|
|
except:
|
|
pass
|
|
if rotation:
|
|
rot = cmds.getAttr(source_transform + ".rotate")[0]
|
|
rot = api.MVector(rot[0], rot[1], rot[2])
|
|
rot = _mirror_rotation(parent_matrix=source_parent_mat, m_parent_matrix=target_parent_mat, rot=rot)
|
|
try:
|
|
cmds.setAttr(target_transform + ".rotate", rot[0], rot[1], rot[2])
|
|
except:
|
|
pass
|
|
|
|
# scale we assume the axis correlate and mirror
|
|
if scale:
|
|
scale = cmds.getAttr(source_transform + ".scale")[0]
|
|
cmds.setAttr(target_transform + ".scale", scale[0], scale[1], scale[2])
|
|
|
|
|
|
def _mirror_position(parent_matrix, m_parent_matrix, pos):
|
|
"""this mirrors the position"""
|
|
|
|
driver_mat_fn = api.MTransformationMatrix(api.MMatrix.kIdentity)
|
|
driver_mat_fn.setTranslation(pos, api.MSpace.kWorld)
|
|
driver_mat = driver_mat_fn.asMatrix()
|
|
|
|
scale_matrix_fn = api.MTransformationMatrix(api.MMatrix.kIdentity)
|
|
scale_matrix_fn.setScale([-1.0, 1.0, 1.0], api.MSpace.kWorld)
|
|
scale_matrix = scale_matrix_fn.asMatrix()
|
|
|
|
pos_matrix = driver_mat * parent_matrix
|
|
pos_matrix = pos_matrix * scale_matrix
|
|
pos_matrix = pos_matrix * m_parent_matrix.inverse()
|
|
mat_fn = api.MTransformationMatrix(pos_matrix)
|
|
m_pos = mat_fn.translation(api.MSpace.kWorld)
|
|
|
|
return m_pos
|
|
|
|
|
|
def _mirror_rotation(parent_matrix, m_parent_matrix, rot):
|
|
"""this mirrors the rotation"""
|
|
|
|
driver_mat_fn = api.MTransformationMatrix(api.MMatrix.kIdentity)
|
|
|
|
# set the values to radians
|
|
rot[0] = math.radians(rot[0])
|
|
rot[1] = math.radians(rot[1])
|
|
rot[2] = math.radians(rot[2])
|
|
|
|
euler = api.MEulerRotation(rot[0], rot[1], rot[2])
|
|
|
|
driver_mat_fn.setRotation(euler)
|
|
driver_matrix = driver_mat_fn.asMatrix()
|
|
|
|
world_matrix = driver_matrix * parent_matrix
|
|
rot_matrix = parent_matrix.inverse() * world_matrix
|
|
rot_matrix_fn = api.MTransformationMatrix(rot_matrix)
|
|
rot = rot_matrix_fn.rotation(asQuaternion=True)
|
|
rot.x = rot.x * -1.0
|
|
rot.w = rot.w * -1.0
|
|
rot_matrix = rot.asMatrix()
|
|
final_rot_matrix = m_parent_matrix * rot_matrix * m_parent_matrix.inverse()
|
|
|
|
rot_matrix_fn = api.MTransformationMatrix(final_rot_matrix)
|
|
rot = rot_matrix_fn.rotation(asQuaternion=False)
|
|
m_rot = api.MVector()
|
|
m_rot[0] = math.degrees(rot[0])
|
|
m_rot[1] = math.degrees(rot[1])
|
|
m_rot[2] = math.degrees(rot[2])
|
|
|
|
return m_rot
|