API Introduction

This page gives a brief introduction to using the UsdSkel API to read skel data encoded in USD.

Querying Skeleton Structure And Animation

Given a scene that contains a UsdSkelSkeleton, a consuming application is free to access that primitive and reason about its structure on its own, if it chooses to do so. But UsdSkel also provides a more convenient API for querying data on a Skeleton: UsdSkelSkeletonQuery. A UsdSkelSkeletonQuery is created through a UsdSkelCache, using a UsdSkelSkeleton primtivive, as in the following example:

As with other cache structures in Usd – UsdGeomXformCache, UsdGeomBBoxCache, etc. – the UsdSkelCache that was constructed here is meant to be a persistent cache. The cache is thread-safe, and can be re-used for any numbers of primitives. When reading a USD file, it is best to create a single UsdSkelCache to reuse for each new UsdSkelSkeleton that is encountered.

A UsdSkelSkeletonQuery provides convenience methods for extracting joint transforms, in a variety of spaces.

  • C++:
    // Local-space joint transforms
    VtMatrix4dArray localSpaceXforms;
    skelQuery.ComputeJointLocalTransforms(&localSpaceXforms, time);
    // Joint transforms in the space of the Skeleton.
    VtMatrix4dArray skelSpaceXforms;
    skelQuery.ComputeJointSkelTransforms(&skelSpaceXforms, time);
  • Python:
    # Local-space joint transforms
    localSpaceXforms = skelQuery.ComputeJointLocalTransforms(time)
    # Joint transforms in the space of the Skeleton.
    skelSpaceXforms = skelQuery.ComputeJointSkelTransforms(time)

Note that this allows the animated transforms of a Skeleton to be extracted without having to deal with some of the more complicated aspects of the Skeleton encoding, such as the inheritance of the skel:animationSource relationship.

Joint Paths and Names

Each joint in a Skeleton is identified by a token, which represents the path to a joint in a hierarchy. For example:

def Skeleton "Skel" {
uniform token[] joints = [
"Shoulder", "Shoulder/Elbow", "Shoulder/Elbow/Hand"
]
...
}

Each of these tokens can be converted to an SdfPath, after which the SdfPath methods can be used to extract different components of the path. For instance, to extract just the name component of each path (Shoulder/Elbow -> Elbow), do the following:

  • C++:
    for (size_t i = 0; i < skelQuery.GetJointOrder().size(); ++i) {
    SdfPath jointPath(skelQuery.GetJointOrder()[i]);
    std::cout << "Name of joint " << i << " is "
    << jointPath.GetName() << std::endl;
    }
  • Python:
    for i,jointToken in enumerate(skelQuery.GetJointOrder()):
    jointPath = Sdf.Path(jointToken)
    print "Name of joint", i, "is", jointPath.name

For the schema example above, this code will print:

Name of joint 0 is Shoulder
Name of joint 1 is Elbow
Name of joint 2 is Hand

It should be noted that, if extracting the name of a joint in this manner, joint names are not guaranteed to be unique. For example, suppose a Skeleton has two arms. That may be encoded as:

def Skeleton "Skel" {
uniform token[] joints = [
"LeftShoulder", "LeftShoulder/Elbow", "LeftShoulder/Elbow/Hand",
"RighShoulder", "RightShoulder/Elbow", "RightShoulder/Elbow/Hand"
]
...
}

Although the path of every joint is unique, there is no guarantee over the uniqueness of the name. This is true of primitives in USD in general: Names need not be globally unique, but rather only need to be unique amongst their sibling primitives.

Querying the Joint Hierarchy

