# -= ml_puppet.py =- # __ by Morgan Loomis # ____ ___ / / http://morganloomis.com # / __ `__ \/ / Revision 26 # / / / / / / / 2023-04-22 # /_/ /_/ /_/_/ _________ # /_________/ # # ______________ # - -/__ License __/- - - - - - - - - - - - - - - - - - - - - - - - - - - - - # # Copyright 2018-2023 Morgan Loomis # # Permission is hereby granted, free of charge, to any person obtaining a copy of # this software and associated documentation files (the "Software"), to deal in # the Software without restriction, including without limitation the rights to use, # copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the # Software, and to permit persons to whom the Software is furnished to do so, # subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # # ___________________ # - -/__ Installation __/- - - - - - - - - - - - - - - - - - - - - - - - - - # # Copy this file into your maya scripts directory, for example: # C:/Documents and Settings/user/My Documents/maya/scripts/ml_puppet.py # # Run the tool in a python shell or shelf button by importing the module, # and then calling the primary function: # # import ml_puppet # ml_puppet.main() # # # __________________ # - -/__ Description __/- - - - - - - - - - - - - - - - - - - - - - - - - - - # # Support tools for puppets. # # ____________ # - -/__ Usage __/- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # # Launch the UI to see the options available. Press a button to make a selection # or run a command. Right click buttons to create a hotkey for that option. All # options are selection-sensitive, so for example if you have two puppets # referenced into a scene, and select any part of one of them and run # select_controls, it will select all the controls for that puppet only. With # nothing selected it will select all controls in the scene. For Fk/Ik switching, # select any part of the appendage you want to switch. So for an arm, you can select # the ik hand control, the fk shoulder, the pole vector, and either way it will # know to do the switch for that arm. # # # ___________________ # - -/__ Requirements __/- - - - - - - - - - - - - - - - - - - - - - - - - - # # This script requires the ml_utilities module, which can be downloaded here: # https://raw.githubusercontent.com/morganloomis/ml_tools/master/ml_utilities.py # # __________ # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /_ Enjoy! _/- - - __author__ = 'Morgan Loomis' __license__ = 'MIT' __category__ = 'None' __revision__ = 24 import maya.cmds as mc import maya.mel as mm from functools import partial import math, re, warnings, os try: import ml_utilities as utl utl.upToDateCheck(35) except ImportError: result = mc.confirmDialog( title='Module Not Found', message='This tool requires the ml_utilities module. Once downloaded you will need to restart Maya.', button=['Download Module','Cancel'], defaultButton='Cancel', cancelButton='Cancel', dismissString='Cancel' ) if result == 'Download Module': mc.showHelp('http://morganloomis.com/tool/ml_utilities/',absolute=True) ml_convertRotationOrder = None try: import ml_convertRotationOrder except ImportError: pass ml_resetChannels = None try: import ml_resetChannels except ImportError: pass ml_copyAnim = None try: import ml_copyAnim except ImportError: pass ml_match = None try: import ml_match except ImportError: pass ml_snap = None try: import ml_snap except ImportError: pass PUP_ID_PREFIX = 'pupID_' CONTROL_ATTR = PUP_ID_PREFIX+'control' PUPPET_ATTR = PUP_ID_PREFIX+'puppet' APPENDAGE_ATTR = 'puppeteer_appendage' JOINT_WORLD_MATRIX_ATTR = 'skinCluster_worldMatrix' JOINT_PREBIND_MATRIX_ATTR = 'skinCluster_preBindMatrix' SKELETON_NODE_ATTR = 'puppeteerDataSkeleton' def main(): defer_initPuppetContextMenu() filter_graphEditor() # ==================================================================================== # TAGS # ==================================================================================== def get_tagged_nodes_below(node, tag): nodes = [] allKids = mc.listRelatives(node, ad=True, pa=True) for kid in allKids: if mc.attributeQuery(tag, exists=True, node=kid): nodes.append(kid) return nodes def get_tagged_nodes_above(node, tag): nodes = [] antecedents = mc.listRelatives(node, ad=True, pa=True, parent=True) for each in antecedents: if mc.attributeQuery(tag, exists=True, node=each): nodes.append(each) return nodes def getTag(node, tag): ntAttr = PUP_ID_PREFIX+tag if mc.attributeQuery(ntAttr, exists=True, node=node): return mc.getAttr(node+'.'+ntAttr) return False def getNodeType(node): if mc.attributeQuery('asset_type', node=node, exists=True): return mc.getAttr(node+'.asset_type', asString=True) return getTag(node, 'nodeType') def get_tagged_nodes(tag, namespaceFromNodes=None): allNodes = [] #if something is selected, get it from within that namespace. if not namespaceFromNodes: namespaceFromNodes = mc.ls(sl=True) namespaces = [] if namespaceFromNodes: for each in namespaceFromNodes: namespaces.append(utl.getNamespace(each)) else: namespaces = ['*:',''] for ns in list(set(namespaces)): allNodes.extend(mc.ls(ns+'*.'+tag, o=True)) return allNodes # ==================================================================================== # CONTROLS # ==================================================================================== def is_control(node): return mc.attributeQuery(CONTROL_ATTR, exists=True, node=str(node)) def get_controls(nodes=None): return get_tagged_nodes(CONTROL_ATTR, namespaceFromNodes=nodes) def get_controls_below(node): return get_tagged_nodes_below(node, CONTROL_ATTR) # ==================================================================================== # PUPPET # ==================================================================================== def is_puppet(node): return mc.attributeQuery(PUPPET_ATTR, exists=True, node=str(node)) def get_puppets(node=None): nodes = mc.ls(sl=True) if not nodes: if not node: return get_tagged_nodes(PUPPET_ATTR) nodes = [node] return get_tagged_nodes(PUPPET_ATTR, namespaceFromNodes=nodes) def select_puppets(nodes=None, *args): pups = get_puppets(nodes) if pups: mc.select(pups) def select_controls(nodes=None, *args): ctrls = get_controls(nodes=nodes) if ctrls: mc.select(ctrls) def getWorldSpaceControls(nodes, *args): ctrls = get_controls(nodes=nodes) if not ctrls: return wsCtrls = [] for ctrl in ctrls: if getTag(ctrl, 'baseName') == 'root': wsCtrls.append(ctrl) continue ssData = getSpaceSwitchData(ctrl) if not ssData: continue for attr, value in list(ssData.items()): if value['currentValue'] == 'World': wsCtrls.append(ctrl) break return wsCtrls def selectWorldSpaceControls(nodes, *args): ctrls = getWorldSpaceControls(nodes) if ctrls: mc.select(ctrls) def select_keyed(nodes, *args): if nodes: mc.select(nodes) keySel = utl.KeySelection() if keySel.keyedInHierarchy(): mc.select(keySel.nodes, replace=True) else: mc.select(clear=True) def selectIkControls(nodes, *args): ctrls = get_controls(nodes) if not ctrls: return mc.select( [x for x in ctrls if getTag(x, 'descriptor') == 'ik']) def selectFkControls(nodes, *args): ctrls = get_controls(nodes) if not ctrls: return mc.select( [x for x in ctrls if getTag(x, 'descriptor') == 'fk']) def invertSelection(nodes, *args): ctrls = get_controls(nodes) if not ctrls: return sel = mc.ls(sl=True) if not sel: mc.select(ctrls) else: mc.select([x for x in ctrls if x not in sel]) # ==================================================================================== # APPENDAGES # ==================================================================================== def is_appendage(node): return mc.attributeQuery(APPENDAGE_ATTR, exists=True, node=str(node)) def get_appendage_controls(nodes=None): if not nodes: nodes = mc.ls(sl=True) appendages = get_appendages(nodes) if not appendages: return controls = [] for appendage in appendages: controlsBelow = get_tagged_nodes_below(appendage, CONTROL_ATTR) if controlsBelow: controls.extend(controlsBelow) return controls def select_appendage_controls(nodes=None, *args): ctrls = get_appendage_controls(nodes=nodes) if ctrls: mc.select(ctrls) def get_appendage(node): if mc.attributeQuery(APPENDAGE_ATTR, node=node, exists=True): return node parent = mc.listRelatives(node, parent=True, pa=True) if parent: return get_appendage(parent[0]) return None def get_appendages(nodes): return list(set([get_appendage(x) for x in nodes])) def select_appendage(nodes=None, *args): apps = get_appendages(nodes=nodes) if apps: mc.select(apps) def getDagMenuScript(): result = mm.eval('whatIs createSelectMenuItems') filename = None if 'found in: ' in result: filename = result.split('found in: ')[-1] if os.path.isfile(filename): return filename mm.eval('source dagMenuProc.mel') result = mm.eval('whatIs dagMenuProc') if not 'found in: ' in result: return None filename = result.split('found in: ')[-1] return filename def defer_initPuppetContextMenu(): cmd = 'import ml_puppet;ml_puppet.initPuppetContextMenu()' mc.evalDeferred(cmd) def initPuppetContextMenu(): #get the file name filename = getDagMenuScript() if not filename: raise RuntimeError('Unable to initialize Puppet Context Menu.') #globalize a proc which would otherwise fail mm.eval('''global proc optionalDagMenuProc( string $parent, string $item ) { string $object[] = `listRelatives -path -s $item`; string $shape = ""; int $gotVisible = 0; if( size($object) < 1 ) return; for( $i=0; $i 1: append = 's' mc.menuItem(label='Reset Control'+append, command=ml_resetChannels.resetPuppetControl) mc.menuItem(divider=True) mc.menuItem(label='Mirroring', subMenu=True) mc.menuItem(label='Mirror Pose', command=partial(mirrorPose,None)) mc.menuItem(label='Flip Pose', command=partial(flipPose,None)) mc.menuItem(label='Mirror Animation', command=partial(mirrorAnimation,None)) mc.menuItem(label='Flip Animation', command=partial(flipAnimation,None)) mc.setParent(parent, menu=True) #rotate order if ml_convertRotationOrder and mc.getAttr(node+'.rotateOrder', channelBox=True): roo = mc.getAttr(node+'.rotateOrder') #rotOrders = ('xyz') mc.menuItem(label='Convert Rotate Order...', command=partial(convertRotateOrderUI, nodes)) mc.menuItem(divider=True) #== from here out, populate by code nodes =============================== # codeNodes = [] # for a in [node, appendage]: # connected = mc.listConnections(a+'.message', source=False) or [] # for b in connected: # if mc.attributeQuery('puppeteer_data_code', node=b, exists=True): # codeNodes.append(b) # for codeNode in codeNodes: # #eval menu command # pass #== generic matching =============================== # includes fk/ik and space switching if not ml_match: return systems = ml_match.get_systems(nodes) if not systems: return driverAttrs = [] #need to combine systems that have the same driver names and ranges into one menu item menu = {} valueLists = [] for system in systems: driverNode, driverAttr = system.driver.split('.',1) if not mc.attributeQuery(driverAttr, node=driverNode, maxExists=True): print('Max value required for menu') continue min = mc.attributeQuery(driverAttr, node=driverNode, min=True)[0] max = mc.attributeQuery(driverAttr, node=driverNode, max=True)[0] valueList = mc.attributeQuery(driverAttr, node=driverNode, listEnum=True) if valueList: valueList = valueList[0].split(':') else: valueList = list(range(int(min), int(max+1))) #special case for fk/ik readability if len(valueList) == 2 and driverAttr in ['fk_ik', 'fkIk']: valueList = ['FK', 'IK'] menuData = tuple([driverAttr]+valueList) if not menuData in menu: menu[menuData] = [] menu[menuData].append(system) labels = [x[0] for x in menu.keys()] labelsUnique = len(labels) == len(set(labels)) for menuData in menu.keys(): #figure the label based on the number of unique things selected #two identical labels can have different value lists, so they need to prefixed. #prefix labels unless there's only 1 entry, or if all the labels are already unique label = menuData[0] systems = menu[menuData] if len(labels) > 1 and not labelsUnique: systemDrivers = [x.driver.split('.')[0] for x in systems] prefix = ', '.join(systemDrivers) if len(prefix) > 24: prefix = prefix[:20] + '. . .' label = prefix+label mc.menuItem(label=label, subMenu=True) first_system = systems[0] currentValue = None value = None values = [mc.getAttr(s.system+'.driver') for s in systems] allEqual = values.count(values[0]) == len(values) currentValue = mc.getAttr(first_system.system+'.driver') # ------------------------------------------- mc.menuItem(label='Switch Current', subMenu=True) for i, each in enumerate(menuData[1:]): if allEqual and i == currentValue: #need to take into account min continue mc.menuItem(label=each, command=partial(do_match_current, systems, i)) mc.setParent('..', menu=True) # ------------------------------------------- mc.menuItem(label='Switch Range', subMenu=True) for i, each in enumerate(menuData[1:]): mc.menuItem(label=each, command=partial(do_match_range, systems, i)) mc.setParent('..', menu=True) mc.setParent('..', menu=True) def do_match_current(systems, toValue, *args): '''Run fk/ik or space switch on current frame for one or more match systems.''' if not isinstance(systems, (list, tuple)): systems = [systems] for system in systems: system.match_current(toValue) def do_match_range(systems, toValue, *args): '''Run fk/ik or space switch over frame range for one or more match systems.''' if not isinstance(systems, (list, tuple)): systems = [systems] for system in systems: system.match_range(toValue) def match_fk_range(*args): match_selected(0, range=True) def match_ik_range(*args): match_selected(1, range=True) def match_fk_current(*args): match_selected(0, range=False) def match_ik_current(*args): match_selected(1, range=False) def match_selected(toValue, range=False): sel = mc.ls(sl=True) if not sel: return systems = ml_match.get_systems(sel) for system in systems: if range: system.match_range(toValue) else: system.match_current(toValue) def convertRotateOrderUI(nodes, *args): ''' wrapper ''' if ml_convertRotationOrder: if nodes: mc.select(nodes[-1]) ml_convertRotationOrder.ui() ml_convertRotationOrder.loadTips() def fk_ik_ui(): ''' User interface for arc tracer ''' with utl.MlUi('matching', 'Matching', width=400, height=180, info='''Select systems to match. Any control affected by the fk/ik system will do.''') as win: win.buttonWithPopup(label='Match Current FK', command=match_fk_current, annotation='Match to FK over range.', shelfLabel='fk', shelfIcon='ikEffector') win.buttonWithPopup(label='Match Current IK', command=match_ik_current, annotation='Match to IK over range.', shelfLabel='ik', shelfIcon='ikEffector') win.buttonWithPopup(label='Match Range FK', command=match_fk_range, annotation='Match to FK over range.', shelfLabel='fk', shelfIcon='ikEffector') win.buttonWithPopup(label='Match Range IK', command=match_ik_range, annotation='Match to IK over range.', shelfLabel='ik', shelfIcon='ikEffector') #================= def space_switch(nodes=None, toSpace=None, switchRange=False, bakeOnOnes=False): sel = mc.ls(sl=True) if switchRange: start, end = utl.frameRange() controls = [] attributes = [] locators = [] values = [] for node in nodes: ml_snap.setAttr_preserveTransform(node+'.space', toSpace) if switchRange: utl.matchBake(controls, locators, maintainOffset=True, bakeOnOnes=bakeOnOnes) for ctrl, attr, value in zip(controls, attributes, values): if mc.keyframe(ctrl+'.'+attr, query=True, name=True): mc.cutKey(ctrl+'.'+attr, time=(start,end)) mc.setKeyframe(ctrl+'.'+attr, value=value, time=(start,end)) else: mc.setAttr(ctrl+'.'+attr, value) utl.matchBake(locators, controls) else: for ctrl, attr, value, loc in zip(controls, attributes, values, locators): utl.setAnimValue(ctrl+'.'+attr, value) snap(ctrl, loc) mc.delete(locators) if sel: mc.select(sel) # __________________________________ # == POSE AND ANIM MIRRORING ======= def getMirrorName(node, a='Lf_', b='Rt_'): if a in node: return node.replace(a,b) elif b in node: return node.replace(b,a) return None def getMirrorMap(nodes=None): ''' Returns a map of all paired nodes within a puppet ''' puppets = get_puppets(nodes) puppets = mc.ls(puppets, long=True)[0] allNodes = mc.ls('*.mirrorIndex', o=True, long=True, recursive=True) found = {} pairs = {} for node in allNodes: for puppet in puppets: if not node.startswith(puppet): continue value = mc.getAttr('{}.mirrorIndex'.format(node)) if value in list(found.keys()): pairs[found[value]] = node pairs[node] = found[value] continue found[value] = node return pairs def getMirrorPairs(nodes): ''' Returns a dictionary of paired nodes. Keys are the input nodes, values are the mirrored nodes. ''' nodes = mc.ls(nodes, long=True) #mirrorMap = getMirrorMap(nodes) mirrorPairs = {} #for each in nodes: #if each in mirrorMap: #mirrorPairs[each] = mirrorMap[each] for each in nodes: mirror = getMirrorName(each) if mirror and mc.objExists(mirror): mirrorPairs[each] = mirror return mirrorPairs def getMirrorAxis(node): axis = [] if mc.attributeQuery('mirrorAxis', exists=True, node=node): mirrorAxis = mc.getAttr('{}.mirrorAxis'.format(node)) if mirrorAxis and not hasFlippedParent(node): axis = mirrorAxis.split(',') return axis def selectReplaceMirror(nodes, *args): pairs = getMirrorPairs(nodes) if not pairs: return mc.select(list(pairs.values())) def selectAddMirror(nodes, *args): pairs = getMirrorPairs(nodes) if not pairs: return mc.select(list(pairs.keys())+list(pairs.values())) def copyPose(fromNode, toNode, flip=False): attrs = mc.listAttr(fromNode, keyable=True) if not attrs: return #if attributes are aliased, get the real names for mirroring axis aliases = mc.aliasAttr(fromNode, query=True) if aliases: for alias,real in zip(aliases[::2],aliases[1::2]): if alias in attrs: attrs.remove(alias) attrs.append(real) axis = getMirrorAxis(toNode) for attr in attrs: if attr == 'mirrorAxis': continue if not mc.attributeQuery(attr, node=toNode, exists=True): continue fromPlug = '{}.{}'.format(fromNode, attr) toPlug = '{}.{}'.format(toNode, attr) fromValue = mc.getAttr(fromPlug) toValue = mc.getAttr(toPlug) if attr in axis: fromValue *= -1.0 toValue *= -1.0 try: utl.setAnimValue(toPlug, fromValue) except:pass if flip: try: utl.setAnimValue(fromPlug, toValue) except:pass def mirrorPose(nodes=None, *args): if not nodes: nodes = mc.ls(sl=True) if not nodes: raise RuntimeError('No nodes provided to mirror.') pairs = getMirrorPairs(nodes) done = [] for node, mirror in list(pairs.items()): if node not in done: copyPose(node, mirror) done.append(mirror) def flipPose(nodes=None, *args): if not nodes: nodes = mc.ls(sl=True) if not nodes: raise RuntimeError('No nodes provided to mirror.') nodes = mc.ls(nodes, long=True) flipPairs = getMirrorPairs(nodes) flipSingles = [x for x in nodes if x not in list(flipPairs.keys())] #do the singles: for node in flipSingles: for axis in getMirrorAxis(node): plug = '{}.{}'.format(node,axis) if mc.getAttr(plug, keyable=True): try: utl.setAnimValue(plug, mc.getAttr(plug)*-1.0) except:pass #do the pairs done = [] for node, mirror in list(flipPairs.items()): if node not in done: copyPose(node, mirror, flip=True) done.append(mirror) def copyAnimation(fromNode, toNode): print('copy', fromNode.split('|')[-1], toNode.split('|')[-1]) mc.copyKey(fromNode) mc.pasteKey(toNode, option='replaceCompletely') for axis in getMirrorAxis(toNode): mc.scaleKey(toNode, attribute=axis, valueScale=-1) def swapAnimation(fromNode, toNode): if not mc.keyframe(fromNode, query=True, name=True): mc.cutKey(toNode, clear=True) return attrs = mc.listAttr(fromNode, keyable=True) if not attrs: return for attr in attrs: if not mc.attributeQuery(attr, node=toNode, exists=True): mc.cutKey(fromNode, attribute=attr, clear=True) continue fromPlug = '{}.{}'.format(fromNode, attr) toPlug = '{}.{}'.format(toNode, attr) srcCurve = mc.listConnections(fromPlug, source=True, destination=False, type='animCurve') dstCurve = mc.listConnections(toPlug, source=True, destination=False, type='animCurve') copySrc=None copyDst=None if srcCurve: copySrc = mc.duplicate(srcCurve[0])[0] if dstCurve: copyDst = mc.duplicate(dstCurve[0])[0] if copySrc: try: mc.cutKey(copySrc) mc.pasteKey(toNode, attribute=attr, option='replaceCompletely') except:pass if copyDst: try: mc.cutKey(copyDst) mc.pasteKey(fromNode, attribute=attr, option='replaceCompletely') except:pass for axis in getMirrorAxis(toNode): mc.scaleKey(toNode, attribute=axis, valueScale=-1) mc.scaleKey(fromNode, attribute=axis, valueScale=-1) def mirrorAnimation(nodes=None, *args): if not nodes: nodes = mc.ls(sl=True) if not nodes: raise RuntimeError('No nodes provided to mirror.') pairs = getMirrorPairs(nodes) done = [] for node, mirror in list(pairs.items()): if node not in done: copyAnimation(node, mirror) done.append(mirror) def flipAnimation(nodes, *args): nodes = mc.ls(nodes, long=True) pairs = getMirrorPairs(nodes) flipSingles = [x for x in nodes if x not in list(pairs.keys())] #do the singles: for node in flipSingles: for axis in getMirrorAxis(node): plug = '{}.{}'.format(node,axis) mc.scaleKey(plug, attribute=axis, valueScale=-1) done = [] for node, mirror in list(pairs.items()): if node not in done: swapAnimation(node, mirror) done.append(mirror) def hasFlippedParent(node, testRange=3): parent = mc.listRelatives(node, parent=True, pa=True) for null in range(testRange): if not parent: return False if mc.getAttr(parent[0]+'.scaleX') < 0: return True parent = mc.listRelatives(parent[0], parent=True, pa=True) return False def list_skinned_meshes(namespace): ''' List transform nodes in the puppet namespace that have a skinCluster on a non-intermediate shape. ''' transforms = mc.ls(f'{namespace}:*', type='transform') or [] skinned = [] for xfm in transforms: shapes = mc.listRelatives(xfm, shapes=True, noIntermediate=True, path=True) or [] for shape in shapes: history = mc.listHistory(shape, groupLevels=True, pruneDagObjects=True) if history and mc.ls(history, type='skinCluster'): skinned.append(xfm) break return skinned def copy_ghost_blendShape(source_mesh, ghost_mesh): ''' Create matching blendShape nodes and targets on the ghost mesh. ''' source_shapes = mc.listRelatives(source_mesh, shapes=True, noIntermediate=True) or [] ghost_shapes = mc.listRelatives(ghost_mesh, shapes=True, noIntermediate=True) or [] if not source_shapes or not ghost_shapes: return ghost_shape = ghost_shapes[0] ghost_short = ghost_mesh.split(':')[-1] for source_shape in source_shapes: history = mc.listHistory(source_shape, pruneDagObjects=True) or [] blendshapes = mc.ls(history, type='blendShape') or [] for bs in blendshapes: bs_name = bs.split(':')[-1] if mc.objExists(bs_name): #temporarily rename pass alias_list = mc.aliasAttr(bs, query=True) or [] if not alias_list: continue ghost_bs = mc.blendShape(ghost_mesh, name=bs_name)[0] for i in range(0, len(alias_list), 2): alias = alias_list[i] weight_attr = alias_list[i + 1] target = mc.duplicate(ghost_shape, returnRootsOnly=True)[0] #need error checking for target name target = mc.rename(target, alias) mc.blendShape(ghost_bs, edit=True, target=(ghost_shape, i/2, target, 1.0)) mc.delete(target) src_plug = f'{bs}.{weight_attr}' dst_plug = f'{ghost_bs}.{alias}' src = mc.listConnections(src_plug, source=True, destination=False, plugs=True) if src: mc.connectAttr(src[0], dst_plug, force=True) else: mc.setAttr(dst_plug, mc.getAttr(src_plug)) def create_ghost_triangle(source_mesh, joints=None): ''' Create a single-triangle proxy mesh at the world origin for one skinned source. Returns (ghost_transform, cleanup_nodes) where cleanup_nodes includes the ghost and any orphan geometry created for blendshape targets. ''' short_name = source_mesh.split(':')[-1].split('|')[-1] if mc.objExists(short_name): utl.error(f'Ghost name collision for {source_mesh}') raise RuntimeError ghost_mesh = mc.polyCreateFacet( p=[(0, 0, 0), (0,0,0.010), (0, 0.01, 0)], constructionHistory=False, name=short_name, )[0] #copy all blendshapes copy_ghost_blendShape(source_mesh, ghost_mesh) #skin to all joints skin = utl.getSkinCluster(source_mesh) if not joints: joints = mc.skinCluster(skin, query=True, influence=True) mc.skinCluster(ghost_mesh, joints) return ghost_mesh def create_ghost_triangles(namespace, joints=None): ''' Create ghost triangle proxies for all skinned meshes in a namespace.. ''' triangles = [] for source in list_skinned_meshes(namespace): triangle = create_ghost_triangle(source, joints=joints) triangles.append(triangle) return triangles def export_animations(namespaceFilePairs, includeSkin=True, startFrame=None, endFrame=None): ''' namespaceFilePairs is a dict. Keys are namespaces, values are target filepaths. ''' if not isinstance(namespaceFilePairs, dict): raise TypeError('namespaceFilePairs argument must be a dictionary') bakeList = [] for namespace, fbxFile in namespaceFilePairs.items(): if not isinstance(namespace, str): raise TypeError('Namespaces should be a strings.') if not os.path.isdir(os.path.dirname(fbxFile)): raise RuntimeError('Directory does not exist: {}'.format(os.path.dirname(fbxFile))) quarantine = [] frame_rate = utl.getFrameRate() if not startFrame: startFrame, endFrame = utl.frameRange() bakeList = [] exportMap = {} for namespace, fbxFile in namespaceFilePairs.items(): fbxFile = os.path.normpath(fbxFile).replace('\\', '/') skel = Skeleton(namespace=namespace) skel.init_skeletonNodes() for root in skel.root_names(): if mc.objExists('|'+root): quarantine.append(QuarantineNode(root)) #add a namespace, since we're going to have multiple skeletons for baking exportNamespace = f'{namespace}EXPORT' i=1 while(mc.namespace(exists=exportNamespace) or mc.namespace(exists=f':{exportNamespace}')): exportNamespace = f'{exportNamespace}{i}' i+=1 mc.namespace(set=":") mc.namespace(add=exportNamespace) mc.namespace(set=exportNamespace) skel.create_skeleton(blendShapeProxy=not includeSkin) joints = skel.joints(include_scale=True) bakeList.extend(joints) exportMap[exportNamespace] = [x.joint for x in skel.roots] if includeSkin: skel.apply_bindPose() triangles = create_ghost_triangles(namespace, joints=joints) #add triangles to be exported exportMap[exportNamespace].extend(triangles) #add blendshapes to bake list for t in triangles: bs = mc.ls(mc.listHistory(t), type='blendShape') if bs: bakeList.extend(bs) skel.connect_skeleton() #may need to disconnect matrix connections, or other attrs which can't be baked. for j in joints: try: conns = mc.listConnections(f'{j}.{JOINT_WORLD_MATRIX_ATTR}', s=True, d=False, plugs=True) mc.disconnectAttr(conns[0], f'{j}.{JOINT_WORLD_MATRIX_ATTR}') except Exception: pass #do the bake all at once with utl.IsolateViews(): mc.bakeResults(bakeList, time=(startFrame,endFrame), sampleBy=1, preserveOutsideKeys=False, simulation=True) mc.filterCurve(bakeList) #go through each exported namespace. fbx_kwargs = dict( inputConnections=False, bakeComplexAnimation=False, colladaFrameRate=frame_rate, constraints=False, skins=includeSkin ) for exportNamespace, nodes in exportMap.items(): #get selection mc.select(nodes, replace=True) #remove this namespace mc.namespace( removeNamespace=f':{exportNamespace}', mergeNamespaceWithParent=True) nodes = mc.ls(sl=True) try: FBX_export(fbxFile, **fbx_kwargs) except Exception as err: mc.delete(bakeList) for q in quarantine: q.release() raise err mc.delete(nodes) for q in quarantine: q.release() def export_animation(namespace=None, fbxFile=None, includeSkin=True): ''' Export the puppet with the given namespace, to the given filepath. Namespace is required. When ghost_triangle is True, create lightweight skinned triangle proxies for each skinned mesh in the namespace (skin + blendshape data) instead of joint-attribute blendShape proxies. ''' if not namespace: sel = mc.ls(sl=True) if not len(sel) == 1: raise RuntimeError('select 1 puppet') ns = utl.getNamespace(sel[0]) namespace = ns.strip(':') if not namespace: raise RuntimeError('puppet must have a namespace') if fbxFile: if not os.path.isdir(os.path.dirname(fbxFile)): raise RuntimeError('Directory does not exist: {}'.format(fbxFile)) else: filename = mc.fileDialog2(caption='Export Animation', fileFilter='Animation Files (*.fbx)', fileMode=0, dialogStyle=1) if not filename: return fbxFile = filename[0] if not fbxFile.endswith('.fbx'): fbxFile+='.fbx' fbxFile = os.path.normpath(fbxFile).replace('\\', '/') export_animations({namespace:fbxFile}, includeSkin=includeSkin) def FBX_export(filename, selection=True, **kwargs): ''' FBX command wrapper. ''' filename = filename.replace('\\','/') #if not 'fileVersion' in kwargs: #kwargs['fileVersion'] = 'FBX202000' if not 'generateLog' in kwargs: kwargs['generateLog'] = False if kwargs.get('skins', False) and utl.MAYA_VERSION >= 2024: kwargs['inputConnections'] = True if not 'inAscii' in kwargs: kwargs['inAscii'] = False mm.eval('FBXResetExport') for k,v in kwargs.items(): if isinstance(v, bool): v = str(v).lower() elif isinstance(v, str): v = '"'+v+'"' if not isinstance(v, (int,float)): v = f'-v {v}' cmd = f'FBXExport{k[0].upper()}{k[1:]} {v}' print(cmd) mm.eval(cmd) cmd = f'FBXExport -f "{filename}"' if selection: cmd+=' -s' print(cmd) mm.eval(cmd) class QuarantineNode(object): ''' Class for temporarily grouping assembly nodes that may cause name clashes. ''' def __init__(self, node): self.node = node self.group = mc.group(node, name='QUARANTINE_#') def release(self): try: mc.ungroup(self.group) except RuntimeError as err: print(f'Failed to ungroup: {self.group}') raise err def get_skeleton_nodes(namespace=None): '''Return all skeleton nodes found within a given namespace.''' ns = '' if namespace: ns = '{}:'.format(namespace) return mc.ls(ns+'*.'+SKELETON_NODE_ATTR, o=True) class Skeleton(object): ''' Skeleton object represents the skeleton data for a character. It mostly just handles the SkeletonEntry objects, which do most of the heavy lifting. ''' def __init__(self, namespace=None): #entries is a dictionary of skeletonEntries with key being name self._entries = {} self.roots = [] self.namespace = namespace def root_names(self): return [x.name for x in self._entries.values() if not x.parent] def joint_names(self): return [x.name for x in self._entries.values()] def joints(self, include_scale=True): j = [] for x in self._entries.values(): j.append(x.joint) if include_scale and x.scale: j.append(x.scale) return j def ordered_entries(self): ''' Return skeleton entries in hierarchical pre-order (roots before descendants). Suitable for traversing the hierarchy from root to leaf. Does not require scene joints (callable after init_skeletonNodes / init_skeletonNode only). ''' children = {} roots = [] for entry in self._entries.values(): parent = entry.parent if parent and parent.name in self._entries: children.setdefault(parent.name, []).append(entry) else: roots.append(entry) ordered = [] visited = set() def pre_order(entry): if entry.name in visited: return visited.add(entry.name) ordered.append(entry) child_entries = children.get(entry.name, []) child_entries.sort(key=lambda e: (e.skeletonNode, e.index)) for child in child_entries: pre_order(child) roots.sort(key=lambda e: e.name) for root in roots: pre_order(root) for entry in self._entries.values(): if entry.name not in visited: ordered.append(entry) return ordered def init_skeletonNodes(self): for skelNode in get_skeleton_nodes(namespace=self.namespace): self.init_skeletonNode(skelNode) def init_skeletonNode(self, skelNode): #NEW METHOD entries = [] if mc.attributeQuery('enabled',node=skelNode, exists=True) and not mc.getAttr(f'{skelNode}.enabled'): return entries if mc.attributeQuery('chain',node=skelNode, exists=True): i=0 while True: if not mc.listConnections('{}.chain[{}].active_matrix'.format(skelNode, i), source=True, destination=False): break entry = SkeletonEntry(skelNode, i) self._entries[entry.name] = entry entries.append(entry) i+=1 else: i=0 while True: if not mc.listConnections('{}.jointChain[{}]'.format(skelNode, i), source=True, destination=False): break entry = SkeletonEntry(skelNode, i) self._entries[entry.name] = entry i+=1 return entries def create_skeleton(self, blendShapeProxy=False): ''' create a joint hierarchy from initialized skeleton entries ''' for entry in self._entries.values(): entry.create_joint() for entry in self._entries.values(): if not entry.parent or not entry.parent.name: self.roots.append(entry) continue entry.joint = mc.parent(entry.joint, self._entries[entry.parent.name].joint)[0] entry.joint = mc.rename(entry.joint, entry.name) #check if there's a scale joint underneath, and update that variable as well if entry.scale: kids = mc.listRelatives(entry.joint, pa=True, type='joint') or [] for kid in kids: if kid.rsplit('|',1)[-1] == entry.name+'_scale': entry.scale = kid break #create a joint proxy of the blendshape node if blendShapeProxy: if self.namespace: blendshapes = mc.ls(self.namespace+':*', type='blendShape') or [] else: blendshapes = mc.ls(type='blendShape') or [] for bs in blendshapes: aliasList = mc.aliasAttr(bs, query=True) mc.select(clear=True) joint = mc.joint(name=bs.split(':')[-1]) joint = mc.parent(joint, self.roots[0].joint)[0] for n in range(0,len(aliasList),2): attr = aliasList[n] mc.addAttr(joint, ln=attr, keyable=True) mc.connectAttr(bs+'.'+attr, joint+'.'+attr) def refresh(self): #this is kind of brute force, should be optimized pass # del(self._entries) # self._entries = {} # self.init_skeletonNodes() # for entry in self._entries.values(): # if not entry.joint: # entry.create_joint() # for entry in self._entries.values(): # if not entry.parent: # self.roots.append(entry.name) # continue # current = mc.listRelatives(entry.joint, parent=True) # if current and current[0] == entry.parent.name: # continue # entry.joint = mc.parent(entry.joint, entry.parent.name)[0] #or, go through all skeleton nodes, find ones that don't have joints, and only create those joints? #also check parents? def connect_skeleton(self): ''' Connect joints to skeleton nodes in the scene ''' for entry in self._entries.values(): entry.connect_joint() # if entry.do_split_scale: # entry.split_scale_create() # entry.split_scale_connect() def connect_skin(self): for entry in list(self._entries.values()): entry.connect_skin() mc.dgdirty(a=True) def apply_bindPose(self): for entry in self.ordered_entries(): entry.apply_bindPose() def update_skeleton(self): """ Update existing skeleton to match current skeleton node data. - Creates new joints if they don't exist - Reparents joints if hierarchy has changed - For removed joints: deletes if not skinned, otherwise parents to world and removes skeleton attributes """ # Re-initialize from skeleton nodes to get current data self._entries = {} self.roots = [] self.init_skeletonNodes() # Get all existing skeleton joints in scene existing_joints = mc.ls('*.'+JOINT_WORLD_MATRIX_ATTR, o=True, type='joint') or [] existing_joint_names = set(j.split('|')[-1] for j in existing_joints) # Get all joint names that should exist according to data target_joint_names = set(self._entries.keys()) # Joints to create (in data but not in scene) joints_to_create = target_joint_names - existing_joint_names # Joints to remove (in scene but not in data) joints_to_remove = existing_joint_names - target_joint_names # Joints to check for reparenting (exist in both) joints_to_check = target_joint_names & existing_joint_names # Step 1: Create missing joints for name in joints_to_create: entry = self._entries[name] entry.create_joint() # Step 2: Parent new joints and check/fix hierarchy of existing joints for name in target_joint_names: entry = self._entries[name] # Make sure joint reference is set for existing joints if not entry.joint and mc.objExists(name): for node in mc.ls(name, type='joint'): entry.joint = node break if not entry.joint: continue # Determine correct parent target_parent = None if entry.parent and entry.parent.name: target_parent = entry.parent.name # Get current parent current_parent = mc.listRelatives(entry.joint, parent=True, pa=True) current_parent_name = current_parent[0].split('|')[-1] if current_parent else None # Reparent if needed if target_parent != current_parent_name: if target_parent and mc.objExists(target_parent): entry.joint = mc.parent(entry.joint, target_parent)[0] entry.joint = mc.rename(entry.joint, name) print(f"Reparented joint: {name} -> {target_parent}") elif not target_parent and current_parent: # Should be a root joint entry.joint = mc.parent(entry.joint, world=True)[0] entry.joint = mc.rename(entry.joint, name) print(f"Unparented joint to world: {name}") if not entry.parent: self.roots.append(entry) # Step 3: Handle removed joints for name in joints_to_remove: # Find the joint joints = [j for j in existing_joints if j.split('|')[-1] == name] if not joints: continue joint = joints[0] # Check if skinned is_skinned = False skin_connections = mc.listConnections( f'{joint}.worldMatrix[0]', source=False, destination=True, type='skinCluster' ) if skin_connections: is_skinned = True if is_skinned: # Parent to world and remove skeleton attributes parent = mc.listRelatives(joint, parent=True, pa=True) if parent: mc.parent(joint, world=True) # Disconnect and remove skeleton attributes if mc.attributeQuery(JOINT_WORLD_MATRIX_ATTR, node=joint, exists=True): connections = mc.listConnections( f'{joint}.{JOINT_WORLD_MATRIX_ATTR}', source=True, destination=False, plugs=True ) if connections: mc.disconnectAttr(connections[0], f'{joint}.{JOINT_WORLD_MATRIX_ATTR}') mc.deleteAttr(joint, attribute=JOINT_WORLD_MATRIX_ATTR) if mc.attributeQuery(JOINT_PREBIND_MATRIX_ATTR, node=joint, exists=True): connections = mc.listConnections( f'{joint}.{JOINT_PREBIND_MATRIX_ATTR}', source=True, destination=False, plugs=True ) if connections: mc.disconnectAttr(connections[0], f'{joint}.{JOINT_PREBIND_MATRIX_ATTR}') mc.deleteAttr(joint, attribute=JOINT_PREBIND_MATRIX_ATTR) # Disconnect transform connections for attr in ['translate', 'rotate', 'scale']: connections = mc.listConnections( f'{joint}.{attr}', source=True, destination=False, plugs=True ) if connections: for conn in connections: try: mc.disconnectAttr(conn, f'{joint}.{attr}') except RuntimeError: pass print(f"Removed from skeleton (skinned - kept in scene): {name}") else: # Not skinned, safe to delete # First reparent children to this joint's parent children = mc.listRelatives(joint, children=True, pa=True, type='joint') parent = mc.listRelatives(joint, parent=True, pa=True) if children: for child in children: if parent: mc.parent(child, parent[0]) else: mc.parent(child, world=True) mc.delete(joint) print(f"Deleted joint: {name}") # Step 4: Reconnect joints that may have lost connections for name in joints_to_check: entry = self._entries.get(name) if not entry or not entry.joint: continue # Check if already connected if mc.attributeQuery(JOINT_WORLD_MATRIX_ATTR, node=entry.joint, exists=True): existing_conn = mc.listConnections( f'{entry.joint}.{JOINT_WORLD_MATRIX_ATTR}', source=True, destination=False ) if existing_conn: continue # Already connected # Connect if not connected entry.connect_joint() print(f"Reconnected joint: {name}") mc.dgdirty(a=True) return { 'created': list(joints_to_create), 'removed': list(joints_to_remove), 'updated': list(joints_to_check) } class SkeletonEntry(object): ''' Data point representing a joint in a skeleton hierarchy, before and after it is created. ''' def __init__(self, skeletonNode, index): self._name = None self._parent = False self._children = [] self._localMatrix = None self.skeletonNode = str(skeletonNode) self.index = index self.joint = None self.scale = None self.decompose = None @property def plug(self): #NEW METHOD if mc.objExists(self.active): return self.active #OLD METHOD return '{}.jointChain[{}]'.format(self.skeletonNode, self.index) @property def struct(self): return f'{self.skeletonNode}.chain[{self.index}]' @property def rest(self): return f'{self.struct}.rest_matrix' @property def active(self): return f'{self.struct}.active_matrix' @property def do_split_scale(self): if mc.attributeQuery('split_scale', node=self.skeletonNode, exists=True): return bool(mc.getAttr(f'{self.skeletonNode}.split_scale')) return False @property def name(self): ''' Joint name is derived from the node providing the world matrix for the joint, minus suffix. ''' if not self._name: #NEW METHOD if mc.attributeQuery('chain', node=self.skeletonNode, exists=True): plug = f'{self.skeletonNode}.chain[{self.index}].active_matrix' if mc.listConnections(plug, source=True, destination=False): self._name = get_skeleton_joint_name(plug) if not self._name: #OLD METHOD if mc.attributeQuery('jointChain', node=self.skeletonNode, exists=True): plug = f'{self.skeletonNode}.jointChain[{self.index}]' if mc.listConnections(plug, source=True, destination=False): self._name = get_skeleton_joint_name(plug) return self._name @property def parent(self): ''' walk up the parent connection until we find another skeleton node ''' if self._parent is False: if mc.getAttr('{}.hierarchical'.format(self.skeletonNode)) and self.index != 0: #first, if it's a hierarchical chain and it's not the first index, #just parent to the previous index self.set_parent(SkeletonEntry(self.skeletonNode, self.index-1)) else: #otherwise, chase the parent attribute until we hit a skeleton node self._parent = None trace = mc.listConnections(f'{self.skeletonNode}.parent', source=True, destination=False, plugs=True) or False while trace: node, attr = trace[0].split('.',1) if mc.attributeQuery(SKELETON_NODE_ATTR, node=node, exists=True) and ('jointChain' in attr or 'chain' in attr): index = int(trace[0].split('[')[-1].split(']')[0]) #found the parent skeleton data node, get the joint it's pointing to self.set_parent(SkeletonEntry(node, index)) break trace = mc.listConnections(trace[0], source=True, destination=False, plugs=True) if trace: continue splitAttr = attr.split('.')[-1] parentAttr = mc.attributeQuery(splitAttr, listParent=True, node=node) if not parentAttr: trace=None break parentAttr = parentAttr[0] if '[' in attr: i = attr.split('[')[-1].split(']')[0] parentAttr = f'{parentAttr}[{i}]' parentCon = mc.listConnections(f'{node}.{parentAttr}', source=True, destination=False, plugs=True) if not parentCon: trace=None break #assume it's the same leaf attr trace = [f'{parentCon[0]}.{attr.split(".")[-1]}'] return self._parent @property def is_leaf(self): ''' This needs more testing, still isn't working for all situations. ''' def _trace_child_connections(plug): plugs = mc.listConnections(plug, source=False, destination=True, plugs=True) or [] for p in plugs: node, attr = p.split('.',1) if attr == 'parent' and mc.attributeQuery(SKELETON_NODE_ATTR, node=node, exists=True): return True if _trace_child_connections(p): return True #check whether it's hierarchical and not the last index if mc.getAttr('{}.hierarchical'.format(self.skeletonNode)) and self.index + 1 < mc.getAttr('{}.chain'.format(self.skeletonNode), size=True): return False #check whether it's not plugged into the parent attribute of any other skelnodes, recursively return not _trace_child_connections(self.plug) def set_parent(self, parent): '''''' if not isinstance(parent, SkeletonEntry): raise RuntimeError('Parent should be SkeletonEntry type.') parent.append_child(self) self._parent = parent def append_child(self, child): if not child in self._children: self._children.append(child) def create_joint(self): ''' ''' mc.select(clear=True) found = [] if mc.objExists(self.name): for node in mc.ls(self.name): if mc.nodeType(node) != 'joint': warnings.warn(f'Joint name is already in use by another node! {node}') else: found.append(node) if found: self.joint = found[0] else: self.joint = mc.createNode('joint', name=self.name) try: mc.addAttr(self.joint, ln=JOINT_WORLD_MATRIX_ATTR, dt='matrix', keyable=True) except Exception: pass if self.do_split_scale and not self.is_leaf: mc.select(clear=True) self.scale = mc.createNode('joint', name=self.name+'_scale') mc.setAttr(f'{self.scale}.segmentScaleCompensate', 0) mc.addAttr(self.scale, ln=JOINT_WORLD_MATRIX_ATTR, dt='matrix', keyable=True) self.scale = mc.parent(self.scale, self.joint)[0] mc.setAttr(f'{self.joint}.segmentScaleCompensate', 0) mc.setAttr(f'{self.joint}.displayLocalAxis', 1) def parent_joint(self): ''' This should only be used for speed if a single joint is being created and you know it has a parent. The skeleton instance can get corrupted because this isn't being tracked. ''' if self.parent: self.joint = mc.parent(self.joint, self.parent.name)[0] @property def localMatrix(self): if not self.joint: self._localMatrix = None return None if not self._localMatrix: #look for it first a = mc.listConnections('{}.translate'.format(self.joint), d=False, type='decomposeMatrix') if a: b = mc.listConnections('{}.inputMatrix'.format(a[0]), type='multMatrix', d=False) if b: j = mc.listConnections('{}.matrixIn[0]'.format(b[0]), type='joint', d=False) if j and j == self.joint: self._localMatrix = b if not self._localMatrix: mc.connectAttr(self.plug, '{}.{}'.format(self.joint, JOINT_WORLD_MATRIX_ATTR)) self._localMatrix = mc.createNode('multMatrix', name='{}_localMatrix'.format(self.name)) mc.connectAttr(self.plug, '{}.matrixIn[0]'.format(self._localMatrix)) mc.connectAttr('{}.parentInverseMatrix[0]'.format(self.joint), '{}.matrixIn[1]'.format(self._localMatrix)) #prebind matrix if mc.objExists(self.rest) and mc.listConnections(self.rest, source=True, destination=False): try: mc.addAttr(self.joint, ln=JOINT_PREBIND_MATRIX_ATTR, dt='matrix', keyable=True) except: pass self._preBindInverse = mc.createNode('inverseMatrix', name='{}_preBindInverse'.format(self.name)) mc.connectAttr(self.rest, f'{self._preBindInverse}.inputMatrix') mc.connectAttr(f'{self._preBindInverse}.outputMatrix', f'{self.joint}.{JOINT_PREBIND_MATRIX_ATTR}') return self._localMatrix def connect_joint(self): if not self.joint: raise RuntimeError('joint not created yet.') self.decompose = mc.createNode('decomposeMatrix', name='{}_decompose'.format(self.name)) mc.connectAttr('{}.matrixSum'.format(self.localMatrix), '{}.inputMatrix'.format(self.decompose)) for x in 'XYZ': mc.connectAttr(f'{self.decompose}.outputTranslate{x}', f'{self.joint}.translate{x}') mc.connectAttr(f'{self.decompose}.outputRotate{x}', f'{self.joint}.rotate{x}') if not self.scale: mc.connectAttr(f'{self.decompose}.outputScale{x}', f'{self.joint}.scale{x}') else: mc.connectAttr(f'{self.decompose}.outputScale{x}', f'{self.scale}.scale{x}') def apply_bindPose(self): if not self.joint: return if not ml_snap: raise RuntimeError('ml_snap module is required for setting bindpose.') skin = mc.listConnections(self.plug, source=False, destination=True, type='skinCluster', plugs=True) or [] if not skin: mc.setAttr(f'{self.joint}.translate',0,0,0) mc.setAttr(f'{self.joint}.rotate',0,0,0) mc.setAttr(f'{self.joint}.scale',1,1,1) if not self.scale: return mc.setAttr(f'{self.scale}.translate',0,0,0) mc.setAttr(f'{self.scale}.rotate',0,0,0) mc.setAttr(f'{self.scale}.scale',1,1,1) return skin, attr = skin[0].split('.',1) index = int(attr.split('[')[-1].strip(']')) prebind_matrix = utl.Matrix(mc.getAttr(f'{skin}.bindPreMatrix[{index}]')) ml_snap.set_worldMatrix(self.joint, prebind_matrix.inverse()) if self.scale: mc.setAttr(f'{self.scale}.translate',0,0,0) mc.setAttr(f'{self.scale}.rotate',0,0,0) mc.setAttr(f'{self.scale}.scale',1,1,1) def connect_skin(self): #connect to skincluster jnt = self.scale or self.joint for skinCon in mc.listConnections(self.plug, source=False, destination=True, type='skinCluster', plugs=True) or []: mc.connectAttr(jnt+'.worldMatrix[0]', skinCon, force=True) return #skipping this connection for now, as it is not fully supported by modules and compile and 2024 #maybe some day. #do this as a separate loop in case skin is connected to joints already. if mc.attributeQuery(JOINT_PREBIND_MATRIX_ATTR, node=self.joint, exists=True): for skinCon in mc.listConnections(self.joint, source=False, destination=True, type='skinCluster', plugs=True) or []: skin = skinCon.split('.')[0] i = skinCon.split('[')[-1].strip(']') try: mc.connectAttr(f'{self.joint}.{JOINT_PREBIND_MATRIX_ATTR}', f'{skin}.bindPreMatrix[{i}]') except: pass def add_skeleton(includeBlendshapes=False): skel = Skeleton() skel.init_skeletonNodes() skel.create_skeleton(blendShapeProxy=includeBlendshapes) skel.connect_skeleton() skel.connect_skin() def update_skeleton(): """ Update existing skeleton to match current skeleton node data. More efficient than remove_skeleton + add_skeleton as it only modifies what has changed. Returns: dict: Summary of changes with keys 'created', 'removed', 'updated' """ skel = Skeleton() result = skel.update_skeleton() skel.connect_skin() return result def remove_skeleton(): ''' ''' specialJoints = mc.ls('*.'+JOINT_WORLD_MATRIX_ATTR, o=True, type='joint') count = 0 for joint in specialJoints: matrix = mc.listConnections('{}.{}'.format(joint, JOINT_WORLD_MATRIX_ATTR), source=True, destination=False, plugs=True) outputs = mc.listConnections('{}.worldMatrix[0]'.format(joint), source=False, destination=True, plugs=True) or [] if not matrix: continue for each in outputs: skin = each.split('.')[0] if mc.nodeType(skin) != 'skinCluster': continue mc.connectAttr(matrix[0], each, force=True) count+=1 if mc.attributeQuery(JOINT_PREBIND_MATRIX_ATTR, node=joint, exists=True): i = each.split('[')[-1].strip(']') preBind = f'{skin}.bindPreMatrix[{i}]' if mc.isConnected(f'{joint}.{JOINT_PREBIND_MATRIX_ATTR}', preBind): value = mc.getAttr(preBind) mc.disconnectAttr(f'{joint}.{JOINT_PREBIND_MATRIX_ATTR}', preBind) mc.setAttr(preBind, value, type='matrix') #selectively delete special joints that only have special joint children for joint in specialJoints: try: mc.delete(joint) except: pass mc.dgdirty(a=True) return count def is_skeleton(node): return mc.attributeQuery(SKELETON_NODE_ATTR, exists=True, node=node) def get_skeleton_from_joint(joint): if not mc.attributeQuery(JOINT_WORLD_MATRIX_ATTR, exists=True, node=joint): return None skel = mc.listConnections(f'{joint}.{JOINT_WORLD_MATRIX_ATTR}', source=True, destination=False) if skel and is_skeleton(skel[0]): return skel[0] return None def get_skeleton_joint_name(jointOrPlug): jointOrPlug = str(jointOrPlug) matrix = None if '.' not in jointOrPlug: if mc.attributeQuery(JOINT_WORLD_MATRIX_ATTR, exists=True, node=jointOrPlug): skel = mc.listConnections(f'{jointOrPlug}.{JOINT_WORLD_MATRIX_ATTR}', source=True, destination=False, plugs=True) if not skel: raise RuntimeError(f'Skeletonized joint not connected to a skeleton node: {jointOrPlug}') jointOrPlug = skel[0] else: #eventually handle more types of inputs raise RuntimeError(f'Joint input must be a skeletonized joint: {jointOrPlug}') #check this is a skeleton node attr if not is_skeleton(jointOrPlug.split('.')[0]): raise RuntimeError(f'Plug must be from a skeleton node: {jointOrPlug}') #check its a matrix attr? if mc.getAttr(jointOrPlug, type=True) != 'matrix': raise RuntimeError(f'Non-matrix attribute: {jointOrPlug}') matrix = mc.listConnections(jointOrPlug, source=True, destination=False) if not matrix: raise RuntimeError(f'Skeleton connection is not driven: {jointOrPlug}') return get_skeleton_name_from_matrix(matrix[0]) def get_skeleton_name_from_matrix(matrixNode): name = matrixNode.rsplit('|',1)[-1].rsplit('_',1)[0].rsplit(':',1)[-1] return name.replace('__','_') def filter_graphEditor(): if not mc.outlinerEditor('graphEditor1OutlineEd', q=True, exists=True): mm.eval('GraphEditor;') filter = mc.itemFilter(byName='*.offsetParentMatrix', negate=True) mc.outlinerEditor('graphEditor1OutlineEd', e=True, attrFilter=filter, ignoreHiddenAttribute=True) mm.eval('AEdagNodeCommonRefreshOutliners();')