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

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

Thursday, 29 June 2017

Python and PYQT Image Compare Tool

As my work is predominantly focused within Maya this means that I do not have a lot of experience of creating standalone tools for use outside a Maya environment.
These days Maya has everything you need to write decent tools. Python is embedded along with PySide for PyQt and now that the API has support for Python this has made it more easy to write complex tooling compared to a purely Mel and C++ offering of the bad old days.
However writing tooling outside is a different beast as none of this stuff comes pre-installed.

ImageCompare Tool

For my first foray into the world of standalone applications I decided to write a simple tool with the aim of getting me used to things. In the end I went for one that would find similar or the same images based on a source image giving you the option of checking or deleting the duplicates.


Python and PyQt
Python Interpreter: Python 2.7 64 bit
PyQt: PyQt for 2.7 64 bit exe

When I first approached the most confusing thing was looking at all the different versions of the software and libraries there were and working out which one was applicable. 64 Bit or 32 Bit? 2.7, 3.3, source, binary, exe, tar...blah, blah, blah. After some false starts I found a good combination that worked well for me although I guess that depending on requirements you may need different versions of the software.
The links above give you enough to get you up and writing tools without much hassle and as all of the above are installers they will be setup automatically skipping the more complicated requirements of the PyQt manual install that includes fiddling with sip.


Python Imaging Library
Python Imaging Library: PIL 64 bit exe

As this tool was intended for finding duplicate images I needed an extra library referred to as Pil, Python imaging library.
I wanted to be able to open an image and then provided it's dimensions matched the source return the pixel RGB values based on a sample rate, for instance every tenth pixel. As long as the values match then the compare continues until the end of the image is reached. If there is no disparity a match has been found. PIL gives all of this and more and seemed to be incredibly quick at pixel sampling


Py2exe
Compile your python: Py2exe 64 bit

To test it rather than running it over and over in the Python environment I opted to convert it to an executable using py2exe. It was quick to convert the Python code into a tool that could be run with a simple double click. The only drawback of this method was that when there was a fault in my code the error was lost as the window would close before it could be read. In the end I had to create a little batch file to run the executable with a pause at the end to allow me to read each problem as it appeared.

To work with py2exe you will need a setup.py file. This is used by py2exe and gives it basic instructions about how to compile your py file(s). I found that to run my main program I needed to call it in separate py file. This is linked to the setup.py file so that when compiling py2exe will make sure that your program is run correctly based on what is in this file. Py2exe also sources all of the libraries you are using and includes them with the executable.
In addition you might consider using the batch file mentioned earlier to actually run your program whilst you are iterating and testing. This way you can catch any errors that occur.
These files are placed in a relevant location to the Python folder. In my case I placed them directly at the root of the Python27 folder.

If successfully compiled the executable will be placed into a 'dist' folder along with other libraries that the program requires.
One other thing to note is that if you wish to add an icon to your new application then all you need do is specify the filename after the 'icon_resources' key contained within the setup.py file. The caveat here is that it appears the setup needs to run twice to properly embed the icon. This is probably a bug or simply perhaps something I have missed. If run twice this obviously doubles the length of compilation time.

Check out the video below to see what I have so far and then below that is the source code for the application.

ImageCompare: Python, PyQt, PIL and py2exe from SBGrover on Vimeo.


Try it out!!
Below I include the files I have created for my application simply to give you an idea of the setup and to have something to try out.

1. Write your code

