Instancing

Instancing is based on composition

As described above, composition facilitates content reuse in OpenUSD.

OpenUSD provides instancing as a feature on top of its core composition system.

Instancing is specified by setting the “instanceable” metadata on prims with composition arcs. If “instanceable” resolves to be true, a “prototype” is either retrieved or instantiated. A “prototype” captures the fully composed sub hierarchy at the instanceable prim in a private namespace, invisible to the user. It is identified internally by OpenUSD by hashing the prims’ composition operators as a key.

Note that Prim paths within instances are still addressable via OpenUSD, even though the prototype prims don’t really exist in that location (they exist in the implicitly created prototype, not the instances). OpenUSD will refer to such locations as “instance proxy prims”. This allows clients such as UIs to easily display instance “contents”, without having to perform prototype look ups. OpenUSD also allows “instance proxy” membership in collections.

It is possible to view prototypes in usdview by enabling the “Prototype Prims” option in the “Show” menu on top of the Scene Graph panel.

Opinions directly expressed on descendants of instanceable prims are ignored to preserve the shareability of the prototypes across instances. In other words, the instance proxy prims are read-only or “immutable”.

Only fields of the “instanceable root” prim, such as composition arcs, transforms, primvars, etc. may vary from its underlying “prototype”. See “parameterization” in the “refinement” section below for details.

If no composition arcs are directly authored on an instanceable prim, the prim behaves as if “instanceable” was set to false. A composed prim requires both “instanceable” set to true and direct composition arcs to be authored for a prototype to be generated by composition.

If the instanced prim has variant sets, only instances with the same variant selections will share a prototype. If animation is scattered via referencing on instanced assets, only instances with animation layers and offsets will share a prototype.

Instanceable prim hierarchies can be deinstanced by setting instanceable=False if sparse overrides are needed downstream.

Because the creation of protoypes is performed automatically by OpenUSD, but the choice to do this lies with the user (by authoring the “instanceable” metadata), this style of instancing is referred to as “implicit prototypes” with “explicit instances”.

stage with referenced component assets

stage with instanceable referenced component assets

Assembly of referenced assets

Visualization of implicit prototypes

Instanceable Internal Arcs

Setting the instanceable metadata on prims which hold internal composition arcs, will trigger the creation of implicit prototypes as discussed. For mechanisms to edit or refine instances of the instance, see the “Editing” and “Refining” chapters below.

This style of instancing can be used to model similar approaches in many CAD and DCC applications. A (non-instanceable) prim hierarchy is “internally composed” multiple times onto instanceable prims. In contrast to composing external layers, an internal arc only specifies the target prim path of the reference, omitting the layer.

This approach might be a valuable choice when the goal is “true to source” scene description. It might also be chosen when the referenced data needs to be embedded into the stage.

../../../_images/internal_instancing.svg

There are a number of trade-offs to consider with this approach:

  1. If the same instanceable subhierarchy (asset) is re-defined in multiple layers, OpenUSD will not be able to identify as being identical and create duplicate prototypes. Examples may be assets such as robots in individually generated “assembly cell” USD layers, car parts which should be reused across multiple “car_trim” USD layers or furniture which is reused across multiple “level” USD layers in a BIM model. To the user, this issue can be unexpected, especially if the process that generated the internal composition arcs offered the creation of “instances”.

To avoid this problem, consider approaches such as Instanceable External Arcs, Globally Refinable Instancing for Internal Arcs or Globally Refinable Instancing for External Arcs.

The example below illustrates the issue, showing the “barrel” asset being defined twice, once in the factory and again, in the loading dock.

../../../_images/internal-instancing-two_bundles.svg
  1. Because the target hierarchy is not explicitly namespaced, it can be difficult to understand for consumers which parts of the scene graph are composition sources and which are targets. For example, a user may disable “Barrel_0” in the example below which unexpectedly would also prune the hierarchy under “Barrel_1”.

  2. Especially for small amounts of data, the associated performance improvements may not be worth the increase of the stages’ structural complexity, especially when principles such as encapsulation are taken into account. Also remember that an instanced hierarchy consists of a root prim which is editable and the internal hierarchy of the instance, which is immutable. This means that an instance has to consist of at least two prims - single prim instancing is not possible.

Instanceable External Arcs

