Wednesday 19 January 2011

Houdini Python .asCode()

Been writing some Houdini python code recently to help with the import and export of animation data from our 3 main animation packages (Maya, XSI, Houdini) as well as to my C++ / Python engines.

Houdini has a good python model and I discovered a really useful feature in most of the nodes which is a method called .asCode(). What this does it returns as a string the code used to create the current node.

This is really good as you can get output and inspect for you own code. I decided to write a simple shelf tool for automating this process and dumping the data out to a file.

GetAbsoluteFileName
The first issue I had was with the Houdini file manager shown below
If a file is chosen from the normal file system we get an absolute file name, however Houdini has the ability to set up certain local environment variables which it uses internally. These however are not translated into the python scripts. These variables are $HIP, $JOP, $HOME which will be returned with any filename selected for example $HIP/myGeo.bgeo.

This needs to be translated into an absolute filename before python can access it, so a simple function was written to pop up the dialog and then strip any of the environment variables and replace them with the actual values. This is shown in the function below (which I use in a lot of other code)
########################################################################################################################
##  @brief a basic function to return a file name / absolute path stripping off $HIP etc
##  @param[in] _title the title to be displayed in the file box
##  @param[in] _wildCard the file selection wildcard i.e. *.obj etc
##  @param[in] _fileType the houdini file type option e.g. hou.fileType.Any
##  @returns a fully qualified file path or None
########################################################################################################################

def GetAbsoluteFileName(_title,_wildCard,_fileType) :
 # launch a file select and get the data
 file=hou.ui.selectFile(None,"Select File To Save",False,_fileType,"*.py","",False,False,hou.fileChooserMode.Write)
 # if it was empty bomb out and return none
 if file =="" :
  return None
 else :
  # so we got some data we need to split it as we could have $JOB $HIP or $HOME prepended
  # to it  if we partition based on the / we get a tuple with "", "/","/....." where the
  # first element is going to be an environment var etc.
  file=file.partition("/")
  # we have $HOME so extract the full $HOME path and use it
  if file[0]=="$HOME" :
   prefix=str(hou.getenv("HOME"))
  elif file[0] == "$HIP" :
  #we have $HIP so extract the full $HIP path
   prefix=str(hou.getenv("HIP"))
  # we have a $JOB so extract the full $JOB path
  elif file[0] == "$JOB" :
   prefix=str(hou.getenv("JOB"))
  # nothing so just blank the string
  else :
   prefix=str("")
  #now construct our new file name from the elements we've found
  return "%s/%s" %(prefix,file[2])

Now this has been written we can use it to popup a dialog and return the filename

HouAsCode.py
The rest of the function is simple. We see which nodes are selected in houdini iterate through all of them calling the asCode() method

file=GetAbsoluteFileName("Enter File to Save ","*.py",hou.fileType.Any)
if file != None :
 fileOut=open(file,'w')
 fileOut.write("#############################################\n")
 fileOut.write("# generated by Hou as Code script \n")
 fileOut.write("#############################################\n")

 for objects in hou.selectedNodes() :
  fileOut.write("#############################################\n")
  fileOut.write("# from object %s \n" %(objects.name()))
  fileOut.write("#############################################\n")
  fileOut.write("%s \n" %(objects.asCode()))
  fileOut.write("#############################################\n\n")
 fileOut.close()

You can download the full script here

Installation
The easiest way to run this script is to place a shelf button into Houdini with the script embedded into it. First right click onto the shelf you wish to place the button on as shown
Next we edit the tool and add the name etc as shown
Finally we can paste the script into the tool as shown in the next dialog

Make sure the script language combo box is set to python and then press the accept button.

Output
The following is a sample output from the tool

#############################################
# generated by Hou as Code script 
#############################################
#############################################
# from object helixRename 
#############################################
# Initialize parent node variable.
if locals().get("hou_parent") is None:
    hou_parent = hou.node("/obj/helixObjectImport/helixBakeChannel")

# Code for /obj/helixObjectImport/helixBakeChannel/helixRename
hou_node = hou_parent.createNode("rename", "helixRename", run_init_scripts=False, load_contents=True)
hou_node.move(hou.Vector2(0, 0))
hou_node.setAudioFlag(False)
hou_node.setExportFlag(False)
hou_node.bypass(False)
hou_node.setDisplayFlag(False)
hou_node.setSelected(True)
hou_node.setUnloadFlag(False)

