- Practical Maya Programming with Python
- Robert Galanakis
- 2184字
- 2021-09-03 10:05:26
Writing a character creator
So far, we've written the code to convert a hierarchy of transforms into joints. Now we must write something that we can hook up to a menu and a workflow. An overly simplified solution is to convert the current selection by assigning the following expression into a shelf button:
map(skeletonutils.convert_to_skeleton, pmc.selection())
But that's a pretty bad high-level function. What about error handling, user feedback, and the high-level decisions that we put off while writing the converter?
What we really want is something like the following:
charcreator.convert_hierarchies_main()
This can be hooked up to a menu button, which provides a better experience for the user and a place to make those decisions that we kept out of the skeletonutils
module.
Stubbing out the character creator
Create a charcreator.py
file next to the skeletonutils.py
file in your development root, and open it up in your favorite IDE. Go ahead and type the following code, which will stub out the functions we will build in this section.
import pymel.core as pmc import skeletonutils def convert_hierarchies_main(): """'convert_hierarchies(pmc.selection())'. Prints and provides user feedback so only call from UI. """ def convert_hierarchies(rootnodes): """Calls 'convert_hierarchy' for each root node in 'rootnodes' (so passing in '[parent, child]' would convert the 'parent' hierarchy assuming 'child' lives under it). """ def convert_hierarchy(node): """Converts the hierarchy under and included 'rootnode' into joints in the same namespace as 'rootnode'. Deletes 'rootnode' and its hierarchy. Connections to nodes are not preserved on the newly created joints. """
In the preceding code we've done some basic imports and defined three higher-level functions that we know we'll need. They are documented in terms of each other, and each one is quite clear in what it does. This also gives us a good idea of the utility functions we'll need to write. For example, we will need a way to select only unique root nodes from the input to convert_hierarchies
, and a way to walk along a skeleton.
Let's begin by implementing convert_hierarchies_main
, which works on the current selection, then convert_hierarchies
, which converts a collection of hierarchies, and then finally, convert_hierarchy
, which converts a single hierarchy.
Implementing convert_hierarchies_main
The entry point of any program has traditionally been called its main. For example, the Python idiom of having if __name__ == '__main__':
at the bottom of a file asks, "is this file the script being run from the command line". Other languages may have a main
method in a binary to indicate the program's entry point when executed. We use the same convention here to specify that "this function provides some feedback (such as printing), so use this from the user interface, such as the shelf or a menu, but not from other libraries." This is just a convention I've established because I rigorously keep user interface code out of libraries. User interface not only includes graphical elements such as dialogs, but also calls to raw_input
and print statements. You can come up with your own convention, but keep UI code distinct from non-UI code.
Anyway, the implementation of convert_hierarchies_main
has no surprises:
def convert_hierarchies_main(): """'convert_hierarchies(pmc.selection())'. Prints and provides user feedback so only call from UI. """ nodes = pmc.selected(type='transform') #(1) if not nodes: pmc.warning('No transforms selected.') #(2) return new_roots = convert_hierarchies(nodes) #(3) print 'Created:', ','.join([r.name() for r in new_roots]) #(4)
This code should be self-explanatory but here's a quick breakdown:
- Get all transforms that are currently selected.
- If no transforms are selected, warn and return.
- Convert selected transforms.
- Print out the newly created roots to inform the user what was converted. Recall that the pattern of
<delimiter>.join(<string list>)
was discussed in Chapter 1, Introspecting Maya, Python, and PyMEL.
So with that function out of the way, let's get further into the actual meat of our program.
Implementing convert_hierarchies
The convert_hierarchies
function does two things: it pulls only the unique root nodes from the inputs, and invokes convert_hierarchy
on each of them. We'll start by implementing the functionality to find the unique roots.
Decomposing into composable functions
One way to pull only the unique roots from the inputs is to go through each input, and if any of its ancestors are in a collection of unique roots, it is not a unique root and can be skipped. Ancestors of a node include all the nodes between the node itself and the tree's root. For node N in the following diagram, nodes P1 and P2 are its ancestors.

