Skip to content

Usd Asset Guide

Introduction

alt layout.... UsdAssetGuide2

A few people from Sidefx, Epic, Foundry, Tangent, ALA got together to have a chat about USD. We noticed a substantial knowledge gap between having a poke around kitchen.usd vs actually implementing USD for a studio.

After some back and forth, we felt it'd be handy to give a field guide of how to get started with USD, trying to stay as platform agnostic and jargon free as possible.

Step 1 was to look at assets.

Shawn Dunn from Epic wrote an asset brief, Mike Lydon from SideFx had a go at following the brief in Solaris, eventually roping in Rob Stauffer and Chris Rydalch to help. This is a WIP document to explain those concepts in plain English.

Chris went above and beyond though. He had a think and went and made the component builder workflow in lops. It's really really good, you can just skip this entire guide and put a component builder in your lops scene. He's also made a great video to go with it, set aside an hour and watch, you'll thank me for it.

https://youtu.be/EgTqz6y_oAs

Basic asset

The most simple definition of an asset would be a 3D mesh. Lets assume we want something slightly more complex than that, and want an asset that contains a mesh and a material.

As such we need a few things:

  • A mesh
  • A material
  • A way to assign that material to a mesh.

On top of these, we'll likely need a few other things:

  • Names for the mesh and the material
  • Scene hierarchy locations for the mesh and material
  • On-disk location(s) to save this asset.

The first is self evident, the others need a little explaining.

Scene hierarchy location

Example scene hierarchy in Houdini (Solaris), Maya Outliner, Katana, USDView

Scene hierarchy simply refers to where in a tree view of your asset the various bits live. This tree view goes by several names in different packages, but should be immediately recognizable for your preferred app:

  • Maya Outliner, or...
  • Unreal World Outliner
  • Katana Scene Graph
  • Unity Hierarchy window
  • Max Scene Explorer
  • C4D Objects Tab
  • Blender Outliner/Scene Collection
  • Houdini Tree View

Some of those applications will only have 3d objects in the hierarchy, things like materials and material nodes will live somewhere else. In USD nothing is hidden, so meshes, materials, render nodes are all visible in this hierarchy, called the Scene Graph.

We should be able to say 'the mesh will be found at...

/Assets/myAsset/geometry/mesh 

...and the material at...

/Assets/myAsset/materials/mymaterial

...for example. Those locations and names are up to you, but they need to be defined.

On disk location(s)

We need to say where the file is saved, that's intuitive enough. But why would we need to define multiple save locations?

The short answer: USD allows you to say up front, 'the bucket asset lives at BucketAsset.usd', but under the hood BucketAsset.usd might point to 2 other USD files, BucketMesh.usd and BucketSurfacing.usd.

Modelling and surfacing save to their respective files, downstream departments just load the high level asset, and it all works.

The longer answer:

Production pipeline diagrams will show a nice straight line between modelling and surfacing. It implies modelling save a file, it flows down to surfacing, they save a material on top of the file, it flows down to the next department.

The reality is the departments work in parallel, publishing at at different rates. Surfacing might start work on a material weeks before a model is ready, in other cases modelling might have many assets published, and need to see it in shots for context. Using the simplistic pipeline either surfacing couldn't start anything until models are ready, and modelling could never see their work until surfacing published a material for every mesh.

But what if you could break that? What if you could specify a location on disk for modelling to save their work, another for surfacing, and have a third location that combines the two? That way modelling could save as often as they want, surfacing could even start before modelling, and the asset will always get the combo of both.

In USD, it's possible to break an asset into layers, and define different save locations for each layer. In this case we'll define the geometry in one layer which is our final output, save the material into a different layer with its own save location, and import it to the final asset.

Refined asset steps

Let's go through these steps again in more detail:

  • Create a 'main' layer, and store the mesh in it and a hierarchy location (called a primitive path or prim path in USD)
  • Create a layer, define a material in it and a save location
  • Reference the material layer into the main layer and set its prim path
  • Assign the material
  • Write a USD file