Instancing external arcs works similarly. Setting instanceable to “true” on the source prims will trigger the creation of “Prototypes” - implicitly derived from the composition arcs specified by the “instanceable root”.

../../../_images/external_instances.svg

All prims which compose the same layer / prims will now share the same underlying composed “prototype” and the composed asset will become immutable - for mechanisms to edit or refine instances of the asset, see the “Editing” and “Refining” chapters below. For mechanisms to edit or refine instances of the asset, see the “Editing References” chapter below.

The “Asset Interface” concept that is outlined in “Principles of scaleable Asset Structures” describes an asset which is well suited for instancing workflows and provisions an explicit editing namespace to provide a target for downstream edits. Also see the External Asset Libraries for a more comprehensive structure.

Reasons to use Instancing

Performance

As instancing generally enables scalability in renderers through shared geometric representations, it commonly is described as a memory optimization. However, the composed OpenUSD stage is only responsible for resolving the location of values and never has to read the entire geometric representation of the scene into memory.

While scene graph instancing generally improves the time and memory required to compose a stage, it primarily does so via optimization of stage traversal.

Consider a script counting the screws in a warehouse with instanced shelves. The naive solution would traverse the entire composed scene, counting each screw. The instancing-aware solution would instead count the screws in each shelf prototype and reuse that count when encountering an instance of the shelf on the stage. A script operating on a warehouse with hundreds of shelves could replace the traversal of hundreds of subgraphs with a simple cache lookup. This effect can be compounded by nested scene graph instancing which is they key to achieving scene scales such as large factories or cities.

As a consequence, it is recommended to orient scene graph instance structures around traversal cost. Attempting to instance too granular subgraphs may result in more prims and complicate traversal without reaping the performance benefits.

Hierarchical Encapsulation

A prototype is an encapsulated subgraph. Relationships (like material bindings) on geometry cannot refer to prims that exist outside the prototype. Component models often are a good granularity for instancing as they encapsulate their materials and geometry.

Consider that materials also constitute “encapsulated subgraphs” - ie, they are represented by hierarchies of nodegraphs and nodes in the materials namespace. Performance benefits from designing instanceable material networks might be substantial and may even outweigh geometry instancing, which is already optimized by file format level deduplication and renderer level mesh deduplication.

Scene Management

For me it was largely the management of all the prims. Changing collisions over 3k prims is pretty hard crashed often. The point instancer loads much faster

Material Instances

In OpenUSD, Materials are fully encapsulated shader nodegraphs. In production environments, materials graphs may often include a large number of shader nodes which may contribute significantly to the total prim count of a composed stage. Additionally, materials are often referenced from a centralized material library and may be specialized further in the context of the asset.

Shader parameters that are intended to be controlled by downstream consumers of the materials (for example, asset paths for textures) should be “promoted” or “exposed” on the material root prim. This provides a convenient interface to users and ensures that parameterization of the material is consolidated in a single location in the scene graph.

These factors make materials a good use case for scene graph instancing with the potential to reduce overal prim count and speeding up render translation (the renderer already “knows” which materials are duplicates, it doesn’t need to de-duplicate transpiled or compiled shader code to detect them).

../../../_images/material_instances.svg

Editing Instances

Edit Targets

Because instance prototypes are created by OpenUSD implicitly, they can not be edited directly. They are not shown to the user and their naming is not persistent.

However, as prototypes represent the results of fully resolved composition arcs, it is possible to “edit” prototypes by modifying the instances’ arc targets. In other words, if an instanceable prim composes another prim, any change to that prim will be propagated to all instances (either “live” or upon recomposition, depending on the chosen composition arc, see: here ).

In some cases, finding and editing the target might be fairly simple. For example, if an instance only holds a single internal reference arc. In other cases, a prims composition might be very complex or the composed source data may be read only.

For this reason, assets might be authored upstream with “speculative” composition arcs which are intended to serve as edit targets for “instance refinements”. This is described in the “Refining Instances by composition” sections below.

It is also possible to add internal composition arcs later to individual instances to facilitate refinement. This is referred to as refinement “at the point of assembly”.

Creating new prototypes

If prims need to be deleted or re-parented for individual instances, there is (currently) no way to do this non-distructively in OpenUSD. In orther words, it is necessary to edit (rather than refine) the composed source data..

