Skeletal Rigging Basics: How Bones and Skin Weights Drive a 3D Character
Learn how joints, bone hierarchies, and skin weights make a 3D character move, and how a rigged GLB actually deforms in web and game engines.
Skeletal rigging is the technique that lets a single mesh bend like a living thing. Instead of animating thousands of vertices by hand, you build a simplified skeleton of joints, attach every vertex to one or more of those joints with numeric skin weights, and then animate only the skeleton. The mesh follows. Every walking character in a game, every waving avatar in a web configurator, and every animated GLB you download from an asset store works this way.
This guide explains the three layers of a rigged character, the math that moves each vertex in plain terms, how the glTF format stores all of it, and a practical first-rig workflow with the failure cases you will actually hit. The running example is a low-poly explorer character: 8,450 vertices, 16,896 triangles, one material, and a skeleton of 62 joints, exported as a GLB with a walk cycle.
The three layers of a rigged character#
A rigged asset is three cooperating structures, and keeping them separate in your head prevents most beginner confusion:
- The mesh is the visible surface: vertices, triangles, normals, and UVs. On its own it is a statue.
- The skeleton is an invisible hierarchy of joints. It never renders. It exists only to define coordinated motion.
- The binding (the "skin") is the table of weights that says how strongly each vertex follows each joint.
Animation is a fourth, separable layer: an animation clip changes joint rotations and positions over time, and the binding translates that skeletal motion into surface deformation. This separation is why one walk cycle can drive many different character meshes, and why services such as Mixamo can apply a library of motion clips to a character you upload: the clips target a skeleton, not your specific vertices.
The Khronos glTF 2.0 specification mirrors this split exactly. A glTF file stores the skeleton as ordinary scene nodes, the binding as a skin object, and the motion as animation objects that retarget node transforms. Nothing about the mesh's triangles changes at runtime; only the positions computed from them do.
Bones and joints: what the skeleton actually stores#
Modeling tools show a skeleton as a chain of elongated octahedrons or sticks, which is why artists say bone. Runtime formats store something simpler: a joint, which is one transform — a position, a rotation, and usually a scale — parented to another joint. The "bone" you see in Blender is just the visual segment between a joint and its child. glTF has no bone primitive at all; a skin is a list of node indices, and each of those nodes is a joint.
For the explorer character, the 62 joints break down roughly like this:
- 1 root joint at the hips, the ancestor of everything;
- 4 spine and neck joints plus 1 head joint;
- 2 clavicle, 2 shoulder, 2 elbow, and 2 wrist joints;
- 30 finger joints (3 per finger, 5 fingers, 2 hands);
- 2 hip, 2 knee, 2 ankle, and 2 toe joints;
- 12 helper joints: forearm twist, upper-arm twist, and similar assistants covered later.
Fingers dominate the count. If your character never needs articulate hands — a distant background character, for instance — cutting to 1 joint per finger or none at all removes up to 24 joints with no visible cost at that distance. Joint count is a budget like triangle count, and it is spent where the camera looks.
The Blender manual's armature introduction describes the same structure from the authoring side: an armature object contains bones, bones form parent chains, and posing the armature deforms any mesh bound to it.
The hierarchy: why moving one bone moves many#
Joints compose transforms down the chain. A joint's final position in the world is its own local transform multiplied by its parent's, and its parent's parent's, up to the root. Rotate the shoulder 40 degrees and the elbow, wrist, and all 15 finger joints of that hand swing along without any of their own values changing. This is forward kinematics, and it is the reason a skeleton is a hierarchy instead of a flat list.
Two practical consequences follow directly from that math:
- Joint placement is destiny. The joint is the pivot. If the elbow joint sits 2 centimeters behind the arm's visual center, every bend will crease in the wrong place, and no amount of weight painting will fix it. Place joints at the anatomical rotation centers — through the middle of the limb at the fold line — before touching weights.
- The root carries the character. Moving the whole character means moving the root joint, not every joint at once. Game engines rely on this: locomotion systems read or drive root motion, and a rig whose root is not a common ancestor of everything breaks them.
Rotation propagates but so do mistakes. A scale of 1.2 accidentally left on the spine multiplies through the head and both arm chains. Before binding, apply or freeze transforms so every joint starts from a clean identity scale — a step every export checklist repeats because forgetting it produces bizarre, hard-to-diagnose deformation later.
Skinning: how each vertex follows the skeleton#
Binding mesh to skeleton is called skinning. The near-universal runtime method is linear blend skinning (LBS), and you can state it in one sentence: each vertex's final position is a weighted average of where its assigned joints would carry it.
Concretely, for a vertex with weights on the shoulder (0.7) and the elbow (0.3):
- Compute where the vertex would land if it rigidly followed the shoulder. Take 70% of that position.
- Compute where it would land if it rigidly followed the elbow. Take 30% of that.
- Add them. That blended point is the rendered position.
Two supporting pieces make step 1 and 2 well defined. The bind pose is the skeleton pose in which the mesh was attached — usually a T-pose or A-pose. Each joint carries an inverse bind matrix, which converts a mesh-space vertex into that joint's local space as it was at bind time. "Follow the shoulder rigidly" then means: apply the shoulder's inverse bind matrix, then the shoulder's current world transform. When the current pose equals the bind pose the two matrices cancel and the mesh is exactly undeformed — a useful sanity check. The glTF skinning tutorial walks through this same computation with a worked two-joint example, and the glTF specification defines the per-joint skinning matrix as the product of the joint's global transform and its inverse bind matrix.
Normals deform too. A vertex normal blended with the same weights keeps lighting consistent as the surface bends; engines handle this automatically, but it explains why lighting seams can appear where weights change abruptly.
Skin weights in practice: the four-influence budget#
Skin weights obey two rules that formats and engines enforce:
- Weights are non-negative and sum to 1.0 per vertex. The glTF specification requires normalized weights, and authoring tools maintain the invariant for you — Blender's weight tools offer Auto Normalize so that painting one influence up pushes the others down proportionally.
- A vertex gets a small, fixed number of influences — almost always 4. glTF stores joints and weights in per-vertex attributes named JOINTS_0 and WEIGHTS_0, each holding exactly 4 components; more influences require a second attribute set that many engines never read. three.js's SkinnedMesh consumes precisely this structure as skinIndex and skinWeight vertex attributes, and Unity's Quality Settings expose a Skin Weights option that caps runtime influences at 1, 2, or 4 per vertex (with an Unlimited option for film-style rigs).
Four is not a stingy limit; it is usually the right artistic number. A mid-forearm vertex might weight 0.6 forearm, 0.25 elbow, 0.15 forearm-twist. A belly vertex might be 1.0 hips and nothing else. Vertices influenced by 6 or 8 joints do not look smoother — they look mushy, because no single joint clearly owns them, and they will silently change appearance when an engine re-normalizes them down to its 4-influence cap at import. Author within the budget you will ship with.
Weight quality has a bigger visible payoff than weight quantity. The classic beginner artifact — the shin stretching when the opposite leg lifts — is always a few stray vertices weighted 0.05 to the wrong leg's joint. Small wrong weights on distant joints are the main thing weight painting exists to remove.
How a GLB stores the rig#
Understanding the file layout demystifies engine behavior. In a rigged GLB:
- Joints are ordinary nodes in the scene hierarchy, with the same translation/rotation/scale as any other node.
- A skin object lists the joint nodes by index and points at an accessor containing one inverse bind matrix per joint — a 4×4 of floats, 64 bytes per joint, so the explorer's 62 joints cost just under 4 KB of matrix data.
- The mesh primitive carries JOINTS_0 (4 joint indices per vertex) and WEIGHTS_0 (4 weights per vertex) alongside POSITION and NORMAL.
- Animations are separate objects whose channels target node transforms with keyframed samplers. Deleting all animations leaves a fully rigged, poseable character; deleting the skin leaves animations that move the skeleton while the mesh stands still.
The Simple Skin example in the Khronos tutorial series builds a complete two-joint rigged glTF in about 80 lines of JSON and is the fastest way to see every piece in one place.
Two format-level gotchas matter to anyone optimizing rigged files. First, joint indices refer to positions in the skin's joint list, so any tool that removes or reorders nodes must rewrite JOINTS_0 data or the mesh will follow the wrong joints — a corrupted-looking result with a subtle cause. Second, skinned vertices cost more memory and bandwidth than static ones: JOINTS_0 and WEIGHTS_0 together add 16 to 32 bytes per vertex depending on component types, which is worth knowing when a rigged asset's buffer size surprises you.
Rigging your first character: a practical workflow#
The fastest reliable route for a beginner in Blender, generalizable to any DCC:
- Finish the mesh first. Skinning stores per-vertex data; adding or deleting vertices after binding scrambles or discards it. Apply scale, remove doubles, and fix non-manifold geometry now.
- Model in a bind-friendly pose. An A-pose (arms lowered about 30 degrees) or T-pose keeps armpits and shoulders accessible. Slightly bent elbows and knees (5 to 10 degrees) tell automatic weighting and IK systems which way the joint folds.
- Build the joint chain from the root. Hips first, then spine, limbs, and head. Name joints consistently with left/right suffixes — Blender mirrors "UpperArm.L" onto "UpperArm.R" automatically, and engines match humanoid rigs by name patterns.
- Place joints at rotation centers. Enter the mesh's side and front views and drag each joint to the visual pivot of its region: knee at the knee crease, jaw at the ear line.
- Bind with automatic weights. Blender's armature parenting documentation covers the With Automatic Weights option, which computes an initial weight map from a heat-diffusion solve. On clean, watertight meshes the result is 80 to 90% usable.
- Test with extreme poses immediately. Rotate the shoulder to 90 degrees, the knee to 130 degrees, the head 60 degrees each way. Problems visible now are weight problems; problems only visible in animation are usually joint-placement problems.
Automatic weighting fails loudly on dirty geometry. Blender reports a bone-heat solving error when the mesh has overlapping shells, interior faces, or disconnected parts the solver cannot reach — the fix is mesh cleanup, not weight painting. Accessories like belts and pouches are often easier to bind 100% to a single nearby joint than to include in the automatic solve.
Weight painting: fixing what automatic weights miss#
Weight painting is direct manipulation of the binding table with a brush, visualized as a heat map — red for weight 1.0, blue for 0.0. The Blender weight painting introduction documents the mode; the craft is knowing where to look. Work through the joints in order of visible damage:
- Stray influence: vertices far from a joint carrying a small weight to it. Symptom: the chest ripples when an arm raises. Fix: select the joint, subtract or blur its weight in the offending region with Auto Normalize enabled.
- Hard boundaries: weight jumping from 1.0 to 0.0 across one edge loop. Symptom: a visible crease or shading seam at rest, a fold like bent sheet metal in motion. Fix: blur the boundary across 2 to 4 edge loops so the transition is gradual.
- Orphan vertices: vertices with no meaningful weight at all, left behind at their bind position while the body walks away. Many tools can select ungrouped or under-weighted vertices; assign them fully to the nearest joint.
- Symmetry breaks: the left knee deforms well and the right one badly. Paint one side with X-mirror enabled, or copy and mirror the finished side rather than painting both by hand.
Two habits raise quality quickly. Pose while you paint — keep the elbow bent at 90 degrees in one window while adjusting its weights in another, so every stroke shows its consequence. And accept that some artifacts are not weight problems: a thigh that collapses when the knee bends 130 degrees on a 3-loop knee needs more geometry, and no weight map can conjure surface where there are no vertices. That is a topology decision, which is why deformation-aware edge flow is planned during retopology rather than patched afterward.
Common failure patterns and what they actually mean#
A short diagnostic table for the artifacts every first rig produces:
- Candy-wrapper twist: rotate a forearm 180 degrees along its own axis and linear blend skinning averages opposing rotations straight through the limb's core, pinching the wrist to a point. This is a mathematical limit of LBS, not an authoring error. Production rigs spread the twist across 1 or 2 dedicated forearm twist joints; the alternative, dual quaternion skinning, blends rigid motions instead of matrices and eliminates the collapse, as introduced by Kavan et al.'s dual quaternion skinning paper. glTF and most real-time engines ship LBS, so twist joints remain the portable answer.
- Volume loss at bends: an elbow bent past 120 degrees flattens because averaged positions cut the corner of the arc. Helper joints or corrective shapes restore volume; modest bends hide it.
- Mesh explodes on import: vertices scatter wildly in the engine but look fine in the DCC. Almost always non-uniform or unapplied scale on the armature or mesh at export, or a joint-order mismatch introduced by an optimization tool rewriting nodes without rewriting JOINTS_0.
- Character deforms but slides through the world: the animation moves the hips locally while the root stays still, or vice versa — a root-motion convention mismatch between the clip and the engine's locomotion settings rather than a rig defect.
- Lighting seams at joints: abrupt weight boundaries splitting normal interpolation; blur the weights, and confirm the engine recomputes or correctly skins normals.
What a rig costs at runtime#
Skinning is cheap by modern standards, but it is not free, and its costs concentrate in specific places:
- Per-vertex math: every rendered frame, the vertex shader blends up to 4 joint matrices per vertex. At 60 Hz a game has a 16.7 ms frame budget; skinning the explorer's 8,450 vertices is a trivial slice of it on any GPU, which is why influence caps matter less for speed than for memory layout and consistency.
- Joint matrix upload: each skinned mesh uploads its joint palette every frame it animates — 62 joints × 64 bytes is about 4 KB per frame for the explorer. Fifty such characters is still under 200 KB per frame; joint counts hurt CPU animation sampling before they hurt bandwidth.
- Animation sampling: evaluating keyframes for every joint of every animated character each frame is CPU work that scales linearly with joint count. This is where 62 joints versus 190 joints becomes measurable on mobile, and why background characters deserve simpler skeletons and lower sampling rates.
- Attribute memory: the extra 16 to 32 bytes per vertex for joints and weights, which compression such as quantization can reduce like any other attribute.
The optimization instincts you already have for static meshes carry over: budget joints like triangles, spend them where the camera is, and simplify what the player never inspects.
A pre-export checklist for rigged GLBs#
- Mesh finalized: transforms applied, doubles removed, non-manifold geometry fixed before binding.
- Joints placed at anatomical pivots; limbs modeled with slight pre-bend in the fold direction.
- Every joint scale is exactly 1.0 and the armature itself carries no unapplied transform.
- Weights normalized to a sum of 1.0 with at most 4 influences per vertex.
- No vertex is unweighted; no vertex carries stray weight to a joint across the body.
- Extreme-pose test passed: shoulders at 90 degrees, knees at 130 degrees, head turned both ways.
- Twist-prone limbs either have twist joints or documented rotation limits.
- Exported GLB reopened in a viewer that plays animation — not just a static thumbnail — and checked against the DCC viewport.
- Any post-export optimization tool verified to preserve skinning attributes and joint order, with the animated result compared before and after.
Rigging rewards the same discipline as every other part of the asset pipeline: clean inputs, placement before painting, testing in the poses that will actually ship, and verifying the exported file rather than trusting the tool that wrote it.
Sources and further reading#
- Khronos glTF 2.0 specification: skins and skinning math
- Khronos glTF tutorial: Simple Skin worked example
- Khronos glTF tutorial: Skins in depth
- Blender Manual: Armatures introduction
- Blender Manual: Armature parenting and automatic weights
- Blender Manual: Weight painting introduction
- three.js documentation: SkinnedMesh
- Unity Manual: Quality Settings and per-vertex skin weights
- Kavan, Collins, Žára, O'Sullivan: Skinning with Dual Quaternions (I3D 2007)