ComputeJointInfluences for meshes with only a subset of the joint

Hi Koen,

let me know if that works: if so, I would like to publish it as a tutorial, but I would need more insights from you what to focus on… (obviously this was created with the help of our ‘helper Friends’… , I am constantly trying to prevent Drift and Haluzination… → SO if that little MD file is BS and AIslop, than let me know, so that I can get my system more targeted…)

UsdSkel Joint Index Remapping: Mesh-Local to Skeleton Space

Problem: Joint indices returned from UsdSkelSkinningQuery::ComputeJointInfluences() map to the mesh’s skel:joints array, but you need indices into the skeleton’s joints array. The mesh may only store a subset of joints from the full skeleton.

Root Cause: GetJointMapper() provides mapping FROM skeleton joint order TO mesh-local joint order, but you need the inverse mapping. The UsdSkelBindingAPI::GetJointsAttr() documentation [7] explains that mesh skel:joints is optional and “may vary from the order of joints on the Skeleton itself” [8], which is why remapping is necessary.

Solution

The key insight is that GetJointMapper() maps FROM skeleton TO mesh-local, but you need to map FROM mesh-local TO skeleton. You need to build the inverse mapping by comparing joint paths. The UsdSkelSkinningQuery::GetJointOrder() method [1] provides access to the mesh-local joint order, while UsdSkelSkeletonQuery::GetJointOrder() [2] provides the skeleton’s joint order. This exact problem and solution approach has been discussed in the Alliance for OpenUSD forum [9], confirming the path-comparison remapping strategy.

C++ Solution

#include <pxr/usd/usdSkel/cache.h>
#include <pxr/usd/usdSkel/skinningQuery.h>
#include <pxr/usd/usdSkel/skeletonQuery.h>
#include <pxr/usd/usdSkel/bindingAPI.h>
#include <pxr/base/vt/array.h>
#include <pxr/base/tf/token.h>
#include <unordered_map>

UsdSkelCache skelCache;
VtArray<int> jointIndices;
VtArray<float> jointWeights;

if (UsdSkelRoot skelRoot = UsdSkelRoot::Find(prim)) {
    // Populate the cache - required before GetSkinningQuery() works
    skelCache.Populate(skelRoot, UsdTraverseInstanceProxies());
    
    // Find the Skeleton that should affect this prim
    // See UsdSkelBindingAPI::GetInheritedSkeleton() [[3]](#appendix-sources)
    UsdSkelSkeleton skel = UsdSkelBindingAPI(prim).GetInheritedSkeleton();
    
    // Get skinning query - see UsdSkelSkinningQuery class reference [[1]](#appendix-sources)
    if (UsdSkelSkinningQuery skinningQuery = skelCache.GetSkinningQuery(prim)) {
        // Get joint influences (indices are into mesh's skel:joints)
        // See UsdSkelSkinningQuery::ComputeJointInfluences() [[1]](#appendix-sources)
        skinningQuery.ComputeJointInfluences(&jointIndices, &jointWeights);
        
        // Get the skeleton query to access skeleton joint order
        UsdSkelSkeletonQuery skeletonQuery = skelCache.GetSkeletonQuery(skel);
        if (!skeletonQuery) {
            // Fallback: get skeleton directly
            skeletonQuery = skelCache.GetSkeletonQuery(skel.GetPrim());
        }
        
        if (skeletonQuery) {
            // Get mesh-local joint order (from skel:joints)
            // See UsdSkelSkinningQuery::GetJointOrder() [[1]](#appendix-sources)
            VtTokenArray meshJointOrder;
            skinningQuery.GetJointOrder(&meshJointOrder);
            
            // Get skeleton joint order
            // See UsdSkelSkeletonQuery::GetJointOrder() [[2]](#appendix-sources)
            VtTokenArray skeletonJointOrder = skeletonQuery.GetJointOrder();
            
            // Build reverse lookup: mesh joint path -> skeleton joint index
            std::unordered_map<TfToken, int, TfToken::HashFunctor> meshToSkelMap;
            for (size_t i = 0; i < meshJointOrder.size(); ++i) {
                const TfToken& meshJointPath = meshJointOrder[i];
                
                // Find this mesh joint in the skeleton's joint order
                for (size_t j = 0; j < skeletonJointOrder.size(); ++j) {
                    if (skeletonJointOrder[j] == meshJointPath) {
                        meshToSkelMap[meshJointPath] = static_cast<int>(j);
                        break;
                    }
                }
            }
            
            // Remap joint indices from mesh-local to skeleton space
            VtArray<int> remappedIndices;
            remappedIndices.reserve(jointIndices.size());
            
            for (int meshLocalIdx : jointIndices) {
                if (meshLocalIdx >= 0 && 
                    static_cast<size_t>(meshLocalIdx) < meshJointOrder.size()) {
                    const TfToken& meshJointPath = meshJointOrder[meshLocalIdx];
                    auto it = meshToSkelMap.find(meshJointPath);
                    if (it != meshToSkelMap.end()) {
                        remappedIndices.push_back(it->second);
                    } else {
                        // Joint not found in skeleton (shouldn't happen, but handle gracefully)
                        remappedIndices.push_back(-1);
                    }
                } else {
                    remappedIndices.push_back(-1);
                }
            }
            
            // Now remappedIndices contains indices into skeleton's joints array
            // Use remappedIndices instead of jointIndices
        }
    }
}

