Value Resolution#

What Is Value Resolution?#

Value resolution is how OpenUSD figures out the final value of a property or piece of metadata by looking at all the different sources that might have information about it. Think of it like solving a puzzle where you have multiple pieces of information from different places, and you need to figure out what the final answer should be.

Even though value resolution combines many pieces of data together, it’s different from composition. Understanding this difference helps you work with USD more effectively.

Note

Animation splines were recently added to OpenUSD and are also part of value resolution. We’ll update this lesson to include them soon.

How Does It Work?#

Key Differences Between Composition and Value Resolution#

  1. Composition is cached, value resolution is not

    When you open a stage or add new scene data, USD creates an index of the composition logic and result at the prim-level for quick access. However, USD doesn’t pre-calculate the final values of properties. This keeps the system fast and uses less memory.

    Tip

    If you need to get the same attribute value many times, you can use special tools like UsdAttributeQuery to cache this information yourself.

  2. Composition rules vary by composition arc, value resolution rules vary by data type

    Composition figures out where all the data comes from and creates an index of sources for each prim. Value resolution then takes this ordered list (from strongest to weakest) and combines the opinion data according to what type of information it is.

Resolving Different Types of Data#

Resolving Metadata#

For most metadata, the rule is simple: the strongest opinion wins. Think of it like a voting system where the most authoritative source gets the final say.

Some metadata like prim specifier, attribute typeName, and several others have special resolution rules. A common metadata type you may encount with special resolution rules are dictionaries (like customData). Dictionaries combine element by element, so if one layer has customData["keyOne"] and another has customData["keyTwo"], the final result will have both keys.

Resolving Relationships#

Relationships work differently because they can have multiple targets. Instead of just picking the strongest opinion, USD combines all the opinions about what the relationship should point to, following specific rules for how to merge lists (i.e. list ops).

Resolving Attributes#

Attributes are special because they have three possible sources of values at each location:

  1. Value clips - Animation data stored in separate files

  2. Time samples - Specific values at specific times

  3. Default value - A non-time-varying value

Value resolution of attributes in the first two cases also account for time scaling and offset operators (e.g. Layer offsets) and interpolation for time codes that fall between two explicit time samples.

Working With Python#

 1from pxr import Usd, UsdGeom
 2
 3# Open a stage
 4stage = Usd.Stage.Open('example.usd')
 5
 6# Get a prim
 7prim = stage.GetPrimAtPath('/World/MyPrim')
 8
 9# Get an attribute
10attr = prim.GetAttribute('myAttribute')
11# Usd.TimeCode.Default() is implied
12default_value = attr.Get()
13# Get the value at time code 100.
14animated_value = attr.Get(100)
15# Use EarliestTime to get earliest animated values if they exist
16value = attr.Get(Usd.TimeCode.EarliestTime())

When you get an attribute value without an explicit time code, the default time code (UsdTimeCode::Default()) is usually not what you want if your stage has animation. Instead, use UsdTimeCode::EarliestTime() to make sure you get the actual animated values rather than just the default value.

Examples#

Example 1: Attribute Value Resolution and Animation#

This example shows how a transform attribute (the xformOp:scale authored by XformCommonAPI) resolves from four sources: a fallback value when no authored value exists, an authored default value, authored time sample values, and interpolated values between time samples.

 1from pxr import Usd, UsdGeom
 2
 3# Time settings
 4start_tc = 1
 5end_tc = 120
 6cube_anim_start_tc = 60
 7mid_t = (cube_anim_start_tc + end_tc) // 2
 8time_code_per_second = 30
 9