Implementation and output

Here's how that looks in Houdini's Solaris. Solaris represents USD operations as nodes, you can see this represents a flowchart of the steps we just defined pretty closely:

It creates a layer to load a model, defines another layer for materials and sets a save location, combines the layers, assigns materials, saves a file.

The end result of this is a few usd files on disk. USD can be saved as binary (the default) or plain text, here's the plain text result with some highlighting for the important bits:

Here's the same in a collapsed text box if you want to copy/paste into an editor and have a play:

BucketAsset.usda

JSON
#usda 1.0
(
    endTimeCode = 1
    framesPerSecond = 24
    metersPerUnit = 1
    startTimeCode = 1
    subLayers = [
        @./Bucket.usdc@
    ]
    timeCodesPerSecond = 24
    upAxis = "Y"
)

over "BucketAsset"
{
    def "Materials" (
        append references = @./Materials/BucketMaterials.usda@</BucketAsset/Materials>
    )
    {
    }

    over "Render"
    {
        over "Bucket"
        {
            rel material:binding = </BucketAsset/Materials/BucketMat>
        }

        over "Rope"
        {
            rel material:binding = </BucketAsset/Materials/BucketMat>
        }
    }
}
#usda 1.0
(
    endTimeCode = 1
    framesPerSecond = 24
    metersPerUnit = 1
    startTimeCode = 1
    subLayers = [
        @./Bucket.usdc@
    ]
    timeCodesPerSecond = 24
    upAxis = "Y"
)

over "BucketAsset"
{
    def "Materials" (
        append references = @./Materials/BucketMaterials.usda@</BucketAsset/Materials>
    )
    {
    }

    over "Render"
    {
        over "Bucket"
        {
            rel material:binding = </BucketAsset/Materials/BucketMat>
        }

        over "Rope"
        {
            rel material:binding = </BucketAsset/Materials/BucketMat>
        }
    }
}

That file refers to BucketMaterials.usda, you'll find that below. You can see it stores all the nodes that make up the shading network, and the connections between the nodes.

BucketMaterials.usda

JSON
#usda 1.0
(
    metersPerUnit = 1
    upAxis = "Y"
)

