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