10# Stage setup
11file_path = "_assets/value_resolution_attr.usda"
12stage = Usd.Stage.CreateNew(file_path)
13stage.SetTimeCodesPerSecond(time_code_per_second)
14stage.SetStartTimeCode(start_tc)
15stage.SetEndTimeCode(end_tc)
16
17# World, Default Prim, and Ground
18world_xform = UsdGeom.Xform.Define(stage, "/World")
19stage.SetDefaultPrim(world_xform.GetPrim())
20UsdGeom.XformCommonAPI(world_xform).SetRotate((-75, 0, 0))
21
22# Create Ground Cube
23ground = UsdGeom.Cube.Define(stage, world_xform.GetPath().AppendChild("Ground"))
24UsdGeom.XformCommonAPI(ground).SetScale((10, 5, 0.1))
25UsdGeom.XformCommonAPI(ground).SetTranslate((0, 0, -0.1))
26
27# Static cube with schema-defined default scale (no scale op authored)
28static_default_cube = UsdGeom.Cube.Define(stage, world_xform.GetPath().AppendChild("StaticDefaultCube"))
29static_default_cube.GetDisplayColorAttr().Set([(0.2, 0.2, 0.8)])
30static_default_cube_xform_api = UsdGeom.XformCommonAPI(static_default_cube)
31static_default_cube_xform_api.SetTranslate((8, 0, 1))
32UsdGeom.Xformable(static_default_cube).AddScaleOp()  # add scale op but do not author a value
33
34# select a non-default cube scale value
35cube_set_scale = (1.5, 1.5, 1.5)
36
37# Static cube with an authored default scale (no time samples)
38static_cube = UsdGeom.Cube.Define(stage, world_xform.GetPath().AppendChild("StaticCube"))
39static_cube.GetDisplayColorAttr().Set([(0.8, 0.2, 0.2)])
40static_cube_xform_api = UsdGeom.XformCommonAPI(static_cube)
41static_cube_xform_api.SetScale(cube_set_scale)  # set static_cube scale
42static_cube_xform_api.SetTranslate((-8, 0, 1.5))
43
44# Animated cube: same default as StaticCube plus time samples
45anim_cube = UsdGeom.Cube.Define(stage, world_xform.GetPath().AppendChild("AnimCube"))
46anim_cube.GetDisplayColorAttr().Set([(0.2, 0.8, 0.2)])
47anim_cube_xform_api = UsdGeom.XformCommonAPI(anim_cube)
48anim_cube_xform_api.SetScale(cube_set_scale)  # SAME as static_cube
49anim_cube_xform_api.SetTranslate((0, 0, 1.5))
50
51# Author time samples for scale and translate
52# anim_cube_xform_api.SetScale(cube_set_scale, Usd.TimeCode(start_tc))
53anim_cube_xform_api.SetScale((2.5, 2.5, 2.5), Usd.TimeCode(cube_anim_start_tc))  # first animated sample
54anim_cube_xform_api.SetScale((5, 5, 5), Usd.TimeCode(end_tc))  # last sample
55anim_cube_xform_api.SetTranslate((0, 0, 2.5), Usd.TimeCode(cube_anim_start_tc))
56anim_cube_xform_api.SetTranslate((0, 0, 5.0), Usd.TimeCode(end_tc))
57
58# Read back using resolved scale values
59_, _, default_cube_fallback_scale, _, _ = UsdGeom.XformCommonAPI(static_default_cube).GetXformVectors(Usd.TimeCode.Default())
60_, _, static_cube_default_scale, _, _ = static_cube_xform_api.GetXformVectors(Usd.TimeCode.Default())
61_, _, anim_cube_default_scale, _, _ = anim_cube_xform_api.GetXformVectors(Usd.TimeCode.Default())
62_, _, anim_cube_earliest_scale, _, _ = anim_cube_xform_api.GetXformVectors(Usd.TimeCode.EarliestTime())
63_, _, anim_cube_tc1_scale, _, _ = anim_cube_xform_api.GetXformVectors(Usd.TimeCode(start_tc))
64_, _, scale_mid, _, _ = anim_cube_xform_api.GetXformVectors(Usd.TimeCode(mid_t))
65
66# Illustrate that Get() is the same as Get(Usd.TimeCode.Default())
67no_time_code_is_default = static_cube.GetSizeAttr().Get() == static_cube.GetSizeAttr().Get(Usd.TimeCode.Default())
68
69print(f"When querying a value Get() is the same as Get(Usd.TimeCode.Default()): {no_time_code_is_default}\n")
70
71print(f"Scale - StaticDefaultCube (no authored xformOp:scale -> schema fallback):  {default_cube_fallback_scale}")  # returns identity fallback value.
72print(f"Scale - StaticCube (authored default at Default time):  {static_cube_default_scale}")  # returns the user authored default value.
73print(f"Scale - AnimCube (authored default at Default time):  {anim_cube_default_scale}")  # returns the user authored default value.
74print(f"Scale - AnimCube at EarliestTime t={cube_anim_start_tc}:  {anim_cube_earliest_scale}")  # first authored time sample value
75print(f"Scale - AnimCube at t={start_tc} (before first sample, clamped):  {anim_cube_tc1_scale}")  # resolved value prior to authored value.
76print(f"Scale - AnimCube at mid_t={mid_t} (interpolated):  {scale_mid}")  # interpolated between samples
77
78stage.Save()
When querying a value Get() is the same as Get(Usd.TimeCode.Default()): True

Scale - StaticDefaultCube (no authored xformOp:scale -> schema fallback):  (1, 1, 1)
Scale - StaticCube (authored default at Default time):  (1.5, 1.5, 1.5)
Scale - AnimCube (authored default at Default time):  (1.5, 1.5, 1.5)
Scale - AnimCube at EarliestTime t=60:  (2.5, 2.5, 2.5)
Scale - AnimCube at t=1 (before first sample, clamped):  (2.5, 2.5, 2.5)
Scale - AnimCube at mid_t=90 (interpolated):  (3.75, 3.75, 3.75)
Loading your model...
_assets/value_resolution_attr.usda
        
    