def Xform "BucketAsset"
{
    def Xform "Materials"
    {
        def Material "BucketMat"
        {
            token outputs:displacement.connect = </BucketAsset/Materials/BucketMat/BucketUSDPreview.outputs:displacement>
            token outputs:karma:displacement.connect = </BucketAsset/Materials/BucketMat/BucketMat_displace.outputs:displacement>
            token outputs:karma:surface.connect = </BucketAsset/Materials/BucketMat/BucketMat_surface.outputs:surface>
            token outputs:surface.connect = </BucketAsset/Materials/BucketMat/BucketUSDPreview.outputs:surface>

            def Shader "BucketMat_surface"
            {
                uniform token info:implementationSource = "sourceAsset"
                uniform asset info:sourceAsset = @opdef:/Vop/principledshader::2.0?SurfaceVexCode@
                int inputs:baseBumpAndNormal_enable = 1
                vector3f inputs:basecolor = (1, 1, 1)
                asset inputs:basecolor_texture = @../Textures/repacked_BucketWithRopeHandle/repacked_BucketWithRopeHandle_UV_%(UDIM)d_BaseColor.png@
                int inputs:basecolor_useTexture = 1
                asset inputs:baseNormal_texture = @../Textures/repacked_BucketWithRopeHandle/repacked_BucketWithRopeHandle_UV_%(UDIM)d_Normal.png@
                float inputs:metallic = 1
                asset inputs:metallic_texture = @../Textures/repacked_BucketWithRopeHandle/repacked_BucketWithRopeHandle_UV_%(UDIM)d_Metallic.png@
                int inputs:metallic_useTexture = 1
                float inputs:rough = 1
                asset inputs:rough_texture = @../Textures/repacked_BucketWithRopeHandle/repacked_BucketWithRopeHandle_UV_%(UDIM)d_Roughness.png@
                int inputs:rough_useTexture = 1
                token outputs:surface
            }

            def Shader "BucketMat_displace"
            {
                uniform token info:implementationSource = "sourceAsset"
                uniform asset info:sourceAsset = @opdef:/Vop/principledshader::2.0?DisplacementVexCode@
                int inputs:dispTex_enable = 1
                float inputs:dispTex_scale = 0.005
                asset inputs:dispTex_texture = @../Textures/repacked_BucketWithRopeHandle/repacked_BucketWithRopeHandle_UV_%(UDIM)d_Height.png@
                token outputs:displacement
            }

            def Shader "BucketUSDPreview"
            {
                uniform token info:id = "UsdPreviewSurface"
                color3f inputs:diffuseColor.connect = </BucketAsset/Materials/BucketMat/usduvtextureBaseColor.outputs:rgb>
                float inputs:metallic = 1
                float inputs:metallic.connect = </BucketAsset/Materials/BucketMat/usduvtextureMetallic.outputs:r>
                normal3f inputs:normal.connect = </BucketAsset/Materials/BucketMat/usduvtextureNormal.outputs:rgb>
                float inputs:roughness.connect = </BucketAsset/Materials/BucketMat/usduvtextureRoughness.outputs:r>
                token outputs:displacement
                token outputs:surface
            }

            def Shader "usduvtextureBaseColor"
            {
                uniform token info:id = "UsdUVTexture"
                asset inputs:file = @../Textures/repacked_BucketWithRopeHandle/repacked_BucketWithRopeHandle_UV_1001_BaseColorA.png@
                float2 inputs:st.connect = </BucketAsset/Materials/BucketMat/usdprimvarreader_st.outputs:result>
                vector3f outputs:rgb
            }

            def Shader "usdprimvarreader_st"
            {
                uniform token info:id = "UsdPrimvarReader_float2"
                token inputs:varname = "st"
                float2 outputs:result
            }

            def Shader "usduvtextureMetallic"
            {
                uniform token info:id = "UsdUVTexture"
                asset inputs:file = @../Textures/repacked_BucketWithRopeHandle/repacked_BucketWithRopeHandle_UV_1001_Metallic.png@
                float2 inputs:st.connect = </BucketAsset/Materials/BucketMat/usdprimvarreader_st.outputs:result>
                float outputs:r
            }

            def Shader "usduvtextureRoughness"
            {
                uniform token info:id = "UsdUVTexture"
                asset inputs:file = @../Textures/repacked_BucketWithRopeHandle/repacked_BucketWithRopeHandle_UV_1001_Roughness.png@
                float2 inputs:st.connect = </BucketAsset/Materials/BucketMat/usdprimvarreader_st.outputs:result>
                float outputs:r
            }

            def Shader "usduvtextureNormal"
            {
                uniform token info:id = "UsdUVTexture"
                asset inputs:file = @../Textures/repacked_BucketWithRopeHandle/repacked_BucketWithRopeHandle_UV_1001_Normal.png@
                float2 inputs:st.connect = </BucketAsset/Materials/BucketMat/usdprimvarreader_st.outputs:result>
                vector3f outputs:rgb
            }
        }
    }
}
#usda 1.0
(
    metersPerUnit = 1
    upAxis = "Y"
)