ImageCompare.py

 # Import the modules  
 import sys  
 from PyQt4 import QtCore, QtGui  
 from functools import partial  
 import Image  
 import os  
 import subprocess  
   
   
 class VerticalWidget(QtGui.QWidget):  
   
   def __init__(self):  
     super(VerticalWidget, self).__init__()  
     self.layout = QtGui.QVBoxLayout(self)  
     self.setLayout(self.layout)  
   
   
 class HorizontalWidget(QtGui.QHBoxLayout):  
   
   def __init__(self, layout):  
     super(HorizontalWidget, self).__init__()  
     layout.addLayout(self, QtCore.Qt.AlignLeft)  
   
   
 class MainButtonWidget(QtGui.QPushButton):  
   
   def __init__(self, layout, name, main_object, command, width):  
     super(MainButtonWidget, self).__init__()  
     layout.addWidget(self, QtCore.Qt.AlignLeft)  
     self.setMaximumWidth(width)  
     self.setMaximumHeight(24)  
     self.setText(name)  
     self.main_object = main_object  
     self.command = command  
   
   def mouseReleaseEvent(self, event):  
   
     if event.button() == QtCore.Qt.LeftButton:  
       self.run_command(self.command)  
   
   def run_command(self, command):  
     exec command  
   
   
 class TabLayout(QtGui.QTabWidget):  
   def __init__(self, tab_dict):  
     super(TabLayout, self).__init__()  
   
     for tab in tab_dict:  
       self.addTab(tab[0], tab[1])  
   
   
 class OutputView(QtGui.QListWidget):  
   def __init__(self, layout):  
     super(OutputView, self).__init__()  
     layout.addWidget(self, QtCore.Qt.AlignLeft)  
     self.setSelectionMode(0)  
     self.setUpdatesEnabled(True)  
   
   
 class ListView(QtGui.QListWidget):  
   def __init__(self, layout):  
     super(ListView, self).__init__()  
     layout.addWidget(self, QtCore.Qt.AlignLeft)  
     self.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection)  
     self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)  
     self.connect(self, QtCore.SIGNAL("customContextMenuRequested(QPoint)" ), self.rightClicked)  
   
   def rightClicked(self, QPos):  
     self.listMenu = QtGui.QMenu()  
   
     menu_item_a = QtGui.QAction("Open in Explorer", self.listMenu)  
     self.listMenu.addAction(menu_item_a)  
     menu_item_a.triggered.connect(self.open_in_explorer)  
   
     menu_item_b = QtGui.QAction("Delete File", self.listMenu)  
     self.listMenu.addAction(menu_item_b)  
     menu_item_b.triggered.connect(self.delete_file)  
   
     menu_item_c = QtGui.QAction("Open File", self.listMenu)  
     self.listMenu.addAction(menu_item_c)  
     menu_item_c.triggered.connect(self.open_file)  
   
     parentPosition = self.mapToGlobal(QtCore.QPoint(0, 0))  
     self.listMenu.move(parentPosition + QPos)  
   
     self.listMenu.show()  
   
   def open_in_explorer(self):  
     path = self.currentItem().text()  
     path = path.replace("/", "\\")  
     subprocess.Popen('explorer /select,' + r'%s' %path)  
   
   def open_file(self):  
     path = self.currentItem().text()  
     os.startfile(str(path))  
   
   def delete_file(self):  
     path_list = self.selectedItems()  
   
     for path in path_list:  
       os.remove(str(path.text()))  
       self.takeItem(self.row(path))  
   
   
 class SpinBox(QtGui.QSpinBox):  
   
   def __init__(self, layout):  
     super(SpinBox, self).__init__()  
     layout.addWidget(self, QtCore.Qt.AlignLeft)  
     self.setValue(10)  
     self.setMaximumWidth(40)  
     self.setMaximum(100)  
     self.setMinimum(0)  
   
   
 class FileTextEdit(QtGui.QTextEdit):  
   
   def __init__(self, layout):  
     super(FileTextEdit, self).__init__()  
     layout.addWidget(self, QtCore.Qt.AlignLeft)  
     self.setMaximumHeight(24)  
     self.setWordWrapMode(0)  
     self.setHorizontalScrollBarPolicy(1)  
     self.setVerticalScrollBarPolicy(1)  
   
   
 class Text(QtGui.QLabel):  
   
   def __init__(self, layout, text):  
     super(Text, self).__init__()  
     layout.addWidget(self, QtCore.Qt.AlignRight)  
     self.setText(text)  
     self.setMaximumWidth(70)  
   
   
 class ImageCompare_Helpers():  
   
   def get_image(self, path):  
     try:  
       im = Image.open(path)  
     except:  
       print "Pick a VALID image file (.jpg, .gif, .tga, .bmp, .png, .tif)"  
     rgb_im = im.convert('RGB')  
   
     return rgb_im  
   
   def get_pixel_color(self, img, sample_size):  
     width, height = img.size  
     pixel_total = width * height  
     pixel_set = []  
     pixel_range = pixel_total / sample_size  
   
     for pixel in range(pixel_range)[0::10]:  
       x, y = self.convert_to_pixel_position(pixel, width)  
       r, g, b = img.getpixel((x, y))  
       pixel_set.append([r, g, b])  
   
     return pixel_set  
   
   def convert_to_pixel_position(self, val, width):  
     x = val % width  
     y = val / width  
   
     return x, y  
   
   def get_file(self, main_object):  
   
     file = QtGui.QFileDialog.getOpenFileName(None, 'Select Source File', 'c:/',  
                            selectedFilter='*.jpg')  
   
     if file:  
       main_object.file_text.setText(file)  
   
   def get_folder(self, main_object):  
   
     folder = QtGui.QFileDialog.getExistingDirectory(None, 'Select Search Folder')  
   
     if folder:  
       main_object.folder_text.setText(folder)  
   
   
   def do_it(self, main_object):  
     #main_object.main_widget.close()  
     #main_object.app.quit()  
   
     output_view = main_object.output_view  
     output_view.clear()  
     main_object.list_view.clear()  
   
     directory = main_object.folder_text.toPlainText()  
     filename = main_object.file_text.toPlainText()  
   
     if filename and directory:  
       matching_images = []  
       self.sample_size = main_object.samples.value()  
       self.accuracy = main_object.threshold.value()  
       self.accuracy = abs(self.accuracy - 100)  
   
       img1 = self.get_image(str(filename))  
       img1_pixels = self.get_pixel_color(img1, self.sample_size)  
       all_images = self.read_all_subfolders(str(directory))  
       width, height = img1.size  
       img1_size = width * height  
       output_view.addItem("##################")  
       output_view.addItem("SOURCE IMAGE ----> " + str(filename))  
       output_view.addItem("")  
   
       for image in all_images:  
   
         output_view.scrollToBottom()  
         main_object.app.processEvents()  
         image = image.replace("\\", "/")  
   
         if image != filename:  
           output_view.addItem("Comparing: " + image)  
           img2 = self.get_image(image)  
           img2_width, img2_height = img2.size  
   
           if img2_width * img2_height == img1_size:  
             img2_pixels = self.get_pixel_color(img2, self.sample_size)  
             same = self.compare_images(img1_pixels, img2_pixels)  
   
             if same:  
               matching_images.append(image)  
   
       output_view.addItem("")  
       output_view.addItem("##################")  
       output_view.addItem("MATCHING IMAGES")  
   
       if matching_images:  
   
         for i in matching_images:  
           output_view.addItem(i)  
           main_object.list_view.addItem(i)  
   
       else:  
         output_view.addItem("NONE")  
   
       output_view.scrollToBottom()  
   
   def compare_images(self, img1_pixel_set, img2_pixel_set):  
     length = len(img1_pixel_set)  
   
     for count, i in enumerate(img1_pixel_set):  
       img1_total = i[0] + i[1] + i[2]  
       img2_total = img2_pixel_set[count][0] + img2_pixel_set[count][1] + img2_pixel_set[count][2]  
       img2_upper = img2_total + self.accuracy  
       img2_lower = img2_total - self.accuracy  
   
       if img2_lower <= img1_total <= img2_upper:  
   
         if count == length - 1:  
           return 1  
   
       else:  
         return 0  
         break  
   
   def read_all_subfolders(self, path):  
     all_images = []  
     suffix_list = [".jpg", ".gif", ".tga", ".bmp", ".png", ".tif"]  
   
     for root, dirs, files in os.walk(path):  
   
       for file in files:  
   
         for suffix in suffix_list:  
   
           if file.lower().endswith(suffix):  
             all_images.append(os.path.join(root, file))  
   
     return all_images  
   
   
 class ImageCompare_UI(ImageCompare_Helpers):  
   
   def __init__(self):  
     self.app = None  
     self.main_widget = None  
   
   def run_ui(self, ImageCompare):  
     self.app = QtGui.QApplication(sys.argv)  
     self.main_widget = QtGui.QWidget()  
     self.main_widget.resize(600, 600)  
   
     main_layout = VerticalWidget()  
   
     # VIEW FILES  
     vertical_layout_list = VerticalWidget()  
     self.list_view = ListView(vertical_layout_list.layout)  
     #vertical_layout_list.layout.setContentsMargins(0,0,0,0)  
     MainButtonWidget(vertical_layout_list.layout, "Delete All", self,  
              "", 128)  
   
     # FIND FILES  
     vertical_layout = VerticalWidget()  
     horizontal_layout_a = HorizontalWidget(vertical_layout.layout)  
   
     Text(horizontal_layout_a, "Source File")  
     self.file_text = FileTextEdit(horizontal_layout_a)  
     MainButtonWidget(horizontal_layout_a, "<<", self,  
              "file = self.main_object.get_file(self.main_object)", 32)  
   
     horizontal_layout_b = HorizontalWidget(vertical_layout.layout)  
   
     Text(horizontal_layout_b, "Source Folder")  
     self.folder_text = FileTextEdit(horizontal_layout_b)  
     MainButtonWidget(horizontal_layout_b, "<<", self,  
              "file = self.main_object.get_folder(self.main_object)", 32)  
   
     horizontal_layout_c = HorizontalWidget(vertical_layout.layout)  
   
     Text(horizontal_layout_c, "Accuracy %")  
     self.threshold = SpinBox(horizontal_layout_c)  
     self.threshold.setValue(50)  
     self.threshold.setToolTip("Deviation threshold for RGB Values. Higher means more deviation but more inaccuracy")  
     Text(horizontal_layout_c, "  Pixel Steps")  
     self.samples = SpinBox(horizontal_layout_c)  
     self.samples.setToolTip("Steps between each pixel sample. Higher is faster but less accurate")  
     horizontal_layout_c.addStretch()  
   
     # OUTPUT WINDOW  
     self.output_view = OutputView(vertical_layout.layout)  
     MainButtonWidget(vertical_layout.layout, "Cancel Search", self,  
              "", 128)  
     MainButtonWidget(vertical_layout.layout, "Run Image Compare", self, "self.main_object.do_it(self.main_object)", 128)  
   
     vertical_layout.layout.addStretch()  
   
     self.tab = TabLayout([[vertical_layout, "Find Files"], [vertical_layout_list, "Results"]])  
     main_layout.layout.addWidget(self.tab)  
   
     self.main_widget.setLayout(main_layout.layout)  
     self.main_widget.show()  
     sys.exit(self.app.exec_())  
   
   
 class ImageCompare(ImageCompare_UI):  
   
   def __init__(self):  
     print "RUNNING Compare"  
     QtCore.pyqtRemoveInputHook()  
     self.run_ui(self)  
   
   

