Joe Hultgren - Technical Artist
  • Blog
  • Demo reel
  • About Me
  • Maya Scripts (bitbucket)

Speeding up rigging code.

9/13/2017

0 Comments

 
So our game NBA 2K18 comes out Friday  so the art team has been pretty  relaxed for the past couple of weeks. With this down time I've finally had a moment to handle our largest complaint from the character team, Speed.

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.

    
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.

Final thoughts:
​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.
0 Comments

Mayapy tips

2/25/2017

1 Comment

 
I've  been  working at 2K as a technical artist for the last 4 months and I have been using mayapy a lot so I thought I'd list a few tips that have saved me a lot of time or were tricky to figure out.

  • 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 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.
​

    
  • 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.
1 Comment

5e Starter Set Maps

4/22/2016

0 Comments

 
I drew some custom maps based off the maps in the 5e starter set.  The Thunder Tree map comes with roofs to be used as tokens on top of it and versions with and without webs.
0 Comments

Custom D&D Maps

4/21/2016

1 Comment

 
These are a couple of maps  I  made for fun. They aren't tied to any campaign.
1 Comment

D&D tokens

4/21/2016

2 Comments

 
Here are a number of  tokens that I made for a D&D campaign I tried running.
2 Comments

Mixing Maya UI elements into a Qt Designer interface

10/25/2013

0 Comments

 
Joint Orient UI using qt designer and maya elements
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.
PictureInterface as seen in Qt Designer
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)
If you want the finished version of this script you can find it here: https://bitbucket.org/jhultgre/maya-scripts/src/b2b5a7c61c7c56a4ed02624e75c2efb5538e8ad0/tools/jointOrient.py?at=default

The ui file can be found here: https://bitbucket.org/jhultgre/maya-scripts/src/b2b5a7c61c7c56a4ed02624e75c2efb5538e8ad0/ui%20files/jointOrient.ui?at=default

Or go to the maya scripts link in the sidebar for all  of my maya tools.
0 Comments

Tools reel

10/20/2013

0 Comments

 
I've created an updated tools reel that you can watch here or on my demo reel page.

Joe Hultgren Maya Tools Reel from Joe Hultgren on Vimeo.

0 Comments

Pymel's attribute system

9/9/2013

0 Comments

 
So last time we looked at making some utility nodes that we can use in our scripts. Today we'll look at how to actually use them and in doing so we'll look into how pymel handles attributes.

If  your 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.
https://bitbucket.org/jhultgre/maya-scripts/src/default/shelf%20tools/setupConstraintBlend.py
0 Comments

Using utility shading nodes in Pymel

8/14/2013

3 Comments

 
When building a rig utility nodes like mulipyDivide or reverse are a handy way of setting up functionality of controls. But after you've setup an IKFK blend for what feels like the hundredth time you start looking for a way to automate connecting up all those attributes. 

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.
3 Comments

Reorganized scripts site

8/13/2013

0 Comments

 
I've been meaning to do this for awhile but I've gone back and reorganized my maya scripts site. There are now folders that group things into different categories and some of the files have been renamed to be more descriptive. I've also started adding some newer things I've worked on. The wiki page over at bitbucket explains what the folders are.
0 Comments
<<Previous

    Archives

    September 2017
    February 2017
    April 2016
    October 2013
    September 2013
    August 2013
    March 2012
    February 2012
    August 2011
    July 2011
    May 2011
    April 2011
    February 2011
    October 2010
    August 2010
    July 2010
    June 2010
    April 2010
    January 2010
    October 2009
    July 2009
    June 2009

    Categories

    All
    Art
    Blender
    Code
    Flash
    Free
    Maya
    Personal
    Pymel
    Python
    Rig
    School
    Siggraph
    Vancouver
    Vfs
    Website
    Work

    RSS Feed

    View my profile on LinkedIn
Powered by Create your own unique website with customizable templates.