Stumbling Toward 'Awesomeness'

A Technical Art Blog

Friday, October 17, 2014

Embedding Icons and Images In Python with XPM

xpm1

As technically-inclined artists, we often like to create polished UIs, but we have to balance this with not wanting to complicate the user experience (fewer files the better). I tend to not use too many icons with my tools, and my Maya tools often just steal existing icons from Maya: because I know they will be there.

However, you can embed bitmap icons into your PySide/PyQt apps by using the XPM file format, and storing it as a string array. Often I will just place images at the bottom of the file, this keeps icons inside your actual tool, and you don’t need to distribute multiple files or link to external resources.

Here’s an example XPM file:

/* XPM */
static char *heart_xpm[] = {
/* width height num_colors chars_per_pixel */
"7 6 2 1",
/* colors */
"` c None",
". c #e2385a",
/* pixels */
"`..`..`",
".......",
".......",
"`.....`",
"``...``",
"```.```"
};

This is a small heart, you can actually see it, in the header you se the ‘.’ maps to pink, you can see the ‘.’ pattern of a heart. The XPM format is like C, the comments tell you what each block does.
Here’s an example in PySide that generates the above button with heart icon:

import sys
from PySide import QtGui, QtCore
 
def main():
    app = QtGui.QApplication(sys.argv)
    heartXPM = ['7 6 2 1','N c None','. c #e2385a','N..N..N',\
    '.......','.......','N.....N','NN...NN','NNN.NNN']
    w = QtGui.QWidget()
    w.setWindowTitle('XPM Test')
    w.button = QtGui.QPushButton('XPM Bitmaps', w)
    w.button.setIcon(QtGui.QIcon(QtGui.QPixmap(heartXPM)))
    w.button.setIconSize(QtCore.QSize(24,24))
    w.show()
 
    sys.exit(app.exec_())
 
if __name__ == '__main__':
    main()

You need to feed QT a string array, and strip everything out. Gimp can save XPM, but you can also load an image into xnView and save as XPM.

Addendum: Robert pointed out in the comments that pyrcc4, a tool that ships with PyQt4, can compile .qrc files into python files that can be imported. I haven’t tried, but if they can be imported, and are .py files, I guess they can be embedded as well. He also mentioned base64 encoding bitmap images into strings and parsing them. Both these solutions could really make your files much larger than XPM though.

posted by Chris at 3:23 PM  

Thursday, October 16, 2014

Tracing and Visualizing Driven-Key Relationships

sdk

Before I get into the collosal mess that is setting driven keys in Maya, let me start off by saying, when I first made an ‘SDK’ in college, back in 1999, never did I think I would still be rigging like this 15 years later. (Nor did I know that I was setting a ‘driven key’, or a ‘DK’ which sounds less glamorous)

How The Mess is Made

simple

Grab this sample scene [driven_test]. In the file, a single locator with two attributes drives the translation and rotation of two other locators. Can’t get much ismpler than that, but look at this spaghetti mess above! This simple driven relationship created 24 curves, 12 blendWeighted nodes, and 18 unitConversion nodes. So let’s start to take apart this mess. When you set a driven-key relationship, it uses an input and a curve to drive another attribute:

curves2

When you have multiple attributes driving a node, maya creates ‘blendWeighted’ nodes, this blends the driven inputs to one scalar output, as you see with the translateX below:

curves

Blending scalars is fairly straight forward, but for rotations, get ready for this craziness: A blendWeighted node cannot take the output of an animCurveUA (angle output), the value must first be converted to radians, then blended. But the final node cannot take radians, so the result must be converted back to an euler angle. This happens for each channel.

craziness

If you think this is retarded; welcome to the club. It is a very cool usage of general purpose nodes in Maya, but you wouldn’t think so much of rigging was built on that, would you? That when you create a driven-key it basically makes a bunch of nodes and doesn’t track an actual relationship, because of this, you can’t even reload a relationship into the SDK tool later to edit! (unless you traverse the spaghetti or takes notes or something)

I am in love with Node Edtor, but by default hypergraph did hide some of the spaghetti, it used to hide unitConversions as ‘auxiliary nodes’:

auxnode

Node Editor shows unitConversions regardless of whether aux nodes are enabled, I would rather see too much and know what’s going on than have things hidden, but maybe that’s just me. You can actually define what nodes are considered aux nodes and whether ‘editors’ show them, but I am way off on a tangent here.

So just go in there and delete the unit conversion yourself and wire the euler angle output, which you would think is a float.. into the blendWeighted input, which takes floats. Maya won’t let you, it creates the unitConversion for you because animCurveUA outputs angles, not floats.

