Practical USD: Point Instancers, Kinds and More [4/4] (Patreon)
Content
We covered USD's instanceable keyword in the last article and at the time I mentioned USD supports another form of instancing: point instancers.
Point instancers are used for scattering a large amount of geometry to points on another mesh. This workflow is even more memory efficient than regular instances. Let's look at how it's done in USD Composer.
On the left side toolbar there's a brush tool that allows us to "paint" instances. Anyone who has used Unreal Engine should be familiar with this type of workflow. For the purposes of this article we'll put together a very quick demo, then look at the resulting USD code and memory usage.
First, let's create a plane which will act as the surface we paint our instances onto.
After selecting the brush tool we can load any mesh we want to be instanced. In this case I picked knight.usd from our chess set. We can then set various parameters including scale randomization, brush size, scatter density, etc.
One stroke later, we have a herd of horses of random size and rotation:
Here's what the relevant section of the resulting USDA file looks like. All of the position, size and rotation data has been stored in arrays under a PointInstancer definition:
Let's check in on memory efficiency. I created a quick scene with 105 knight instances using a point instancer.
I then made another scene with 105 instanceable copies. Finally, I removed all the instances from that file, making it into 105 unique copies. Here's how the memory usage in usdview stacks up:
It's essential to use some form of instancing when working with large amounts of copied geometry. The unique copies scene is using an unmanageably high amount of RAM (in this case, around 3GB). The scene with normal instances is vastly superior. However, we can see here that using the point instancer is even more efficient. A point instancer isn't the right solution for every scene as it removes some level of control. It's intended for populating huge scenes with millions, billions, or even trillions of copies. At that level, the memory savings become more valuable, while in a tiny example scene like ours 50-100MB of RAM hardly matters.
Moving on, another USD term you might have run into is kind. This keyword is used to define any given geometry's function in the overall hierarchy. There are four "kinds" of geometry: groups, assemblies, components, and subcomponents. These names are fairly descriptive, but as an example let's say we have two tree models, a maple and an oak. We could create a kind hierarchy as follows:
Trees (group)
∟ Maple (assembly)
∟ Trunk (component)
∟ Leaves (component)
∟ Leaf (subcomponent)
∟ Leaf (subcomponent)
∟ Oak (assembly)
∟ Trunk (component)
∟ Leaves (component)
∟ Leaf (subcomponent)
∟ Leaf (subcomponent)
Note that assemblies can contain groups, but components can only contain subcomponents. A subcomponent can contain nothing else.
Another method of categorization in USD is purpose. Defining the purpose of a prim allows us to control how it is displayed. There are four purposes: default, guide, proxy and render. The proxy purpose can be used to identify low resolution geometry useful for quick feedback while working on the scene, but which wouldn't show up in renders. A mesh with a render purpose will display in the final render. All USD-enabled apps allow us to choose which purposes are currently displayed in the viewport. Here's usdview:
Before moving on to our final topic, let's quickly cover sublayers. A sublayer is another way of importing or merging outside data into our scene. We already talked about accomplishing this using references or payloads to load a mesh stored on disk. What a reference does is append itself to our existing hierarchy. A sublayer just stacks on top of anything else already present.
Here's an example of references (left) versus sublayers (right). In this case, the references can be loaded as nested into other prims: we created a "pieces" Xform, put the knight in that, then brought the pawn in as a child of the knight.
Sublayers are much simpler: they are read into the root of the stage and stacked on top of each other, as shown here in usdview (again, references on the left and sublayers on the right):
Lastly, let's touch on something that is good to be aware of but not really necessary to interact with in much detail: opinions.
When you have a massive number of layers all coming together to form a final stage, you might run into issues of which version of any given prim gets precedence.
USD determines this via opinion strength: the strongest opinion wins. The official ranking of opinion strength, from strongest to weakest, is LIVRPS: local, inherits, variants, references, payloads, specializes.
This can get very complicated and isn't worth wrestling with from an end-user/artist perspective. Most of us will never encounter issues related to opinion strength. The topic is probably only important to fully dig into if you're a developer setting up a pipeline for a team of artists to work within. Artists themselves should not be required to consider if some value declared "locally" is undesirably favored over the value from a variant. However, it's useful to understand the concept as it might help debugging or troubleshooting a stage that doesn't seem to be coming together as expected.
Conclusion
So there you have it! Over the course of four articles we covered everything you need to know to become conversant with USD. Prims, layers, references, variants, and much more... USD is enormously complex and in many ways we only scratched the surface. I hope this series helped demystify the terminology and concepts that are core to USD and gave you a good starting point to make the framework work for you!