PyQt: Maya Character Picker

Coming Soon:- 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

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)")  

Monday, 24 April 2017

Tip #1: Remove Target from Blendshape (when mesh target has been removed from scene)

A while ago I was writing a blendshape management system that would allow the adding, editing and deletion of a blendshape target.
One of the problems I had when looking more deeply into this was that, prior to 2017 (where I believe its been fixed) the Autodesk tool to delete a blendshape target required that the targets still existed as geometry somewhere in the scene. This is all well and good but if like me you do not want to be dealing with heavy scenes then handing a character rig with dozens of blendshapes attached was not going to help matters, especially with multiple rigs referenced into one scene. So I chose to delete my targets using a tool to rebuild them if required or simply storing them in a seperate scene.
So with the physical targets removed from the scene it appeared that if I wanted to remove a target from the blendshape it was tantamount to rebuilding the blendshape from scratch and re-hooking up any automation that might have previously existed. Not Cool.
It would appear that it is infact possible to remove targets after the scene mesh has been removed using the removeMultiInstance command. The Autodesk documentation list this description.

Removes a particular instance of a multiElement. This is only useful for input attributes since outputs will get regenerated the next time the node gets executed. This command will remove the instance and optionally break all incoming and outgoing connections to that instance. If the connections are not broken (with the -b true) flag, then the command will fail if connections exist.

Indeed, if you input the name of your blendshape along with the weight and target group index Maya should both remove the data at the given index, unhooking any connections. The only thing that is required is keeping track of your target indices. In the posted video I show a basic example of the issues with the in built tool and how these simple lines of code get around the issue. Obviously you will need to adjust the blendshape name and index to make use of it.

 import maya.cmds as mc  
        
 def delete_blendshape_target(blendshape_name, target_index):  
      mc.select(d=True)  
      mc.removeMultiInstance(blendshape_name + ".weight[%s]"%target_index, b=True)  
      mc.removeMultiInstance(blendshape_name + ".inputTarget[0].inputTargetGroup[%s]"%target_index, b=True)  
        
 blendshape_name = "blendShape1"  
 target_index = 0  
 delete_blendshape_target(blendshape_name, target_index)  

Remove Blendshape Targets from SBGrover on Vimeo.

Tuesday, 18 April 2017

Visibility Node v2.0

Here is an update to the previous visibility node. As discussed in the last post the intention with this version was to test out a quicker alternative in changing the visibility for individual faces in a mesh for the purpose of visualising internal meshes and details.
This time however rather than adjusting the vertex face alpha values the node would make use of the component modifiers already available in Maya.These modifiers are created and maintained as historical entities to your mesh. If you apply a poly smooth then a modifier will be created for the purpose into which will hook your mesh 'orig shape', the output passing into your mesh itself. It is possible to chain the modifiers together each one maintaining a list of edited components that pass down the chain until they reach the actual mesh where the result can be seen.
It is the fact that these modifiers allow the input of a component list that allows them to be used for the purpose of dynamically adjusting a mesh based on fluctuating input data.
In the case of this example we want to see inside a mesh when a collider intersects. For this we use a deleteComponent modifier which allows us to input the original mesh shape and a list of components to be considered for deletion. These components are derived by using the ever useful MFnMesh::allIntersections method which based on casting rays can define if a mesh is colliding with another before returning the vertices that are inside.
By querying the faces that are made up of these vertices it is possible to pass a complete list over to the modifier to be 'modified'.
Below is an example of how the nodes hook into one another to achieve the desired effect.

One drawback of note is that you are always limited to the shape of your 'orig mesh'. It is possible to update this shape but this can prove to be a bit of a pain so do not expect this method to work for skinned and deformed meshes with ease.
I had initially created the node in such a way that it was able to accept a continually changing face count from the orig shape without the requirement of updating class variables. However this was at a sacrifice to speed as I was rebuilding the vertex lists for every update. Fevsy of Constrain n' Bake suggested that I only read in the vertex list once on the initialisation and then maintain it as required which is why I trigger an update by switching the update channel on the node. It speeds up the evaluation at the cost of making the node less fluid to interact with. Which is better? You decide.
Thanks also go to Hans Goddard for inspiring me with a demonstration of his implementation in this video.

..and here is mine

