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.

The Color SOP updating its node color from the picked color, in real-time! (Sorry about the compression)

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:

The circled option is the callback script for the “Quick Add Objects” button.

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:

The OnCreated event script for the MOPs Instancer. It’s just setting the name to something easy to read (and lowercase so your OUT node stays on top!).

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 argumentparm_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.

Dita sporting a fresh new haircut.

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:

Check out our fancy new version slider!

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.


1 Comment

Al · 08/12/2023 at 18:28

Cool cat!

Leave a Reply

Your email address will not be published. Required fields are marked *