Time and Animated Values

OpenUSD supports authoring animated values that do not resolve to a single static value but vary in value over time. This functionality is typically used for describing animations in a USD scene, but can be used for any scenario where time-varying values are needed, such as simulations.

To support animated values, OpenUSD provides features to author animated values, query and interpolate values over time, and work with “clips” of animated data.

Understanding TimeCodes

TimeCodes are the basic time coordinate for USD. By themselves, TimeCodes are unitless, and do not necessarily conceptually map to “frames per second”. TimeCodes also do not represent other industry standard time codes, such as SMPTE. Keeping TimeCodes unitless gives clients flexibility to encode their time-varying data in ways that make the most sense for their application, while at the same time OpenUSD still provides mechanisms for mapping TimeCodes to real time units for decoding and playback, as described in Mapping TimeCodes to Real Time below.

TimeCodes are encoded in USD layers as numeric (double precision) values. The following simple example shows animated attribute values (using TimeSamples) represented as pairs of TimeCodes and values (in this case, float values specifying a translation along the X-axis).

double xformOp:translateX.timeSamples = {
     1: 0,
     25: 10,
     99: 5,
 }

TimeCodes are most commonly used as the time coordinate for animated values, as shown above. However, you can also specify that an attribute or metadata field’s value type is a TimeCode.

def "PrimA"
(
    # custom metadata with timecode value type
    customData = {
        timecode timeCodeMetadata = 24
    }
)
{
    # attribute with timecode value type
    timecode timeCodeAttr = 24
}

When used this way, the attribute or metadata field’s value will be subject to any TimeCode scaling or offsets via composition, as described in Working with Automatic and Explicit TimeCode Remapping Across Composition.

Avoid Using Infinite TimeCodes

When working with TimeCodes, you may encounter situations where you want to use “infinite” time coordinates. For example, you might have an animated attribute where you want to set an arbitrary “earliest” value at time “negative infinity”, so you can adjust the TimeCode later.

While OpenUSD allows authoring and querying using infinite (-inf/inf) TimeCodes, we strongly discourage working with infinite TimeCodes. TimeCodeRanges created with infinite TimeCodes are not considered valid, DCC tools may not be able to represent infinite TimeCodes, and the use of infinite TimeCodes may create future incompatibilities.

Working With TimeCodes Programmatically

OpenUSD provides ways for working with TimeCodes programmatically, for cases where you want to query an animated attribute at a particular TimeCode, or over a TimeCode range. The following example Python code demonstrates using TimeCode APIs to query and author animated values. Note that while you can use double-precision values when querying an animated attribute, we recommend using TimeCodes to ensure future compatibility.

# Given an attribute with animated values, get the value
# at a specific timeCode (which could be interpolated)
workTimeCode = Usd.TimeCode(14)
workValue = translate_attr.Get(workTimeCode)

# Set a new value at a specific timeCode (for this example we assume
# the attribute is using time samples for animated values)
newTimeCode = Usd.TimeCode(24)
translate_attr.Set(2, newTimeCode)

When working with TimeCodes programmatically, there are some situations where the exact numeric value for a TimeCode is not known, or can’t be represented. OpenUSD provides the following features for these situations.

  • For cases where you are looking for the “earliest” time in a set of TimeCodes, OpenUSD provides UsdTimeCode.EarliestTime(). For example, if you had the following animated values for the xformOp:translateX attribute:

    double xformOp:translateX.timeSamples = {
        3: 5,
        25: 10,
        99: 5,
    }
    

    The following Python query would get the attribute value at the earliest TimeCode for xformOp:translateX (for this example, the value of 5 at the earliest TimeCode, which is 3).

    earliestValue = translate_attr.Get(Usd.TimeCode.EarliestTime())
    
  • For cases where you want the value immediately before a given TimeCode, OpenUSD provides UsdTimeCode.PreTime(time). This is useful in situations where an attribute’s value can change discontinuously at a specific TimeCode, for example, when evaluating attributes with “held” interpolation values, or splines with dual-valued knots at the given TimeCode. UsdTimeCode.PreTime(time) will evaluate the limit of an attribute’s animated value as time approaches the given time from the “left”. For example, using the same set of animated values for the xformOp:translateX attribute that we used previously:

    double xformOp:translateX.timeSamples = {
        3: 5,
        25: 10,
        99: 5,
    }
    

    The following Python query would get the attribute value for the xformOp:translateX attribute just prior to TimeCode 25 (which in this example would be evaluated to 10.0, as the interpolated value is continuous at TimeCode 25).

    preValue = translate_attr.Get(Usd.TimeCode.PreTime(25))
    
  • OpenUSD has a special “Default” sentinel TimeCode coordinate. The Default TimeCode is used when working with attributes that have default values — “static”, non-time-varying values separate from animated values (if any) for an attribute. In inequality comparisons, the Default TimeCode is considered less than any numeric TimeCode, include EarliestTime(). The following Python example queries the xformOp:translateX attribute for its default value, if any.

    defaultValue = translate_attr.Get(Usd.TimeCode.Default())
    

