Timecodes and Frames

OpenUSD can control the playback range and rate using timecodes and frames per second. Let's look at why and how to use these.

Timecodes and Frames

When an OpenUSD stage contains animation it needs to specify the playback range and speed. This is done using metadata on the root layer of the stage, but it can be overridden in the session layer for temporarily tweaking the playback settings in a playback tool. You may have seen this in layer metadata with keys like timeCodesPerSecond or the older framesPerSecond.

The recommended approach is to use the time code versions, like this.

(
    timeCodesPerSecond = 24
    startTimeCode = 1
    endTimeCode = 120
)

In this case you'd get 24 samples per second, and the animation would play for 5 seconds. The end point is included in the playback range, so you would expect to get exactly 120 samples, or 120 images if you're rendering, numbered from 1 to 120 inclusive.

If there are time samples on attributes they can have any range of time code numbers, even outside of 1 to 120. The stage's playback range is the default when a user presses play or makes a recording, but it isn't a hard limitation.

If timeCodesPerSecond is not set it defaults to 24, which is a common default for film production. If the start and end time codes are not set there is no playback range specified.

The playback duration in seconds can be found like (1 + endTimeCode - startTimeCode) / timeCodesPerSecond = duration. Let's abbreviate time code as tc, and time codes per second as tcps, then in our case (1 tc + 120 tc - 1 tc) / 24 tcps = 5.0 s.

These are double precision floating point values, so you can use fractional values if they are helpful. Film makers reading this may know that broadcast TV used to use a playback rate of 29.97 frames per second, not quite 30. If you need to sync to audio at that rate you might need to set your timeCodesPerSecond and playback range accordingly. For instance, to get a one minute clip at 30 tcps we multiply 30 tcps * 60 s = 1800 tc, so we'd want startTimeCode = 1 and endTimeCode = 1800. To get a one minute clip at 29.97 tcps we can do the same math 29.97 tcps * 60 s = 1798.2 tc, therefore endTimeCode = 1798. You could set it to 1798.2 but since we render at whole values that won't change the number of frames rendered.

There's nothing magic about using 1 as the startTimeCode. This can be any value that's useful, including zero or negative numbers. In film making it's common to use values like startTimeCode = 1001 endTimeCode = 1120 to leave lots of space for "preroll". That's where you can start a simulation before the camera starts rolling so that it has time to get looking good. A fire simulation might need a minute to spark up, a water simulation might need to get some ripples going, or a garment might need time to settle on a CG character's body.

Mixed Frame Rates

If a stage has a timeCodesPerSecond value and it references or sublayers content that has a different timeCodesPerSecond value, a scaling factor is automatically applied to that new content's time samples so that everything plays at the stage's requested playback rate. The layers are not edited, the scaling factor is in memory only.

Maybe you're making a Spiderverse movie and you want to have two different playback rates 🕷️ If that's you then you can disable this by setting an environment variable called PCP_DISABLE_TIME_SCALING_BY_LAYER_TCPS.

What's With The Naming?

In older USD files you're likely to see framesPerSecond, startFrame and endFrame. These names were deprecated when USD started getting more use in other industries. If you're writing out posing information for a robotic arm it's not really animation frames, right?

To make USD more generic the decision was made to rename these. I'm not sure how we got these names, but my best guess is this: The term timecode is often used in film production to refer to systems that allow you to synchronize different recordings. If you filmed a scene with a camera and captured sound with a different device you could use a timecode system to make sure they stay in sync for playback. That's a lot like the film making usage of USD, especially in the visual effects world. It's also a pretty generic sounding term so I think it can adapt to other industries without seeming too weird.

For a while everyone was writing out USD files with the new and the old version, to make sure the files were backwards compatible to older versions of USD. I still see that done today, but it hasn't been necessary since 2016 or so. You might see a USD file with something like this in the top level metadata.

(
    timeCodesPerSecond = 30
    framesPerSecond = 30
    startTimeCode = 0
    startFrame = 0
    endTimeCode = 150
    endFrame = 150
)

This isn't necessary, but doesn't break anything either. USD will use the time code versions and ignore the frame versions. Time code will be used if it's available, then it will fall back to the frame version, then it will fall back to the defaults.

Practical Considerations

How 3D applications use this frame range is really up to them. Everything I said in the first section is true in usdview, which is a reference implementation for USD playback, but another client might choose differently.

In Blender 4 there's a checkbox in USD Import labeled "Set Frame Range". This gets the stage's startTimeCode and endTimeCode set for playback, but in my testing it does not change the frame rate to match timeCodesPerSecond. In Houdini I've never figured out how to get the stage's playback range to be used in the main Houdini playback bar.

I think this metadata is useful for animations even if your tools don't use it because it documents what you intended when you created the animation. It gives you a way to show that this is supposed to be 10 seconds long, and it lets you take advantage of the rate-auto-scaling behavior. If the playback rate is wrong in your application, at least it will be wrong in a consistent way!