Simstim

Houdini

Houdini digital assets are versioned packages: namespaces, pinned instances, and why 2.10 beats 2.9

A Houdini digital asset turns a node network into a reusable node type whose name carries a namespace and a version, much like a package. How Houdini 22 resolves those names, why saved scenes stay pinned to old versions, and a 29-line Python model of the rules.

· 9 min read · houdini · houdini beyond vex · python · pipeline · versioning

Hython output on a dark code panel, showing a new node resolving to simstim::ripple::2.10 while a node from an older scene stays on 2.9 and a string max picks 2.9, beside a Karma render of the two versions' ripples.

TL;DR Houdini lets you package a network of nodes as a new node type, called a digital asset (HDA). Its internal name works like a package coordinate: namespace::name::version. New nodes get the newest version, saved scenes keep the version they were built with, and versions are compared as numbers, not strings. We model those rules in 29 lines of plain Python, check them against Houdini 22, and show the hou calls that create and upgrade a real asset.

Houdini in 60 seconds

Houdini is a procedural 3D tool: you build geometry by wiring nodes into a graph, and every node is an operator with typed inputs, an output and a panel of parameters. SOPs (surface operators) are the nodes that work on geometry. A subnet is a node that holds a whole sub-graph, the way a block holds statements.

A digital asset, or HDA (Houdini Digital Asset), is what you get when you promote a subnet to a node type of its own. The definition (the inner network, the parameter interface, help, scripts) lives in a library file, usually a .hda file (Houdini Indie writes .hdalc, and Apprentice has its own format). Every node you place from it is an instance that refers to that definition by name. If you think of a subnet as inlined code, an HDA is a function compiled into a library, and the instance is a call site.

That makes the name the contract, and Houdini's name format works a lot like a package manager's.

The type name is a package coordinate

SideFX's docs give the format as [namespace::]name[::version]. The New Digital Asset dialog fills in both optional parts by default: your user name as the namespace, plus a version. SideFX's example is sanjay::threelightrig::1.0. The parts do what you'd expect:

  • The namespace keeps your asset from colliding with a built-in node, a vendor's asset, or a node SideFX adds later. Houdini already ships a Ripple SOP, so an asset called plain ripple would clash with it; simstim::ripple doesn't. It is the npm scope or the Java package of the asset world.
  • The version may contain only digits and dots. 2, 2.1 and 19.1.3 are fine; 2a and alpha are not. No pre-release tags, no build metadata. Houdini doesn't reject simstim::ripple::2a, though. It reads it as a node called 2a in a namespace called simstim::ripple, so it never takes part in version resolution.
  • The label shown in the UI is separate and free-form, so users see "Ripple" while scripts see simstim::ripple::2.0.

SideFX uses the scheme for its own nodes. In Houdini 22, createNode("ripple") gives you ripple::2.0, and ripple:: gives you the original Ripple, still installed for the scenes that use it.

The docs also say the name is permanent: "You can change the label later, but you can't change the internal name without recreating the asset." Choose the namespace before anyone uses the asset.

Resolution: new code gets latest, old code stays pinned

Here is the part that matters for a pipeline. When several versions of simstim::ripple are installed side by side, Houdini's Tab menu shows only the latest by default. createNode("simstim::ripple"), with no version, gets the highest version number, and a definition with no version at all counts as the lowest. Nodes already saved in a scene keep their full versioned name, so they keep using the old definition. In the docs' words: "Instances of the old version will still work and use the old implementation, while users placing a new node will get the latest version."

We checked that in Houdini 22.0.368. We published simstim::ripple::2.9 to a library folder, placed a node with the unversioned name (it got 2.9) and saved the scene. Then we added 2.10 to the same folder and reopened the scene in a fresh session, with that folder on the asset scan path. The saved node was still simstim::ripple::2.9, and its points matched 2.9's formula to 7e-7. A new node got 2.10. Installing 2.10 by hand after opening the scene gave the same result, and the install order made no difference.

