February 12, 2023No Comments

HDA Action Button Scripts

Action Button scripts can help you inject some interactivity into your HDAs inside Houdini, or generally make operations faster or simpler. You've likely used them in many nodes built into Houdini, but if you're not too familiar with building tools or working with python you've probably never tried modifying them or building your own. On this page I've added (and will update over time) the action button scripts that I have made (or stole from nodes in Houdini) and use in a lot of tools of my own.

Through the page, you'll be able to find "Action Help" and "Action Icon" text which goes into the fields below where you paste the script. These provide the tooltip and icon you see, respectively.


Group Selection

The oh-so-common group select. If you've ever selected a group from the viewport, this is the script you've used. However, I've added a bit so you can customize it more readily without needing to use the SideFX HOU reference to look up the different commands you see.

import soputils

kwargs['geometrytype'] = kwargs['node'].parmTuple('NAMEOFGROUPTYPEPARM')
# Alternatively, the above line can be
# kwargs['geometrytype'] = hou.geometryType.GEOMETRYTYPEHERE(Points, Edges, Primitives, Vertices)
kwargs['node'] = kwargs['node'].node('NAMEOFNODETOSELECTFROM')
# If you want to select from the input of the current node, you can remove the above line
kwargs['inputindex'] = 0
# kwargs['shift'] = 1 # Will force selection using groups if enabled

soputils.selectGroupParm(kwargs)

Action Help:
Select geometry from an available viewport.
Action Icon:
BUTTONS_reselect


Attribute Visualization

Generally, being able to visualize attributes can be incredibly useful. There are different ways to go about it, a few are outlined below, but a rule of thumb is that you generally want to add this kind of action button to string parameters being used to select attributes from (this goes very well with attribute selection menu scripts). What if you don't want to attach this action button to such a string parameter? Well, that is also acceptable but you need to change kwargs['attribname'] to be equal to either another parameter which IS a string for an attribute, or simply hardcode it. For example:
kwargs['attribname'] = "variant"
would make it so this action button is essentially a toggle for the attribute "variant", and I believe it would figure out the attribute class automatically. All of the Attribute Visualization scripts use the same Action Help and Icon.

Visualize Color - Attribute-As-Is

Good for vector attributes mostly. There is no remapping of color here, so a vector of (1,0,0) will be red and a float will be greyscale.

import soputils

soputils.actionToggleVisualizer(kwargs,
{ 'type': hou.viewportVisualizers.type('vis_color'),
'parms': {
    'colortype': 0
} })

Visualize Color - Remap From White to Red

Great for float attributes, not so good for vector attributes. I believe I stole this from the heightfield masking nodes.

import soputils

soputils.actionToggleVisualizer(kwargs,
{ 'type': hou.viewportVisualizers.type('vis_color'),
'parms': {
    'colortype': 'attribramped',
    'rangespec': 'min-max',
    'minscalar': 0,
    'maxscalar': 1,
    'treatasscalar': True,
    'using': 'compabs',
    'component': 0,
    'colorramp': hou.Ramp((hou.rampBasis.Linear,
                           hou.rampBasis.Linear),
                           (0, 1),
                           ((1, 1, 1), (1, 0, 0)))
} })

Complex Example: Visualize Remapped Color for Dynamically Generated (and Deleted) Attributes

In a recent tool I created controls in the UI to be able to modify masks (attributes) that were then used within it, but deleted before being output with the geometry. So first there is a wrangle inside which is doing all the creation/modification of these masks. Then operations using the masks came afterwards. I chose to create a switch that would output the unmodified geometry with the masks on it so we could visualize it. Otherwise, there was no way to see precisely what the mask looked like which isn't very artist-friendly.

To make matters more complex, the tool's operation actually works in layers and relies on a multiparm setup. The action button is within this layer setup and shows a different mask on every layer (depending on how said layer mask is defined). So in my UI, I have:

  • A toggle that each action button checks and switches to say "we're in visualizing mode" and that redirects the geometry to show the masked version instead.
  • A hidden int parameter set to the layer being visualized (used for nodes inside to show the right attribute).
  • And a check for if anything is currently being visualized to brute-force turn that off before proceeding (had issues otherwise)

So this isn't exactly something to follow as a reference but feel free to study it or modify and use it if you see it being beneficial.

import soputils
import toolutils