This is why our very simple example file generated over 50 nodes. On Ryse, Maruis’ face has about 73,000 nodes of driven-key spaghetti. (47,382 curves,  1,420 blendWeighted, 24,074 unitConversion)

Finding and traversing

So how do we find this stuff and maybe query and deal with it? Does Maya have some built in way of querying a driven relationship? No. Not that I have ever found. You must become one with the spaghetti! setDrivenKeyframe has a query mode, but that only tells you what ‘driver’ or ‘driven’ nodes/attrs are in the SDK window if it’s open, they don’t query driver or driven relationships!

We know that these intermediate nodes that store the keys are curves, but they’re a special kind of curve, one that doesn’t take time as an input. Here’s how to search for those:

print len(cmds.ls(type=("animCurveUL","animCurveUU","animCurveUA","animCurveUT")))

So what are these nodes? I mentioned above that animCurveUA puts out an angle:

  • animCurveUU – curve that takes a double precision float and has a double output
  • animCurveUA – takes a double and outputs an angle
  • animCurveUL – takes a double and outputs a distance (length)
  • animCurveUT – takes a double and outputs a time

When working with lots of driven-key relationships you frequently want to know what is driving what, and this can be very difficult because of all the intermediate node-spaghetti. Here’s what you often want to know:

  • What attribute is driving what – for instance, select all nodes an attr drives, so that you can add them back to the SDK tool. ugh.
  • What is being driven by an attribute

I wrote a small script/hack to query these relationships, you can grab it here [drivenKeyVisualizer]. Seriously, this is just a UI front end to a 100 line script snippet, don’t let the awesomeness of QT fool you.

dkv1

The way I decided to go about it was:

  1. Find the driven-key curves
  2. Create a tiny curve class to store little ‘sdk’ objects
  3. List incoming connections (listConnections) to get the driving attr
  4. Get the future DG history as a list and reverse it (listHistory(future=1).reverse())
  5. Walk the reversed history until I hit a unitConversion or blendWeighted node
  6. Get it’s outgoing connection (listConnections) to find the plug that it’s driving
  7. Store all this as my sdk objects
  8. Loop across all my objects and generate the QTreeWidget

Here’s how I traversed that future history (in the file above):

 #search down the dag for all future nodes
 futureNodes = [node for node in cmds.listHistory(c, future=1, ac=1)]
 #reverse the list order so that you get farthest first
 futureNodes.reverse()
 drivenAttr = None
 
 #walk the list until you find either a unitConversion, blendWeighted, or nothing
 for node in futureNodes:
     if cmds.nodeType(node) == 'unitConversion':
         try:
             drivenAttr = cmds.listConnections(node + '.output', p=1)[0]
             break
         except:
             cmds.warning('blendWeighted node with no output: ' + node)
             break
     elif cmds.nodeType(node) == 'blendWeighted':
         try:
             drivenAttr = cmds.listConnections(node + '.output', p=1)[0]
             break
         except:
             cmds.warning('blendWeighted node with no output: ' + node)
             break
 if not drivenAttr:
     drivenAttr = cmds.listConnections(c + '.output', p=1)[0]
 if drivenAttr:
     dk.drivenAttr = drivenAttr
 else:
     cmds.warning('No driven attr found for ' + c)

This of course won’t work if you have anything like ‘contextual rigging’ that checks the value of an attr and then uses it to blend between two banks of driven-keys, but because this is all general DG nodes, as soon as you enter nodes by hand, it’s no longer really a vanilla driven-key relationship that’s been set.

If you have a better idea, let me know, this above is just a way I have done it that has been robust, but again, I mainly drive transforms.

 What can you do?

Prune Unused Pasta

pruned

Pruned version of the driven_test.ma DAG

By definition, when rigging something with many driven transforms like a face, you are creating driven-key relationships based on what you MIGHT need. This goes for when making the initial relationship, or in the future when you maybe want to add detail. WILL I NEED to maybe translate an eyelid xform to get a driven pose I want.. MAYBE.. so you find yourself keying rot/trans on *EVERYTHING*. That’s what I did in my example, and the way the Maya SDK tool works, you can’t really choose which attrs per driven node you want to drive, so best to ‘go hunting with a shotgun’ as we say. (shoot all the trees and hope something falls out)

Ok so let’s write some code to identify and prune driven relationships we didn’t use.