Two rippled grids side by side, labeled simstim::ripple::2.9 and simstim::ripple::2.10: on the left, strong rings across the whole grid; on the right, rings that fade toward the edges
Both nodes are in one scene, with the same Amplitude and Frequency. The left one was placed while 2.9 was the newest version and is still 2.9 after the scene is saved and reopened; the right one was placed after 2.10 was published. Karma render, Houdini 22.0.368.

If you write C, this is glibc's symbol versioning. On x86-64, a binary linked against glibc before 2.14 binds to memcpy@GLIBC_2.2.5 and keeps the old behavior, which also handles overlapping buffers, while newly linked code gets memcpy@GLIBC_2.14. Both live in the same library. A Houdini scene saved with simstim::ripple::2.9 is that old binary.

There are two ways out of the default. A trailing :: asks for the definition with no version (simstim::ripple::), and a leading :: asks for no namespace (::ripple gives Houdini's own ripple::2.0). The HOUDINI_OPNAMESPACE_HIERARCHY environment variable lists preferred namespaces in priority order, so a studio can make a bare name mean its own asset on every machine. With it set to simstim, createNode("ripple") gave us simstim::ripple::2.10 instead of Houdini's Ripple. An entry can also pin a version, but in our test only when it included the node category: simstim::Sop/ripple::2.9 worked, while simstim::ripple::2.9, the form in SideFX's example, changed nothing.

We wrote the version rules down as a small Python model. It runs anywhere; we ran it with Python 3.12.10 and with Houdini 22's Python 3.13.10.

# A toy model of how Houdini picks an asset definition by type name.
# Rules from SideFX's "Asset namespaces and versions" page: versions are
# digits and dots, a name with no version counts as the lowest version,
# and a reference without a version gets the highest version installed.
import re

installed = ["simstim::ripple", "simstim::ripple::1.0",
             "simstim::ripple::2.9", "simstim::ripple::2.10"]

def version_key(type_name):
    parts = type_name.split("::")
    ver = parts[2] if len(parts) == 3 else ""
    if ver and not re.fullmatch(r"\d+(\.\d+)*", ver):
        raise ValueError(f"bad version in {type_name!r}")
    # compare numerically, component by component: (2, 10) > (2, 9)
    return tuple(int(n) for n in ver.split(".")) if ver else ()

def resolve(ref):
    if ref.endswith("::"):                     # "name::" means no version
        return ref[:-2]
    if len(ref.split("::")) == 3:              # versioned name: pinned
        return ref
    matches = [t for t in installed if t == ref or t.startswith(ref + "::")]
    return max(matches, key=version_key)       # unversioned: newest wins

print("new node  simstim::ripple      ->", resolve("simstim::ripple"))
print("saved     simstim::ripple::1.0 ->", resolve("simstim::ripple::1.0"))
print("explicit  simstim::ripple::    ->", resolve("simstim::ripple::"))
print("string max (the wrong answer)  ->", max(installed))

Output:

new node  simstim::ripple      -> simstim::ripple::2.10
saved     simstim::ripple::1.0 -> simstim::ripple::1.0
explicit  simstim::ripple::    -> simstim::ripple
string max (the wrong answer)  -> simstim::ripple::2.9

Houdini gave the same answers for all three lookups with these four definitions installed. The last line is the trap for a hand-rolled version picker: compare 2.10 and 2.9 as strings and 2.9 wins. A pipeline script that sorts asset names to find "the latest" needs the tuple key, not sorted(). Inside Houdini you can ask instead: a node type's versionNamespaceOrder() lists the installed versions newest first, as Houdini ranks them. For our four it returned simstim::ripple::2.10, ::2.9, ::1.0, then the unversioned one.

2.10 isn't a corner case you have to go looking for. Right-click an asset node and choose Digital Asset > Increase Minor Version, and the code behind that menu proposes 2.10 for a 2.9 asset.

The same thing with the real API

Inside Houdini, the hou Python module does the work. This runs in the Python Shell (Windows > Python Shell) with a subnet already built at /obj/geo1/ripple. Since Houdini 20, createDigitalAsset and changeNodeType live on hou.OpNode, which SOP nodes inherit.

import hou

sub = hou.node("/obj/geo1/ripple")             # a subnet you've built
name = hou.hda.fullNodeTypeNameFromComponents("", "simstim", "ripple", "2.0")
ext = hou.hda.defaultFileExtension()           # ".hdalc" under Indie
path = hou.text.expandString("$HOUDINI_USER_PREF_DIR/hda/simstim--ripple-2.0" + ext)

ptg = sub.parmTemplateGroup()                  # the subnet's parameters
node = sub.createDigitalAsset(name=name, hda_file_name=path,
                              description="Ripple",
                              min_num_inputs=1, max_num_inputs=1)
node.type().definition().setParmTemplateGroup(ptg)   # not copied for you
print(node.type().name())                      # the full versioned name
print(hou.hda.componentsFromFullNodeTypeName(node.type().name()))

# Upgrading is explicit: move every 1.0 instance onto 2.0, keeping parms.
old = hou.nodeType(hou.sopNodeTypeCategory(), "simstim::ripple::1.0")
for inst in (old.instances() if old else ()):
    inst.changeNodeType("simstim::ripple::2.0", keep_parms=True)

It prints:

simstim::ripple::2.0
('', 'simstim', 'ripple', '2.0')

The file name follows SideFX's recommended "Standard" pattern, namespace, then name, then version. defaultFileExtension() picks the extension for your license; under Indie, a name ending in .hda is written exactly as given. The setParmTemplateGroup line matters: createDigitalAsset leaves the subnet's spare parameters on the converted node only, so without it every new instance comes up with no parameters at all.

Nothing upgrades on its own. That is a feature: a render farm re-opening a scene from three months ago gets the network the artist approved, not whatever was published since.

What trips you up, and what clicks

  • There are two "versions." The Version field in the asset's Type Properties window "has no relation to the ::version part of the node type name," per the docs. Only the name part drives resolution.
  • A new version is a new type. 2.0 is not an edit of 1.0; it is a separate definition. Parameters that keep their names carry across changeNodeType; renamed ones come back at their new defaults.
  • Lookups don't resolve. createNode("simstim::ripple") picks the newest version, but hou.nodeType(category, "simstim::ripple") is an exact lookup and returns the unversioned definition. Ask for the version you mean.
  • Check the proposed number. The code behind Increase Major Version keeps the minor part, so a 2.9 asset is offered 3.9, not 3.0. The name is editable in the dialog.
  • Bumping is a breaking-change decision. If a change would alter what old scenes compute, make a new version. If it only fixes a crash, editing the existing definition reaches every scene. That is semantic versioning, just without a tool to enforce it.
  • Unlocked instances drift. Right-click an instance and choose Allow Editing of Contents, and matchesCurrentDefinition() returns False: the node no longer follows its library. matchCurrentDefinition() puts it back. Audit for unlocked nodes before trusting an upgrade.
  • Embedded definitions travel with the scene. Saving an asset into the .hip file is convenient for sharing, but the copy in the scene and the one in your library can quietly diverge.

Try it yourself

The Python model needs nothing but Python 3. Extend it with a namespace hierarchy list, or make it reject a version like 2a at install time instead of at lookup.

For the real thing, Houdini Apprentice is free for non-commercial use:

  1. Build a small SOP subnet at /obj/geo1/ripple with a spare parameter or two. In the snippet, change 2.0 to 1.0 in the name and path lines, and run it without its last three lines (the upgrade). The subnet becomes a simstim::ripple::1.0 node. Rename that node so the path is free.
  2. Build a changed subnet at /obj/geo1/ripple and run the snippet as written, again without the last three lines.
  3. Place a Ripple from the Tab menu and compare its type().name() with the first node's. The new one is 2.0; the old one is still 1.0.
  4. Run the last three lines. The 1.0 node moves onto 2.0 and keeps the values of the parameters both versions share.

To see both versions in the Tab menu, set Edit > Preferences > Shelf Tools and Tab Menu > TAB Menu Operator Namespace Filtering to Show All Operators.

Sources