Sunday, August 12, 2018

Create Control Rig Script

Script Description:

This script I wrote for generating control rigs for skinning skeletons. It allows the user to make control chains that are either FK, IK or an FK/IK switch. Once the user has made some controls the second half the UI is for assembling the pieces into a cohesive rig. The user can make a world control and cog control. They can also parent controls together and parent all top level controls to the world control.


UI:


The UI for this script is divided into two parts. The top of the UI is for making controls and the bottom is for assembling those controls into a complete rig.








Functions: 

In order to build the controls I had to be able to duplicate joints to be control joints. One of the challenges I ran into with this was that originally I had been duplicating the parent joint with its children. The problem was that it meant that lots of extra joints were being created. For example, if I was trying to make the arm controls the hand would get duplicated as well. As a solution I ended up duplicating only the parent joint without the children and then making a list of the duplicated joints. Once I had that list I parented them together to match the skinning skeleton hierarchy. Giving me the joints I needed without anything extra.

#duplicates joints based off given joint list
def duplicateJoints(self, jointList, prefix):
 dupJointList = []
 #duplicates all joints in joint list without their children and adds them to a list
 for joint in jointList:
  dupJoint = pm.duplicate(joint, name = prefix + str(joint), po = True)
  pm.parent(dupJoint[0], world = True)
  dupJointList.append(dupJoint)
 
 #parents the joints together     
 for i in range(len(dupJointList) -1):
  prnt = dupJointList[i +1]
  child = dupJointList[i]
  self.parentObject(prnt[0], child[0])
  
 #hides the created joints to clean up the viewport
 pm.hide(dupJointList[len(dupJointList) - 1])
 
 #returns the duplicated joints
 return dupJointList






One of the more complicated things to write for this script was the function for making the IK chains. One of the reasons was that IK chains have to be made in a chain of joints that is at least two joints in length so right off the bat I had to make a check for if the start joint was equal to the end joint and then give the user a warning if they were. 

The next challenge was figuring out how to place the pole vectors. Ideally they are out in space from the center of the chain. So I gave the user the ability to specify the middle joint in the chain as well as a set of radio buttons to specify if the pole vector should be in front of, behind, to the right of, or to the left of the model. For placing the pole vector I assumed z positive was forward. Then using the world space location of the middle joint along with where the user specified the pole vector should go in relation to the model, I placed the pole vector in the world. Once it's placed I constrain it to the handle with a pole vector constraint. 

The other thing I had to solve was the rotation at the end of the IK chain. To do this I looked for the first child joint of the end joint in the chain. Duplicate the child joint to make an IK joint and then make a chain from the end of the IK chain to the child joint. Once the handle is made I parent it to the IK handle for the entire chain thus enabling the IK for the original chain to rotate. Since I can't be sure that there is a child joint if I can't find one I give the user a warning saying that the end of the chain won't be able to rotate. I leave the IK chain in the scene since the translation is still functional and depending on the users needs it might work just fine.