This can be done by copying the original composition target (however simple or complex that may be) and re-compose it onto new instances as needed. This might also present an opportunity to introduce a variant arc to the asset, so that the new variation can be chosen easily for each instance (see “parameterization” below).

Refining Instances

The term “Refining” is chosen here to denote that the following techniques do not distructively modify instances (ie, it is not possible to perform namespace editing such as re-parenting or deleting). Instead, they provide a mechanism for layered refinements such as attribute value changes, material bindings, creation of additional prims or changes in metadata.

Various techniques exist to refine instances. Some of these can be described as intuitive, but others require a deep understanding of OpenUSDs composition mechanism and will benefit from dedicated tooling to improve the user experience.

De-instancing

The simplest approach to instance editing is to set instanceable on specific instances to “False”. If applied in moderation, this might be a perfectly acceptable approach in many situations. Remember that various de-duplication systems are still operating “behind the scenes” to avoid data duplication.

../../../_images/de_instancing.svg
scenario.usda
 1#usda 1.0
 2
 3def Scope "Factory"
 4    (
 5        references = @./factory.usda@
 6    )
 7{
 8    over "AssembledBarrels" () {
 9        # Visibility is inherited from the ancestor prim into the instances
10        token visibility = "invisible"
11    }
12}

By hierarchical inheritance

Instances can be spatially varied because transformation operations in OpenUSD are hierarchical. Properties that have computed hierarchical inheritance are expected to work with instancing. For example, an instance with an “invisible” ancestor should never be imaged, regardless of the visibility state of the “prototype”.

../../../_images/hierarchical_inheritance.svg
scenario.usda
 1#usda 1.0
 2
 3def Scope "Factory"
 4    (
 5        references = @./factory.usda@
 6    )
 7{
 8    over "AssembledBarrels" () {
 9        # Visibility is inherited from the ancestor prim into the instances
10        token visibility = "invisible"
11    }
12}

Properties that don’t have inheritance semantics must be varied through composition arcs to preserve instancing (see “refining instances” below).

By parameterization

There are two ways instance root prims can be parameterized: primvars and variants.

The example below applies parameterization to an instanced asset.

The root prim of an asset will be the first place where a user goes to figure out if prims have discrete variants. Asset structures may enforce naming conventions and the presence of specific variants. For example, it may be expected that assets provide color_variant to describe supported albedo variations.

def Xform "RaceCar" (
        variantSets = ["color_variant"]
    ) {
        variantSet "colorVariant" = {

            "red" { ... }
            "blue" { ... }
            "green" { ... }

        }
    }

Some variation cannot be effectively or efficiently discretized into variants. For these cases, primvars can be used as another form of asset parameterization. Primvars are extra interpolatable parameters primarily for Gprim prims to provide additional data to shading contexts. In OpenUSD, primvars have inheritance semantics and can be authored on parent scopes, including the entrypoint of an instance. Materials can be constructed to read primvars:asset_base_color or other entrypoint primvars. In the event that multiple prims in a hierarchy author the same primvar, keep in mind that child opinions are stronger than parents. Below, we use asset_ as a prefix to avoid namespace collisions.

def Xform "RaceCar" {

    color3f primvars:asset_base_color (
        doc = "primary paint color"
    )

    color3f primvars:asset_accent_color (
        doc = "color of accent stripe"
    )
}

Unless otherwise documented or annotated as internal, variants and primvars authored on an asset entrypoint should be generally considered “public” and safe for downstream contexts to edit and set with an expectation of stability.

../../../_images/parameterization.svg

Both variant selection and primvars on the asset entrypoint are compatible with scene graph instancing. Variations of variant selections will generate new prototypes for downstream contexts while primvars will not. This generally makes parameterization through primvars the lighter choice for single property parameters, providing upfront memory savings at the cost of additional lookups in materials.

Explicit “Refinement Namespaces”

OpenUSD’s prototypes are implicit and read only. However, because they are keyed off of composition, it is possible to construct arcs in a way that provide a namespace (prim) where explicit prototype edits can be placed. Depending on the use case, the specific placement and contents of the namespace might be different. For example, it may contain the entire definition of the asset or simply provide an opportunity for downstream consumers to add refining edits.

In all cases, it is recommended to utilize abstract prims to provide these explicit “editing namespaces”. They are not part of the default traversal and can be easily identified and filtered through OpenUSD’s traversal predicates. Conventiontionally, these prims are named “prototypes” and they are of type “scope”.