Mapping TimeCodes to Real Time

TimeCode coordinates for animated values are mapped to real-time seconds by scaling by the layer’s timeCodesPerSecond metadata. The following example sets timeCodesPerSecond to 24.

#usda 1.0
(
    timeCodesPerSecond = 24
)

Assuming the scene used a TimeCode range that started at 0, this would mean a TimeCode of 240 would correspond to 10 seconds of real time.

OpenUSD also provides the legacy framesPerSecond layer metadata, used as an indication of the desired playback rate when the animation is viewed in a playback device (DCC tool, usdview, etc.).

Legacy Behavior of framesPerSecond

framesPerSecond normally does not affect TimeCode scaling and does not combine or interfere with any timeCodesPerSecond value. However, OpenUSD does provide a special legacy behavior where the framesPerSecond value, if set, is used as a fallback value for timeCodesPerSecond if timeCodesPerSecond is not set. The order of precedence OpenUSD uses for determining the timeCodesPerSecond to use is:

  1. timeCodesPerSecond from session layer

  2. timeCodesPerSecond from root layer

  3. framesPerSecond from session layer

  4. framesPerSecond from root layer

  5. fallback value of 24

The general best practice is to use timeCodesPerSecond to specify how TimeCodes are scaled to real time, and framesPerSecond if you need to encode a specific playback rate on playback devices, regardless of how many samples per second are recorded in the USD scene. We provide the information about framesPerSecond as fallback for timeCodesPerSecond primarily as a debugging aid, should you observe unexpected time-scaling. The fallback behavior derives only from USD’s relationship to Pixar’s Presto animation system.

Specifying Layer Start and End Times

You can specify start and end time codes in a layer using the startTimeCode and endTimeCode layer metadata.

#usda 1.0
(
    startTimeCode = 1
    endTimeCode = 240
)

This TimeCode range is primarily used by clients when determining the initial playback range to expose to users in a tool. For example, usdview uses this range to set the initial range in the playback bar.

usdview showing playback bar with start/end range

startTimeCode and endTimeCode are not used by OpenUSD to set an actual value resolution evaluation range. For example, if you had authored animated values outside the startTimeCode/endTimeCode range, you can still query for those values, or for interpolated values outside the startTimeCode/endTimeCode range.

Using TimeCode Ranges

If you need to programmatically create a closed range of TimeCodes, use UsdUtilsTimeCodeRange. Note that you cannot use TimeCode’s EarliestTime or Default as the start or end TimeCode for the range. You can specify a non-zero stride used for iterating over TimeCodes in the range. The following Python snippet creates a TimeCodeRange using the stage’s start and end TimeCode, with a stride of 2 TimeCodes, to iterate and get an attribute value over that range.

timeCodeRange = UsdUtils.TimeCodeRange(stage.GetStartTimeCode(),
                                       stage.GetEndTimeCode(), 2)
for timeCode in timeCodeRange:
    print(f"At TimeCode {timeCode}, attribute value is: "
          f"{example_attribute.Get(timeCode)}")

Negative stride values are allowed only if the range’s start TimeCode is greater than or equal to the range’s end TimeCode.

Working with Automatic and Explicit TimeCode Remapping Across Composition