def Xform "BucketAsset"
{
    def Xform "Materials"
    {
        def Material "BucketMat"
        {
            token outputs:displacement.connect = </BucketAsset/Materials/BucketMat/BucketUSDPreview.outputs:displacement>
            token outputs:karma:displacement.connect = </BucketAsset/Materials/BucketMat/BucketMat_displace.outputs:displacement>
            token outputs:karma:surface.connect = </BucketAsset/Materials/BucketMat/BucketMat_surface.outputs:surface>
            token outputs:surface.connect = </BucketAsset/Materials/BucketMat/BucketUSDPreview.outputs:surface>

            def Shader "BucketMat_surface"
            {
                uniform token info:implementationSource = "sourceAsset"
                uniform asset info:sourceAsset = @opdef:/Vop/principledshader::2.0?SurfaceVexCode@
                int inputs:baseBumpAndNormal_enable = 1
                vector3f inputs:basecolor = (1, 1, 1)
                asset inputs:basecolor_texture = @../Textures/repacked_BucketWithRopeHandle/repacked_BucketWithRopeHandle_UV_%(UDIM)d_BaseColor.png@
                int inputs:basecolor_useTexture = 1
                asset inputs:baseNormal_texture = @../Textures/repacked_BucketWithRopeHandle/repacked_BucketWithRopeHandle_UV_%(UDIM)d_Normal.png@
                float inputs:metallic = 1
                asset inputs:metallic_texture = @../Textures/repacked_BucketWithRopeHandle/repacked_BucketWithRopeHandle_UV_%(UDIM)d_Metallic.png@
                int inputs:metallic_useTexture = 1
                float inputs:rough = 1
                asset inputs:rough_texture = @../Textures/repacked_BucketWithRopeHandle/repacked_BucketWithRopeHandle_UV_%(UDIM)d_Roughness.png@
                int inputs:rough_useTexture = 1
                token outputs:surface
            }

            def Shader "BucketMat_displace"
            {
                uniform token info:implementationSource = "sourceAsset"
                uniform asset info:sourceAsset = @opdef:/Vop/principledshader::2.0?DisplacementVexCode@
                int inputs:dispTex_enable = 1
                float inputs:dispTex_scale = 0.005
                asset inputs:dispTex_texture = @../Textures/repacked_BucketWithRopeHandle/repacked_BucketWithRopeHandle_UV_%(UDIM)d_Height.png@
                token outputs:displacement
            }

            def Shader "BucketUSDPreview"
            {
                uniform token info:id = "UsdPreviewSurface"
                color3f inputs:diffuseColor.connect = </BucketAsset/Materials/BucketMat/usduvtextureBaseColor.outputs:rgb>
                float inputs:metallic = 1
                float inputs:metallic.connect = </BucketAsset/Materials/BucketMat/usduvtextureMetallic.outputs:r>
                normal3f inputs:normal.connect = </BucketAsset/Materials/BucketMat/usduvtextureNormal.outputs:rgb>
                float inputs:roughness.connect = </BucketAsset/Materials/BucketMat/usduvtextureRoughness.outputs:r>
                token outputs:displacement
                token outputs:surface
            }

            def Shader "usduvtextureBaseColor"
            {
                uniform token info:id = "UsdUVTexture"
                asset inputs:file = @../Textures/repacked_BucketWithRopeHandle/repacked_BucketWithRopeHandle_UV_1001_BaseColorA.png@
                float2 inputs:st.connect = </BucketAsset/Materials/BucketMat/usdprimvarreader_st.outputs:result>
                vector3f outputs:rgb
            }

            def Shader "usdprimvarreader_st"
            {
                uniform token info:id = "UsdPrimvarReader_float2"
                token inputs:varname = "st"
                float2 outputs:result
            }

            def Shader "usduvtextureMetallic"
            {
                uniform token info:id = "UsdUVTexture"
                asset inputs:file = @../Textures/repacked_BucketWithRopeHandle/repacked_BucketWithRopeHandle_UV_1001_Metallic.png@
                float2 inputs:st.connect = </BucketAsset/Materials/BucketMat/usdprimvarreader_st.outputs:result>
                float outputs:r
            }

            def Shader "usduvtextureRoughness"
            {
                uniform token info:id = "UsdUVTexture"
                asset inputs:file = @../Textures/repacked_BucketWithRopeHandle/repacked_BucketWithRopeHandle_UV_1001_Roughness.png@
                float2 inputs:st.connect = </BucketAsset/Materials/BucketMat/usdprimvarreader_st.outputs:result>
                float outputs:r
            }

            def Shader "usduvtextureNormal"
            {
                uniform token info:id = "UsdUVTexture"
                asset inputs:file = @../Textures/repacked_BucketWithRopeHandle/repacked_BucketWithRopeHandle_UV_1001_Normal.png@
                float2 inputs:st.connect = </BucketAsset/Materials/BucketMat/usdprimvarreader_st.outputs:result>
                vector3f outputs:rgb
            }
        }
    }
}