#makes Ik handle for given start and end joints
def makeIk(self, ctrlType):
 if self.startJoint == self.endJoint:
  ikChainWarning = QtWidgets.QMessageBox()
  ikChainWarning.setWindowTitle('Warning')
  ikChainWarning.setText('Can\'t make an Ik chain one joint in length. Please select two different joints to continue.')
  ikChainWarning.exec_()
  #deletes the created control joints that were made by the make controls function
  ctrlJoint = self.findCtrlJoint(self.startJoint)
  pm.delete(ctrlJoint)
 else:
  #makes joints to be driven by ik controls directly 
  ikJoints = self.duplicateJoints(self.jointList, 'IK_')
  #for the joints in the list of ikJoints
  for j in range(len(ikJoints)):
   #if not being used for an IK/FK switch constrain control joints
   if self.switch == False:
    ctrlJoint = self.findCtrlJoint(self.jointList[j])
    self.parentConstrainObject(ikJoints[j], ctrlJoint)
   
  #makes the Ik handle
  handle = pm.ikHandle(sj = ikJoints[len(ikJoints) - 1][0], ee = ikJoints[0][0])
  #hides the handle to clean up the veiwport
  pm.hide(handle[0])
 
  #makes a control 
  ctrl = self.getSetPostion((10.0 * self.scaleFactor), 'IK_' + str(self.endJoint), self.endJoint, ctrlType)
  
  #hides attributes on the ik control that the user doesn't need
  attrList = ['scaleX', 'scaleY', 'scaleZ', 'visibility']
  for attr in attrList:
   pm.setAttr(str(ctrl[1][0]) + '.' + attr,  k = False, cb = False)
  
  #parents the IK handle under the ctrl 
  self.parentObject(ctrl[1][0], handle[0])
  
  #makes a control for the pole vector 
  poleCtrl = self.getSetPostion((5.0 * self.scaleFactor), 'IK' + str(self.middleJoint) + '_Pole_Vector', self.middleJoint, ctrlType)
  
  #hides attributes the pole vector control doesn't need 
  for attr in attrList:
   pm.setAttr(str(poleCtrl[1][0]) + '.' + attr,  k = False, cb = False)
  
  #finds offset group for the pole vector control
  children = pm.listRelatives(poleCtrl[0], ad = True)
  offset = children[len(children)-1]
  
  #gets the world space postion of the joint to use for placing the pole vector 
  jointTrans = pm.xform(self.middleJoint, query = True, ws = True, t = True)
  #determines the x, y and z values to move the pole vector control based off user selection 
  #if the user selected front 
  if self.poleVectorPostion == -2:
   x = jointTrans[0]
   y = jointTrans[1]
   z = (100 * self.scaleFactor)
  #if the user selected back
  elif self.poleVectorPostion == -3:
   x = jointTrans[0]
   y = jointTrans[1]
   z = (-100 * self.scaleFactor)
  #if the user selcted right 
  elif self.poleVectorPostion == -4:
   x = (-100 * self.scaleFactor)
   y = jointTrans[1]
   z = jointTrans[2]
  #if the user selcted left 
  elif self.poleVectorPostion == -5:
     x = (100 * self.scaleFactor)
     y = jointTrans[1]
     z = jointTrans[2]
    #if the user didn't make a selection gives a warning 
  else:
     poleVectorPostionWarning = QtWidgets.QMessageBox()
     poleVectorPostionWarning.setWindowTitle('Warning')
     poleVectorPostionWarning.setText('No pole vector postion selected. Please select a pole vector postion to continue.')
     poleVectorPostionWarning.exec_()
    
     #removes non functioning partial rig
     pm.delete(ikJoints)
     pm.delete(poleCtrl)
     pm.delete(ctrl)
     ctrlJoint = self.findCtrlJoint(self.startJoint)
     pm.delete(ctrlJoint)
     return
  
  #moves the pole vector
  pm.xform(offset, ws = True, t = (x,y,z))
  #constrains the ik handle to the pole vector 
  pm.poleVectorConstraint(poleCtrl[1][0], handle[0])
     
  #makes a control for the top of the ik chain and constrains the ik chain to it 
  topCtrl = self.getSetPostion((10.0 * self.scaleFactor), 'Ik_' + str(self.startJoint), self.startJoint, ctrlType)
  self.parentConstrainObject(topCtrl[1][0], ikJoints[len(ikJoints) - 1])
  
  #locks and hides unnecessary attributes from the control
  for attr in attrList:
   pm.setAttr(str(topCtrl[1][0]) + '.' + attr, k = False, cb = False)
     
  #tries to find a child joint to make a secondary ik handle to enable rotation of the end of the ik chain
  try:
   #finds the children of the end joint 
   childJoint = pm.listRelatives(self.endJoint, c = True, type = 'joint')[0]
   childJointList = [childJoint]
   
   #makes an ik joint and parents it to the rest of the ik chain 
   ikChildJoint = self.duplicateJoints(childJointList, 'IK_')
   self.parentObject(ikJoints[0][0], ikChildJoint[0])
 
   #makes an ik handle and parents it the other handle to enable rotation at the end of the ik chain 
   ikChildHandle = pm.ikHandle(sj = ikJoints[0][0], ee = ikChildJoint[0][0])
   self.parentObject(handle[0], ikChildHandle[0])
   
   #hides the ik handles 
   pm.hide(ikChildHandle[0])
  
  #if there are no child joints lets the user know that rotation won't work but leaves the control    
  except:
   noChildWarning = QtWidgets.QMessageBox()
   noChildWarning.setWindowTitle('Warning')
   noChildWarning.setText('No child joints after the end of the IK chain. The control on the end of the chain will not be able to rotate.')
   noChildWarning.exec_()

  #returns the list of ik joints and all the controls for use in ik/fk switching function
  return ikJoints, ctrl, poleCtrl, topCtrl






