Casey Hupke asked an interesting question today: “how can I get a Color SOP to automatically update its node color to match the picked color?” This reminded me of some other pipeline work I’d done in the past to customize existing nodes in Houdini; for example, adding a few spare parms to the File Cache SOP or similar nodes to enable better default naming conventions and version control.
If you’ve only dabbled in writing your own tools for Houdini, your first instinct when trying to solve a problem that an existing node doesn’t quite solve would be to write your own HDA, but that opens up a new can of worms: now you have a new node dependency in your scene that needs to either exist at your whole facility, or be embedded into the file if you need to share it for any reason. In a case like Casey’s, you really don’t need an entirely new tool that just wraps around a Color SOP… you just need a very slight tweak to the Color SOP that automates a node property. If you share this file with someone else who doesn’t have your custom configuration, it’s still just a plain old Color SOP to them!
Parameter Callbacks
Normally in Houdini, if you’re writing your own digital asset it’s pretty easy to get a custom script to fire when a parameter on your asset is modified. From the Type Properties window, you can select any parameter and look for the Callback Script:
This script will fire anytime the parameter is modified by the user (meaning, not by some other automated process). If you’re not familiar with parameter callbacks using Python, let’s dissect this line:
hou.phm().do_quick_select(kwargs)
The function hou.phm()
means “this node definition’s Python module”. An HDA always has an included Python Module where you can store custom scripts related to that node. In this case, when this parameter is modified (it’s a button, so whenever the button is pushed) I want to find the function called do_quick_select
in the Python Module, and run it with the mysterious kwargs
argument.
WTF is kwargs?
The kwargs
argument shows up a lot in Houdini callbacks of all kinds, and it’s a little weird but very important to understand. When a callback function fires, it needs to be potentially aware of a lot of different things about the event that just happened: what node was modified, what parameter was modified, and so on. Each event is different and potentially carries different arguments across. Rather than require your Python functions to have individually named arguments for all of these possible values, it just stuffs all of them into a single Python dictionary full of these keyword arguments, or kwargs
. So if you need to know what node was modified, kwargs['node']
is the hou.Node object. If you need to know what parameter specifically changed, you have kwargs['parm']
. A full list of these keyword arguments for parameter callbacks specifically is available here. Note that there are other kinds of callbacks aside from parameter callbacks that will have different kwargs
, such as Python tool states.
In the case of do_quick_select(kwargs)
shown above, the code stored in the HDA’s Python Module looks a bit like this:
def do_quick_select(kwargs):
""" Fires when Quick Add parm is modified. Add all object paths to the multiparm list. """
me = kwargs['node']
paths = me.evalParm('quick_add').split()
me.parm('quick_add').set("")
if paths:
for i in paths:
index = me.parm('instanceobjects').evalAsInt()
# get path relative to instancer
relpath = me.relativePathTo(hou.node(i))
me.parm('instanceobjects').insertMultiParmInstance(index)
me.parm('instancepath' + str(index + 1)).set(relpath)
It’s not terribly important what this function is actually doing (it’s grabbing all your selected objects and stuffing them into the Instancer), but pay attention to how the function is defined: a single argument kwargs
, and then the hou.Node object representing the Instancer that owns the button is quickly fetched from kwargs['node']
and bound to me
for convenience.
Anyways, if we were writing a custom HDA for our modified Color SOP, we could just write one of these parameter callbacks on the Color parameter and be done with it. But if we don’t want to write a custom HDA just for this functionality, what needs to happen?
File-Based Digital Asset Event Handlers
If you’ve ever made your own HDAs in Houdini, you might have seen or played with the various event handlers that are available to HDAs. These event handlers reflect various things that can happen to HDAs during use: “On Created”, “On Input Changed”, etc. For example, here’s the “On Created” event script that fires inside the MOPs Instancer:
Most often you find these event scripts buried inside the Type Properties window, but this isn’t the only place you can put them! It’s possible to place these event scripts as files on disk that Houdini will automatically recognize if they’re in the correct location. Check this easy-to-miss description from the documentation:
Files matching the pattern
HOUDINIPATH/scripts/category/nodename_event.py
(for example,$HOUDINI_USER_PREF_DIR/scripts/obj/geo_OnCreated.py
) will run when the given event type occurs to a node of the given type.
What this means is that for the Color SOP, for example, we can create a Python file at the location $HOUDINI_PATH/scripts/sop/color_OnCreated.py
and that script will run automatically anytime a Color SOP is created. No need to write a wrapper, we can just start making modifications from here!
By the way, if you don’t know what the “programmatic” name of a SOP is for the purposes of these scripts, just check the Type Properties window and look at the very top. The name of the node will be just to the right of “Operator Type:” in bold. The Color SOP is mercifully just named “color”. Remember that this is case-sensitive, including the event name!
Now we have a possible hook into modifying this SOP for our own purposes, without creating any new nodes or whatever. There’s a bit more work to do, though, to create the equivalent of a parameter callback without making a new HDA.
Node Event Callbacks
Any node in Houdini can be instructed to fire what’s called a “callback” when certain properties of the node are changed. These changes are called events: for example, when an input is changed, when an upstream node cooks, or when a parameter is changed. A full list of these node-based events is here. These callbacks can be added via the HOM function hou.Node.addEventCallback()
. The documentation for this function is here. Note one very important line here:
Callbacks only persist for the current session. For example, they are not saved to the
.hip
file
This means that we’ll need to apply our callback both when a Color SOP is created (the OnCreated
event), and when it’s loaded from an existing file (the OnLoaded
event). For the purposes of this example, both of these events are going to effectively be the same code. So let’s see some code:
import hou
import traceback
def color_changed(node, event_type, **kwargs):
parm_tuple = kwargs['parm_tuple']
if parm_tuple is not None:
# print(parm_tuple.name())
if parm_tuple.name() == "color":
# the color parm was just modified
color = parm_tuple.eval()
hcolor = hou.Color(color)
try:
node.setColor(hcolor)
except:
# the node is probably locked. just ignore it.
pass
try:
me = kwargs['node']
if me is not None:
# print("creating callback")
me.addEventCallback((hou.nodeEventType.ParmTupleChanged, ), color_changed)
except:
print(traceback.format_exc())
There’s a little bit to break down here. First of all, check the formatting of the color_changed
callback itself. The arguments are node
, event_type
, and **kwargs
. All node event callback functions require the first two arguments, node
and event_type
. The third argument, **kwargs
, represents the kwargs
dictionary with all those handy values we might want.
As an aside: the asterisks in front of **kwargs
are something in Python called an unpacking operator. It more or less takes a list of positional arguments, like pee=poo
and butt=fart
, and turns them into a dictionary. It’s not terribly important to remember exactly why this is, but just remember to format your callbacks like this.
Second, look towards the bottom there for addEventCallback()
. This method of hou.Node
has two arguments: a tuple of hou.nodeEventType
names, and a callback function name. In this case, the only event that we want to trigger our callback is hou.nodeEventType.ParmTupleChanged
, which fires whenever a parameter is modified. Again, in the documentation for hou.NodeEventType
, check out the description of ParmTupleChanged
:
Runs after a parameter value changes. You can get the new value using hou.ParmTuple.eval().
Extra keyword argument:
parm_tuple
(hou.ParmTuple).
That parm_tuple
keyword argument is what’s going to get passed along to **kwargs
in our color_changed
callback function! That’s how we know exactly which parameter was just changed. Now let’s scan the code again… at the bottom, we’re adding an event callback to our node that fires whenever a parameter is changed, and that callback’s name is color_changed. At the top, our color_changed
function gets the parm_tuple
keyword argument and makes sure it’s valid (is not None), and then checks the name of the parameter. If the parameter’s name is “color”, which is the actual name of the “Color” parameter on the Color SOP, then we evaluate that parameter and convert it to a hou.Color
object, then set the node’s color to that hou.Color
. That’s the whole thing!
Save that entire script to $HOUDINI_PATH/scripts/sop/color_OnCreated.py
and $HOUDINI_PATH/scripts/sop/color_OnLoaded.py
and restart Houdini, and you should see the node color update as you pick new colors. Amazing!
Here’s a cat
Too much text at once. Here’s a cat.
Adding spare parms
Here’s another example. Let’s say you want the ROP Alembic Output SOP to have a version control parameter, similar to the updated File Cache SOP. You could again write a wrapper HDA, but you could also just add a few spare parameters to the ROP Alembic Output SOP and automate this whole thing without adding extra dependencies to your files. Here’s how this could look:
import hou
import traceback
try:
# get the node that was just created
me = kwargs['node']
# create "version" spare parameter.
# first get the ParmTemplateGroup of the HDA. this is the overall parameter layout.
parm_group = me.parmTemplateGroup()
# now create a new ParmTemplate for the "version" spare parm. This is an integer slider.
version_template = hou.IntParmTemplate(name="version", label="Version", num_components=1, min=1, default_value=(1, 1, 1))
# we'll put this new spare parm right before the "frame range" parameter, named "trange".
range_template = parm_group.find("trange")
parm_group.insertBefore(range_template, version_template)
# now we need to write this modified ParmTemplateGroup back to the individual node's ParmTemplateGroup.
# this is effectively how you add a spare parm to a node without modifying the HDA itself.
me.setParmTemplateGroup(parm_group)
# finally, change the default output path so that it uses this version number (with 3-digit padding).
me.parm("filename").set('$HIP/output_`padzero(3, ch("version"))`.abc')
except:
# just in case a ROP Alembic Output SOP is created as a locked node by something else. don't need to see errors.
pass
This is a lot of code, but basically this is adding a spare parameter to any newly created ROP Alembic Output SOP named “version” and then setting the default output path to an expression that references this new version control. In HOM terms, you’re reading the node’s ParmTemplateGroup
, creating a new IntParmTemplate
(a spare parameter definition), inserting it into the ParmTemplateGroup
we read earlier, and then writing this new ParmTemplateGroup
definition back onto the node because you can’t modify them in-place.
Save this to $HOUDINI_PATH/scripts/sop/rop_alembic_OnCreated.py
and your newly-created ROP Alembic Output SOPs will look like this:
I hope this helps open up some new doors for your own personal pipeline (or your studio’s pipeline!). Avoiding introducing new node dependencies just for minor tweaks to existing nodes can really help keep your pipeline lean and make it easier to share files in the future.
6 Comments
Al · 08/12/2023 at 18:28
Cool cat!
gottesgerl · 03/05/2024 at 03:29
….or you put the node color in the driver`s seat with “hou.pwd().color().rgb()[0]..[1]..[2]” in the nodes color parm. Free update issues included. ^^ (then switching on/off any toggle helps tremendously if in a hurry)
Philippe · 06/16/2024 at 05:49
Hey,
thanks for the post, very useful tips !
I’m trying to do this with node definitions that use ‘::’ such as ‘principledshader::2.0’. the ‘:’ are unauthorized for filenames and therefore can’t create ‘principledshader::2.0_OnCreated.py’
Any ideas for a workaround to this ? ^^
toadstorm · 07/08/2024 at 13:57
Hi Philippe,
Sorry for the late reply, my blog can’t seem to email me.
See here for documentation on event script files for nodes with special characters: https://www.sidefx.com/docs/houdini/hom/locations.html#node_event_files
Philippe · 06/16/2024 at 11:31
Hey,
This post is awesome, thank you!
I was wondering if there is something similar that can be applied for node definitions that have this pattern “XXX::2.0” as “:” is an unauthorized character for file names. I tried a few things without any luck, any hints ?
Clayton · 07/16/2024 at 09:17
Very cool explanation (and cat). Thank you for the write-up.