A skinned mesh stores up to four (or eight) bone indices and weights per vertex; its Skeleton holds the bones, whose matrices Babylon 89,603 uploads every frame (in a texture by default) for the vertex shader to blend. For glTF files each Bone is linked to a TransformNode, which the animation groups animate:
const { meshes, skeletons: [skeleton] } = await BABYLON.ImportMeshAsync('fox.glb', scene);
const fox = meshes.find((mesh) => mesh.skeleton);
console.log(`${fox.getTotalVertices()} vertices, ${fox.numBoneInfluencers} influences each, ` +
`${skeleton.bones.length} bones`);
for (const bone of skeleton.bones.slice(1, 4)) {
console.log(` ${bone.name} <- ${bone.getParent().name}, node ${bone.getTransformNode().name}`);
}Output
1728 vertices, 4 influences each, 24 bones b_Root_00 <- _rootJoint, node b_Root_00 b_Hip_01 <- b_Root_00, node b_Hip_01 b_Spine01_02 <- b_Hip_01, node b_Spine01_02
To bend a bone, rotate its linked node; attachToBone(bone, mesh) puts a prop in a paw, Debug.SkeletonViewer draws the bones, and BoneIKController and BoneLookController aim limbs and heads.
<!doctype html>
<style>
body { margin: 0; background: #f7f3ec; font: 13px system-ui, sans-serif; color: #333; }
canvas { width: 100%; max-width: 600px; height: 310px; display: block; touch-action: none; outline: none; }
#info { max-width: 600px; margin: 0; padding: 4px 8px; font-family: monospace; white-space: pre; }
</style>
<canvas id="view"></canvas>
<pre id="info"></pre>
<script src="https://cdn.jsdelivr.net/npm/babylonjs@9.28.0/babylon.js"></script>
<script>
// The book inspects the Fox sample's skeleton. Here a skinned mesh is built in code instead:
// a flexible lamp neck whose vertices each follow two of four chained bones.
const canvas = document.getElementById('view');
const engine = new BABYLON.Engine(canvas, true);
const scene = new BABYLON.Scene(engine);
scene.clearColor = BABYLON.Color4.FromHexString('#f7f3ecff');
const camera = new BABYLON.ArcRotateCamera('cam', -Math.PI / 2, 1.3, 4, new BABYLON.Vector3(0, 1, 0), scene);
camera.attachControl(canvas, true);
new BABYLON.HemisphericLight('sky', new BABYLON.Vector3(0.3, 1, -0.5), scene);
// Four bones, each 0.5 m above its parent: a hierarchy like a limb
const skeleton = new BABYLON.Skeleton('neck', 'neck', scene);
const bones = [];
for (let i = 0; i < 4; i++) {
const local = BABYLON.Matrix.Translation(0, i ? 0.5 : 0, 0);
bones.push(new BABYLON.Bone('bone' + i, skeleton, bones[i - 1] || null, local));
}
// The mesh: a tube 2 m tall; each vertex gets 4 bone indices and 4 weights
const neck = BABYLON.MeshBuilder.CreateCylinder('neck', { height: 2, diameter: 0.14, tessellation: 16, subdivisions: 24 }, scene);
neck.bakeTransformIntoVertices(BABYLON.Matrix.Translation(0, 1, 0)); // base at y = 0
const positions = neck.getVerticesData(BABYLON.VertexBuffer.PositionKind);
const indices = [], weights = [];
for (let i = 0; i < positions.length; i += 3) {
const along = Math.min(Math.max(positions[i + 1] / 0.5, 0), 3.999); // 0..4 bone lengths
const bone = Math.floor(along), t = along - bone;
indices.push(bone, Math.min(bone + 1, 3), 0, 0); // blend this bone with the next
weights.push(1 - t, t, 0, 0);
}
neck.setVerticesData(BABYLON.VertexBuffer.MatricesIndicesKind, indices);
neck.setVerticesData(BABYLON.VertexBuffer.MatricesWeightsKind, weights);
neck.skeleton = skeleton;
neck.numBoneInfluencers = 2;
neck.material = new BABYLON.StandardMaterial('brass', scene);
neck.material.diffuseColor = BABYLON.Color3.FromHexString('#c9a14a');
const shade = BABYLON.MeshBuilder.CreateCylinder('shade', { height: 0.35, diameterTop: 0.15, diameterBottom: 0.5 }, scene);
shade.material = new BABYLON.StandardMaterial('shadeMat', scene);
shade.material.diffuseColor = BABYLON.Color3.FromHexString('#1f5f8b');
shade.attachToBone(bones[3], neck); // a prop that follows the last bone
shade.position.y = 0.55;
shade.rotation.z = Math.PI / 2;
const base = BABYLON.MeshBuilder.CreateCylinder('base', { height: 0.08, diameter: 0.6 }, scene);
base.material = shade.material;
// Draw the bones on top of the mesh
const viewer = new BABYLON.Debug.SkeletonViewer(skeleton, neck, scene, false, 3, { displayMode: BABYLON.Debug.SkeletonViewer.DISPLAY_LINES });
viewer.color = BABYLON.Color3.FromHexString('#b5452f');
scene.onBeforeRenderObservable.add(() => {
const t = performance.now() / 1000;
bones.slice(1).forEach((bone, i) => { // each bend adds to its parent's
bone.setRotation(new BABYLON.Vector3(0, 0, Math.sin(t + i * 0.6) * 0.45), BABYLON.Space.LOCAL);
});
});
document.getElementById('info').textContent =
`${neck.getTotalVertices()} vertices, ${neck.numBoneInfluencers} influences each, ${skeleton.bones.length} bones\n` +
skeleton.bones.slice(1).map((bone) => ` ${bone.name} <- ${bone.getParent().name}`).join('\n');
engine.runRenderLoop(() => scene.render());
addEventListener('resize', () => engine.resize());
</script>