The notes below are based on info found in the Houdini docs as well as other sources.

Digital Asset

Tips

  • Asset Definition Toolbar
    • The asset definition toolbar shows the name space and version info of the HDA and can be useful to enable:
    • Assets > Asset Definition Toolbar > Show Menu Always

Creation

For a really nice and streamlined workflow of creating HDA you can use the SideFx Labs Digital Asset RMB menu.

Preferences

Before you create an HDA it is a good idea to configure the preferences. Below is the way I have setup my prefs, but that is depending on your workflow etc.

  • RMB click the subnet > Digital Asset > Preferences
  • Digital Asset Save Preferences
    • Name Construction
      • Author Entries
        Here I am using the reverse domain name notation, which in my case is:
        • se.petfactory
      • Athor Namespace : On
      • Branch Namespace : Off
      • Versioning : On
    • Tab Menu
      • Menu Entry : Digital Assets
      • Asset Label : Automatic
      • Display Branch in Label : Off
    • Save To
      • Library Directory : Custom Preference -> path/to/your/dir
      • Library File Name Automatic
      • Prefix Type Category : On

Create New

  • Select the node(s) you want to create a digital asset from.
  • Click the create subnet button (in the top of the node graph tool bar)
    • Or Ctrl + C
  • RMB click the subnet > Digital Asset > Create New
  • If you setup the preferences the way you like, the only thing you need to set is Type Name. (Possible the version number)

Custom HDA Path

Set custom path to HDA