To help keep making FK chains simple I divided it up into two functions. One for making the FK controls and one for assembling the chain. The make control function handles placing the controls, and constraining the FK joints to them. 


#this function sets up an fk control given a skinning joint 
def makeFKCtrl(self, skJoint, fkJoint, name, ctrlType):
 joint = self.findCtrlJoint(skJoint)
 
 #if this chain is not for an IK/FK switch parent constrain control joint to the fk joint 
 if self.switch == False:
  #constrains the control joint to the fk joint 
  self.parentConstrainObject(fkJoint, joint)
 
 #makes a control and places it on the fk joint
 ctrl = self.getSetPostion((10.0 * self.scaleFactor), name, fkJoint, ctrlType)
 #constrains the fk joint to the fk control 
 self.parentConstrainObject(ctrl[1], fkJoint)
 
 #hides unnecessary attributes on the control 
 attrList = ['translateX', 'translateY', 'translateZ', 'scaleX', 'scaleY', 'scaleZ', 'visibility']
 for attr in attrList:
  pm.setAttr(str(ctrl[1][0]) + '.' + attr, k = False, cb = False)
 
 return ctrl






The FK chain function calls the make FK controls function the right number of times and parents the controls together to make the chain.


#makes a chain of fk controls for all the specified joint and it's children     
def makeFKChain(self, ctrlType):
 #makes joints to be driven by fk controls directly 
 fkJoints = self.duplicateJoints(self.jointList, 'FK_')

 #ctrlList will contain ctrl and prnt groups 
 #starting with bottom of the chain 
 self.ctrlList = []
 #makes controls and adds there prnt groups and ctrls to a list
 for joint in range(len(fkJoints)):
  childName = str(fkJoints[joint][0])
  childCtrl = self.makeFKCtrl(self.jointList[joint], fkJoints[joint], childName, ctrlType)
  self.ctrlList.append(childCtrl)
 
 #parents the fk controls together 
 for i in range(len(self.ctrlList)-1):
  prnt = self.ctrlList[i +1]
  child = self.ctrlList[i]
  self.parentObject(prnt[1][0], child[0])
  
 return fkJoints

 






Like the FK function I divided making the IK/FK switch into two different functions one for making a control to hold the IK/FK switch attribute and one for connecting attributes and placing the control in the world.  With the function that makes the control I use the setAttr function to lock and hide all the attributes on the control since it's just made for holding the attribute to switch the mode of the joint chain from FK to IK. I then use the addAttr function to add a float attribute to be the IK/FK switch attribute. 

