PythonToHoudini8

From Odwiki

Jump to: navigation, search


Contents

The Python Connection

Introduction

Prior to Houdini 9, one way to connect to a live Houdini session via Python is to bypass the 'hcommand' utility altogether and communicate directly over the port. The advantage of this is that you aren't piping through that command - you're directly interacting with the current session. I will endeavour to give the basics of what goes on during this process and provide the working skeleton of a Python program that is a good place for would-be Houdini Pythoners to get their spam.

Note: it is assumed that the reader has a passing understanding of Python, although no advanced concepts are needed.


The Basics

Start up a session of Houdini, open up a textport and enter the command:

/obj -> openport 12000

The number 12000 isn't particularly important, however it's worth noting that on most capable operating systems it's not allowed for non-administrators to create access to ports that are very low, thus it's always a good idea to punch a healthy number in there. I'm going to use 12000 as the port of choice for this example, including what's in the code, but you can use whatever port you'd like - just be sure to match up the number you type in here with what you use in your Python program.

What this has done is made the current Houdini session "listen" to port 12000 for any activity. Any access over this port will be grabbed by Houdini and interpreted, whether it makes sense or not. Right off the bat, for example, if you have telnet installed on your computer, you can enter:

% telnet localhost 12000

in a shell (this is a Linux example - Windows will vary) and you should get:

Trying 127.0.0.1... Connected to localhost. Escape character is '^]'.

and silence. Type in "opls", and lo and behold, a listing of ops in whatever Houdini root you're in is listed! Telnet is sending the raw text you're typing(using the telnet protocol) and pumping that directly to port 12000. Python, Perl, almost any scripting language, really, can do the same.

Note for Windows

On Windows, you can't use telnet to an openport if you're using standalone mode which basically tells Houdini to operate as if the computer had no network ability. To turn it off, run the License Administrator. Then in the menu, choose File > Change License Server. Turn off Standalone mode. Hit Ok.

The Program

Let's get busy:

#!/usr/bin/env python

import socket, string , sys

#GLOBAL
#Define the system and port number - port should prob be passed as an arg
SERVER = 'localhost'
PORT = 12000

#define the socket to Houdini
HOU = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

def hou_conn():
   "Open a connection with Houdini"
   HOU.connect((SERVER, PORT))
   return()

def send_data(command):
   "Pass a command to Houdini"
   HOU.send(command + '\n')
   return()

def get_data():
   "Keep grabbing data from Houdini until you hit the delimiter"
   delimiter = '\x00'
   buffer = ""
   while delimiter not in buffer:
       buffer = buffer + HOU.recv(8192)
   return(buffer[:-2]) #trim the delimiter

def hou_close():
   "Close the connection with Houdini"
   HOU.close()
   return()

def build_obj():
   "Queries the session for an obj list, and builds a dictionary of params"
   send_data("cd /obj ; opls")
   buffer = get_data()
   objects = string.split(buffer)
   OBJ = {}
   for obj in objects:
       send_data("opparm -d "+ obj + " * ")
       objparms = get_data()
       num_parms = objparms.count(' (')
       #strip out the redundant 'opparm objname' at the head
       objparms = objparms[objparms.find(obj)+len(obj)+1:]
       key = []
       value = []
       for i in range(0,num_parms+1):
           key.append(objparms[:objparms.find('(')].strip())
           objparms = objparms[objparms.find('(')+1:]
           value.append(objparms[:objparms.find(' ) ')])
           objparms = objparms[objparms.find(' ) ')+3:]
       dict_obj = {}
       dict_obj.clear()
       for i in range(0,num_parms):
           dict_obj[key[i]] = value[i]
       OBJ[obj] = dict_obj
   return(OBJ)

def main():
   "Here we are, the main floor"
   hou_conn()

   #query the session for the opscript output for light1
   send_data("cd /obj ; opscript light1")
   buffer = get_data()
   print "HERE'S THE OPSCRIPT FOR LIGHT1:\n\n",buffer
   
   #now, a little more ambitious - create a dictionary containing all the
   # parameters for all the objects!

   OBJ = build_obj()

   print "\nHERE's THE WORKS!:\n\n",str(OBJ)  #that's a spicy meatball!

   print "\nHERE's THE PARAMS FOR LIGHT3:\n\n",OBJ['light3'].keys()

   print "\n...and here's a list of objects stored in OBJ:\n\n",OBJ.keys()

   print "\nand here's the aperture value for cam1:\n\n",OBJ['cam1']['aperture']

   hou_close() #close the connection!