The structure of the joint hierarchy itself can also be queried through the UsdSkelSkeletonQuery. For example, suppose that in an application, every joint is described as a Joint object, which has a pointer to its parent Joint. Code for creating a Joint object for every joint in a Skeleton might look something like the following:

  • C++:
    // The ordered set of Joint objects of an imaginary application.
    std::vector<Joint> joints;
    // GetTopology() returns a UsdSkelTopology object, which describes
    // the parent<->child relationships. It also gives the number of joints.
    size_t numJoints = skelQuery.GetTopology().GetNumJoints();
    for (size_t i = 0; i < numJoints; ++i) {
    std::string name = SdfPath(skelQuery.GetJointOrder()[i]).GetName();
    int parent = skelQuery.GetTopology().GetParent(i);
    if (parent >= 0) {
    Joint parentJoint = joints[parent];
    joints.push_back(Joint(name, parentJoint));
    } else {
    // Root joint
    joints.push_back(Joint(name));
    }
    }
  • Python:
    # The ordered set of Joint objects of an imaginary application.
    joints = []
    # GetTopology() returns a UsdSkel.Topology object, which describes
    # the parent<->child relationships. It also gives the number of joints.
    numJoints = len(skelQuery.GetTopology())
    for i in range(numJoints):
    name = Sdf.Path(skelQuery.GetJointOrder()[i]).name
    parent = skelQuery.GetTopology().GetParent(i)
    if parent >= 0:
    parentJoint = joints[parent]
    joints.append(Joint(name, parentJoint))
    else:
    joints.append(Joint(name))

In the above code snippet, indexing into the joints array with the parent index of a joint might appear unsafe, since it might not be clear whether or not the parent joint had been constructed yet. However, it is a schema requirement that the set of joints is ordered, with parent joints coming before child joints. That is, linearly iterating through the ordered set of joints on a Skeleton must always describe a top-down hierarchy traversal. When a UsdSkelSkeletonQuery is constructed, its topology is validated, and the resulting query object is only valid if that topology check passes. So given a valid UsdSkelSkeletonQuery, the above code snippet will be safe.

To further expand on that point, the topology of a Skeleton may be directly validated as follows:

  • C++:
    UsdSkelSkeleton skel(skelPrim);
    VtTokenArray joints;
    if (skel.GetJointsAttr().Get(&joints)) {
    UsdSkelTopology topology(joints);
    std::string whyNot;
    bool valid = topology.Validate(&whyNot);
    }
  • Python:
    skel = UsdSkel.Skeleton(skelPrim)
    joints = skel.GetJointsAttr().Get(joints)
    if joints:
    topology = UsdSkel.Topology(joints)
    valid,whyNot = topology.Validate()

But again, if data is being queried through a UsdSkelSkeletonQuery, this validation occurs automatically.

Skeleton Bindings

Before applying skinning to primitives, we need to first identify which primitives are skinnable, and which Skeleton affects them. It is also desired that this discovery process helps facilitate data sharing. For instance, suppose a model consists of 1000 individual meshes. In order to skin those meshes on a GPU, we would need to first compute appropriate skinning transforms to upload to the GPU. It would be awfully inefficient to do that for each individual mesh – I.e., to perform redundant computations, and upload the same set of transforms for each mesh.