Python Solution

from pxr import Usd, UsdSkel, Vt

def remap_joint_indices_to_skeleton_space(prim):
    """
    Remap joint indices from mesh-local skel:joints to skeleton joints.
    
    Args:
        prim: The skinnable prim (mesh) with UsdSkelBindingAPI
        
    Returns:
        tuple: (remapped_indices, joint_weights) or None if remapping fails
    """
    skel_cache = UsdSkel.Cache()
    
    # Find SkelRoot
    skel_root = UsdSkel.Root.Find(prim)
    if not skel_root:
        return None
    
    skel_cache.Populate(skel_root, Usd.TraverseInstanceProxies())
    
    # Get the skeleton
    # See UsdSkelBindingAPI::GetInheritedSkeleton() [[3]](#appendix-sources)
    binding_api = UsdSkel.BindingAPI(prim)
    skel = binding_api.GetInheritedSkeleton()
    if not skel:
        return None
    
    # Get skinning query - see UsdSkelSkinningQuery class reference [[1]](#appendix-sources)
    skinning_query = skel_cache.GetSkinningQuery(prim)
    if not skinning_query:
        return None
    
    # Get joint influences (indices are into mesh's skel:joints)
    # See UsdSkelSkinningQuery::ComputeJointInfluences() [[1]](#appendix-sources)
    joint_indices = Vt.IntArray()
    joint_weights = Vt.FloatArray()
    if not skinning_query.ComputeJointInfluences(joint_indices, joint_weights):
        return None
    
    # Get skeleton query - see UsdSkelSkeletonQuery class reference [[2]](#appendix-sources)
    skeleton_query = skel_cache.GetSkeletonQuery(skel)
    if not skeleton_query:
        return None
    
    # Get mesh-local joint order (from skel:joints)
    # See UsdSkelSkinningQuery::GetJointOrder() [[1]](#appendix-sources)
    mesh_joint_order = Vt.TokenArray()
    if not skinning_query.GetJointOrder(mesh_joint_order):
        # If no custom joint order, mesh uses skeleton order directly
        skeleton_joint_order = skeleton_query.GetJointOrder()
        # If mesh order matches skeleton order, no remapping needed
        return (joint_indices, joint_weights)
    
    # Get skeleton joint order
    skeleton_joint_order = skeleton_query.GetJointOrder()
    
    # Build reverse lookup: mesh joint path -> skeleton joint index
    mesh_to_skel_map = {}
    for i, mesh_joint_path in enumerate(mesh_joint_order):
        try:
            skel_idx = skeleton_joint_order.index(mesh_joint_path)
            mesh_to_skel_map[i] = skel_idx
        except ValueError:
            # Joint not found in skeleton (shouldn't happen, but handle gracefully)
            mesh_to_skel_map[i] = -1
    
    # Remap joint indices from mesh-local to skeleton space
    remapped_indices = Vt.IntArray()
    remapped_indices.reserve(len(joint_indices))
    
    for mesh_local_idx in joint_indices:
        if mesh_local_idx >= 0 and mesh_local_idx < len(mesh_joint_order):
            skel_idx = mesh_to_skel_map.get(mesh_local_idx, -1)
            remapped_indices.append(skel_idx)
        else:
            remapped_indices.append(-1)
    
    return (remapped_indices, joint_weights)


