官术网_书友最值得收藏!

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:

  1. Get all transforms that are currently selected.
  2. If no transforms are selected, warn and return.
  3. Convert selected transforms.
  4. 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:

  1. 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)).
  2. 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.
  3. We call the closure with each node.
  4. 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.
  5. 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:

  1. Define two dictionaries that hold the configuration for different games, or characters, or whatever you want to customize.
  2. 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!

主站蜘蛛池模板: 泽普县| 永年县| 资溪县| 双鸭山市| 怀柔区| 台南县| 邯郸县| 崇文区| 福建省| 洮南市| 昭平县| 湄潭县| 榆社县| 慈利县| 华安县| 高青县| 莲花县| 长子县| 香港 | 固镇县| 西畴县| 长岭县| 洛扎县| 永善县| 尚义县| 恩施市| 镇赉县| 南京市| 北票市| 马鞍山市| 瓮安县| 天镇县| 苍溪县| 乌海市| 陈巴尔虎旗| 麻阳| 措美县| 定安县| 来宾市| 方山县| 峨边|