“Edit Namespaces” can be applied to external and internal referencing scenarios.

Internally Refinable Instancing

This approach extends “Instanceable Internal Arcs” described above and addresses the issue of being non-explicit in declaring the location where reference targets are found.

Internally refineable instancing is desirable when a subgraph is repeatedly used within a layer (or asset) but there’s no expectation that downstream consumers will want to make any edits to the prototype without deinstancing.

Consider some barrel geometry internally referenced into a factory.

../../../_images/internally_refinable_instances.svg
factory.usda
 1#usda 1.0
 2(
 3    defaultPrim = "Factory"
 4)
 5
 6class Scope "prototypes" () {
 7    def Xform "Barrel" (
 8        kind = "component"
 9    ) {
10        def Xform "Geometry" {
11            def Cylinder "barrel" {}
12        }
13    }
14}
15
16def Scope "Factory" (
17    kind = "assembly"
18) {
19    def Scope "BarrelBundle" (
20        kind = "group"
21    ) {
22        def "Barrel_0" (
23            instanceable = True
24            references = </prototypes/Barrel>
25        ) {
26            token[] xformOpOrder = ["xformOp:translate"]
27            double3 xformOp:translate = (100, 0, 0)
28        }
29        def "Barrel_1" (
30            instanceable = True
31            references = </prototypes/Barrel>
32        ) {
33            token[] xformOpOrder = ["xformOp:translate"]
34            double3 xformOp:translate = (110, 0, 0)
35        }
36    }
37}

Changes to /prototypes/Barrel/Geometry will be reflected in the implicitly generated shared prototype for Barrel_0 and Barrel_1. We describe this pattern as being “internally refinable” because the prototypes can only be refined in the immediate layer stack. If /Factory were to be referenced into another layer stack, /prototypes/Barrel will no longer be available to place edits. Barrel_0 and Barrel_1 can only be edited through deinstancing or adding additional composition arcs. Its construction around the “references” composition arc makes it one of the cheaper patterns of scene graph instancing.

Warning: Some of the trade-offs discussed in Instanceable Internal Arcs still apply, in particular the risk of multiple re-definitions of identical hierarchies.

Locally Refinable Instancing for Internal Arcs

Abstract prims can be nestable underneath an asset’s root prim to bring prototype edit targets along into downstream contexts. However, to remain a live connection, even when referenced in downstream layerstacks, the composition arc needs to be changed from a reference to a “specialize” arc. Specialize arcs remain “live” in any referencing context so any edits that are applied to the prototypes after the asset has been referenced still propagate.

../../../_images/locally_refinable_instancing_for_internal_references.svg
factory.usda
 1#usda 1.0
 2
 3def "Factory_0" (references = @./factory.usda@) {
 4    over "prototypes" () {
 5        over "Barrel" {
 6            over "Geometry" {
 7                over "barrel" {
 8                    color3f[] primvars:displayColor = [(.6, .2, .2)]
 9                }
10            }
11        }
12    }
13}
14
15def Xform "Factory_1" (references = @./factory.usda@) {
16    token[] xformOpOrder = ["xformOp:translate"]
17    double3 xformOp:translate = (20, 0, 0)
18}

/Factory/prototypes/Barrel will be available in downstream contexts and can propagate edits to Barrel_0 and Barrel_1 through the specializes arc.

Note that this approach creates the potential for a prototype explosion. Consider a factory where Barrels are aggregated into multiple bundles and locally refinable instancing is used at the bundle level. Prototypes will be shared between barrels if there are no refinements, but as bundles add refinements, new prototypes will proliferate.

Locally Refinable Instancing for External Arcs

Locally refinable instancing can also be combined with externally referenced assets at the point of assembly. This allows for refinements in the context of the assembly, for example to refine all instances of an asset within a particular assembly, but not in others.

