Skip to content

Three.js Bones and Skinning — Skeletal Animation

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about Three.js Bones and Skinning. We cover key concepts, practical examples, and best practices to help you master this topic.

Three.js bones and skinning enable character animation through a skeletal hierarchy where a mesh deforms based on the movement of its underlying bone structure.

What You'll Learn

By the end of this guide, you will create a bone hierarchy, bind a mesh to bones with skin weights, animate bones with keyframes, import skinned models from glTF, and control animation playback.

Why Bones Matter

Static 3D characters look lifeless. Bones enable realistic deformation — arms swing, legs walk, faces Express. Durga Antivirus Pro uses skinned characters for training simulations where animated agents demonstrate security procedures.

flowchart TD
    A[Skeleton Root Bone] --> B[Hip Bone]
    B --> C[Spine Bone]
    C --> D[Head Bone]
    B --> E[Left Arm]
    E --> F[Left Forearm]
    B --> G[Right Arm]
    G --> H[Right Forearm]
    B --> I[Left Leg]
    I --> J[Left Calf]
    B --> K[Right Leg]
    K --> L[Right Calf]

Creating a Bone Hierarchy

var rootBone = new THREE.Bone();
rootBone.position.y = 0;

var upperBone = new THREE.Bone();
upperBone.position.y = 1.5;
rootBone.add(upperBone);

var headBone = new THREE.Bone();
headBone.position.y = 1.0;
upperBone.add(headBone);

var skeleton = new THREE.Skeleton([rootBone, upperBone, headBone]);

var skeletonHelper = new THREE.SkeletonHelper(rootBone);
scene.add(skeletonHelper);

Expected output: A visible skeleton hierarchy with three bones — root, upper, and head — displayed as colored lines and spheres.

Creating a Skinned Mesh

// Create a simple box geometry with enough vertices for deformation
var geometry = new THREE.BoxGeometry(1, 2.5, 0.8, 4, 8, 4);

// Skin indices — which bones affect each vertex
var position = geometry.attributes.position;
var skinIndices = [];
var skinWeights = [];

for (var i = 0; i < position.count; i++) {
    var y = position.getY(i);

    if (y > 0.5) {
        // Upper body — affected by upperBone (bone index 1)
        skinIndices.push(1, 0, 0, 0);
        skinWeights.push(1, 0, 0, 0);
    } else if (y < -0.5) {
        // Lower body — affected by rootBone (bone index 0)
        skinIndices.push(0, 0, 0, 0);
        skinWeights.push(1, 0, 0, 0);
    } else {
        // Middle — blend between root and upper
        var blend = (y + 0.5) / 1.0;
        skinIndices.push(0, 1, 0, 0);
        skinWeights.push(1 - blend, blend, 0, 0);
    }
}

geometry.setAttribute('skinIndex', new THREE.Uint16BufferAttribute(skinIndices, 4));
geometry.setAttribute('skinWeight', new THREE.Float32BufferAttribute(skinWeights, 4));

var material = new THREE.MeshStandardMaterial({
    color: 0x4488ff,
    skinning: true
});

var mesh = new THREE.SkinnedMesh(geometry, material);
mesh.bind(skeleton);
mesh.add(rootBone);
scene.add(mesh);

Expected output: A box-shaped character with bones inside. Moving the upper bone causes the top half of the mesh to deform while the bottom stays in place.

Animating Bones

var mixer = new THREE.AnimationMixer(mesh);

var times = [0, 1, 2];
var values = [
    0, 0, 0,     // frame 0 — upright
    0.5, 0, 0,   // frame 1 — lean right
    -0.5, 0, 0   // frame 2 — lean left
];

var track = new THREE.VectorKeyframeTrack(
    '.bones[1].position',
    times,
    values
);

var clip = new THREE.AnimationClip('sway', 2, [track]);
var action = mixer.clipAction(clip);
action.play();

// In animation loop:
function animate() {
    var delta = clock.getDelta();
    mixer.update(delta);
    renderer.render(scene, camera);
}

Expected output: The character sways left and right in a looping animation as the upper bone position animates.

Importing Skinned Models From glTF

var loader = new GLTFLoader();
loader.load('models/character.glb', function(gltf) {
    var model = gltf.scene;
    scene.add(model);

    var mixer = new THREE.AnimationMixer(model);
    var action = mixer.clipAction(gltf.animations[0]);
    action.play();

    // Store mixer for animation loop
    mixers.push(mixer);
});

Expected output: A fully skinned character loaded from glTF with its animation playing automatically.

Common Mistakes

1. Missing skinning: true on Material

SkinnedMesh requires skinning: true in the material. Without it, skinIndices and skinWeights are ignored.

2. Bones Not Added to Scene

Bones must be part of the scene graph. Add the root bone to the mesh or scene: mesh.add(rootBone).

3. Incorrect Skin Weight Sum

Each vertex's skin weights must sum to 1.0. If weights sum to less than 1, vertex position is incorrect.

4. Bone Indices Out of Range

Bone indices must be valid indices into the skeleton's bone array. Out-of-range indices cause errors.

5. Not Updating AnimationMixer

The mixer must be updated each frame with delta time. Without this, animations do not advance.

Practice Questions