setup.py

 from distutils.core import setup  
 from py2exe.build_exe import py2exe  
   
 setup_dict = dict(  
   windows = [{'script': "main.py",  
         "icon_resources": [(1, "image_compare.ico")], "dest_base": "ImageCompare"}],  
 )  
 setup(**setup_dict)  
 setup(**setup_dict)  
   

main.py

 import ImageCompare as ic  
 compare = ic.ImageCompare()  

2. Run py2exe to compile it
In cmd type:

 python setup.py py2exe   
Unless python folder is in environment variables you will need to do this within the python27 folder.

3. Run a batch file to catch errors and iterate on your code
Create a new batch file that contains:

 ImageCompare.exe  
 pause  

Friday, 23 June 2017

UV Based Blendshape Conversion

Something we work with a lot in the Games Industry are LOD's. For those who don't know this stands for Level Of Detail and the purpose of them is to provide incrementally more or less detailed versions of a mesh and possibly skeleton based on the current view distance.
These days it is trivial to set these up using third party software but certain things do not always get taken into account. One of these is blendshapes. If our top level mesh - the one that was originally modeled - has a set of corrective shapes then these do not get transferred to the LOD's when they are created.
The point of the tool in this post is to help alleviate that issue by taking the blendshape targets on the top LOD and converting it down to each LOD in turn even though the topology is completely different.
Historically blendshape targets have always had issues working with differing topology as they require an exact match for vertex orders between shapes. If this changes the results can be diabolical.
This tool gets around this by ignoring vertex orders. One thing that each LOD has in common with the original mesh is UV layout such as in the image below. Although these do not match they are be relatively close.