The mesh geometry is stored in Bucket.usdc, a binary format, here a download link:

Bucket.usdc

While this could be stored in plain text as well, there's often no need for it, as its unlikely to be looked at by humans. Binary is better for size on disk and loading speed.

Adding Render and Proxy meshes with Purpose

Most studios want an asset available in various formats. A final quality one for rendering, a low res one for layout for example.

USD allows this by tagging meshes with a 'purpose'. USD allows 3 kinds of purpose out of the box:

  • Render
  • Proxy
  • Guide

As implied render is final hero geometry, proxy is a lightweight standin, guide might be for anim controls for example.

So to extend this setup, we'll define another layer, load the proxy mesh into there. We then combine the hero and low-res meshes together into a single heirarchy, assign the purpose tag to the hero and low res meshes, and assign material as before:

Here's the Solaris overview of that:

A sublayer is one of several ways of combining hierarchies of things. It does a 'dumb' merge, so it just takes all the stuff from each of the inputs and throws them together.

In this gif you can see one node has a hierarchy under /BucketAsset/Render,the other under /BucketAsset/Proxy, the sublayer combines so that we end up with a tree with both Proxy and Render under /BucketAsset:

With the render and proxy models sitting together in the hierarchy, they get their purpose tag's set.

The rest is as same as before, create a layer to bring in the materials, assign, write out a file.

In terms of the usd file itself the changes are pretty straightforward; it now loads in 2 meshes via the sublayer command, and sets the purpose tags:

Here's the full usda file:

BucketAssetWithProxy.usda

JSON
#usda 1.0
(
    endTimeCode = 1
    framesPerSecond = 24
    metersPerUnit = 1
    startTimeCode = 1
    subLayers = [
        @./BucketProxy.usdc@,
        @./Bucket.usdc@
    ]
    timeCodesPerSecond = 24
    upAxis = "Y"
)

over "BucketAsset"
{
    over "Proxy"
    {
        uniform token purpose = "proxy"

        over "Bucket"
        {
            rel material:binding = </BucketAsset/Materials/BucketMat>
        }

        over "Rope"
        {
            rel material:binding = </BucketAsset/Materials/BucketMat>
        }
    }

    over "Render"
    {
        uniform token purpose = "render"

        over "Bucket"
        {
            rel material:binding = </BucketAsset/Materials/BucketMat>
            rel proxyPrim = </BucketAsset/Proxy/Bucket>
            uniform token purpose = "render"
        }

        over "Rope"
        {
            rel material:binding = </BucketAsset/Materials/BucketMat>
            rel proxyPrim = </BucketAsset/Proxy/Rope>
            uniform token purpose = "render"
        }
    }

    def "Materials" (
        append references = @./Materials/BucketMaterials.usda@</BucketAsset/Materials>
    )
    {
    }
}
#usda 1.0
(
    endTimeCode = 1
    framesPerSecond = 24
    metersPerUnit = 1
    startTimeCode = 1
    subLayers = [
        @./BucketProxy.usdc@,
        @./Bucket.usdc@
    ]
    timeCodesPerSecond = 24
    upAxis = "Y"
)

