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