Saturday 29 October 2016

Subfolders, referencing - Yes, its more Maya scripting fun!

In the last couple of weeks, I've had to modify and tweak all the scripts that I developed for my students to automate file management from Maya.  In the process, I added new features and added extra functionality to handle some of the changes that this class had decided to implement.

One of my students asked me if I would be sharing these things on my blog - so yup, here we go - again...  I have a few, but to keep these posts nice and short I'll start with just a couple.

Collecting subfolders

To manage correct file locations and naming standards, I've always used a custom file saving tool rather then the standard Save/Save As.  This was in an effort to alleviate the headaches surrounding students doing things 'their way' and breaking paths, etc.

In previous years I'd specified a fairly basic folder structure where all files would be stored in the Maya project.  This year, additional subfolders were added and this meant I had to have a mechanism within my custom saver tool to make these accessible.

All I had to do was extract the subfolder paths into a selectable drop-down list that they could select from.  To read in a subfolder structure from a root path is actually a pretty simple task, handled by python's os.walk() function.

When used, this walks through a root path, and scans for files and folders.  It returns three values for each folder it finds as a list of tuples.  Each tuple contains the folders' physical path, a list of subfolder names that were in the folder, and a list of file names.  We can see what this returns with this simple piece of code.

import os

# Lets collect file structure for a simple directory

getStructure = os.walk("C:\\temp")

# Print the details

for item in getStructure:
    print "directory was : ",item[0]
    print "subdirectories : ", item[1]
    print "files :", item[2]

One powerful feature of python is list comprehension, or the ability to populate a list with data collected through calling a function.

This mean't I could automatically create a list of subfolder structures by calling the os.walk() function within a list itself!  This python functionality is powerful stuff.  I could collect the subfolder structure in just one line of code.

import os

rootPath = "C:\\temp"


# Use os.walk to grab the paths as we walk through them then

# subtract the rootPath - and store into a list called 'subD'.
#
# All done through list comprehension. Cool python stuff!

# Remember that the first tuple value is the path (index 0)

# Read path string starting from the subfolder name after rootPath
subD = [p[0][len(rootPath):] for p in os.walk(rootPath)]

print "Subdirectories found under ",rootPath

for item in subD:
    # First entry usually appears blank being the root
    if not item == '':
        print item

Reference duplication outside the Reference editor

One interesting question I was asked was how to easily duplicate references in a scene.  Using the normal duplicate command (ctrl d) on any referenced item simply creates a physical (non referenced) duplicate of the geometry.

Sure, you can duplicate references from the reference editor window, but this is a slow and inefficient way to work if you are planning to do it consistently.  Keyboard hotkeys would be the answer - however setting this up to be called from a keyboard shortcut requires a little code.  There was no simple "duplicate reference" call...

Basically, a reference is simply a file that is accessed externally from disk when the scene is loaded.  Of course that clue in itself meant the answer was obviously related to file handling.  Maya's file command is one large, feature-packed function.  It handles file management, and I've mostly just used it in scripts to save the current Maya file from a script...  However it is also the function used to load in files as references...

For this particular task, I used MEL rather than Python as it seemed less clunky when it comes to creating commands in Maya's hotkey editor.  It takes three steps - grab the selected reference, determine its filename and Load the reference again (ie. becoming a duplicate).

I liked that using this approach also allowed me to set a namespace - knowing which references were duplicates could come in useful later along the production line for diagnosing any problems, or perhaps to clear/import/refresh these particular references with a custom tool.  I used the namespace dupReference to tag a reference.

// Grab the selected items
string $selR[] = `ls -selection`;
// Get the filename for the referenced item
string $path = `referenceQuery -filename $selR[0]`;
// load the file in as a reference, namespace as dupReference
file -r -ns "dupReference" $path;

More reference fun

I like the idea of having the ability to extract stats and information from a scene, especially when it comes to diagnosing problems during a project.  Referencing is a great feature, but it can (and does) get a tad glitchy.  I figured this would be a start to maybe a more in-depth tool kit later on.

Now that I had a specific namespace for my duplicates, reporting how many duplicate references were present in a scene seemed to be something to consider.  Reference node names are constructed as nameSpaceRN.  Much like any duplication command in Maya, each duplicate has an incremental value at the end to make sure the names are unique.