# set attrib to visualize and create visualizer
kwargs['attribname'] = f"__mask{kwargs['script_multiparm_index']}"
hda = kwargs["node"]
layer_num = hda.parm('vis_layer')
vis_tog = hda.parm('tog_vis')
soputils.turnOffVisualizers(
    hou.viewportVisualizers.type('vis_color'), 
    hou.viewportVisualizerCategory.Scene, 
    None, 
    toolutils.sceneViewer().curViewport())
if str(layer_num.eval()) == str(kwargs['script_multiparm_index']) and vis_tog.eval() == 1:
    layer_num.set(0)
    vis_tog.set(0)
    
else:
    layer_num.set(kwargs['script_multiparm_index'])
    vis_tog.set(1)
    soputils.actionToggleVisualizer(kwargs,
        { 'type': hou.viewportVisualizers.type('vis_color'),
        'parms': {
            'colortype': 'attribramped',
            'rangespec': 'min-max',
            'minscalar': 0,
            'maxscalar': 1,
            'treatasscalar': True,
            'using': 'compabs',
            'component': 0,
            'colorramp': hou.Ramp((hou.rampBasis.Linear,
                                   hou.rampBasis.Linear),
                                   (0, 1),
                                   ((1, 1, 1), (1, 0, 0)))
        } })

Action Help:
Toggle visualization. Ctrl-LMB: Open the visualization editor
Action Icon:
VIEW_visualization


Set Vector Parameters (Directions)

Vector direction parameters can be tricky. Sure, you can do a kind of "dumb" set by dragging numbers around, but what if there were better ways? Below I've outlined a few fairly useful action buttons I regularly add to tools to make setting direction vectors a much faster, more intuitive process.

Note: I often use multiple action buttons for any given parm. In the Action Button Tricks there is a solution for how to do this.

Simple Vector

X, Y, Z.. these are fairly straightforward vectors to enter, but I like having buttons for each vs having to enter numbers because it's a bit faster for the user. Click the action button twice and it switches to a second vector. Theoretically, you could create a list of vectors for 1 button to cycle through as well... may have to add that code later.

# Set parm to specified vector.
node = kwargs['node']
num = kwargs['script_multiparm_index']
vectorParm = node.parmTuple(f"VectorParmToSet{num}")

targetDir = (1,0,0)
if vectorParm.eval() == targetDir:
    targetDir = (-1,0,0)
for num in range(3):
    vectorParm[num].set(targetDir[num])

Action Help:
Sets the direction to be the specified vector. Press again to switch to the negative vector.
Action Button:
Needs a custom icon for the specified vector. I usually use X, Y, or Z as the icon but Houdini doesn't have those built-in believe it or not.


Action Button Tricks

Setting up Multiple Action Buttons for 1 Parameter

I often use multiple action buttons (for vector parms) in particular, but that's not very straightforward. You can do the same by doing the following:

  1. First, set an action button on the target parameter and enable the setting "Horizontally Join to Next Parameter".
  2. For each additional action button you want, add a "Data" parameter. Normally I believe these are meant for storing some kind of temporary cached data in the HDA itself but I've never used them for that purpose. Instead, disable the label, enable "Horizontally Join to Next Parameter" (unless it's the last one in the list), and set the action button on this parm as well. It should join up right next to the one that you just set.

If done correctly, you should have a parameter that looks something like this.


Finding Existing Icons to Add to Action Buttons

BUTTON_reselect? VIEW_visualization? What the heck ARE these? Well, Houdini has 1000s of icons built-in. You can find them if you want. On Windows, they exist in C:\Program Files\Side Effects Software\Houdini XX.X.XXX\houdini\config\Icons\icons.zip.

But don't do that.

There are a couple of different Icon Browser panels/tools that have been created and released online. This one by Timothée Maron is free. They will have instructions on how to install them. It's pretty straightforward, and then you'll have a panel directly in Houdini that you can use to find icons that come with Houdini itself. These icons don't have what you'd normally recognize as a path or url to their location which is odd but convenient.

What if you create your own icons to add to a tool???

The icons you see in the above parm were made that way, and that is also not very hard... there just isn't really any documentation on it (so here you go).

Storing on-disk

This is my favourite way to access icons I've created. However, it can be a touch temperamental if you don't have your env variables set up with a proper location to store your tools. I will write an article on that soon, but in the meantime, I'd recommend creating an "icons" folder in your C:\Users\XXXXX\Documents\houdiniXX.X directory.