Visibility Node v2.0 from SBGrover on Vimeo.
If you want to have a go yourself below is a basic Python Plugin to get started with. This is an example from the first version of the node and does not support the component modifiers. Note that this plugin is also an MPxDeformer rather than an MPxNode.

 import maya.cmds as cmds  
 import maya.OpenMaya as OpenMaya  
 import maya.OpenMayaAnim as OpenMayaAnim  
 import maya.OpenMayaMPx as OpenMayaMPx  
   
 class visibilityDeformer(OpenMayaMPx.MPxDeformerNode):  
      kPluginNodeId = OpenMaya.MTypeId(0x00000013)  
      kPluginNodeTypeName = "visibilityDeformer"  
      accelParams = OpenMaya.MMeshIsectAccelParams() #speeds up intersect calculation  
      intersector = OpenMaya.MMeshIntersector() #contains methods for efficiently finding the closest point to a mesh, required for collider  
        
      def __init__(self):  
           OpenMayaMPx.MPxDeformerNode.__init__( self )  
   
      def deform( self, block, geoItr, matrix, index ):  
             
           #get ENVELOPE  
           envelope = OpenMayaMPx.cvar.MPxGeometryFilter_envelope  
           envelopeHandle = block.inputValue(envelope)  
           envelopeVal = envelopeHandle.asFloat()  
             
           if envelopeVal!=0:  
             
                #get DEFORMED MESH  
                inMesh = self.get_input_geom(block, index)  
                  
                #get COLLIDER MESH (as worldMesh)  
                colliderHandle = block.inputValue(self.collider)  
                inColliderMesh = colliderHandle.asMesh()  
                  
                if not inColliderMesh.isNull():  
                     inColliderFn = OpenMaya.MFnMesh(inColliderMesh)  
                       
                     #get COLLIDER WORLD MATRIX  
                     colliderMatrixHandle = block.inputValue(self.colliderMatrix)  
                     colliderMatrixVal = colliderMatrixHandle.asMatrix()  
                       
                     #get BOUNDING BOX MIN VALUES  
                     colliderBoundingBoxMinHandle = block.inputValue(self.colliderBoundingBoxMin)  
                     colliderBoundingBoxMinValDouble = colliderBoundingBoxMinHandle.asFloat3()  
                       
                     #get BOUNDING BOX MAX VALUES  
                     colliderBoundingBoxMaxHandle = block.inputValue(self.colliderBoundingBoxMax)  
                     colliderBoundingBoxMaxValDouble = colliderBoundingBoxMaxHandle.asFloat3()  
                       
                     colliderBoundingBoxMinVal = OpenMaya.MPoint(colliderBoundingBoxMinValDouble[0], colliderBoundingBoxMinValDouble[1], colliderBoundingBoxMinValDouble[2])  
                     colliderBoundingBoxMaxVal = OpenMaya.MPoint(colliderBoundingBoxMaxValDouble[0], colliderBoundingBoxMaxValDouble[1], colliderBoundingBoxMaxValDouble[2])  
                       
                     #build new bounding box based on given values  
                     bbox = OpenMaya.MBoundingBox()  
                     bbox.expand(colliderBoundingBoxMinVal)  
                     bbox.expand(colliderBoundingBoxMaxVal)  
                       
                     self.accelParams = inColliderFn.autoUniformGridParams()  
                       
                     #deal with main mesh  
                     inMeshFn = OpenMaya.MFnMesh(inMesh)  
                     inPointArray = OpenMaya.MPointArray()  
                     inMeshFn.getPoints(inPointArray, OpenMaya.MSpace.kWorld)  
                       
                     ##BEGIN DIRECT COLLISION##  
                       
                     deformed_list = []  
                     col = OpenMaya.MColor(1,1,1,0)  
                       
                     for num in range(inPointArray.length()):  
                       
                          point = OpenMaya.MPoint(inPointArray[num])  
   
                          bbox_flag = False  
                            
                          if bbox.contains(point):  
                               bbox_flag = True  
                               vec = OpenMaya.MVector()  
                               inMeshFn.getVertexNormal(num, vec, OpenMaya.MSpace.kWorld)  
   
                               ##-- allIntersections arguments --##  
                               floatPoint = OpenMaya.MFloatPoint(point)  
                               floatVec = OpenMaya.MFloatVector(vec)  
                               faceIds = None  
                               triIds = None  
                               idsSorted = False  
                               space = OpenMaya.MSpace.kWorld  
                               maxParam = 100000  
                               testBothDirs = False  
                               accelParams = None#self.accelParams  
                               sortHits = False  
                               hitPoints = OpenMaya.MFloatPointArray()  
                               hitRayParams = None  
                               hitFaces = OpenMaya.MIntArray()  
                               hitTriangles = OpenMaya.MIntArray()  
                               hitBary1 = None  
                               hitBary2 = None  
                               tolerance = 0.0001  
                               ####################################  
                            
                               inColliderFn.allIntersections( floatPoint, floatVec, faceIds, triIds, idsSorted, space, maxParam, testBothDirs, accelParams, sortHits, hitPoints, hitRayParams, hitFaces, hitTriangles, hitBary1, hitBary2, tolerance )  
                       
                               #for all hits of length of 1 do the following  
                               if hitPoints.length()%2 == 1:  
                                      
                                    iterator = OpenMaya.MItMeshVertex(inMesh)  
                                    util = OpenMaya.MScriptUtil()  
                                    util.createFromInt(0)  
                                    pInt = util.asIntPtr()  
                                    iterator.setIndex(num, pInt)  
                                    faceArray = OpenMaya.MIntArray()  
                                    iterator.getConnectedFaces(faceArray)  
                                      
                                    for f_idx in range(faceArray.length()):   
                                         polyVertArray = OpenMaya.MIntArray()  
                                         inMeshFn.getPolygonVertices(faceArray[f_idx], polyVertArray)  
   
                                         for v_idx in range(polyVertArray.length()):   
                                              inMeshFn.setFaceVertexColor(col, faceArray[f_idx], polyVertArray[v_idx])  
                  
                string = "dgdirty %s;"%self.name()  
                OpenMaya.MGlobal.executeCommand(string, False, False)  
                                                          
                       
      def get_input_geom(self, block, index):  
           input_attr = OpenMayaMPx.cvar.MPxGeometryFilter_input  
           input_geom_attr = OpenMayaMPx.cvar.MPxGeometryFilter_inputGeom  
           input_handle = block.outputArrayValue(input_attr)  
           input_handle.jumpToElement(index)  
           input_geom_obj = input_handle.outputValue().child(input_geom_attr).asMesh()  
           return input_geom_obj  
                  
             
 def creator():  
      return OpenMayaMPx.asMPxPtr(visibilityDeformer())  
   
        
 def initialize():  
      gAttr = OpenMaya.MFnGenericAttribute()  
      mAttr = OpenMaya.MFnMatrixAttribute()  
      nAttr = OpenMaya.MFnNumericAttribute()  
        
      visibilityDeformer.collider = gAttr.create( "colliderTarget", "col")  
      gAttr.addDataAccept( OpenMaya.MFnData.kMesh )  
             
      visibilityDeformer.colliderBoundingBoxMin = nAttr.createPoint( "colliderBoundingBoxMin", "cbbmin")  
        
      visibilityDeformer.colliderBoundingBoxMax = nAttr.createPoint( "colliderBoundingBoxMax", "cbbmax")  
        
      visibilityDeformer.colliderMatrix = mAttr.create("colliderMatrix", "collMatr")  
        
      visibilityDeformer.multiplier = nAttr.create("multiplier", "mult", OpenMaya.MFnNumericData.kFloat, 1)  
        
      visibilityDeformer.addAttribute( visibilityDeformer.collider )  
      visibilityDeformer.addAttribute( visibilityDeformer.colliderMatrix )  
      visibilityDeformer.addAttribute( visibilityDeformer.colliderBoundingBoxMin )  
      visibilityDeformer.addAttribute( visibilityDeformer.colliderBoundingBoxMax )  
      visibilityDeformer.addAttribute( visibilityDeformer.multiplier )  
        
      outMesh = OpenMayaMPx.cvar.MPxGeometryFilter_outputGeom  
        
      visibilityDeformer.attributeAffects( visibilityDeformer.collider, outMesh )  
      visibilityDeformer.attributeAffects( visibilityDeformer.colliderBoundingBoxMin, outMesh )  
      visibilityDeformer.attributeAffects( visibilityDeformer.colliderBoundingBoxMax, outMesh )  
      visibilityDeformer.attributeAffects( visibilityDeformer.colliderMatrix, outMesh )  
      visibilityDeformer.attributeAffects( visibilityDeformer.multiplier, outMesh )  
   
        
 def initializePlugin(obj):  
      plugin = OpenMayaMPx.MFnPlugin(obj, 'Grover', '1.0', 'Any')  
      try:  
           plugin.registerNode('visibilityDeformer', visibilityDeformer.kPluginNodeId, creator, initialize, OpenMayaMPx.MPxNode.kDeformerNode)  
      except:  
           raise RuntimeError, 'Failed to register node'  
   
             
 def uninitializePlugin(obj):  
      plugin = OpenMayaMPx.MFnPlugin(obj)  
      try:  
           plugin.deregisterNode(visibilityDeformer.kPluginNodeId)  
      except:  
           raise RuntimeError, 'Failed to deregister node'  
             
   
   