if __name__ == "__main__":
   main()
   sys.exit()

The Guide

Apologies, a Wiki isn't always the best place to post a program in any language, but Python is particularly sticky because it uses indentation rather than brackets for bracing sections of code. It appears to be correct here, but remember if you're going to cut and paste from the web page to respect the indents!

Let's start at the beginning and run through this. Apart from the typical sys and string modules being imported, we also import the socket module. This is what we use to communicate over a port. We define a couple of global variables, SERVER and PORT, to match the machine(the special case name 'localhost') and the port we opened in Houdini.

Next, we use the magic in the socket module to create an object that will communicate over the port, HOU. For further options check out the Python docs, but this should work for what we're doing. Using the various built-in methods, such as "connect" and "send", we'll talk to Houdini.

I'm an orderly sod, so I like to have things broken up into functions, no matter how simple. The next few functions pretty well do what their names say: they CONNECT(hou_conn()), SEND(send_data()), GET(get_data()), and SHUT DOWN the connection with Houdini(hou_close()). Examining these show pretty obvious socket calls, with the possible exception of get_data(). Let's discuss that briefly.

When you talk over a socket to a program, there's a number of different ways you can decide to communicate. Remember, this is a very raw form of communication - there's no protocols that Houdini uses. It simply listens to that port, and whatever text or binary information it receives, it attempts to interpret as if you had typed that into a textport. This is pretty primitive stuff! In fact, when you type something to Houdini in this manner, there's no way you can be sure exactly how much data, if any, will be sent back! Some programs will send a header at the start, dictating how large the incoming packet will be, but Houdini can't do this as it would interfere with the command-line style interface it's using. To add to the confusion, you must tell Python how large a packet you want to grab everytime you wait to get information back! If you check out the HOU.recv(8192) call in the get_data() function, that is actually saying "give me 8192 bytes of data from this port, please." What if you're expecting back 8193? 20000? What Houdini does, luckily, is to send '\x00' at the end of each transmission. That's what this function does: it "goes back to the well" as many times as it needs with it's "8192 byte bucket" until it hits the end of transmission.

OK, enough geek talk. Let's move onto the juice of the program.

We're going to skip the build_obj() function for now, because it's a slightly more advanced example of getting some very useful information out of a session and putting it into those juicy data formats that Python excels at. For now, let's move onto the main() section and look at the program running.

The first thing we do is call hou_conn(), so we can talk to Houdini, then we send a command string to it - in this case an explicit string "cd /obj ; opscript light1", which is pretty straightforward. However, if we stop there, nothing will happen, since the only result of this command is to go to the /obj location, and send back a streaming script that can be used to create the current incarnation of light3(I'm assuming you have a default session with three lights, a cam1, an ambient light, and an empty "model" geometry). We need to grab that data that Houdini spits out, and we do that with the get_data() command, which in this case will pump that info into a variable called "buffer". We then print it.

Now let's get a little more sophisticated. The purpose of the build_obj() function is to go into /obj, find out what objects are there, then create a Python dictionary called OBJ, which contains keys called cam1, light1, light2, etc. - the contents of /obj. The values associated with these keys are in fact themselves dictionaries, which contain all the Houdini parameters and their values used to define the objects. If I wanted to reference cam1's fstop, I could reference:

"OBJ['cam1']['fstop']"

I'm not going to handhold the entire process, because I think to the average Pythoner it should be fairly straightforward, but let's peruse it briefly.

First, we send the opls command and grab the listing of the contents of the /obj location. Using a string.split, we punch a list of these objects into the objects variable.

After initializing the OBJ dictionary, we begin the loop that will step through each of these objects. For each one, we send the opparm command that feeds back the parms for the current object. Note I quickly trim off the first two words since the result of opparm give it to you in the form of an executable command. In this case we just want the data, so we snip out "opparm objname". Next for each of the parms we create key and value lists, that will be used later as key:value entries to build the dictionary. We also snip away at our param list as we go, until they are completed.

After this, we cycle through our key:value pairs and assign them to the dictionary, then repeat the whole thing for the next object.

Back in the main(), there are a few examples of accessing information in the OBJ structure we've built. After that, we do what every well-behaved programmer does - closes the door behind them, with hou_close().

Where From Here?

Anyone familiar with the power of Python knows the answer to this. OOP support in Python would allow the user to set up their own classes and actions based on the Houdini paradigm of a hip file being represented internally as a directory structure, allowing for some pretty sophisticated tricks in generating or moving data around. The various structures available could allow for some powerful massaging of the data in a hip file. You could even make your own "textport" in Python, typing in custom commands along with the standard hscript fare. Additionally, GUI's can be created easily with Python:Tk, letting you prototype some interfaces to Houdini. Of course, the power of HDA's hasn't even been broached - the possibility of embedding Python code inside of an HDA has some interesting possibilities.

There's still some issues that need to be dealt with even in this simple example. For instance, there are a couple of params that actually use brackets inside brackets for params(not nice!), and this could cause an improper parse. I've currently hacked around this by using an assumption that the final right delimiter for a parameter from Houdini will always be a right bracket with single spaces on either side, which is the standard way that opparm sends out it's information. It's unlikely, but conceivable, that this specific combination could occur inside a parameter and this will mess up the parsing. This should be done using a proper parser that will keep track of left/right brackets to ensure nothing is being mangled. Also, currently there's no automated method of predetermining the port number. My current thinking is that perhaps this whole thing should be called as an hscript program, which opens a port, and upon succeeding, passes that port number to a Python program as a parameter. See "Wrapping in HDA" below.

Hopefully this program gets you up and running quickly and gets you thinking of some clever tricks that be done using Python with Houdini!

Wrapping in HDA

One way of initializing this script would be to create a button in an HDA. In the Callback of that button put ( on Linux ):

set portnum = `arg(run("openport -a"),1)`;unix python /mnt/scripts/houdiniSocket.py $portnum &

There are a couple of things to note here:

1) A port number is automatically created using the -a option. This number is then passed on as the first argument in Python.

