Get animated prim attributes per frame/time

To repro/explain my issue, imagine I have created a cube and animated its translation attribute by manually setting keys across frames/time. Now I want to print the cube’s translate attribute in the console at different frames/times. However when I do that I get the same value printed for all the frames I asked for. I don’t understand why!

stage = omni.usd.get_context().get_stage()
cube = stage.GetPrimAtPath(f"/World/Cube")
print(“Translation:”, cube.GetAttribute(‘xformOp:translate’).Get(time=0))
print(“Translation:”, cube.GetAttribute(‘xformOp:translate’).Get(time=10))
print(“Translation:”, cube.GetAttribute(‘xformOp:translate’).Get(time=20))
print(“Translation:”, cube.GetAttribute(‘xformOp:translate’).Get(time=30))

and I get (0,0,0) for all of the print statements even though (0,0,0) only corresponds to the first frame.
If I use a for loop, I get the same value printed that corresponds to the last frame number I requested in the loop.
It does not make any sense. What am I missing?

Hello @mamoham! I have some information for you from the dev team:

The OV animation system encodes keyframe data as an animationData prim that lives as a child of the animated prim (instead of using USD timeSamples). Animation is then calculated and applied at runtime in OV.

I was able to dig up similar conversation about this internally. I hope this helps!

Question:

They are setting timeseries data using the animation timeline and failing to retrieve the translations through script - saving the USDA I see that rather than USD timeseries data, it’s encoding AnimationData. Can someone point me to docs on this schema or how we can do the equivalent to prim.GetAtribute("xformOp:translate").Get(time=x) ?

Response:

They can use evaluateCurve() function in omni::anim::IAnimCurve interface to evaluate curve value at a given time.

Hi. I guess you are using curve animation. You add a key through property window to create curve animation. It evaluates value from Bezier curve and uses it to update prim attributes during stage playing.

It’s not USD time samples. So you can’t get the value with time code and attribute Get().

Anyway, you can still query evaluated value using omni::anim::IAnimCurve::evaluateCurve function. If you want it, I can provide more detail.

@mamoham I’m just going to add this Python snippet here too. The dev team tells me that the API for this is still influx, but I wanted to offer a solution if you’re looking to do something now.

The is a sort of helper function for getting the curve value at a given time, but falls back to timesamples and set value if the attribute is not keyframed.

from pxr import Sdf, AnimationSchemaTools
def get_attr_value(prim:Usd.Prim, attr_name:str, time:float=0):
    if not prim.HasAttribute(attr_name):
        raise RuntimeError(f"The prim: {prim} does not have an attribute named: '{attr_name}'")
    attribute = prim.GetAttribute(attr_name)
    resolved_value = attribute.Get(time)
    if AnimationSchemaTools.HasAnimation(prim):
        type_name = attribute.GetTypeName()
        if isinstance(type_name, Sdf.ValueTypeName):
            if type_name.isArray:
                # Skip anim data check if we have an array type (e.g. points, normals, etc.)
                return resolved_value

        # stuff single value attributes into an array make it easier.
        if not hasattr(resolved_value, "__len__") or type_name == "token":
            resolved_value = [resolved_value]
         
        suffixes = ["x","y","z"][:len(resolved_value)]
        if len(resolved_value) == 4:
            suffixes.insert(0, "w")

        icurve = omni.anim.curve.acquire_interface()
        values = []
        # Collect the evaluated values for all the curves related to the attribute.
        for suffix in suffixes:
            value = icurve.evaluate_curve(str(prim.GetPath().AppendProperty("animationData:binding")), f"{attr_name}:{suffix}", time)
            values.append(value)

    # Fallback on timesamples or set value.
    for i, item in enumerate(values):
        if item is None:
            values[i] = resolved_value[i]
    return values[0] if len(values) == 1 else values