Skip to content

Instantly share code, notes, and snippets.

@sketchpunk
Last active December 29, 2025 18:31
Show Gist options
  • Select an option

  • Save sketchpunk/d91aab8e20186cc3a37ae8ed70db0ef9 to your computer and use it in GitHub Desktop.

Select an option

Save sketchpunk/d91aab8e20186cc3a37ae8ed70db0ef9 to your computer and use it in GitHub Desktop.
ThreeJS GLTFLoader - Parse out skeleton & clips
// let [clips, aSkel, aGrp] = await loadAnimations( `${url}/anim/source-animation.glb` );
// let aHelper = new THREE.SkeletonHelper( aSkel.bones[0] );
// App.scene.add( aGrp || aSkel.bones[0], aHelper );
// const mixer = new THREE.AnimationMixer( new THREE.Object3D() );
// const action = this.mixer.clipAction( clips[0], aSkel.bones[0] );
// action.play();
async function loadAnimations( url ){
const tf = await new GLTFLoader().loadAsync( url );
const [ skel, grp ] = parseGLTFSkeleton( tf );
return [ tf.animations ?? [], skel, grp ];
}
function parseGLTFSkeleton( obj ){
const json = obj.parser.json; // GLTF Json Data
const n = json.nodes; // Shortcut to nodes
const joints = json.skins[0].joints; // Shortcut to skeleton joint node indices
const map = {}; // Map Node Index to Bone Index
const bones = []; // Collection of bones to build skeleton
let j; // GLTF Joint
let b; // 3JS Bone
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Create Bones
for( const [i,ni] of joints.entries() ){
j = n[ni]
if( !j.isBone ) continue;
// NOTE: Remove periods, clip removes it from track names which causes not
// found errors cause of name mismatch
b = new THREE.Bone();
b.name = j.name.replaceAll('.', '');
map[ ni ] = i;
// console.log( b.name )
if( j.rotation ) b.quaternion.fromArray( j.rotation );
if( j.translation ) b.position.fromArray( j.translation );
if( j.scale ) b.scale.fromArray( j.scale );
bones.push( b );
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Create Parent-Child Relationship
for( const [i,ni] of joints.entries() ){
j = n[ni];
if( j.isBone && j.children ){
b = bones[i];
for( const c of j.children ) b.add( bones[ map[c] ] );
}
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
const skel = new THREE.Skeleton( bones );
// const hlpr = new THREE.SkeletonHelper( bones[0] );
// App.scene.add( bones[0], hlpr );
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Ancestors transforms as Groups
// NOTE: Blender tends to add parent nodes that transform the skeleton's root
const nFind = ( ni )=>{
for( const [i, o] of n.entries() ){
if( o.children && o.children.includes( ni ) ) return i;
}
return null;
};
const groups = [];
let ni = joints[0];
do{
if( ( ni = nFind( ni ) ) !== null ){
j = n[ni];
if( j.translation || j.rotation || j.scale ){
const g = new THREE.Group();
g.name = j.name;
if( j.translation ) g.position.fromArray( j.translation );
if( j.rotation ) g.quaternion.fromArray( j.rotation );
if( j.scale ) g.scale.fromArray( j.scale );
// Add last transform group as a child to the new one
if( groups.length > 0 ) g.add( groups.at( -1 ) );
groups.push( g );
}
}
}while( ni !== null );
let grp = null;
if( groups.length > 0 ){
groups[0].add( skel.bones[0] ); // Root lives under most inner group
grp = groups.at( -1 ); // Save outer most group for adding to scene
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
return [ skel, grp ];
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment