I have written many tools at different companies, I taught myself, and don’t have a CS degree. I enjoy shot-sculpting, skinning, and have been known to tweak parameters of on-screen visuals for hours; I don’t consider myself a ‘coder’; still can’t allocate my own memory. I feel I haven’t really used OOP from an architecture standpoint. So I bought an OOP book, and set out on a journey of self improvement.
‘OOP’ In Maya
In Maya, you often use DG nodes as ‘objects’. At Crytek we have our own modular nodes that create meta-frameworks encapsulating the character pipeline at multiple levels (characters, characterParts, and rigParts). Without knowing it, we were using Object Oriented Analysis when designing our frameworks, and even had some charts that look quite a bit like UML. DG node networks are connected often with message nodes, this is akin to a pointer to the object in memory, whereas with a python ‘object’ I felt it could always easily lose it’s mapping to the scene.
It is possible now with the OpenMaya C++ API to store a pointer to the DG node in memory and just request the full dag path any time you want it, also PyMel objects are Python classes and link to the DG node even when the string name changes.
“John is 47 Years Old and 6 Feet Tall”
Classes always seemed great for times when I had a bunch of data objects, the classic uses are books in a library, or customers: John is 47 years old and likes the color purple. Awesome. However, in Maya, all our data is in nodes already, and those nodes have attributes, those attributes serialize into a Maya file when I save: so I never really felt the need to use classes.
Although, all this ‘getting’, ‘setting’ and ‘listing’ really grows tiresome, even when you have custom methods to do it fairly easily.
It was difficult to find any really useful examples of OOP with classes in Maya. Most of our code is for ‘constructing’: building a rig, building a window, etc. Code that runs in a linear fashion and does ‘stuff’. There’s no huge architecture, the architecture is Maya itself.
Class Warfare
I wanted to package my information in classes and pass that back and forth in a more elegant way –at all times, not just while constructing things. So for classes to be useful to me, I needed them to synchronously exist with DG nodes.
I also didn’t want to have to get and set the information when syncing the classes with DG nodes, that kind of defeats the purpose of Python classes IMO.
Any time I opened a tool I would ‘wrap’ DG nodes in classes that harnessed the power of Python and OOP. To do this meant diving into more of the deep end, but since that was what was useful to me, that’s what I want to talk about here.
To demonstrate, let’s construct this example:
#the setup
loc = cmds.spaceLocator()
cons =[cmds.circle()[0], cmds.circle()[0]]
meshes =[cmds.sphere()[0], cmds.sphere()[0], cmds.sphere()[0]]
cmds.addAttr(loc, sn='controllers', at='message')
cmds.addAttr(cons, sn='rigging', at='message')for con in cons: cmds.connectAttr(loc[0] + '.controllers', con + '.rigging')
cmds.addAttr(loc, sn='rendermeshes', at='message')
cmds.addAttr(meshes, sn='rendermesh', at='message')for mesh in meshes: cmds.connectAttr(loc[0] + '.rendermeshes', mesh + '.rendermesh')
So now we have this little node network:
Now if I wanted to wrap this network in a class. We are going to use @property to give us the functionality of an attribute, but really a method that runs to return us a value (from the DG node) when the ‘attribute’ is queried. I believe using properties is key to harnessing the power of classes in Maya.
Next up, we add a setter, which runs code when you set a property ‘attribute’:
class GameThing(object):
def__init__(self, node):
self.node= node
#controllers@propertydef controllers(self):
return cmds.listConnections(self.node + ".controllers")@controllers.setterdef controllers(self, cons):
#disconnect existing controller connectionsfor con in cmds.listConnections(self.node + '.controllers'):
cmds.disconnectAttr(self.node + '.controllers', con + '.rigging')for con in cons:
if cmds.objExists(con):
ifnot cmds.attributeQuery('rigging', n=con, ex=1):
cmds.addAttr(con, longName='rigging', attributeType='message', s=1)
cmds.connectAttr((self.node + '.controllers'),(con + '.rigging'), f=1)else:
cmds.error(con + ' does not exist!')
So now when we set the ‘controllers’ attribute/property, it runs a method that blows away all current message connections and adds new ones connecting your cons:
To me, something like properties makes classes infinitely more useful in Maya. For a short time we tried to engineer a DG node at Crytek that when an attr changed, could eval a string with a similar name on the node. This is essentially what a property can do, and it’s pretty powerful. Take a moment to look through code of some of the real ‘heavy lifters’ in the field, like zooToolBox, and you’ll see @property all over the place.
I am giving a talk at SIGGRAPH 2012 entitled ‘Film/Game Convergence: What’s Taking So Long?‘ where I discuss the inherent differences between games and film and go over a few case studies of projects that attempted to use a game engine for film previs. I also talk a bit about the development of our CINEBOX application, the decisions we had to make, and how we dealt with many of the issues previous attempts have run into.
STUDIO WORKSHOPS
I will be giving two more Studio Workshops this year, the first is a followup to last year’s Introduction to Python, entitled ‘Python Scripting in Maya‘. The other workshop is ‘Building a Game Level‘, which is the same basic workshop I gave last year where I show people how to make a playable game level in CryEngine in an hour. Studio Workshops are hands-on sessions where each attendee has a computer and follows along with the instructor. It’s a great chance for people of all ages to learn new things.
Maybe it’s me, but I often find myself parsing weird ascii text files from others. Sometimes the authors knew what the data was and there’s no real markup. Take this joint list for example:
So the first line is the number of joints then it begins in three line intervals stating from the root outwards: joint name, parent integer, position. I used to make a pretty obtuse loop using a modulus operator. Basically, modulus is the remainder left over after division. So X%Y gives you the remainder of X divided by Y; here’s an example:
for i inrange(0,20+1):
if i%2==0: print i
#>> 0#>> 2#>> 4#>> 6#>> 8#>> 10
The smart guys out there see where this is goin.. so I never knew range had a ‘step’ argument. (Or I believe I did, I think I actually had this epiphany maybe two years ago, but my memory is that bad.) So parsing the above is as simple as this:
I gave a workshop/talk at SIGGRAPH geared toward introducing people to Python. There were ~25 people on PCs following along, and awkwardly enough, many more than that standing and watching. I prefaced my talk with the fact that I am self-taught and by no means an expert. That said, I have created many python tools people use every day at industry-leading companies.
Starting from zero, in the next hour I aimed to not only introduce them to Python, but get them doing cool, usable things like:
Iterating through batches/lists
Reading / writing data to excel files
Wrangling data from one format to another in order to create a ‘tag cloud’
Many people have asked for the notes, and I only had rough notes. I love Python, and I work with this stuff every day, so I have had to really go back and flesh out some of what I talked about. This tutorial has a lot less of the general chit-chat and information. I apologize for that.
Installation / Environment Check
Let’s check to see that you have the tools properly installed. If you open the command prompt and type ‘python’ you should see this:
So Python is correctly installed, for the following you can either follow along in the cmd window (more difficult) or in IDLE, the IDE that python ships with (easier). This can be found by typing IDLE into the start menu:
Variables
Variables are pieces of information you store in memory, I will talk a bit about different types of variables.
Strings
Strings are pieces of text. I assume you know that, so let’s just go over some quick things:
string='this is a string'printstring#>>this is a string
num ='3.1415'print num
#>>3.1415
One thing to keep in mind, the above is a string, not a number. You can see this by:
print num + 2#>>Traceback (most recent call last):#>> File "basics_variables.py", line 5, in#>> print num + 2#>>TypeError: cannot concatenate 'str' and 'int' objects
Python is telling you that you cannot add a number to a string of text. It does not know that ’3.1415′ is a number. So let’s convert it to a number, this is called ‘casting’, we will ‘cast’ the string into a float and back:
Lists are the simplest ways to store pieces of data. Let’s make one by breaking up a string:
txt ='jan tony senta michael brendon phillip jonathon mark'
names = txt.split(' ')print names
#>>['jan', 'tony', 'senta', 'michael', 'brendon', 'phillip', 'jonathon', 'mark']for item in names: print item
#>>jan#>>tony#>>senta#>>michael
...
Split breaks up a string into pieces. You tell it what to break on, above, I told it to break on spaces txt.split(‘ ‘). So all the people are stored in a List, which is like an Array or Collection in some other languages.
You can call up the item by it’s number starting with zero:
print names[0], names[5]#>>jan phillip
TIP: [-1] index will return the last item in an array, here’s a quick way to get a file from a path:
So this is good, but these are just the keys, we need to know the values. Here’s another way to do this, using .keys()
dict={'sascha':'tech artist','harry': 142.1,'sean':False}for key indict.keys(): print key,'is',dict[key]#>>sean is False#>>sascha is tech artist#>>harry is 142.1
So, dictionaries are a good way to store simple relationships of key and value pairs. In case you hadn’t notices, I used some ‘floats’ and ‘ints’ above. A float is a number with a decimal, like 3.1415, and an ‘int’ is a whole number like 10.
Creating Methods (Functions)
A method or function is like a little tool that you make. These building blocks work together to make your program.
Let’s say that you have to do something many times, you want to re-use this code and not copy/paste it all over. Let’s use the example above of names, let’s make a function that takes a big string of names and returns an ordered list:
def myFunc(input):
people =input.split(' ')
people =sorted(people)return people
txt ='jan tony senta michael brendon phillip jonathon mark'
orderedList = myFunc(txt)print orderedList
#>>['brendon', 'jan', 'jonathon', 'mark', 'michael', 'phillip', 'senta', 'tony']
Basic Example: Create A Tag Cloud From an Excel Document
So we have an excel sheet, and we want to turn it into a hip ‘tag cloud’ to get people’s attention.
If we go to http://www.wordle.net/ you will see that in order to create a tag cloud, we need to feed it the sentences multiple times, and we need to put a tilde in between the words of the sentence. We can automate this with Python!
First, download the excel sheet from me here: [info.csv] The CSV filetype is a great way to read/write docs easily that you can give to others, they load in excel easily.
file='C:\\Users\\chris\\Desktop\\intro_to_python\\info.csv'
f =open(file,'r')
lines = f.readlines()
f.close()print lines
#>> ['always late to work,13\n', 'does not respect others,1\n', 'does not check work properly,5\n', 'does not plan properly,4\n', 'ignores standards/conventions,3\n']
‘\n’ is a line break character, it means ‘new line’, we want to get rid of that, we also want to just store the items, and how many times they were listed.
file='C:\\Users\\chris\\Desktop\\intro_to_python\\info.csv'
f =open(file,'r')
lines = f.readlines()
f.close()dict={}for line in lines:
split = line.strip().replace(' ','~').split(',')dict[split[0]]=int(split[1])printdict#>>{'ignores~standards/conventions': 3, 'does~not~respect~others': 1, 'does~not~plan~properly': 4, 'does~not~check~work~properly': 5, 'always~late~to~work': 13}
Now we have the data in memory in an easily readable way, let’s write it out to disk.
output =''for key indict.keys():
for i inrange(0,dict[key]): output +=(key + '\n')
f =open('C:\\Users\\chris\\Desktop\\intro_to_python\\test.txt','w')
f.write(output)
f.close()
There we go. In one hour you have learned to:
Read and write excel files
Iterate over data
Convert data sets into new formats
Write, read and alter ascii files
If you have any questions, or I left out any parts of the presentation you liked, reply here and I will get back to you.
I recently wrote a custom tool to diff CryEngine layer files in P4, and was surprised how simple it was. What follows is a quick tutorial on adding custom python tools to Perforce.
Start by heading over to Tools>Manage Custom Tools… Then click ‘New’:
You can pass a lot of information to an external tool, here is a detailed rundown. As you see above, we pass the client spec (local) file name (%f) to a python script, let’s create a new script called ‘custom_tool.py’:
What this does is simply spits out the sys.argv in a way you can see it. So now you can feed any file you right click in Perforce into a python script:
If you would like to actually do something with a file or revision on the server and are passing the %F flag to get the depot file path, you then need to use p4 print to redirect the file contents (non-binary) to a local file:
Perforce is a wily beast. A lot of companies use it, but I feel few outside of the IT department really have to deal with it much. As I work myself deeper and deeper into the damp hole that is asset validation, I have really been writing a lot of python to deal with certain issues; but always scripts that work from the outside.
Perforce has a system that allows you to write scripts that are run, server side, when any number of events are triggered. You can use many scripting languages, but I will only touch on Python.
Test Environment
To follow along here, you should set up a test environment. Perforce is freely downloadable, and free to use with 2 users. Of course you are going to need python, and p4python. So get your server running and add two users, a user and an administrator.
Your First Trigger
Let’s create the simplest python script. It will be a submit trigger that says ‘Hello World’ then passes or fails. If it passes, the item will be checked in to perforce, if it fails, it will not. exiting while returning a ’1′ is considered a fail, ’0′ a pass.
print'Hello World!'print'No checkin for you!'sys.exit(1)
Ok, so save this file as hello_trigger.py. Now go to a command line and enter ‘p4 triggers’ this will open a text document, edit that document to point to your trigger, like so (but point to the location of your script on disk):
Close/save the trigger TMP file, you should see ‘Triggers saved.’ echo’d at the prompt. Now, when we try to submit a file to the depot, we will get this:
So: awesome, you just DENIED your first check-in!
Connecting to Perforce from Inside a Trigger
So we are now denying check-ins, but let’s try to do some other things, let’s connect to perforce from inside a trigger.
from P4 import P4, P4Exception
p4 = P4()try:
#use whatever your admin l/p was#this isn't the safest, but it works at this beginner level
p4.user="admin"
p4.password="admin"
p4.port="1666"
p4.connect()
info = p4.run("info")print info
sys.exit(1)#this will return any errorsexcept P4Exception:
for e in p4.errors: print e
sys.exit(1)
So now when you try to submit a file to depot you will get this:
Passing Info to the Trigger
Now we are running triggers, accepting or denying checkins, but we really don’t know much about them. Let’s try to get enough info to where we could make a decision about whether or not we want the file to pass validation. Let’s make another python trigger, trigger_test.py, and let’s query something from the perforce server in the submit trigger. To do this we need to edit our trigger file like so:
Triggers:
test change-submit //depot/... "python X:/projects/2010/p4/test_trigger.py %user% %changelist%"
This will pass the user and changelist number into the python script as an arg, the same way dragging/dropping passed args to python in my previous example. So let’s set that up, save the script from before as ‘test_trigger.py’ as shown above, and add the following:
importsysfrom P4 import P4, P4Exception
p4 = P4()
describe =[]try:
p4.user="admin"
p4.password="admin"
p4.port="1666"
p4.connect()except P4Exception:
for e in p4.errors: print e
sys.exit(1)printstr(sys.argv)
describe = p4.run('describe',sys.argv[2])printstr(describe)
p4.disconnect()sys.exit(1)
So, as you can see, it has returned the user and changelist number:
However, for this changelist to be useful, we query p4, asking the server to describe the changelist. This returns a lot of information about the changelist.
Where to Go From here
The few simple things shown here really give you the tools to do many more things. Here are some examples of triggers that can be created with the know-how above:
Deny check-ins of a certain filetype (like deny compiled source files/assets)
Deny check-ins whose hash digest matches an existing file on the server
Deny/allow a certain type of file check-in from a user in a certain group
Email a lead any time a file in a certain folder is updated
Did you find this helpful? What creative triggers have you written?
In Python, a Decorator is a type of macro that allows you to inject or modify code in functions or classes. I was turned onto this by my friend Matt Chapman at ILM, but never fully grasped the importance.
class myDecorator(object):
def__init__(self, f):
self.f= f
def__call__(self):
print"Entering",self.f.__name__
self.f()print"Exited",self.f.__name__
@myDecorator
def aFunction():
print"aFunction running"
aFunction()
When you run the code above you will see the following:
So when we call a decorated function, we get a completely different behavior. You can wrap any existing functions, here is an example of wrapping functions for error reporting:
class catchAll:
def__init__(self, function):
self.function= function
def__call__(self, *args):
try:
returnself.function(*args)exceptException, e:
print"Error: %s" % (e)@catchAll
def unsafe(x):
return1 / x
print"unsafe(1): ", unsafe(1)print"unsafe(0): ", unsafe(0)
So when we run this and divide by zero we get:
unsafe(1): 1
unsafe(0): Error: integer division or modulo by zero
Using decorators you can make sweeping changes to existing code with minimal effort, like the error reporting function above, you could go back and just sprinkle these in older code.
I have really been trying to learn some Python fundamentals lately, reading some books and taking an online class. So: wow. I can’t believe that I have written so many tools, some used by really competent people at large companies, without really understanding polymorphism and other basic Python concepts.
Here’s an example of my sequence method from before, but making it a class using special class methods:
This is what the final product will look like. Two D300s, mounted as close as possible, sync’d metering, focus, flash, and shutter. Rig cost: Less than 30 dollars! Of course you are going to need two d300s and paired lenses, primes or zooms with a wide rubberband spanning them if you are really hardcore. Keep in mind, the intraoccular is 13.5cm, this is a tad more than double the normal human width, but it’s the best we can do with the d300 [horizontal].
Creating the Camera Bar
This mainly involves you going to the local hardware store with your D300 and looking at the different L-brackets available. It’s really critical that you get the cameras as close as possible, so mounting one upside down is preferable. It may look weird, but heck, it already looks weird; might as well go full retard.
I usually get an extra part for the buttons, because they will need to be somewhere that you can easily reach
Creating the Cabling
Nikon cables with Nikon 10-pin connectors aren’t cheap! The MC-22, MC-23, MC-25, or MC-30 are all over 60 dollars! I bought remote shutter cables at DealExtreme.com. I wanted to make my own switch, and also be able to use my GPS, and change the intraoccular, so the below describes that setup. If you just want to sync two identical cams, the fastest way is to buy a knock-off MC-23, which is the JJ MA-23 or JJ MR-23. I bought two JueYing RS-N1 remote shutters and cut them up. [$6 each]
I only labeled the pins most people would be interested in, for a more in depth pin-out that covers more than AE/AF and shutter (GPS, etc), have a look here. I decided to use molex connectors from RC cars, they make some good ones that are sealed/water-resistant and not too expensive.
So the cables have a pretty short lead. This so that I can connect them as single, double, have an intraoccular as wide or as short as any cable I make.. The next thing is to wire these to a set of AF/AE and shutter buttons.
Black focuses/meters and red is the shutter release. It’s not easy to find buttons that have two press states: half press and full press. If you see above, shutter is the combination of AF/AE, ground, and shutter. This is before the heat shrink is set in place.
Altogether
So that should be it. Here’s my first photo with sync’d metering, focus, flash, and shutter. They can even do bursts at high speed. Next post I will try to look into the software side, and take a look at lens distortion, vignetting, and other issues.
I have been parsing through the files of other people a lot lately, and finally took the time to make a little function to give me general information about a sequence of files. It uses regex to yank the numeric parts out of a filename, figure out the padding, and glob to tell you how many files in the sequence. Here’s the code and an example usage:
#returns [base name, padding, filetype, number of files, first file, last file]def getSeqInfo(file):
dir=os.path.dirname(file)file=os.path.basename(file)
segNum =re.findall(r'\d+',file)[-1]
numPad =len(segNum)
baseName =file.split(segNum)[0]
fileType =file.split('.')[-1]
globString = baseName
for i inrange(0,numPad): globString +='?'
theGlob =glob.glob(dir+'\\'+globString+file.split(segNum)[1])
numFrames =len(theGlob)
firstFrame = theGlob[0]
lastFrame = theGlob[-1]return[baseName, numPad, fileType, numFrames, firstFrame, lastFrame]
I know this is pretty simple, but I looked around a bit online and didn’t see anything readily available showing how to deal with different numbered file sets. I have needed something like this for a while that will work with anything from OBJs sent from external contractors, to images from After Effects…
So I have always been wondering how you can create almost like a ‘droplet’ to steal the photoshop lingo, from a python script. A while ago I came across some sites showing how to edit shellex in regedit to allow for files to be dropped on any python script and fed to it as args (Windows).
It’s really simple, you grab this reg file [py_drag_n_drop.reg] and install it.
Now when you drop files onto a python script, their filenames will be passed as args, here’s a simple script to test.
importsys
f =open('c:\\tmp.txt','w')for arg insys.argv:
f.write(arg + '\n')
f.close()
When you save this, and drop files onto its icon, it will create tmp.txt, which will look like this:
The script itself is the first arg, then all the files. This way you can easily create scripts that accept drops to do things like convert files, upload files, etc..
I have been really amazing myself at how much knowledge I have forgotten in the past five or six months… Most of the work I did in the past year utilized the UIC module to load UI files directly, but I can find very little information about this online. I was surprised to see that even the trusty old Rapid GUI Programming with Python and Qt book doesn’t cover loading UI files with the UIC module.
So, here is a tiny script with UI file [download] that will generate a pyqt example window that does ‘stuff’:
importsysfrom PyQt4 import QtGui, QtCore, uic
class TestApp(QtGui.QMainWindow):
def__init__(self):
QtGui.QMainWindow.__init__(self)self.ui= uic.loadUi('X:/projects/2010/python/pyqt_tutorial/pyqt_tutorial.ui')self.ui.show()self.connect(self.ui.doubleSpinBox, QtCore.SIGNAL("valueChanged(double)"), spinFn)self.connect(self.ui.comboBox, QtCore.SIGNAL("currentIndexChanged(QString)"), comboFn)self.connect(self.ui.pushButton, QtCore.SIGNAL("clicked()"), buttonFn)def spinFn(value):
win.ui.doubleSpinBoxLabel.setText('doubleSpinBox is set to ' + str(value))def buttonFn():
win.ui.setWindowTitle(win.ui.lineEdit.text())def comboFn(value):
win.ui.comboBoxLabel.setText(str(value) + ' is selected')if __name__ =="__main__":
app = QtGui.QApplication(sys.argv)
win = TestApp()sys.exit(app.exec_())
Change the path to reflect where you have saved the UI file, and when you run the script you should get this:
EDIT: A few people have asked me to update this for other situations
PySide Inside Maya:
importsysfrom PySide.QtUiToolsimport *
from PySide.QtCoreimport *
from PySide.QtGuiimport *
class TestApp(QMainWindow):
def__init__(self):
QMainWindow.__init__(self)
loader = QUiLoader()self.ui= loader.load('c:/pyqt_tutorial.ui')self.ui.show()self.connect(self.ui.doubleSpinBox, SIGNAL("valueChanged(double)"), spinFn)self.connect(self.ui.comboBox, SIGNAL("currentIndexChanged(QString)"), comboFn)self.connect(self.ui.pushButton, SIGNAL("clicked()"), buttonFn)def spinFn(value):
win.ui.doubleSpinBoxLabel.setText('doubleSpinBox is set to ' + str(value))def buttonFn():
win.ui.setWindowTitle(win.ui.lineEdit.text())def comboFn(value):
win.ui.comboBoxLabel.setText(str(value) + ' is selected')
win = TestApp()
PyQT Inside Maya:
importsysfrom PyQt4 import QtGui, QtCore, uic
class TestApp(QtGui.QMainWindow):
def__init__(self):
QtGui.QMainWindow.__init__(self)self.ui= uic.loadUi('c:/pyqt_tutorial.ui')self.ui.show()self.connect(self.ui.doubleSpinBox, QtCore.SIGNAL("valueChanged(double)"), spinFn)self.connect(self.ui.comboBox, QtCore.SIGNAL("currentIndexChanged(QString)"), comboFn)self.connect(self.ui.pushButton, QtCore.SIGNAL("clicked()"), buttonFn)def spinFn(value):
win.ui.doubleSpinBoxLabel.setText('doubleSpinBox is set to ' + str(value))def buttonFn():
win.ui.setWindowTitle(win.ui.lineEdit.text())def comboFn(value):
win.ui.comboBoxLabel.setText(str(value) + ' is selected')
win = TestApp()
The Vatican is not very open with it’s art, the reason they scream ‘NO PHOTO’ when you pull a camera out in the chapel is that they sold the ability to take photos of it to a Japanese TV Station (Nippon TV) for 4.2 million dollars. Because the ceiling has long been in the public domain, the only way they can sell ‘the right to photograph’ the ceiling is by screwing over us tourists who visit. If you take a photo, they have no control over that image –because they don’t own the copyright of the work.
Many of you who know me, know I am a huge fan of Michelangelo’s work, this data was just too awesomely tempting and when I saw it posted publicly online, I really wanted to get my hands on the original assets.
Here is a python script to grab all of the image tiles that the flash app reads, and then generate the 8k faces of the cubemap. In the end you will have a 32,000 pixel cubemap.
First we copy the swatches from the website:
def getSistineCubemap(saveLoc):
importurllib#define the faces of the cubemap, using their own lettering scheme
faces =['f','b','u','d','l','r']#location of the images
url ='http://www.vatican.va/various/cappelle/sistina_vr/Sistine-Chapel.tiles/l3_'#copy all the swatches to your local drivefor face in faces:
for x inrange(1,9):
for y inrange(1,9):
file=(face + '_' + str(y) + '_' + str(x) + '.jpg')urllib.urlretrieve((url + face + '_' + str(y) + '_' + str(x) + '.jpg'),(saveLoc + file))urllib.urlcleanup()print"saved " + file
Many, many people are having weird, buggy camera issues where you rotate a view and it snaps back to the pre-tumbled state (view does not update properly). There are posts all over, and Autodesk’s official response is “Consumer gaming videocards are not supported”. Really? That’s basically saying: All consumer video cards, gaming or not, are unsupported. I have had this issue on my laptop, which is surely not a ‘gaming’ machine. Autodesk says the ‘fix’ is to upgrade to an expensive pro-level video card. But what they maybe would tell you if they weren’t partnered with nVidia is: It’s an easy fix!
Find your Maya ENV file:
C:\Documents and Settings\Administrator\My Documents\maya\2009-x64\Maya.env
And add this environment variable to it:
MAYA_GEFORCE_SKIP_OVERLAY=1
Autodesk buried this information in their Maya 2009 Late Breaking Release Notes, and it fixes the issue completely! However, even on their official forum, Autodesk employees and moderators reply to these draw errors as follows:
Maya 2009 was tested with a finite number of graphics cards from ATI and Nvidia, with drivers from each vendor that provided the best performance, with the least amount of issues. (at the time of product launch). A list of officially qualified hardware can be found here: http://www.autodesk.com/maya-hardware. Maya is not qualified/supported on consumer gaming cards. Geforce card users can expect to have issues. This is clearly stated in the official qualification charts mentioned above.
Many of you might remember the fluoroscopic shoulder carriage videos I posted on my site about 4 years ago. I always wanted to do a sequence of MRI’s of the arm moving around. Thanks to Helena, an MRI tech that I met through someone, I did just that. I was able to get ~30 mins of idle time on the machine while on vacation.
The data that I got was basically image data. It’s slices along an axis, I wanted to visualize this data in 3D, but they did not have software to do this in the hospital. I really wanted to see the muscles and bones posed in three dimensional space as the arm went through different positions, so I decided to write some visualization tools myself in maxscript.
At left is a 512×512 MRI of my shoulder; arm raised (image downsampled to 256, animation on 5′s, ). The MRI data has some ‘wrap around’ artifacts because it was a somewhat small MRI (3 tesla) and I am a big guy, when things are close to the ‘wall’ they get these artifacts, and we wanted to see my arm. I am uploading the raw data for you to play with, you can download it from here: [data01] [data02]
Volumetric Pixels
Above is an example of 128×128 10 slice reconstruction with greyscale cubes.
I wrote a simple tool called ‘mriView’. I will explain how I created it below and you can download it and follow along if you want. [mriView]
The first thing I wanted to do was create ‘volumetric pixels’ or ‘voxels’ using the data. I decided to do this by going through all the images, culling what i didn’t want and creating grayscale cubes out of the rest. There was a great example in the maxscript docs called ‘How To … Access the Z-Depth channel’ which I picked some pieces from, it basically shows you how to efficiently read an image and generate 3d data from it.
But we first need to get the data into 3dsMax. I needed to load sequential images, and I decided the easiest way to do this was load AVI files. Here is an example of loading an AVI file, and treating it like a multi-part image (with comments):
on loadVideoBTN pressed do
(
--ask the user for an avi
f = getOpenFileName caption:"Open An MRI Slice File:" filename:"c:/" types:"AVI(*.avi)|*.avi|MOV(*.mov)|*.mov|All|*.*|"
mapLoc = f
if f == undefined then (return undefined)
else
(
map = openBitMap f
--get the width and height of the video
heightEDT2.text = map.height as string
widthEDT2.text = map.width as string
--gethow many frames the video has
vidLBL.text = (map.numFrames as string + " slices loaded.")
loadVideoBTN.text = getfilenamefile f
imageLBL.text = ("Full Image Yeild: " + (map.height*map.width) as string + " voxels")
slicesEDT2.text = map.numFrames as string
threshEDT.text = "90"
)
updateImgProps()
)
We now have the height in pixels, the width in pixels, and the number of slices. This is enough data to begin a simple reconstruction.
We will do so by visualizing the data with cubes, one cube per pixel that we want to display. However be careful, a simple 256×256 video is already possibly 65,536 cubes per slice! In the tool, you can see that I put in the original image values, but allow the user to crop out a specific area.
Below we go through each slice, then go row by row, looking pixel by pixel looking for ones that have a gray value above a threshold (what we want to see), when we find them, we make a box in 3d space:
height = 0.0
updateImgProps()
--this loop iterates through all slices (frames of video)
for frame = (slicesEDT1.text as integer) to (slicesEDT2.text as integer) do
(
--seek to the frame of video that corresponds to the current slice
map.frame = frame
--loop that traverses y, which corresponds to the image height
for y = mapHeight1 to mapHeight2 do
(
voxels = #()
currentSlicePROG.value = (100.0 * y / totalHeight)
--read a line of pixels
pixels = getPixels map [0,y-1] totalWidth
--loop that traverses x, the line of pixels across the width
for x = 1 to totalWidth do
(
if (greyscale pixels[x]) < threshold then
(
--if you are not a color we want to store: do nothing
)
--if you are a color we want, we will make a cube with your color in 3d space
else
(
b = box width:1 length:1 height:1 name:(uniqueName "voxel_")
b.pos = [x,-y,height]
b.wirecolor = color (greyscale pixels[x]) (greyscale pixels[x]) (greyscale pixels[x])
append voxels b
)
)
--grabage collection is important on large datasets
gc()
)
--increment the height to bump your cubes to the next slice
height+=1
progLBL.text = ("Slice " + (height as integer) as string + "/" + (totalSlices as integer) as string + " completed")
slicePROG.value = (100.0 * (height/totalSlices))
)
Things really start to choke when you are using cubes, mainly because you are generating so many entities in the world. I added the option to merge all the cubes row by row, which sped things up, and helped memory, but this was still not really the visual fidelity I was hoping for…
Point Clouds and ‘MetaBalls’
I primarily wanted to generate meshes from the data so the next thing I tried was making a point cloud, then using that to generate a ‘BlobMesh’ (metaball) compound geometry type. In the example above, you see the head of my humerus and the tissue connected to it. Below is the code, it is almost simpler than boxes, it just takes finessing edit poly, i have only commented changes:
I make a plane and then delete all the verts to give me a ‘clean canvas’ of sorts, if anyone knows a better way of doing this, let me know:
This can get really time and resource intensive. As a result, I would let some of these go overnight. This was pretty frustrating, because it slowed the iteration time down a lot. And the blobMesh modifier was very slow as well.
Faking Volume with Transparent Planes
I was talking to Marco at work (Technical Director) and showing him some of my results, and he asked me why I didn’t just try to use transparent slices. I told him I had thought about it, but I really know nothing about the material system in 3dsMax, much less it’s maxscript exposure. He said that was a good reason to try it, and I agreed.
I started by making one material per slice, this worked well, but then I realized that 3dsMax has a limit of 24 materials. Instead of fixing this, they have added ‘multi-materials’, which can have n sub-materials. So I adjusted my script to use sub-materials:
--here we set the number of sub-materials to the number of slices
meditMaterials[matNum].materialList.count = totalSlices
--you also have to properly set the materialIDList
for m=1 to meditMaterials[matNum].materialList.count do
(
meditMaterials[matNum].materialIDList[m] = m
)
Now we iterate through, generating the planes, assigning sub-materials to them with the correct frame of video for the corresponding slice:
p = plane name:("slice_" + frame as string) pos:[0,0,frame] width:totalWidth length:totalHeight
p.lengthsegs = 1
p.widthsegs = 1
p.material = meditMaterials[matNum][frame]
p.castShadows = off
p.receiveshadows = off
meditMaterials[matNum].materialList[frame].twoSided = on
meditMaterials[matNum].materialList[frame].selfIllumAmount = 100
meditMaterials[matNum].materialList[frame].diffuseMapEnable = on
newMap = meditMaterials[matNum].materialList[frame].diffuseMap = Bitmaptexture filename:mapLoc
newmap.starttime = frame
newmap.playBackRate = 1
newmap = meditMaterials[matNum].materialList[frame].opacityMap = Bitmaptexture fileName:mapLoc
newmap.starttime = frame
newmap.playBackRate = 1
showTextureMap p.material on
mat += 1
This was very surprising, it not only runs fast, but it looks great. Of course you are generating no geometry, but it is a great way to visualize the data. The below example is a 512×512 MRI of my shoulder (arm raised) rendered in realtime. The only problem I had was an alpha-test render error when viewed directly from the bottom, but this looks to bea 3dsMax issue.
I rendered the slices cycling from bottom to top. In one MRI the arm is raised, in the other, the arm lowered. The results are surprisingly decent. You can check that video out here. [shoulder_carriage_mri_xvid.avi]
You can also layer multiple slices together, above I have isolated the muscles and soft tissue from the skin, cartilage and bones. I did this by looking for pixels in certain luminance ranges. Above in the image I am ‘slicing’ away the white layer halfway down the torso, below you can see a video of this in realtime as I search for the humerus; this is a really fun & interesting way to view it:
I can now easily load up any of the MRI data I have and view it in 3d, though I would like to be able to better create meshes from specific parts of the data, in order to isolate muscles or bones. To do this I need to allow the user to ‘pick’ a color from part of the image, and then use this to isolate just those pixels and remesh just that part. I would also like to add something that allows you to slice through the planes from any axis. That shouldn’t be difficult, just will take more time.
This is something we had been discussing over at CGTalk, we couldn’t find a way to figure out Reaction Manager links through maxscript. It just is not exposed. Reaction Manager is like Set Driven in Maya or a Relation Constraint in MotionBuilder. In order to sync rigging components between the packages, you need to be able to query these driven relationships.
I set about doing this by checking dependencies, and it turns out it is possible. It’s a headache, but it is possible!
The problem is that even though slave nodes have controllers with names like “Float_Reactor”, the master nodes have nothing that distinguishes them. I saw that if I got dependents on a master node (it’s controllers, specifically the one that drives the slave), that there was something called ‘ReferenceTarget:Reaction_Master‘:
So here is a fn that gets Master information from a node:
fn getAllReactionMasterRefs obj =
(
local nodeRef
local ctrlRef
for n = 1 to obj.numSubs do
(
ctrl = obj[n].controller
if (ctrl!=undefined) then
(
for item in (refs.dependents ctrl) do
(
if item as string == "ReferenceTarget:Reaction_Master" then
(
nodeRef = (refs.dependentNodes item)
ctrlRef = ctrl
)
)
getAllReactionMasterRefs obj[n]
)
)
return #(nodeRef, ctrlRef)
)
The first item is an array of the referenced node, and the second is the controller that is driving *some* aspect of that node.
You now loop through this node looking for ‘Float_Reactor‘, ‘Point3_Reactor‘, etc, and then query them as stated in the manual (‘getReactionInfluence‘, ‘getReactionFalloff‘, etc) to figure out the relationship.
Here is an example function that prints out all reaction data for a slave node:
fn getAllReactionControllers obj =
(
local list = #()
for n = 1 to obj.numSubs do
(
ctrl = obj[n].controller
if (ctrl!=undefined) then
(
--print (classof ctrl)
if (classof ctrl) == Float_Reactor \
or (classof ctrl) == Point3_Reactor \
or (classof ctrl) == Position_Reactor \
or (classof ctrl) == Rotation_Reactor \
or (classof ctrl) == Scale_Reactor then
(
reactorDumper obj[n].controller data
)
)
getAllReactionControllers obj[n]
)
)
Here is the output from ‘getAllReactionControllers $Box2‘:
Conclusion
So, once again, no free lunch here. You can loop through the scene looking for Masters, then derive the slave nodes, then dump their info. It shouldn’t be too difficult as you can only have one Master, but if you have multiple reaction controllers in each node effecting the other; it could be a mess. I threw this together in a few minutes just to see if it was possible, not to hand out a polished, working implementation.
Over the past few years I have noticed that Photoshop often, usually after it is left idling for a few hours or days, no longer imports the windows clipboard.
Here is a fix if you don’t mind getting your hands dirty in the registry:
The above is for photoshop cs2, depending on your version you will have to look in different reg locations. There is also a problem when you hit a ‘size limit’ for an incoming clipboard image, and Photoshop dumps it. This can also be circumvented by editing the registry:
This is a simple proof-of-concept showing how to implement a perforce animation browser via python for MotionBuilder. Clicking an FBX animation syncs it and loads it.
The script can be found here: [p4ui.py], it requires the [wx] and [p4] libraries.
Clicking directories goes down into them, clicking fbx files syncs them and loads them in MotionBuilder. This is just a test, the ‘[..]‘ doesn’t even go up directories. Opening an animation does not check it out, there is good documentation for the p4 python lib, you can start there; it’s pretty straight forward and easy: sure beats screen scraping p4 terminal stuff.
You will see the following, you should replace this with the p4 location of your animations, this will act as the starting directory.
path1 ='PUT YOUR PERFORCE ANIMATION PATH HERE (EXAMPLE: //DEPOT/ANIMATION)'
info = p4i.run("info")print info[0]['clientRoot']
That should about do it, there are plenty of P4 tutorials out there, my code is pretty straight forward. The only problem was where I instanced it, be sure to instance it with something other than ‘p4′, I did this and it did not work, using ‘p4i’ it did without incident:
This is a tip that a coworker (Tetsuji) showed me a year ago or so, I was pretty damn sure my ATI drivers were bluescreening my system, but I wanted to hunt down proof. So you have just had a bluescreen and your pc rebooted. Here’s how to hunt down what happened.
First thing you should see when you log back in is this:
It’s really important that you not do anything right now; especially don’t click one of those buttons. Click the ‘click here‘ text ad then you will see this window.
Ok, so this doesn’t tell us much at all. We want to get the ‘technical information’, so click the link for that and you will see something like this:
Here is why we did not click those buttons before; when you click those buttons, these files get deleted. So copy this path and go to this folder. Copy the contents elsewhere, and close all those windows. So you now have these three files:
The ‘dmp’ file (dump file) will tell us what bluescreened our machine, but we need some tools to read it. Head on over to the Microsoft site and download ‘Debugging Tools for Windows’ (x32, x64). Once installed, run ‘WinDbg‘. Select File->Open Crash Dump… and point it at your DMP file. This will open, scroll down and look for something like this:
In this example the culprit was ‘pgfilter.sys‘, something installed by ‘Peer Guardian’, a hacky privacy protection tool I use at home. There is a better way to cut through a dump file, you can also type in ‘!analyze -v‘, this will generate something like this:
In this example above you see that it’s an ATI driver issue, which I fixed by replacing the card with an nvidia and tossing the ATI into our IT parts box (junkbox).
Facial motion capture stabilization is basically where you isolate the movement of the face from the movement of the head. This sounds pretty simple, but it is actually a really difficult problem. In this post I will talk about the general process and give you an example facial stabilization python script.
Disclaimer: The script I have written here is loosely adapted from a MEL script in the book Mocap for Artists, and not something proprietary to Crytek. This is a great book for people of all experience levels, and has a chapter dedicated to facial mocap. Lastly, this script is not padded out or optimized.
To follow this you will need some facial mocap data, there is some freely downloadable here at www.mocap.lt. Grab the FBX file.
Stabilization markers
Get at least 3 markers on the actor that do not move when they move their face. These are called ’stabilization markers’ (STAB markers). You will use these markers to create a coordinate space for the head, so it is important that they not move. STAB markers are commonly found on the left and right temple, and nose bridge. Using a headband and creating virtual markers from multiple solid left/right markers works even better. Headbands move, it’s good to keep this in mind, above you see a special headrig used on Kong to create stable markers.
It is a good idea to write some tools to help you out here. At work I have written tools to parse a performance and tell me the most stable markers at any given time, if you have this data, you can also blend between them.
Load up the facial mocap file you have downloaded, it should look something like this:
In the data we have, you can delete the root, the headband markers, as well as 1-RTMPL, 1-LTMPL, and 1-MNOSE could all be considered STAB markers.
General Pipeline
As you can see, mocap data is just a bunch of translating points. So what we want to do is create a new coordinate system that has the motion of the head, and then use this to isolate the facial movement.
This will take some processing, and also an interactive user interface. You may have seen my tutorial on Creating Interactive MotionBuilder User Interface Tools. You should familiarize yourself with that because this will build on it. Below is the basic idea:
You create a library ‘myLib’ that you load into motionbuilder’s python environment. This is what does the heavy lifting, I say this because you don’t want to do things like send the position of every marker, every frame to your external app via telnet. I also load pyEuclid, a great vector library, because I didn’t feel like writing my own vector class. (MBuilder has no vector class)
Creating ‘myLib’
So we will now create our own library that sits inside MBuilder, this will essentially be a ‘toolkit’ that we communicate with from the outside. Your ‘myLib’ can be called anything, but this should be the place you store functions that do the real processing jobs, you will feed into to them from the outside UI later. The first thing you will need inside the MB python environment is something to cast FBVector3D types into pyEuclid. This is fairly simple:
#casts point3 strings to pyEuclid vectorsdef vec3(point3):
return Vector3(point3[0], point3[1], point3[2])#casts a pyEuclid vector to FBVector3ddef fbv(point3):
return FBVector3d(point3.x, point3.y, point3.z)
Next is something that will return an FBModelList of models from an array of names, this is important later when we want to feed in model lists from our external app:
#returns an array of models when given an array of model names#useful with external apps/telnetlib uidef modelsFromStrings(modelNames):
output =[]for name in modelNames:
output.append(FBFindModelByName(name))return output
Now, if you were to take these snippets and save them as a file called myLib.py in your MBuilder directory tree (MotionBuilder75 Ext2\bin\x64\python\lib), you can load them into the MBuilder environment. (You should have also placed pyEuclid here)
It’s always good to mock-up code in telnet because, unlike the python console in MBuilder, it supports copy/paste etc..
In the image above, I get the position of a model in MBuilder, it returns as a FBVector3D, I then import myLib and pyEuclid and use our function above to ‘cast’ the FBVector3d to a pyEuclid vector. It can now be added, subtracted, multiplied, and more; all things that are not possible with the default MBuilder python tools. Our other function ‘fbv()‘ casts pyEuclid vectors back to FBVector3d, so that MBuilder can read them.
So we can now do vector math in motionbuilder! Next we will add some code to our ‘myLib’ that stabilizes the face.
Adding Stabilization-Specific Code to ‘myLib’
One thing we will need to do a lot is generate ‘virtual markers’ from the existing markers. To do this, we need a function that returns the average position of however many vectors (marker positions) it is fed.
#returns average position of an FBModelList as FBVector3ddef avgPos(models):
mLen =len(models)if mLen ==1:
return models[0].Translation
total = vec3(models[0].Translation)for i inrange(1, mLen):
total += vec3(models[i].Translation)
avgTranslation = total/mLen
return fbv(avgTranslation)
Here is an example of avgPos() in use:
Now onto the stabilization code:
#stabilizes face markers, input 4 FBModelList arrays, leaveOrig for leaving original markersdef stab(right,left,center,markers,leaveOrig):
pMatrix = FBMatrix()
lSystem=FBSystem()
lScene = lSystem.Scene
newMarkers =[]def faceOrient():
lScene.Evaluate()
Rpos = vec3(avgPos(right))
Lpos = vec3(avgPos(left))
Cpos = vec3(avgPos(center))#build the coordinate system of the head
faceAttach.GetMatrix(pMatrix)
xVec =(Cpos - Rpos)
xVec = xVec.normalize()
zVec =((Cpos - vec3(faceAttach.Translation)).normalize()).cross(xVec)
zVec = zVec.normalize()
yVec = xVec.cross(zVec)
yVec = yVec.normalize()
facePos =(Rpos + Lpos)/2
pMatrix[0]= xVec.x
pMatrix[1]= xVec.y
pMatrix[2]= xVec.z
pMatrix[4]= yVec.x
pMatrix[5]= yVec.y
pMatrix[6]= yVec.z
pMatrix[8]= zVec.x
pMatrix[9]= zVec.y
pMatrix[10]= zVec.z
pMatrix[12]= facePos.x
pMatrix[13]= facePos.y
pMatrix[14]= facePos.z
faceAttach.SetMatrix(pMatrix,FBModelTransformationMatrix.kModelTransformation,True)
lScene.Evaluate()#keys the translation and rotation of an animNodeListdef keyTransRot(animNodeList):
for lNode in animNodeList:
if(lNode.Name=='Lcl Translation'):
lNode.KeyCandidate()if(lNode.Name=='Lcl Rotation'):
lNode.KeyCandidate()
Rpos = vec3(avgPos(right))
Lpos = vec3(avgPos(left))
Cpos = vec3(avgPos(center))#create a null that will visualize the head coordsys, then position and orient it
faceAttach = FBModelNull("faceAttach")
faceAttach.Show=True
faceAttach.Translation= fbv((Rpos + Lpos)/2)
faceOrient()#create new set of stabilized nulls, non-destructive, this should be tied to 'leaveOrig' laterfor obj in markers:
new= FBModelNull(obj.Name + '_stab')
newTran = vec3(obj.Translation)new.Translation= fbv(newTran)new.Show=Truenew.Size=20new.Parent= faceAttach
newMarkers.append(new)
lPlayerControl = FBPlayerControl()
lPlayerControl.GotoStart()
FStart =int(lPlayerControl.ZoomWindowStart.GetFrame(True))
FStop =int(lPlayerControl.ZoomWindowStop.GetFrame(True))
animNodes = faceAttach.AnimationNode.Nodesfor frame inrange(FStart,FStop):
#build proper head coordsys
faceOrient()#update stabilized markers and key themfor m inrange(0,len(newMarkers)):
markerAnimNodes = newMarkers[m].AnimationNode.Nodes
newMarkers[m].SetVector(markers[m].Translation.Data)
lScene.Evaluate()
keyTransRot(markerAnimNodes)
keyTransRot(animNodes)
lPlayerControl.StepForward()
We feed our ‘stab‘function FBModelLists of right, left, and center stabilization markers, it creates virtual markers from these groups. Then ‘markers’ is all the markers to be stabilized. ‘leavrOrig’ is an option I usually add, this allows for non-destructive use, I have just made the fn leave original in this example, as I favor this, so this option does nothing, but you could add it. With the original markers left, you can immediately see if there was an error in your script. (new motion should match orig)
Creating an External UI that Uses ‘myLib’
Earlier I mentioned Creating Interactive MotionBuilder User Interface Tools, where I explain how to screenscrape/use the telnet Python Remote Server to create an interactive external UI that floats as a window in MotionBuilder itself. I also use the libraries mentioned in the above article.
The code for the facial stabilization UI I have created is here: [stab_ui.py]
I will now step through code snippets pertaining to our facial STAB tool:
This returns a list of strings that are the currently selected models in MBuilder. This is the main thing that our external UI does. The person needs to interactively choose the right, left, and center markers, then all the markers that will be stabilized.
At the left here you see what the UI looks like. To add some feedback to the buttons, you can make them change to reflect that the user has selected markers. We do so by changing the button text.
Example:
def rStabClick(self,event):
self.rStabMarkers= getSelection()printstr(self.rStabMarkers)self.rStab.Label=(str(len(self.rStabMarkers)) + " Right Markers")
This also stores all the markers the user has chosen into the variable ‘rStabMarkers‘. Once we have all the markers the user has chosen, we need to send them to ‘myLib‘ in MBuilder so that it can run our ‘stab‘ function on them. This will happen when they click ‘Stabilize Markerset‘.
Above we now use ‘modelsFromStrings‘ to feed ‘myLib’ the names of selected models. When you run this on thousands of frames, it will actually hang for up to a minute or two while it does all the processing. I discuss optimizations below. Here is a video of what you should have when stabilization is complete:
Kill the keyframes on the root (faceAttach) to remove head motion
Conclusion: Debugging/Optimization
Remember: Your stabilization will only be as good as your STAB markers. It really pays off to create tools to check marker stability.
Sometimes the terminal/screen scraping runs into issues. The mbPipe function can be padded out a lot and made more robust, this here was just an example. If you look at the external python console, you can see exactly what mbPipe is sending to MBuilder, and what it is receiving back through the terminal:
Sending>>> selectedModels = FBModelList()
Sending>>> FBGetSelectedModels(selectedModels,None,True)
Sending>>> for item in selectedModels: print item.Name
['Subject 1-RH1', 'Subject 1-RTMPL']
All of the above can be padded out and optimized. For instance, you could try to do everything without a single lPlayerControl.StepForward() or lScene.Evaluate(), but this takes a lot of MotionBuilder/programming knowhow; it involves only using the keyframe data to generate your matrices, positions etc, and never querying a model.