# Usage example:
stage = Usd.Stage.Open("path/to/file.usd")
mesh_prim = stage.GetPrimAtPath("/SkelRoot/Mesh")

result = remap_joint_indices_to_skeleton_space(mesh_prim)
if result:
    remapped_indices, joint_weights = result
    # Now remapped_indices contains indices into skeleton's joints array
    print(f"Remapped {len(remapped_indices)} joint indices")

Why GetJointMapper().Remap() Doesn’t Work Directly

The GetJointMapper() returns a mapper that maps FROM skeleton joint order TO mesh-local joint order. When you call Remap() on it, you’re mapping skeleton-space data to mesh-local space, which is the opposite of what you need. The UsdSkel Schemas In-Depth documentation [5] explains that remapping via mapper works by matching joint names, but the mapper operates in the forward direction (skeleton → mesh-local) only.

The mapper is designed for use cases like:

  • Mapping skeleton joint transforms to mesh-local joint order for skinning
  • Mapping animation data from skeleton order to mesh-local order

But for remapping indices (not transforms), you need the inverse mapping, which requires comparing the joint paths/orders directly using GetJointOrder() methods [1] [2]. This limitation has been discussed in related forum threads [10] and GitHub issues [11], where users encountered similar challenges with explicit joint orders.

Alternative: Using GetJointOrder() Directly

If the mapper is null (meaning mesh uses skeleton order directly), you can skip remapping:

VtTokenArray meshJointOrder;
if (skinningQuery.GetJointOrder(&meshJointOrder)) {
    // Mesh has custom joint order, need remapping
    // ... use remapping code above ...
} else {
    // Mesh uses skeleton order directly, no remapping needed
    // jointIndices already map to skeleton joints
}

Key Takeaways

  1. GetJointMapper() maps skeleton → mesh-local, not the other way around (see [5])
  2. To remap indices, compare joint paths between mesh skel:joints and skeleton joints using GetJointOrder() methods [1] [2]
  3. Build a reverse lookup map from mesh joint paths to skeleton joint indices (as discussed in [9])
  4. Handle the case where mapper is null (mesh uses skeleton order directly) - see UsdSkelBindingAPI::GetJointsAttr() behavior [7]

Appendix: Sources

[1] UsdSkelSkinningQuery Class Reference

Link: Universal Scene Description: UsdSkelSkinningQuery Class Reference

The official OpenUSD API reference for UsdSkelSkinningQuery documents the exact methods used in this solution: ComputeJointInfluences(), GetJointMapper(), and GetJointOrder(). This class reference provides the authoritative documentation for extracting joint influences from skinnable primitives and accessing the mesh-local joint order, which is essential for building the reverse mapping from mesh indices to skeleton indices.

[2] UsdSkelSkeletonQuery Class Reference

Link: Universal Scene Description: UsdSkelSkeletonQuery Class Reference

The UsdSkelSkeletonQuery class reference documents the GetJointOrder() method that returns the skeleton’s authoritative joint order. This method provides the canonical joint ordering that skeleton-space data uses, making it the target for remapping mesh-local joint indices. The skeleton query is obtained through UsdSkelCache::GetSkelQuery() and serves as the source of truth for skeleton joint ordering in the solution.

[3] UsdSkelBindingAPI Class Reference

Link: Universal Scene Description: UsdSkelBindingAPI Class Reference

