Skip to content

BabylonJS - Bones and Skeletons

Skeletal animation is a fundamental technique for animating complex characters and objects in 3D graphics. It involves defining an internal hierarchy of ‘bones’ (transform nodes) that form a ‘skeleton’. The visible geometry (the mesh ‘skin’) is then attached or ‘skinned’ to these bones. When the bones are moved or rotated, the mesh deforms accordingly, allowing for realistic character movement like walking, running, or waving.

Babylon.js provides Bone and Skeleton classes, along with associated animation capabilities. While you can create skeletons and bones programmatically using these classes, it’s far more common and practical to create them in 3D modeling software (like Blender, 3ds Max, Maya) and export them along with the mesh and animations, typically using the glTF format (.glb or .gltf).

When you load a model file (like a glTF) that contains skeletal animations, Babylon.js automatically parses the skeleton and bone data. You can access the skeletons associated with the loaded meshes.

import * as BABYLON from '@babylonjs/core';
import '@babylonjs/loaders/glTF'; // Ensure glTF loader is imported
// Assuming 'scene' is defined
async function loadModelWithSkeleton() {
try {
const result = await BABYLON.SceneLoader.ImportMeshAsync(
null, // Load all meshes
"models/", // Path to models folder
"character.glb", // Your animated model file
scene
);
if (result.skeletons && result.skeletons.length > 0) {
const mainSkeleton = result.skeletons[0];
console.log(`Loaded skeleton: ${mainSkeleton.name} with ${mainSkeleton.bones.length} bones.`);
// Access individual bones (e.g., by index or name)
const headBone = mainSkeleton.bones.find(bone => bone.name === "mixamorig_Head"); // Example name
if (headBone) {
console.log("Found head bone:", headBone.getIndex());
}
// Meshes are often automatically linked to the skeleton
const characterMesh = result.meshes[1]; // Often the mesh is not the first element (root node might be)
if (characterMesh && characterMesh.skeleton === mainSkeleton) {
console.log(`Mesh '${characterMesh.name}' is skinned to skeleton '${mainSkeleton.name}'.`);
}
// Start an animation (if the model has animation groups)
if (result.animationGroups && result.animationGroups.length > 0) {
const walkAnimation = result.animationGroups.find(ag => ag.name === "Walk");
if (walkAnimation) {
walkAnimation.start(true, 1.0, walkAnimation.from, walkAnimation.to, false);
console.log("Playing 'Walk' animation.");
} else {
// Play the first animation group if specific name not found
result.animationGroups[0].start(true, 1.0, result.animationGroups[0].from, result.animationGroups[0].to, false);
console.log(`Playing first animation group: ${result.animationGroups[0].name}`);
}
}
} else {
console.log("No skeletons found in the loaded model.");
}
} catch (error) {
console.error("Error loading model:", error);
}
}
loadModelWithSkeleton();

A common use case is attaching accessories or equipment (like a hat, sword, or flashlight) to a specific bone of an animated character, so the accessory moves correctly with the character’s animation.

You can achieve this using the mesh.attachToBone(bone, parentMesh) method.

// Assuming 'mainSkeleton', 'characterMesh', and 'scene' are defined
// And we have found the 'headBone' as in the previous example
if (headBone && characterMesh) {
// Create a hat mesh
const hatMesh = BABYLON.MeshBuilder.CreateCylinder("hat", {
diameterTop: 0.4,
diameterBottom: 0.8,
height: 0.5,
tessellation: 24
}, scene);
const hatMaterial = new BABYLON.StandardMaterial("hatMat", scene);
hatMaterial.diffuseColor = new BABYLON.Color3(0.2, 0.2, 0.2); // Dark color
hatMesh.material = hatMaterial;
// Attach the hat to the head bone of the character mesh
hatMesh.attachToBone(headBone, characterMesh);
// Optional: Adjust the hat's position/rotation relative to the bone's origin
hatMesh.position.y = 0.2; // Move slightly up relative to bone origin
hatMesh.rotation.x = Math.PI / 12; // Tilt slightly forward
console.log("Hat attached to head bone.");
}

Now, when the character’s animation plays and the headBone moves or rotates, the hatMesh will follow automatically because it’s attached.

Demo: Loading Animated Character and Attaching Items

Section titled “Demo: Loading Animated Character and Attaching Items”