The following snippet demonstrates how this can be addressed efficiently through UsdSkel. We will show the complete code first, before describing the individual parts in more detail:

  • C++:
    UsdSkelCache skelCache;
    // Traverse through the prims on the stage to find where we might
    // have prims to skin.
    auto it = stage->Traverse();
    for (const UsdPrim& prim : it) {
    if (prim.IsA<UsdSkelRoot>()) {
    it.PruneChilren();
    UsdSkelRoot skelRoot(prim);
    skelCache.Populate(skelRoot, UsdTraverseInstanceProxies()));
    std::vector<UsdSkelBinding> bindings;
    skelCache.ComputeSkelBindings(skelRoot, &bindings,
    // Iterate over the bindings related to this SkelRoot
    for (const UsdSkelBinding& binding : bindings) {
    // Get the Skeleton for this binding.
    UsdSkelQuery skelQuery =
    skelCache.GetSkelQuery(binding.GetSkeleton());
    VtMatrix4dArray skinningXforms;
    if (skelQuery.ComputeSkinningTransforms(&skinningXforms, time)) {
    // Iterate over the prims that are skinned by this Skeleton.
    for (const UsdSkelSkinningQuery& skinningQuery :
    binding.GetSkinningTargets()) {
    const UsdPrim& primToSkin = skinningQuery.GetPrim();
    // Process prim / apply skinning
    }
    }
    }
    }
    }
  • Python:
    skelCache = UsdSkel.Cache()
    # Traverse through the prims on the stage to find where we might
    # have prims to skin.
    it = iter(stage.Traverse())
    for prim in it:
    if prim.IsA(UsdSkel.Root):
    it.PruneChildren()
    skelRoot = UsdSkel.Root(prim)
    skelCache.Populate(skelRoot, Usd.TraverseInstanceProxies()))
    bindings = skelCache.ComputeSkelBindings(
    skelRoot, Usd.TraverseInstanceProxies())
    # Iterate over the bindings related to this SkelRoot
    for binding in bindings:
    # Get the Skeleton for this binding.
    skelQuery = skelCache.GetSkelQuery(binding.GetSkeleton())
    skinningXforms = skelQuery.ComputeSkinningTransforms(time)
    if skinningXforms:
    # Iterate over the prims that are skinned by this Skeleton.
    for skinningQuery in binding.GetSkinningTargets():
    primToSkin = skinningQuery.GetPrim()
    # Process prim / apply skinning

The first part of this should be familiar:

  • C++:
    UsdSkelCache skelCache;
  • Python:
    skelCache = UsdSkel.Cache()

When accessing a UsdSkelSkeletonQuery for a Skeleton, we constructed a UsdSkelCache. Again, in a context where data is being read from USD, this cache is intended to persist and be reused across multiple prims (or multiple stages, for that matter). This code example actually makes for a good example of how the cache can be shared across multiple primitives.

  • C++:
    auto it = stage->Traverse();
    for (const UsdPrim& prim : it) {
    if (prim.IsA<UsdSkelRoot>()) {
    it.PruneChilren();
    ...
    }
    }
  • Python:
    it = iter(stage.Traverse())
    for prim in it:
    if prim.IsA(UsdSkel.Root):
    it.PruneChildren()
    ...

Here, we traverse through the primitives on the stage. When we encounter a UsdSkelRoot primitive, we know that we have discovered a branch of the scene graph that might contain skeletally-posed models.

Being able to identify subsets of the scene graph that contain skeletal characters is part of the motivation behind the existence of SkelRoot primitives. It is common in IO contexts to use the SkelRoot as a point for dispatching common computations needed when translating Skeletons.

Both when calling Populate() as well as when computing bindings, a predicate is passed that enables traversal of instances. This code is assuming that it is reasonably to process instanced, skinned primitives. If that is not the case, we could instead pass UsdPrimDefaultPredicate.

A UsdSkelBinding object is a simply a mapping of some Skeleton to a set of skinnable primitives. We can compute those mappings by way of the UsdSkelCache, but must first Populate() that section of the scene graph on the cache.

What we gain from using this API is that the UsdSkelCache is doing all the work of properly resolving inherited binding properties for us, allowing us to get at the question we're really interested: What prims are we skinning, and with which Skeletons?

  • C++:
    for (const UsdSkelBinding& binding : bindings) {
    // Get the Skeleton for this binding.
    UsdSkelQuery skelQuery =
    skelCache.GetSkelQuery(binding.GetSkeleton());
    ...
    }
  • Python:
    # Iterate over the bindings related to this SkelRoot
    for binding in bindings:
    # Get the Skeleton for this binding.
    skelQuery = skelCache.GetSkelQuery(binding.GetSkeleton())
    ...

There could be any number of Skeletons beneath a SkelRoot. There will be a UsdSkelBinding associated with each uniquely bound Skeleton. So we must iterate over all of them.