CAUTION: I would only ever do this in a ‘publish’ step, where you take your final rig and delete crap to make it faster (or break it) for animators. Also, I have never used this exact code below in production, I just created it while making this blog post as an example. I have run it on some of our production rigs and haven’t noticed anything terrible, but I also haven’t really looked. 😀

def deleteUnusedDriverCurves():
    for driverCurve in cmds.ls(type=("animCurveUL","animCurveUU","animCurveUA","animCurveUT")):
        #delete unused driven curves
        if not [item for item in cmds.keyframe(driverCurve, valueChange=1, q=1) if abs(item) > 0.0]:
            cmds.delete(driverCurve)
 
deleteUnusedDriverCurves()

This deletes any curves that do not have a change in value. You could have multiple keys, but if there’s no curve, let’s get rid of it. Now that we have deleted some curves, we have some blendWeighted nodes that now aren’t blending anything and unitConversions that are worthless creatures. Let’s take care of them:

def deleteUnusedBlendNodes():
    #rewire blend nodes that aren't blending anything
    for blendNode in cmds.ls(type='blendWeighted'):
        if len(cmds.listConnections(blendNode, destination=0)) == 1:
            curveOutput = None
            drivenInput = None
 
            #leap over the unitConversion if it exists and find the driving curve
            curveOutput = cmds.listConnections(blendNode, destination=0, plugs=1, skipConversionNodes=1)[0]
            #leap over the downstream unitConversion if it exists
            conns = cmds.listConnections(blendNode, source=0, plugs=1, skipConversionNodes=1)
            for conn in conns:
                if cmds.nodeType(conn.split('.')[0]) == 'hyperLayout': conns.pop(conns.index(conn))
            if len(conns) == 1:
                drivenInput = conns[0]
            else:
                cmds.warning('BlendWeighted had more than two outputs? Node: ' + blendNode)
 
            #connect the values, delete the blendWeighted
            cmds.connectAttr(curveOutput, drivenInput, force=1)
            cmds.delete(blendNode)
            print 'Removed', blendNode, 'and wired', curveOutput, 'to', drivenInput
 
deleteUnusedBlendNodes()

We find all blendWeighted nodes with only one input, then traverse out from them and directly connect whatever it was the node was blending, then we delete it. This is a bit tricky and I still think I may have missed something because I wrote this example at 2am, but I ran it on some rigs and haven’t seen an issue.

Here are the results running this code on an example rig:

pruned_graph

Create a Tool To Track / Mirror / Select Driven Relationships

This is a prototype I was working on at home but shelved, I would like to pick it up again when I have some time, or would be open to tossing it on github if people would like to help. It’s not rocket science, it’s besically what Maya should have by default. You just want to track the relationships you make, and also select all nodes driven by an attr. Also mirror their transforms across an axis (more geared toward driven transforms).

sdkWrangler

Write Your Own Driver Node

Many places create their own ‘driven node’ that just stores driven relationships. Judd Simantov showed a Maya node that was used on Uncharted2 to store all facial poses/driven relationships:

facePoseNode

The benefits of making your own node are not just in DAG readability, check out the time spent evaluating all these unitConversion and blendWeighted nodes in a complex facial rig (using the new Maya 2015 EXT 1 Profiler tool) –that’s over 760ms! (click to enlarge)

profiler

Though it’s not enough to make a node like this, you need to make a front end to manage it, here’s the front end for this node:

poseFaceUI

Give Autodesk Feedback!

feedback

As the PSD request is ‘under review’, maybe when they make the driver, they can make a more general framework to drive things in Maya.

Conclusion

As you can see, there are many ways to try to manage and contain the mess generated by creating driven-key relationships.  I would like to update this based on feedback, it’s definitely not an overview, and speaks more to people who have been creating driven-key relationships for quite some time.  Also, if you would find a tool like my Maya SDK replacement useful, let me know, especially if you would like to help iron it out and/or test it.

posted by Chris at 3:16 PM  

Wednesday, September 24, 2014

Maya DAG nodes in QT tools (don’t use string paths)

fragile

 

String paths to Maya objects are _fragile_. This data is stale as soon as you receive it, long paths are better than short, and taking into account namespaces is even better –BUT when it comes down to it, the data is just old as soon as you get it, and there is a better way.

A Full Path that is Never Stale

If you store a node in a Maya Python API MDagPath object, you can ask at any time and get its full path. Because it’s basically a pointer to the object in memory. Here’s an example:

import maya.OpenMaya as om
 
#create and populate an MSelectionList
sel = om.MSelectionList()
sel.add(cmds.ls(sl=1)[0])
 
#create and fill an MDagPath
mdag = om.MDagPath()
sel.getDagPath(0, mdag)
 
