Simstim

Latest

USD layers are git for scene data: resolving “strongest opinion wins” in 25 lines of Python

OpenUSD stacks layers of partial edits and resolves them when you read the scene. With only pip install usd-core, build a two-layer scene, override one value, and ask USD which file each value came from.

· 5 min read · usd · python · pipeline · tutorial · other td tools

The text of shot.usda and asset.usda beside Karma renders of each layer, a grey sphere at radius 1 and the same sphere at radius 2.5, above the GetPropertyStack output listing 2.5 from shot.usda and 1.0 from asset.usda.

TL;DR Pixar's Universal Scene Description (USD) doesn't store a scene as one file. It stacks layers of partial edits and resolves them at read time, much like a merge that never gets flattened. Using nothing but pip install usd-core, we build a two-layer scene, override one value, and ask USD which file each value came from.

OpenUSD in 60 seconds

USD is an open-source scene description format and C++/Python library from Pixar. Houdini (through Solaris/LOPs), Blender, Unreal, Maya and Omniverse all read and write it. The terms you need:

  • Stage: the composed scene you query. Think of it as the working tree.
  • Layer: one file (.usda is text, .usdc is binary). A layer holds opinions, not a finished scene.
  • Prim: a node in the scene hierarchy, addressed by a path like /World/Ball, which works like a filesystem path.
  • Attribute: a typed value on a prim, such as radius or displayColor.
  • def vs over: def declares that a prim exists. over says "if this prim exists somewhere weaker, change these fields."

The rule that makes this work is that each layer contributes opinions, and when several layers hold an opinion on the same attribute, the strongest one wins. USD calls this process composition. The Alliance for OpenUSD's Core Specification 1.0, ratified in December 2025, includes a normative specification of the composition algorithm, so it isn't just "whatever Pixar's code does" anymore.

The core idea: a scene is a stack of diffs

Most of us learned to treat a 3D scene file as a document: open, edit, save, and hope nobody else touched it. USD treats the scene as the result of a function over a stack of files. Modeling publishes asset.usda. Layout, animation and lighting each write their own thin layer on top, and nobody edits anyone else's file.

If you write code, the closest model is a list of dicts merged at lookup time, strongest first:

# Mental model only. Real USD composition is far richer (references, variants, inherits...).
layers = [{"radius": 2.5}, {"radius": 1.0, "color": (0.5, 0.5, 0.5)}]  # strong -> weak
def resolve(key):
    return next(layer[key] for layer in layers if key in layer)
print(resolve("radius"), resolve("color"))  # 2.5 (0.5, 0.5, 0.5)

This is the real thing. Save it as layers.py, run pip install usd-core, then run python layers.py. No DCC or GPU needed.

# Two layers, one scene: USD's "strongest opinion wins" in ~25 lines.
from pxr import Usd, UsdGeom, Gf

# 1. The asset layer: what modeling publishes.
asset = Usd.Stage.CreateNew("asset.usda")
ball = UsdGeom.Sphere.Define(asset, "/World/Ball")           # writes a "def"
ball.GetRadiusAttr().Set(1.0)
ball.GetDisplayColorAttr().Set([Gf.Vec3f(0.5, 0.5, 0.5)])
asset.GetRootLayer().Save()

# 2. The shot layer: sublayers the asset, then overrides one value.
shot = Usd.Stage.CreateNew("shot.usda")
shot.GetRootLayer().subLayerPaths.append("asset.usda")        # weaker layer
over = shot.OverridePrim("/World/Ball")                       # writes an "over"
UsdGeom.Sphere(over).GetRadiusAttr().Set(2.5)
shot.GetRootLayer().Save()

# 3. Resolve: ask the composed stage, then ask *why*.
radius = shot.GetAttributeAtPath("/World/Ball.radius")
print("resolved radius:", radius.Get())                       # 2.5
for spec in radius.GetPropertyStack(Usd.TimeCode.Default()):  # strongest first
    print("  opinion", spec.default, "from", spec.layer.identifier)
print("color:", UsdGeom.Sphere(over).GetDisplayColorAttr().Get())  # from asset
print(shot.GetRootLayer().ExportToString())

Output with usd-core 26.8 (paths shortened):

resolved radius: 2.5
  opinion 2.5 from .../shot.usda
  opinion 1.0 from .../asset.usda
color: [(0.5, 0.5, 0.5)]

The printed shot.usda is the most useful part. It holds no sphere and no color, just a pointer to the asset underneath and one changed value:

#usda 1.0
(
    subLayers = [
        @asset.usda@
    ]
)

over "World"
{
    over "Ball"
    {
        double radius = 2.5
    }
}

That file is a diff. It stays small and readable, and because it lives in its own file, the modeler can republish the asset without clobbering the shot's edit, and the other way round.

What trips you up, and what clicks

  1. An over is not a copy. It's natural to assume that overriding a prim duplicates it into the stronger layer. It doesn't. An over with nothing to land on composes to an undefined prim: the stage skips it when you traverse, and nothing renders. The prim only becomes real because asset.usda has a def for it underneath. Read .usda files as patches rather than scenes, and the format makes sense.
  2. Sublayer order is strength order, and it's easy to get backwards. In subLayerPaths, earlier entries are stronger. That's the opposite of CSS, where a later stylesheet wins.
  3. GetPropertyStack answers "why is this value 2.5?" That's the question pipeline TDs spend their days on. The property stack lists every opinion, strongest first, along with the layer it lives in. It's git blame for an attribute.
  4. Sublayers are only one arc. USD has several composition arcs: local opinions (including sublayers), inherits, variants, relocates, references, payloads and specializes. Their strength order is the mnemonic LIVERPS; older material says LIVRPS, from before relocates were added. Today covered only the L. References are a natural next step, because they let one asset appear a thousand times without a thousand copies.

Why a TD cares

This is the same non-destructive thinking as a procedural node graph, applied to whole departments instead of points. Houdini's Solaris context is a node-graph front end for authoring exactly these layers: in a LOP network, each node effectively writes opinions like the one above.

Try it yourself

  1. pip install usd-core (it runs on plain CPython, no DCC needed) and run the script.
  2. Add a third layer, lighting.usda, that overrides displayColor to red, and insert it into shot.usda's subLayerPaths before asset.usda. Then check the property stack. One catch: on its own, lighting.usda doesn't know Ball is a sphere, so UsdGeom.Sphere(over).GetDisplayColorAttr().Set(...) fails with "Empty typeName". Give the attribute its type yourself: over.CreateAttribute("primvars:displayColor", Sdf.ValueTypeNames.Color3fArray, custom=False).Set([Gf.Vec3f(1, 0, 0)]) (import Sdf from pxr).
  3. Now move it after asset.usda. Predict the color before you run it.
  4. Free viewers: usdview isn't in the usd-core wheel, but Blender (File → Import → Universal Scene Description), Houdini Apprentice's Solaris context and Unreal Engine (with the USD Importer plugin) can all open shot.usda.

Python API used: Usd.Stage.CreateNew, UsdGeom.Sphere.Define, Usd.Stage.OverridePrim, Sdf.Layer.subLayerPaths, Usd.Attribute.GetPropertyStack.

Sources