Sunday, August 12, 2018

Pin Control Script

Script Description:

The next script I worked on which I once again wrote for both Maya and Motion Builder was a simple tool for pinning controls. It’s an animation tool to be used on IK controls to hold them in the same place over a period of time. It also does some blending after the end of the hold to smooth out the transition back to the animation.

UI:

For this script I wrote a very basic UI. I wanted to give the user the ability to know what control they were selecting and then have a button for pinning and another button for baking down the animation layer with the pinning into the base animation layer. 






Functions: 

One of the challenges with this script was that because the keys that determine pinning are made on a separate animation layer that modifies the base animation layer, I had to be sure that they were set in a range that had keys on the base animation layer. I started by using the Motion Builder system functions to swap the current layer to the base animation layer to allow me to work off of it. Once I had the animation nodes I took the first node and got the time for both the start and end frame of the base animation. I then compared them to the times of the keys set on the animation layer. If they did not fall in the range of the keys on the base animation layer then I gave the user a warning and returned that the time was not valid otherwise I return that the time is valid. 


def timeCheck(self):
 timeValid = True
 self.system.CurrentTake.SetCurrentLayer(0)
 self.baseAnimNodes = self.getAnimNodes()
 #finds the first key on the base animation curve
 firstFrame = self.baseAnimNodes[0].Nodes[0].FCurve.Keys[0].Time
 #finds the last key on the base animation curve
 lastFrame = self.baseAnimNodes[0].Nodes[0].FCurve.Keys[len(self.baseAnimNodes[0].Nodes[0].FCurve.Keys)-1].Time
 #checks that the keys to be edited are in range of the base animation
 if self.startTime < firstFrame or  self.endTime > lastFrame:
  utils.makeWarning('Frame range is not valid.')
  timeValid = False
   
 return timeValid






This was how I checked that the time was valid in Maya. I used the animation layer class in Maya to get the nodes for the animation layer that the user set keys on. Then using the keyframe function from pymel in query mode with the tc flag for time change to get a list of times that the layer has keys set on it. I then repeated the process for the base layer though I used the getBaseAnimCurves function from the animation layer class to find the animation nodes and be sure that they are from the base animation layer. Once I have both like in the other version of the script I compare times to be sure that the keys chosen are valid. 


#checks that user frame range is within the range of frames on the control 
def isValid(self):
 valid = True 
 #gets nodes for pin layer
 self.layerNodes =  self.affectingLayers[0].getAnimCurves()
 layerKeyTimesList = pm.keyframe(self.layerNodes[0], query = True, tc = True)
 #finds the first frame that the control is keyed on 
 self.firstFrame = layerKeyTimesList[0]
 #finds the last frame the control is keyed on 
 self.lastFrame = layerKeyTimesList[len(layerKeyTimesList)-1]
 
 #gets nodes for base layer
 self.baseAnimNode = self.affectingLayers[0].getBaseAnimCurves()
 baseKeyTimes = pm.keyframe(self.baseAnimNode[0], query = True, tc = True)
 #finds first frame
 self.baseFirstFrame = baseKeyTimes[0]
 #finds last frame
 self.baseLastFrame = baseKeyTimes[len(baseKeyTimes) -1] 
 
 #checks that frames on pin layer are in range of the first and last frame on base layer
 if self.firstFrame < self.baseFirstFrame or self.lastFrame > self.baseLastFrame:
  timeInvalidWarning = QtWidgets.QMessageBox()
  timeInvalidWarning.setWindowTitle('Warning')
  timeInvalidWarning.setText('Frame range is not valid.')
  timeInvalidWarning.exec_()
  valid = False
 
 return valid  






The tricky part of this script was figuring out how Motion Builder handles animation layers and then using that information to set keys to pin the control. What I found was that I needed the difference between the first frame which was the one being held and the current frame on the base animation layer. Then I set a key on the frame with the value of the difference causing it to match the start frame. 