The binding holds a reference to the Skeleton. As we saw earlier, we can extract a UsdSkelSkeletonQuery from the UsdSkelCache using that Skeleton, which provides a more convenient API for extracting data from the Skeleton.

  • C++:
    VtMatrix4dArray skinningXforms;
    if (skelQuery.ComputeSkinningTransforms(&skinningXforms, time)) {
    ...
    }
  • Python:
    skinningXforms = skelQuery.ComputeSkinningTransforms(time)
    if skinningXforms:
    ...

The skinning transforms have been included at this point only as an example, to emphasize the point that this serves as a common code site at which properties related to a Skeleton can be computed, which are subsequently shared across all of the primitives that are skinned by that Skeleton.

  • C++:
    // Iterate over the prims that are skinned by this Skeleton.
    for (const UsdSkelSkinningQuery& skinningQuery :
    binding.GetSkinningTargets()) {
    const UsdPrim& primToSkin = skinningQuery.GetPrim();
    ...
    }
  • Python:
    # Iterate over the prims that are skinned by this Skeleton.
    for skinningQuery in binding.GetSkinningTargets():
    primToSkin = skinningQuery.GetPrim()
    ...

At this point, we have a Skeleton – or better yet, a UsdSkelSkeletonQuery – and can traverse over the 'skinning targets', which are the set of primitives that are skinned by that Skeleton.

The set of skinned primitives are returned as UsdSkelSkinningQuery objects. Just as UsdSkelSkeletonQuery objects provide convenient API for querying data related to a Skeleton, a UsdSkelSkinningQuery provides convenient API for reading data related to primitives that are skinned, such as joint influences. See the skinning query section for more information.

Discovering Bindings On Skinnable Primitives

In the Skeleton Bindings section, we explored a top-down traversal of a stage, which allowed us to efficiently associate a Skeleton with multiple prims that are affected by that Skeleton. Sometimes, such top-down traversal patterns are not possible, and we need to discover bindings the other way around: That is, given a primitive, discover the Skeleton that affects it, and begin computing data required to skin it.

As with the previous section, we will start with a complete coding example, before breaking down the individual parts.

Once more, the first line should seem familiar:

  • C++:
    UsdSkelCache skelCache;
  • Python:
    skelCache = UsdSkel.Cache()

As with previous examples, we utilize a UsdSkelCache. Again, we emphasize that such caches should persist, and be shared across multiple prims.

We want to be able to extract a UsdSkelSkinningQuery, which provides useful utilities for working with skinnable primitives. As with UsdSkelSkeletonQuery objects, skinning queries are accessed through the UsdSkelCache. But before they are accessed, we need to Populate() the UsdSkelCache with the section of the scene graph that contains the skinnable primitive. Cache population causes the cache to be pre-populated with information about inherited bindings, which is necessary when accessing skinning queries.

Passing the predicate produced by UsdTraverseInstanceProxies() ensures that instanced, skinnable prims are populated on the cache. If there is no need to consider instanced primitives, then a predicate that does not traverse instance proxies – such as UsdPrimDefaultPredicae – may be used instead.

The SkelRoot that encapsulates a primitive can be found using UsdSkelRoot::Find. If no SkelRoot is found, that means that the primitive is not encapsulated within a SkelRoot, and so any properties on the prim related to skinning should be ignored.

Having found a SkelRoot and populated the UsdSkelCache, we can access a UsdSkelSkinningQuery object for the primitive that is being skinned. If the resulting UsdSkelSkinningQuery is invalid, that means that either the primitive is not considered to be skinnable, or the skinning properties are malformed in some way. If the latter, appropriate warning messages will have been posted.

If we've acquired a valid UsdSkelSkinningQuery, we know that a primitive is a valid candidate for skinning. The next logical step might be to determine which Skeleton affects skinning. UsdSkelBindingAPI::GetInheritedSkeleton can be used to discover the bound Skeleton, based on the inherited skel:skeleton binding properties. As before, once we have a Skeleton, we can get access to a UsdSkelSkeletonQuery to assist value extraction.

UsdSkelSkinningQuery: Extracting joint influences

