PyQt: Maya Character Picker

Example of a fully featured character picker using the magic of PySide

Texture Based Deformer

Deform a mesh based on the colour values derived from a procedural texture

Visibility Node v2.0

A tool to help visualise hidden mesh objects by utilising componentModifiers

UV Based Blendshape Conversion

Convert blendshape targets on meshes with differing topologies

Python and PYQT Image Compare Tool

Investigation into writing a standalone application that can be compiled and run within Windows

Monday, 15 May 2017

Soft IK Constraint

The Maya IK Solver is a rigging staple but it does have a few shortcomings. Among them is the angle between the two bones appearing to snap when getting close to zero. This can negatively affect animation and it's not easy to manage.
In comes Soft IK.
It's not a new concept but one worth some investigation especially as far as I am aware it is still not part of core Maya. This node piggy backs on the standard IK Solver basically tracking the position of a driving transform but applying a function curve over the top that gradually attenuates the movement based on an input value. When looking into this I found a great site that showed a solution that had been worked on for XSI. Check this out here.
As ever I have included a python implementation of the node to try out with some support code.

Soft_IK_Constraint from SBGrover on Vimeo.

Python plugin

 import maya.cmds as cmds  
 import maya.OpenMaya as OpenMaya  
 import maya.OpenMayaMPx as omMPx  
 import math  
   
 kPluginNodeTypeName = "SoftIKConstraint"  
 kPluginNodeClassify = 'utility/general'  
 kPluginNodeId = OpenMaya.MTypeId(0x81081)  
   
  
 class SoftIKConstraint(omMPx.MPxConstraint):   
   
      matA = OpenMaya.MObject()  
      matB = OpenMaya.MObject()  
      matC = OpenMaya.MObject()  
      matInv = OpenMaya.MObject()  
      soft = OpenMaya.MObject()  
      output = OpenMaya.MObject()  
      pointX = OpenMaya.MObject()  
      pointY = OpenMaya.MObject()  
      pointZ = OpenMaya.MObject()  
      magRetrieved=0  
      chainLength=0  
      #e=2.71828  
        
      def __init__(self):  
   
           omMPx.MPxConstraint.__init__(self)  
             
      def compute(self,plug,data):  
             
           if plug == SoftIKConstraint.output:  
   
                softVal = data.inputValue(SoftIKConstraint.soft).asFloat()  
                inverseWorldMatrix = data.inputValue(SoftIKConstraint.matInv).asMatrix()  
                transformAWorldMatrix = data.inputValue(SoftIKConstraint.matA).asMatrix()  
                transformBWorldMatrix = data.inputValue(SoftIKConstraint.matB).asMatrix()  
                       
                vecAwm = OpenMaya.MPoint() * transformAWorldMatrix  
                vecBwm = OpenMaya.MPoint() * transformBWorldMatrix  
                vecAB = vecAwm-vecBwm  
                currentLength=getMagnitude(vecAB)  
                  
                if self.magRetrieved == 0:  
                     transformCWorldMatrix = data.inputValue(SoftIKConstraint.matC).asMatrix()  
                     vecCwm = OpenMaya.MPoint() * transformCWorldMatrix  
                     vecAC = vecAwm-vecCwm  
                     vecCB = vecCwm-vecBwm  
                     self.chainLength = getMagnitude(vecAC) + getMagnitude(vecCB)   
                     self.magRetrieved = 1  
                  
                if softVal == 0:  
                     ratioOfLength=1  
                else:  
                     affectedLength=self.chainLength*softVal  
                     unnafectedLength=self.chainLength-affectedLength  
                       
                     #create a falloff based on where the unnafected length ends. The function creates a curve all the way from 0 to max length but the result is forced to become linear where the unnafected length applies.  
                     if currentLength <= unnafectedLength:  
                          fractionOfLength = currentLength  
                     else:  
                          fractionOfLength = affectedLength*(1-math.exp((unnafectedLength - currentLength)/affectedLength))+unnafectedLength       
                     ratioOfLength = fractionOfLength / currentLength  
                worldPosition = ((vecAwm * (1.0 - ratioOfLength)) + OpenMaya.MVector(vecBwm * ratioOfLength))  
                localPosition = worldPosition * inverseWorldMatrix  
   
                outHandle = data.outputValue(SoftIKConstraint.output)  
                outHandleX = outHandle.child(SoftIKConstraint.pointX)  
                outHandleY = outHandle.child(SoftIKConstraint.pointY)  
                outHandleZ = outHandle.child(SoftIKConstraint.pointZ)  
                outHandleX.setMDistance(OpenMaya.MDistance(localPosition.x))  
                outHandleY.setMDistance(OpenMaya.MDistance(localPosition.y))  
                outHandleZ.setMDistance(OpenMaya.MDistance(localPosition.z))  
   
                data.setClean(plug)  
           else:  
                return OpenMaya.kUnknownParameter  
   
 def getMagnitude(vector):  
      magnitude=math.sqrt((vector.x*vector.x) + (vector.y*vector.y) + (vector.z*vector.z))  
      return magnitude  
        
 def nodeCreator():  
   
      return omMPx.asMPxPtr(SoftIKConstraint())  
        
 def nodeInitializer():   
        
      inputMatrix = OpenMaya.MFnMatrixAttribute()  
      softAttr = OpenMaya.MFnNumericAttribute()  
      outputAttr = OpenMaya.MFnUnitAttribute()  
      compoundOutputAttr = OpenMaya.MFnNumericAttribute()  
   
      SoftIKConstraint.matB = inputMatrix.create("HandleDriverWorldMatrix","hcwm")  
      inputMatrix.setHidden(1)  
      SoftIKConstraint.matA = inputMatrix.create("ChainDriverWorldMatrix","cdwm")  
      inputMatrix.setHidden(1)  
      SoftIKConstraint.matC = inputMatrix.create("MiddleJointWorldMatrix","mjwm")  
      inputMatrix.setHidden(1)  
      SoftIKConstraint.matInv = inputMatrix.create("HandleParentInverseMatrix","hpim")  
      inputMatrix.setHidden(1)  
      SoftIKConstraint.soft = softAttr.create("SoftIKValue","sik",OpenMaya.MFnNumericData.kFloat,0)  
      softAttr.setMin(0)  
      softAttr.setMax(1)  
      softAttr.setChannelBox(1)  
        
      #scene scale independent output  
      SoftIKConstraint.pointX = outputAttr.create("outputX", "outx",OpenMaya.MFnUnitAttribute.kDistance,0)  
      outputAttr.setWritable(0)  
      SoftIKConstraint.pointY = outputAttr.create("outputY", "outy",OpenMaya.MFnUnitAttribute.kDistance,0)  
      outputAttr.setWritable(0)  
      SoftIKConstraint.pointZ = outputAttr.create("outputZ", "outz",OpenMaya.MFnUnitAttribute.kDistance,0)  
      outputAttr.setWritable(0)  
      SoftIKConstraint.output = compoundOutputAttr.create("Output", "out",SoftIKConstraint.pointX,SoftIKConstraint.pointY,SoftIKConstraint.pointZ)  
      compoundOutputAttr.setWritable(0)  
      compoundOutputAttr.setHidden(1)    
   
      SoftIKConstraint.addAttribute(SoftIKConstraint.matA)  
      SoftIKConstraint.addAttribute(SoftIKConstraint.matB)  
      SoftIKConstraint.addAttribute(SoftIKConstraint.matC)  
      SoftIKConstraint.addAttribute(SoftIKConstraint.matInv)  
      SoftIKConstraint.addAttribute(SoftIKConstraint.soft)  
      SoftIKConstraint.addAttribute(SoftIKConstraint.output)  
        
      SoftIKConstraint.attributeAffects(SoftIKConstraint.matA,SoftIKConstraint.output)  
      SoftIKConstraint.attributeAffects(SoftIKConstraint.matB,SoftIKConstraint.output)  
      SoftIKConstraint.attributeAffects(SoftIKConstraint.matC,SoftIKConstraint.output)  
      SoftIKConstraint.attributeAffects(SoftIKConstraint.matInv,SoftIKConstraint.output)  
      SoftIKConstraint.attributeAffects(SoftIKConstraint.soft,SoftIKConstraint.output)  
   
 def initializePlugin(mobject):  
      print "> Initialising SoftIK Plugin"  
      fnPlugin = omMPx.MFnPlugin(mobject)  
      fnPlugin.registerNode(kPluginNodeTypeName,kPluginNodeId,nodeCreator,nodeInitializer,omMPx.MPxNode.kDependNode,kPluginNodeClassify)  
        
 def uninitializePlugin(mobject):  
      print "> Uninitialising SoftIK Plugin"  
      fnPlugin = omMPx.MFnPlugin(mobject)  
      fnPlugin.deregisterNode(kPluginNodeId)  

