Overview
Deep dive into new style Houdini digital asset versioning. Shoutout to Faitel that inspired this post through his comment on Houdini Environment setup blog post. We will also go into HDA naming and namespaces and why you should leverage namespaces when creating Houdini Digital Assets.
Here is the official documentation on what we will be talking about.
Namespaces
"Namespaces are one honking great idea -- let's do more of those!" - Bonus points if you know where that quote comes from (I linked the quote if you don't know and want to find out).
We begin with namespaces since they are heavily involved in the new style versioning - the version is simply in the last namespace.
When you create a Houdini Digital Asset (HDA), in the operator name, you assign the namespaces, with a :: divider. I usually go with the style in the screenshot.

HDA Versioning - name components
1 2 3 4 5 |
node = hou.pwd() geo = node.geometry() parent = node.parent() print(parent.type().nameComponents()) # ('', 'lcg', 'hdaVersioning', '1.0.000') |
The documentation on the node type name components lists that the tuple is listed in the following order:
- scope network type
- node type namespace
- node type core name
- version
It is really handy to put all of your custom nodes into your own namespace, because you can avoid name clashing, as well as using Python to easily find your stuff in hip files.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
# Usage: myNodes = getNodesByNamespace('lcg', hou.pwd().getParent()) def getNodesByNamespace(namespace, parentNode): if namespace.endswith('::'): namespace = namespace[:-2] all_instances = [] filteredInstances = [] for node_type in hou.sopNodeTypeCategory().nodeTypes().values(): if node_type.name().startswith('%s::' % namespace): all_instances.extend(node_type.instances()) for instance in all_instances: if instance.parent().path() == parentNode.path(): filteredInstances.append(instance) return filteredInstances |
Versioning
First, let's make sure you have the HDA version selection enabled in the Houdini user interface. Go to Widows -> Asset Manager. Select any HDA and change the menu drop down to display the asset bar menu. You will see a drop down menu that gives you access to all of the versions for all the definitions inside the hda library file path.

HDA Versioning - Display asset bar
The way you can create a new version is by right clicking on the HDA and select the "Show in asset manager... command. In the Operators tab, your operator will be selected. Right click on it and select "Copy..."' and increase the version number in the pop up dialog and click "Accept". You will now see another option in the asset bar drop down menu. You can select this to jump between versions.

HDA Versioning - Make a copy

Asset bar versions
I added in a network that sets a message string read be a font sop to display the info about the HDA.

HDA Versioning
Automating HDA Versioning with a shelf tool
Here is a toolbar tool that can automate the creation of new versions. Select any HDA and run the tool.

Hda Versioning - Automation
Follow the Houdini Environment Setup , to make it easy to drop in your own resources like toolbars. Below is the Python code for this tool.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 |
import hou import PySide2.QtWidgets import PySide2.QtCore from functools import partial from distutils.version import StrictVersion def run(): # This is the main function that gets called by the shelf tool on the bottom ( run() ) # Store current selection selection = hou.selectedNodes() # If it is more than one or none, abort if len(selection) > 1 or len(selection) == 0: hou.ui.displayMessage('Please select a single Houdini Digital Asset node to save and update version on.') return # If it is not a Houdini Digital Asset, abort if not selection[0].type().definition(): hou.ui.displayMessage('Please select a single Houdini Digital Asset node to save and update version on.') return # Store as hda_node hda_node = selection[0] # The definition has a ton of useful stuff definition = hda_node.type().definition() # Where the hda is saved to libraryFilePath = definition.libraryFilePath() # Store full name current_full_name = hda_node.type().name() # Last index of the name components is the current version current_version_string = hda_node.type().nameComponents()[-1] # Split versions apart and store major and minor version in a separate variable current_major = current_version_string.split('.')[0] current_minor = current_version_string.split('.')[1] # Set the 3 digit revision number to 0 if the HDA is only using the single float versioning (1.0 and not 1.0.005) current_revision = 0 if len(current_version_string.split('.')) < 3 else current_version_string.split('.')[2] # This is how you can get to all the definitions of the Houdini Digital Asset all_definitions = hou.hda.definitionsInFile(libraryFilePath) # This sets the node to the latest version of those stored # hda_node.changeNodeType(all_definitions[-1].nodeTypeName()) # We have everything we need, now let's create a custom window # Instantiate the VersionWindow class - at this point, I would jump over to that code - keep in mind, we will # return a window object and afterwards we will set it's initial state, using everything we just learned about # this HDA. version_window = VersionWindow() # Set window title version_window.setWindowTitle('Versioning for: {hda_name}'.format(hda_name=current_full_name)) # Set current version label with current version version_window.current_version_label.setText(current_version_string) # Set current path label version_window.current_path_label.setText(libraryFilePath) # Set value of the integer editor and set the editor to not go down from there version_window.major_version.setValue(int(current_major)) version_window.major_version.setMinimum(int(current_major)) # Set value of the integer editor and set the editor to not go down from there version_window.minor_version.setValue(int(current_minor)) version_window.minor_version.setMinimum(int(current_minor)) # Set value of the integer editor and set the editor to not go down from there version_window.revision_version.setValue(int(current_revision)) version_window.revision_version.setMinimum(int(current_revision)) # Connect the button signal to the set new version command and pass the hda node and the version window as arguments version_window.set_version.clicked.connect(partial(set_new_version_button_command, hda_node, version_window)) # Show the window version_window.show() def set_new_version_button_command(node, version_window): """ :param node: hou.Node :param version_window: VersionWindow :return: None """ library_filepath = node.type().definition().libraryFilePath() name_base = '::'.join(node.type().name().split('::')[:-1]) new_version = '{major}.{minor}.{revision}'.format(major=version_window.major_version.value(), minor=version_window.minor_version.value(), revision=version_window.revision_version.textFromValue( version_window.revision_version.value())) new_name = '{name_base}::{new_version}'.format(name_base=name_base, new_version=new_version) # Here we can compare the current version to new version and detect if the new version is lower or equal if not StrictVersion(new_version) > StrictVersion(node.type().nameComponents()[-1]): hou.ui.displayMessage('The version number is the same as the current version - please increase number') return # Create a pop up to give the user a chance to confirm or cancel answer = hou.ui.displayMessage('Creating a new version for {node_name}\nNew Version - {new_version}'.format(node_name=node.type().name(), new_version=new_version), title='Setting New Version', buttons=['OK', 'Cancel']) # If answer 'OK', create new version and set to latest version if answer == 0: node.type().definition().copyToHDAFile(library_filepath, new_name) all_definitions = hou.hda.definitionsInFile(library_filepath) node.changeNodeType(all_definitions[-1].nodeTypeName()) # Close version window version_window.close() # PySide2 UI # Set the parent to Houdini application window class VersionWindow(PySide2.QtWidgets.QMainWindow): def __init__(self, parent=hou.ui.mainQtWindow()): super(VersionWindow, self).__init__(parent) # Function to build the UI # Create main widget main_widget = PySide2.QtWidgets.QWidget(self) self.setCentralWidget(main_widget) # Initialize the layout global_layout = PySide2.QtWidgets.QVBoxLayout() layout = PySide2.QtWidgets.QFormLayout() main_widget.setLayout(global_layout) # Create Controls - Display Current Version self.current_version_label = PySide2.QtWidgets.QLabel() self.current_version_label.setMinimumWidth(300) # Create Controls - Display Library Path self.current_path_label = PySide2.QtWidgets.QLabel() # Create Controls - Display Divider line = PySide2.QtWidgets.QFrame() line.setFrameStyle(PySide2.QtWidgets.QFrame.HLine | PySide2.QtWidgets.QFrame.Sunken) # Create Controls - Major version int editor self.major_version = PySide2.QtWidgets.QSpinBox() # Create Controls - Minor version int editor self.minor_version = PySide2.QtWidgets.QSpinBox() # Create Controls - custom spin box that supports a zero padded syntax for integers (001 instead of 1) self.revision_version = PaddedSpinBox() # Create Controls - Create New Version button self.set_version = PySide2.QtWidgets.QPushButton('Create New Version') # Add controls to layout and set label layout.addRow('Current Version:', self.current_version_label) layout.addRow('Library Path:', self.current_path_label) layout.addRow(line) layout.addRow('Major Version:', self.major_version) layout.addRow('Minor Version:', self.minor_version) layout.addRow('Revision Version:', self.revision_version) # Global layout setting global_layout.addLayout(layout) global_layout.addWidget(self.set_version) # PySide2 UI - custom QSpinBox that supports a zero padded syntax # Subclass PySide2.QtWidgets.QSpinBox class PaddedSpinBox(PySide2.QtWidgets.QSpinBox): def __init__(self, parent=None): super(PaddedSpinBox, self).__init__(parent) # Custom format of the actual value returned from the text def valueFromText(self, text): regExp = PySide2.QtCore.QRegExp(("(\\d+)(\\s*[xx]\\s*\\d+)?")) if regExp.exactMatch(text): return regExp.cap(1).toInt() else: return 0 # Custom format of the text displayed from the value def textFromValue(self, value): return str(value).zfill(3) run() |
You can download the toolbar from here and use as is or change it to fit your needs.
Also here is the must have link to the Houdini Python documentation
Conclusion
Many times in production, I use version control like Perforce to store HDA versions that are distributed to teams. Houdini's HDA versioning gives another layer of flexibility to experiment and move pipelines forward without the risk of breaking existing setups. When a new version is introduced, the already existing nodes stay on the version that they were originally created on.
To update them, use the Python code in the beginning of the article to find the instances and set their version.
Hope you have found the information useful. Feel free to drop a line below with any comments you have, or just to say hello.