Friday, December 7, 2012

Alternative to maya.standalone

Recently I had a challenge to write a tool that would export scene information of a maya file. I had the tool working for a while, however it needed to have maya GUI open so as to be able to load the files that it needed to export. The problem I faced this time around was the need for the exporter to work in the background as it needed to open a different file to the one currently being used. From the get-go I wanted to avoid using mayabatch as I didn't want to go the MEL path. After some research, the alternative that I found was maya.standalone module. All seemed fine and dandy, but after some time testing this module, I found that there was a major drawback to maya's standalone module. The very problem is that as soon as you try to load any file that utilizes Mental Ray, or just loads the Mayatomr.mll plugin, the remote maya crashes. There are alternatives to bypass that, but it basically means making sure that the file you are loading in no way requests for that plugin. If you are using ".ma" files, one way is to make sure that there is no requirement request for the Mayatomr.mll. Just search the file in a text editor (of course only works if you are using a maya ASCII '.ma' format), and delete the line if it is there. However, keep in mind that if you do have something in the scene that requires that plugin, you will get errors, and many things might become unstable and rather unpredictable. In my case, I could not bypass the MR issue. We are using MentalRay, and there is no way for me to go through nearly a 100 assets and adjust every one of them. Being desperate I started googling everywhere for a possible alternative. I did notice that some people mentioned the use of pymel for their batching, but unfortunately noone ever clarified what they meant, and how exactly they utilized pymel. I used python's subprocess module to call for a python script to be run by mayapy. Here is what the call looks like:
import subprocess
import dataExporter #this is the script I want to run remotely
import sys
import os.path

paths = sys.path
rootPath = None
for path in paths:
    if os.path.exists(path + "\\Maya.exe"):
        rootPath = path + "\\mayapy.exe"
        break

scriptPath = dataExporter.__file__
myVar = "Hello World"
command = '{0} {1} {2}'.format(rootPath, scriptPath, myVar)
mayaVar = subprocess.Popen(command, stdin=subprocess.PIPE,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE)
subprocess.Popen.wait(mayaVar)
a, b = mayaVar.communicate()
if mayaVar.returncode:
    print b
else:
    print a

First of all, I need to give credit to Henry Foster for a wonderful post that allowed me to clarify the use of subprocess and how to get the data from it. Here is his original post.
Now I'll take a bit to clarify what I am doing in the code. The first part:
paths = sys.path
rootPath = None
for path in paths:
    if os.path.exists(path + "\\Maya.exe"):
        rootPath = path + "\\mayapy.exe"
        break
This piece of code simply figures out where your location for mayapy.exe is. Pretty straight forward. Now for some fun stuff:
scriptPath = dataExporter.__file__
myVar = "Hello World"
command = '{0} {1} "{2}"'.format(rootPath, scriptPath, myVar)
mayaVar = subprocess.Popen(command, stdin=subprocess.PIPE,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE)
subprocess.Popen.wait(mayaVar)
output, errors = mayaVar.communicate()
if not mayaVar.returncode:
    print output
else:
    print errors
Here I am looking for the full path to the script I am going to run in the subprocess, and then initialize the subprocess itself. The "mayaVar" variable holds for us the output that that happens inside the subprocess. As you can see from the code, I am passing to the subprocess first the location of mayapy.exe, then the location of the script I want mayapy to run, and then I pass it the single variable that the script is going to accept. I also open up the PIPE from the subprocess to send and receive values between our mayapy. Then comes a rather important part of the script where I request the subprocess to wait for itself to finish the job. Now that part isnt' always necessary. However, my tool needed for the exported data to be available to continue, so I couldn't allow maya to just go on. If the wait method is not envoked, your subprocess will run just fine, but the output values from the mayaVar will not be available right away. In some cases, i.e. batch exporting, you probably don't even want to wait as you might be doing some other logic after the batch has been sent off. Keep in mind that invoking wait() will cause maya to stall and wait for the output from your subprocess and hence will not allow any user interaction during that time. Once my data is exported I check whether the process was a success at all, and if any errors were thrown. That is done via subprocess' "returncode": 1 - True, some errors have occured, 0 - False, nothing went wrong, all is good (I found this quite unintuitive at start, but got used to it). Now once your process is done, you can assign the subprocess outputs to some variables. And finally here is the actual variation of the 'dataExporter' file that can give you some sort of output:
import sys
import pymel.core as pm