It is more easy to find matches for positions in 2D space than 3D space so it makes sense to try and leverage the UV layout of each LOD to produce a better 3D representation of the mesh.
Looking into this I broke the process down to the following with the idea of converting this into a node for V2.0.

# ON BOOT (or forced update) - HEAVY
# set up iterators for the source and target objects
# get per vertex the uv indices for each mesh ( itr.getUVIndices() )
# set up array for vertex idx ---> uv idx
# set up array for the inverse uv idx ---> vertex idx
# get the UV positions for the source mesh ( MFnMesh.getUVs() )
# get the vertex positions for the source and target mesh ( MFnMesh.getPoints() )

# iterate through the target vertex index to uv index array and set up a vertex to vertex mapping ( target --> source )
# get the uv position for the target uv index
# compare this position to ALL positions in the array returned from the UV positions for the source mesh. The aim is to find the minimum eulidian distance between two sets of UV coordinates
# get the index of the UV that this value applies to and convert it to a a vertex index using the source uv to vertex index array
# mapping: vertex_map_array[target vertex index] = source_vertex_index

# EVALUATION - LIGHT

# iterate through the vertex mapping
# target_idx = current iteration
# get the source index from the current iteration index in the vertex mapping array
# get the source point position for the current index
# set the target point array at the index of the current iteration to the source point