2) The "&" is used to run Python in the background. This frees up Houdini to send and receive commands from Python.

Next you will need to change one line in the above code:

PORT = string.atoi(sys.argv[1])

This will take that first argument, as a string, and convert it to an integer.

Wrapping *Everything* in HDA

To add to the above notion, you can also wrap the Python code directly into the HDA, thus ensuring the program version you're going to run is the correct one. Once your Python code is written and you've created your digital asset, select the Tools/Operator Type Manager from Houdini, and find the location of your HDA. Let's say you've created an Object context OTL, and you've called the asset "Python" for this example. RMB over it and select "Edit Contents", and a new window will come up, most likely with some existing contents. Using the File Name browser, find and select your Python script, enter a Section Name for the code(let's call this the name of the program: houdininSocket.py, to keep things orderly), and hit Add File. Notice your code is now in the Section List, and you may edit it on the right. Hit Accept to save the code to your asset.

Next we'll tweak the callback code mentioned above to have Houdini write out the code to disk on the fly, before calling it, using the otcontentsave command:

otcontentsave -o /tmp/tmp.py Object/python houdiniSocket.py
; set portnum = `arg(run("openport -a"),1)`
;unix python /tmp/tmp.py $portnum &

(all the above should be on one line). Note the above entry "Object/python" - this is the context and name of the OTL you have created, you will need to alter this based on those parameters.

This is a little bit messy - writing out code to /tmp and not cleaning up after itself, so it will be necessary to clean this process up to account for permissions and housekeeping. Note, however, the importance of the "&" coming after the python call - if the backgrounding doesn't happen right there, then Houdini will lock up - as previously mentioned it must be run in the background so that Houdini can interact with the code.

Code Bits

I've added this section to be able to add information I learn along the way.

Send and Receive Data

Sometimes you only want to send information without the need to retrieve any data. For instance you might just want to set the current frame. When this happens, Houdini is still trying to send Python information ( Either that or the Socket class holds that return data in a buffer. Not sure which ). The next time you send information and expect to set a variable to the return result, you'll find that the results will be one command behind.( I've noticed this with the "pane" command. )

For this reason it would be better to combine both the send and receive definitions. This way you can chose to use the result from that, or leave it. This can easily be done with the following code bit:

    def hcommand(self,command):
        self.send_data(command)
        hou_get = self.get_data()
        return(hou_get)

With this definition, you can replace both the send_data() and get_data() commands with hcommand(). for example you can either set a variable to the result or just send the command:

ObjList = hcommand('opcf /obj; opls')