Create your icons in your program of choice, I personally use Affinity Designer but Adobe Illustrator would be another popular one. You ideally want to export your icons as .svg files which are vectors and can scale without issue. Don't make them too detailed, remember the scale they will be when you're seeing them on screen. I usually purposely make my canvas size something like 64x64 pixels so I can have it close to the scale you'll see in Houdini.

Once exported, assuming you put them in the above folder, you can access the icons using the path:
$HOUDINI_USER_PREF_DIR/icons/nameOfIcon.svg
in your action button icon path. If you define your own env variables (ideally using a packages method, will be documenting) then you can have a path that looks more like:
$JTOOLS/icons/nameOfIcon.svg
This is ideal because if your actual tool is stored at $JTOOLS/otls/nameOfTool.hda, then the icons will automatically load as long as your tool does, so you can be confident that the icons will always exist alongside the tools.

Storing in the HDA

It is also possible to store icons directly inside the "Extra Files" of your HDA. It is recommended that as you add files, keep the filename in the section name (what gets created when you add files) so that Houdini can tell what kind of file is located there. The big plus of this approach is that the icons are guaranteed to be stored with that tool, but that is less convenient if you want to use the same icon with multiple tools. However, as I've never actually gotten this method to work, all I can do is refer you to this page to try it for yourself.

February 12, 2023No Comments

HDA Menu Scripts

A list of different menu scripts which make HDAs easier to use in Houdini. Note that you need to change the Menu Script Language to Python vs HScript for all of these. Updated as I create them.


Group Selection

Fairly straightforward. Generates a menu with the existing groups of a particular type. There are a couple variations of the code depending on if you want to use a different parameter to control the type being selected (which is how the group node works).

Simple Group Selection (Primitive groups)

The only modification needed is to potentially change hou.geometryType.Primitive to one of these other variations depending what you want to select.

node = kwargs["node"]
menu = []

# change group_types to equal whatever kind of geometry you want
if node:
    menu = node.geometry().generateGroupMenu(group_types=hou.geometryType.Primitives)

return menu

Full Group Selection

Note that you'll need to modify and remove lines to make this work, but designed to work the same way as a group node does in terms of selection. So it wants a "group" string parameter which is likely what this is connected to, as well as a "grouptype" string parameter with menu options for what kind of menu you want to generate.

node = kwargs["node"]
type = node.parmTuple("grouptype")[0]
# Alternatively, the above line can be...
# kwargs['geometrytype'] = hou.geometryType.GEOMETRYTYPEHERE(Points, Edges, Primitives, Vertices)
geo = node.geometry()
menu = []

# Select the groups to work with based on the selected group type
# The expectation is that "type" is coming from a SOP group node, the grouptype parm.
if node:
    if type == "vertices":
        menu = geo.generateGroupMenu(group_types=hou.geometryType.Vertices)
    elif type == "points":
        menu = geo.generateGroupMenu(group_types=hou.geometryType.Points)
    elif type == "edges":
        menu = geo.generateGroupMenu(group_types=hou.geometryType.Edges)
    elif type == "prims":
        menu = geo.generateGroupMenu(group_types=hou.geometryType.Primitives)
    else:
        menu = geo.generateGroupMenu()

return menu

Attribute Selection

Simple Attribute Selection (Point Attributes)

The only modification needed is to select the type of attributes to find. Options listed here.

node = kwargs['node']
input = node.input(0)

if input:
    geo = input.geometry()
    menu = geo.generateAttribMenu(attrib_type=hou.attribType.Point)
    return menu
    
else:
    return ["", "No Input Connected"]

Full Attribute Selection

By default, the below script will select Point, Prim, and Vertex Attributes. The datatypes (options listed here) vary, as do the min and max sizes in order to demonstrate different ways to change or extend the code.

node = kwargs['node']
input = node.input(0)

if input:
    geo = input.geometry()
    menu = list(geo.generateAttribMenu(attrib_type=hou.attribType.Point, data_type=hou.attribData.Int, min_size=3))
    menu += list(geo.generateAttribMenu(attrib_type=hou.attribType.Prim, data_type=hou.attribData.String, max_size=1))
    menu += list(geo.generateAttribMenu(attrib_type=hou.attribType.Vertex, data_type=hou.attribData.Float, min_size=2, max_size=3))
    return menu

else:
    return ["", "No Input Connected"]

Attribute Values as Menu