../../../_images/locally_refinable_instancing_for_external_references.svg
loading_dock.usda
 1#usda 1.0
 2(
 3    defaultPrim = "LoadingDock"
 4)
 5
 6def Scope "LoadingDock" {
 7    class Scope "prototypes" {
 8        def Xform "Barrel" {} # Leave the Barrel asset unchanges
 9    }
10
11    def "Barrel_0" (
12        instanceable = True
13        references = @./barrel.usda@
14        specializes = </LoadingDock/prototypes/Barrel>
15    ) {
16        token[] xformOpOrder = ["xformOp:translate"]
17        double3 xformOp:translate = (100, 0, 0)
18    }
19    def "Barrel_1" (
20        instanceable = True
21        references = @./barrel.usda@
22        specializes = </LoadingDock/prototypes/Barrel>
23    ) {
24        token[] xformOpOrder = ["xformOp:translate"]
25        double3 xformOp:translate = (110, 0, 0)
26    }
27}
factory.usda
 1#usda 1.0
 2(
 3    defaultPrim = "Factory"
 4)
 5
 6def Scope "Factory" (
 7    kind = "assembly"
 8) {
 9    class Scope "prototypes" {
10        def Xform "Barrel" {
11            color3f[] primvars:displayColor = [(0, 1, 0)]  # Color all Barrels in Factories Green.
12        }
13    }
14
15    def Scope "BarrelBundle" (
16        kind = "group"
17    ) {
18        def "Barrel_0" (
19            instanceable = True
20            references = @./barrel.usda@
21            specializes = </Factory/prototypes/Barrel>
22        ) {
23            token[] xformOpOrder = ["xformOp:translate"]
24            double3 xformOp:translate = (100, 0, 0)
25        }
26        def "Barrel_1" (
27            instanceable = True
28            references = @./barrel.usda@
29            specializes = </Factory/prototypes/Barrel>
30        ) {
31            token[] xformOpOrder = ["xformOp:translate"]
32            double3 xformOp:translate = (110, 0, 0)
33        }
34    }
35}
scenario.usda
 1#usda 1.0
 2
 3def "Factory_0" (references = @./factory.usda@) {
 4    over "prototypes" () {
 5        over "Barrel" {
 6            over "Geometry" {
 7                over "barrel" {
 8                    # Override the Barrel prototype in this factory to be red
 9                    color3f[] primvars:displayColor = [(.6, .0, .0)]
10                }
11            }
12        }
13    }
14}
15
16def Xform "LoadingDock_1" (references = @./loading_dock.usda@) {
17    token[] xformOpOrder = ["xformOp:translate"]
18    double3 xformOp:translate = (40, 0, 0)
19}
20
21def Xform "Factory_1" (references = @./factory.usda@) {
22    token[] xformOpOrder = ["xformOp:translate"]
23    double3 xformOp:translate = (20, 0, 0)
24}

Globally Refinable Instancing for Internal arcs

Are there ways that we can guarantee that all barrels share the same prototype? We can specialize our scene with global prototypes.

../../../_images/globally_refinable_instancing_for_internal_references.svg
factory.usda
 1#usda 1.0
 2(
 3    defaultPrim = "Factory"
 4)
 5
 6class Scope "prototypes" () {
 7    def Xform "Barrel" (
 8        kind = "component"
 9    ) {
10        def Xform "Geometry" {
11            def Cylinder "barrel" {}
12        }
13    }
14}
15
16def Scope "Factory" (
17    kind = "assembly"
18) {
19    def Scope "BarrelBundle" (
20        kind = "group"
21    ) {
22        def "Barrel_0" (
23            instanceable = True
24            specializes = </prototypes/Barrel>
25        ) {
26            token[] xformOpOrder = ["xformOp:translate"]
27            double3 xformOp:translate = (100, 0, 0)
28        }
29        def "Barrel_1" (
30            instanceable = True
31            specializes = </prototypes/Barrel>
32        ) {
33            token[] xformOpOrder = ["xformOp:translate"]
34            double3 xformOp:translate = (110, 0, 0)
35        }
36    }
37}

This structure is very similar to internally refinable instances, but because the instances are specializing paths outside the model context, Barrel_0 and Barrel_1 can get refinements from /prototypes/Barrel at multiple levels of referencing.

scenario.usda
 1#usda 1.0
 2
 3class Scope "prototypes" {
 4    over "Barrel" {
 5        over "Geometry" {
 6            over "barrel" {
 7                color3f[] primvars:displayColor = [(.6, .2, .2)]
 8            }
 9        }
10    }
11}
12
13def "Factory" (references = @./factory.usda@) {}