This demo loads an animated character model (like the ‘Dude’ model, commonly available in glTF format) and attaches simple shapes to its hand and head bones.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Babylon.js - Bones and Skeletons Demo</title>
<script src="https://cdn.babylonjs.com/babylon.js"></script>
<script src="https://cdn.babylonjs.com/loaders/babylonjs.loaders.min.js"></script>
<style>
html, body { overflow: hidden; width: 100%; height: 100%; margin: 0; padding: 0; }
#renderCanvas { width: 100%; height: 100%; touch-action: none; }
</style>
</head>
<body>
<canvas id="renderCanvas"></canvas>
<script type="text/javascript">
const canvas = document.getElementById("renderCanvas");
const engine = new BABYLON.Engine(canvas, true);
const createScene = async function () {
const scene = new BABYLON.Scene(engine);
scene.clearColor = new BABYLON.Color4(0.8, 0.9, 1.0, 1.0); // Light blue sky
const camera = new BABYLON.ArcRotateCamera("Camera", Math.PI / 2, Math.PI / 2.5, 3, new BABYLON.Vector3(0, 1, 0), scene);
camera.attachControl(canvas, true);
camera.wheelPrecision = 50;
camera.lowerRadiusLimit = 1;
camera.upperRadiusLimit = 10;
const light = new BABYLON.HemisphericLight("light", new BABYLON.Vector3(0, 1, 0), scene);
light.intensity = 0.8;
const pointLight = new BABYLON.PointLight("pointLight", new BABYLON.Vector3(0, 1.5, -1), scene);
pointLight.intensity = 0.5;
const ground = BABYLON.MeshBuilder.CreateGround("ground", {width: 10, height: 10}, scene);
try {
// Load the animated character (replace path/filename)
// Ensure your model has bones named like 'mixamorig_RightHand' and 'mixamorig_Head'
// You can inspect bone names using console.log(skeleton.bones)
const result = await BABYLON.SceneLoader.ImportMeshAsync(null, "models/", "dude.glb", scene);
const dudeMesh = result.meshes.find(m => m.skeleton); // Find the mesh with a skeleton
const skeleton = result.skeletons[0];
if (dudeMesh && skeleton) {
dudeMesh.position = new BABYLON.Vector3(0, 0, 0); // Position dude on ground
dudeMesh.scaling.scaleInPlace(0.01); // Dude model might be large
camera.target = dudeMesh.position.add(new BABYLON.Vector3(0,1,0)); // Target camera near dude center
console.log("Dude model loaded.");
// Find specific bones (Names depend on your model! Inspect in Blender/console)
const rightHandBone = skeleton.bones.find(b => b.name === "mixamorig_RightHand");
const headBone = skeleton.bones.find(b => b.name === "mixamorig_Head");
// Attach a small sphere to the right hand
if (rightHandBone) {
const handItem = BABYLON.MeshBuilder.CreateSphere("handItem", { diameter: 0.05 }, scene);
const itemMat = new BABYLON.StandardMaterial("itemMat", scene);
itemMat.diffuseColor = BABYLON.Color3.Red();
handItem.material = itemMat;
handItem.attachToBone(rightHandBone, dudeMesh);
handItem.position = new BABYLON.Vector3(0.05, 0.05, 0); // Adjust relative position
console.log("Attached item to right hand.");
} else {
console.warn("Right hand bone not found.");
}
// Attach a small box to the head
if (headBone) {
const headItem = BABYLON.MeshBuilder.CreateBox("headItem", { size: 0.08 }, scene);
const headItemMat = new BABYLON.StandardMaterial("headItemMat", scene);
headItemMat.diffuseColor = BABYLON.Color3.Blue();
headItem.material = headItemMat;
headItem.attachToBone(headBone, dudeMesh);
headItem.position.y = 0.15; // Adjust relative position
console.log("Attached item to head.");
} else {
console.warn("Head bone not found.");
}
// Play animation (find an appropriate animation group name)
const idleAnim = result.animationGroups.find(ag => ag.name.toLowerCase().includes("idle")); // Try to find an idle anim
if (idleAnim) {
idleAnim.start(true, 1.0, idleAnim.from, idleAnim.to, false);
console.log(`Playing animation: ${idleAnim.name}`);
} else if (result.animationGroups.length > 0) {
result.animationGroups[0].start(true, 1.0, result.animationGroups[0].from, result.animationGroups[0].to, false);
console.log(`Playing first animation group: ${result.animationGroups[0].name}`);
}
} else {
console.error("Could not find skinned mesh or skeleton in loaded model.");
}
} catch (error) {
console.error("Error loading character model:", error);
// Add a fallback object if loading fails
const fallback = BABYLON.MeshBuilder.CreateBox("fallback", {}, scene);
fallback.position.y = 0.5;
}
return scene;
};
createScene().then(sceneToRender => {
engine.runRenderLoop(function () {
if (sceneToRender) {
sceneToRender.render();
}
});
});
window.addEventListener("resize", function () {
engine.resize();
});
</script>
</body>
</html>

For the demo to work, you need a glTF file (e.g., dude.glb) placed in a models folder. This model must contain a mesh skinned to a skeleton, and the skeleton should have bones with names similar to mixamorig_RightHand and mixamorig_Head. You also need at least one animation group defined in the file. You might need to inspect your specific model in Blender or log skeleton.bones to the console to find the correct bone names.

If the dude.glb model loads correctly, the demo will show an animated character standing on a ground plane. A small red sphere will be attached to the character’s right hand, and a small blue box will be attached to its head. As the character performs its idle (or first available) animation, the red sphere and blue box will move rigidly along with the corresponding hand and head bones.

While less common for complex characters, you can define bones and skeletons programmatically:

// Create a skeleton
const skeleton = new BABYLON.Skeleton("mySkeleton", "skel01", scene);
// Create bones (name, parent skeleton, parent bone (optional), matrix)
const rootBone = new BABYLON.Bone("root", skeleton, null, BABYLON.Matrix.Identity());
const childBone = new BABYLON.Bone("child", skeleton, rootBone, BABYLON.Matrix.Translation(0, 1, 0)); // Position relative to parent
// ... add more bones
// Link skeleton to a mesh (requires vertex weights/indices matching the bones)
// mesh.skeleton = skeleton;
// This part (setting weights) is complex and usually done in modeling software.