over "BucketAsset"
{
    over "Proxy"
    {
        uniform token purpose = "proxy"

        over "Bucket"
        {
            rel material:binding = </BucketAsset/Materials/BucketMat>
        }

        over "Rope"
        {
            rel material:binding = </BucketAsset/Materials/BucketMat>
        }
    }

    over "Render"
    {
        uniform token purpose = "render"

        over "Bucket"
        {
            rel material:binding = </BucketAsset/Materials/BucketMat>
            rel proxyPrim = </BucketAsset/Proxy/Bucket>
            uniform token purpose = "render"
        }

        over "Rope"
        {
            rel material:binding = </BucketAsset/Materials/BucketMat>
            rel proxyPrim = </BucketAsset/Proxy/Rope>
            uniform token purpose = "render"
        }
    }

    def "Materials" (
        append references = @./Materials/BucketMaterials.usda@</BucketAsset/Materials>
    )
    {
    }
}

Adding mesh variations

It's pretty common in a production to find assets like car01 car02 car03, tree01 tree02 tree03 etc. Most of the time these are defined as unique assets, everyone assumes this is how it has to be.

With USD however, you could have a single 'car' asset, or a single 'tree' asset, and have the ability to choose from a range of cars or trees. These are called variants.

Variants go beyond just pointing to different models on disk though. You could also have variants that only swap between materials (eg clean car vs a dirty car), or get really tricky and selectively hide and show parts of a mesh within a variant.

'So what' you might say, 'we can do that in [insert 3d app] fine'. You might also say 'separate asset definitions are good enough'. The big win here is once again this is available to all 3d apps that can talk USD. So while this asset might be authored in Houdini, the layout department can be loading cars and trees in maya, and swapping between different variants. And then further downstream again in Katana, lighters can access and swap between variants if there's a last minute change, without having to go all the way back to layout, or modelling, or a callsheet for the assets belonging to a shot or set.

Here's the Solaris method for adding a geometry variant:

In this example Mike has taken the bucket asset, hidden the handle, and made that a variant. What's cool about this method is USD will be smart enough not to save the entire bucket to disk with the handle, then entire bucket again without a handle, it's smart enough to just store the difference between the models. Not impressive with a bucket, but that could save huge amounts of memory and disk space for massive assets with relatively small changes between variants.

Adding level of detail

What if you need more than just a highres ('render') and lowres ('proxy') version of your asset? Variants can be used for this too. While the previous example was a swap between a handle and no-handle version of bucket, (and we might name that variant 'handle'), we could have a a variant selection called 'lod', and put high/medium/low res meshes into those variants.

The Solaris network below is showing off by procedurally creating those mesh reductions, but the steps involved would be the same for any other app or pipeline:

  • take your highres mesh
  • create medium and low res reductions (as many as you feel you require)
  • define a variant called 'lod' (or whatever name you want)
  • attach those reductions to the variant

This network goes the extra mile and also defines lod's for both the render and proxy purposes. You could argue you don't need those, but hey, choice is good right?

Adding material variation

And here we go all the way, setting material variations as well as model variations. You can see the network is getting pretty baroque, but the base idea is the same:

  • bring in the mesh
  • create the LOD's
  • create several materials
  • assign materials to lods
  • create a variant set that lets you choose between materials and lods

Versioning

A modeller publishes v027 of a chair. A surfacing artist publishes v12 of a chair material. A layout artist pulls a chair in back when it was v011, and surfacing hasn't started, publishes a set as v002. Then lighting get the set and are asked what version of the chair they're using, and make sure to skip v08 of the surfacing, but use the latest chair.

This nightmare of versions and departments is all too common in production, which is why USD has the concept of 'which asset should I load' built into its core.

In an ideal scenario a studio's production system (Shotgun, f-track, internal systems) will keep track of which versions are available for any given asset. USD files themselves can talk directly to that production database, so that collections of USD elements don't need paths on disk, just the right way to ask the database 'which version should I load?'. A studio could choose to have all assets default to the latest available, or to a set of known 'approved' assets, or whatever other configuration needed.

The part of USD that deals with this is called the Asset Resolver, and while powerful when implemented properly, could bring a studio to its knees if done badly. Imagine a scene involving a forest next to a city; when the shot is loaded it'll suddenly hit the production database with hundreds and thousands of queries. These need to run in parallel and quickly, so the tech requirements on an Asset Resolver are high.