Getting a node's ancestors should sound tangential to creating a character and generally useful. These are clear indicators that it should live as a utility function. Open up skeletonutils.py
and add the following function at the bottom:
def ancestors(node): """Return a list of ancestors, starting with the direct parent and ending with the top-level (root) parent.""" result = [] parent = node.getParent() while parent is not None: result.append(parent) parent = parent.getParent() return result
We know we've hit the root of the tree when node.getParent()
returns None
. We can see the ancestors function in action by walking through the following small joint hierarchy:
>>> j1 = pmc.joint(name='J1') >>> j2 = pmc.joint(name='J2') >>> j3 = pmc.joint(name='J3') >>> import skeletonutils >>> skeletonutils.ancestors(j1) [] >>> skeletonutils.ancestors(j3) [nt.Joint(u'J2'), nt.Joint(u'J1')]
Remember that nothing about this function is tied to character creation or joint conversion at all. This makes it much easier to understand and to test, and also makes the character creator code simpler.
Tip
We can clean the ancestors
function up further by changing it to use the yield
keyword. We'll look at yield
in Chapter 4, Leveraging Context Managers and Decorators in Maya.
If we think about the code to find the unique roots, we'll find that there's nothing specific to character creation here either. Let's add the following code to the bottom of skeletonutils.py
to find the unique roots of a collection of nodes:
def uniqueroots(nodes): #(1) """Returns a list of the nodes in 'nodes' that are not children of any node in 'nodes'.""" result = [] def handle_node(n): #(2) """If any of the ancestors of n are in realroots, just return, otherwise, append n to realroots. """ for ancestor in ancestors(n): if ancestor in nodes: #(4) return result.append(n) #(5) for node in nodes: #(3) handle_node(node) return result
Let's walk through this code block by block:
- There may be a better way to write this docstring, since this is a tricky algorithm to express. You'll also notice that the docstring expresses the idea, but the implementation is very different. We can very well implement the function the way the docstring is written, but it would be significantly slower and more complex. The docstring should express the what and not the how, but sometimes the two overlap (check out
help(dict.get)
). - We create a closure inside the
uniqueroots
function to handle each node. The closure "closes over" the result variable from the outer scope. We looked at closures in more detail earlier in this chapter, and will continue to use them throughout the book. Their importance cannot be overstated. - We call the closure with each node.
- If the ancestor of any input node is another input node, the node we are looking at can be ignored; it will already be handled by its ancestor.
- From inside the closure, we append to the
result
list.
Like ancestors
, the uniqueroots
function should be understandable and testable in the abstract.
>>> reload(skeletonutils) >>> skeletonutils.uniqueroots([j1, j2]) [nt.Joint(u'J1')] >>> skeletonutils.uniqueroots([j2]) [nt.Joint(u'J2')]
Since we are done with all of our utility functions, we can go back to charcreator.py
and implement our convert_hierarchies
function as follows. Notice how simple it is.
def convert_hierarchies(rootnodes):roots = skeletonutils.uniqueroots(rootnodes) result = [convert_hierarchy(r) for r in roots] return result
Implementing convert_hierarchy
Finally, we need to implement the hierarchy conversion. We already have most of its functionality through the joint converter we've already written. The only thing it has to do is delete the original hierarchy after conversion. Let's implement convert_hierarchy
inside of charcreator.py
as follows:
def convert_hierarchy(node): result = skeletonutils.convert_to_skeleton(node) pmc.delete(node) return result
It's worth pointing out that the deletion of node (and implicitly, all of its descendants) only happens if the hierarchy creation completes successfully. We wouldn't want to partially convert a hierarchy and then delete the inputs, leaving the user high and dry with a corrupt scene. We'll look more at best practices for handling errors in Chapter 3, Dealing with Errors.
Supporting inevitable modifications
At this point, the basic character creator is done. You hook it up to a shelf or menu as explained in Chapter 5, Building Graphical User Interfaces for Maya, and bask in your glory.
Or not. The tool hasn't been released for a day and already you get a feature request. Animators want the joints to be larger. But because the joint display size works well for another game the code is used for, you can't just change it globally inside the skeletonutils._convert_to_joint
function.
When you've written enough code, you tend to expect certain changes, and learn to always expect change in general. This does not mean you should build support for unneeded features just in case. In fact, this is one of the absolute worst things you can do. But you should keep an eye open to make sure your code will be able to change in likely ways, even if you don't add explicit support immediately.
One such inevitable modification is passing arguments from higher level functions (such as charcreator.convert_hierarchies
) down to implementation functions (such as skeletonutils._convert_to_joint
). However, you cannot just do this blindly. Providing an assumePreferredAngles
parameter (a keyword argument to pmc.joint
) to convert_hierarchies
wouldn't make much sense and you'd end up with a bloated codebase.
One thing we can look at providing here is some sort of configuration that callers can choose from. The knowledge of this configuration can be limited to charcreator.py
, and it can unpack its values when it prepares to call skeletonutils
. Pay attention to the highlighted code in the revised functions inside skeletonutils.py
.
def _convert_to_joint(node, parent, prefix, jnt_size, lcol, rcol, ccol): j = pmc.joint(name=prefix + node.name()) safe_setparent(j, parent) j.translate.set(node.translate.get()) j.rotate.set(node.rotate.get()) j.setRadius(jnt_size) def calc_wirecolor(): x = j.translateX.get() if x < -0.001: return rcol elif x > 0.001: return lcol else: return ccol j.overrideColor.set(calc_wirecolor()) return j def convert_to_skeleton( rootnode, prefix='skel_', joint_size=1.0, lcol=BLUE, rcol=GREEN, ccol=YELLOW, _parent=None): if _parent is None: _parent = rootnode.getParent() j = _convert_to_joint( rootnode, _parent, prefix, joint_size, lcol, rcol, ccol) for c in rootnode.getChildren(): convert_to_skeleton( c, prefix, joint_size, lcol, rcol, ccol, j) return j
All we are doing is exposing more parameters (for joint size and color) to customization. Instead of hardcoding things such as joint colors, or not even allowing the joint size to be set, we take them in as arguments and provide sensible defaults.
We can then take advantage of these newly exposed parameters in charcreator.py
.
GREEN = 14 BLUE = 6 YELLOW = 17 PURPLE = 8 AQUA = 28 # (1) SETTINGS_DEFAULT = { 'joint_size': 1.0, 'right_color': BLUE, 'left_color': GREEN, 'center_color': YELLOW, 'prefix': 'char_', } SETTINGS_GAME2 = { 'joint_size': 25.0, 'right_color': PURPLE, 'left_color': AQUA, 'center_color': GREEN, 'prefix': 'game2char_', } #(2) def convert_hierarchies_main(settings=SETTINGS_DEFAULT): nodes = pmc.selected(type='transform') if not nodes: pmc.warning('No transforms selected.') return new_roots = convert_hierarchies(nodes, settings) print 'Created:', ','.join([r.name() for r in new_roots]) #(2) def convert_hierarchies(rootnodes, settings=SETTINGS_DEFAULT): roots = skeletonutils.uniqueroots(rootnodes) result = [convert_hierarchy(r, settings) for r in roots] return result # (2) def convert_hierarchy(rootnode, settings=SETTINGS_DEFAULT): result = skeletonutils.convert_to_skeleton( #(3) rootnode, joint_size=settings['joint_size'], prefix=settings['prefix'], rcol=settings['right_color'], lcol=settings['left_color'], ccol=settings['center_color']) pmc.delete(rootnode) return result
We've added a settings
parameter to each function to allow the configuration of joint color, prefix, and sizes to be customized. Let's walk over the changes to the charcreator.py
file's code:
- Define two dictionaries that hold the configuration for different games, or characters, or whatever you want to customize.
- Add the
settings
parameter to each function so that it can be overridden where needed.
Pass the settings into the convert_to_skeleton
function. If you want to make the keys of the settings dictionaries match the keyword argument names in convert_to_skeleton
, you can rewrite the line as skeletonutils.convert_to_skeleton(node, **settings)
. It's up to you. I've chosen a more verbose way of doing things here. Check out the Appendix, Python Best Practices, for more details about using the double asterik (**
) syntax if you are unfamiliar with it.
By designing the high-level charcreator
module around a configuration concept, and making the lower-level skeletonutils
functions explicit about what arguments they take in, we've achieved several benefits. The two modules are not coupled or tied together. We can expose more parameters in the skeletonutils.convert_to_skeleton
function without having to change the charcreator.convert_hierarchy
function. We can also change the way the charcreator
functions work, and not have to change the skeletonutils
module. The high-level character creator code has lots of assumptions built in, and the low-level skeleton library code has very few. This is a powerful methodology to take, especially in Python, where you can iterate on high-level code very rapidly.
Tip
Another potential way to iterate on configuration quickly would be to move the settings
dictionary out of the code and into data files such as json
or yaml
. Then you can modify these files by hand or even build a simple editor.
Overall, we've developed a flexible and robust system that is far more powerful than its number of lines of code would indicate!