The main complaints about speed were that our rigging code took too long and our tools felt unresponsive .
I started by running cProfile on our rigging code to find any bottlenecks. The starting profile showed that it was talking about 96 seconds to rig my test character. It also showed that we were spending a lot of time doing file I/O and making blendshapes.
I noticed that we were reading in a bunch of files, moving them all by some amount, then saving them out to use later. I saved about 10 seconds by saving out the transform values in a text file then applying it right before we need to use each object in the rig. There was some cleanup code that was run on each of those intermediate files that need to be modified to run at import time instead of on a clean file, but that was fairly straight forward.
We make tons of blendshapes by wrap deforming things to the main geo. I saved 5 seconds by not making blendshapes were we didn’t need to. So no right side shapes on meshes that are only affected by the left side.
I tried checking about 10 percent of the verts on the meshes to see if anything moved before making a blendshape. Unfortunately vert look up is pretty slow in pymel so it didn’t really save any time.
I also switched from duplicating the wrapped mesh and adding all the blendshapes at once to repeatedly adding one blendshape from the wrapped mesh to the final mesh, breaking the connections between them, and renaming the alias by hand. It didn’t save much time either but I felt more confident that the blend shape names wouldn’t be renamed by maya.
I also wrote my own code to import objs for blend shapes. Once I have one of the blendshapes loaded I duplicate it then I only need to read the vert positions of every other shape and set the point on the duplicate. It was just barely faster but I think I might be able to improve it with multithreading.
We also have some generic blendshapes that most characters use. I have a modified version of the blendshape import code that calculates the character specific versions of those files using numpy arrays. Using arrays of vert positions you can get the final blend shape’s vert positions with code like this. We calculate the characters delta first so we can reuse it for all the generic shapes.
character_shape_delta = character_base_shape - generic_baseshape
for generic_pose in generic_poses:
generated_blendshape = characer_shape_delta + generic_pose
Finally I slimmed down some of the partial rigs we use to generate things, saving another 10 seconds or so there.
In the end I ended up saving 30 second with my best test time of 66 seconds. I was hoping to get under a minute, but I hit a brick wall there and I was ready to move onto speeding up our tools which I will talk a little bit about later.
Most of our rigging code goes back at least 4 years and when you are releasing a yearly game a lot of things get added and removed from it every time, leaving a lot of cruf sitting around. I think every time I tried to answer a “TODO: why are we doing things this way” I speed things up a little bit, or at least I could provide an actual answer to the question.
]]>Make a mayapy build system in Sublime Text. Hitting ctrl+b is a lot faster then going to the cmd line to launch your program. If you’re using MayaMaya[https://github.com/danbradham/manymaya] write a function that just takes a tuple and unpacks it for your main function. It makes it easier to go back and forth between maya and mayapy and you can catch and log errors in this function.
@instance
def batch_args(args):
actual_function(*args) # unpack the args to the function
@instance
def batch_kwargs(kwargs):
actual_function(**kwargs) # unpack the keyword arguments to the function
If you want to use a normal python subprocess for something, you need to pass subprocess.Popen the environment variables you want it to use. Specifically os.environ['PYTHONHOME']
needs to get set back to your python’s install folder.
]]>
]]>
]]>
One of the nice things in Maya is being able to make interfaces for tool using Qt and Qt Designer. Unfortunately not everything in Qt has a corresponding UI element in maya, even for some things that you think there would be. One most glaring omissions is that a Qt float spinbox doesn’t get mapped to a maya float field.
While recreating the Comet Joint Orient tool over to python I ran into this issue while trying to replicate the world up and tweak vector fields. If you just want to get the value from a float spinbox you can by routing its information into something maya does know how to talk to, usually a line edit, using Qt signals/slots mechanism. However the comet version of the tool has buttons that set the field to a given preset vector and I couldn’t get away with only reading the values. The obvious solution to that is to try creating float fields in maya and add them to the Qt interface.
In order to add new elements to the interface we need to know which layout to we want to put things in. For this tool we have two layers of nested layouts in Qt Designer, horizontal layouts for each row of elements and a vertical layout that hold all the horizontal layouts. If we take the Show Axis button for example and ask it what layout its in we would expect something like JointOrient | verticalLayout | horizontalLayout1. Lets see what we get. |
win_name = pm.loadUI(uiFile='path/to/ui/file')
# get buttons in interface
buttons = [x for x in pm.lsUI(type='button') if win_name in x]
print next(x for x in buttons if 'show_axis' in x).getParent()
# Result
JointOrient|verticalLayout
Wait a minute were is our horizontal layout? So it turns out when you have nested layouts maya decides to only see the parent layout. This obviously causes us a problem when we are trying to find a specific layout, so what do we do.
The answer to this problem is to organize our interface using widgets instead of layouts, at least in the places where we want to add things after loading the ui file. Once we have a widget we can set it to have a horizontal layout for the things we add.
Now with our widget layout lets try the same thing that we did with the show axis button. Make a button inside the widget to look for. We are expecting something like JointOrient | verticalLayout | widget. |
win_name = pm.loadUI(uiFile='path/to/ui/file')
# get buttons in interface
buttons = [x for x in pm.lsUI(type='button') if win_name in x]
print next(x for x in buttons if 'world_placeholder' in x).getParent()
# Result
JointOrient|verticalLayout|horizontalLayout_5
Alright so now we have a horizontalLayout but why didn’t our widget show up in the path? Whats going on is that maya doesn’t see the widget, but since it sees the top layout in a widget we can still get what we want. Since we have a reference to the layout from our place holder button we can just delete the button and use the reference as the parent for the new elements we add.
win_name = pm.loadUI(uiFile='path/to/ui/file')
# get buttons in interface
buttons = [x for x in pm.lsUI(type='button') if win_name in x]
# get location for where world vector goes and get rid of placeholder
world_placeholder = next(x for x in buttons if 'world_placeholder' in x)
world = world_placeholder.getParent()
pm.deleteUI(world_placeholder)
# make floatFields and buttons using normal ui commands
pm.text(label='World Up Dir:', p=world)
world_x = pm.floatField(v=1.0, p=world, pre=2)
world_y = pm.floatField(v=0.0, p=world, pre=2)
world_z = pm.floatField(v=0.0, p=world, pre=2)
pm.button(label='X', p=world, c=set_world_x)
pm.button(label='Y', p=world, c=set_world_y)
pm.button(label='Z', p=world, c=set_world_z)
2023/03/05 I had links to full scripts on a bitbucket repo that is now dead. I probably still have the code somewhere but haven’t had time to rehost it anywhere
]]>If you’re coming from mel or maya.cmds the syntax you’re used to to access attributes will look this.
# mel
getAttr cube.translateX;
# maya.cmds
cmds.getAttr('cube.translateX')
In pymel we can get an attribute in the same way as maya.cmds.
pm.getAttr('cube.translateX')
However due to pymel representing everything as a PyNode object we have a few other ways to get attributes.
# .attr method
mycube = pm.polyCube()[0]
mycube.attr('translateX')
# short hand method
mycube.translateX
mycube.tx
# node constructor method
pm.PyNode('cube.translateX')
pm.Attribute('cube.translateX')
The .attr method looks pretty similar to getAttr except that you call it as a method on a PyNode. The common reason to use this is when you don’t know what attribute you want when writing your code. You might be getting which attribute you want at runtime from either an interface or a function.
Using the short hand syntax is convenient when know what attribute you want from the start. You can use either short names or long names of any attribute including attributes you have added yourself. I usually use this way because it makes my code easier to type and read.
The third way of getting an attribute is using node constructors. I’ve listed two ways to use method pm.PyNode and pm.Attribute. Both of these return the same thing an attribute object. In fact the .attr and short hand syntax also return attribute objects.
Having these attribute objects lets us easily keep track of the attribute we want no matter what else if going on in our scene. We can rename or change the parent of the object it is attached too and the attribute object will still point to the correct place.
In order to use our attribute there are a few methods to know.
myattr = mycube.translateX
# get and set values
myattr.get()
myattr.set(4)
# set and break connections
myattr.connect(mysphere.translateY)
myattr.disconnect(mysphere.translateY)
# short hand for connections
myattr >> mysphere.translateY
myattr // mysphere.translateY
Now .get and .set should be pretty self explanatory, they let you find out or change the value of the attribute.
If you want to change connections to an attribute you have a choice in syntax either long form or short form. For making connections I generally use the short form because its faster to type, but if you don’t do a lot of scripting you may want to use the long from so you remember what you are doing.
When disconnecting connections I always use the long from. This is a little bit preference for me because I like to be more verbose when I’m getting rid of something and also because the .disconnect methods has a few options that the short hand syntax doesn’t.
# disconnect a certain connection
myattr.disconnect(mysphere.translateY)
# disconnect all connections
myattr.disconnect()
# disconnect all inputs
myattr.disconnect(inputs=True)
# disconnect all outputs
myattr.disconnect(outputs=True)
If you want more information on attributes check out the article in the pymel docs. http://download.autodesk.com/global/docs/maya2014/ja_jp/PyMel/attributes.html
If you want to see an example of using attributes you can look at a simple script I made to setup a blend attribute for a selection of constraints.
2023/3/5: there was link to a bitbucket repo that no longer exists. I still have the code somewhere, but haven’t had time to rehost it anywhere
]]>If you go to the pymel docs and search for how to create these nodes you probably wont find what you where hoping for. While the latest docs have information on the nodes themselves there aren’t any good examples like there are for most of the other pymel commands.
Since the docs aren’t helping lets go back to Maya and look at what is output to the script editor when we make these nodes by hand. Open up the hypershade or node editor and make some of the nodes we want. You should see something like this.
shadingNode -asUtility reverse;
shadingNode -asUtility multiplyDivide;
shadingNode -asUtility vectorProduct;
Looking at these they are all made with the shadingNode command. If we go back to the pymel docs we can find a command that matches up in rendering/shadingNode. The examples are still pretty sparse but essentially we use the command like this.
import pymel.core as pm
pm.shadingNode('[NODETYPE]', [NODECLASSIFICATION]=True)
For [NODETYPE] we enter what node we want as a string, in our case ‘reverse’ or ‘multiplyDivide’. [NODECLASSIFICATION] is needed to tell maya where to put this node in the hypershade. We use as asUtility=True since we are making utility nodes, if you are making a light or shader you should use asLight or asShader respectively following the docs. So when we fill those things in our code will look like this.
utility = pm.shadingNode('reverse', asUtility=True)
The command returns a pynode which contains the node made which we can store in a variable in this case utility. With our node safely in a variable we can use it however we want.
Now that we have our utility nodes how do we hook it up?
To do this we have to look into pymel’s attribute system which I’ll be getting into next time.
]]>