Python helper code

 # working on a scene in cm  
 import maya.cmds as mc  
 shoulder = mc.joint(n="shoulder", p=(-3,0,0))  
 elbow = mc.joint(n="elbow", p=(0,0,1))  
 wrist = mc.joint(n="wrist", p=(3,0,0))  
 mc.xform(mc.group(mc.circle(n="shoulder_ctrl"),w=True, n="shoulder_ctrl_grp"), ws=True, t=(-3,0,0))  
 mc.xform(mc.group(mc.circle(n="wrist_ctrl"),w=True, n="wrist_ctrl_grp"), ws=True, t=(3,0,0))  
 handle = mc.ikHandle(sj=shoulder, ee=wrist)[0]  
 mc.delete(mc.ls(type="softIKConstraint"))  
 mc.flushUndo()  
 mc.unloadPlugin("softIKConstraint")  
 mc.loadPlugin(r"softIKConstraint")  
 sik = mc.createNode("SoftIKConstraint")  
 mc.connectAttr("shoulder_ctrl.worldMatrix", sik + ".ChainDriverWorldMatrix")  
 mc.connectAttr(elbow + ".worldMatrix", sik + ".MiddleJointWorldMatrix")  
 mc.connectAttr("wrist_ctrl.worldMatrix", sik + ".HandleDriverWorldMatrix")  
 mc.connectAttr(handle + ".parentInverseMatrix", sik + ".HandleParentInverseMatrix")  
 mc.pointConstraint("shoulder_ctrl", shoulder)  
 mc.connectAttr(sik + ".Output", handle + ".translate")  