or

hcommand('fcur 12')

Examples

Example 1-Bundle viewer

This example will provide a basic Tkinter window with a browser setup to view all the hip file's Bundles. It can be taken further, but this example will give you some basic ideas on how to transfer information to allow for manipulation from within Python.

In this example I have setup three different widgets with different purposes. This first widget will provide a list of all the Bundle names. Each time you click on one of those names, their contents will be displayed in the second widget. The bottom Text widget will act as as textport. This can be used as a place to send debug information, or a way to let the user know what is happening.

Please note this script uses Pmw Megawidgets. You will need to have this loaded first before you can run this script.

#!/usr/bin/env python

from Tkinter import *
import tkFileDialog
import Pmw,string,sys
import socket


#Global Variables
HOST = "localhost"
PORT = string.atoi(sys.argv[1])
HOU = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
HOU.connect((HOST, PORT))
Bundle_Dict = {}            #Create default dictionary to build from.



class App:

    def __init__(self,master):
        frame = Frame(master)
        frame.pack(padx=10,pady=10)


        #----Define Buttons -------#

        self.BundleNames = Pmw.ScrolledListBox(frame,listbox_height=5,usehullsize=1,
                                               hull_width=150,hull_height=230,
                                               label_text="Bundle List",labelpos=N,
                                               selectioncommand=self.selectionCommand)
        self.BundleContents = Text(frame,width=50,height=20)                  
        self.ListObjects = Button(frame,text="Load Bundle List",state=NORMAL,
                                  command=self.list_objects)
        self.QuitButton = Button(frame, text="Quit",command=self.Quit)
        textport_rgb = "#%02x%02x%02x" % (128, 192, 200)
        self.Textport = Text(frame, width=55, height=10,background=textport_rgb)



        #---Define Layout----------#
        self.BundleNames.grid(row=0,column=0,sticky=NW)
        self.BundleContents.grid(row=0,column=1,sticky=NW)
        self.ListObjects.grid(row=0,column=0,sticky=S)
        self.Textport.grid(row=2,columnspan=2,sticky=S)
        self.QuitButton.grid(row=2,column=1,sticky=SE)
        


    #---------Definitions-----------------#


    def send_data(self,command):
        #Pass a command to Houdini
        HOU.send(command + '\n')
        return()

    def get_data(self):
        #Retrieve Data from Houdini until you hit the delimiter
        delimiter = '\x00'
        buffer = ""
        while delimiter not in buffer:
            buffer = buffer + HOU.recv(8192)
        return(buffer[:-2]) #trim the delimiter

    def hou_close(self):
        #Close the connection with Houdini
        HOU.close()
        return()
    
    def Quit(self):
        self.send_data("closeport "+str(PORT))
        self.hou_close
        root.quit()
        
    def list_objects(self):
        self.send_data("opcf /obj ; opbls")
        ObjList = self.get_data()               #Find out just the bundle names.
        BundleList = string.split(ObjList)
        for i in BundleList:                    #Build Bundle Dictionary
            self.send_data("opbls -L "+i)
            currentContent = self.get_data()
            currentContent = string.split(currentContent)
            Bundle_Dict[i]=currentContent
        for i in BundleList:
            self.BundleNames.insert(END,i)
        self.ListObjects.configure(state='disabled')
        #self.TexportCommand(BundleList)        #For testing feedback

    def selectionCommand(self):
        sels = self.BundleNames.getcurselection()
	if len(sels) == 0:
	    self.Textport.insert(END,'Must Load Bundle List First\n') 
	else:
            selDict = Bundle_Dict[sels[0]]
            self.BundleContents.delete(0.0,END)
            #self.BundleContents.insert(END,selDict)
            for i in selDict:
                start = string.rfind(i,"/")
                objname = i[start+1:]
                self.BundleContents.insert(END,i+"\n")
            self.TexportCommand("Current list for "+sels[0])

    def TexportCommand(self,command):
        self.Textport.delete(0.0,END)
        self.Textport.insert(END,command)
        self.Textport.insert(END,"\n")

    def SetFrame(self):
        setframenum = self.setFrameField.getvalue()
        self.send_data("fcur "+setframenum)


root = Tk()	
root.title("Bundle Manager")
root.geometry('+0+0')		#position window on monitor
app = App(root)
root.mainloop()

Insert non-formatted text here

© 2009 od[force].net | advertise