The support code. Run this to build a quick scene which supports the plugin above.
 import maya.cmds as cmds  
 cmds.polySphere()  
 cmds.polySphere()  
 cmds.move(3,0,0)  
 cmds.select('pSphere1', 'pSphere2')  
 cmds.polyColorPerVertex(r=0.5, g=0.5, b=0.5 ,a=1 , cdo=True)  
 cmds.delete(ch=True)  
 cmds.delete(cmds.ls(type='visibilityDeformer'))  
 cmds.flushUndo()  
 cmds.unloadPlugin('visibilityDeformer')  
 cmds.loadPlugin('visibilityDeformer')  
 cmds.select('pSphere1')  
 deform = cmds.deformer(type='visibilityDeformer')[0]  
 cmds.connectAttr('pSphere2.worldMesh',deform + ".colliderTarget", f=True)  
 cmds.connectAttr('pSphere2.worldMatrix',deform + ".colliderMatrix", f=True)  
 cmds.connectAttr('pSphere2.boundingBoxMin',deform + ".colliderBoundingBoxMin", f=True)  
 cmds.connectAttr('pSphere2.boundingBoxMax',deform + ".colliderBoundingBoxMax", f=True)  
   

Tuesday, 11 April 2017

Texture Based Deformer

I recently thought about Maya's in built texture deformation tools and became interested in having a stab at making a deformer that could produce the equivalent of 'Texture To Geometry' and 'Displacement To Polygons' but in real time.
Having created a number of deformers in the past I was aware of some of the required pre requisites. However one thing that was a mystery was how I would sample colours from a UV position using the API.Some digging around suggested three options that might be suitable.
The first is MImage::readFromTextureNode. This method can be called to pull an image in ready to have pixels read. However MImage deals predominantly with specific image files. I wanted to base this deformer around a procedural texture for easy customisation.
The second, MRenderUtil::sampleShadingNetwork initially looked promising but after further investigation I was concerned that it would update very slowly as it took such things as  shading and lighting into account.
The third was MDynamicsUtil::evalDynamics2dTexture. This proved the most likely candidate, especially due to its compatibility with procedural textures so I set about building a basic one shot Python implementation to test the logic. I have included it in this post so others can try it out.
With a few fits and starts I succeeded in creating a solution that I deemed worthy to convert into a C++ node. The speed increase was phenomenal especially as we are dealing with potentially vast numbers of vertices. The deformer is able to hit over 300000 triangles on my machine without to much of an issue. In the included video I show it performing deformation on a mesh a little under 200000 triangles.