# Code for /obj/helixObjectImport/helixBakeChannel/helixRename/stdswitcher1 parm 
if locals().get("hou_node") is None:
    hou_node = hou.node("/obj/helixObjectImport/helixBakeChannel/helixRename")
hou_parm = hou_node.parm("stdswitcher1")
hou_parm.set(0)
hou_parm.setAutoscope(False)
hou_parm.lock(False)

# Code for /obj/helixObjectImport/helixBakeChannel/helixRename/renamefrom parm 
if locals().get("hou_node") is None:
    hou_node = hou.node("/obj/helixObjectImport/helixBakeChannel/helixRename")
hou_parm = hou_node.parm("renamefrom")
hou_parm.set("?[xyz]")
hou_parm.setAutoscope(False)
hou_parm.lock(False)

# Code for /obj/helixObjectImport/helixBakeChannel/helixRename/renameto parm 
if locals().get("hou_node") is None:
    hou_node = hou.node("/obj/helixObjectImport/helixBakeChannel/helixRename")
hou_parm = hou_node.parm("renameto")
hou_parm.set("t[xyz]0")
hou_parm.setAutoscope(False)
hou_parm.lock(False)

# Code for /obj/helixObjectImport/helixBakeChannel/helixRename/scope parm 
if locals().get("hou_node") is None:
    hou_node = hou.node("/obj/helixObjectImport/helixBakeChannel/helixRename")
hou_parm = hou_node.parm("scope")
hou_parm.set("*")
hou_parm.setAutoscope(False)
hou_parm.lock(False)

# Code for /obj/helixObjectImport/helixBakeChannel/helixRename/srselect parm 
if locals().get("hou_node") is None:
    hou_node = hou.node("/obj/helixObjectImport/helixBakeChannel/helixRename")
hou_parm = hou_node.parm("srselect")
hou_parm.set("max")
hou_parm.setAutoscope(False)
hou_parm.lock(False)

# Code for /obj/helixObjectImport/helixBakeChannel/helixRename/units parm 
if locals().get("hou_node") is None:
    hou_node = hou.node("/obj/helixObjectImport/helixBakeChannel/helixRename")
hou_parm = hou_node.parm("units")
hou_parm.set("seconds")
hou_parm.setAutoscope(False)
hou_parm.lock(False)

# Code for /obj/helixObjectImport/helixBakeChannel/helixRename/timeslice parm 
if locals().get("hou_node") is None:
    hou_node = hou.node("/obj/helixObjectImport/helixBakeChannel/helixRename")
hou_parm = hou_node.parm("timeslice")
hou_parm.set(0)
hou_parm.setAutoscope(False)
hou_parm.lock(False)

# Code for /obj/helixObjectImport/helixBakeChannel/helixRename/unload parm 
if locals().get("hou_node") is None:
    hou_node = hou.node("/obj/helixObjectImport/helixBakeChannel/helixRename")
hou_parm = hou_node.parm("unload")
hou_parm.set(0)
hou_parm.setAutoscope(False)
hou_parm.lock(False)

# Code for /obj/helixObjectImport/helixBakeChannel/helixRename/export parm 
if locals().get("hou_node") is None:
    hou_node = hou.node("/obj/helixObjectImport/helixBakeChannel/helixRename")
hou_parm = hou_node.parm("export")
hou_parm.set("/obj")
hou_parm.setAutoscope(False)
hou_parm.lock(False)

# Code for /obj/helixObjectImport/helixBakeChannel/helixRename/gcolor parm tuple
if locals().get("hou_node") is None:
    hou_node = hou.node("/obj/helixObjectImport/helixBakeChannel/helixRename")
hou_parm_tuple = hou_node.parmTuple("gcolor")
hou_parm_tuple.set((0.9, 0.9, 0))
hou_parm_tuple.setAutoscope((False, False, False))
hou_parm_tuple.lock((False, False, False))

# Code for /obj/helixObjectImport/helixBakeChannel/helixRename/gcolorstep parm 
if locals().get("hou_node") is None:
    hou_node = hou.node("/obj/helixObjectImport/helixBakeChannel/helixRename")