Notice Get(..., Usd.TimeCode.Default()) returns the user defined default (non‑time‑sampled) value, Get(..., Usd.TimeCode.EarliestTime()) returns the first time sampled value, and if a time before the first sample is queried USD also returns the first sampled value.

Example 2: Custom Data and Relationship Value Resolution#

This example composes layers to show two resolution rules for dictionary metadata like customData (per key by strength) as well as relationships list editing semantics.

 1from pxr import Usd, UsdGeom
 2import os
 3
 4# --- Layer 1 (weaker)
 5layer_1_path = "_assets/value_resolution_layer_1.usda"
 6layer_1_stage = Usd.Stage.CreateNew(layer_1_path)
 7
 8layer_1_xform = UsdGeom.Xform.Define(layer_1_stage, "/World/XformPrim")
 9layer_1_xform_prim = layer_1_xform.GetPrim()
10
11# "/World/XformPrim" customData
12layer_1_xform_prim.SetCustomDataByKey("source",  "layer_1")
13layer_1_xform_prim.SetCustomDataByKey("opinion",  "weak")
14layer_1_xform_prim.SetCustomDataByKey("unique_layer_value", "layer_1_unique_value")  # only authored in layer_1
15
16# Relationship contribution from base
17look_a = UsdGeom.Xform.Define(layer_1_stage, "/World/Looks/LookA")
18layer_1_xform_prim.CreateRelationship("look:targets").AddTarget(look_a.GetPath())
19layer_1_stage.Save()
20
21# --- Layer 2 (stronger)
22layer_2_path = "_assets/value_resolution_layer_2.usda"
23layer_2_stage = Usd.Stage.CreateNew(layer_2_path)
24
25layer_2_xform = UsdGeom.Xform.Define(layer_2_stage, "/World/XformPrim")
26layer_2_xform_prim = layer_2_xform.GetPrim()
27
28# "/World/XformPrim" customData
29layer_2_xform_prim.SetCustomDataByKey("source",  "layer_2")
30layer_2_xform_prim.SetCustomDataByKey("opinion",  "strong")
31
32# Relationship contribution from override
33look_b = UsdGeom.Xform.Define(layer_2_stage, "/World/Looks/LookB")
34layer_2_xform_prim.CreateRelationship("look:targets").AddTarget(look_b.GetPath())
35layer_2_stage.Save()
36
37# --- Composed stage. First sublayer listed (layer_2) is strongest
38composed_path = "_assets/value_resolution_composed.usda"
39composed_stage = Usd.Stage.CreateNew(composed_path)
40composed_stage.GetRootLayer().subLayerPaths = [os.path.basename(layer_2_path), os.path.basename(layer_1_path)]
41
42xform_prim = composed_stage.GetPrimAtPath("/World/XformPrim")
43resolved_custom_data = xform_prim.GetCustomData() 
44
45# resolved custom data:
46print("Resolved CustomData:")
47for key, value in resolved_custom_data.items():
48    print(f"- '{key}': '{value}'")
49
50# resolved relationship targets:
51targets = xform_prim.GetRelationship("look:targets").GetTargets()
52print(f"\nResolved relationship targets: {[str(t) for t in targets]}")  # both LookA and LookB
53
54composed_stage.Save()
55
56# Write out the composed stage to a single file for inspection
57explicit_composed_path = '_assets/value_resolution_composed_explicit.usda'
58txt = composed_stage.ExportToString(addSourceFileComment=False)
59with open(explicit_composed_path, "w") as f:
60    f.write(txt)
Resolved CustomData:
- 'opinion': 'strong'
- 'source': 'layer_2'
- 'unique_layer_value': 'layer_1_unique_value'

Resolved relationship targets: ['/World/Looks/LookB', '/World/Looks/LookA']
        
    

Here source and opinion resolve from the stronger layer, while unique_layer_value persists from the weaker layer since the stronger layer did not author that key. The resolved relationship includes both LookA and LookB because list editing merged the targets.

Key Takeaways#

Value resolution gives OpenUSD its powerful ability to combine data from multiple sources while keeping the system fast and efficient.

This is incredibly useful in real-world workflows. For example, imagine multiple teams working on different parts of a scene - value resolution seamlessly combines everyone’s work into a single model without anyone’s contributions being lost.

Here’s a concrete example with a robot arm:

  • The base layer defines the robot arm’s default position at (0, 0, 0)

  • The animation layer overrides this to move the arm to (5, 0, 0) during operation

During value resolution, OpenUSD combines these layers, resulting in the robot arm being positioned at (5, 0, 0) while keeping all other unchanged properties from the base layer.

Understanding value resolution is key to working effectively with OpenUSD’s non-destructive workflow and getting the best performance in multi-threaded applications.