# set all points onto the target object

This appears to work reasonably well at least as a first step.
There are potential pitfalls to take into account. For example, what do we do with target meshes that have more vertices? At the moment the closest point will be found which will mean we will probably end up with overlapping vertices. Again what do we do with meshes that have less vertices. At the moment the areas that are missing the desired geometry may not fit the source mesh nicely. These are expected issues but could potentially be circumvented. That is for another day. In the meantime take a look at the video below which shows the secret of how to turn a sphere into a torus.

Blendshape Target Convertor V1.0 from SBGrover on Vimeo.

Tuesday, 6 June 2017

Collision Based Deformer

This post briefly details three examples of a deformer that can react to collisions with another object. The end video shows all three examples in action and as ever a basic python version of the compiled plugin is included to get started with.


Direct deformation
The first example is the most basic implementation of the node to achieve direct deformation.
Using the MFnMesh::allIntersections to detect intersection between two meshes and extracting and applying the delta between the intersecting points it is possible to create an effect of direct deformation.
It is worth noting that allIntersections has some caveats.
- The first is that any mesh that you are working with must be a closed surface. This is because it calculates collision by firing a ray from a given point and calculates how many surfaces it has passed through before it dies. If it passes through one it must be inside a mesh, if two it must be outside. An open mesh has the risk of only having one hit even if the point is inside the mesh.
- The second is that as all deltas are obtained by returning the closest point on the collision objects surface from a given point on the colliding object it is possible that the returned closest point might be on the opposite side of the collision object. This is because the colliding object has travelled past a centre line switching where the closest point will now be. This will give the result of vertices snapping to the wrong side of a mesh although the effect can be quite interesting.



Secondary deformation
The second example expands on the first and adds secondary deformation. This version retains all the features of the first but also pushes the intersecting vertex out along its normal to give an idea of volume retention. This is adjustable so that the result can be extended or switched off alltogether. This deformer gives control of the falloff shape using an MRampAttribute and an attribute to define how much of the surface the effect covers. it is also possible to paint its attributes to have fine control over the end result.



Sticky deformation
The third example changes direction and stores all colliding deformed points in an array only updating their position if their delta increases. Added to this is a compute based timer that gradually returns the mesh back to its original shape unless collided with again.

Collision Based Deformer from SBGrover on Vimeo.

Below is a python implementation of the first example to get started with. This will give you the direct deformation. Be aware that as this is using Python the results are much slower than a compiled plugin so it is best not to throw this at dense geometry. Included is a helper function to build a scene with the plugin.