Coding examples from the previous sections demonstrated how to find skinnable primitives and gain access to a UsdSkelSkinningQuery object for a skinnable primitive. Here we briefly demonstrate some of the basic queries that can be used to extract joint influences from skinning queries:

  • C++:
    VtIntArray jointIndices;
    VtFloatArray jointWeights;
    skinningQuery.ComputeJointInfluences(&jointIndices, &jointWeights);
  • Python:
    influences = skinningQuery.ComputeJointInfluences()
    if influences:
    jointIndices,jointWeights = influences

Use UsdSkelSkinningQuery::IsRigidlyDeformed to determine whether or not these arrays represent rigid influences, or varying (per-point) influences.

If the skinnable primitive is not rigidly deforming, then these arrays store a fixed number of influences per point. The full set of influences for the first point come first, followed by the influences for the second point, and so forth. UsdSkelSkinningQuery::ComputeVaryingJointInfluences

returns the number of influences that map to each point.

If the skinnable primitive is rigidly deforming, then all of the resulting influences apply to every point. Such a deformation can also be applied by altering a primitive's transform – hence, a rigid deformation. It is up to the client to determine how to deal with rigid influences.

Not all applications are capable of dealing with rigid transformations. If that's the case, UsdSkelSkinningQuery::ComputeVaryingJointInfluences can be used instead:

  • C++:
    VtIntArray jointIndices;
    VtFloatArray jointWeights;
    skinningQuery.ComputeVaryingJointInfluences(
    numPoints, &jointIndices, &jointWeights);
  • Python:
    influences = skinningQuery.ComputeVaryingJointInfluences(numPoints)
    if influences:
    jointIndices,jointWeights = influences

When calling UsdSkelSkinningQuery::ComputeVaryingJointInfluences, rigid influences are automatically expanded out to define per-point influences.

Another restriction encountered in some applications is that they have a limit on the number of influences that may be specified per point. We do not feel that it is appropriate to enforce such application-specific limitations on the storage encoding, so UsdSkel has defines no limit on the number of influences.

However, UsdSkel does provide utility methods to allow influence arrays to be resized, which such applications may use:

  • C++:
    int numInfluencesPerComponent = skinningQuery.GetNumInfluencesPerComponent();
    if (numInfluencesPerComponent > 4) {
    UsdSkelResizeInfluences(&jointIndices, numInfluencesPerComponent, 4);
    UsdSkelResizeInfluences(&jointWeights, numInfluencesPerComponent, 4);
    }
  • Python:
    numInfluencesPerComponent = skinningQuery.GetNumInfluencesPerComponent()
    if numInfluencesPerComponent > 4:
    UsdSkel.ResizeInfluences(jointIndices, numInfluencesPerComponent, 4);
    UsdSkel.ResizeInfluences(jointWeights, numInfluencesPerComponent, 4);

Testing Skinning with UsdSkelBakeSkinning

UsdSkel provides a UsdSkelBakeSkinning method that bakes the results of skinning directly into points and transforms, effectively converting skeletally posed primitives into normal geometry caches, with no special skeletal behaviors.

UsdSkelBakeSkinning is intended both to serve as a reference implementation for skinning, and to help facilitate testing.

Warning
UsdSkelBakeSkinning is intended for testing and debugging, and emphasizes correctness over performance. It should not be used in performance-sensitive contexts.

Skinning can be baked on a stage as follows:

  • C++:
    UsdSkelBakeSkinning(stage->Traverse());
    stage->Save();
  • Python:
    UsdSkel.BakeSkinning(stage.Traverse());
    stage.Save();

Writing Skeletons

The following code demonstrates the full USD/UsdSkel API, showing how an animated Skeleton might be authored by a DCC application. It is not meant to be a definitive example on the right way to author a Skeleton, but merely serves as a simple example to start from.

For simplicity, this example focuses solely on encoding a Skeleton, and does not including bindings for skinnable primitives.