Parameters

  • Add Parms to HDA
    • Alt MMB click the parm of a node (in the HDA subnet) with the Operator Type Properties window open
  • Conditional display (docs)
    • { parm_name [operator] value …} …
    • Available operators: ==, !=, <, >, >=, <=, =~ (matches pattern), !~ (doesn’t match pattern).
    • { enablefeature == 1 count > 10 }
    • Note if you want to use inside a multiparm remember to suffix the parm name with an #
      • { use_prefix# == 1}

Button Strip

  • Add a button strip to HDA
    • Open the Edit Type Propreties dialog
      • RMB > Type propertias
    • In the Parametrs section darg and drop a Buttos Strip Parameter to you HDA
    • With the parameter added and selected make sure trhe Menu tab is selected in the Parameter Description window
    • In the Menu Items section add items to your button strip by filling in the Token and Labels.
    • Toggle & Normal
      • If you want a button strip that allows for multiple choices to be set choose the Toggle
      • If you want Radiao button style (only one choice at a time) select Normal

Get the parametar value from the button strip. To read more about button strip scripts the docs are here

  • Get data from a Toggle button strip
    • Note that the value you get when quering the value of a button strip set to Toggle is a bitfield
    • To easily process the data you can use the functions below
    • The snippet below are run an a button strip with 3 menu items.
      • The token on the items are 0, 1, 2
      • Theese can be whatever you want, as can the label be.
      • In this example the first and third menu item was enabled
    • As you can see in the strip_to_tokens def the selected enabled tokens are returned in a list
    • In the bitfield_to_list the “value” is returned in a list with the length of the size parameter
    • The values are [1, 2, 4] in this case since we have 3 items (the value 0 is all turned off)
    • As you can see we use powers of to
    • To set the value of a button strip in Toggle mode you need to add the values together.
      • To select the first and third item of the button strip we set the parm value to 5
        • 1 (the first item) + 4 (the third) = 5
      • To select the first, second and third item of the button strip we set the parm value to 7
        • 1 + 2 + 4 = 7
def strip_to_tokens(parm):
    bitfield = parm.eval()
    tokens = parm.parmTemplate().menuItems()
    return [token for n, token in enumerate(tokens) if bitfield & (1 << n)]

def is_n_selected(bitfield, n):
    return bitfield & (1 << n)

def bitfield_to_list(bitfield, size=32):
    return [bitfield & (1 << n) for n in range(size)]

node = hou.node('/obj/geo1/btn_strip_toggle')
button_strip_parm = node.parm('strip')
bitfield = button_strip_parm.eval()

strip_to_tokens_val = strip_to_tokens(button_strip_parm)
print(f'\n{"-"*20}\n\nstrip_to_token\n\n{strip_to_tokens_val}\n\n')
# ['0', '2']


bitfield_to_list_val = bitfield_to_list(bitfield, 8)
print(f'\n{"-"*20}\n\nbitfield_to_list\n\n{bitfield_to_list_val}\n\n')
# [1, 0, 4, 0, 0, 0, 0, 0]


is_n_selected_val = is_n_selected(bitfield, 0)
print(f'\n{"-"*20}\n\nis_n_selected\n\n{is_n_selected_val}')
# 1

Get the values from a button strip in Normal mode

def get_selected_token(parm):
    selected = parm.eval()
    tokens = parm.parmTemplate().menuItems()
    return tokens[selected]

Here is a Hip file with some examples

button_strip_bitmask.hiplc
Example of a button strip, in Normal and Toggle mode

Tags

Operator Type Properties

To customize parameters tags can bu used.

  • Header Toggle
    • Add a Toggle parameter
      • give it a name, lets use “my_toggle” for now
      • Note! it is the Name, not the Label
    • Add a Folder parameter
      • Make it collapsible
      • Click Built-in Tags
      • In the tree view select Standard Tags > Parameter Folders …
      • Select Header Toggle Reference (Simple/collapsible) and press accept
      • In the Tags table view a tag Name with value “sidefx::header_toggle” will be added
        • In the Tag Value field enter the name of the parameter (we called it “my_toggle”)
    • You can keep the Toggle parm outside of the folder
    • In the Folder parm Disable When field you can enter:
      • { my_toggle == 0 }

Viewport Selection

  • Add Vieport Geometry Selection parameter
    • Go to Edit Parameter Interface
    • Add a string parameter to your node
    • Under the “Action Button” tab of the string parameter enter the following python snippet:
      import soputils 
      kwargs['geometrytype'] = (hou.geometryType.Points,) 
      kwargs['inputindex'] = 0
      kwargs['ordered'] = True
      soputils.selectGroupParm(kwargs)
      
      • Note that you can change the kwargs[‘inputindex’] to the input index of the node that you want to select geo from. You can also change geometry type.
      • Note that you can skip the “ordered” key in the kwargs dict. This will be the default behaviour (like it is in the select field of a group node). If the “ordered” key is not present, and if you select all points the select field will be set to empty (which in a group node means select all) which might not be what you want.
  • Use the HDA as selection source
    • As you probably have seen the default behaviour when we use selectGroupParm is that it uses the parent node as the source of the selection.
    • This might not be what we want, if we for instance hide some geo in our HDA and want to select using that as a source.
    • To fix this we can add a nodepath key to the kwargs that specifies the source.
    • in the soputils module the selectGroupParms uses the function getSelectionNode that in turn uses the nodepath value from the kwargs dict.
    • Add the snippet below to the Action Button script.
    nodepath = kwargs['node'].path()
    kwargs['nodepath'] = nodepath
    
  • Process the selected points
    You might want to do something with the selected points, to do this:
    • In the “Action Button” tab (of the string parameter) append the following code to call a script in your hda python module.
      kwargs['node'].hdaModule().process_points(kwargs)
      
    • In your HDA Python Module add a function that will be called from the callback script.
      def process_points(kwargs):
      
          node = kwargs['node']
          parm_name = kwargs['parmtuple'].name()
          pattern = kwargs['node'].parm(parm_name).eval()
      
          # the index of the input node   
      
          input_index = kwargs['inputindex']
          input_node = node.input(input_index)
      
          # the index of the multiparm
      
          script_multiparm_index = kwargs['script_multiparm_index']
      
          try:
              pnts = input_node.geometry().globPoints(pattern)
              # do somethig with the points...
      
              print(pnts)
      
          except hou.OperationFailed as e:
              print(e)
      

  • Get data from the selected geo
    I wanted to convert the ptnum from the viewport selection and replace the ptnum with the name attr stored on the point.
    • first

  • Add KineFX point group selector
    • You can also use the snippet that the secondary motion tool uses in its action button.
      from kinefx.ui import rigtreeutils
      rigtreeutils.selectPointGroupParm(kwargs)
      
      
      

If you want to create a dynamic menu, a menu based on current state or “live” data we can do the following

  • Create a String field dropdown replace menu
    • Add a String parameter to your HDA
    • In the Parameter Description section select the Menu tab
    • Enable Use Menu toggle
    • From the drop down select Replace (Field + Single Selection Menu)
    • Select the Menu Script (radio button)
    • In the text edit field we can write our python snippet that builds the menu.
      • Note that the script should return a list with the Value followed by the Label
        menu_data = ['value A', 'Label A', 'value B', 'Label B']
        return menu_data
        
      • Note that you can call a function in the python module by calling hou.phm().your_nice_menu(kwargs)
        def your_nice_menu(kwargs):
            menu_data = ['value A', 'Label A', 'value B', 'Label B']
            return menu_data
        

If you want to add a dropdown menu to for instance set preset on some parms you can use the following workflow. I borrowed the setup from the Heightfield layer properties node.

  • Create an HDA
    • Add an Ordered Menu parm
      • On the Parameter tab
        • Give the menu parm the name “preset” and label “Preset”
        • In the Callback script add hou.phm().set_preset(kwargs)
      • On the Menu tab
        • Add Menu Items:
          Note that we can choose to leave the token section empty on the first menu entry, since we will not use it
          • Token: leave empty Label Set Preset ↓
          • Token: low Label: Low
          • Token: mid Label: Mid
          • Token: high Label: High
    • Add Callback Script
      • Add python Module on the scripts section and add following snippet:
        def set_preset(kwargs):
        
            node = kwargs['node']  
            selected = kwargs["script_value"]   
                      
            dict_low =  {   
                'value': 0.0,
                'num':0
            }
            dict_mid =  {   
                'value': 0.5,
                'num':12
            }
            dict_high =  {   
                'value': 1.0,
                'num':42
            }
        
            if selected == 'low':
                node.setParms(dict_low)
            elif selected == 'mid':
                node.setParms(dict_mid)
            elif selected == 'high':
                node.setParms(dict_high)
        
            kwargs['parm'].set(0)         
        
        
        
        
        

Extra Files

Lets add an extra file to an HDA.

  • RMB click the HDA > Type Properties and select the Extra Files tab.
  • Click the Filename file chooser amnd navigate to the file.
  • Click Add File and press Accept to close the type properties widget.

Now the file has been saved inside the HDA on disc. So lets load the file within the HDA.

  • On the file node (inside the HDA) click the geometry file chooser.
  • On the left hand side, in the Locations section of the file chooser widget select opdef:/
  • Navigate to the HDA that we just saved our file to.
    • Note we will find the file in a directory of the HDA type. If we created a sop HDA it will be inside the sop dir. If we created the HDA with a namespace the directory of the HDA will be prefixed with this namespace as well. If we have different versions, the versions will be stored in different dirs.

Icon

When you embed icons it is a good idea to use .svg
You can embed an icon in the HDA using the method described above (Extra files) but there is an easier way.

  • Add icon (without adding it to the HDA)
    • Add icon to directory
      • Lets say that you have added a package that points to a directory on disc.
      • In the directory (that is on the HOUDINI_PATH) we create two nested folders. Like this: config/Icons (note the capital I in icons)
      • In the icons we can add our .svg icons
    • Add icon to HDA
      • Now we can use the icons by only giving the name of the file (we can skip the extension)
      • We can do this both for the HDA icon and the Interactive > Shelf Tool > Options: icon that appears in the tab menu.

Interactive

Shelf tools

You might want to add a tab menu script to your HDA that can speed up creating a certain setup, possibly containing more nodes then just your HDA.

  • Tab Menu Script
    • Open the Edit Type Operator Properties window of your HDA.
      • RMB click on the HDA > Type Properties
    • Switch to the Interactive tab and on that tab switch to Shelf Tools
    • Click on the Create New dropdown and select Default Tool
    • Select the new default tool in the list
    • Name
      • By default the name of the HDA will be $HDA_DEFAULT_TOOL
      • On the tool we adeed I kept the first part and just added a suffix that makes sense
      • for instance $HDA_DEFAULT_TOOL_setup (the $HDA_DE… will be expanded to whatever the name of the HDA is)
    • Label
      • Add Label that makes sense, for instance
      • $HDA_LABEL Setup
    • Switch to the Scripts tab of the Shelf Tools tab
      • Lets add this snippet or something more useful…
      import soptoolutils
      hda_node = soptoolutils.genericTool(kwargs, '$HDA_NAME')
      parent = hda_node.parent()
      hda_pos = hda_node.position()
      
      null_node = parent.createNode('null')
      null_node.setPosition(hda_pos+hou.Vector2(0,-1))
      null_node.setInput(0, hda_node)
      null_node.setSelected(True)
      
      print(kwargs['toolname'])
      
    • Swicth to the Context tab and click which context the tab menu script is to be used in
    • Here we can also add a Tab SubMenu path
      • Note that you can use slashes to create sub directories!

HDA Python

PythonModule

HDAModule User-defined Python module containing functions, classes, and constants that are stored with and accessed from a digital asset.

  • RMB click the HDA Allow Editing of Contents
  • RMB click the HDA Type Properites
  • On the scripts tab select Python Module from the Event Handler combobox. We now get a python module “automatically” added to our HDA.

Lets write som test code and execute it from a button on the HDA.

  • On the scripts tab, with the PythonModule selected enter the some code to be called
def speak(kwargs):
	node = kwargs['node']
    print('speak was called from PythonModule', node)
  • Add a button to the HDA and as a python callback script we add the following snippet:
hou.phm().speak(kwargs)
  • Note
    • phm() → hou.HDAModule
      • This shortcut lets you write hou.phm() instead of hou.pwd().hdaModule(). You are most likely to use this shortcut from event handlers, button callbacks, and menu generation scripts in digital assets.
    • To make this work the python module must be named PythonModule (which it is if we add it this way)

Additional Modules

If you find that a digital asset has too much Python code to store in one module, it’s possible to create submodules. Lets create a separate python module and call it from our “main” python module.

  • RMB click the HDA > Type Properties and select the Scripts tab
  • In the Event Handler combobox select Custom Script
  • Empty vs Existing
    • Empty Section
      • You can add an empty section
      • In the Section Name enter the name of the module you want to create
        • Lets call it custom
      • Then press Add Empty Section
      • Now you can select the script in the “scripts” chooser and start writing
      • Or, as I prefer, RMB click the HDA > Edit Extra Sections Source Code and edit in an external editor.
    • Existing
      • You can add an existing python script to the HDA this way
      • Select the python script to include (with the filebrowser labeled “filename”)
      • This will auto populate the section name with the file name (which is fine i guess, you can remove the .py extension if you want)
      • Click Add File to add the custom script to the HDA.
      • Note that the filename line edit has been populated with a file path to the script
        • You can press reload all files to reload if you have edited the sript on disc (I usually do not do this)
        • Warning if you have edited the custom script on the hda and then reload you will loose the edits you made.
  • Create Module From Section
    • To be able to use the custom module we need to createModuleFromSection in the main PythonModule
    • To add a custom module add the following lines of code to the “main” Python module
      • change the custom placeholder text to whatever your module name is called
  • Note
    • Seems like you need to import hou in the custom module if you use that.
# toolutils.createModuleFromSection(module_name, node_type, section_name)

import toolutils
custom_module = toolutils.createModuleFromSection("custom", kwargs["type"], "custom")

Then we can call the function from the main PythonModule or from a parameter of the hda

# from Python Module

custom.your_func(kwargs)
# from button parameter

hou.phm().custom.your_func(kwargs)

UserData

Custom QtWidget

Here is a way to create a custom Qt ui that can intercat with the HDA node. The Qt Widget instance is saved to the cachedUserDict (which is not persistant) if it exists open it if not create it.
Also testing the userDict which is persistant and the cachedUserDict which is not. Crteate 3 buttons and connect them to the functions in the script.

Read more about userData in the SideFX docs

from pprint import pprint

from PySide2 import QtCore
from PySide2 import QtWidgets
from PySide2 import QtGui

def show_widget(kwargs):
    
    hda_node = kwargs['node']
    main_window = hou.qt.mainWindow()
    
    w = hda_node.cachedUserData('widget')
    if w is None:
        w = HelloWidget(main_window)
        w.set_hda(hda_node)
        hda_node.setCachedUserData('widget', w)
        
    pos = QtGui.QCursor.pos()-QtCore.QPoint(30, 20)
    w.close()
    w.move(pos)
    w.show()
    
def read_persistent(kwargs):
    hda_node = kwargs['node']
    print(hda_node.userData('persistent_data'))
    pprint(hda_node.userDataDict())

def read_cached(kwargs):
    hda_node = kwargs['node']
    print(hda_node.cachedUserData('cached_data'))
    pprint(hda_node.cachedUserDataDict())
    
    
class HelloWidget(QtWidgets.QWidget):

    def __init__(self, parent=None):
        super().__init__(parent)

        self.setGeometry(400, 400, 250, 200)
        self.setWindowTitle('Hello')
        self.setWindowFlags(QtCore.Qt.Tool)
        vbox = QtWidgets.QVBoxLayout(self)
        
        self.line_edit = QtWidgets.QLineEdit()
        vbox.addWidget(self.line_edit)
        button = QtWidgets.QPushButton('Hello')
        button.clicked.connect(self.speak)
        vbox.addStretch()
        vbox.addWidget(button)
        
    def set_hda(self, hda_node):
        self.hda_node = hda_node
        
    def speak(self):
 
        text = self.line_edit.text()
        persistent_text = f'Persistent {text}'
        cached_text = f'Cached {text}'
         
        self.hda_node.setUserData('persistent_data', persistent_text)
        self.hda_node.setCachedUserData('cached_data', cached_text)

Snippets

Revert to Default

def reset_all_parms_in_containing_folder(kwargs):
    '''This will reset all parms in the containing folder (including sub folders)
    of the parm that is the sender of this callback'''
    
    hda_node = kwargs['node']
    parm = kwargs['parm']
    folder_labels = parm.containingFolders()
    if not folder_labels:
        print('no folders found')
        return
    
    parm_list = hda_node.parmsInFolder(folder_labels)
    for parm in parm_list:
        # print(parm.description())

        parm.revertToDefaults()

Help

It is a really good idea to add some help notes. Both for yourself down the line and if you share the HDA. There is a help tab on the HDA where you can add some notes. Read the docs here

Expressions

String parm python expression

In an HDA I was building I wanted a way to construct the filepath of a rop file output node inside a cop network. To get an idea of how I could approach this I had a look inside the labs map baker.

  • My HDA had a “Output Directory” chooser on the top level of the HDA.
  • If we step inside the HDA we are inside a geomery context, in this context I had two detail attributes (which in this case were generated by a python node, but the attrs could of course have been generated by some other node). I wanted theese detail attrs to be part of the file path
    • udim_s
    • matpath_s
  • Inside the geometry context there was a cop network and inside this I had the rop file output which output picture parm I wanted to control.

To setup the expression

  • Put the cursor in the string field you want to add the expression to
  • Press Alt E to open up a script editor
  • Write your expression, or paste one you written elsewhere (I found this to be a bit finicky, where it would error out if I got some syntax wrong and after that would not behave… took some trial and errors to get it right)
  • If you want it to run as Python, in the string field RMB click > Expression > Change Language to Python. (You do not need to change it at the top menu bar of the node)

Here is an example of the script I ended up using.

import hou

out_dir = hou.pwd().parent().parent().parm("output_directory").evalAsString()

udim = hou.pwd().parent().parent().node('python').geometry().attribValue('udim_s')
matpath = hou.pwd().parent().parent().node('python').geometry().attribValue('matpath_s')

file_path = '{}/{}.{}.jpg'.format(out_dir, matpath, udim)

return file_path

Extra

Asset name

The general form of an asset’s internal name is

[namespace::]node_name[::version]

The namespace and version are both optional. You can have a name with both a namespace and a version, or just a namespace, or just a version, or neither.

Namespaces

The namespace identifier lets you name your assets without worrying about using the same name as a built-in Houdini node or as a third-party asset you might use someday. (Note that this only applies to the internal name of the node… you can always use any string you want for the human readable label that appears in the user interface.) A useful convention to ensure you use a unique namespace name is to reverse the DNS address of your website. For example, if the creator´s website is at houdini.bacon.org, they would use org.bacon.houdini as the namespace for their assets.

Versions

The version string allows you to create multiple independent versions of an asset without having to change the “main name”. Instances of the old version will still work and use the old implementation, while users placing a new node will get the latest version. The version can only contain numbers and periods (.). For example, myasset::2, myasset::2.1, myasset::19.1.3, but not myasset::2a or myasset::alpha.

Asset name & scripting

Some scripting commands require the node category and node name together (for example Object/geo, Sop/copy, Dop/popsolver). To use namespaced names with these commands, use the form [namespace::]node_category/node_name[::version] For example, com.sundae::Sop/copy::2.0

Gotcha!

  • Outside References
    • Make all refernces relative to the HDA
    • CHOP net
      • I got a warning that some nodes inside a CHOP net were referencing objects outside the HDA
      • It was the Export Prefix parameter set to “../../..”
      • To fix this CTRL + MMB click the parameter to reset it to “../../”