Siggraph Presentation

This guide will be officially introduced at Siggraph 2023 - Houdini Hive on Wednesday, 9. of August 2023 at 11:00 AM PST.

Tips & Tricks

You can find all the .hip files of our shown examples in our USD Survival Guide - GitHub Repo.

Table of Contents

  1. Composition
    1. Extracting payloads and references from an existing layer stack with anonymous layers
    2. Efficiently re-writing existing hierarchies as variants
    3. Adding overrides via inherits
  2. How do I check if an attribute has time samples (if there is only a single time sample)?
  3. Where are Houdini's internal lop utils stored?
  4. How do I get the LOPs node that last edited a prim?
  5. How do I store side car data from node to node?

Composition

Now we've kind of covered these topics in our A practical guide to composition and Composition - LIVRPS sections.

We strongly recommend reading these pages before this one, as they cover the concepts in a broader perspective.

Extracting payloads and references from an existing layer stack with anonymous layers

When building our composition in USD, we always have to make sure that layers that were generated in-memory are loaded via the same arc as layers loaded from disk. If we don't do this, our composition would be unreliable in live preview vs cache preview mode.

Composition arcs always reference a specific prim in a layer, therefore we usually attach our caches to some sort of predefined root prim (per asset). This means that if we import SOP geometry, with multiple of these root hierarchies, we should also create multiple references/payloads so that each root prim can be unloaded/loaded via the payload mechanism.

Instead of having a single SOP import or multiple SOP imports that are flattened to a single layer, we can put a SOP import within a for loop. Each loop iteration will then carry only the data of the current loop index (in our example box/sphere/tube) its own layer, because we filter the sop level by loop iteration.

The very cool thing then is that in a Python node, we can then find the layers from the layer stack of the for loop and individually payload them in.

Again you can also do this with a single layer, this is just to demonstrate that we can pull individual layers from another node.

You can find this example in the composition.hipnc file in our USD Survival Guide - GitHub Repo.

Efficiently re-writing existing hierarchies as variants

Via the low level API we can also copy or move content on a layer into a variant. This is super powerful to easily create variants from caches.

Here is how it can be setup in Houdini:

Here is the code for moving variants:

from pxr import Sdf, Usd

node = hou.pwd()
layer = node.editableLayer()

source_node = node.parm("spare_input0").evalAsNode()
source_stage = source_node.stage()
source_layer = source_node.activeLayer()

with Sdf.ChangeBlock():
    edit = Sdf.BatchNamespaceEdit()

    iterator = iter(Usd.PrimRange(source_stage.GetPseudoRoot()))
    for prim in iterator:
        if "GEO" not in prim.GetChildrenNames():
            continue
        iterator.PruneChildren()
        prim_path = prim.GetPath()
        prim_spec = layer.GetPrimAtPath(prim_path)
        # Move content into variant
        variant_set_spec = Sdf.VariantSetSpec(prim_spec, "model")
        variant_spec = Sdf.VariantSpec(variant_set_spec, "myCoolVariant")
        variant_prim_path = prim_path.AppendVariantSelection("model", "myCoolVariant")
        edit.Add(prim_path.AppendChild("GEO"), variant_prim_path.AppendChild("GEO"))
        # Variant selection
        prim_spec.SetInfo("variantSetNames", Sdf.StringListOp.Create(prependedItems=["model"]))
        prim_spec.variantSelections["model"] = "myCoolVariant"
        
    if not layer.Apply(edit):
        raise Exception("Failed to apply layer edit!")

And for copying:

from pxr import Sdf, Usd

node = hou.pwd()
layer = node.editableLayer()

source_node = node.parm("spare_input0").evalAsNode()
source_stage = source_node.stage()
source_layer = source_node.activeLayer()

with Sdf.ChangeBlock():
    iterator = iter(Usd.PrimRange(source_stage.GetPseudoRoot()))
    for prim in iterator:
        if "GEO" not in prim.GetChildrenNames():
            continue
        iterator.PruneChildren()
        prim_path = prim.GetPath()
        prim_spec = Sdf.CreatePrimInLayer(layer, prim_path)
        prim_spec.specifier = Sdf.SpecifierDef
        prim_spec.typeName = "Xform"
        parent_prim_spec = prim_spec.nameParent
        while parent_prim_spec:
            parent_prim_spec.specifier = Sdf.SpecifierDef
            parent_prim_spec.typeName = "Xform"
            parent_prim_spec = parent_prim_spec.nameParent
                    
        # Copy content into variant
        variant_set_spec = Sdf.VariantSetSpec(prim_spec, "model")
        variant_spec = Sdf.VariantSpec(variant_set_spec, "myCoolVariant")
        variant_prim_path = prim_path.AppendVariantSelection("model", "myCoolVariant")
        Sdf.CopySpec(source_layer, prim_path, layer, variant_prim_path)
        # Variant selection
        prim_spec.SetInfo("variantSetNames", Sdf.StringListOp.Create(prependedItems=["model"]))
        prim_spec.variantSelections["model"] = "myCoolVariant"

Adding overrides via inherits

We can add inherits as explained in detail in our composition - LIVRPS section.

We typically drive the prim selection through a user defined prim pattern/lop selection rule. In the example below, for simplicity, we instead iterate over all instances of the prototype of the first pig prim.