The UsdSkelBindingAPI class reference documents the GetInheritedSkeleton() method used to find the skeleton bound to a skinnable primitive. This API is the entry point for discovering skeleton bindings and is essential for obtaining the skeleton that provides the target joint order for remapping. The binding API also defines the skel:joints attribute that can specify custom joint orderings on meshes.

[4] UsdSkel API Introduction

Link: API Introduction

The UsdSkel API Introduction provides conceptual overview of the skeletal animation system, explicitly mentioning UsdSkelBindingAPI::GetInheritedSkeleton() and the use of UsdSkelSkeletonQuery for querying skeleton data. This documentation explains the relationship between skeletons, bindings, and skinning queries, providing context for why the remapping solution requires both skeleton and skinning queries to compare joint orders.

[5] UsdSkel Schemas In-Depth: Joint Order

Link: Schemas In-Depth

The UsdSkel Schemas In-Depth documentation explains flexible joint orderings and how remapping works via mapper by matching joint names. This page explicitly states that joint orderings can differ between skeletons, animations, and meshes, and can be sparse subsets, which directly explains why the index mismatch problem occurs. The documentation clarifies that the mapper remaps data from source order to target order, confirming why inverse mapping is needed for index remapping.

[6] UsdSkelSkeleton: joints Attribute

Link: UsdSkelSkeleton Class Reference

The UsdSkelSkeleton class reference documents the joints attribute, which defines the skeleton’s authoritative joint list and order. This attribute provides the canonical joint ordering that all skeleton-space operations use, making it the target for remapping mesh-local joint indices. Understanding this attribute is crucial for recognizing that skeleton joints represent the authoritative joint order in the solution.

[7] UsdSkelBindingAPI::GetJointsAttr Behavior (NVIDIA Docs)

Link: usdrt::UsdSkelBindingAPI — usdrt 7.5.1 documentation

NVIDIA’s Omniverse documentation for UsdSkelBindingAPI::GetJointsAttr() explains the critical behavior: this optional joints list defines which joints the jointIndices apply to, and if not defined, indices apply to the bound Skeleton’s joints. This documentation clarifies why meshes can have custom joint orderings via skel:joints that differ from the skeleton’s order, which is the root cause of the remapping problem solved in this document.

[8] NVIDIA pxr-usd-api: Joint Order Documentation

Link: UsdSkel module — Omniverse Kit 106.3.0 documentation

NVIDIA’s pxr-usd-api documentation explicitly states that joint token order “may vary from the order of joints on the Skeleton itself”, confirming that mesh-local joint orderings can differ from skeleton joint orderings. This documentation validates the problem statement and confirms that the remapping solution is necessary when working with meshes that define custom joint orderings, such as those exported from Maya.

[9] Alliance for OpenUSD Forum: “ComputeJointInfluences for meshes with only a subset of the joint”

Link: ComputeJointInfluences for meshes with only a subset of the joint

This forum thread describes exactly the problem solved in this document: meshes with skel:joints that are a subset of the skeleton’s joints, causing index mismatches between ComputeJointInfluences() results and skeleton joint indices. The discussion confirms the solution approach of building a remap table by comparing joint paths, validating the path-comparison remapping strategy used in this document.

[10] Alliance for OpenUSD Forum: “Joint ordering, documentation incorrect…?”

Link: Joint ordering, documentation incorrect or just me messing up again?

This forum discussion explores joint ordering challenges and documentation ambiguities, providing context for why the remapping problem can be confusing. The thread discusses how joint orderings work in practice and highlights common pitfalls, which helps explain why GetJointMapper().Remap() doesn’t directly solve the inverse mapping problem for indices.

[11] GitHub Issue: “[UsdSkel] Explicit Joint Orders not handled by …”

Link: [UsdSkel] Explicit Joint Orders not handled by HdxCompute / maya import · Issue #789 · PixarAnimationStudios/OpenUSD · GitHub

This GitHub issue discusses problems with explicit joint orders not being handled correctly in certain scenarios, providing additional context for joint ordering edge cases. The issue highlights that joint ordering can be a source of confusion and that proper remapping is necessary when working with custom joint orderings, reinforcing the need for the solution documented here.