PLUGIN

 import maya.OpenMaya as OpenMaya  
 import maya.OpenMayaAnim as OpenMayaAnim  
 import maya.OpenMayaMPx as OpenMayaMPx  
   
 class collisionDeformer(OpenMayaMPx.MPxDeformerNode):  
      kPluginNodeId = OpenMaya.MTypeId(0x00000012)  
      kPluginNodeTypeName = "collisionDeformer"  
        
      def __init__(self):  
           OpenMayaMPx.MPxDeformerNode.__init__( self )  
           self.accelParams = OpenMaya.MMeshIsectAccelParams() #speeds up intersect calculation  
           self.intersector = OpenMaya.MMeshIntersector() #contains methods for efficiently finding the closest point to a mesh, required for collider  
   
      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 COLLIDER MESH (as worldMesh)  
                colliderHandle = block.inputValue(self.collider)  
                inColliderMesh = colliderHandle.asMesh()  
                  
                if not inColliderMesh.isNull():  
                       
                     #get collider fn mesh  
                     inColliderFn = OpenMaya.MFnMesh(inColliderMesh)  
                       
                     #get DEFORMED MESH  
                     inMesh = self.get_input_geom(block, index)  
                       
                     #get COLLIDER WORLD MATRIX to convert the bounding box to world space  
                     colliderMatrixHandle = block.inputValue(self.colliderMatrix)  
                     colliderMatrixVal = colliderMatrixHandle.asMatrix()  
                       
                     #get BOUNDING BOX MIN VALUES  
                     colliderBoundingBoxMinHandle = block.inputValue(self.colliderBoundingBoxMin)  
                     colliderBoundingBoxMinVal = colliderBoundingBoxMinHandle.asFloat3()  
                       
                     #get BOUNDING BOX MAX VALUES  
                     colliderBoundingBoxMaxHandle = block.inputValue(self.colliderBoundingBoxMax)  
                     colliderBoundingBoxMaxVal = colliderBoundingBoxMaxHandle.asFloat3()  
                       
                     #build new bounding box based on given values  
                     bbox = OpenMaya.MBoundingBox()  
                     bbox.expand(OpenMaya.MPoint(colliderBoundingBoxMinVal[0], colliderBoundingBoxMinVal[1], colliderBoundingBoxMinVal[2]))  
                     bbox.expand(OpenMaya.MPoint(colliderBoundingBoxMaxVal[0], colliderBoundingBoxMaxVal[1], colliderBoundingBoxMaxVal[2]))  
                       
                     #set up point on mesh and intersector for returning closest point and accelParams if required  
                     pointOnMesh = OpenMaya.MPointOnMesh()   
                     self.intersector.create(inColliderMesh, colliderMatrixVal)  
                       
                     #set up constants for allIntersections  
                     faceIds = None  
                     triIds = None  
                     idsSorted = False  
                     space = OpenMaya.MSpace.kWorld  
                     maxParam = 100000  
                     testBothDirs = False  
                     accelParams = None  
                     sortHits = False  
                     hitRayParams = None  
                     hitFaces = None  
                     hitTriangles = None  
                     hitBary1 = None  
                     hitBary2 = None  
                     tolerance = 0.0001  
                     floatVec = OpenMaya.MFloatVector(0, 1, 0) #set up arbitrary vector n.b this is fine for what we want here but anything more complex may require vector obtained from vertex  
                       
                     #deal with main mesh  
                     inMeshFn = OpenMaya.MFnMesh(inMesh)  
                     inPointArray = OpenMaya.MPointArray()  
                     inMeshFn.getPoints(inPointArray, OpenMaya.MSpace.kWorld)  
                       
                     #create array to store final points and set to correct length  
                     length = inPointArray.length()  
                     finalPositionArray = OpenMaya.MPointArray()  
                     finalPositionArray.setLength(length)  
   
                     #loop through all points. could also be done with geoItr  
                     for num in range(length):  
                          point = inPointArray[num]  
                            
                          #if point is within collider bounding box then consider it  
                          if bbox.contains(point):  
                               ##-- allIntersections variables --##  
                               floatPoint = OpenMaya.MFloatPoint(point)  
                               hitPoints = OpenMaya.MFloatPointArray()  
   
                               inColliderFn.allIntersections( floatPoint, floatVec, faceIds, triIds, idsSorted, space, maxParam, testBothDirs, accelParams, sortHits, hitPoints, hitRayParams, hitFaces, hitTriangles, hitBary1, hitBary2, tolerance )  
                       
                               if hitPoints.length()%2 == 1:       
                                    #work out closest point  
                                    closestPoint = OpenMaya.MPoint()  
                                    inColliderFn.getClosestPoint(point, closestPoint, OpenMaya.MSpace.kWorld, None)  
                                      
                                    #calculate delta and add to array  
                                    delta = point - closestPoint  
                                    finalPositionArray.set(point - delta, num)  
                                      
                               else:  
                                    finalPositionArray.set(point, num)  
                                      
                          #if point is not in bounding box simply add the position to the final array  
                          else:  
                               finalPositionArray.set(point, num)  
                                      
                     inMeshFn.setPoints(finalPositionArray, OpenMaya.MSpace.kWorld)  
                                           
      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(collisionDeformer())  
   
        
 def initialize():  
      gAttr = OpenMaya.MFnGenericAttribute()  
      mAttr = OpenMaya.MFnMatrixAttribute()  
      nAttr = OpenMaya.MFnNumericAttribute()  
        
      collisionDeformer.collider = gAttr.create( "colliderTarget", "col")  
      gAttr.addDataAccept( OpenMaya.MFnData.kMesh )  
             
      collisionDeformer.colliderBoundingBoxMin = nAttr.createPoint( "colliderBoundingBoxMin", "cbbmin")  
        
      collisionDeformer.colliderBoundingBoxMax = nAttr.createPoint( "colliderBoundingBoxMax", "cbbmax")  
        
      collisionDeformer.colliderMatrix = mAttr.create("colliderMatrix", "collMatr", OpenMaya.MFnNumericData.kFloat )  
      mAttr.setHidden(True)  
        
      collisionDeformer.multiplier = nAttr.create("multiplier", "mult", OpenMaya.MFnNumericData.kFloat, 1)  
        
      collisionDeformer.addAttribute( collisionDeformer.collider )  
      collisionDeformer.addAttribute( collisionDeformer.colliderMatrix )  
      collisionDeformer.addAttribute( collisionDeformer.colliderBoundingBoxMin )  
      collisionDeformer.addAttribute( collisionDeformer.colliderBoundingBoxMax )  
      collisionDeformer.addAttribute( collisionDeformer.multiplier )  
        
      outMesh = OpenMayaMPx.cvar.MPxGeometryFilter_outputGeom  
        
      collisionDeformer.attributeAffects( collisionDeformer.collider, outMesh )  
      collisionDeformer.attributeAffects( collisionDeformer.colliderBoundingBoxMin, outMesh )  
      collisionDeformer.attributeAffects( collisionDeformer.colliderBoundingBoxMax, outMesh )  
      collisionDeformer.attributeAffects( collisionDeformer.colliderMatrix, outMesh )  
      collisionDeformer.attributeAffects( collisionDeformer.multiplier, outMesh )  
   
        
 def initializePlugin(obj):  
      plugin = OpenMayaMPx.MFnPlugin(obj, 'Grover', '1.0', 'Any')  
      try:  
           plugin.registerNode('collisionDeformer', collisionDeformer.kPluginNodeId, creator, initialize, OpenMayaMPx.MPxNode.kDeformerNode)  
      except:  
           raise RuntimeError, 'Failed to register node'  
   
             
 def uninitializePlugin(obj):  
      plugin = OpenMayaMPx.MFnPlugin(obj)  
      try:  
           plugin.deregisterNode(collisionDeformer.kPluginNodeId)  
      except:  
           raise RuntimeError, 'Failed to deregister node'  
             
   
   
HELPER CODE
 #simply create two polygon spheres. Move the second away from the first, select the first and run the code below.  
 import maya.cmds as cmds  
   
 cmds.delete(cmds.ls(type='collisionDeformer'))  
 cmds.flushUndo()  
 cmds.unloadPlugin('collisionDeformer.py')  
 cmds.loadPlugin('collisionDeformer.py')  
 cmds.deformer(type='collisionDeformer')  
 cmds.connectAttr('pSphere2.worldMesh', 'collisionDeformer1.colliderTarget')  
 cmds.connectAttr('pSphere2.matrix', 'collisionDeformer1.colliderMatrix')  
 cmds.connectAttr('pSphere2.boundingBox.boundingBoxSize.boundingBoxSizeX', 'collisionDeformer1.colliderBoundingBox.colliderBoundingBoxX')  
 cmds.connectAttr('pSphere2.boundingBox.boundingBoxSize.boundingBoxSizeY', 'collisionDeformer1.colliderBoundingBox.colliderBoundingBoxY')  
 cmds.connectAttr('pSphere2.boundingBox.boundingBoxSize.boundingBoxSizeZ', 'collisionDeformer1.colliderBoundingBox.colliderBoundingBoxZ')  

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)