In my personal opinion, python in Houdini has been one of the most rewarding things to learn. In Houdini, python is a very open ended and well documented, and allows you to customize and enhance your daily experience in the software.
The aim of this advent challenge is to take anyone from nothing to comfortable building tools to aid your experience.
If you are interesting in following this, I suggest downloading this template package, and following the instructions on getting installed.
There are no rules, except to join the Discord, ask questions, and have fun!
Each day will ideally have a "Extra Info" section with some useful things at a similar level of difficulty to the days info. Read these and try and implement them where you see fit.
Download the template package from here, and place it somewhere in your assets folder. Open the TDP.json. Inside it, it will look like this:
{
  "env": [
    {
      "TDP": "path/to/folder"
    }
  ],
  "path": "$TDP"
}
Replace path/to/folder with the actual path to the TDP folder, for example: D:/Assets/TDP, and then copy this .json file to your Houdini preference folder (houdini20.5/packages)
For this template, I am using the name Twelve Days of Python, so TDP. We set the environment variable up with this name, and in most cases, I will prefix any file names with this, this ensures all the references to files down the line wont be ambiguous. eg. utils.py is probably going to exist somewhere, so calling it TDPUtils.py has as much higher chance of being unique.
Verify the install by opening Houdini, pressing CTRL+S and typing $TDP in the path, and check if it directs you to the correct folder.
While I don't claim to be the ideal teacher for this stuff, I have found myself looking for things and coming up short. Information is spread wide online, and once you know what to search for, things start popping up and searching becomes easier.
This is why I think arguably the most important thing to hone is your understanding of what you need to do. The thing I love most about this is that the code literally exists. There are functions for everything, and learning how to map more generic concepts into your specific use-case is a really powerful tool. Before that, we need to start at the beginning. (Something like this)
Quick reminder to download/redownload the package as you read this, as I may have added something since the first time you saw it.
Houdini has many places to store custom code: hou.session, shelf scripts, HDAs, python nodes, python panels, startup scripts, etc. Most of these are embedded in Houdini, which makes version control tough, and you may even find yourself ended up with a busted shelf where all of your tools are now blank (it's happened to me!).
We use this custom package to solve a few problems: Being able to share your files, being able to enable/disable things, to manage version control, and above all else, just to edit in your favorite code editor.
You don't have to do this, but I urge you to follow along and keep things neat and tidy.
Functions: A function in Python is a reusable block of code that performs a specific task. In the context of Houdini, functions are crucial for creating custom tools, managing node operations, and automating workflows. We will use these to bundle up our code into high performing chunks that we can adapt later down the line.
Function Arguments: Function arguments in Python allow you to pass different values to functions, and control the flow of code in the function. We can use this to customize node creation, parameter manipulation, and other operations.
Node Types: The way we know nodes might be something like "Attribute Noise", but behind the scenes, these nodes have what is called a "type name", which is what you see when you MMB on a node at the top. Attribute Noise ends up being "attribnoise::2:0". The node info shelf tool in the package can help you see these names.
We will start of simple with the basics on how to construct a function and how to pass arguments to it, this will allow us to create small wrappers that house our more complex code. This is useful for as a framework pass code between our Houdini and our code that lives in our package folder. This is also nice as at allows us to wrap commonly used workflows in a function and reuse it throughout our code.
We will create a system to make output nulls for our selected nodes, and learn how to loop through multiple nodes, this will help us understand the fundamentals of working with nodes and their paths, and how we can define nodes explicitly or programatically.
Once we know that, we will go over how to create new nodes, store them in our code and make connections to them and other nodes, as this is the foundation for all of our usual automation tasks. We will learn about node types, and how to use the docs to understand more about our nodes.
Further down the line, we will go into the more advanced things, like adding scripts the menus, creating UI prompts for options, reading attributes from our geometry, and even making our own custom python panels for menus and popups.
If you got this far, you may be wondering: "Where the hell is the python?" It's coming, but first, we need to learn to understand.
That being said, if you want to do your first ever python in Houdini: Open Houdini, click "window" at the top, and open the python shell. Paste this in:
import hou
hou.ui.displayMessage("Whoaaaaa!")
container = hou.node("/obj").createNode("geo", "whoaaaaa")
font = container.createNode("font")
font.parm("text").set("12 Days of Python")
a = font.createOutputNode("null", "OUT")
a.setColor(hou.Color((1.0, 0.0, 0.0)))
b = a.createOutputNode("null", "OUT")
b.setColor(hou.Color((0.0, 1.0, 0.0)))
c = b.createOutputNode("null", "OUT")
c.setColor(hou.Color((0.0, 0.0, 1.0)))
hou.ui.displayMessage("Let's get started!")
The simplest thing you can do is make Houdini talk back to you. The first thing we are going to do, is go up to the shelf tab for our package. You will see tool already called "Reload Module", as well as one called "Node Info". Don’t mind these for now, and right click in the empty space and create a new shelf tool.
Shelf tools can store and run code just the way they are, but it’s good practice to store the code in a file in your package, import it and run it from the shelf tool.
This is very important. It ensures that the script is stored in the package folder too and accessible by anyone who has the package installed.
Let’s come up with a good name for our script, for this example, let’s call it TDPUtils. In the code block, we write:
import TDPUtils
TDPUtils.greet()
You may be wondering what this means, where is TDPUtils and where is it being imported from?
Well, let’s open our package folder in our favorite code editor, and go to /scripts/python and create our file called TDPUtils.py.
You may have noticed, in our shelf tool, we used TDPUtils.greet(), this is when we make the greet function in our file:
def greet():
print("Hey there!")
Now, a caveat of Houdini, all of these files are loaded when it starts, so clicking our shelf tool will fail to pull our changes. Restart Houdini before the next step.
Click our shelf tool, and we should see a small window pop up that says… "Hey there!".
Congratulations, you have made your first shelf tool!
Click here for some notes about reloading these files so you can avoid restarting Houdini every time you make a change.
The "Reload Module" shelf tool acts as a button that will refresh all the loaded python files in the module. This is crucial for ensuring Houdini is reading the updated files you have been editing in your code editor. After any changes in your files, you can click this button and all your changes will be reloaded into Houdini.
Now that you have made your first function, one might think of how and where these could be used, and often times a bit of modularity is required too. Let's take the example of our greet function: How can we add more functionality? Running with our greet function, we can adjust the function to accept an argument. Let's call it first_time:
def greet(first_time):
if first_time:
print("Hey there!")
else:
print("Hello again!")
We added a condition to check if the first time argument is True, and if we reload and run now, we will get an error. We need to adjust our shelf tool to call the function with the argument:
import TDPUtils
TDPUtils.greet(True)
Now, if we pass True, it replies "Hey there!", and if we pass False, we get "Hello again!".
This is a simple example, but the concept of setting up logic and passing information from Houdini to our script is the foundation of everything we can do with Python in Houdini.
When we create our function, we can set it up to accept an optional argument, where we set a default value for the argument, and if the argument is omitted, it will use that value:
def greet(first_time=True):
if first_time:
print("Hey there!")
else:
print("Hello again!")
Now if we call greet() without the argument, we print "Hey there!".
If you did the prompt for Day 1, you may have ended up with a function that looks something like this:
import hou
def print_nodes():
nodes = hou.selectedNodes()
print(nodes)
# or maybe
for node in nodes:
print(nodes)
The second bit, looping through our nodes is also one of the bread and butter workflows when doing simpler stuff, especially in the start, where most of our interests lie in speeding up tedious workflows.
We know how to create a script and run it in Houdini, how to use arguments in our functions, how to loop through multiple nodes.
As showcased above, getting a node can be done with a few methods, the easiest of which is going to be the selected nodes. This is also common, as often you will want to operate on these. If you did the above, you would see it prints some gibberish as well as some recognizable info, such as the node name. The correct thing to do now would be to go and read through the docs page for hou.OpNode and hou.Node (the difference is explained in todays extra info).
For this exercise, we will make a simple script to create an output null for any node we have selected. To start, we can work in our TDPUtils.py file, and create a new function. I will call it create_output_null. It's good to be as verbose as possible with naming, as things can get complex fast, and being vague doesn't help anybody.
import hou
def greet(first_time=True):
if first_time:
print("Hey there!")
else:
print("Hello again!")
def create_output_null():
pass
We left pass there, which essentially means "do nothing". Now that we have this ready, we can quickly make a new shelf tool, and make it call our function:
import TDPUtils
TDUtils.create_output_null()
From this point on, the creating of shelf tools won't be explained again, so save this area if its something you need to come back to.
Now, we can go back to our script, let's walk through creating our function.
import hou
def create_output_null():
node = hou.selectedNodes()[0]
print(type(node))
Here we use [0] to get the first selected node (more on this later, don't worry). Here, we print type(node). This will give us the type of the data stored in the variable. So now that we have our node, and the print statement has confirmed our node type to be hou.SopNode (Just a variation of hou.OpNode) it's a good time to go and read the docs page for hou.OpNode and see what functions are available to us.
We will be using .createOutputNode(), which has the required arguments listed. Remember how we create optional arguments in yesterday's code? Take note of which arguments are optional in this case. The only required one, is node_type_name. In our case, we want to create a null, so create a null manually, and click the shelf tool "Node Info". This will print the node info for us to use, specifically "Type Name: null", so we can build our script now:
import hou
def create_output_null():
node = hou.selectedNodes()[0]
node.createOutputNode("null")
Now, if we select a node and run this, it will create an null, and connect it to the output of our node. This works well for a single node, which we have selected with our [0] bit, but now the next step is to do it for all of our nodes.
Let's first adjust our script and run it:
import hou
def create_output_null():
nodes = hou.selectedNodes() # We get our selected items
print(type(nodes))
It is crucial to know that what we get back here is: <class 'tuple'>. More reading on this in the extra info, as well as here. The bottom line is that this is a list of nodes, and in python, we can loop through a list/tuple very easily with by writing for item in list_of_items. Let's modify our script to run through each item:
import hou
def create_output_null():
nodes = hou.selectedNodes() # We get our selected items
for node in nodes:
node.createOutputNode("null")
Now, when we select a bunch of nodes, and run the script, we get a null for each node. Knowing that inside of the loop we are operating on each node one by one, we can go a step further and set the name of the null to the name of our selected node:
import hou
def create_output_null():
nodes = hou.selectedNodes() # We get our selected items
for node in nodes:
new_name = "OUT_" + node.name()
output = node.createOutputNode("null", node_name=new_name)
We pass node_name explicitly here, by writing node_name=new_name, but technically as its the second argument, we can just write node.createOutputNode("null", new_name), but it is good practice to keep track of what arguments you are passing to the functions
Here, we create a new_name variable to store the name, as well as store the null as output. You will use this if you do the prompt for today.
Now, we have a clean null named "Out_" for each of our nodes. Congratulations, you have connected some nodes! Bind this shelf tool to a hotkey and you have a useful workflow enhancer.
Some more info on creating nodes: createOutputNode is only one way to make a node, there are many more hidden in the docs. node.createNode() is a bread an butter function for creating a node, but it doesn't handle making connections for us like we had before. As always, the docs are contain a wealth of information, but I will outline a quick way to create a node and connect it to our selected node:
import hou
def create_output_null():
node = hou.selectedNodes()[0] # we are only getting the first node
new_name = "OUT_" + node.name()
A really important thing here, is that when you create a node, it exists inside its parent. For example. a simple Box node may have a path like /obj/geo1/box1. The node's parent is geo1, and we use this to create the node. An analogy: In a book you have page and on the page you have a word. Logically, if life were Python, you would call page.createWord() rather than word.createWord() as words don't "contain" other words.
import hou
def create_output_null():
node = hou.selectedNodes()[0] # we are only getting the first node
new_name = "OUT_" + node.name()
parent = node.parent() # get the parent node
null = parent.createNode("null", node_name=new_name)
Now, the node is created, but it is not connected. There are helper functions for setting the first input, but for this example, I will use the long winded approach to explain it:
import hou
def create_output_null():
node = hou.selectedNodes()[0] # we are only getting the first node
new_name = "OUT_" + node.name()
parent = node.parent() # get the parent node
null = parent.createNode("null", node_name=new_name)
null.setInput(0, node)
Here, we call null.setInput, which takes the number of the input on the node, as well as the node to connect to it.
Since Houdini 20.0, hou.Node has been further split up into sub-classes. This is due to the fact that different options may be available depending on whether a node is a sop node, obj node, etc. Most of the stuff exists on hou.Node, but if you can't find info you're looking for, it is worth checking out the specific node page for the node you are working on.
For the case above, the only real difference between the list and the tuple is that you won't be able to directly modify the tuple, and should rather recreate it as a list (with list()) if you need to mess with it.
If you did the prompt for Day 2, you may have found node.moveToGoodPosition(), here's what I used:
def create_output_null():
nodes = hou.selectedNodes() # We get our selected items
for node in nodes:
new_name = "OUT_" + node.name()
output = node.createOutputNode("null", node_name=new_name)
output.moveToGoodPosition()
We know how to create a script and run it in Houdini, how to use arguments in our functions, how to loop through multiple nodes. We know how to create nodes, how to connect them to other nodes.
If we assume we have our node stored as node in a script, we can get a parameter by doing node.parm("parm_name"). Here is the docs page for node.parm() and if you click on it, you will see "-> hou.Parm". This shows what the function returns, and its worth clicking through to hou.Parm and seeing all the useful stuff there is to learn.
Create a Transform Sop and select it. A fundamental thing to understand with getting a parameter, is that we get the hou.Parm back, and from there, we can get its value, get its value, etc.
Create a function in TDPUtils.py called get_parms and make a shelf tool that calls it.
Now, let's loop through all selected nodes (this is a good thing to use, even if you only need one node, as it allows you to adapt it to work for multiple nodes if needed). A node like Transform that has a vector parameter, with multiple values, is still technically 3 parameters under the hood. Mousing over the label will reveal: Parameters: tx ty tz. In this case, let's just work on tx
import hou
def get_parms():
for node in hou.selectedNodes():
our_parm = node.parm("tx")
print(our_parm)
If we run the script with the transform selected, it will print: <hou.Parm tx in /obj/geo1/transform2>. As we know, this is a hou.Parm, and if we want to get the value, we can change our script:
import hou
def get_parms():
for node in hou.selectedNodes():
our_parm = node.parm("tx")
value = our_parm.eval()
print(value)
We use eval() to evaluate the parameter. This will read the value at the current frame, and evaluate any expressions we have set for the parameter ($FF for example).
We can also set a parameter just as easily:
import hou
def get_parms():
for node in hou.selectedNodes():
our_parm = node.parm("tx")
value = our_parm.eval()
our_parm.set(value*2)
In this case, our_parm is just a reference to node.parm("tx"), so we can call the .set function on it, to set the parameter to whatever we pass as the argument.
Now that we have set a parameter, we also set the expression, just as we could do when typing in the parameter box. Let's take the tx parameter, and set it to ty and multiply it by 2.
In this case, we don't want to set it to the value of ty, but specifically set it as a reference to ty. There are other hou.Parm methods we can use, but for the sake of learning, we will be doing it manually.
We will need to create 2 variables, one being our tx parameter, the other being our tx parameter:
import hou
def get_parms():
for node in hou.selectedNodes():
tx = node.parm("tx")
ty = node.parm("ty")
Now, if we think about how this would be written manually in the tx box, it would be: ch("ty")*2 (This is a crude example as the paths are on the same node, but we will expand on it later).
If we read the docs page for parm.setExpression, we can see the only required argument is a string that is the expression, and we will build that expression in our script. What we need is the path of the parameter.
import hou
def get_parms():
for node in hou.selectedNodes():
tx = node.parm("tx")
ty = node.parm("ty")
ty_path = ty.path()
print(ty_path)
In my case, I get /obj/geo1/transform1/ty which is the absolute path of the parameter. For now, we will use this to construct our reference (some fun in Extra Info). Let's add to our script to build the reference:
import hou
def get_parms():
for node in hou.selectedNodes():
tx = node.parm("tx")
ty = node.parm("ty")
ty_path = ty.path()
ref_string = 'ch("' + ty_pat3 + '") * 2'
print(ref_string)
If we run this, it prints ch("/obj/geo1/transform1/ty") * 2, which is just what we need. The last step is to set the expression on the parm:
import hou
def get_parms():
for node in hou.selectedNodes():
tx = node.parm("tx")
ty = node.parm("ty")
ty_path = ty.path()
ref_string = 'ch("' + ty_pat3 + '") * 2'
tx.setExpression(ref_string)
Congratulations, you have set a parameter expression!
hou.Parm has some really useful methods, such as parm.node() which returns the path of the node the parameter exists on. We can use this to run a script using the parameter we right click on, or we can use this to change our absolute path above to a relative one.
Here is the same script, but we use node.relativePathTo() to get the path required, and we construct the string using f-strings:
import hou
def get_parms():
for node in hou.selectedNodes():
tx = node.parm("tx")
ty = node.parm("ty")
start_node = tx.node()
start_name = tx.name()
end_node = ty.node()
end_name = ty.name()
rel_path = start_node.relativePathTo(end_node)
ref_string = f'ch("{rel_path}/{end_name}") * 2'
tx.setExpression(ref_string)