When a layer uses composition via sublayering, references, or payloads, OpenUSD can apply a scale and/or offset to the TimeCode coordinates for animated values and the values of any TimeCode-valued attributes and metadata from the target layer. This provides the flexibility to use animated data in your workflow in different scenes with different time frames of reference.

Automatic Scaling of timeCodesPerSecond

If a layer specifies timeCodesPerSecond and is targeted by a sublayer, reference, or payload composition arc, the TimeCode values and animated value coordinates in the targeted layer are automatically scaled to map into the time frame defined by the source layer’s timeCodesPerSecond.

The following example animationA.usda layer specifies a timeCodesPerSecond of 12.

animationA.usda
#usda 1.0
(
    timeCodesPerSecond = 12
)

This layer is sublayered into another layer animationRoot.usda that specifies a timeCodesPerSecond of 24.

animationRoot.usda
#usda 1.0
(
    timeCodesPerSecond = 24
    subLayers = [
      @animationA.usda@
    ]
)

With animationRoot.usda as the root layer in a composed stage, any TimeCodes for animated attribute values on prims from animationA.usda would be automatically scaled to animationRoot.usda’s time frame. In this case, the TimeCodes would be scaled by a factor of 2, so that an animated value at TimeCode 12 in animationA.usda (which would map to 1 second in animationA.usda’s timeCodesPerSecond) would have its TimeCode scaled to 24 to match the desired 1 second time mapping in animationRoot.usda’s time scaling. The following diagram shows a couple of TimeCodes in animationA.usda scaled to map to the animationRoot.usda timeCodesPerSecond mapping.

Automatic scaling of TimeCodes

This automatic scaling is applied across the entire LayerStack targeted by the sublayer/reference/payload composition arc. For example, animationA.usda could also sublayer another layer, animationB.usda (not shown) that has its own timeCodesPerSecond of 6.

animationA.usda with sublayer
#usda 1.0
(
    timeCodesPerSecond = 12
    subLayers = [
      @animationB.usda@  # animationB has timeCodesPerSecond = 6
    ]
)

In the composed stage, the TimeCodes for animated values in animationB.usda would be automatically scaled by 2 to fit within animationA.usda’s time frame, and then automatically scaled again by 2 to fit within animationRoot.usda’s time frame.

The automatic scaling is also applied to authored values of attributes or metadata fields of the TimeCode value type. For example, suppose animationA.usda had a prim with an attribute and metadata field of TimeCode value type.

animationA.usda with TimeCode value type attribute and metadata
#usda 1.0
(
    timeCodesPerSecond = 12
)

def "PrimA"
(
    # metadata with timecode value type
    customData = {
        timecode timeCodeMetadata = 10
    }
)
{
    # attribute with timecode value type
    timecode timeCodeAttr = 15
}

If this layer were sublayered into animationRoot.usda with a timeCodesPerSecond of 24, the metadata field and attribute values would be automatically scaled in the flattened results.

animationRoot.usda flattened with scaled timecode values
#usda 1.0
(
    timeCodesPerSecond = 24
)

def "PrimA" (
    customData = {
        timecode timeCodeMetadata = 20
    }
)
{
    timecode timeCodeAttr = 30
}

Note there is no automatic application of a layer’s startTimeCode or endTimeCode as offsets for TimeCode coordinates in a composed stage. There is a way to manually specify an offset, discussed in the next section.

Configuring TimeCode Scaling and Offsets Using LayerOffsets

If you need to scale or shift the TimeCodes in a sublayer or referenced or payloaded layer, you can do so by adding a “LayerOffset” to the composition arc. For example, you might have character animation that you want to re-use for a different background character, but it needs to be re-timed for the current scene.

The following layerOffsetsRoot.usda layer specifies a LayerOffset with an offset of 10 and a scale of 2 for a sublayer composition.

layerOffsetsRoot.usda
#usda 1.0
(
    timeCodesPerSecond = 24
    subLayers = [
      @layerOffsetsA.usda@ (offset = 10; scale = 2)
    ]
)