Globally refinable instances avoids the issue of locally refinable instances potentially generating multiple prototypes. However, without well defined naming conventions, globally refinable instances can result in unexpected namespace collisions. When pulling data from multiple asset libraries or sources, one should consider additional namespacing to avoid collisions between two similarly named prototypes.

factory.usda
class Scope "prototypes" {
    class Scope "AssetLibrary_v1" {
        class Xform "Barrel" {
            def Xform "Geometry" {
                def Cylinder "barrel" {}

            }
        }
    }
}

Abstract prims as “namespace containers” for edit namespaces

Globally Refinable Instancing for External Arcs

In the asset interface

When it is anticipated that Assets will be globally refined downstream, it might be advantageous to author the assets’ interface with an empty, “speculative” global refinement namespace.

barrel.usda
 1#usda 1.0
 2(
 3    defaultPrim = "Barrel"
 4)
 5
 6class Scope "prototypes" {
 7    over "Barrel" () {}
 8}
 9
10def Xform "Barrel" (
11        specializes = </prototypes/Barrel>
12        kind = "component"
13    ) {
14    def Xform "Geometry" {
15        def Cylinder "barrel" {}
16    }
17}

This allows downstream consumers to add edits into all Barrel instances in the stage, regardless of their depth of instancing.

../../../_images/globally_refinable_instancing_for_external_references.svg
scenario.usda
 1#usda 1.0
 2
 3class Scope "prototypes" {
 4    over "Barrel" {
 5        over "Geometry" {
 6            over "barrel" {
 7                color3f[] primvars:displayColor = [(.6, .2, .2)]
 8            }
 9        }
10    }
11}
12
13def "Factory" (references = @./factory.usda@) {}
At the point of assembly

Specialize and inherit arcs are computationally more expensive than references and payloads (because they are evaluated “live”). Projects may also consume assets from external libraries, so it’s not always possible to add the arcs into the assets.

Rather than adding the arcs into each asset, it is also possible to add the arcs to each instance at the point of assembly, similar to the approach shown in “Globally Refinable Instancing for Internal arcs”.

../../../_images/globally_refinable_instancing_for_external_references_at_the_point_of_assembly.svg
factory.usda
 1#usda 1.0
 2(
 3    defaultPrim = "Factory"
 4)
 5
 6class Scope "prototypes" () {
 7    over "Barrel" () {}
 8}
 9
10
11def Scope "Factory" (
12    kind = "assembly"
13) {
14    def Scope "BarrelBundle" (
15        kind = "group"
16    ) {
17        def "Barrel_0" (
18            instanceable = True
19            references = @./barrel.usda@
20            specializes = </prototypes/Barrel>
21        ) {
22            token[] xformOpOrder = ["xformOp:translate"]
23            double3 xformOp:translate = (100, 0, 0)
24        }
25        def "Barrel_1" (
26            instanceable = True
27            references = @./barrel.usda@
28            specializes = </prototypes/Barrel>
29        ) {
30            token[] xformOpOrder = ["xformOp:translate"]
31            double3 xformOp:translate = (110, 0, 0)
32        }
33    }
34}
scenario.usda
 1#usda 1.0
 2
 3class Scope "prototypes" {
 4    over "Barrel" {
 5        over "Geometry" {
 6            over "barrel" {
 7                color3f[] primvars:displayColor = [(.6, .2, .2)]
 8            }
 9        }
10    }
11}
12
13def "Factory" (references = @./factory.usda@) {}

This makes it possible to “inject” opinions into instanceable, externally referenced assets.

The trade off with this approach is that, in mutli level instancing scenarios, it will be necessary to add the arcs at each level of instancing.

For example, if instanceable barrel assets are referenced into instanceable shelf assets which are referenced into a factory, a new inherit arc will need to be added on the shelfs, which in turn adds the arcs to the to the barrels.

At the point of assembly, asset reference in explicit prototype

In this approach, the external reference is placed into the explicit edit context. This approach avoids the multi level instancing issues described with the previous approach.

The downsides are that each prototype creates a duplicate of the asset structure, although the cost for this is likely to be very low as the asset needs to be loaded eventually and the prototypes hierarchy is not traversed for rendering.

A potentially bigger issue might be that instance root prims will also not be “loadable” if the assets have a payload arc in their interface. Instead, users will have to manage load state on the prototypes. One possible work around might be to place the payload arcs on a child prim of the asset interface, for example on the /materials and /geometry scopes.