Texture Deformer from SBGrover on Vimeo.

The python code below assumes you have created a sphere ('pSphere1'), added some kind of procedural texture via a lambert ('lambert2') and is using uv's from 'map1'.
   
 import maya.OpenMaya as om  
 import maya.OpenMayaFX as omfx  
   
 obj_sel = om.MSelectionList()  
 obj_sel.add('pSphere1')  
 obj_dag = om.MDagPath()  
 obj_sel.getDagPath(0, obj_dag)  
 itr = om.MItMeshVertex(obj_dag)  
   
 length = itr.count()  
 scaler = 1  
   
 #declare arrays  
 vtx_pos_array = om.MPointArray()  
 vtx_nor_array = om.MVectorArray()  
 uv_array = om.MIntArray()  
 uColArray = om.MDoubleArray()  
 vColArray = om.MDoubleArray()  
   
 #set array lengths  
 vtx_pos_array.setLength(length)  
 vtx_nor_array.setLength(length)  
 uColArray.setLength(length)  
 vColArray.setLength(length)  
   
 #declare vars for itr  
 global_count = 0  
 v_pos = om.MPoint()  
 n_vec = om.MVector()  
   
 #horrible MScriptUtil shenanigans  
 uv_list = [0, 0]  
 uv_util = om.MScriptUtil()  
 uv_util.createFromList(uv_list, 2)  
 uv = uv_util.asFloat2Ptr()  
   
 #iterate to get uv positions  
 while not itr.isDone():  
      itr.getUV(uv, 'map1')  
      v_pos =      itr.position(om.MSpace.kWorld)  
      vtx_pos_array.set(v_pos, global_count)  
      itr.getNormal(n_vec, om.MSpace.kWorld)  
      vtx_nor_array.set(n_vec, global_count)       
      u = uv_util.getFloat2ArrayItem(uv, 0, 0)  
      v = uv_util.getFloat2ArrayItem(uv, 0, 1)       
      uColArray.set(u, global_count)  
      vColArray.set(v, global_count)  
      global_count += 1  
      itr.next()  
   
 #get color attribute from node        
 imgObj = om.MObject()  
 sel = om.MSelectionList()  
 om.MGlobal.getSelectionListByName('lambert2', sel)  
 sel.getDependNode(0, imgObj)  
 fnThisNode = om.MFnDependencyNode(imgObj)  
 attr = fnThisNode.attribute( "color" )  
   
 #set up output arrays for avalDynamics2dTexture  
 outColours = om.MVectorArray()  
 outAlphas = om.MDoubleArray()  
   
 #do it!  
 omfx.MDynamicsUtil.evalDynamics2dTexture(imgObj, attr, uColArray, vColArray, outColours, outAlphas)  
        
 itr.reset()  
 global_count = 0  
   
 #iterate over the mesh to deform it  
 while not itr.isDone():  
      point = vtx_pos_array[global_count]  
      normal = vtx_nor_array[global_count]  
      colour = outColours[global_count]  
      col_avg = ((colour.x + colour.y + colour.z) /3)  
      pos = om.MPoint(point.x + (normal.x * col_avg * scaler), point.y + (normal.y * col_avg * scaler), point.z + (normal.z * col_avg * scaler))  
      itr.setPosition(pos, om.MSpace.kWorld)  
      global_count += 1  
      itr.next()       