#this function sets keys pinning the control in place     
def setKeyValue(self):
 #gets anim nodes for translation and rotation 
 self.animLayerNodes = self.getAnimNodes()
 #gets FBTime objects for given frames 
 self.startTime = self.animLayerNodes[0].Nodes[0].FCurve.Keys[0].Time
 self.endTime = self.animLayerNodes[0].Nodes[0].FCurve.Keys[len(self.animLayerNodes[0].Nodes[0].FCurve.Keys)-1].Time
 
 #gets frame numbers for the keys
 self.startFrame = self.startTime.GetFrame()
 self.endFrame = self.endTime.GetFrame() 
 
 #checks that given frames are in a valid time range
 frameNum = self.frameNumberCheck()
 valid = self.timeCheck() 

  
 if valid and frameNum:
  #FBTime to be incremented in the for loop 
  incTime = self.startTime
  #sets new keys in a loop
  for key in range(self.startFrame, self.endFrame):
   #adds one frame to current time 
   incTime += FBTime(0,0,0,1,0)
   #sets new keys for all three fcurves for rotation and translation  
   for i in range(2):
    for j in range(3):
     self.system.CurrentTake.SetCurrentLayer(0)
     firstValue = self.getKeyValue(i, j, self.startTime, self.baseAnimNodes)
     currentValue = self.getKeyValue(i, j, incTime, self.baseAnimNodes)
     setValue = firstValue - currentValue 
     self.system.CurrentTake.SetCurrentLayer(self.numLayers-1)
     self.animLayerNodes[i].Nodes[j].FCurve.KeyAdd(incTime, setValue)
  #blend end of hold
  self.zeroKey()







Setting keys worked very differently in Maya because instead of just setting keys with values that were the difference between the two keys I had to find the difference then add it to the start frame value to get the value to set my pin keys at. 

def setKeys(self):
 numKeysValid = self.frameNumberCheck()
 #checks to see if frame range is valid 
 valid = self.isValid()
 
 #if the frame range is valid 
 if valid == True and numKeysValid == True:
  #gets the value from the first hold frame 
  self.startValues = self.getKeyValue(self.firstFrame, self.baseAnimNode)
  incTime = self.firstFrame
  for key in range(int(self.firstFrame), int(self.lastFrame)):
   incTime += 1.0
   self.currentValues = self.getKeyValue(incTime, self.baseAnimNode)
   list = []
   #list of possible control attributes
   self.attList = ['visablitiy','translateX', 'translateY', 'translateZ', 'rotateX', 'rotateY', 'rotateZ', 'scaleX', 'scaleY', 'scaleZ']
   for v in range(10):
    value = (self.startValues[v] - self.currentValues[v]) + self.currentValues[v]
    
    if value == 0:
     value = self.startValues[v]
    list.append(value)
    pm.setKeyframe(t = int(incTime), al = self.affectingLayers[0], at = self.attList[v], v = value)
     
  #adds a transtion key between end of hold and the rest of the animation     
  self.zeroKey()






To blend the end of the hold with the rest of the animation I wrote this function to set a zero key five frames after the end of the hold on the new layer. In Motion Builder this was fairly easy to do. I set a key with values that match the value of the start frame on each of the different FCurves. 

#adds a base key at the end of the hold to blend into the rest of the animation     
def zeroKey(self):
 #gets the key to adjust 
 setKey = self.endTime + FBTime(0,0,0,5,0) 

 #sets a key with the same value as the first animation layer key
 for i in range(2):
  for j in range(3):
   setValue = self.getKeyValue(i, j, self.startTime, self.animLayerNodes)
   self.animLayerNodes[i].Nodes[j].FCurve.KeyAdd(setKey,setValue)






Setting zero keys in Maya works very differently than in motion builder. In order to set one in Maya I had to get the value of the key at the current time on the base animation layer and then set a key with the same value on the animation layer that I was pinning the control from. 

#sets a zero key five frames after the hold to create a blend     
def zeroKey(self):
 self.zeroValues = self.getKeyValue(self.lastFrame + 5.0, self.baseAnimNode)
 for v in range(10):
    pm.setKeyframe(t = self.lastFrame + 5.0, al = self.affectingLayers[0], at = self.attList[v], value = self.zeroValues[v])

No comments:

Post a Comment