def createControl(shapeData, name=None, targetNode=None, link=False, parent=None): """ Create a control at the given target with the given shape data. Args: shapeData: A dict containing control shape data name: A string name of the control created targetNode: An optional transform node to position the control at upon creation link (bool): If true, link the control to the targetNode parent: An optional transform node to parent the control to """ if targetNode and not isinstance(targetNode, pm.nt.Transform): raise TypeError('targetNode must be a Transform node') if name is None: name = 'ctl1' # create control transform ctl = pm.group(em=True, n=name) addShapes(ctl, shapeData) if targetNode: # match target node transform settings ctl.setAttr('rotateOrder', targetNode.getAttr('rotateOrder')) pulse.nodes.matchWorldMatrix(targetNode, ctl) if link: pulse.links.link(targetNode, ctl) # group to main ctls group if parent: ctl.setParent(parent) # apply meta data to keep track of control shapes meta.setMetaData(ctl, CONTROLSHAPE_METACLASS, {}) return ctl
def run(self): # add meta class to the control, making it # easy to search for by anim tools, etc meta.setMetaData(self.controlNode, self.config['controlMetaClass'], {}) if self.zeroOutMethod == 1: # freeze offset matrix pulse.nodes.freezeOffsetMatrix(self.controlNode) elif self.zeroOutMethod == 2: # create an offset transform pulse.nodes.createOffsetTransform(self.controlNode) # lockup attributes keyableAttrs = pulse.nodes.getExpandedAttrNames(self.keyableAttrs) lockedAttrs = pulse.nodes.getExpandedAttrNames( ['t', 'r', 'rp', 's', 'sp', 'ra', 'sh', 'v']) lockedAttrs = list(set(lockedAttrs) - set(keyableAttrs)) for attrName in keyableAttrs: attr = self.controlNode.attr(attrName) attr.setKeyable(True) for attrName in lockedAttrs: attr = self.controlNode.attr(attrName) attr.setKeyable(False) attr.showInChannelBox(False) attr.setLocked(True) # show rotate order in channel box self.controlNode.rotateOrder.setLocked(True) self.controlNode.rotateOrder.showInChannelBox(True) # update rig meta data self.extendRigMetaDataList('animControls', [self.controlNode])
def test_removeLockedData(self): meta.setMetaData(self.node, 'myMetaClass', 'myTestData') self.node.attr(meta.core.METADATA_ATTR).setLocked(True) result = meta.removeMetaData(self.node) self.assertFalse(result) data = meta.getMetaData(self.node) self.assertEqual(data, {'myMetaClass': 'myTestData'})
def run(self): # add meta class to the control, making it # easy to search for by anim tools, etc meta.setMetaData(self.controlNode, self.config['controlMetaClass'], {}) if self.createOffset: offsetNode = pulse.nodes.createOffsetGroup(self.controlNode) # lockup attributes keyableAttrs = pulse.nodes.getExpandedAttrNames(self.keyableAttrs) lockedAttrs = pulse.nodes.getExpandedAttrNames( ['t', 'r', 'rp', 's', 'sp', 'ra', 'sh', 'v']) lockedAttrs = list(set(lockedAttrs) - set(keyableAttrs)) for attrName in keyableAttrs: attr = self.controlNode.attr(attrName) attr.setKeyable(True) for attrName in lockedAttrs: attr = self.controlNode.attr(attrName) attr.setKeyable(False) attr.showInChannelBox(False) attr.setLocked(True) # show rotate order in channel box self.controlNode.rotateOrder.setLocked(True) self.controlNode.rotateOrder.showInChannelBox(True)
def test_removeClassData(self): meta.setMetaData(self.node, 'myMetaClass', None) meta.setMetaData(self.node, 'mySecondMetaClass', None) result = meta.removeMetaData(self.node, 'myMetaClass') self.assertTrue(result) self.assertTrue(meta.isMetaNode(self.node)) self.assertFalse(meta.hasMetaClass(self.node, 'myMetaClass')) self.assertTrue(meta.hasMetaClass(self.node, 'mySecondMetaClass'))
def save(self): # TODO: handle locked nodes data = { 'sets': [s.asDict() for s in self.sets] } node = self.getOrCreateNode() # update name to resolve node creation differences self.name = getCollectionNameFromNode(node) meta.setMetaData(node, META_CLASSNAME, data)
def setUp(self): self.nodeA = pm.group(em=True) meta.setMetaData(self.nodeA, 'ClassA', 'A') self.nodeB = pm.group(em=True) meta.setMetaData(self.nodeB, 'ClassB', 'B') meta.setMetaData(self.nodeB, 'ClassD', 'D') self.nodeC = pm.group(em=True) meta.setMetaData(self.nodeC, 'ClassC', 'C') meta.setMetaData(self.nodeC, 'ClassD', 'D')
def saveToNode(self, node, create=False): """ Save this Blueprint to a node, creating a new node if desired. Args: node: A PyNode or node name create: A bool, whether to create the node if it doesn't exist """ if create and not pm.cmds.objExists(node): node = pm.cmds.createNode('network', n=node) data = self.serialize() meta.setMetaData(node, BLUEPRINT_METACLASS, data)
def createSpace(node, name): """ Create a new space Args: node: A PyNode or string node name name: A string name of the space to create """ data = { 'name': name, } meta.setMetaData(node, SPACE_METACLASS, data)
def setMirroringData(node, otherNode): """ Set the mirroring data for a node Args: node: A node on which to set the mirroring data otherNode: The counterpart node to be stored in the mirroring data """ data = { 'otherNode': otherNode, } meta.setMetaData(node, MIRROR_METACLASS, data, undoable=True)
def createRigNode(name): """ Create and return a new Rig node Args: name: A str name of the rig """ if cmds.objExists(name): raise ValueError( "Cannot create rig, node already exists: {0}".format(name)) node = pm.group(name=name, em=True) for a in ('tx', 'ty', 'tz', 'rx', 'ry', 'rz', 'sx', 'sy', 'sz'): node.attr(a).setLocked(True) node.attr(a).setKeyable(False) # set initial meta data for the rig meta.setMetaData(node, RIG_METACLASS, {'name': name}) return node
def saveToNode(self, node, create=False): """ Save this Blueprint to a node, creating a new node if desired. Args: node (PyNode or str): A node or node name create (bool): If true, create the node if necessary Returns: The node on which the blueprint was saved """ if create and not cmds.objExists(node): node = cmds.createNode('network', n=node) data = self.serialize() st = time.time() meta.setMetaData(node, BLUEPRINT_METACLASS, data, replace=True) et = time.time() LOG.debug('blueprint save time: {0}s'.format(et - st)) return node
def setLinkMetaData(node, linkData): """ Set the metadata for a linked node """ meta.setMetaData(node, className=LINK_METACLASS, data=linkData)
def setLinkMetaData(self, node, linkData): """ Set the metadata for a linked node """ linkData['type'] = self.linkType meta.setMetaData(node, className=LINK_METACLASS, data=linkData)
def test_removeData(self): meta.setMetaData(self.node, 'myMetaClass', None) self.assertTrue(meta.isMetaNode(self.node)) result = meta.removeMetaData(self.node) self.assertTrue(result) self.assertFalse(meta.isMetaNode(self.node))
def test_multiClassData(self): cls1 = 'myMetaClass1' cls2 = 'myMetaClass2' meta.setMetaData(self.node, cls1, None) meta.setMetaData(self.node, cls2, None) self.assertEqual(meta.getMetaData(self.node), {cls1: None, cls2: None})
def test_setAndGetData(self): setData = ['myData', {'a': 1, 'b': 2}, ('x', 'y', 'z')] className = 'myMetaClass' meta.setMetaData(self.node, className, setData) self.assertEqual(meta.getMetaData(self.node, className), setData)
def run(self): # retrieve mid and root joints midJoint = self.endJoint.getParent() rootJoint = midJoint.getParent() # duplicate joints for ik chain ikJointNameFmt = '{0}_ik' ikjnts = pulse.nodes.duplicateBranch(rootJoint, self.endJoint, nameFmt=ikJointNameFmt) for j in ikjnts: # TODO: debug settings for build actions j.v.set(True) rootIkJoint = ikjnts[0] midIkJoint = ikjnts[1] endIkJoint = ikjnts[2] # parent ik joints to root control rootIkJoint.setParent(self.rootCtl) # create ik and hook up pole object and controls handle, effector = pm.ikHandle(name="{0}_ikHandle".format(endIkJoint), startJoint=rootIkJoint, endEffector=endIkJoint, solver="ikRPsolver") # add twist attr to end control self.endCtlIk.addAttr('twist', at='double', k=1) self.endCtlIk.twist >> handle.twist # connect mid ik ctl (pole vector) pm.poleVectorConstraint(self.midCtlIk, handle) # parent ik handle to end control handle.setParent(self.endCtlIk) # TODO: use pick matrix and mult matrix to combine location from ik system with rotation/scale of ctl # constraint end joint scale and rotation to end control pm.orientConstraint(self.endCtlIk, endIkJoint, mo=True) pm.scaleConstraint(self.endCtlIk, endIkJoint, mo=True) # setup ikfk switch attr (integer, not blend) self.rootCtl.addAttr("ik", min=0, max=1, at='short', defaultValue=1, keyable=1) ikAttr = self.rootCtl.attr("ik") # create choices for world matrix from ik and fk targets rootChoice = pulse.utilnodes.choice(ikAttr, self.rootCtl.wm, rootIkJoint.wm) rootChoice.node().rename(f"{rootJoint.nodeName()}_ikfk_choice") midChoice = pulse.utilnodes.choice(ikAttr, self.midCtlFk.wm, midIkJoint.wm) midChoice.node().rename(f"{midJoint.nodeName()}_ikfk_choice") endChoice = pulse.utilnodes.choice(ikAttr, self.endCtlFk.wm, endIkJoint.wm) endChoice.node().rename(f"{self.endJoint.nodeName()}_ikfk_choice") # connect the target matrices to the joints pulse.nodes.connectMatrix(rootChoice, rootJoint, pulse.nodes.ConnectMatrixMethod.SNAP) pulse.nodes.connectMatrix(midChoice, midJoint, pulse.nodes.ConnectMatrixMethod.SNAP) pulse.nodes.connectMatrix(endChoice, self.endJoint, pulse.nodes.ConnectMatrixMethod.SNAP) # connect visibility self.midCtlIk.v.setLocked(False) self.endCtlIk.v.setLocked(False) ikAttr >> self.midCtlIk.v ikAttr >> self.endCtlIk.v fkAttr = pulse.utilnodes.reverse(ikAttr) self.midCtlFk.v.setLocked(False) self.endCtlFk.v.setLocked(False) fkAttr >> self.midCtlFk.v fkAttr >> self.endCtlFk.v # add connecting line shape if self.addPoleLine: # keep consistent color overrides for the mid ctl color = pulse.nodes.getOverrideColor(self.midCtlIk) pulse.controlshapes.createLineShape(midIkJoint, self.midCtlIk, self.midCtlIk) if color: pulse.nodes.setOverrideColor(self.midCtlIk, color) # cleanup handle.v.set(False) for jnt in ikjnts: # TODO: lock attrs jnt.v.set(False) # add metadata to controls ikfk_ctl_data = { 'root_fk_ctl': self.rootCtl, 'mid_fk_ctl': self.midCtlFk, 'end_fk_ctl': self.endCtlFk, 'root_ik_ctl': self.rootCtl, 'mid_ik_ctl': self.midCtlIk, 'end_ik_ctl': self.endCtlIk, 'end_joint': self.endJoint, } ikfk_ctls = { self.rootCtl, self.midCtlIk, self.endCtlIk, self.midCtlFk, self.endCtlFk } if self.extraControls: ikfk_ctls.update(self.extraControls) for ctl in ikfk_ctls: meta.setMetaData(ctl, IKFK_CONTROL_METACLASS, ikfk_ctl_data)
def link(leader, follower): """ Link the follower to a leader """ meta.setMetaData(follower, className=LINK_METACLASS, data=leader)
def setupSpaceConstraint(node, spaceNames, follower=None, useOffsetMatrix=True): """ Set up a node to be constrained for a space switch, but do not actually connect it to the desired spaces until `connectSpaceConstraint` is called. This is necessary because the transforms that represent each space may not have been defined yet, but the desire to constrain to them by space name can be expressed ahead of time. Args: node (PyNode): The node that will contain space switching attrs spaceNames (str list): The names of all spaces to be applied follower (PyNode): If given, the node that will be constrained, otherwise `node` will be used. Useful when wanting to create the space constrain attributes on an animation control, but connect the actual constraint to a parent transform useOffsetMatrix (bool): When true, will connect to the offsetParentMatrix of the follower node, instead of directly into the translate, rotate, and scale. This also eliminates the necessity for a decompose matrix node. """ if useOffsetMatrix and cmds.about(api=True) < 20200000: # not supported before Maya 2020 useOffsetMatrix = False if not follower: follower = node # setup space switching attr if not node.hasAttr(SPACESWITCH_ATTR): enumNames = ':'.join(spaceNames) node.addAttr(SPACESWITCH_ATTR, at='enum', en=enumNames) spaceAttr = node.attr(SPACESWITCH_ATTR) spaceAttr.setKeyable(True) else: spaceAttr = node.attr(SPACESWITCH_ATTR) nodeName = node.nodeName() offsetChoiceName = nodeName + '_spaceOffset_choice' spaceChoiceName = nodeName + '_space_choice' multMatrixName = nodeName + '_space_mmtx' decompName = nodeName + '_space_decomp' # create utility nodes offsetChoice = pm.shadingNode('choice', n=offsetChoiceName, asUtility=True) spaceChoice = pm.shadingNode('choice', n=spaceChoiceName, asUtility=True) utilnodes.loadMatrixPlugin() multMatrix = pm.shadingNode('multMatrix', n=multMatrixName, asUtility=True) if not useOffsetMatrix: decomp = pm.shadingNode('decomposeMatrix', n=decompName, asUtility=True) # setup connections spaceAttr >> offsetChoice.selector spaceAttr >> spaceChoice.selector offsetChoice.output >> multMatrix.matrixIn[0] spaceChoice.output >> multMatrix.matrixIn[1] # follower.pim >> multMatrix.matrixIn[2] if not useOffsetMatrix: multMatrix.matrixSum >> decomp.inputMatrix # final connection to the follower occurs # during connectSpaceConstraint. spaceData = [] # native space indeces always take priority, # which means dynamic spaces may be adversely affected # if the native spaces change on a published rig # TODO: ability to reindex dynamic spaces for i, spaceName in enumerate(spaceNames): spaceData.append({ 'name': spaceName, # TODO: is `switch` needed anymore? 'switch': None, 'index': i, }) data = { # native spaces in this constraint 'spaces': spaceData, # dynamic spaces (added during animation), which may be # from the native rig, or from an external one 'dynamicSpaces': [], # transform that is actually driven by the space constraint 'follower': follower, # the utility nodes that make up the space constraint 'offsetChoice': offsetChoice, 'spaceChoice': spaceChoice, 'multMatrix': multMatrix, 'useOffsetMatrix': useOffsetMatrix, } if not useOffsetMatrix: # decomp only exists when not using offset matrix data['decompose'] = decomp meta.setMetaData(node, SPACECONSTRAINT_METACLASS, data)
def run(self): shouldCreateOffset = False if self.createFollowerOffset == 0: # Always shouldCreateOffset = True elif self.createFollowerOffset == 1 and self.follower.nodeType( ) != 'joint': # Exclude Joints and the follower is not a joint shouldCreateOffset = True _follower = self.follower _toeFollower = self.toeFollower if shouldCreateOffset: _follower = pulse.nodes.createOffsetTransform(self.follower) _toeFollower = pulse.nodes.createOffsetTransform(self.toeFollower) # TODO(bsayre): expose as option self.useCustomAttrs = False if self.useCustomAttrs: # create 'lift' and 'ballToe' blend attrs self.control.addAttr("ballToe", min=0, max=1, at='double', defaultValue=0, keyable=1) ballToeAttr = self.control.attr('ballToe') self.control.addAttr("lift", min=0, max=1, at='double', defaultValue=0, keyable=1) liftAttr = self.control.attr('lift') lockedAttrs = ['tx', 'ty', 'tz', 'sx', 'sy', 'sz'] else: # use tx and tz to control ball toe and lift blend ballToeAttr = self.control.tx liftAttr = self.control.tz # configure magic control # limit translate attrs and use them to drive blends pm.transformLimits(self.control, tx=(0, 1), tz=(0, 1), etz=(True, True), etx=(True, True)) lockedAttrs = ['ty', 'sx', 'sy', 'sz'] # lockup attributes on the magic control for attrName in lockedAttrs: attr = self.control.attr(attrName) attr.setKeyable(False) attr.showInChannelBox(False) attr.setLocked(True) # transform that will contain the final results of planted targets if self.plantedTarget: planted_tgt = self.plantedTarget else: planted_tgt = pm.group(em=True, p=self.control, n='{0}_mf_anklePlanted_tgt'.format( self.follower.nodeName())) # use liftControl directly as target lifted_tgt = self.liftControl # toe tgt is only used for the toe follower, not the main ankle follower # (keeps toe control fully locked when using ball pivot) toeDown_tgt = pm.group(em=True, p=self.toePivot, n='{0}_mf_toeDown_tgt'.format( self.toeFollower.nodeName())) toeUp_tgt = pm.group(em=True, p=_toeFollower.getParent(), n='{0}_mf_toeUp_tgt'.format( self.toeFollower.nodeName())) # ball pivot will contain result of both toe and ball pivot ballToe_tgt = pm.group(em=True, p=self.ballPivot, n='{0}_mf_ballToe_tgt'.format( self.follower.nodeName())) heel_tgt = pm.group(em=True, p=self.heelPivot, n='{0}_mf_heel_tgt'.format( self.follower.nodeName())) followerMtx = pulse.nodes.getWorldMatrix(self.follower) toeFollowerMtx = pulse.nodes.getWorldMatrix(self.toeFollower) # update pivots to match world rotation of control and create # offset so that direct connect rotations will match up for node in (self.toePivot, self.ballPivot, self.heelPivot): followerMtx.translate = (0, 0, 0) followerMtx.scale = (1, 1, 1) pulse.nodes.setWorldMatrix(node, followerMtx) pulse.nodes.createOffsetTransform(node) if node == self.toePivot: # after orienting toe pivot, re-parent ballPivot self.ballPivot.setParent(self.toePivot) # update toe target transforms to match toe follower transform for node in (toeDown_tgt, toeUp_tgt): pulse.nodes.setWorldMatrix(node, toeFollowerMtx) # update target transforms to match follower transform # (basically preserves offsets on the follower) for node in (ballToe_tgt, heel_tgt): # , ankle_tgt): pulse.nodes.setWorldMatrix(node, followerMtx) # connect direct rotations to heel pivot done after creating targets so that # the targets WILL move to reflect magic control non-zero rotations (if any) self.control.r >> self.heelPivot.r # connect blended rotation to toe / ball pivots # use ballToe attr to drive the blend (0 == ball, 1 == toe) toeRotBlendAttr = pulse.utilnodes.blend2(self.control.r, (0, 0, 0), ballToeAttr) ballRotBlendAttr = pulse.utilnodes.blend2((0, 0, 0), self.control.r, ballToeAttr) toeRotBlendAttr >> self.toePivot.r ballRotBlendAttr >> self.ballPivot.r # hide and lock the now-connected pivots for node in (self.toePivot, self.ballPivot, self.heelPivot): node.t.lock() node.r.lock() node.s.lock() node.v.set(False) # create condition to switch between ball/toe and heel pivots # TODO(bsayre): use dot-product towards up to determine toe vs heel isToeRollAttr = pulse.utilnodes.condition(self.control.ry, 0, [1], [0], 2) plantedMtxAttr = pulse.utilnodes.choice(isToeRollAttr, heel_tgt.wm, ballToe_tgt.wm) # connect final planted ankle matrix to ankle target transform pulse.nodes.connectMatrix(plantedMtxAttr, planted_tgt, pulse.nodes.ConnectMatrixMethod.SNAP) planted_tgt.t.lock() planted_tgt.r.lock() planted_tgt.s.lock() planted_tgt.v.setKeyable(False) # create matrix blend between planted and lifted targets # use lift attr to drive the blend (0 == planted, 1 == lifted) plantedLiftedBlendAttr = self.createMatrixBlend( planted_tgt.wm, lifted_tgt.wm, liftAttr, '{0}_mf_plantedLiftedBlend'.format(self.follower.nodeName())) # connect final matrix to follower # TODO(bsayre): this connect eliminates all transform inheritance, is # world space control what we want? or do we need to inject offsets and # allow parent transforms to come through pulse.nodes.connectMatrix(plantedLiftedBlendAttr, _follower, pulse.nodes.ConnectMatrixMethod.SNAP) # create toe up/down matrix blend, (0 == toe-up, 1 == toe-down/ball pivot) # in order to do this, reverse ballToe attr, then multiply by isToeRoll # to ensure toe-down is not active when not using toe pivots # reverse ballToeAttr, so that 1 == toe-down/ball ballToeReverseAttr = pulse.utilnodes.reverse(ballToeAttr) # multiply by isToe to ensure ball not active while using heel pivot isToeAndBallAttr = pulse.utilnodes.multiply(ballToeReverseAttr, isToeRollAttr) # multiply by 1-liftAttr to ensure ball not active while lifting liftReverseAttr = pulse.utilnodes.reverse(liftAttr) toeUpDownBlendAttr = pulse.utilnodes.multiply(isToeAndBallAttr, liftReverseAttr) ballToeMtxBlendAttr = self.createMatrixBlend( toeUp_tgt.wm, toeDown_tgt.wm, toeUpDownBlendAttr, '{0}_mf_toeUpDownBlend'.format(self.toeFollower.nodeName())) # connect final toe rotations to toeFollower # TODO(bsayre): parent both tgts to ankle somehow to prevent locking pulse.nodes.connectMatrix(ballToeMtxBlendAttr, _toeFollower, pulse.nodes.ConnectMatrixMethod.SNAP) # add meta data to controls ctlData = { 'plantedTarget': planted_tgt, 'liftControl': self.liftControl, } meta.setMetaData(self.control, MAGIC_FEET_CTL_METACLASSNAME, ctlData, False) liftCtlData = {'control': self.control} meta.setMetaData(self.liftControl, MAGIC_FEET_LIFT_CTL_METACLASSNAME, liftCtlData, False)
def prepareSpaceConstraint(node, follower, spaceNames): """ Prepare a new space constraint. This sets up the constrained node, but does not connect it to the desired spaces until `createSpaceConstraint` is called. Args: node (PyNode): The node that will contain space switching attrs follower (PyNode): The node that will be constrained, can be `node`, or more commonly, a parent (offset) of `node`. spaceNames (str list): The names of all spaces to be applied """ # setup space switching attr if not node.hasAttr(SPACESWITCH_ATTR): enumNames = ':'.join(spaceNames) node.addAttr(SPACESWITCH_ATTR, at='enum', en=enumNames) spaceAttr = node.attr(SPACESWITCH_ATTR) spaceAttr.setKeyable(True) else: spaceAttr = node.attr(SPACESWITCH_ATTR) nodeName = node.nodeName() offsetChoiceName = nodeName + '_space_offset_choice' spaceChoiceName = nodeName + '_space_choice' multMatrixName = nodeName + '_space_mmtx' decompName = nodeName + '_space_decomp' # create utility nodes offsetChoice = pm.shadingNode('choice', n=offsetChoiceName, asUtility=True) spaceChoice = pm.shadingNode('choice', n=spaceChoiceName, asUtility=True) pulse.utilnodes.loadMatrixPlugin() multMatrix = pm.shadingNode('multMatrix', n=multMatrixName, asUtility=True) decomp = pm.shadingNode('decomposeMatrix', n=decompName, asUtility=True) # setup connections spaceAttr >> offsetChoice.selector spaceAttr >> spaceChoice.selector offsetChoice.output >> multMatrix.matrixIn[0] spaceChoice.output >> multMatrix.matrixIn[1] follower.pim >> multMatrix.matrixIn[2] multMatrix.matrixSum >> decomp.inputMatrix # connection from decomp output to the follower trs # occurs after the actual space nodes are connected spaceData = [] # native space indeces always take priority, # which means dynamic spaces may be adversely affected # if the native spaces change on a published rig # TODO: ability to reindex dynamic spaces for i, spaceName in enumerate(spaceNames): spaceData.append({ 'name': spaceName, # TODO: is `switch` needed anymore? 'switch': None, 'index': i, }) data = { # native spaces in this constraint 'spaces': spaceData, # dynamic spaces (added during animation), which may be # from the native rig, or from an external one 'dynamicSpaces': [], # transform that is actually driven by the space constraint 'follower': follower, # the utility nodes that make up the space constraint 'offsetChoice': offsetChoice, 'spaceChoice': spaceChoice, 'multMatrix': multMatrix, 'decompose': decomp, } meta.setMetaData(node, SPACECONSTRAINT_METACLASS, data)
def prepareSpaceConstraint(node, follower, spaceNames): """ Prepare a new space constraint. This sets up the constrained node, but does not connect it to the desired spaces until `createSpaceConstraint` is called. Args: node (PyNode): The node that will contain space switching attrs follower (PyNode): The node that will be constrained, can be different than the control. spaceNames (str list): The names of all spaces to be applied """ data = { # native spaces in this constraint 'spaces': [], # dynamic spaces (added during animation), which may be # from the native rig, or from an external one 'dynamicSpaces': [], # transform that is actually constrained 'constrainedNode': follower, # the addition utility nodes internal to the constraint 'translateAdd': None, 'rotateAdd': None, 'scaleAdd': None, } # native space indeces always take priority, # which means dynamic spaces may be adversely affected # if the native spaces change on a published rig # TODO: ability to reindex dynamic spaces for i, spaceName in enumerate(spaceNames): data['spaces'].append({ 'name': spaceName, 'switch': None, 'index': i, }) # create addition utilities for transform attrs addUtilities = [ ('t', 'translateAdd'), ('r', 'rotateAdd'), ('s', 'scaleAdd'), ] for attrName, metaKey in addUtilities: # create addition utility defaultVal = 1 if (attrName == 's') else 0 # create add node with first item set to defaults addNode = pulse.utilnodes.add( (defaultVal, defaultVal, defaultVal)).node() addNode.rename('{0}_add{1}'.format( node.nodeName(), attrName.upper())) # connect to the follower node addNode.output3D >> follower.attr(attrName) # store reference to utility in meta data data[metaKey] = addNode meta.setMetaData(node, SPACECONSTRAINT_METACLASS, data) # setup space switching attr if not node.hasAttr(SPACESWITCH_ATTR): enumNames = ':'.join(spaceNames) node.addAttr(SPACESWITCH_ATTR, at='enum', en=enumNames) sattr = node.attr(SPACESWITCH_ATTR) sattr.setKeyable(True)
def run(self): # add attrs # --------- self.control.addAttr('roll', at='double', keyable=True) roll = self.control.attr('roll') self.control.addAttr('tilt', at='double', keyable=True) tilt = self.control.attr('tilt') self.control.addAttr('toeSwivel', at='double', keyable=True) toeSwivel = self.control.attr('toeSwivel') self.control.addAttr('heelSwivel', at='double', keyable=True) heelSwivel = self.control.attr('heelSwivel') self.control.addAttr('bendLimit', at='double', keyable=True, defaultValue=self.bendLimitDefault, minValue=0) bend_limit = self.control.attr('bendLimit') self.control.addAttr('straightAngle', at='double', keyable=True, defaultValue=self.straightAngleDefault, minValue=0) straight_angle = self.control.attr('straightAngle') # keep evaluated Bend Limit below Straight Angle to avoid zero division and flipping problems clamped_bend_limit = utilnodes.min_float( bend_limit, utilnodes.subtract(straight_angle, 0.001)) # setup hierarchy # --------------- # control > heel > outerTilt > innerTilt > toe > ball offset_connect_method = pulse.nodes.ConnectMatrixMethod.CREATE_OFFSET pulse.nodes.connectMatrix(self.control.wm, self.heelPivot, offset_connect_method) pulse.nodes.connectMatrix(self.heelPivot.wm, self.outerTiltPivot, offset_connect_method) pulse.nodes.connectMatrix(self.outerTiltPivot.wm, self.innerTiltPivot, offset_connect_method) pulse.nodes.connectMatrix(self.innerTiltPivot.wm, self.toePivot, offset_connect_method) pulse.nodes.connectMatrix(self.toePivot.wm, self.ballPivot, offset_connect_method) # ballPivot > ankleFollower pulse.nodes.connectMatrix(self.ballPivot.wm, self.ankleFollower, offset_connect_method) # toePivot > toeFollower pulse.nodes.connectMatrix(self.toePivot.wm, self.toeFollower, offset_connect_method) # calculate custom foot attrs # --------------------------- # drive heel rotation with negative footRoll heel_roll = utilnodes.clamp(roll, -180, 0) heel_roll >> self.heelPivot.rotateX # get percentage blend between 0..BendLimit and BendLimit..StraightAngle zero_to_bend_pct = utilnodes.setRange(roll, 0, 1, 0, clamped_bend_limit) bend_to_toe_pct = utilnodes.setRange(roll, 0, 1, clamped_bend_limit, straight_angle) # multiply pcts to get ball rotation curve that goes up towards BendLimit, # then back down towards StraightAngle neg_bend_to_toe_pct = utilnodes.reverse(bend_to_toe_pct) ball_roll_factor = utilnodes.multiply(zero_to_bend_pct, neg_bend_to_toe_pct) ball_roll = utilnodes.multiply(ball_roll_factor, clamped_bend_limit) ball_roll >> self.ballPivot.rotateX # multiply bend pct to get toe rotation coming from bend, then # add in extra rotation to add after straight angle is reached, so that foot roll isn't clamped toe_roll_from_blend = utilnodes.multiply(bend_to_toe_pct, straight_angle) toe_roll_excess = utilnodes.clamp( utilnodes.subtract(roll, straight_angle), 0, 180) toe_roll = utilnodes.add(toe_roll_from_blend, toe_roll_excess) toe_roll >> self.toePivot.rotateX # TODO: mirror swivel values (since roll cannot be flipped, we can't just re-orient the pivot nodes) # swivels are mostly direct, toe swivel positive values should move the heel outwards utilnodes.multiply(toeSwivel, -1) >> self.toePivot.rotateZ heelSwivel >> self.heelPivot.rotateZ # inner and outer tilt simply need clamped connections outer_tilt = utilnodes.clamp(tilt, 0, 180) outer_tilt >> self.outerTiltPivot.rotateY inner_tilt = utilnodes.clamp(tilt, -180, 0) inner_tilt >> self.innerTiltPivot.rotateY # lock up nodes # ------------- for pivot in [ self.toePivot, self.ballPivot, self.outerTiltPivot, self.innerTiltPivot, self.heelPivot ]: pivot.v.set(False) for a in ('tx', 'ty', 'tz', 'rx', 'ry', 'rz', 'sx', 'sy', 'sz'): attr = pivot.attr(a) attr.setLocked(True) attr.setKeyable(False) # setup meta data # --------------- foot_ctl_data = { 'foot_ctl': self.control, 'ball_ctl': self.ballControl, 'ankle_follower': self.ankleFollower, 'toe_follower': self.toeFollower, } meta_nodes = {self.control, self.ballControl} meta_nodes.update(self.extraControls) for node in meta_nodes: meta.setMetaData(node, FOOT_CTL_METACLASSNAME, foot_ctl_data)