from pxr import Gf, Sdf, Usd
node = hou.pwd()
stage = node.editableStage()
pig_a_prim = stage.GetPrimAtPath(Sdf.Path("/pig_A"))
# Inspect prototype and collect what to override
prototype = pig_a_prim.GetPrototype()
# Create overrides
class_prim = stage.CreateClassPrim(Sdf.Path("/__CLASS__/pig"))
edit_prim = stage.DefinePrim(class_prim.GetPath().AppendPath("geo/shape"))
xform = Gf.Matrix4d()
xform.SetTranslate([0, 2, 0])
edit_prim.CreateAttribute("xformOpOrder", Sdf.ValueTypeNames.TokenArray).Set(["xformOp:transform:transform"])
edit_prim.CreateAttribute("xformOp:transform:transform", Sdf.ValueTypeNames.Matrix4d).Set(xform)
# Add inherits
instance_prims = prototype.GetInstances()
for instance_prim in instance_prims:
    inherits_api = instance_prim.GetInherits()
    # if instance_prim.GetName() == "pig_B":
    #     continue
    inherits_api.AddInherit(class_prim.GetPath(), position=Usd.ListPositionFrontOfAppendList)    

How do I check if an attribute has time samples (if there is only a single time sample)?

We often need to check if an attribute has animation or not. Since time samples can come through many files when using value clips, USD ships with the Usd.Attribute.ValueMightBeTimeVarying() method. This checks for any time samples and exists as soon as it has found some as to opening every file like Usd.Attribute.GetTimeSamples does.

The issue is that if there is only a single time sample (not default value) it still returns False, as the value is not animated per se. (By the way, this is also how the .abc file did it). Now that kind of makes sense, the problem is when working with live geometry in Houdini, we don't have multiple time samples, as we are just looking at the active frame. So unless we add a "Cache" LOP node afterwards that gives us multiple time samples, the GetTimeSamples will return a "wrong" value.

Here is how we get a correct value:

def GetValueMightBeTimeVarying(attribute, checkVariability=False):
    """Check if an attribute has time samples.
    Args:
        attribute (Usd.Attribute): The attribute to check.
        checkVariability (bool): Preflight check if the time variability metadata is uniform,
                                 if yes return False and don't check the value
    Returns:
        bool: The state if the attribute has time samples
    """
    if checkVariability and attribute.GetVariability() == pxr.Sdf.VariabilityUniform:
        return False
    # Get the layer stack without value clips
    property_stack = attribute.GetPropertyStack(pxr.Usd.TimeCode.Default())
    if property_stack[0].layer.anonymous:
        # It is fast to lookup data that is in memory
        return attribute.GetNumTimeSamples() > 0
    # Otherwise fallback to the default behaviour as this inspects only max two timeSamples
    return attribute.ValueMightBeTimeVarying()

The logic is relatively simple: When looking at in-memory layers, use the usual command of GetNumTimeSamples as in-memory layers are instant when querying data. When looking at on disk files, use the ValueMightBeTimeVarying, as it is the fastest solution.

You can find the shown file here: UsdSurvivalGuide - GitHub

Houdini Attribute Value Might Be Time Varying

Where are Houdini's internal lop utils stored?

You can find Houdini's internal loputils under the following path:

$HFS/houdini/python3.9libs/loputils.py

It is not an official API module, so use it with care, it may be subject to change.

You can simply import via import loputils. It is a good point of reference for UI related functions, for example action buttons on parms use it at lot.

Here you can find the loputils.py - Sphinx Docs online.

How do I get the LOPs node that last edited a prim?

When creating data on your layers, Houdini attaches some custom data to the customData prim metadata. Among the data is the HoudiniPrimEditorNodes. This stores the internal hou.Node.sessionId and allows you to get the last node that edited a prim.

This value is not necessarily reliable, for example if you do custom Python node edits, this won't tag the prim (unless you do it yourself). Most Houdini nodes track it correctly though, so it can be useful for UI related node selections.

...
def Xform "pig" (
    customData = {
        int[] HoudiniPrimEditorNodes = [227]
    }
    kind = "component"
)
...
Here is how you retrieve it:
import hou
from pxr import Sdf
stage = node.stage()
prim = stage.GetPrimAtPath(Sdf.Path("/pig"))
houdini_prim_editor_nodes = prim.GetCustomDataByKey("HoudiniPrimEditorNodes")
edit_node = None
if houdini_prim_editor_nodes:
    edit_node = hou.nodeBySessionId(houdini_prim_editor_nodes[-1])

You can also set it:

import hou
from pxr import Sdf, Vt
node = hou.pwd()
stage = node.editableStage()
prim = stage.GetPrimAtPath(Sdf.Path("/pig"))
houdini_prim_editor_nodes = prim.GetCustomDataByKey("HoudiniPrimEditorNodes") or []
houdini_prim_editor_nodes = [i for i in houdini_prim_editor_nodes]
houdini_prim_editor_nodes.append(node.sessionId())
prim.SetCustomDataByKey("HoudiniPrimEditorNodes", Vt.IntArray(houdini_prim_editor_nodes))

The Houdini custom data gets stripped from the file, if you enable it on the USD rop (by default it gets removed). Alt text

How do I store side car data from node to node?

To have a similar mechanism like SOPs detail attributes to track data through your network, we can write our data to the /HoudiniLayerInfo prim. This is a special prim that Houdini creates (and strips before USD file write) to track Houdini internal data. It is hidden by default, you have to enable "Show Layer Info Primitives" in your scene graph tree under the sunglasses icon to see it. We can't track data on the root or session layer customData as Houdini handles these different than with bare bone USD to enable efficient node based stage workflows.

You can either do it via Python:

import hou
import json
from pxr import Sdf
node = hou.pwd()
stage = node.editableStage()
prim = stage.GetPrimAtPath(Sdf.Path("/HoudiniLayerInfo"))
custom_data_key = "usdSurvivalGuide:coolDemo"
my_custom_data = json.loads(prim.GetCustomDataByKey(custom_data_key) or "{}")
print(my_custom_data)
prim.SetCustomDataByKey(custom_data_key, json.dumps(my_custom_data))

Or with Houdini's Store Parameters Values node. See the docs for more info (It also uses the loputils module to pull the data).