If you're working on a Houdini pipeline you'll find that you often need to find all the nodes of a particular type in the scene. For example, this might be because your CG assets always get loaded using the same type of node.
Let's say that you want to try to find all the File SOPs. In Python, you can easily do that with a single line:
hou.sopNodeTypeCategory().nodeType(“file”).instances()
If you’re not sure what’s going on here, don’t worry… I’ll go into details in a minute.
This article gives an introduction to navigating your way around Houdini’s node type system. The concepts are pretty simple, but a good understanding of them is essential if you’re working with Python in Houdini.
Scopes, Namespaces, and Versions
In most cases, the one-line code shown above works fine, but it doesn’t handle the general case. Node type names can actually be made up of four components. Only the <name> component is required, all others are optional:
<scope>::<namespace>::<name>::<version>
(See here for more detailed information: Houdini Documentation)
To retrieve the components of an HDA’s name, you could write your own method, but Houdini already has its own routine:
hou.hda.componentsFromFullNodeTypeName(node_type_name)
This function will always return a tuple of 4 strings. The components are always in the order of scope, namespace, name, and version. If a string component is empty, then that component was not in the name. For example, if we have a node called:
my_studio::asset_loader::2.0
...then the result of calling componentsFromFullNodeTypeName() will give us:
("", "my_studio", "asset_loader", "2.0")
This gives us the basis of a routine that we could write to find all the nodes, given just the name component.
Note that the hou.NodeType object has a method called nameComponents() which does exactly the same thing.
Node Type Categories
First, let’s just quickly talk about node type categories. When we talk about node type categories, we’re referring to SOPs, ROPs, COPs, etc. In Python, we can access them using the hou.nodeTypeCategories() function which returns a dictionary object.
In this dictionary, the keys are the names of the categories, while the values are the corresponding NodeTypeCategory Python objects. Like this:
>>> print hou.nodeTypeCategories().items()
[('Shop', <hou.NodeTypeCategory for Shop>),
('Cop2', <hou.NodeTypeCategory for Cop2>),
('CopNet', <hou.NodeTypeCategory for CopNet>),
('ChopNet', <hou.NodeTypeCategory for ChopNet>),
('Object', <hou.NodeTypeCategory for Object>),
('Driver', <hou.NodeTypeCategory for Driver>),
('Manager', <hou.NodeTypeCategory for Manager>),
...
We can also access specific node type categories directly by using the appropriate function, e.g.
>>> hou.sopNodeTypeCategory()
<hou.NodeTypeCategory for Sop>
>>> hou.ropNodeTypeCategory()
<hou.NodeTypeCategory for Driver>
It can get a bit confusing with these objects. The way they’re presented, they almost appear as different class types, but actually they’re all of the same Python class type “NodeTypeCategory”. The same class is able to represent different node categories, SOP, ROP, etc. and it has methods to give you access to the node types available.
We can ask each NodeTypeCategory for a list of node types that are available. This is basically what we did earlier:
file_node_type = hou.sopNodeTypeCategory().nodeType(“file”)
Here we're asking the SOP node category for the node type called “file”. It’s worth noting that the node type we’re being given here is exactly the same object we get when we ask a particular node for it’s type like this:
file_node_type = hou.node(“/obj/geo1/file1”).type()
Once we have that node type, we ask for all the instances of it in the Houdini scene. It will return us a list of “file” nodes found.
file_node_type.instances()
Getting All Node Types In A Category
We’ve seen how to ask for a specific node type from a node category. How do we access the full list of types? Well, the NodeTypeCategory class gives us the nodeTypes() method to do just that. It returns a dictionary. The dictionary keys are the node type names, and the values are the node type objects. This means we can easily loop over all the nodes in the category:
for type_name, node_type in category.nodeTypes().iteritems():
# Do stuff...
Checking The HDA Name
The loop in the previous section will be the basis of our function to find all nodes with the given name. With each value for “type_name", we can check the 3rd component of the HDA name (indexed as 2 because it’s zero based):
node_name == hou.hda.componentsFromFullNodeTypeName(type_name)[2]
and from the “node_type” variable, we can then get a list of node instances for that type.
That’s all there is to it! We can now look at writing the final function.
Simplifying The Inputs
Usually, we’ll be looking for a node type under a specific node type category, e.g. SOPs, so we’ll have that as an input parameter. The type could be a little ambiguous though. It could be supplied as a string or as the actual node category type object. If it’s a string, it would be useful not to have to worry too much about getting the right case. So we’ll make a helper function to convert a string to a valid NodeTypeCategory:
def str_to_category(category):
category = category.title()
if category.endswith("net"):
category = category[:-3] + "Net"
return hou.nodeTypeCategories().get(category)
We can pass this “vopnet”, or “VopNET” and it’ll still give us the vopNetNodeTypeCategory object. Note that the context for ROPs is given as “Driver”. It’s a bit inconsistent and something about it feels a little "Windows 95” in the naming, but hey, it’s no big deal.
Putting It All Together
With a bit of error checking and exception reporting, this is the final code:
import hou
def get_node_instances(category, node_name):
if isinstance(category, basestring):
category = str_to_category(category)
if not isinstance(category, hou.NodeTypeCategory):
raise TypeError("Not a valid NodeTypeCategory")
result = []
for type_name, node_type in category.nodeTypes().iteritems():
name_part = \
hou.hda.componentsFromFullNodeTypeName(type_name)[2]
if node_name == name_part:
result.extend(node_type.instances())
return result
def str_to_category(category):
category = category.title()
if category.endswith("net"):
category = category[:-3] + "Net"
return hou.nodeTypeCategories().get(category)
Which you can call it in either of these ways:
>>> get_node_instances("sop", "file")
[<hou.SopNode of type file at /obj/geo1/file1>,
<hou.SopNode of type file at /obj/dopnet1/sopgeo1/file1>]
or
>>> get_node_instances(hou.sopNodeTypeCategory(), "file")
[<hou.SopNode of type file at /obj/geo1/file1>,
<hou.SopNode of type file at /obj/dopnet1/sopgeo1/file1>]
It's worth keeping this code in a easy-to-import location, as I'm sure you'll end up using it a lot.
One last thing...
If you're concerned about the performance aspects of repeatedly calling this function, you might want to consider using a similarly written function as the basis of creating a cache in memory. I'd suggest a using a dictionary with node type names as the keys, and the values being a list of strings of full node type names that are currently installed. It's better to use the node type name rather than the OTL Definition object, in case it is ever uninstalled.
Of course, the problem with caches is knowing when to invalidate them, which can be a tricky problem to solve.
the def get_node_instances won't work in py3 right?