Q1: What is the maximum number of bones that can affect a single vertex? A: Four. Three.js supports up to 4 bone influences per vertex via skinIndex and skinWeight attributes.

Q2: What does material.skinning = true do? A: It enables the GPU skinning shader that deforms vertices based on bone transforms and weights.

Q3: How do you play a loaded animation? A: Create an AnimationMixer, call clipAction, and play the action. Update the mixer in the animation loop.

Q4: What is the purpose of skin weights? A: Skin weights control how much each bone influences each vertex. A weight of 1 means full influence.

Q5: How do you blend between two animations? A: Use action.crossFadeFrom(otherAction, duration) or adjust action.weight for blending.

Challenge: Create a simple 3-bone character that performs a walking cycle animation. The root bone moves up and down, the upper bone rotates side to side, and the head bone bobs.

FAQ

What is the difference between Bone and SkinnedMesh?

Bone is a transformation node in the skeleton. SkinnedMesh is the mesh that deforms according to the bones. Multiple skinned meshes can use one skeleton.

Can I skin with more than 4 bones per vertex?

No. Three.js is limited to 4 bone influences per vertex. Use additional vertices with shared positions for more influences.

How are animations stored in glTF?

glTF stores animations as keyframe tracks targeting specific node paths (translation, rotation, scale). Three.js GLTFLoader converts these to AnimationClips.

What is an AnimationMixer?

The mixer manages multiple animation actions on a single object, handling blending, cross-fading, and time synchronization.

Can I export skinned animations from Blender?

Yes. Export as glTF with animations. Enable 'Include > Animations' and 'Deformation > Skin' in the Blender glTF export settings.

Try It Yourself — Simple Character Animation

Build a complete page with a skinned box character that sways with a smooth animation.

<!DOCTYPE html>
<html>
<head>
    <title>Bones and Skinning Demo</title>
    <style>
        body { margin: 0; overflow: hidden; background: #1a1a2e; }
    </style>
</head>
<body>
<script type="importmap">
{
    "imports": {
        "three": "https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js",
        "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"
    }
}
</script>
<script type="module">
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";

var scene = new THREE.Scene();
scene.background = new THREE.Color(0x1a1a2e);
var camera = new THREE.PerspectiveCamera(50, innerWidth / innerHeight, 0.1, 100);
camera.position.set(4, 3, 6);
var renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(devicePixelRatio);
document.body.appendChild(renderer.domElement);
var controls = new OrbitControls(camera, renderer.domElement);

var ambient = new THREE.AmbientLight(0x404060);
scene.add(ambient);
var dirLight = new THREE.DirectionalLight(0xffffff, 1);
dirLight.position.set(5, 10, 7);
scene.add(dirLight);

// Create simple bones
var rootBone = new THREE.Bone();
rootBone.position.y = 0;
var upperBone = new THREE.Bone();
upperBone.position.y = 1.2;
rootBone.add(upperBone);
var headBone = new THREE.Bone();
headBone.position.y = 1.0;
upperBone.add(headBone);

var skeleton = new THREE.Skeleton([rootBone, upperBone, headBone]);

// Create geometry
var geo = new THREE.CylinderGeometry(0.8, 1, 2.5, 8, 6);
var pos = geo.attributes.position;
var skinIdx = [];
var skinWgt = [];

for (var i = 0; i < pos.count; i++) {
    var y = pos.getY(i);
    if (y > 0.4) {
        skinIdx.push(2, 0, 0, 0);
        skinWgt.push(1, 0, 0, 0);
    } else if (y > -0.4) {
        var blend = (y + 0.4) / 0.8;
        skinIdx.push(1, 2, 0, 0);
        skinWgt.push(1 - blend, blend, 0, 0);
    } else {
        skinIdx.push(0, 1, 0, 0);
        skinWgt.push(1, 0, 0, 0);
    }
}

geo.setAttribute('skinIndex', new THREE.Uint16BufferAttribute(skinIdx, 4));
geo.setAttribute('skinWeight', new THREE.Float32BufferAttribute(skinWgt, 4));

var mat = new THREE.MeshStandardMaterial({
    color: 0x4488ff,
    skinning: true,
    roughness: 0.4,
    metalness: 0.1
});

var mesh = new THREE.SkinnedMesh(geo, mat);
mesh.bind(skeleton);
mesh.add(rootBone);
scene.add(mesh);

var skeletonHelper = new THREE.SkeletonHelper(mesh);
scene.add(skeletonHelper);

var mixer = new THREE.AnimationMixer(mesh);
var times = [0, 1, 2];
var track = new THREE.VectorKeyframeTrack(
    '.bones[1].position', times, [0, 1.2, 0, 0.3, 1.2, 0, -0.3, 1.2, 0]
);
var clip = new THREE.AnimationClip('sway', 2, [track]);
var action = mixer.clipAction(clip);
action.play();

var clock = new THREE.Clock();
function animate() {
    mixer.update(clock.getDelta());
    controls.update();
    renderer.render(scene, camera);
    requestAnimationFrame(animate);
}
animate();

window.addEventListener("resize", function() {
    camera.aspect = innerWidth / innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(innerWidth, innerHeight);
});
</script>
</body>
</html>

What's Next

Build a complete project applying all Three.js concepts.

Project — Complete Three.js project tutorial. Performance Optimization — Revisit optimization techniques.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro