Skeleton Path Mismatch
Severity: ⚠️ Warning
Fixable: ✅ Automatic
Prevalence: 🔥 High
Summary
Section titled “Summary”The SkelAnimation prim’s joint paths don’t match the Skeleton prim’s joint paths. This causes RealityKit to silently ignore the animation, leaving the model stuck in its bind pose (T-pose). This is one of the most common causes of “my animation works in Blender but not in RealityKit” problems.
Evidence
Section titled “Evidence”Community Reports
Section titled “Community Reports”- Stack Overflow: “RealityKit is intolerant of joint names containing slashes” - Documents skeleton
nilissues from joint naming - Stack Overflow: “Animation not playing after conversion to USDZ” - Multiple questions about animations failing
- Mark Horgan: “Creating Animated Characters for RealityKit” - Documents Blender→USDZ workflow issues
- Reddit r/visionos: “Blender animation workflow” - Community discussions on export problems
GitHub Issues
Section titled “GitHub Issues”- Pixar USD does not enforce path matching between Skeleton and SkelAnimation - this is a consumer (RealityKit) requirement
- Apple does not publicly document this requirement in detail
Apple Developer Forums
Section titled “Apple Developer Forums”This issue appears frequently in visionOS animation threads, typically manifesting as:
- “Animation works in QuickLook but not RealityKit”
- “entity.skeleton returns nil”
- “availableAnimations is empty”
When It Occurs
Section titled “When It Occurs”| DCC Tool | Export Path | Triggers Issue? | Notes |
|---|---|---|---|
| Blender | File → Export → USD (.usda/.usdc) | ✅ Always | Adds /Armature/ or root bone prefix |
| Blender | Via FBX → Reality Converter | 🔶 Sometimes | Depends on armature naming |
| Mixamo | Direct download | ✅ Always | Uses mixamorig: namespace |
| Maya | USD Export | ❌ Rarely | Usually produces matching paths |
| Houdini | USD ROP | 🔶 Sometimes | Depends on skeleton setup |
Reproduction Steps
Section titled “Reproduction Steps”- Open Blender with a rigged, animated character
- Name the Armature something other than the root bone (e.g., “Armature” with root bone “Hips”)
- Export as USD with default settings
- Load in Preflight → “Animation Path Mismatch” warning appears
- Load in RealityKit → Animation doesn’t play
Test File
Section titled “Test File”Minimal test file: /tmp/problematic_skeleton.usda
#usda 1.0( defaultPrim = "World" upAxis = "Y" )
def SkelRoot "World" { def Skeleton "Skeleton" { uniform token[] joints = ["root", "root/spine", "root/spine/head"] }
def SkelAnimation "Animation" { # PROBLEM: These paths have an extra "/root/" prefix uniform token[] joints = ["/root/root", "/root/root/spine", "/root/root/spine/head"] }}Root Cause
Section titled “Root Cause”USD Schema Flexibility
Section titled “USD Schema Flexibility”The USD UsdSkel schema allows SkelAnimation to use either:
- Relative paths:
"root/spine/head"(relative to skeleton root) - Absolute paths:
"/World/Character/Skeleton/root/spine/head"(scene-absolute)
Blender’s USD exporter often produces paths with an extra hierarchy level that doesn’t exist in the Skeleton’s joints array.
DCC Tool Behavior
Section titled “DCC Tool Behavior”Blender (most common trigger):
- Creates an Armature object containing bones
- USD export includes the Armature name in animation paths but not in Skeleton paths
- Result: Animation has
/Armature/Bonebut Skeleton hasBone
Mixamo:
- Uses
mixamorig:namespace prefix on all bones - When retargeted to a character without this namespace, paths don’t match
Why It Fails Silently
Section titled “Why It Fails Silently”RealityKit attempts to bind animation channels to skeleton joints by exact path match. When paths don’t match:
- No error is thrown
- Animation resource appears valid
playAnimation()succeeds but nothing moves- Model stays in bind pose
Symptoms in RealityKit
Section titled “Symptoms in RealityKit”Users experience:
- Model stuck in T-pose/bind pose - Animation plays but no visible movement
entity.availableAnimationsshows animation - The animation IS loadedentity.model?.mesh.skeletonreturns nil - Sometimes the skeleton binding fails entirely- No console errors - Silent failure with no diagnostic output
- Works in QuickLook - macOS QuickLook may be more forgiving with path matching
Detection
Section titled “Detection”File: PreflightCore/USDClient.swift
Function: checkSkeletonPaths() (lines 259-354)
Algorithm
Section titled “Algorithm”- Collect all joint paths from
Skeletonprims into a normalized set - For each
SkelAnimation, get the first joint path - Normalize and check if it exists in the Skeleton’s set
- If not found, try dropping path components as prefixes:
/root/root/spine→ tryroot/spine→ match found!- The dropped prefix (
/root/) is the fix target
// Key detection logicfor i in 1..<components.count { let suffixArr = components.dropFirst(i) let normSuffix = normalize(suffixArr.joined(separator: "/"))
if normalizedAll.contains(normSuffix) { // Found the extraneous prefix let cleanPrefix = (firstJoint.hasPrefix("/") ? "/" : "") + prefix results.append(.warning( "Animation Path Mismatch", fix: .remapSkeleton(source: cleanPrefix, target: "") )) }}Solution
Section titled “Solution”Preflight Automatic Fix
Section titled “Preflight Automatic Fix”File: PreflightCore/USDSurgeryClient.swift
Function: performPathRemapping()
What the “Remap Skeleton” button does:
- Opens the USD stage for editing
- Finds all
SkelAnimationprims - For each animation’s
jointsattribute:- Strips the detected prefix from each path
- Ensures paths are relative (no leading
/)
- Saves the modified stage to a new file
// Fix logic (simplified)if joint.hasPrefix(source) { newPath = String(joint.dropFirst(source.count)) // Remove leading slash for relative paths while newPath.hasPrefix("/") { newPath = String(newPath.dropFirst()) }}Manual Fix (Blender)
Section titled “Manual Fix (Blender)”Prevent the issue at export time:
- Rename Armature to match root bone: If your root bone is “Hips”, name the Armature “Hips” too
- Use GLTF/FBX as intermediate: Export to FBX, then convert with Reality Converter
- Apply armature before export: In Object mode, select mesh, Ctrl+A → Apply All Transforms
Manual Fix (Text Editor)
Section titled “Manual Fix (Text Editor)”Open the .usda file and find-replace the animation joints:
def SkelAnimation "Animation" { uniform token[] joints = ["/root/root", "/root/root/spine"] uniform token[] joints = ["root", "root/spine"]}Validation
Section titled “Validation”Official Documentation
Section titled “Official Documentation”- Apple: Validating USD Files - General USD validation
- Pixar: UsdSkel Schema - SkelAnimation documentation
- Apple WWDC 2019: “Working with USD” - USD fundamentals
Tested Configurations
Section titled “Tested Configurations”| Source | Target | Result | Notes |
|---|---|---|---|
| Blender 4.0 | visionOS 1.1 | ✅ Fix verified | /Armature/ prefix stripped |
| Blender 4.2 | visionOS 2.0 | ✅ Fix verified | Same behavior |
| Mixamo FBX | visionOS 1.1 | 🔶 Partial | Namespace requires different handling |
Related Issues
Section titled “Related Issues”- Up-Axis Mismatch - Often occurs together in Blender exports
- Blend Shape Timeline Lock - Another animation playback issue
- Missing Default Prim - Can cause loading failures that mask this issue
Changelog
Section titled “Changelog”| Date | Change |
|---|---|
| 2024-12-18 | Initial documentation with automatic fix |
| 2024-12-18 | Added string-based prefix matching (SdfPath was unreliable) |