../../../_images/globally_refinable_instancing_for_external_references_in_prototypes.svg
factory.usda
 1#usda 1.0
 2(
 3    defaultPrim = "Factory"
 4)
 5
 6class Scope "prototypes" {
 7    def "Barrel" (
 8        references = @./barrel.usda@
 9    ) {
10        over "Geometry" {
11            over "barrel" {
12                color3f[] primvars:displayColor = [(.2, .2, .6)]
13            }
14        }
15    }
16}
17
18def Scope "Factory" (
19    kind = "assembly"
20) {
21    def Scope "BarrelBundle" (
22        kind = "group"
23    ) {
24        def Xform "Barrel_0" (
25            instanceable = True
26            specializes = </prototypes/Barrel>
27        ) {
28            token[] xformOpOrder = ["xformOp:translate"]
29            double3 xformOp:translate = (100, 0, 0)
30        }
31        def Xform "Barrel_1" (
32            instanceable = True
33            specializes = </prototypes/Barrel>
34        ) {
35            token[] xformOpOrder = ["xformOp:translate"]
36            double3 xformOp:translate = (110, 0, 0)
37        }
38    }
39}
scenario.usda
 1#usda 1.0
 2
 3class Scope "prototypes" {
 4    over "Barrel" {
 5        over "Geometry" {
 6            over "barrel" {
 7                color3f[] primvars:displayColor = [(.6, .2, .2)]
 8            }
 9        }
10    }
11}
12
13def "Factory" (references = @./factory.usda@) {}

Point Instancing

Point instancing provides a vectorized representation of instances that share the same scene description but may differ in (potentially animated) attributes like position, orientation, or scale. Point Instancing is more efficient than scene graph instancing, as it does not require a prim for each instance and implicit prototypes do not need to be computed. This comes at the expense of decreased flexibility and addressability - the lack of a unique instance namespace does introduce limitations and requires a specialized toolset. For example, it is generally not possible to simply transform an instance in a point instancer with common translate/rotate/scale tools that are intended to modify xform ops. Instead, “instance painting” or particle system tools might need to be used.

Prototypes are explicit and are specified via an array of relationships. They can be of arbitrary complexity, but are oftentimes used for much smaller instances than Scene Graph Instances. For example, leaves on trees or pebbles on a beach.

Prototypes are organized as children of the PointInstancer to uphold the principle of hierarchical encapsulation (if a point instancer is de-activated, all of its children are de-activated as well).

../../../_images/point_instancer.svg
factory.usda
 1#usda 1.0
 2(
 3    defaultPrim = "Factory"
 4)
 5
 6def Scope "Factory" (
 7    kind = "assembly"
 8) {
 9    def PointInstancer "BarrelBundle" (
10        kind = "group"
11    )
12    {
13        quath[] orientations = [(1, 0, 0, 0), (1, 0, 0, 0), (1, 0, 0, 0), (1, 0, 0, 0)]
14        point3f[] positions = [(100, 0, 0), (110, 0, 0), (100, 10, 0), (110, 10, 0)]
15
16        int[] protoIndices = [0, 0, 0, 0]
17        rel prototypes = </Factory/BarrelBundle/Prototypes/Barrel>
18
19        def "Prototypes" (
20            kind = "group"
21        )
22        {
23            def "Barrel" (
24                prepend references = @./barrel.usda@
25            ) {}
26        }
27    }
28}
scenario.usda
 1#usda 1.0
 2
 3def Scope "Factory"
 4    (
 5        references = @./factory.usda@
 6    )
 7{
 8    over "BarrelBundle" () {
 9        int64[] invisibleIds = [2]
10
11        color3f[] primvars:displayColor = [(1, 0, 0), (0, 1, 0), (0, 0, 1), (1, 1, 0)] (
12            interpolation = "vertex"
13        )
14    }
15}

Animation

Because point instancers are based on array attributes, it is quite simple to animate large numbers of instances by setting time samples on the arrays. Note that the “InactiveIds” array can not be animated, so “invisibleIds” needs to be used instead to specify which instances are visible.

Note that the animation can also be set by using value clips, which allow for time scaling, repetition, stitching etc. Use cases include vegetation animation (“keep alive”) or recorded process simulations.