In its vanilla form USD ships with a simple Asset Resolver; simply one that points to regular paths on disk. If you haven't take the plunge to implement an Asset Resolver, what can you do?

One possible option is simlinks. As modellers publish chair_geo_v01.usd, chair_geo_v02.usd, the publish code creates chair_geo.usd, which is a simlink to the latest version. That way a layout artist downstream can just point to it, and as the modellers update, they too will get the update.

Sets

Sets in a simple form can be just a combination of other USD files. Previously we used sublayers to do a 'dumb' combination of files, but for a set we would want more control. You wouldn't expect all the items for a bedroom set to just all be sitting in a flat hierarchy at the top of the outliner, you'd want items organised into logical groups.

References give you this functionality to organise, so you can reference a usd file on disk like you would do with a maya reference, but you have the ability to only refer to sub-sections of that file if you want, and also to be explicity and say 'this will be parented beneath /bedroom/wardrobe/topshelf/'

A simple set could look like this:

Which under the hood is just a set of references to usd files on disk, and their transforms:

set.usda

JSON
#usda 1.0
(
    endTimeCode = 1
    framesPerSecond = 24
    metersPerUnit = 1
    startTimeCode = 1
    timeCodesPerSecond = 24
    upAxis = "Y"
)

def Xform "set" (
    kind = "group"
)
{
    def "PigAsset" (
        kind = "component"
        add payload = @./PigAsset.usda@
    )
    {
        matrix4d xformOp:transform = ( (0.33999316950907743, 0, -0.22122418961586124, 0), (0, 0.4056297540664673, 0, 0), (0.22122418961586127, 0, 0.33999316950907743, 0), (-1.3854224424692467, 0.3093985690746131, 0.11506395521922762, 1) )
        uniform token[] xformOpOrder = ["xformOp:transform"]
    }

    def "BoxAsset" (
        kind = "component"
        add payload = @./BoxAsset.usda@</BoxAsset>
    )
    {
        matrix4d xformOp:transform = ( (0.30488509121989554, 0, 0, 0), (0, 0.30488509121989554, 0, 0), (0, 0, 0.30488509121989554, 0), (0.1113539646492816, 0, -2.087946345947296, 1) )
        uniform token[] xformOpOrder = ["xformOp:transform"]
    }

    def Xform "props" (
        kind = "group"
    )
    {
        matrix4d xformOp:transform = ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) )
        uniform token[] xformOpOrder = ["xformOp:transform"]

        def Xform "buckets" (
            kind = "group"
        )
        {
            def "BucketAsset" (
                kind = "component"
                add payload = @./BucketAsset.usda@</BucketAsset>
            )
            {
                matrix4d xformOp:transform = ( (0.5121324183710805, 0, 0.8589065060024802, 0), (0, 1, 0, 0), (-0.8589065060024802, 0, 0.5121324183710805, 0), (-0.5144985079553781, 6.454063949945521e-9, -1.0041869573334419, 1) )
                uniform token[] xformOpOrder = ["xformOp:transform"]
            }

            def "BucketAsset2" (
                kind = "component"
                add payload = @./BucketAsset.usda@</BucketAsset>
            )
            {
                matrix4d xformOp:transform = ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) )
                uniform token[] xformOpOrder = ["xformOp:transform"]
            }
        }
    }
}
#usda 1.0
(
    endTimeCode = 1
    framesPerSecond = 24
    metersPerUnit = 1
    startTimeCode = 1
    timeCodesPerSecond = 24
    upAxis = "Y"
)