Once in a while, you need to create a menu of the values of a specific attribute. An example could be if you want certain transform attributes to only apply to say, the polygons with their variant attribute set to "dog". The expectation in the below code is that you have a string attribute (called "targetAttrib" in this case) being used to say which attribute to target, but you could also hard-set it by typing
attribName = "variant"
for example.

node = kwargs["node"]
attribName = node.parm('targetAttrib').evalAsString()
input = node.input(0)

menu = []

if node:
    geo = input.geometry()
    findAttrib = geo.findPointAttrib(attribName)

    if findAttrib:
        type = geo.findPointAttrib(attribName).dataType()
        list_menu = None
        if type == hou.attribData.String:
            list_menu = geo.pointStringAttribValues(attribName)
        elif type == hou.attribData.Int:
            list_menu = geo.pointIntAttribValues(attribName)

        list_menu = list(dict.fromkeys(list_menu))
        list_menu = sorted(list_menu)
            
        for value in list_menu:
            if value not in menu:
                menu.append(str(value))
                menu.append(str(value))

return menu

List all Parameters on a node (Int and Float only)

This splits parameter names so they appear in an order designed to find "random" controls first. Sorts by parameters that have the following in their names, in this order: seed -> iter -> parameters with size 1 -> parameters with size 3.
FURTHERMORE, it is designed to work inside of a multiparm so you will see kwargs['script_multiparm_index'] which represents the mparm number. It also adds separators into the menu if you don't know how to do that.

def allParmTemplates(group_or_folder):
    """
    Creates a generator (iterator) I can use to access ALL parmTemplates in a folder, including the folders.
    
    """
    for parm_template in group_or_folder.parmTemplates():
        yield parm_template

    # Note that we don't want to return parm templates inside multiparm
    # blocks, so we verify that the folder parm template is actually
    # for a folder.
        if parm_template.type() == hou.parmTemplateType.Folder and parm_template.isActualFolder():
            for sub_parm_template in allParmTemplates(parm_template):
                yield sub_parm_template

hda = hou.pwd()
node = hda.parm(f"node_var{kwargs['script_multiparm_index']}").evalAsNode()
seeds = []
iters = []
parms = []
vectors = []
if node:
    for parm_template in allParmTemplates(node.parmTemplateGroup()):   
        
        if parm_template.type() in [hou.parmTemplateType.Int, hou.parmTemplateType.Float]:
            if parm_template.numComponents() > 1:
                parm_name = f"{parm_template.name()} "
                if parm_template.namingScheme() == hou.parmNamingScheme.Base1:
                    parm_name += "123 (Choose One)"
                if parm_template.namingScheme() == hou.parmNamingScheme.XYZW:
                    parm_name += "xyz (Choose One)"
                if parm_template.namingScheme() == hou.parmNamingScheme.XYWH:
                    parm_name += "xywh (Choose One)"
                if parm_template.namingScheme() == hou.parmNamingScheme.UVW:
                    parm_name += "uvw (Choose One)"
                if parm_template.namingScheme() == hou.parmNamingScheme.RGBA:
                    parm_name += "rgba (Choose One)"
                vectors.append(parm_name)
                vectors.append(parm_name)
            else:
                if parm_template.name().find("seed") != -1:
                    seeds.append(parm_template.name())
                    seeds.append(parm_template.name())
                elif parm_template.name().find("iter") != -1:
                    iters.append(parm_template.name())
                    iters.append(parm_template.name())
                else:
                    parms.append(parm_template.name())
                    parms.append(parm_template.name())
    if len(seeds) > 0 and len(iters+parms+vectors) > 0:
        seeds.append("_separator_")
        seeds.append("_separator_")
    if len(iters) > 0 and len(parms+vectors) > 0:
        iters.append("_separator_")
        iters.append("_separator_")
    if len(parms) > 0 and len(vectors) > 0:
        parms.append("_separator_")
        parms.append("_separator_")
    return seeds+iters+parms+vectors

else:
    return ("Node is invalid", "Node is invalid")

Contact

Calgary, AB | Canada
edward@jaworenko.design | Email
LinkedIn | Connect with me
Milanote | Knowledge Dump
Sketchfab | Tutorials in 3D
Thingiverse | 3D Printers & Physical Design
Artstation | 3D Art (updating)
Behance | Formal Design (updating)
Vimeo | Motion & Animation
Pinterest | Things that inspire me
Instagram | Sometimes I post things

© Edward Jaworenko 2023

Back to top Arrow