Monday, 1 May 2017

Speed Node

Some time ago our animation team was working on a scene with a chase that included multiple vehicles that had to move at constantly changing speeds. As we were working within an environment that effected real world values the speeds of the vehicles were of utmost importance.
I had already played with custom locators and nodes in Maya and thought that an interesting task to set myself would be to create a locator that could feed back a speed based on distance covered over a frame taking into account the frame rate.
It transpired that the challenge here was not so much getting the calculations to work in a proper fashion so that the node would output the correct speeds but rather getting the locator to work in both legacy mode and viewport 2.0. We use viewport 2.0 because amongst other things its more advanced rendering capabilities allow us to visualise our shaders in a proper fashion. It was challenging taking the relatively straightforward commands that construct items for the legacy viewport and converting them so that they were also compatible with viewport 2.0. Autodesk does provide some background on this in their documentation here. Ultimately this was really due to my inexperience with the Maya API, C++, viewport 2.0 and parallel evaluation all of which in some fashion shaped my journey to completing this node. For what is a relatively simple node it felt like a lot of work.
I will hopefully add a post on the custom locators I have created in the future. Although the final node is compiled in C++, I have included some Python code to give any aspiring individual a little bit of help into how the node works. This code does not create any locator, instead outputting to an annotation node. It is also important to note that it will only work in DG mode. The support code will switch to this or you can do this manually.

SpeedNode from SBGrover on Vimeo.