Monday, 10 April 2017

Visibility Node v1.0

To kick off this blog I rooted out an old plugin just to remind myself how it worked. I had intended it to be use by the animation department to allow them to work with characters based inside objects but had instead left it to rot. It's pretty straightforward providing you have a prior knowledge of how to utilise MFnMesh::allIntersections to register mesh collisions. In the case of this node after registering a hit I would then colour the face vertex with an alpha value to render it transparent returning it to normal when it is no longer intersecting.
The only issue... it's quite slow when hitting over 10k polygons which renders this fairly useless even after compiling it with C++.
When speaking with Fevsy over at Constrain 'n Bake he suggested I use a polyModifier. The idea is to have the orig mesh connected into a poly modifier, deleteComponent for example, and also into the custom node. The deformer tracks intersection between a mesh and the deformed mesh and passes a list of the intersected points into the delete modifier. He pointed me to this video by Hans Godard which at 6 seconds in confirms the method somewhat so I think I shall give it a go with version two. For now, here is a short video of version one.

I have included the code for the Python version of this node in a later post available here.

Reveal Node from SBGrover on Vimeo.

Thursday, 25 August 2016

Blendshape: Instancing Scene Data using inMesh

Whilst looking into the Blendshapes for the purpose of Corrective Targets in a Facial Rig I discovered something interesting.
An empty shape node can be created and then have the outMesh of another plugged into its inMesh. It will then inherit the component data for the input mesh. This appears to have no cost on scene size.
Try it out:

Create a cube
Set its subdivisions to 100 in all axis
Save the scene
Inspect the file and check its size

Now create another cube and plug the outMesh of the first into the inMesh of the second (from and to the shape nodes). You should notice that the second cube ends up looking identical to the first.
Save the scene
Inspect the file and check its size

You should notice that the file size will only have increased by a nominal amount.
It would appear that as the data is in effect instanced the cost is only placed for one of the cubes. Maya will only make you pay for the differences between the first and second cubes - their deltas.

Try editing the second cube. If you move a large group of faces and save the file you will now see that the file size has increased again.
What use is this? I don't need a bunch of identical mesh data within a scene. Well actually this could be useful for populating large scenes with stuff like trees and rocks. However I propose that it could also be useful for storing out the deltas used when working with blendshape targets.
For instance I have noticed people online complaining that file size gets very large when they work with large amounts of corrective data for projects. Most of the time the fix is to either delete the targets or to store them away in another file. It can be useful to keep them in the same file however for such things as editing or retrieving normal data.
So as an alternative create a 'base mesh', a duplicate of the object you are working with, and then each time you need a new corrective duplicate the base mesh and plug its outMesh into the inMesh of the new corrective. You should be able to make your edits and apply as a blendshape corrective. Maya will only tot up a cost for the vertex difference, scene size will hence be lowered and you have the added bonus that any edits to the base mesh will propagate through all target shapes so you can make relatively complex changes quickly without having the problem of passing the data back to the correctives.

By the way, I haven't actually implemented this yet... but in theory it should work.