Diagnose and fix broken bone/blend shape/annotation mappings on 3D characters. Analyzes model data, compares against presets, extracts animation data, and generates corrected mappings. Use when annotations don't work, AUs don't animate correctly, or importing new character models. Primary focus on skeletal-only models like the Betta fish.
This skill helps diagnose and repair broken mappings for 3D character models in the LoomLarge system. It covers:
Model Loading (GLB/GLTF)
├── Extract skeleton bones (names, hierarchy)
├── Extract mesh morph targets (blend shapes)
├── Extract embedded animations (clips with keyframes)
└── Compare against expected preset (CC4, Fish, etc.)
Mapping Analysis
├── Find missing bones (expected but not in model)
├── Find missing morphs (expected but not in model)
├── Suggest alternatives (fuzzy matching, similar names)
└── Generate corrected preset config
Animation Extraction
├── Parse AnimationClip tracks
├── Identify which bones have keyframes
├── Calculate activation patterns (range of motion)
└── Suggest AU mappings based on observed behavior
| File | Purpose |
|---|---|
| src/presets/annotations.ts | Character annotation configs with bone names |
| node_modules/loomlarge/src/presets/cc4.ts | CC4 preset - AU to morph/bone mappings |
| node_modules/loomlarge/src/presets/bettaFish.ts | Fish preset - skeletal-only mappings |
| node_modules/loomlarge/src/mappings/types.ts | AUMappingConfig interface |
| src/scenes/CharacterGLBScene.tsx | Model loading and engine initialization |
| File | Purpose |
|---|---|
| src/services/modelIntrospectionService.ts | Extract bones/morphs/animations from loaded models |
| src/services/mappingAnalysisService.ts | Compare expected vs actual mappings, suggest fixes |
| src/services/animationAnalysisService.ts | Analyze animation clips to infer AU mappings |
| src/services/mappingPromptService.ts | Generate AI prompts for Claude analysis |
| src/services/mappingWizardService.ts | Orchestrate the full wizard workflow |
Symptom: AU controls don't animate (e.g., head doesn't turn)
Cause: Model uses different bone naming convention
Example: CC4 uses CC_Base_Head, Mixamo uses mixamorig:Head
Fix: Update boneNodes mapping:
const CUSTOM_BONE_NODES = {
HEAD: 'mixamorig:Head', // was: 'CC_Base_Head'
JAW: 'mixamorig:Jaw', // was: 'CC_Base_JawRoot'
EYE_L: 'mixamorig:LeftEye', // was: 'CC_Base_L_Eye'
EYE_R: 'mixamorig:RightEye', // was: 'CC_Base_R_Eye'
// ...
};
Symptom: Facial expressions don't work (e.g., no smile)
Cause: Model has different morph target names or is missing expected morphs
Example: CC4 expects Mouth_Smile_L, model has mouthSmile_L
Fix: Update auToMorphs mapping:
const CUSTOM_AU_TO_MORPHS: Record<number, string[]> = {
12: ['mouthSmile_L', 'mouthSmile_R'], // was: ['Mouth_Smile_L', 'Mouth_Smile_R']
// ...
};
Symptom: Camera doesn't focus on correct body part, markers appear at wrong position
Cause: Annotation references bones that don't exist in the model
Fix: Update annotations.ts with correct bone names for the character
Symptom: Model has no morph targets at all (like the Betta fish)
Cause: Model was rigged for skeletal animation only, not blend shapes
Fix: Create bone-only AU mappings (like FISH_AU_MAPPING_CONFIG)
Use the model introspection service to get all available bones and morphs:
import { extractModelData } from './services/modelIntrospectionService';
// After model loads in CharacterGLBScene
const modelData = extractModelData(model, meshes, animations);
console.log('Bones:', modelData.bones);
console.log('Morphs:', modelData.morphs);
console.log('Animations:', modelData.animations);
import { analyzeMapping } from './services/mappingAnalysisService';
const analysis = analyzeMapping(modelData, CC4_PRESET);
console.log('Missing bones:', analysis.missingBones);
console.log('Missing morphs:', analysis.missingMorphs);
console.log('Suggested mappings:', analysis.suggestions);
import { generateCorrectedPreset } from './services/mappingAnalysisService';
const correctedPreset = generateCorrectedPreset(modelData, CC4_PRESET);
// This outputs a new AUMappingConfig with corrected bone/morph names
When importing a model with embedded animations, analyze which bones are used:
import { analyzeAnimationClip } from './services/animationAnalysisService';
for (const clip of animations) {
const analysis = analyzeAnimationClip(clip);
console.log(`Animation "${clip.name}":`);
console.log(' Active bones:', analysis.activeBones);
console.log(' Rotation ranges:', analysis.rotationRanges);
}
This helps identify:
maxDegrees)interface AUMappingConfig {
// AU ID → morph target names (e.g., AU 12 → Smile morphs)
auToMorphs: Record<number, string[]>;
// AU ID → bone transformations (e.g., AU 51 → HEAD rotation)
auToBones: Record<number, BoneBinding[]>;
// Semantic key → actual bone name (e.g., 'HEAD' → 'CC_Base_Head')
boneNodes: Record<string, string>;
// Morph category → mesh names (e.g., 'face' → ['CC_Base_Body_1'])
morphToMesh: Record<string, string[]>;
// Viseme keys for lip-sync (typically 15 phoneme positions)
visemeKeys: string[];
// Optional: Default morph/bone blend weights
auMixDefaults?: Record<number, number>;
// Optional: AU metadata (names, descriptions)
auInfo?: Record<string, AUInfo>;
// Optional: Eye mesh node fallbacks
eyeMeshNodes?: { LEFT: string; RIGHT: string };
// Optional: Composite rotation definitions
compositeRotations?: CompositeRotation[];
}
interface BoneBinding {
node: string; // Semantic key (e.g., 'HEAD', 'JAW')
channel: 'rx' | 'ry' | 'rz' | 'tx' | 'ty' | 'tz';
scale: -1 | 1; // Direction multiplier
maxDegrees?: number; // Max rotation for this AU (for rotation channels)
maxUnits?: number; // Max translation (for translation channels)
}
interface Annotation {
name: string; // e.g., 'left_eye', 'head', 'full_body'
bones?: string[]; // Bone names to focus on (use for skinned parts)
meshes?: string[]; // Mesh names (only for non-skinned meshes)
objects?: string[]; // Any object names (['*'] for all)
paddingFactor?: number; // Zoom level (1.0 = tight, 2.0 = loose)
cameraAngle?: number; // View angle (0=front, 90=right, 180=back, 270=left)
}
| Semantic | CC4 | Mixamo | Generic |
|---|---|---|---|
| HEAD | CC_Base_Head | mixamorig:Head | Head |
| NECK | CC_Base_NeckTwist01 | mixamorig:Neck | Neck |
| JAW | CC_Base_JawRoot | mixamorig:Jaw | Jaw |
| EYE_L | CC_Base_L_Eye | mixamorig:LeftEye | Eye_L |
| EYE_R | CC_Base_R_Eye | mixamorig:RightEye | Eye_R |
| TONGUE | CC_Base_Tongue01 | - | Tongue |
| SPINE | CC_Base_Spine01 | mixamorig:Spine | Spine |
| AU | CC4 Pattern | ARKit Pattern | Generic |
|---|---|---|---|
| 1 (Inner Brow Raise) | Brow_Raise_Inner_L/R | browInnerUp | BrowInnerUp_L/R |
| 2 (Outer Brow Raise) | Brow_Raise_Outer_L/R | browOuterUp_L/R | BrowOuterUp_L/R |
| 12 (Smile) | Mouth_Smile_L/R | mouthSmile_L/R | Smile_L/R |
| 43/45 (Blink) | Eye_Blink_L/R | eyeBlink_L/R | Blink_L/R |
When building an in-app wizard to guide users through fixing mappings:
When implementing the AI wizard backend, use these prompt patterns:
Given a 3D model with the following bones:
${boneList.join('\n')}
And expecting these semantic bone mappings:
HEAD, NECK, JAW, EYE_L, EYE_R, TONGUE, SPINE
Analyze which bones likely correspond to each semantic key.
Consider naming patterns like:
- CC4: CC_Base_Head, CC_Base_JawRoot
- Mixamo: mixamorig:Head, mixamorig:Jaw
- Generic: Head, Jaw, LeftEye
Output a mapping from semantic key to actual bone name.
Given a 3D model with the following morph targets:
${morphList.join('\n')}
And needing to map these FACS Action Units:
- AU1: Inner Brow Raiser
- AU2: Outer Brow Raiser
- AU12: Lip Corner Puller (Smile)
...
Find morph targets that likely correspond to each AU.
Consider naming patterns like:
- CC4: Brow_Raise_Inner_L, Mouth_Smile_L
- ARKit: browInnerUp, mouthSmile_L
- Generic: BrowRaise_L, Smile_L
Output a mapping from AU ID to morph target names.
Given animation clip "${clipName}" with these bone tracks:
${trackSummary}
And these rotation patterns:
${rotationPatterns}
Identify which bones are:
1. Primary movers (large rotation range, consistent use)
2. Secondary/follower bones (smaller range, follow primary)
3. Idle/breathing bones (subtle constant motion)
Suggest which AU mappings would recreate this motion.
The Betta fish model is a skeletal-only model with 53 bones and embedded swimming animation. Here's the specific workflow for fixing its mappings:
Armature_rootJoint (root)
Bone_Armature (body root)
Bone001_Armature (HEAD)
Bone009-017_Armature (PECTORAL FINS - decorative head fins)
Bone002_Armature (BODY_FRONT)
Bone003_Armature (BODY_MID)
Bone018, Bone033 (VENTRAL FINS - belly fins)
Bone004_Armature (BODY_BACK)
Bone005_Armature (TAIL_BASE)
Bone020, Bone039+ (TAIL FIN chains)
Bone046-051 (THROAT_L / GILL_R - branchiostegal/operculum)
Bone006_Armature (DORSAL_ROOT)
Bone007, Bone008 (DORSAL FIN sides)
| AU Range | Category | Examples |
|---|---|---|
| 2-7 | Body Orientation | Turn L/R, Pitch Up/Down, Roll L/R |
| 12-15 | Tail (Caudal Fin) | Sweep L/R, Spread/Close |
| 20-27 | Pectoral Fins | Up/Down, Forward/Back (L/R) |
| 30-33 | Ventral/Pelvic Fins | Up/Down (L/R) |
| 40-41 | Head Tilt | Tilt L/R (via dorsal area) |
| 50-53 | Throat/Gill | Throat Expand/Contract, Gill Flare/Close |
| 61-64 | Eye Movement | Look L/R/Up/Down |
import { runFishMappingWizard, printWizardResults } from './services/mappingWizardService';
import { FISH_AU_MAPPING_CONFIG } from 'loomlarge';
// After model loads in CharacterGLBScene onReady:
const result = runFishMappingWizard(
model, // THREE.Object3D
meshes, // THREE.Mesh[]
animations, // THREE.AnimationClip[]
FISH_AU_MAPPING_CONFIG
);
// For console output (Claude skill):
printWizardResults(result.outputs);
// For UI display:
// - result.outputs.modelSummary
// - result.outputs.boneHierarchy
// - result.outputs.analysisReport
// - result.outputs.prompts (for AI backend)
// - result.outputs.correctedPresetCode
Once mappings are correct, you can create animation snippets that combine AU keyframes:
// Example: Swimming animation snippet
const swimmingSnippet = {
name: 'swimming',
duration: 2000,
keyframes: [
// Tail sweep cycle
{ time: 0, au: 12, value: 0.5 }, // Tail left
{ time: 500, au: 13, value: 0.5 }, // Tail right
{ time: 1000, au: 12, value: 0.5 }, // Tail left
{ time: 1500, au: 13, value: 0.5 }, // Tail right
{ time: 2000, au: 12, value: 0 }, // Reset
// Pectoral fin motion
{ time: 0, au: 20, value: 0.3 }, // Pectoral L up
{ time: 250, au: 21, value: 0.3 }, // Pectoral L down
// ... etc
],
};
boneNodes matches the actual bone namerz for horizontal sweep, rx for verticalscale (-1 or 1) in the bindingmaxDegrees in the binding// Test a single AU from browser console:
window.engine.setAU(12, 0.5); // Tail sweep left at 50%
window.engine.setAU(13, 0.5); // Tail sweep right at 50%
window.engine.setAU(20, 1); // Left pectoral up full