Refinement

Refinement opportunities for individual instances in point instancers are fairly limited. However, the prototypes used by point instancers are regular prim hierarchies and can therefore be modelled with scene graph instances. Consequently, the techniques discussed above for scene graph instancing refinement can still be applied to the prototypes used

By Primvar propagation

Primvars (such as DisplayColor) on point instancers are interpolated across all instances and propagated to each instance as constant values. The primary use case for this is to manipulate shading for each instance vis primvar readers in materials.

By Promotion

Individual Instances can also be de-activated or made invisible, which allows for “promotion” workflows. In this approach, individual instances are de-activated and a new Xform is created at their location. The corresponding prototype is then internally referenced onto the Xform. This “promoted” reference can then be instanced and refined as needed.

Nested Instancing

Nested instancing is the key to achieving massive scene scale. A well organized asset hierarchy with large, instanced assets made of smaller assets that are also instanced keeps the total prim count to a minimum whilst providing a lot of minute detail.

Nested point instancing is also compatible with this method, further reducing the prim count for objects with many instances which may not need to be editable / refinable directly.

../../../_images/nested.svg
factory.usda
 1#usda 1.0
 2(
 3    defaultPrim = "Factory"
 4)
 5
 6def Scope "Factory" (
 7        kind = "assembly"
 8    )
 9    {
10    def Scope "BarrelsFront" (
11        kind = "group"
12    ) {
13        def "BarrelBundle_0" (
14            instanceable = True
15            references = @./barrel_bundle.usda@
16        ) {
17            token[] xformOpOrder = ["xformOp:translate"]
18            double3 xformOp:translate = (100, 0, 0)
19        }
20        def "BarrelBundle_1" (
21            instanceable = True
22            references = @./barrel_bundle.usda@
23        ) {
24            token[] xformOpOrder = ["xformOp:translate"]
25            double3 xformOp:translate = (110, 0, 0)
26        }
27    }
28    def Scope "BarrelsBack" (
29        kind = "group"
30    ) {
31        def "BarrelBundle_0" (
32            instanceable = True
33            references = @./barrel_bundle.usda@
34        ) {
35            token[] xformOpOrder = ["xformOp:translate"]
36            double3 xformOp:translate = (130, 0, 0)
37        }
38        def "BarrelBundle_1" (
39            instanceable = True
40            references = @./barrel_bundle.usda@
41        ) {
42            token[] xformOpOrder = ["xformOp:translate"]
43            double3 xformOp:translate = (140, 0, 0)
44        }
45    }
46}

Statistics

OpenUSD provides “Stat” utilities that can be used to analyze the effectiveness of instancing.

The python API call is:

from pxr import UsdUtils
print(UsdUtils.ComputeUsdStageStats(stage))

{'assetCount': 0,
'instancedModelCount': 0,
'modelCount': 0,
'primary': {'primCounts': {'activePrimCount': 11,
                            'inactivePrimCount': 0,
                            'instanceCount': 10,
                            'pureOverCount': 0,
                            'totalPrimCount': 11},
            'primCountsByType': {'Xform': 11}},
'prototypeCount': 2,
'prototypes': {'primCounts': {'activePrimCount': 20,
                            'inactivePrimCount': 0,
                            'instanceCount': 10,
                            'pureOverCount': 0,
                            'totalPrimCount': 20},
                'primCountsByType': {'Cube': 10, 'Xform': 10}},
'totalInstanceCount': 20,
'totalPrimCount': 31,
'usedLayerCount': 12}

Two sets of relevant prim counts are provided:

  • “Prototypes Prim Counts”: These are primitives that are encapsulated in prototypes and are therefore potentially shared multiple times.

  • “Primary Prim Counts” prims: These are primitives that are not encapsulated in prototypes.

So an external asset with 1000k prims, that has been referenced on instanceable prims under /World three times should result in four “Primary” prims and 1000k prims in Prototypes (1004 total). If instancing was not used, the prim count would be much higher at 3001 prims.

Another example, an asset with 10 mesh prims, instanced 10 times in a larger asset, that is also instanced 10 times results in a stage which only contains 31 prims, but 1,111 instance proxy prims. This stage would load/get translated to the renderer at nearly the same speed that a stage with only 30 prims would.

Next: Practical Examples