Python Node
 import sys  
 import maya.OpenMaya as OpenMaya  
 import maya.OpenMayaAnim as OpenMayaAnim  
 import maya.OpenMayaMPx as OpenMayaMPx  
 import math  
 import maya.cmds as mc  
   
   
 class SpeedNode(OpenMayaMPx.MPxNode):  
     
   id = OpenMaya.MTypeId( 0x80117 )  
   drawDbClassification = 'utility/general'  
   drawRegistrantId = "SpeedNode"  
     
   worldMatrix = OpenMaya.MObject()  
   unitType = OpenMaya.MObject()  
   output = OpenMaya.MObject()  
   fpsSwitch = OpenMaya.MObject()  
   name = OpenMaya.MObject()  
   time = OpenMayaAnim.MAnimControl()  
   position = OpenMaya.MPoint()  
   oldPosition = OpenMaya.MPoint()  
   speed = 0  
   oldTime = 0  
   speedType = 'mph'  
   fps = 0.0  
   fpsLib = {5:15,6:24,7:25,8:30,9:48,10:50,11:60}  
   string = ""  
   
   def __init__(self):  
     OpenMayaMPx.MPxNode.__init__(self)  
   
   def compute(self, plug, data):  
   
     inputWorldMatrix = OpenMaya.MMatrix()  
     inputWorldMatrix = data.inputValue(SpeedNode.worldMatrix).asMatrix()  
     self.position.x, self.position.y, self.position.z = inputWorldMatrix(3,0), inputWorldMatrix(3,1), inputWorldMatrix(3,2)  
   
     if plug == SpeedNode.output:  
       fpstime=OpenMaya.MTime()  
       self.fps = self.fpsLib[fpstime.unit()]  
       unit = data.inputValue(SpeedNode.unitType).asShort()  
       inputName = data.inputValue(SpeedNode.name).asString()  
       showFps = data.inputValue(SpeedNode.fpsSwitch).asInt()  
       delta = self.position - self.oldPosition  
       magnitude = math.sqrt((delta.x * delta.x) + (delta.y * delta.y) + (delta.z * delta.z))  
       self.oldPosition = OpenMaya.MPoint(self.position)  
       curTime = self.time.currentTime().value()  
         
       if magnitude != 0 and (curTime - self.oldTime) != 0:  
         self.speed = magnitude / ((curTime - self.oldTime) / self.fps)  
         
       else:  
         self.speed = magnitude  
         
       #mph  
       if unit == 0:  
         #conversion factor for seconds to hour is *3600  
         #conversion factor for cm to miles is *160000  
         if self.speed != 0:  
           self.speed = self.speed*3600/160000  
         self.speedType = 'mph'  
         
       #kph  
       if unit == 1:  
         #conversion factor for seconds to hour is *3600  
         #conversion factor for cm to kilometres is *100000  
         if self.speed != 0:  
           self.speed = self.speed*3600/100000  
         self.speedType = 'kph'  
           
       #mps  
       if unit == 2:  
         #conversion factor for seconds to seconds is 1  
         #conversion factor for cm to metres is *100  
         if self.speed != 0:  
           self.speed = self.speed/100  
         self.speedType = 'mps'  
         
       self.oldTime = curTime  
         
       if showFps:  
         self.string = (inputName+" : %.2f " + self.speedType + " @" + str(self.fps)+ "fps") %abs(self.speed)  
       else:  
         self.string = (inputName+" : %.2f " + self.speedType) %abs(self.speed)  
             
       outputData = data.outputValue(plug)  
       outputData.setString(self.string)  
   
       data.setClean(plug)  
         
 def initialize():  
   matrixAttr = OpenMaya.MFnMatrixAttribute()  
   typedAttr = OpenMaya.MFnTypedAttribute()  
   enumAttr = OpenMaya.MFnEnumAttribute()  
   numericAttr = OpenMaya.MFnNumericAttribute()  
   
   SpeedNode.worldMatrix = matrixAttr.create("inputWorldMatrix","iwm")  
     
   SpeedNode.name = typedAttr.create("name","n",OpenMaya.MFnData.kString)  
     
   SpeedNode.output = typedAttr.create("output","o",OpenMaya.MFnData.kString)  
     
   SpeedNode.unitType = enumAttr.create("unitType","ut")    
   enumAttr.addField("mph",0)  
   enumAttr.addField("kph",1)  
   enumAttr.addField("mps",2)  
   enumAttr.channelBox = True  
     
   SpeedNode.fpsSwitch = numericAttr.create("showFPS","sfps",OpenMaya.MFnNumericData.kBoolean)  
   numericAttr.channelBox = True  
     
   SpeedNode.addAttribute(SpeedNode.unitType)  
   SpeedNode.addAttribute(SpeedNode.fpsSwitch)  
   SpeedNode.addAttribute(SpeedNode.name)  
   SpeedNode.addAttribute(SpeedNode.worldMatrix)  
   SpeedNode.addAttribute(SpeedNode.output)  
   
   SpeedNode.attributeAffects(SpeedNode.fpsSwitch,SpeedNode.output)  
   SpeedNode.attributeAffects(SpeedNode.name,SpeedNode.output)  
   SpeedNode.attributeAffects(SpeedNode.worldMatrix,SpeedNode.output)  
   SpeedNode.attributeAffects(SpeedNode.unitType,SpeedNode.output)  
   
   
 def nodeCreator():  
   return OpenMayaMPx.asMPxPtr(SpeedNode())        
   
 def initializePlugin(obj):  
   plugin = OpenMayaMPx.MFnPlugin(obj)  
   
   try:  
     plugin.registerNode("SpeedNode", SpeedNode.id, nodeCreator, initialize, OpenMayaMPx.MPxNode.kDependNode, SpeedNode.drawDbClassification)  
   except:  
     sys.stderr.write("Failed to register node\n")  
     raise  
   
 def uninitializePlugin(obj):  
   plugin = OpenMayaMPx.MFnPlugin(obj)  
   
   try:  
     plugin.deregisterNode(SpeedNode.id)  
   except:  
     sys.stderr.write("Failed to deregister node\n")  
     pass  
   
   
   

Helper Code

 import maya.cmds as mc  
   
 mc.delete(mc.ls(type="SpeedNode"))  
 mc.flushUndo()  
 mc.unloadPlugin("SpeedNode.py")  
 mc.loadPlugin("SpeedNode.py")  
   
   
 def createSpeedNode():  
   
   mc.evaluationManager( mode="off" )  
   if not mc.pluginInfo('SpeedNode.py', q=True, l=True): mc.loadPlugin('SpeedNode.py')  
   
   sel = mc.ls(sl=True)  
   if sel:  
     for i in sel:  
       newNode = mc.createNode('SpeedNode')  
       mc.connectAttr(i + '.worldMatrix', newNode + '.inputWorldMatrix')  
       ann_node = mc.createNode('annotationShape')  
       mc.parent(ann_node, i)  
       mc.connectAttr(newNode + ".output", ann_node + ".text" )  
   
   else:  
     mc.error("Select Moving Transform(s)")