Stumbling Toward 'Awesomeness'

A Technical Art Blog

Monday, May 12, 2008

Creating Interactive MotionBuilder User Interface Tools

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.

Python/MotionBuilder Resources

You don’t need to be experienced with Python to follow this tutorial, this is actually the first Python script I have written. You do need two things to follow along: [Python] [wxPython]

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[0]))
          self.rotationLabel.SetLabel(('Rot: ' + rot[0]))
 
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].

posted by Chris at 1:03 AM  

2 Comments »

  1. good site hjqdmc

    Comment by ok — 2008/09/25 @ 9:33 AM

  2. I’m trying to get Python working in Motion Builder 2009, can someone contact me? No matter what I do with PYTHONPATH, I always get an ImportError..!

    Comment by robert — 2009/04/03 @ 5:05 PM

RSS feed for comments on this post. TrackBack URI

Leave a comment

Powered by WordPress