hou_parm = hou_node.parm("gcolorstep")
hou_parm.set(0.05)
hou_parm.setAutoscope(False)
hou_parm.lock(False)

hou_node.setColor(hou.Color([0.8, 0.8, 0.8]))
hou_node.setExpressionLanguage(hou.exprLanguage.Python)

# Code to establish connections for /obj/helixObjectImport/helixBakeChannel/helixRename
hou_node = hou_parent.node("helixRename")
if hou_parent.node("helixImportData") is not None:
    hou_node.setInput(0, hou_parent.node("helixImportData"), 0)
hou_node.setUserData("___toolcount___", "1")
hou_node.setUserData("___toolid___", "NCCAPointBakeImport")
 
#############################################

Monday 10 January 2011

ASD / CA1 Assignment Feedback and Comments Part 1 General Feedback

I've finally finished marking all the initial designs for the assignments and I decided to collate some of my thoughts / feedback here to make it easier for you all to see. I've also written individual comments on the assignment sheets (if you can read my handwriting !) so hopefully this will fill in some of you unanswered questions.

Coding Standard
I noticed with a lot of the class diagrams and pseudo code people were not following the coding standard here lots of people we doing things like my_function or position when it should be myFunction and m_position. You get marks for the coding standard as part of the assessment so please have a read of it and try to follow it.

It is important to follow standards as it has two major advantages

  1. Integration of your code into the main NCCA code base is a lot easier (and your code may be used by other students next year)
  2. You will have to use a coding standard in industry so following one is good practice.
Our coding standard is based on a couple of companies standards so it is quite realistic of things you may encounter in the future.

I also noticed several cases of strange / non-descriptive attribute / method names. Try to make you code read like a narrative as much as possible (and use loads of comments). One that really stuck out was m_vector : ngl::Vector.

So it's a vector but what is the vector representing? Try to be as descriptive as possible, modern editors will use code completion so will save typing and it will make your code easier to maintain and debug. 

Use enums
enums are a good way of defining constants and making code "self documenting" I saw one example of code presented which looked like this
if (type == 1)
{
  ... do something
}
else if (type == 2)
{
... do something else
}
Reading this code is hard to tell what 1 and 2 actually are. More maintainable code would look like this
enum Enemies{Tank,Helicopter};
Enemies enemyType=Tank;

if(enemyType==Tank)
{
  ... do something
}
else if (enemyType == Helicopter)
{
  ... do something else
}
The above code defines an enum called Enemies which can now be used as a data type, we can only assign it the values defined in the enum and thus makes the code easier to maintain / manage. By default the first value will be assigned 0. We can also add enums to the public area of a class and use the scope resolution operator to access it (see ngl::Material for good example e.g.  ngl::Material::GOLD)

Arrays
I saw quite a lot of attributes which were m_data : array or m_data[] : float etc etc. Wherever possible do not use an array in your programs, they will be a fixed size and makes your program more prone to overflow errors and you may also need to change the size. Where you need to store data in an array type format use a std::vector these are dynamic arrays which can be set to a fixed size to start with and then we can add or remove other elements when we wish. Performance wise they are almost the same as a normal array and the data is guaranteed to be contiguous so we can use it with other libraries etc.

If a std::vector is not suitable for for you needs then use dynamic allocation of the data using new. For example

float *volumeData = new float[WIDTH*HEIGHT*DEPTH];

Where to Start
The main thing about getting you all to do the design first is it will make the writing of the code easier in the long run. You should all now have class diagrams with a list of attributes and methods. You can now start creating these classes and testing them.

First generate a .h / .cpp file for each of the classes and define the class as follows

#ifndef __MYCLASS_H__
#define __MYCLASS_H__
class MyClass
{
  public :

  private :

};
#endif

Put all the attributes in the private section, and the methods in the public / private elements where appropriate. Once you have done this prototype each of the methods in the .cpp file or if mutators and accessors they may be placed inline in the .h file.

Next you need to write a test harness to check each of the methods and see if they all work. Obviously in more complex systems other classes may be dependent upon each other so work in a peice-wise fashion to get all the different elements working.

It is best to compile you code often and write small bits test and repeat, this will make it easier to spot error and try to reduce error creep.

That's it for this section I will try and address some other points in more posts.