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, 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.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()  
                     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()  
                                    pInt = util.asIntPtr()  
                                    iterator.setIndex(num, pInt)  
                                    faceArray = OpenMaya.MIntArray()  
                                    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;"  
                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_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')  
           plugin.registerNode('visibilityDeformer', visibilityDeformer.kPluginNodeId, creator, initialize, OpenMayaMPx.MPxNode.kDeformerNode)  
           raise RuntimeError, 'Failed to register node'  
 def uninitializePlugin(obj):  
      plugin = OpenMayaMPx.MFnPlugin(obj)  
           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.move(3,0,0)'pSphere1', 'pSphere2')  
 cmds.polyColorPerVertex(r=0.5, g=0.5, b=0.5 ,a=1 , cdo=True)  
 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_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  
 #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  
 #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)  
 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       

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.