Variants
Exploring OpenUSD's variants feature, enabling multiple lightweight versions in a single asset
Graphics models can often benefit from having a few different configurations that share common values. A t-shirt model could have different materials. A tree model might have a version with green leaves, with fall colors and without leaves.
Variants provide a way to construct content that has these types of differences while reusing the common data, like base meshes, materials and textures.
A USD Variant is not a prim type, it's defined using metadata on a prim. This means any prim can have one or more variant sets if they are useful.
How Does It Work?
Let's look at a simple but not very useful example first, just to understand the feature. For this article I'm going to put example content in a github repo, in the folder variants.
For a first simple example let's make a prim that can be a sphere or a cube. In text format this would look something like the following.
The complete file in the github repo can be loaded in usdview
or your favorite USD enabled tool.
We've defined a variant set called shape
. That set of variants has two options, named box
and sphere
. Inside each of those variants I've create a new Mesh with the geometry inside it.
When you first open this file you'll get nothing rendered. The stage will contain only one prim, /Model
. It has no contents. This is because we didn't set a default variant, and all the content in Model is inside the shape
variant set. With no variant chosen, Model has nothing in it.
If we select the box
variant in the shape
variant set (in usdview you do this in the MetaData tab) then a new prim will appear at /Model/BoxMesh
and a cube will render. If you switch to sphere
then BoxMesh disappears and now there is a prim at /Model/SphereMesh
and a sphere will render.
Variant sets can have a default selection, in this case to avoid having nothing in /Model
. Most authoring tools will set that up for you. In text format it would change the previous example to add this variants
bit.
But, That's Useless
Good point straw man!
You could easily make a Box and Sphere asset and reference those instead of making a variant. Since nothing is really shared, there isn't much advantage to the example in the first section.
Let's imagine we make the same thing, but in our variant we just turn on and off subdivision. So, the default is a box still, but we can choose a variant that turns the mesh into a subdivision surface. The subdiv will use the same points as the box, so it will subdivide into a sphere and we save all the size of hard coding our sphere. This is still a bit of a tortured example, but it has the advantage of being simple so I'm going with it.
In this file I've moved the CubeMesh out of the variantSet and renamed it to Geo. I used over
to indicate we're overriding Geo
instead of using def
like in the previous example. The box
variant is the same as in the first example. The sphere
variant now enables catmullClark subdivision for the Mesh Geo
. That way it subdivides into a sphere, and we don't need to keep all the sphere geometry in the file.
Still a bit of a contrived example, but I hope this shows the ways variants can actually be useful. If we put a material binding in the variants, then we could choose a material without changing the meshes. If we wanted to add geometry in some cases, we could add new meshes in the variants. You could imagine other creative uses, changing out physics representations, LODs. switching UVs, adding lights, there are tons of potential use cases.
Strong Local Opinions
It's good to be aware of a common pitfall with variant sets. In the model-smaller.usda
example if we had set the subdivisionScheme
outside the variants then the variant would have no effect.
To be clear, this version is broken.
The only difference between this and the working example is that we set subdivisionScheme
in the def Mesh "Geo"
section. If we now load this on a stage we'd find that no matter what our variant selection is, we still get the cube with no subdivision. There are no warnings or errors in this case either. The file is valid, it just doesn't do what we want.
So what went wrong? We got stung by the LIVRPS algorithm. This controls how USD resolves the strongest values on a stage. The strongest opinion is always "Local". Since we set the subdivisionScheme
directly on the Mesh in the same layer that has the "Variant" opinion, that subdivisionScheme
opinion is "Local" and is stronger than the variant.
This can be extra confusing, because it is OK to set the subdivisionScheme
directly on the Mesh if it's in a different file. If we got the Geo by a "Reference" or a "Sublayer" we would not run into this issue. They are weaker than "Variant" in LIVRPS.
This is a bit of an edge case, but people run into it quite often and it can eat a lot of time to debug, so I wanted to call it out.
Multiple Variants
It's possible to have more than one variant set on the same prim. In that case you can make a selection in each variant at the same time. So if we had a shape variant set that chooses box or sphere, and we had a color variant set that chooses red, blue or green, there would be six unique combinations. Red boxes, green spheres; my god, the power.
I modified the model-smaller.usda
file to add a color
variant set. There are a few ways to do something like this. I chose to add a Material and USD Preview Surface shader inside /Model
. That Material is outside the variants in Model
so that only the diffuse color gets changed out in each variant. This way the variant should read clearly, but remember you could change as many parameters as needed, or even define new materials in variants.
The relevant parts of the new file look like this.
This is a lot of text, but the idea is pretty simple. Now there are two variant sets on Model, the old one called shape
and a new one called color
.
We can open this in a USD editor and select different combinations. We can also set the variant selections when referencing into another layer. That's what I did for the image above. Each reference looks something like this in text form.
If we didn't have variants we might need six different files/models/meshes to create these. If we later decided convert the sphere geometry back to polygons we could make that change in one place and it would update everywhere. If these were separate files we'd need to edit three of them to update the sphere.
Conflicting Opinions
One obvious question at this point is, what if I edit the same property in two different variant sets? In that case the strongest variant set wins.
In our simple example with shape
and color
variant sets the strongest one is the one that's earliest in the list of variantSets
on the Model prim.
prepend variantSets = ["shape", "color"]
So, shape
is stronger than color
. If shape
had an opinion on diffuse color it would override the opinion from color
.
This can get more complicated with new variant sets coming from sublayers, references, parent prims with variants that affect the same properties, etc. All kinds of complicated situations could be constructed, and they will all have a consistent result for the strongest opinion, even if that result could be surprising in some cases. My general advice is to avoid trying to get too tricky with variant sets (or fancy composition tricks in general).
Creating one or more variants sets on a prim, even across sublayers, is pretty easy to understand. More complicated structures can definitely work, but if they get hard to reason about they may become thorns in your side in the future.