#makes a control to hold an IK, FK switch 
def makeIkFkSwitchCtrl(self, name, ctrlType):
 ctrl = self.makeCtrl((5.0 * self.scaleFactor), name, ctrlType)
 #gets all the keyable attributes(ones found in channel box)
 channelAttr =  pm.listAttr(ctrl[1][0], k = True)
 #this locks and hides the attributes 
 for attr in channelAttr:
  pm.setAttr(ctrl[1][0] + '.' + attr, l = True, k = False, cb = False)
 #adds attribute for IK/FK switch      
 pm.addAttr(ctrl[1][0], ln = 'IK_FK_Switch', at = 'float', max = 1.0, min = 0.0, k = True)
 return ctrl
 






I made use of a reverse node that I made with the shadingNode pymel function to make the FK input opposite the IK input since it reverses the value of the input. For example, if the switch attribute is set to 1 the reverse node gives off the value of 0. Once I made one I connected both the visibility of the controls and the weight of the IK and FK joints using the connectAttr function. I connected the IK directly to the switch attribute and the FK to the value from the reverse node. I also place the IK/FK switch on the opposite side of the body from the pole vector so that it would be placed with a predictable pattern. 


#makes an Ik/Fk switch     
def ikFkSwitch(self, fkJoint, IkJoint, skJoint):
 #makes a control for the ik/fk switch attribute
 ctrl = self.makeIkFkSwitchCtrl('Switch', 'Extra')

 #gets the Ik/Fk switch attribute off the Ik/Fk control
 switchAttr =  pm.listAttr(ctrl[1][0], c = True, k = True)[0]

 #constrains the rest of the joints and keeps track of them in a list
 constraintList = []
 for j in range(len(fkJoint)):
  ctrlJoint = self.findCtrlJoint(self.jointList[j])
  childC = pm.parentConstraint(fkJoint[j], IkJoint[0][j], ctrlJoint)
  constraintList.append(childC)

 #make a reverse node
 reverseNode = pm.shadingNode('reverse', asUtility = True)
 #connect the switch attribute to the input of the reverse node 
 pm.connectAttr(ctrl[1][0] + '.' + switchAttr, reverseNode + '.input.inputX')
   
 #connect the constraint weights to the fk/ik switch 
 #also connects control visibility to the switch 
 for constraint in range(len(constraintList)):
  attributes = pm.listAttr(constraintList[constraint], c = True, k = True)
  pm.connectAttr(ctrl[1][0] + '.' + switchAttr, constraintList[constraint] + '.' + attributes[len(attributes) -1])
  pm.connectAttr(reverseNode + '.output.outputX', constraintList[constraint] + '.' + attributes[len(attributes) -2]) 
  pm.connectAttr(reverseNode + '.output.outputX', self.ctrlList[constraint][1][0] + '.visibility') 
  pm.connectAttr(ctrl[1][0] + '.' + switchAttr, IkJoint[constraint + 1][0] + '.visibility')

 #positions the Ik/Fk switch opposite to the position of the pole vector
 ctrlJoint = self.findCtrlJoint(skJoint)
 self.parentConstrainObject(ctrlJoint, ctrl[0])
 child = pm.listRelatives(ctrl[0], c = True, type = 'transform')
 offset = child[0]
 ctrlJointTrans = pm.xform(ctrlJoint, query = True, ws = True, t = True)
 #if the user picked front
 if self.poleVectorPostion == -2:
  x = ctrlJointTrans[0]
  y = ctrlJointTrans[1]
  z = (-20 * self.scaleFactor)
 #if the user picked back 
 elif self.poleVectorPostion == -3:
  x = ctrlJointTrans[0]
  y = ctrlJointTrans[1]
  z = (20 * self.scaleFactor)
 #if the user picked right
 elif self.poleVectorPostion == -4:
  x = (20 * self.scaleFactor)
  y = ctrlJointTrans[1]
  z = ctrlJointTrans[2]
 #if the user picked left
 else:
  x = (-20 * self.scaleFactor)
  y = ctrlJointTrans[1]
  z = ctrlJointTrans[2]
 #places the switch in the world
 pm.xform(offset, ws = True, t = (x, y, z))

No comments:

Post a Comment