def Xform "set" (
    kind = "group"
)
{
    def "PigAsset" (
        kind = "component"
        add payload = @./PigAsset.usda@
    )
    {
        matrix4d xformOp:transform = ( (0.33999316950907743, 0, -0.22122418961586124, 0), (0, 0.4056297540664673, 0, 0), (0.22122418961586127, 0, 0.33999316950907743, 0), (-1.3854224424692467, 0.3093985690746131, 0.11506395521922762, 1) )
        uniform token[] xformOpOrder = ["xformOp:transform"]
    }

    def "BoxAsset" (
        kind = "component"
        add payload = @./BoxAsset.usda@</BoxAsset>
    )
    {
        matrix4d xformOp:transform = ( (0.30488509121989554, 0, 0, 0), (0, 0.30488509121989554, 0, 0), (0, 0, 0.30488509121989554, 0), (0.1113539646492816, 0, -2.087946345947296, 1) )
        uniform token[] xformOpOrder = ["xformOp:transform"]
    }

    def Xform "props" (
        kind = "group"
    )
    {
        matrix4d xformOp:transform = ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) )
        uniform token[] xformOpOrder = ["xformOp:transform"]

        def Xform "buckets" (
            kind = "group"
        )
        {
            def "BucketAsset" (
                kind = "component"
                add payload = @./BucketAsset.usda@</BucketAsset>
            )
            {
                matrix4d xformOp:transform = ( (0.5121324183710805, 0, 0.8589065060024802, 0), (0, 1, 0, 0), (-0.8589065060024802, 0, 0.5121324183710805, 0), (-0.5144985079553781, 6.454063949945521e-9, -1.0041869573334419, 1) )
                uniform token[] xformOpOrder = ["xformOp:transform"]
            }

            def "BucketAsset2" (
                kind = "component"
                add payload = @./BucketAsset.usda@</BucketAsset>
            )
            {
                matrix4d xformOp:transform = ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) )
                uniform token[] xformOpOrder = ["xformOp:transform"]
            }
        }
    }
}

Note that I've parented the entire thing under a top level /set folder. This is important if we want to load this set into shots later. In the same way that assets were referenced into the set, the set will be referenced into shots. Reference can only point to a single primitive. So if there are multiple top level nodes, a reference will only display one.

Structure? Supersets? Payloads? Naming schemes?

Shots

A shot in turn references in the set, and and whatever other per shot stuff required (cameras, lights, animation caches etc)

You can see that there's an interesting inversion of complexity; the shot itself might contain a big complex set, lots of materials, animation and whatnot, but the usd file itself is very minimal and clean:

shot.usda

JSON

#usda 1.0
(
    endTimeCode = 1
    framesPerSecond = 24
    metersPerUnit = 1
    startTimeCode = 1
    timeCodesPerSecond = 24
    upAxis = "Y"
)

def "set" (
    prepend references = @./set.usda@</set>
)
{
}

def Xform "camera"
{
    def "shotcam" (
        prepend references = @./shotcam.usda@</cameras>
    )
    {
    }
}

def Xform "anim"
{
    def "dave" (
        prepend references = @./dave.usd@
    )
    {
    }
}

#usda 1.0
(
    endTimeCode = 1
    framesPerSecond = 24
    metersPerUnit = 1
    startTimeCode = 1
    timeCodesPerSecond = 24
    upAxis = "Y"
)

def "set" (
    prepend references = @./set.usda@</set>
)
{
}

def Xform "camera"
{
    def "shotcam" (
        prepend references = @./shotcam.usda@</cameras>
    )
    {
    }
}

def Xform "anim"
{
    def "dave" (
        prepend references = @./dave.usd@
    )
    {
    }
}

References vs sublayers?

Multi shot workflow

Sequence as master file, then shot overrides?

Idea of grouping similar facing shots into 'scenes', scenes can refer to their own set.

Shot overrides

Stuff about shot overrides here, specific for departments, eg charfx on top of anim caches, fx replacing elements in a set.

'I want to move the chair in these 30 shots, but move the table in these 8 shots. Do I move it for the majority, then um-move it for the rest of them, or split the set to have this in these shots, and that in those shots' etc.

Lighting

How does this dovetail in with the rest of it all? Are we replacing or overriding?