At FMX I gave a talk with my friend/coworker Mathias entitled: Crysis Management: Maxscript Tools Development at Crytek. The first part of this lecture is an introductory course on maxscript, showing many tips and tricks for beginners, the rest serves as an overview of maxscript tools development at Crytek, focusing on the Animation tools. I am very thankful that we were given the ability to do this, because I feel there are no real maxscript ‘cookbooks’ out there. [Tutorials/Slides Section]
Wednesday, May 14, 2008
Tuesday, May 13, 2008
This is another post in my short quest to profile per-frame functions in Motion Builder. This script below uses hotshot, the built in python profiler, to profile not only a function I write, but built in MBuilder c++ modules (FBPlayerControl().StepForward()).
8 9 10
#wrap motionbuilder c module functions you want to profile in python functions def stepForward(): lPlayerControl.StepForward()
Above is a snippet of the script below. We are wrapping the StepForward() MBuilder call in a function called stepForward(). Hotshot can profile any python function, so we trick it by wrapping a function around the c module.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
from pyfbsdk import * import hotshot import hotshot.stats import sys lPlayerControl = FBPlayerControl() #wrap motionbuilder c module functions you want to profile in python functions def stepForward(): lPlayerControl.StepForward() #this is our function that we want to profile def myFn(): FStart = int(lPlayerControl.ZoomWindowStart.GetFrame(True)) FStop = int(lPlayerControl.ZoomWindowStop.GetFrame(True)) FBPlayerControl().GotoStart() for frame in range(FStart,FStop): stepForward() #here we set up the hotshot profiler to create a profile file (binary) #by running our function (myFn) prof = hotshot.Profile("c:\\myFn.prof") prof.runcall(myFn) prof.close() #now we load the profile stats stats = hotshot.stats.load("c:\\myFn.prof") stats.strip_dirs() stats.sort_stats('time', 'calls') #and finally, we print the profile stats to the disk in a file 'myFn.log' saveout = sys.stdout fsock = open('c:\\myFn.log', 'w') sys.stdout = fsock stats.print_stats(20) sys.stdout = saveout fsock.close()
This is what the file we generated (myFn.log) looks like:
1305 function calls in 15411.020 CPU seconds Ordered by: internal time, call count ncalls tottime percall cumtime percall filename:lineno(function) 1304 15398.358 11.809 15398.358 11.809 speedtest.py:9(stepForward) 1 12.662 12.662 15411.020 15411.020 speedtest.py:13(myFn) 0 0.000 0.000 profile:0(profiler)
As you can see, FBPlayerControl().StepForward() is very resource intensive, and should rarely be used when crunching a lot of keyframe data. I hope this helps anyone in finding resource bottlenecks in large scripts, here’s an example of output from a larger script where I am trying to see how long scene.evaluate() and stepforward() are taking, and get rid of them by querying the keyframe data the way KxL showed before:
58745 function calls in 26273.622 CPU seconds Ordered by: internal time, call count ncalls tottime percall cumtime percall filename:lineno(function) 1304 14243.406 10.923 14243.406 10.923 stabtest.py:57(stepForward) 5217 8448.493 1.619 8448.493 1.619 stabtest.py:52(evalScene) 5216 1347.949 0.258 1347.949 0.258 stabtest.py:99(keyTransRot) 5223 1028.827 0.197 1028.827 0.197 stabtest.py:23(getPosition) 1 672.045 672.045 26273.394 26273.394 stabtest.py:62(stab) 1305 268.026 0.205 3963.387 3.037 stabtest.py:67(faceOrient) 5226 53.200 0.010 68.283 0.013 stabtest.py:16(vec3) 5220 40.443 0.008 40.443 0.008 euclid.py:485(__abs__) 2610 31.510 0.012 36.667 0.014 euclid.py:384(__sub__) 5220 30.552 0.006 70.996 0.014 euclid.py:497(normalize) 13058 30.549 0.002 30.549 0.002 euclid.py:285(__init__) 2610 27.759 0.011 32.958 0.013 euclid.py:519(cross) 3918 22.071 0.006 796.673 0.203 stabtest.py:34(avgPos) 1306 16.727 0.013 19.413 0.015 euclid.py:439(__div__) 1306 11.738 0.009 14.161 0.011 euclid.py:354(__add__) 1 0.228 0.228 26273.622 26273.622 stabtest.py:152(stabCall) 4 0.100 0.025 0.100 0.025 stabtest.py:20(fbv) 0 0.000 0.000 profile:0(profiler)
As you can see above, most of the time is spent on scene evaluation and stepforward.
Monday, May 12, 2008
KxL on Autodesk’s ‘The Area‘ forum has really shed some light on to why MotionBuilder was so slow when iterating through frames. It all boils down to the usage of FBSystem().Scene.Evaluate(). This evaluates the entire scene and is very, very slow. Motionbuilder does not allow for getting a parameter at time i, or do partial scene evaluation.
So this basically means that any time you need to get the position, rotation, or matrix of a model, you should go to the keyfame data directly. (unless it’s a constraint!) Here is an example function that prints position every frame. It does so by getting the information from the fcurves themselves, and *not* querying the nodes.
#prints position given a model and a frame def PrintPosition(pModel,pFrame): lFrameTime = FBTime(0,0,0,pFrame) lAnimationNode = pModel.Translation.GetAnimationNode() lPositionV = FBVector3d( lAnimationNode.Nodes.FCurve.Evaluate(lFrameTime), lAnimationNode.Nodes.FCurve.Evaluate(lFrameTime), lAnimationNode.Nodes.FCurve.Evaluate(lFrameTime)) print lPositionV del(lFrameTime, lAnimationNode, lPositionV)
Here is another example, showing how to build a transformation matrix from animation data (nodes). This is required because the normal way of requesting the matrix of a model in MBuilder (model.GetMatrix(pMatrix)), requires an Evaluate(). The following requires importing the following libraries: [math].
def FBMatrixFromAnimationNode( pModel, pTime ): lResult = FBMatrix() lTranslationNode = pModel.Translation.GetAnimationNode() lRotationNode = pModel.Rotation.GetAnimationNode() lScaleNode = pModel.Scaling.GetAnimationNode() lRotationV = FBVector3d( lRotationNode.Nodes.FCurve.Evaluate(pTime) * 0.017453292519943295769236907684886, lRotationNode.Nodes.FCurve.Evaluate(pTime) * 0.017453292519943295769236907684886, lRotationNode.Nodes.FCurve.Evaluate(pTime) * 0.017453292519943295769236907684886) lScaleV = FBVector3d( lScaleNode.Nodes.FCurve.Evaluate(pTime), lScaleNode.Nodes.FCurve.Evaluate(pTime), lScaleNode.Nodes.FCurve.Evaluate(pTime)) sphi = math.sin(lRotationV) cphi = math.cos(lRotationV) stheta = math.sin(lRotationV) ctheta = math.cos(lRotationV) spsi = math.sin(lRotationV) cpsi = math.cos(lRotationV) lResult = (cpsi*ctheta)*lScaleV lResult = (spsi*ctheta)*lScaleV lResult = (-stheta)*lScaleV lResult = (cpsi*stheta*sphi - spsi*cphi)*lScaleV lResult = (spsi*stheta*sphi + cpsi*cphi)*lScaleV lResult = (ctheta*sphi)*lScaleV lResult = (cpsi*stheta*cphi + spsi*sphi)*lScaleV lResult = (spsi*stheta*cphi - cpsi*sphi)*lScaleV lResult = (ctheta*cphi)*lScaleV lResult = lTranslationNode.Nodes.FCurve.Evaluate(pTime) lResult = lTranslationNode.Nodes.FCurve.Evaluate(pTime) lResult = lTranslationNode.Nodes.FCurve.Evaluate(pTime) return lResult
Soon, I hope to build an example tool showing how to iterate through frames and do facial motioncapture stabilization. That was sort of how all this came up, I wrote the tool, but it was ~20 times slower than it should have been.
Monday, May 12, 2008
This is a copy of a tutorial stored in the Tutorial section of my site.
One of the biggest complaints about MotionBuilder, from a Tech Artist/Animator standpoint is it’s Python implementation. Everyone seems to agree: It is very hard to make user-friendly tools in MotionBuilder. Which is why, as you will see, this tutorial is about creating user-friendly tools outside of MotionBuilder.
Why Is Impossible to Create an Interactive UI in MotionBuilder? Is There Another Way?
Because of threading issues, any time you create a window or tool UI in MB, the whole app freezes until you close your window. This makes it very hard to create tools that people interact with and use. I heard from someone at Sony Pictures Imageworks that it is possible to create user-friendly UI, they did it there as a seperate Python app that ‘talks’ to MB through a telnet terminal.
So this is what I did. I first scoured the ‘internets’ and found little or no information on using the telnet ‘Remote Server’ Python implementation in MB, so I decided to create this tutorial.
Plug: Also, if you are serious about Python Scripting in MotionBuilder, You should grab Jason Parks’ Autodesk Master Class: Python Scripting for MotionBuilder Artists, not only because he’s a friend of mine, but also because it’s an invaluable rescource (the only resource) when just starting out.
Getting Started: Remote Server and Telnetlib
Open up MB and select Window>Python Console Tool, under the ‘Configuration’ tab, you should see the following:
The checkboxes ‘Enable‘ and ‘Allow Network Connections‘ are usually off by default. Turn them both on. Now there is a telnet server running on port 4242 of your PC that can accept Python commands and execute them in MB. If you press ‘Telnet Console‘ a terminal will pop open and connect. Entering commands into this terminal is like entering them into the MB Python Console.
Now open IDLE, the Python GUI/Script editor. Create a new script as follows, my comments in green:
#Import the Python telnet library import telnetlib #Create a telnet console to your PC (127.0.0.1) and MotionBuilder (4242) host = telnetlib.Telnet('127.0.0.1', 4242) #Find the prompt, so you can enter a command (seriously) host.read_until('>>>', 5) #Create a null called 'locator' host.write('myNull = FBModelNull("locator")\n') #Close the terminal host.close()
If you alt+tab back to MB, you should see a null named ‘locator’ in the Schematic View. What we are basically doing is creating an invisible terminal window, like the one we saw when we clicked ‘Telnet Console‘. We then want to send the command myNull = FBModelNull(“locator”) over, but in order to do so, we first need to find the prompt. ‘\n‘ means ‘new line’, so we are essentially entering a command at the prompt and pressing [ENTER].
Communication Issues: Writing a Function to Bridge the Gap
First, you do not want to have to do this stuff every time you need to enter a Python command. Second, you need a way of getting data that is returned as a result of your command/query. This is what a simple function to do that looks like in Python:
def mbPipe(command): host.read_until('>>>', .01) #write the command host.write(command + '\n') #read all data returned raw = str(host.read_until('>>>', .1)) #removing garbage i don't want raw = raw.replace('\n\r>>>','') raw = raw.replace('\r','') #make an array, 1 element per line returned rawArr = raw.split('\n') return rawArr
So now we have a function that makes it easier for us to communicate with MB through telnetlib. Here is an example of it’s use:
mbPipe('selectedModels = FBModelList()') mbPipe('FBGetSelectedModels(selectedModels,None,True)') mbPipe('for item in selectedModels: print item.Name')
The code above returned the names of the currently selected items in MB as an array. ([‘Null’, ‘Null 1’, ‘Null 2’, ‘locator’, ‘Test’]) Because this returns an array, it is easy for us to write something a little more complicated like this:
for item in (mbPipe("for item in selectedModels: print item.Name")): self.selListBox.Append(item)
This adds all items selected in MB to a listbox. Now that you have an
understanding of how to communicate with MotionBuilder through the
Remote Server and an external Python process via telnetlib, let’s now
build a tool to take advantage of this.
Practical Example of Interactive, External, MotionBuilder UI
I’m not really going to walk you through this one. It’s basically some modified wxPython example code from the internets bent to my bidding. You will see our mbPipe function and some things from above.
I decided to use wxPython because it’s a very simple and efficient UI library for Python that allows you to quickly create decent custom interfaces. You can of course use anything other tools to build a UI.
To the left here is a little tool. When you press ‘Get Selected Items‘, the listbox will fill with the ‘models’ currently selected in MB. As you click on items in the listbox, the position/rotation text at the bottom will change reflecting the position and rotation of the ‘models’ in MB. Pressing “Delete This Model‘ will delete the object in MB.
import wx import telnetlib host = telnetlib.Telnet("127.0.0.1", 4242) def mbPipe(command): host.read_until('>>>', .01) #write the command host.write(command + '\n') #read all data returned raw = str(host.read_until('>>>', .1)) #removing garbage i don't want raw = raw.replace('\n\r>>>','') raw = raw.replace('\r','') rawArr = raw.split('\n') #cleanArr = [item.replace('\r', '') for item in rawArr] return rawArr class MyFrame(wx.Frame): def __init__(self): # create a frame, no parent, default to wxID_ANY wx.Frame.__init__(self, None, wx.ID_ANY, 'mbUI test',pos=(200, 150),size=(175,280)) #create listbox self.selListBox = wx.ListBox(self, -1, choices=,pos=(8,38),size=(150, 145)) self.selListBox.Bind(wx.EVT_LISTBOX, self.selListBoxChange) #create model metadata self.translationLabel = wx.StaticText(self,-1,'Pos:',pos=(8,185)) self.rotationLabel = wx.StaticText(self,-1,'Rot:',pos=(8,200)) #make buttons self.selItems = wx.Button(self, id=-1,label='Get Selected Items',pos=(8, 8),size=(150, 28)) self.selItems.Bind(wx.EVT_BUTTON, self.selItemsClick) self.selItems.SetToolTip(wx.ToolTip("Gets all 'models' selected in Motion Builder")) self.delItems = wx.Button(self, id=-1,label='Delete This Model',pos=(8, 217),size=(150, 28)) self.delItems.Bind(wx.EVT_BUTTON, self.delItemsClick) self.delItems.SetToolTip(wx.ToolTip("Will delete selected models")) # show the frame self.Show(True) def selItemsClick(self,event): mbPipe("selectedModels = FBModelList()") mbPipe("FBGetSelectedModels(selectedModels,None,True)") self.selListBox.Set() for item in (mbPipe("for item in selectedModels: print item.Name")): self.selListBox.Append(item) def delItemsClick(self,event): mbPipe('FBFindModelByName("' + str(self.selListBox.GetStringSelection()) + '").FBDelete()') mbPipe("selectedModels = FBModelList()") mbPipe("FBGetSelectedModels(selectedModels,None,True)") self.selListBox.Set() for item in (mbPipe("for item in selectedModels: print item.Name")): self.selListBox.Append(item) def selListBoxChange(self,event): trans = mbPipe('FBFindModelByName("' + str(self.selListBox.GetStringSelection()) + '").Translation') rot = mbPipe('FBFindModelByName("' + str(self.selListBox.GetStringSelection()) + '").Rotation') self.translationLabel.SetLabel(('Pos: ' + trans)) self.rotationLabel.SetLabel(('Rot: ' + rot)) application = wx.PySimpleApp() window = MyFrame() application.MainLoop()
Conclusion: This Sucks, but it Makes You A Better Person
Sure, this is a pretty unconventional way to have to create tools, but suffering this will make you a better Technical Artist/Animator. Not to mention you can now create great, interactive tools, without resorting to C++ and the Open Reality SDK.
Sync Issues?: It seems that sometimes MB just closes the connection, or perhaps telnetlib does. Either way, if your tool becomes unresponsive, the trick I found is to close the tool, then uncheck/recheck ‘Allow Network Connections‘ in the ‘Python Console Tool‘ Configuration tab shown above. Then re-run your script.
PyWin32: There is another Python library called Python Win32 Extentions, that allows you to get your hands dirty with COM and things. You can use some of these to make it so that your tool shows up as a child window of the MB app itself, this way users don’t even have to press alt+tab to use your app and it looks like it belongs in MBuilder. Of course there is no mention of this on teh internets, but I found an example for 3DSMax/Python in Adam Pletcher’s GDC talk: Python for Technical Artists, and a Maya/Python example on cgTalk. This is somewhat more difficult; using the above sources I have written a MotionBuilder example of this [here].
Monday, May 5, 2008
I have written a GeSHi syntax file for maxscript. This allows anything that uses GeSHi, like WP-Syntax, to properly highlight maxscript syntax. As an example, this below is the maxscript version of the MBuilder code to print out a position every frame.
fn timeSink obj = ( for i = animationrange.start to animationrange.end do ( at time i (print obj.position) ) ) start = timeStamp() timeSink (selection as array) end = timeStamp() format "Processing took % seconds\n" ((end - start) / 1000.0)
You can grab my syntax file [here]
Friday, May 2, 2008
This is a follow-up to my post about per-frame operations in MotionBuilder. For some reason they take forever. I will show you an example, and compare the MB Python implementation to other scripting implementations by executing the same code in another Autodesk product (3dsMax).
First, lets find a way to see how long it takes to execute a block of code. In the example below, the script will
from pyfbsdk import * import time #get the time t1 = time.time() #get selected items selectedModels = FBModelList() FBGetSelectedModels(selectedModels,None,True) #print each items position for item in selectedModels: print item.Translation #tell us how long that took (time start - time finish) print ('Process took ' + str((time.time() - t1)) + ' seconds')
I can run this on some selected models and it reports: Process took 0.0 seconds. Now if you run this on, say, 60 markers, the output window has no scrollbar, and unless you have a large screen, you will not see the last output telling you how long the process took. So we can replace the print with a messagebox:
duration = str((time.time() - t1)) FBMessageBox('Process Info:',('Process took ' + duration + ' seconds'),'ok')
Testing a Function That Loops Through Keys
Let’s take our example from before, where we go through every frame and print the position of an object in MB. Alter that code by inserting the code from above. Add the ‘import time‘ at the top, and then set t1 before you run posEveryFrame(selectedModels). Now after that function, add your line that generates a messagebox.
When I run this on a single object across 1369 frames I get the messagebox above: it takes almost 10 seconds! Remember, we are just printing a position of an object here. I wrote the same code in Maxscript and it took 0.703 seconds. This is 13 times faster than MotionBuilder, a program that prides itself on dealing with keyframe data!
I think this is because MotionBuilder is actually moving the timeslider itself, even though the UI is not updating! This must be the case, because it is just unbelievably slow. So next we will research whether or not we can grab scene info at a certain time.
Thursday, May 1, 2008
In any animation package, there are many times technical artists/animators will need to iterate through all the frames of animation and do something. This is a given.
Accessing The Timeline
To access timeline functionality in MBuilder you can either get the current take and then request it’s start/stop or use the following, which grabs the start and stop in the current take/timeline:
However, this only returns a frame number in the range of your FPS, for instance, if your end frame is 1369, it will return the value 19L. To better understand this, enter 1369%30 into your Python Console, it will return 19. More info on modulo operations [here]
So how do we get time from MotionBuilder in a usable format? Here are two examples:
#this retuns the real frame number FBPlayerControl().ZoomWindowStop.GetFrame(True) #this returns in seconds</span></code> FBPlayerControl().ZoomWindowStop.GetSecondDouble()
Iterating Through the Timeline
Now that we can get the range of the frames in the timeline, let’s do stuff! Here is an example you can execute in MB showing how to iterate through time and do something for every frame (print frame number):
from pyfbsdk import * lSystem=FBSystem() lScene = lSystem.Scene playaCtrl = FBPlayerControl() #this returns the start/stop as integers<code> fStart = int(playaCtrl.ZoomWindowStart.GetFrame(True)) fStop = int(playaCtrl.ZoomWindowStop.GetFrame(True)) for frame in range(fStart,fStop): print frame
Now, this is not to be confused with doing something on every frame. Above, we are just iterating through the frame numbers themselves. If there are 1000 frames, you are just going from number 1 to 1000 printing the current number.
So now let’s iterate through frames and do something every frame.
from pyfbsdk import * def posEveryFrame(model): lSystem=FBSystem() lScene = lSystem.Scene #we instance the player control to read from it faster playaCtrl = FBPlayerControl() #set our start and stop (as integers) fStart = int(playaCtrl.ZoomWindowStart.GetFrame(True)) fStop = int(playaCtrl.ZoomWindowStop.GetFrame(True)) #tell player control to go to the start of the scene playaCtrl.GotoStart() #now iterate through the frames for frame in range(fStart,fStop): #evaluate the scene to make sure the data is updated lScene.Evaluate() #output the models position print model.Translation #tell the player to go to the next frame playaCtrl.StepForward() selectedModels = FBModelList() FBGetSelectedModels(selectedModels,None,True) #run our function on the first model in the selection posEveryFrame(selectedModels)
So, executing this with a model selected should print the models translation (position) every frame for all frames in your timeslider (transport control).
Now you can start messing around with iterating through frames, and you will immediately notice two things:
1) Iterating through frames in MotionBuilder is slow. Almost slower than my grandmother entering all the values into a calculator (not even one with large numbered buttons)
2) The Python Console that you print to does not have a scroll bar, and does not have selectable text. (complete and utter fail)