Spline Animation#
What Is Spline Animation?#
Time samples are a straightforward way to animate attributes: you author values at chosen time codes, and USD interpolates between them (for example, linearly). That model is a good fit when motion is simple or nearly piecewise linear.
An animation spline stores a curve defined by knots (time-value keys), tangent data, and interpolation modes between knots. Splines can represent smoother or sparser animation than dense time-sample sequences, and they support inner loops (repeating a prototype segment with a value offset) and extrapolation past the authored knot range (for example, repeating the whole curve). Pipeline tools and DCC exports can author splines directly on attributes that support them.
How Does It Work?#
This lesson shows how to build splines with the Python pxr.Ts helpers, attach them to a transform operation with UsdAttribute.SetSpline, and see how layer offsets apply to spline-evaluated motion the same way they do for time samples. For the full rules engine that merges defaults, time samples, splines, and more, see value resolution.
Note
Stage timing metadata (timeCodesPerSecond, startTimeCode, endTimeCode) still defines how time codes map to seconds for playback. When layers disagree on frame rate, USD reconciles timing during value resolution; see Units in OpenUSD for timeCodesPerSecond behavior.
Tip
The embedded 3D previews in this site use GLB conversion that keys off time samples, not raw spline payloads. Each example calls DisplayUSD(..., bake_splines_for_display=True), which writes a temporary flattened USD and evaluates splines into dense samples for the viewer only. Your saved _assets/*.usda files stay spline-authored; open them in usdview to inspect splines directly.
Working With Python#
The Ts package provides Ts.Spline, Ts.Knot, Ts.LoopParams, and Ts.Extrapolation. You construct a spline for an attribute’s value type (for example "float"), add knots, optionally configure loop or extrapolation settings, then call UsdAttribute.SetSpline.
Attention
Splines are only supported on floating-point scalar attributes: half, float, and double. Ts.Spline raises an error for any other type name, so the multi-axis transform ops cannot be splined directly: AddTranslateOp, AddRotateXYZOp, and AddScaleOp all produce a 3-component type (float3, double3, or half3, depending on the precision you request).
Use the single-axis ops instead, or author one spline per component. Single-axis ops are always scalar—AddRotateZOp yields a float by default and AddTranslateXOp a double, and passing UsdGeom.XformOp.PrecisionHalf or PrecisionDouble still gives you half or double. That is why the examples below can pass attr.GetTypeName() straight to Ts.Spline: every precision those ops can produce is one splines support.
Reading attr.Get(timeCode) evaluates the composed spline at that time, subject to layer time mapping—provided no time samples are authored for that attribute at a stronger or equal location. Time samples win over splines during value resolution, so an attribute carrying both returns the sampled value and attr.HasSpline() reports False.
The snippet below is a complete, self-contained example:
1from pxr import Usd, UsdGeom, Ts
2
3stage = Usd.Stage.CreateInMemory()
4xform = UsdGeom.Xform.Define(stage, "/World/Spinner")
5
6# xformOp:rotateZ is a float attribute, which splines support
7spin = xform.AddRotateZOp(opSuffix="yaw")
8attr = spin.GetAttr()
9type_name = str(attr.GetTypeName())
10spline = Ts.Spline(type_name)
11spline.SetKnot(
12 Ts.Knot(
13 typeName=type_name,
14 time=0,
15 value=0,
16 nextInterp=Ts.InterpLinear,
17 )
18)
19spline.SetKnot(
20 Ts.Knot(
21 typeName=type_name,
22 time=48,
23 value=180,
24 nextInterp=Ts.InterpLinear,
25 )
26)
27attr.SetSpline(spline)
For extrapolation modes, knot tangents, and multi-dimensional types, see the OpenUSD C++ documentation for TsSpline.
Examples#
Tip
You can run these examples locally as Jupyter notebooks. See How to Run Notebooks Locally for setup instructions.
Note
Examples on this page use create_new_stage(), a notebook helper imported in a hidden setup cell. In your own scripts, use Usd.Stage.CreateNew() in its place. If you’re running the lessons from a clone of this repository, you can import the helper instead: from lousd.utils.helperfunctions import create_new_stage. See How to Run Notebooks Locally for why.
Example 1: Inner loop on a rotation#
Here a single linear segment from 0° to 90° over 24 frames is repeated four times (one prototype plus three post-loops) with a value offset of 90° each time, so the cube completes a full 360° turn without authoring every quarter-turn as separate samples.
1from pxr import Usd, UsdGeom, Ts
2
3spline_loop_path = "_assets/spline_inner_loop.usda"
4stage = create_new_stage(spline_loop_path)
5UsdGeom.SetStageUpAxis(stage, UsdGeom.Tokens.y)
6stage.SetTimeCodesPerSecond(24)
7stage.SetStartTimeCode(0)
8stage.SetEndTimeCode(96)
9
10world = UsdGeom.Xform.Define(stage, "/World")
11stage.SetDefaultPrim(world.GetPrim())
12
13xform = UsdGeom.Xform.Define(stage, "/World/RotatingCube")
14cube = UsdGeom.Cube.Define(stage, "/World/RotatingCube/Geometry")
15cube.GetSizeAttr().Set(1.2)
16cube.GetDisplayColorAttr().Set([(0.35, 0.65, 0.95)])
17
18yaw = xform.AddRotateZOp(opSuffix="yaw")
19yaw_attr = yaw.GetAttr()
20type_name = str(yaw_attr.GetTypeName())
21
22spline = Ts.Spline(type_name)
23spline.SetKnot(
24 Ts.Knot(
25 typeName=type_name,
26 time=0,
27 value=0,
28 nextInterp=Ts.InterpLinear,
29 )
30)
31loop_params = Ts.LoopParams()
32loop_params.protoStart = 0
33loop_params.protoEnd = 24
34loop_params.numPostLoops = 3
35loop_params.valueOffset = 90
36spline.SetInnerLoopParams(loop_params)
37yaw_attr.SetSpline(spline)
38
39stage.Save()
40
41mid_proto = 12
42print(f"Rotation at t=0: {yaw_attr.Get(0)}")
43print(f"Rotation at t={mid_proto} (mid prototype): {yaw_attr.Get(mid_proto)}")
44print(f"Rotation at t=24 (end of first loop): {yaw_attr.Get(24)}")
45print(f"Rotation at t=48: {yaw_attr.Get(48)}")
46print(f"Attribute stores a spline: {yaw_attr.HasSpline()}")
Rotation at t=0: 0.0
Rotation at t=12 (mid prototype): 45.0
Rotation at t=24 (end of first loop): 90.0
Rotation at t=48: 180.0
Attribute stores a spline: True
The .spline payload in USD text lists the loop parameters and knot data. At evaluation time, value resolution samples that curve at the requested time code.
Example 2: Post extrapolation#
Without extrapolation, querying times past the last knot holds the last value. Setting post extrapolation lets motion continue for the rest of the stage range (or until another rule applies).
This example rocks a cube on the Y axis from -12° to 12° over 60 frames, then keeps swinging by mirroring that segment with Ts.ExtrapLoopOscillate.
1from pxr import Usd, UsdGeom, Ts
2
3spline_extrap_path = "_assets/spline_extrapolation.usda"
4stage = create_new_stage(spline_extrap_path)
5UsdGeom.SetStageUpAxis(stage, UsdGeom.Tokens.y)
6stage.SetTimeCodesPerSecond(30)
7stage.SetStartTimeCode(0)
8stage.SetEndTimeCode(150)
9
10world = UsdGeom.Xform.Define(stage, "/World")
11stage.SetDefaultPrim(world.GetPrim())
12
13xform = UsdGeom.Xform.Define(stage, "/World/RockingCube")
14cube = UsdGeom.Cube.Define(stage, "/World/RockingCube/Geometry")
15cube.GetSizeAttr().Set(1.0)
16cube.GetDisplayColorAttr().Set([(0.9, 0.55, 0.25)])
17
18rock = xform.AddRotateYOp(opSuffix="rock")
19rock_attr = rock.GetAttr()
20type_name = str(rock_attr.GetTypeName())
21
22spline = Ts.Spline(type_name)
23spline.SetKnot(
24 Ts.Knot(
25 typeName=type_name,
26 time=0,
27 value=-12,
28 nextInterp=Ts.InterpCurve,
29 )
30)
31spline.SetKnot(
32 Ts.Knot(
33 typeName=type_name,
34 time=60,
35 value=12,
36 nextInterp=Ts.InterpCurve,
37 )
38)
39spline.SetPostExtrapolation(Ts.Extrapolation(Ts.ExtrapLoopOscillate))
40rock_attr.SetSpline(spline)
41
42stage.Save()
43
44print(f"t=45 (within authored span): {rock_attr.Get(45):.2f}")
45print(f"t=75 (past the last knot, swinging back): {rock_attr.Get(75):.2f}")
46print(f"t=120 (one full oscillation later): {rock_attr.Get(120):.2f}")
t=45 (within authored span): 6.00
t=75 (past the last knot, swinging back): 6.00
t=120 (one full oscillation later): -12.00
Ts.ExtrapLoopOscillate continues the spline by mirroring the authored segment, so the cube swings back to -12° and repeats. Choose the mode that matches the motion: Ts.ExtrapLoopRepeat repeats the segment’s shape and offsets each cycle by the difference between the first and last knot values, which would keep turning this cube in one direction instead of rocking it.
Example 3: Layer offsets and spline time#
References can carry an Sdf.LayerOffset (scale and offset on the source layer’s time line). The same rules apply when the referenced animation is driven by splines: evaluation uses the mapped time when resolving values on the composed stage.
1from pxr import Usd, UsdGeom, Ts, Sdf
2
3slide_asset_path = "_assets/spline_slide_rig.usda"
4rig_stage = create_new_stage(slide_asset_path)
5UsdGeom.SetStageUpAxis(rig_stage, UsdGeom.Tokens.y)
6rig_stage.SetTimeCodesPerSecond(24)
7rig_stage.SetStartTimeCode(0)
8rig_stage.SetEndTimeCode(48)
9
10rig = UsdGeom.Xform.Define(rig_stage, "/SlideRig")
11rig_stage.SetDefaultPrim(rig.GetPrim())
12UsdGeom.Cube.Define(rig_stage, "/SlideRig/Geometry").GetSizeAttr().Set(0.6)
13
14slide = rig.AddTranslateXOp(opSuffix="slide")
15slide_attr = slide.GetAttr()
16type_name = str(slide_attr.GetTypeName())
17
18spline = Ts.Spline(type_name)
19spline.SetKnot(
20 Ts.Knot(typeName=type_name, time=0, value=0, nextInterp=Ts.InterpLinear)
21)
22spline.SetKnot(
23 Ts.Knot(typeName=type_name, time=48, value=4, nextInterp=Ts.InterpLinear)
24)
25slide_attr.SetSpline(spline)
26rig_stage.Save()
27
28offset_scene_path = "_assets/spline_layer_offset_scene.usda"
29scene = create_new_stage(offset_scene_path)
30UsdGeom.SetStageUpAxis(scene, UsdGeom.Tokens.y)
31scene.SetTimeCodesPerSecond(24)
32scene.SetStartTimeCode(0)
33scene.SetEndTimeCode(48)
34
35UsdGeom.Xform.Define(scene, "/World")
36scene.SetDefaultPrim(scene.GetPrimAtPath("/World"))
37
38left_rig = UsdGeom.Xform.Define(scene, "/World/LeftRig")
39left_rig.GetPrim().GetReferences().AddReference(slide_asset_path, primPath="/SlideRig")
40
41right_rig = UsdGeom.Xform.Define(scene, "/World/RightRig")
42right_rig.GetPrim().GetReferences().AddReference(
43 slide_asset_path,
44 primPath="/SlideRig",
45 layerOffset=Sdf.LayerOffset(offset=24),
46)
47
48scene.Save()
49
50composed = Usd.Stage.Open(offset_scene_path)
51early = composed.GetPrimAtPath("/World/LeftRig").GetAttribute("xformOp:translateX:slide")
52late = composed.GetPrimAtPath("/World/RightRig").GetAttribute("xformOp:translateX:slide")
53t_query = 24
54print(f"At global t={t_query}, left rig X (no offset): {early.Get(t_query)}")
55print(f"At global t={t_query}, right rig X (offset +24): {late.Get(t_query)}")
At global t=24, left rig X (no offset): 2.0
At global t=24, right rig X (offset +24): 0.0
The right instance sees source time t - 24 at global time t, so at global frame 24 it is only at the start of its slide. For deeper retiming, value clips remain the heavier-weight option.
Key Takeaways#
Splines complement time samples on the same attributes: you can author smooth, looping, or extrapolated motion without dense sample lists, where tooling supports spline authoring.
pxr.Ts+SetSplineis the usual Python path for constructing curves, inner loops, and extrapolation before saving USD.Splines are for
half,float, anddoubleattributes only—animate single-axis transform ops, or one spline per component, rather than vector types likedouble3.Resolution is per time code:
Get(t)evaluates the composed spline (after strength ordering and time mapping), consistent with how samples andtimeCodesPerSecondinteract across layers. Time samples at a stronger or equal location take precedence over a spline.Layer offsets retime splines just like samples—plan for held behavior outside knot coverage if you rely on offsets or short authored ranges.
For how defaults, samples, splines, and clips combine in the attribute stack, see Value resolution.