To count references in this example, the namespace was extracted from a list of reference nodes in the scene and set as a dictionary key, storing a count value of the number of occurences of the namespace.

import maya.cmds as cmds

# Grab a list all references in the scene

allObj = cmds.ls(type='reference')

# Create a dictionary of name spaces with their counts

refNS = {}

# Loop through all the references, and count them up

for item in allObj:

    # Extract the name spaces. We test for 'RN' first

    # as other types of reference nodes can exist.
    if 'RN' in item:

        # Extract the namespace without the RN at the end.

        indx = item.index('RN')
        nspace = item[:indx]

        # Dictionary keys can't have spaces in them.

        nspace.replace(' ','_')

        # Increase the counter for the namespace.  Note that if the

        # namespace is new, we need to set a starting value or we'll
        # just get an error if we try and increment an unknown value
        if nspace in refNS.keys():
            # Increase counter
            refNS[nspace] += 1
        else:
            refNS[nspace] = 1

print "Statistics : References loaded in this scene"            

for ref in refNS:
    print ref," : ", refNS[ref]

The above code can be easier for people who are relatively new to Python (or even programming) to grasp.  The logic is clear, the code is simple - but we can optimise and streamline this code a little more...

Doing it the Python Way

As we saw back in the subfolder section, I used list comprehension - a powerful technique - to process and create a list of subfolders using functions within the list itself.  We can actually apply this to a lot of stuff!  Here's how we replicate some of the previous code in way less lines...  Be prepared to be wowed (or just plain confused).

refNS=[ns[:ns.index('RN')] for ns in cmds.ls(type='reference') if 'RN' in ns]

That one line replaces the chunk of code used to extract the nameSpaces from the list of reference nodes. Its doing pretty much the exact same thing as the code before, but in just one line.  It creates a list of the referenced files (the ones that have RN at the end as explained prior) with the RN stripped off as returned by the cmds.ls() command.  However it only does that if there is RN in the actual name.  If you follow that logic, hopefully that should make sense...

Making sure we use the right parameters!

Before I move on,  its worth noting how much more you can learn from spending a little more time reading documentation...  I found that originally using a typ='reference' with cmds.ls() will return all reference related nodes in the scene.  This is fine, however what you do need is that whole filtering with the 'RN' check to strip out those references that relate to files.  Hence the previous code...

Removing that typ parameter, and replacing it with referenceNodes=True will return just the referenced file nodes - saving us the need to do that whole filtering approach.  That saves a lot of additional code...  Always worth looking a little closer at the documentation sometimes.

Counting up the polys

Referencing is great when using items that may be edited during a project such as rigs, etc, but for a completed and finished set containing just props, importing geometry into the scene may offer a more stable scene for rendering, as well as less network traffic (one file rather then many) on our render farm.  I've seen a lot of odd issues arise when using lots of references in the past - from broken rendering (objects that look a tad wonky (eh, render incorrectly)) to animation screwing up for no apparent reason on a referenced character rig.

It gets even worse when you start seeing people referencing in files with referenced references...  A reference inception of sorts...

I was interested in getting a total value for the number of faces that my references took up.  The reason for this is that say, I wanted to give my students the option to mass-import references into a scene, I could check to see just how heavy all that referenced geometry would be.

Lets do it!

First of all, we need to create a list of all referenced meshes (those things that contain faces, obviously) which can be done by adding a type='mesh' to our cmds.ls() command, along with the referenceNodes=True.  To make the code shorter, I've used the short names for those parameters (rn and typ)

cmds.ls(rn=True, typ='mesh')

Once we know this, we can create a list of polygon counts by simply asking for the number of polygons using cmds.polyEvaluate(object, f=True).  We then just sum up the total of all the values in our list.  Luckily Python has a sum() function exactly for such a task.

Here is the complete code to return a poly count for all referenced objects.  Its short, sweet and very python-esque in its approach.  Plus its fast!

t=sum([cmds.polyEvaluate(obj,f=True) for obj in cmds.ls(rn=True, typ='mesh')])

And I'm done...

So there you have another very small collection of tips that I've found came in handy the last few weeks.  I have more, and I'll share these up soon.  Until then, I'm back to coding some tools...

0 comments:

Post a Comment