As with previous examples, we begin with complete code, and then break it down into its component parts.

  • C++:
    bool
    WriteAnimatedSkel(
    const UsdStagePtr& stage,
    const SdfPath& skelPath,
    const SdfPathVector& jointPaths,
    const std::vector<GfMatrix4d>& rootTransformsPerFrame,
    const std::vector<VtMatrix4dArray>& jointWorldSpaceTransformsPerFrame,
    const std::vector<UsdTimeCode>& times,
    const VtMatrix4dArray& bindTransforms,
    const VtMatrix4dArray* restTransforms=nullptr)
    {
    if (rootTransformsPerFrame.size() != times.size())
    return false;
    if (jointWorldSpaceTransformsPerFrame.size() != times.size())
    return false;
    if (bindTransforms.size() != jointPaths.size())
    return false;
    UsdSkelSkeleton skel = UsdSkelSkeleton::Define(stage, skelPath);
    if (!skel) {
    TF_WARN("Failed creating a Skeleton prim at <%s>.", skelPath.GetText());
    return false;
    }
    const size_t numJoints = jointPaths.size();
    UsdSkelTopology topo(jointPaths);
    std::string reason;
    if (!topo.Validate(&reason)) {
    TF_WARN("Invalid topology: %s", reason.c_str());
    return false;
    }
    VtTokenArray jointTokens(numJoints);
    for (size_t i = 0; i < jointPaths.size(); ++i) {
    jointTokens[i] = TfToken(jointPaths[i].GetString());
    }
    skel.GetJointsAttr().Set(jointTokens);
    skel.GetBindTransformsAttr().Set(bindTransforms);
    if (restTransforms && restTransforms->size() == numJoints) {
    skel.GetRestTransformsAttr().Set(*restTransforms);
    }
    UsdAttribute rootTransformAttr = skel.MakeMatrixXform();
    for (size_t i = 0; i < times.size(); ++i) {
    rootTransformAttr.Set(rootTransformsPerFrame[i], times[i]);
    }
    stage, skelPath.AppendChild(TfToken("Anim")));
    SdfPathVector({anim.GetPrim().GetPath()}));
    anim.GetJointsAttr().Set(jointTokens);
    // Set root transforms and joint transforms per frame.
    for (size_t i = 0; i < times.size(); ++i) {
    const GfMatrix4d& rootTransform = rootTransformsPerFrame[i];
    const VtMatrix4dArray& jointWorldSpaceTransforms =
    jointWorldSpaceTransformsPerFrame[i];
    if (jointWorldSpaceTransforms.size() == numJoints) {
    VtMatrix4dArray jointLocalSpaceTransforms;
    topo, jointWorldSpaceTransforms,
    &jointLocalSpaceTransforms,
    &rootTransform)) {
    anim.SetTransforms(jointLocalSpaceTransforms, times[i]);
    }
    }
    }
    // Don't forget to call Save() on the stage!
    return true;
  • Python:
    def WriteAnimatedSkel(stage, skelPath, jointPaths,
    rootTransformsPerFrame,
    jointWorldSpaceTransformsPerFrame,
    times, bindTransforms, restTransforms=None):
    if not len(rootTransformsPerFrame) == len(times):
    return False
    if not len(jointWorldSpaceTransformsPerFrame) == len(times):
    return False
    if not len(bindTransforms) == len(jointPaths):
    return False
    skel = UsdSkel.Skeleton.Define(stage, skelPath)
    if not skel:
    Tf.Warn("Failed defining a Skeleton at <%s>.", skelPath)
    return False
    numJoints = len(jointPaths)
    topo = UsdSkel.Topology(jointPaths)
    valid,whyNot = topo.Validate()
    if not valid:
    Tf.Warn("Invalid topology: %s"%reason)
    return False
    jointTokens = Vt.TokenArray([jointPath.pathString for jointPath in jointPaths])
    skel.GetJointsAttr().Set(jointTokens)
    skel.GetBindTransformsAttr().Set(bindTransforms)
    if restTransforms and len(restTransforms) == numJoints:
    skel.GetRestTransformsAttr().Set(restTransforms)
    rootTransformAttr = skel.MakeMatrixXform()
    for i,time in enumerate(times):
    rootTransformAttr.Set(rootTransformsPerFrame[i], time)
    anim = UsdSkel.Animation.Define(stage, skelPath.AppendChild("Anim"))
    binding = UsdSkel.BindingAPI.Apply(skel.GetPrim())
    binding.CreateSkeletonRel().SetTargets([anim.GetPrim().GetPath()])
    anim.GetJointsAttr().Set(jointTokens)
    for i,time in enumerate(times):
    rootTransform = rootTransformsPerFrame[i]
    jointWorldSpaceTransforms = jointWorldSpaceTransformsPerFrame[i]
    if len(jointWorldSpaceTransforms) == numJoints:
    jointLocalSpaceTransforms =\
    UsdSkel.ComputeJointLocalTransforms(
    topo, jointWorldSpaceTransforms, rootTransform)
    if jointLocalSpaceTransforms:
    anim.SetTransforms(jointLocalSpaceTransforms, time)
    # Don't forget to call Save() on the stage!
    return True
  • C++:
    if (rootTransformsPerFrame.size() != times.size())
    return false;
    if (jointWorldSpaceTransformsPerFrame.size() != times.size())
    return false;
    if (bindTransforms.size() != jointPaths.size())
    return false;
  • Python:
    if not len(rootTransformsPerFrame) == len(times):
    return False
    if not len(jointWorldSpaceTransformsPerFrame) == len(times):
    return False
    if not len(bindTransforms) == len(jointPaths):
    return False

