Advice on relationship targets to prims in external layers needed

Hi! I work at Remedy Entertainment where we’re using USD as our level data format. We’re currently trying to add support for something we call (for lack of a better name) ‘External References’ and I’ve come here looking for some advice as what we tried so far hasn’t really worked.

The way we’ve structured our layers is that we have a top-level layer per level, a bunch of other layers for composable things like areas, character, items, and so on, and then we also have a layer for global data which we want to point to from various layers. (This is simplified but I think will help get the point across)

We export our levels and global data to a runtime format where we identify objects by an id that is essentially a hash of its usd path. During runtime our global data is always loaded and we might have one or more levels loaded as well at the same time.

Now, up until this point, whenever a layer needs to point to any of the global data we’ve done so by manually entering string ids for the data you want to reference. We would however much prefer to point to it via relationship targets (to make it work with other tooling we have) and convert those targets into our ids. The problem we have is that because the id is based on the object’s usd path we can’t bring the global data layer in as a usd reference because that of course changes the object’s path.

So we tried instead to sublayer the global data in. But then we stumbled upon another problem which is that the relationship targets don’t seem to survive the reference composition arc. Let me give a simplified example.

We have items.usda:

#usda 1.0
(
    defaultPrim = "items"
)

def "items"
{
    def "briefcase"
    {
    }
}

player.usda:

#usda 1.0
(
    defaultPrim = "player"
    subLayers = [@items.usda@]
)

def "player"
{
    rel equipped_item = </items/briefcase> 
}

level.usda:

#usda 1.0
(
    defaultPrim = "level"
    subLayers = [@items.usda@]
)

def "level"
{
    def "player" (
        references = @player.usda@
    )
    {
    }
}

If we run the following code:

auto stage = pxr::UsdStage::Open( "level.usda" );

pxr::SdfPathVector items;
stage->GetRelationshipAtPath( pxr::SdfPath( "/level/player.equipped_item" ) ).GetTargets( &items );

Then we get the following warning:

In </level/player.equipped_item>: The relationship target </items/briefcase> from </player.equipped_item> in layer @player.usda@ refers to a path outside the scope of the reference from </level/player>.  Ignoring. (getting targets for relationship </level/player.equipped_item> on stage @level.usda@)

And items is empty.

That makes sense (at least I think so). When composing </player.equipped_item> into </level/player.equipped_item> it doesn’t know that level.usda also sublayers in items.usda and that the path is therefore valid.

Since sublayering didn’t work, our next idea is to bring the global data layer in as usd references but do some sort of re-mapping scheme in our tooling when we export the paths into our ids for our runtime format. But is there a way to make the sublayering work? Or are there any other ideas or advice on how we could structure our data to get the sort of referencing we’re looking for?

I haven’t really thought this through, but have you looked at using “inherits” to get at your global data? Inherit (and specialize) composition arcs have the unique behavior of reaching “outside” the reference to the matching location on the composed stage. So any layer that needs to access global data could have a prim “/locals” that “inherits” from “/globals”. within this layer, you could have your relationships point to “/locals”, which would be empty normally (though who knows, maybe you would want to put some layer-local data there too).

Then on the full stage, you’d either sublayer in the layer with the “/globals” prim with the global settings. Or maybe you’d reference the settings onto “/globals”? I still can’t do composition in my head so I’m not totally sure which (if either) of these approaches would work, but I bet you could come up with something. Then all the layers with relationships pointing to “/locals” would see all the local settings with the “/globals” settings overlaid on top as stronger opinions.

Would that get you what you’re looking for?

Hmm, I’m definitely not an expert on inherits/specializes (we don’t use them at Remedy (yet)) but I don’t think that would do much for the problem.

If I’ve understood you correctly, my previous example would then look something like this? (Note: I renamed items.usda to global.usda)

player.usda:

#usda 1.0
(
    defaultPrim = "player"
)

def "local" (
    inherits = </global>
)
{
}

def "player"
{
    rel equipped_item = </local/briefcase> 
}

level.usda:

#usda 1.0
(
    defaultPrim = "level"
)

# alternatively, this could be brought in as a sublayer
def "global" ( 
    references = @global.usda@ 
)
{
}

def "level"
{
    def "player" (
        references = @player.usda@
    )
    {
    }
}

Unless I’ve misunderstood then I think this has the same problem as adding global.usda as a sublayer to both player.usda and level.usda i.e. that </player.equipped_item> won’t compose through the references arc. Also since the path to the prim we’re targeting in the relationship is different we’d still have to do some kind of re-mapping. I.e. we can’t just hash the path in the relationship since it won’t be the same as the path to the briefcase in global.usda.

One thing I feel I should clarify is that our primary concern isn’t being able to read the data composed from global.usda in the level.usda stage. What we’re mainly interested in is a way for the absolute path that the relationship is composed to in level.usda to be the same absolute path as if the relationship was authored & composed in global.usda. Reading the opinions in global.usda from the level.usda stage would be nice, but mainly for validating that the relationship target(s) targeting a prim in global.usda exists. We would ideally want to prevent users from authoring local opinions in level.usda on the global data.