Instancing Queries#
USD scene-graph instancing lets multiple prims share the same child subtree.
Mark prims that bring in content through references or other composition arcs
with instanceable = true. USD automatically groups compatible prims and
creates a shared prototype for each group; you do not create the prototype
yourself. For example, references to the same asset with different variant
selections can produce separate prototypes.
An instance root is the prim where an instance of the shared subtree
appears. A prototype root is the root of that shared subtree. Use
ovstage.instancing to find these prims after population.
For example, two instanceable references can share one prototype:
Discover generated prototype paths through the API; do not hard-code them.#
In every population domain, ordinary instance roots use XformInstance or
ScopeInstance and the synthetic instance tags. PHYSICS and selectors
also preserve the authored type in usd-metadata:typeName. Instanceable prims
authored beneath a PointInstancer are the exception: they keep authored
metadata but still link their prototypes and appear in these queries.
With RENDERING, geometry within an instance can also use MeshInstance or
CubeInstance. These are populated representations, not types you author in
USD. See Population — instance types for the
metadata rules and rendering-only exceptions.
The instancing queries return the instance and prototype roots. A generated
MeshInstance or CubeInstance beneath an instance root is part of that
instance; its runtime type alone does not make it another instance root.
These queries describe scene-graph instances. They do not enumerate individual
PointInstancer elements. A subtree made with Cloning Subtrees is a copy;
cloning alone does not make it an instance of a shared prototype.
Note
These APIs are provisional and may change.
Before Querying#
Queries use the current populated content of the runtime stage. They do not inspect the source USD stage. The Population (USD → ovstage) domain and selectors determine which content is available.
The functions are synchronous and have no ordinal argument. Wait for population or an update to finish, then advance the watermark before querying. Call again after later updates to get fresh results; a returned list does not update itself.
The runnable examples use two independent stages: queries.usda for the
simple case below, and nested.usda for nested instances. Both follow the
same sequence. Here is the population step for the nested scene:
SCENE locates the Python example’s main scene file.
from ovstage import ErrorCode, OvstageError, PopulationDomain, instancing, population
population.open_usd(stage, str(SCENE.with_name("nested.usda")),
ordinal=1, domains=PopulationDomain.PHYSICS)
# open_usd() waits by default. Make its changes available for reads.
stage.advance_write_floor(ordinal=1).wait()
nestedFile locates the nested scene. floor1 has ordinal 1 and
scope OVSTAGE_SCOPE_ALL.
waitPop() and waitOp() wait, check completion, and release the
operation. Later snippets use check() and checkOvx() to check
statuses, and pathListHandles() and resolvePath() to read paths.
These helpers are defined in the runnable C example.
const ovx_string_t nestedPath{ nestedFile.data(), nestedFile.size() };
waitPop(stage, ovstage_population_open_usd_from_file(
stage, nestedPath, 1, NAN, OVSTAGE_POPULATION_DOMAIN_PHYSICS), "populate nested scene");
// Wait for population, then advance the watermark before querying.
waitOp(stage, ovstage_advance_write_floor(stage, &floor1), "advance watermark");
Finding Instances and Prototypes#
The Python functions accept and return path strings:
Function in |
Result |
|---|---|
All prototype-root paths. |
|
Instance-root paths that reference that prototype. |
|
The prototype-root path referenced by that instance. |
Here, InstanceA and InstanceB are instanceable references to the same
subtree:
#usda 1.0
def Xform "World"
{
def Xform "Prototype"
{
def Cube "Box" {}
}
def Xform "InstanceA" (
instanceable = true
prepend references = </World/Prototype>
) {}
def Xform "InstanceB" (
instanceable = true
prepend references = </World/Prototype>
) {}
}
These queries discover the generated prototype and list its instances. Sorting makes the printed output stable.
from ovstage import ErrorCode, OvstageError, PopulationDomain, instancing, population
prototype_roots = instancing.get_prototype_roots(stage)
print("prototype roots:", len(prototype_roots))
# prototype roots: 1
# Discover the generated prototype path from a known instance.
prototype_root = instancing.get_prototype_root(stage, "/World/InstanceA")
instance_roots = sorted(instancing.get_instance_roots(stage, prototype_root))
print("instance roots:", " ".join(instance_roots))
# instance roots: /World/InstanceA /World/InstanceB
// Enumerate generated prototype paths; do not hard-code their names.
ovx_primpath_list_t prototypeRoots = OVX_INVALID_PRIMPATH_LIST;
status = ovstage_instancing_get_prototype_roots(stage, &prototypeRoots);
check(stage, status, "get_prototype_roots");
const std::vector<ovx_primpath_t> protoHandles = pathListHandles(dict, prototypeRoots);
if (protoHandles.empty())
{
std::fprintf(stderr, "expected at least one prototype root\n");
return EXIT_FAILURE;
}
std::printf("prototype roots: %llu\n", static_cast<unsigned long long>(protoHandles.size()));
// prototype roots: 1
// One prototype -> its instance roots (both instanceable references).
const ovx_primpath_t prototypeRoot = protoHandles[0];
ovx_primpath_list_t instanceRoots = OVX_INVALID_PRIMPATH_LIST;
status = ovstage_instancing_get_instance_roots(stage, prototypeRoot, &instanceRoots);
check(stage, status, "get_instance_roots");
std::vector<std::string> instancePaths;
for (const ovx_primpath_t handle : pathListHandles(dict, instanceRoots))
instancePaths.push_back(resolvePath(dict, handle));
std::sort(instancePaths.begin(), instancePaths.end());
std::printf("instance roots of the prototype:");
// instance roots of the prototype: /World/InstanceA /World/InstanceB
for (const std::string& p : instancePaths)
std::printf(" %s", p.c_str());
std::printf("\n");
// One instance -> back to its prototype root. Prim paths are dictionary-interned, so equal
// path means equal handle.
const ovx_string_t instanceAName = literal_to_ovx_string("/World/InstanceA");
ovx_primpath_t instanceA = OVX_INVALID_PRIMPATH;
ovxResult = path_dictionary_create_paths_from_strings(dict, &instanceAName, 1, &instanceA);
checkOvx(dict, ovxResult, "intern-path");
ovx_primpath_t backToPrototype = OVX_INVALID_PRIMPATH;
status = ovstage_instancing_get_prototype_root(stage, instanceA, &backToPrototype);
check(stage, status, "get_prototype_root");
std::printf("/World/InstanceA maps back to the same prototype root: %s\n",
backToPrototype == prototypeRoot ? "yes" : "no");
// /World/InstanceA maps back to the same prototype root: yes
// Lists own references; individual path handles need no release.
checkOvx(dict, path_dictionary_release_path_list_reference(dict, instanceRoots), "release instances");
checkOvx(dict, path_dictionary_release_path_list_reference(dict, prototypeRoots), "release prototypes");
Prototype-root paths are opaque: every population domain names a given prototype the same way, and the name is not USD’s prototype path. Resolve prototype roots through these queries; do not construct or compare them by spelling.
Nested Instances#
A shared subtree can itself contain an instance. get_instance_roots()
returns its paths through the outer instances, and get_prototype_root()
accepts those paths too.
Here, OuterA instances a subtree that contains another instance:
#usda 1.0
(defaultPrim = "World"; metersPerUnit = 1; upAxis = "Z")
def Xform "World"
{
def Xform "LeafSource"
{
def Cube "Box" (prepend apiSchemas = ["PhysicsCollisionAPI"])
{
double size = 1
}
}
def Xform "OuterSource"
{
def Xform "NestedLeaf" (instanceable = true; prepend references = </World/LeafSource>) {}
}
def Xform "OuterA" (instanceable = true; prepend references = </World/OuterSource>) {}
}
These queries find the leaf instance under both OuterSource and
OuterA, then verify that both paths share the same prototype.
from ovstage import ErrorCode, OvstageError, PopulationDomain, instancing, population
leaf = instancing.get_prototype_root(stage, "/World/OuterSource/NestedLeaf")
print("\n".join(sorted(instancing.get_instance_roots(stage, leaf))))
# /World/OuterA/NestedLeaf
# /World/OuterSource/NestedLeaf
print(instancing.get_prototype_root(stage, "/World/OuterA/NestedLeaf") == leaf)
# True
const ovx_string_t names[] = { literal_to_ovx_string("/World/OuterSource/NestedLeaf"),
literal_to_ovx_string("/World/OuterA/NestedLeaf") };
ovx_primpath_t nestedPaths[2];
checkOvx(dict, path_dictionary_create_paths_from_strings(dict, names, 2, nestedPaths), "intern paths");
ovx_primpath_t leaf = OVX_INVALID_PRIMPATH, nestedLeaf = OVX_INVALID_PRIMPATH;
check(stage, ovstage_instancing_get_prototype_root(stage, nestedPaths[0], &leaf), "leaf prototype");
ovx_primpath_list_t leafInstances = OVX_INVALID_PRIMPATH_LIST;
check(stage, ovstage_instancing_get_instance_roots(stage, leaf, &leafInstances), "leaf instances");
std::vector<std::string> leafPaths;
for (auto path : pathListHandles(dict, leafInstances))
leafPaths.push_back(resolvePath(dict, path));
std::sort(leafPaths.begin(), leafPaths.end());
for (const auto& path : leafPaths)
std::printf("%s\n", path.c_str());
// /World/OuterA/NestedLeaf
// /World/OuterSource/NestedLeaf
check(stage, ovstage_instancing_get_prototype_root(stage, nestedPaths[1], &nestedLeaf), "nested prototype");
std::printf("same leaf prototype: %s\n", nestedLeaf == leaf ? "yes" : "no");
// same leaf prototype: yes
checkOvx(dict, path_dictionary_release_path_list_reference(dict, leafInstances), "release leaf instances");
Empty Results and Missing Instances#
get_prototype_roots() returns an empty list when the stage has no
scene-graph prototypes. get_instance_roots() returns an empty list when no
instance roots reference the supplied prototype.
get_prototype_root() raises OvstageError with code
ErrorCode.NOT_FOUND if the path is not an instance root, including when the
prim does not exist. It is not a general prim-existence check; see
Queries for that.
Here, /World/LeafSource is an ordinary source prim. Referencing it from
instances does not make the source prim itself an instance root:
from ovstage import ErrorCode, OvstageError, PopulationDomain, instancing, population
try:
instancing.get_prototype_root(stage, "/World/LeafSource")
except OvstageError as error:
# LeafSource is an ordinary prim, even though instances reference it.
if error.code != ErrorCode.NOT_FOUND:
raise
print("not an instance root")
# not an instance root
const ovx_string_t sourceName = literal_to_ovx_string("/World/LeafSource");
ovx_primpath_t source = OVX_INVALID_PRIMPATH, unusedPrototype = OVX_INVALID_PRIMPATH;
checkOvx(dict, path_dictionary_create_paths_from_strings(dict, &sourceName, 1, &source), "intern source");
status = ovstage_instancing_get_prototype_root(stage, source, &unusedPrototype);
if (status == OVSTAGE_ERROR_NOT_FOUND)
{
std::printf("not an instance root\n");
// not an instance root
}
else
check(stage, status, "get prototype");
C Result Ownership#
The functions in ovstage_instancing.h have the same names with the
ovstage_instancing_ prefix. They use path identifiers rather than strings.
ovstage_instancing_get_prototype_roots() and
ovstage_instancing_get_instance_roots() return caller-owned
ovx_primpath_list_t references. Release them through the
Path Dictionary. ovstage_instancing_get_prototype_root() returns one
ovx_primpath_t that lasts for the dictionary’s lifetime and needs no release.
The Python wrappers convert and release native lists before returning.
Further Reading#
OpenUSD scene-graph instancing — how USD creates and shares prototypes.
Population (USD → ovstage) — load USD content before querying instances.
Path Dictionary — resolve identifiers and release path lists.
C API Reference and Python API Reference — API signatures.