For this method, we expect rootTransformsPerFrame to be an array holding a GfMatrix4d for each time in times. Similarly, jointWorldSpaceTransformsPerFrame holds a VtMatrix4dArray for each time in times, providing the full set of joint transforms for the corresponding time. Finally, the jointPaths input is an array of SdfPath objects giving the path of each joint, and establishing the Joint Order of the skeleton. The required bindTransforms array must be the same size.

A more complete implementation would provide useful warning messages, rather than simply returning false.

  • C++:
    UsdSkelSkeleton skel = UsdSkelSkeleton::Define(stage, skelPath);
    if (!skel) {
    TF_WARN("Failed creating a Skeleton prim at <%s>.", skelPath.GetText());
    return false;
    }
  • Python:
    skel = UsdSkel.Skeleton.Define(stage, skelPath)
    if not skel:
    Tf.Warn("Failed defining a Skeleton at <%s>.", skelPath)
    return False

We start by defining a Skeleton primitive on the stage at the given path. It is good practice to check that the resulting prim is valid. Some reasons why we may be unable to create the prim include:

  • The provided skelPath is not a valid, absolute prim path.
  • An ancestor of the prim at skelPath is already inactive on the stage. It is not possible to acquire a UsdPrim for a descendant of an inactive prim.

A more complete implementation would likely at least validate that the skelPath is not invalid.

  • C++:
    UsdSkelTopology topo(jointPaths);
    std::string reason;
    if (!topo.Validate(&reason)) {
    TF_WARN("Invalid topology: %s", reason.c_str());
    return false;
    }
  • Python:
    topo = UsdSkel.Topology(jointPaths)
    valid,whyNot = topo.Validate()
    if not valid:
    Tf.Warn("Invalid topology: %s"%reason)
    return False

The input jointPaths specify the topology of the Skeleton. We construct a UsdSkelTopology object at this point primarily for use in subsequent transform computations. But this is also a good point to verify that our topology is valid.

  • C++:
    VtTokenArray jointTokens(numJoints);
    for (size_t i = 0; i < jointPaths.size(); ++i) {
    jointTokens[i] = TfToken(jointPaths[i].GetString());
    }
    skel.GetJointsAttr().Set(jointTokens);
  • Python:
    jointTokens = Vt.TokenArray([jointPath.pathString for jointPath in jointPaths])
    skel.GetJointsAttr().Set(jointTokens)