#At any time request the current fullpath to the node
print mdag.fullPathName()

CHALLENGE: Can you write a renaming tool that doesn’t use string path node representation? That works with all DG nodes? Without using PyMel? 😉

Embedding Maya Python Objects in PyQT (UserRole)

So.. if we can store a pointer to an object, how do we track that with QT UIs, pass it around, etc? It can be really difficult to find a way to be responsible here, you have all these lists and node representations, and in the past I would try to sync lists of python MDagPath objects, or make a dictionary that mirrors the listView… but there is a way to store arbitrary python objects on list and tree widgetitems!

In this code snipet, I traverse a selection and add all selected nodes to a list, I add ‘UserRole’ data to each and set that data to be an MDagPath:

    listWids = []
    for item in nodes:
        sel = om.MSelectionList()
        sel.add(item)
        mdag = om.MDagPath()
        sel.getDagPath(0, mdag)
        name = mdag.fullPathName()
        if not self.longNamesCHK.isChecked():
            name = name.split('|')[-1]
        wid = QtGui.QListWidgetItem(listWid)
        wid.setText(name)
        wid.setData(QtCore.Qt.UserRole, mdag)
        listWids.append(wid)

Now, later when we want to get the full path name of this widgetItem, we just ask it for this data. That returns the MDagPath object, and we can ask the object for the current full path to the node:

print self.drivenNodeLST.currentItem().data(QtCore.Qt.UserRole).fullPathName()

So this is a good way to have arbitrary data that travels wit the QT node description, which is usually some kind of widget.

posted by Chris at 12:40 PM  

Tuesday, May 27, 2014

PyQt: Composite Widgets

customWid2

So the past few nights I was racking my brain a bit to get multiple widgets adding to a listview. I wanted to see a list of animations, each item in the list needed to have clickable buttons, and special labels.

I scoured the internets, and dusted off my old trusty ‘Rapid GUI Programming with Python and QT‘ book, I got the idea for the above from the ‘Composite Widgets’ chapter subsection, though they don’t use setItemWidget to insert a composite widget into another widget.

Here is what my QtDesigner file looked like:

customWid

I wanted to dynamically load a UI file of a custom widget and compile it with the UIC module. I first looked at making a delegate, but I just could not get that working, if you have done this with a delegate, let me know in the comments! (From the docs, it seems delegates cannot be composites of multiple widgets)

In the end I used pyuic4 to compile the above UI file into a python code, I dumped this, minus the form/window code, into a class I derive from QWidget:

class animItemWidget(QtGui.QWidget):
    def __init__(self, parent=None):
        super(animItemWidget, self).__init__()
        self.horizontalLayout_4 = QtGui.QHBoxLayout(self)
        self.horizontalLayout_4.setSpacing(2)
        self.horizontalLayout_4.setMargin(3)
        #blah, blah, blah

At the bottom of that length UI frenzy of an init, let’s connect a button to a function:

self.connect(self.button02, QtCore.SIGNAL("clicked()"), self.awesome)

Now define that function, let’s just print that the animation that the widget in the list whose button you clicked is AWESOME:

    def awesome(self):
        print self.label.text() + ' is awesome!'

This could do anything with the anim name or various data bound to this object, like check out/sync a file from Perforce, load a file in Maya, etc.

Now let’s make our main window. We are going to use setItemWidget to insert our animItemWidget into the QListWidget called ‘list’. Notice that I have access to every UI element in the composite widget.

from PyQt4 import QtGui, QtCore, uic
 
class uiControlTest(QtGui.QMainWindow):
    def __init__(self):
        super(uiControlTest, self).__init__()
        self.ui = uic.loadUi('uiControlTest.ui')
        self.ui.show()
 
        for i in range(0,100):
            wid = animItemWidget()
            wid.label_2.setText('Last edited by chrise @2014.06.21:23:17')
            wid.label.setText('Animation ' + str(i))
 
            wid2 = QtGui.QListWidgetItem()
            wid2.setSizeHint(QtCore.QSize(100, 40))
            self.ui.list.addItem(wid2)
            self.ui.list.setItemWidget(wid2, wid)

Now, of course, in my example I just quickly made a bunch of widgets, so their names are all default, but you get the idea. If you have a better way to do this, perhaps something more performant, please let me know in the comments.

Note: It looks like that book is freely available on a college class website, save yourself 50 bucks: http://www.cs.washington.edu/research/projects/urbansim/books/pyqt-book.pdf

posted by Chris at 2:38 AM  

Powered by WordPress