def replyToMessage(message):
    reply = ""
    if message == "Hello World":
        reply = "Hello to you too!"
    elif message == "Goodbye":
        reply = "Leaving already? Ah well, take care!"
    else:
        reply = "I am sorry, not sure what you just said..."
    sys.stdout.write(reply)
    return reply


if __name__ == "__main__":
    replyToMessage(sys.argv[1])
By just importing the pymel.core you initialize a specialized maya.standalone. The 'sys.stdout.write' will write you the output that you will get back. Keep in mind, all of the feedback you get will be in string format. I do not think there is any way to pass more complex data between this script being run and the current maya session you are running as a user without any socket connection. Anyway, hope someone finds this helpful at some point! Till next time, DK

Monday, July 2, 2012

Some "Mind Glitches" about MFnSkinCluster and MPlug...

It's been a while since anything appeared here, but that is due mainly because in the last half a year I have quite seriously moved more and more towards the technical side of life in 3D, and so there weren't so many updates to show for drawing...

However, I now intend to write a little on what I am finding now that I am being more involved in the tools and plugin dev at my new job at Bron Studios.

I have lately been digging more and more into different areas of Maya's API, and there are certain things I am sure I am bound to forget and will need again some day (or perhaps someone else will be looking for the explanations, and maybe these will shed light on what might be going on).

My current endeavor is into creating a nice and solid vertex based skin saving/loading and transfer. As I was looking for different approaches to get the skin data out of a skinCluster, I obviously started down the path of using the MFnSkinCluster. However, I quickly discovered that the information that I get there is rather cumbersome as I need to actually do a full mesh component iteration on an objects vertices to get the weighting data. That is hardly a fast process, and so I tried looking for other possible solutions. I knew that there is ways of getting the data from the skinCluster via the regular Maya Python interface through the skinCluster attributes "weightList" and it's "weights" attributes. The way this attribute stores data is in a compound attribute which is basically a nested list. For each vertex index in the mesh there is a "weightList" entry associated with that index that stores the weights for that vertex. It can be accessed with something like this:

import maya.cmds as m

obj = 'skinCluster2'

items = m.getAttr (obj + '.weightList', mi = True)
for i in items:
    print i
    print m.getAttr (obj + '.weightList[%s].weights'%i)

This will return to you the index of the vert being parsed, as well as a list containing it's weights. However, the problem I encountered here is that to figure out which verts contain which influence objects, you need to do a fair amount of sorting: you will need to loop through the influences, get the verts that each influence affects, then sort them to group each verts' influences to form a coherent table. To me, this is as cumbersome as the MFnSkinCluster, and is also slower, as we aren't parsing through the API.

As I looked around, I sumbled on a wonderful page by Tyler Thornock that utilizes both the API and the attribute parsing to get the skin weights. But as I was going through his method, I started figuring out what each line did, why, and how (as I always tend to). You can find his code HERE. The following line is what got me thinking:
wPlug.selectAncestorLogicalIndex(vId, wlAttr)

I couldn't figure out what this "selectAncestorLogicalIndex" was for. But after digging around I found that this basically set a compound attribute to display a specific part of it's data. In other words, when Tyler was getting plug data from the "weightList" of the skinCluster, and then the "weights" (the wPlug), he needed to set the weights plug to display information for a specific weightList index. Since the wPlug contained all of the info for all of the weightLists, you can't just go through it as any other list. And so the "selectAncestoralLogicalIndex" is used to specify which part of the wPlug to parse.

To show it a little more practically, when MPlugs for the attributes were created:

wlPlug = skinFn.findPlug('weightList')

wPlug = skinFn.findPlug('weights')



...if you would try to check the amount of elements in wPlug with "wPlug.numElements()" Maya would actually crash. However, as soon as you set the LogicalIndex to be one of the verts, and tried to get that again you'd get a result of the amount of influences that vertex is affected by:

wlPlug = skinFn.findPlug('weightList')

wPlug = skinFn.findPlug('weights')

wlAttr = wlPlug.attribute()

wPlug.selectAncestorLogicalIndex(0, wlAttr)

print wPlug.numElements()


This is it for now, I am going to dig a little further, but hopefully after writing this, I will not forget how it works!