The actual joint topology is stored on the Skeleton primitive as an array of tokens (VtTokenArray). We convert the input paths to tokens, and write the result to the Skeleton.

  • C++:
    skel.GetBindTransformsAttr().Set(bindTransforms);
    if (restTransforms && restTransforms->size() == numJoints) {
    skel.GetRestTransformsAttr().Set(*restTransforms);
    }
  • Python:
    skel.GetBindTransformsAttr().Set(bindTransforms)
    if restTransforms and len(restTransforms) == numJoints:
    skel.GetRestTransformsAttr().Set(restTransforms)

Here we author the bindTransforms property of the Skeleton. The restTransforms property has been treated as optional. But if restTransforms are not authored, then a UsdSkelAnimation must be bound to the Skeleton (which we will do shortly), and that animation must include the full set of joints. See BindingAPI: Binding Skeletons for more information on binding animations.

  • C++:
    UsdAttribute rootTransformAttr = skel.MakeMatrixXform();
    for (size_t i = 0; i < times.size(); ++i) {
    rootTransformAttr.Set(rootTransformsPerFrame[i], times[i]);
    }
  • Python:
    rootTransformAttr = skel.MakeMatrixXform()
    for i,time in enumerate(times):
    rootTransformAttr.Set(rootTransformsPerFrame[i], time)

This demonstrates use of UsdGeomXformable API (a base class of UsdSkelSkeleton) for applying transforms on primitives. For this example, the root transform has been written out directly on the Skeleton. In actual production cases, it is not uncommon for the full root transform to instead be set on an ancestor of the Skeleton.

For reasons that are covered in-depth elsewhere, a Skeleton's joint animations are encoded on a separate primitive.

Note that where on the stage we choose to place the UsdSkelAnimation primitive really only starts to matter when using instancing. In the simple case of encoding a small number of skeletons, adding the animation as a child of the Skeleton, as above, is the most straight-forward approach. But when defining skeletons that take advantage of instancing, a SkelAnimation will not be a descendant of the Skeleton.

Here we apply the UsdSkelBindingAPI to the Skeleton, and use it to bind the animation directly to the Skeleton. In more complex instancing scenarios, we might instead choose to bind the animation to an ancestor of the Skeleton.

  • C++:
    anim.GetJointsAttr().Set(jointTokens);
  • Python:
    anim.GetJointsAttr().Set(jointTokens)

Previously, we stored jointTokens on the Skeleton, to encode topology. When creating a UsdSkelAnimation, we need to once again store the joint tokens. The jointTokens set on the animation define the Joint Order of the animation. In this case, the joint order of the animation is identical to that of the Skeleton, but note that that need not be the case (for example, if the Skeleton includes joints for fingers, our animation could exclude the fingers).

  • C++:
    VtMatrix4dArray jointLocalSpaceTransforms;
    topo, jointWorldSpaceTransforms,
    &jointLocalSpaceTransforms,
    &rootTransform)) {
  • Python:
    jointLocalSpaceTransforms =\
    UsdSkel.ComputeJointLocalTransforms(
    topo, jointWorldSpaceTransforms,
    rootTransform)

This method has been written to allow joint transforms to be provided in world space, because it is often easier for applications to reliably translate world space transform, rather than local transforms – for example, because an application may need to skip intermediate joints, or because an application may have weird transform inheritance rules (oddities abound related to inheritance of scale!). Previously we created a UsdSkelTopology object, primarily so that it can be used at this point to convert world space joint transforms into local space, as required by the UsdSkelAnimation schema.

Of course, if it is easy for an application to provide transforms directly in local space, this conversion would be unnecessary.

  • C++:
    anim.SetTransforms(jointLocalSpaceTransforms, times[i]);
  • Python:
    anim.SetTransforms(jointLocalSpaceTransforms, time)

Finally, we write joint transforms on the UsdSkelAnimation primitive.