cool!
which means that my little helper was not totally of!!, because I am not deep enough into the subject to be able to give you a meaningfull answer myself.
I can see that you ended up using C++…
some insights from the text down below…
Why Python Doesn’t Have It:
- Not in C++ API - The C++ API doesn’t have it either, so Python can’t expose it
- Application-Level Concern - OpenUSD considers this an application problem, not a library problem
- Performance - Building inverse maps costs memory/CPU, so it’s opt-in
- Flexibility - Different apps need different remapping strategies
Alternative C++ Solution: Using the Mapper’s Internal Index Map
Your friend discovered a more efficient approach that leverages the mapper’s internal _indexMap structure instead of string comparisons. This is a clever optimization: (more insights fiúrteh down)
→ What more context would be needed to make it a tutorial? could you provide the sample or a similar sample, so we could make it a round tutorial?
cheers Jan
Here’s an update on the insights.
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.
Understanding the Problem: Why C++ is Needed
The Core Issue: Two Different Numbering Systems
Think of it like this: imagine you have a skeleton (like a human skeleton) with 100 bones numbered 0-99. But a mesh (like a hand mesh) only needs 15 of those bones. The mesh creates its own numbering system (0-14) for just those 15 bones it uses.
The Problem:
- When you ask the mesh “which bones affect this vertex?”, it gives you numbers like
[2, 5, 8]
- But those numbers refer to the mesh’s own list (positions 2, 5, 8 in the mesh’s list of 15 bones)
- You need to know which bones those are in the skeleton’s list (positions 23, 47, 89 in the skeleton’s list of 100 bones)
Why This Happens:
- Maya (and other DCC tools) export meshes with only the joints they actually use
- This saves memory and makes files smaller
- But it creates this “translation problem” between two numbering systems
What OpenUSD Python Provides (and What It Doesn’t)
What OpenUSD Python DOES provide:
ComputeJointInfluences() - Gets the joint indices and weights for each vertex
GetJointMapper() - A tool that maps skeleton → mesh-local (forward direction)
GetJointOrder() - Gets the list of joint names/paths for mesh or skeleton
What OpenUSD Python DOESN’T provide:
- A built-in way to map mesh-local indices → skeleton indices (reverse direction)
- A simple “remap these indices” function that does the inverse mapping
- Automatic handling of the index translation problem
Why Python Doesn’t Have This:
The OpenUSD Python bindings are “thin wrappers” around the C++ code. They expose the same methods, but:
- The
GetJointMapper().Remap() method only works in one direction (skeleton → mesh)
- There’s no built-in inverse mapper
- You have to manually build the reverse lookup yourself
What the C++ Solution Does (Step-by-Step)
The C++ solution manually builds a “translation dictionary” by comparing joint names. Here’s how it works:
Step 1: Get Both Lists
// Get the mesh's joint list: ["/Skeleton/Hand", "/Skeleton/Finger1", "/Skeleton/Finger2", ...]
skinningQuery.GetJointOrder(&meshJointOrder);
// Get the skeleton's joint list: ["/Skeleton/Root", "/Skeleton/Spine", ..., "/Skeleton/Hand", ...]
skeletonQuery.GetJointOrder(&skeletonJointOrder);
Step 2: Build a Translation Dictionary
// For each joint in the mesh's list, find where it appears in the skeleton's list
// Example: "/Skeleton/Hand" is at position 2 in mesh list, but position 47 in skeleton list
// Store: meshToSkelMap["/Skeleton/Hand"] = 47
Step 3: Translate the Indices
// When you have a mesh-local index like 2, look up:
// 1. What joint name is at position 2 in mesh list? → "/Skeleton/Hand"
// 2. What position is "/Skeleton/Hand" in skeleton list? → 47
// 3. Use 47 as the skeleton-space index
Why C++ Works Better Here
C++ Advantages:
- Direct Access - C++ can directly access the internal data structures
- Performance - Building lookup maps is faster in C++
- Control - You can build exactly the data structure you need
- No Limitations - You’re not limited by what Python bindings expose
The C++ Solution Provides:
- A
std::unordered_map (like a fast lookup dictionary) that maps joint names → skeleton indices
- Direct iteration over arrays without Python overhead
- Efficient memory management for large meshes
- The ability to build custom remapping logic that OpenUSD doesn’t provide
How All the Pieces Fit Together
┌─────────────────────────────────────────────────────────────┐
│ 1. USD File (Maya Export) │
│ - Skeleton has 100 joints │
│ - Mesh only uses 15 joints │
└───────────────────────┬─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 2. UsdSkelCache (The Organizer) │
│ - Populates cache with skeleton structure │
│ - Creates query objects for mesh and skeleton │
└───────────────────────┬─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 3. UsdSkelSkinningQuery (Mesh Info) │
│ - GetJointOrder() → mesh's 15 joints │
│ - ComputeJointInfluences() → indices [2,5,8] (mesh-local)│
└───────────────────────┬─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 4. UsdSkelSkeletonQuery (Skeleton Info) │
│ - GetJointOrder() → skeleton's 100 joints │
└───────────────────────┬─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 5. C++ Remapping Logic (The Translation) │
│ - Compare joint names between lists │
│ - Build lookup map: mesh index → skeleton index │
│ - Translate [2,5,8] → [47,89,92] (skeleton-space) │
└─────────────────────────────────────────────────────────────┘
The Missing Piece: Why OpenUSD Doesn’t Solve This
OpenUSD’s Design Philosophy:
- OpenUSD provides the building blocks (queries, mappers, order access)
- It doesn’t provide every possible combination of operations
- The inverse mapping is considered an “application-level” concern
What’s Missing:
- No
GetInverseJointMapper() method
- No
RemapIndicesToSkeletonSpace() convenience function
- No automatic detection that remapping is needed
Why It’s Missing:
- Not Common Enough - Most applications don’t need skeleton-space indices
- Performance - Building inverse maps has a cost, so it’s opt-in
- Flexibility - Different applications need different remapping strategies
- API Bloat - Adding every possible operation would make the API huge
What Your Friend’s C++ Solution Achieves
The C++ solution provides:
Complete Control - You build exactly the remapping you need
Performance - Fast lookup using hash maps
Clarity - The code explicitly shows what’s happening
Reliability - No hidden behavior or limitations
What Python Can’t Do:
Can’t efficiently build custom lookup structures
Limited by what bindings expose
Performance overhead for large meshes
Less control over memory management
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
}
}
}
Understanding the C++ Code: A Detailed Walkthrough
For non-programmers, here’s what each part of the C++ solution does:
Part 1: Setting Up the Cache
UsdSkelCache skelCache;
if (UsdSkelRoot skelRoot = UsdSkelRoot::Find(prim)) {
skelCache.Populate(skelRoot, UsdTraverseInstanceProxies());
What this does: Think of UsdSkelCache as a “phone book” that organizes all skeleton information. Before you can ask questions, you need to “populate” it by telling it to scan the USD file and build its index. This is like loading a database before you can query it.
Part 2: Finding the Skeleton
UsdSkelSkeleton skel = UsdSkelBindingAPI(prim).GetInheritedSkeleton();
What this does: This finds which skeleton is “bound” to your mesh. Think of it as asking “which skeleton controls this mesh?” The answer might be inherited from a parent object, so GetInheritedSkeleton() follows the chain up to find it.
Part 3: Getting Joint Influences
if (UsdSkelSkinningQuery skinningQuery = skelCache.GetSkinningQuery(prim)) {
skinningQuery.ComputeJointInfluences(&jointIndices, &jointWeights);
What this does: This asks the mesh “which joints affect each vertex, and how much?” The jointIndices array contains numbers like [2, 5, 8] - but these are mesh-local indices (positions in the mesh’s joint list, not the skeleton’s).
Part 4: Getting Both Joint Lists
VtTokenArray meshJointOrder;
skinningQuery.GetJointOrder(&meshJointOrder);
VtTokenArray skeletonJointOrder = skeletonQuery.GetJointOrder();
What this does: This gets the actual names of joints in both lists. For example:
meshJointOrder might be: ["/Skeleton/Hand", "/Skeleton/Finger1", "/Skeleton/Finger2"]
skeletonJointOrder might be: ["/Skeleton/Root", "/Skeleton/Spine", ..., "/Skeleton/Hand", "/Skeleton/Finger1", ...]
Part 5: Building the Translation Dictionary
std::unordered_map<TfToken, int, TfToken::HashFunctor> meshToSkelMap;
for (size_t i = 0; i < meshJointOrder.size(); ++i) {
const TfToken& meshJointPath = meshJointOrder[i];
for (size_t j = 0; j < skeletonJointOrder.size(); ++j) {
if (skeletonJointOrder[j] == meshJointPath) {
meshToSkelMap[meshJointPath] = static_cast<int>(j);
break;
}
}
}
What this does: This is the “translation dictionary” builder. It goes through each joint in the mesh’s list and finds where that same joint appears in the skeleton’s list.
Example:
- Mesh joint at position 0:
"/Skeleton/Hand" → Found at skeleton position 47 → Store: meshToSkelMap["/Skeleton/Hand"] = 47
- Mesh joint at position 1:
"/Skeleton/Finger1" → Found at skeleton position 89 → Store: meshToSkelMap["/Skeleton/Finger1"] = 89
The std::unordered_map is like a fast lookup table - you can ask “what skeleton index does /Skeleton/Hand map to?” and get 47 instantly.
Part 6: Translating the Indices
VtArray<int> remappedIndices;
for (int meshLocalIdx : jointIndices) {
const TfToken& meshJointPath = meshJointOrder[meshLocalIdx];
auto it = meshToSkelMap.find(meshJointPath);
if (it != meshToSkelMap.end()) {
remappedIndices.push_back(it->second);
}
}
What this does: This is where the actual translation happens. For each mesh-local index (like 2):
- Look up the joint name:
meshJointOrder[2] → "/Skeleton/Finger2"
- Find in dictionary:
meshToSkelMap["/Skeleton/Finger2"] → 92
- Store skeleton index: Add
92 to remappedIndices
Result: [2, 5, 8] (mesh-local) becomes [47, 89, 92] (skeleton-space)
Why This Approach Works
The Key Insight: Joints have names (like "/Skeleton/Hand"), and names are the same in both lists. The C++ solution uses names as the “bridge” between the two numbering systems:
Mesh Index → Joint Name → Skeleton Index
2 → "/Hand" → 47
Why Names Work:
- Joint names are unique identifiers
- They’re the same regardless of position in the list
- They’re what OpenUSD uses internally to match things up
The Algorithm:
- Get both lists (mesh joints, skeleton joints)
- For each mesh joint, find its name in the skeleton list
- Remember the mapping: mesh position → skeleton position
- Use the mapping to translate indices
Alternative C++ Solution: Using the Mapper’s Internal Index Map
Your friend discovered a more efficient approach that leverages the mapper’s internal _indexMap structure instead of string comparisons. This is a clever optimization:
VtArray<int> indices;
indices.resize(jointIndices.size());
for (size_t i = 0; i < jointIndices.size(); i++)
{
indices[i] = i;
}
// Start with identity mapping: [0, 1, 2, 3, ...] for mesh-local indices
VtArray<int> jointLut = indices;
// Build the lookup table (LUT) to map indices from the (sparse) joint array
// in the mesh to the full joint array of the skeleton
if (skinningQuery.GetJointMapper())
{
skinningQuery.GetJointMapper()->Remap(indices, &jointLut);
}
How This Works:
The GetJointMapper() internally maintains an _indexMap array that maps skeleton indices → mesh-local positions. When you call Remap() with an identity array [0, 1, 2, ...] representing mesh-local positions, the mapper uses its internal mapping to determine where those positions correspond in skeleton space.
Why This Is Better:
- No String Comparisons - Avoids expensive token/path string comparisons
- Uses Internal Mapping - Leverages the mapper’s pre-computed
_indexMap structure
- More Efficient - Direct array lookups instead of nested loops with string matching
- Matches USD Internals - Uses the same mechanism USD uses internally for remapping
How It Works Internally:
The mapper’s Remap() method (see animMapper.h lines 226-237) uses _indexMap[i] to map source index i to target index. When you pass mesh-local indices [0, 1, 2, ...]:
- The mapper looks up where each mesh-local position maps in skeleton space
- The result
jointLut contains skeleton indices corresponding to each mesh-local position
- You can then use
jointLut[meshLocalIdx] to get the skeleton index
Example:
// If mesh has 3 joints at skeleton positions [47, 89, 92]:
// Input: indices = [0, 1, 2] (mesh-local positions)
// Output: jointLut = [47, 89, 92] (skeleton indices)
// Then to remap jointIndices:
for (int meshIdx : jointIndices) {
int skeletonIdx = jointLut[meshIdx]; // Direct lookup!
}
Important Note:
This solution works because the mapper’s _indexMap stores the forward mapping (skeleton → mesh-local), but when you pass mesh-local indices as the source, the mapper’s internal logic effectively gives you access to the inverse mapping. This is more efficient than building your own reverse lookup table with string comparisons.
Comparison:
| Approach |
String Comparison |
Mapper-Based |
| Speed |
O(n×m) string comparisons |
O(n) array lookups |
| Memory |
Hash map of tokens |
Direct array access |
| Complexity |
Manual reverse lookup |
Uses mapper internals |
| Maintenance |
More code to maintain |
Leverages existing API |
Your Friend’s Insight:
The key insight is that GetJointMapper()->Remap() can be used creatively to extract the inverse mapping by passing mesh-local indices and letting the mapper’s internal _indexMap do the work. This avoids the need for manual string-based reverse lookup construction.
Potential API Improvement:
As your friend noted, OpenUSD could provide a convenience method like:
// Proposed API (doesn't exist yet):
VtIntArray RemapIndicesToSkeletonSpace(const VtIntArray& meshLocalIndices) const;
This would encapsulate the mapper-based remapping logic and make it more accessible. The implementation would be similar to your friend’s solution but provided as a first-class API method, eliminating the need for users to understand the mapper’s internal structure.
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")
Summary: Why C++ Solves What Python Cannot
Direct Answer to “Why is OpenUSD Python Missing This?”
OpenUSD Python bindings are “thin wrappers” - they expose C++ functions but don’t add convenience methods. The Python API has:
What Python HAS:
ComputeJointInfluences() - Gets mesh-local indices
GetJointOrder() - Gets joint name lists
GetJointMapper() - Gets forward mapper (skeleton → mesh)
What Python DOESN’T HAVE:
RemapIndicesToSkeletonSpace() - No inverse remapping function
GetInverseJointMapper() - No reverse mapper
- Automatic index translation - No built-in solution
Why Python Doesn’t Have It:
- Not in C++ API - The C++ API doesn’t have it either, so Python can’t expose it
- Application-Level Concern - OpenUSD considers this an application problem, not a library problem
- Performance - Building inverse maps costs memory/CPU, so it’s opt-in
- Flexibility - Different apps need different remapping strategies
What C++ Solves That Python Cannot
C++ Advantages:
-
Custom Data Structures
- C++ can build
std::unordered_map (fast hash table) for O(1) lookups
- Python dictionaries work, but have more overhead for large meshes
- C++ gives you control over memory layout and performance
-
Direct Array Access
- C++ can iterate arrays without Python overhead
- Direct pointer access to joint name arrays
- No Python object wrapping/unwrapping
-
Performance at Scale
- For meshes with thousands of vertices, C++ is significantly faster
- Building lookup maps is a one-time cost, but Python adds overhead to every operation
- Memory management is more efficient
-
Complete Control
- You can build exactly the remapping logic you need
- No limitations from Python bindings
- Can optimize for your specific use case
What Your Friend’s C++ Code Achieves:
// This builds a fast lookup table that Python can't match:
std::unordered_map<TfToken, int> meshToSkelMap;
// Lookup time: O(1) - constant time, regardless of skeleton size
// Python dict: O(1) average, but more overhead per lookup
The Bottom Line:
- Python CAN do this - but it’s slower and less efficient
- C++ DOES this better - faster, more control, better for production
- OpenUSD doesn’t provide it - in either language, you build it yourself
- C++ is the right choice - when performance and control matter
The Missing API: What Should Exist But Doesn’t
What Would Be Ideal:
// This doesn't exist, but would be nice:
VtIntArray remappedIndices = skinningQuery.RemapIndicesToSkeletonSpace(jointIndices);
Why It Doesn’t Exist:
- OpenUSD philosophy: provide building blocks, not every convenience function
- Different applications need different remapping strategies
- Adding it would require maintaining inverse mapper state
- Performance concerns - not everyone needs this operation
What Exists Instead:
- Building blocks:
GetJointOrder(), GetJointMapper()
- You build the solution: Compare lists, build map, translate indices
- This gives flexibility but requires more code
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
GetJointMapper() maps skeleton → mesh-local, not the other way around (see [5])
- Two approaches to remap indices:
- String-based approach: Compare joint paths between mesh
skel:joints and skeleton joints using GetJointOrder() methods [1] [2] - more intuitive but slower
- Mapper-based approach: Use
GetJointMapper()->Remap() with identity array to leverage internal _indexMap - more efficient, matches USD internals
- Build a reverse lookup map from mesh joint paths to skeleton joint indices (as discussed in [9])
- Handle the case where mapper is null (mesh uses skeleton order directly) - see
UsdSkelBindingAPI::GetJointsAttr() behavior [7]
Recommended Approach
Use the mapper-based solution (your friend’s approach) when:
- Performance matters (large meshes, many joints)
- You want to match USD’s internal behavior
- You’re comfortable with leveraging internal mapper structures
Use the string-based solution when:
- You need maximum clarity and explicitness
- You want to understand exactly what’s happening
- Performance is not critical
- You’re learning/debugging the remapping process