In the composed stage, TimeCodes for animated values in layerOffsetsA.usda will be scaled and offset accordingly. Note that when mapping from a layer’s time frame to that of the layer that references or sublayers it, OpenUSD will apply scaling first, and then the offset, if both are specified.

\[\text{Adjusted TimeCode} = (\text{Layer TimeCode} * \text{LayerOffset scale}) + \text{LayerOffset offset}\]

In the above example, an animated value at TimeCode 10 in layerOffsetsA.usda will have its TimeCode scaled to 20, and then offset by 10 to 30, in the composed stage. The following diagram shows TimeCodes from layerOffsetsA.usda being scaled by 2 and then offset by 10 in a composed stage.

Layer Offset with scale and offset

LayerOffsets can be specified for references and payloads in a similar way. The following example applies a scale and offset to the TimeCodes for any animated attributes of a referenced Prim.

def Xform "Xform2"
(
    prepend references = @animationA.usda@</Xform2> (offset = 10; scale = 2)
)
{
}

As mentioned in Understanding TimeCodes, TimeCodes can be used as an attribute or metadata field’s value type. LayerOffsets will scale and offset authored TimeCode values just like TimeCode coordinates. For example, assume you had the following timeCodeValues.usda layer.

timeCodeValues.usda
def "PrimA"
(
    # custom metadata with timecode value type
    customData = {
        timecode timeCodeMetadata = 10
    }
)
{
    # attribute with timecode value type
    timecode timeCodeAttr = 15
}

This layer is then sublayered into timeCodeValuesRoot.usda with a LayerOffset scale and offset.

timeCodeValuesRoot.usda
#usda 1.0
(
    startTimeCode = 1
    endTimeCode = 240
    timeCodesPerSecond = 24
    subLayers = [
      @timeCodeValues.usda@ (offset = 10; scale = 2)
    ]
)

If timeCodeValuesRoot.usda is loaded into a stage and flattened, the result will have the LayerOffset applied to the TimeCode attribute and metadata field values from PrimA.

flattened timeCodeValuesRoot.usda
def "PrimA" (
    customData = {
        timecode timeCodeMetadata = 30
    }
)
{
    timecode timeCodeAttr = 40
}

Keep in mind the following requirements for LayerOffsets.

  • Scale cannot be 0. This will cause a composition error and the LayerOffset will be ignored.

  • Scale also cannot be negative (this will also cause a composition error and be ignored), as this could cause incorrect resolution of animated values. If you want to reverse the time order of TimeCodes, consider using the Value Clips feature.

  • Offset can be negative, which results in the TimeCodes being offset “earlier” in the composed stage.

  • A LayerOffset itself cannot be animated. You can’t change the specified offset or scale over time. If you need this behavior, the Value Clips feature can provide this.

In usdview, you can see the LayerOffset for a given layer stack via the “Layer Stack” tab.

usdview with layer offset

Combining Automatic Scaling and Layer Offsets

If you apply a LayerOffset to a target layer, and the target layer has a timeCodesPerSecond that differs from that of the referencing layer, OpenUSD applies both the automatic source/target layer scaling and the LayerOffset scale (in that order) to TimeCodes in the composed stage. For example, assume you have the following animationLayer.usda layer.

animationLayer.usda
#usda 1.0
(
    timeCodesPerSecond = 8
)

If you then sublayered this layer in the root layer of your stage, using a Layer Offset with a scale of 2:

animationRootLayer.usda
#usda 1.0
(
    timeCodesPerSecond = 24
    subLayers = [
      @animationLayer.usda@ (offset = 10; scale = 2)
    ]
)

TimeCodes for animated values in animationLayer.usda would be adjusted as follows. First, the automatic scaling would adjust TimeCodes by a factor of 3 (from the animationRootLayer’s timeCodesPerSecond value of 24 divided by animationLayer’s timeCodesPerSecond value of 8). Then, the adjusted TimeCodes would be adjusted further by a factor of 2 (from the subLayer LayerOffset scale) and offset by 10. For example, a TimeCode coordinate of 10 in animationLayer.usda would be multiplied by 3, then multiplied by 2, and finally offset by 10, resulting in a TimeCode coordinate of 70 in the composed stage.