A character mesh (the 3D model — the skin, the polygons you see) has no idea how to move on its own. It is just a big list of vertices (points in space) connected into triangles. If you want a character to walk, wave, or punch, something has to push those vertices around, frame by frame, in a way that still looks like a body.
The tool for that is a rig (short for "rigging"). A rig is a skeleton made of bones (also called joints) arranged in a hierarchy — a parent/child tree, the same kind of tree you already used for scene graphs and transform parenting. Each bone does not carry any geometry of its own. A bone is just a transform (a position + rotation, and sometimes scale) that lives inside the character. The mesh is then bound to this skeleton so that when a bone moves, the nearby skin moves with it.
This two-part setup — invisible skeleton drives visible skin — is exactly how your own arm works: your bones move, your skin and muscle follow. That is also literally why the technique is called skinning.
Rigging is the job of building that skeleton and attaching the mesh to it correctly. Skinning (this chapter's other half) is the math that decides, for every vertex, how much each nearby bone should push it around. Rigging and skinning together are what let a single mesh bend, without an artist hand-sculpting a new mesh for every frame of every animation.
Nothing new is invented here. A bone is the same idea as any parent/child transform you already worked with: it has a local transform (its position and rotation relative to its parent) and it inherits everything its parent does. Move the shoulder, and the upper arm, forearm, and hand all move with it, even though only the shoulder's transform changed.
To actually draw or deform something, you eventually need each bone's world transform (its position and rotation in the whole scene, not relative to anything). You get that by walking up the parent chain and combining every local transform along the way — parent's world transform combined with your own local transform.
In a real engine this "combining" is a 4x4 matrix multiply (it folds in rotation and scale too, not just position), but the idea is exactly the same as the addition above: a bone's world transform is its parent's world transform combined with its own local transform. Here is a simplified version using plain positions, so you can see the chain-walking without needing matrix math yet:
using UnityEngine;
public class Bone {
public string name;
public Bone parent;
public Vector3 localPosition; // offset from parent, set once in the bind pose
public Bone(string name, Bone parent, Vector3 localPosition) {
this.name = name;
this.parent = parent;
this.localPosition = localPosition;
}
// Walk up the parent chain and add up every local offset
public Vector3 GetWorldPosition() {
Vector3 worldPos = localPosition;
Bone current = parent;
while (current != null) {
worldPos += current.localPosition;
current = current.parent;
}
return worldPos;
}
}
public class RigDemo : MonoBehaviour {
void Start() {
Bone hip = new Bone("Hip", null, new Vector3(0, 0, 0));
Bone spine = new Bone("Spine", hip, new Vector3(0, 1, 0));
Bone chest = new Bone("Chest", spine, new Vector3(0, 1, 0));
Debug.Log(chest.name + " world position: " + chest.GetWorldPosition());
}
}
Hip sits at the origin. Spine is 1 unit above it. Chest is 1 more unit above Spine. Walking up the chain and adding the offsets gives (0, 2, 0). A real skeleton uses full matrices so rotation is included too — if Spine were rotated, Chest would swing around with it, not just slide up — but the walk-up-the-chain idea does not change.
Before a character can be animated, the skeleton is placed inside the mesh in one specific, neutral pose — arms usually out to the sides or slightly down, legs straight, called a T-pose or A-pose. This one moment is called the bind pose (also called the rest pose): the pose the skeleton is in when the skin gets bound to it.
The bind pose matters because every bone's world transform at that exact moment gets recorded and saved. These recorded transforms are the bind matrices — one 4x4 matrix per bone, capturing "where was this bone, in world space, when the mesh was bound to it." They never change again after that; they are baked into the model file.
Why record this at all? Because skinning is not "move the vertex to wherever the bone currently is." It is "move the vertex by however much the bone has moved since the bind pose." Without the bind matrix as a reference point, the engine would have no idea what "no movement yet" even looks like for that vertex.
Skinning is the process of moving mesh vertices based on bone movement. The simplest version: every vertex is assigned to exactly one bone, and moves exactly like that bone does. This is called rigid skinning, and it looks fine for stiff robot parts, but terrible at joints — the mesh would tear into disconnected chunks at every elbow and knee, because vertices near the elbow that belong to the "upper arm" bone would stay behind while vertices belonging to "forearm" fly off with it.
The fix: let a vertex belong to more than one bone at once, each with a weight (a number from 0 to 1 saying how much influence that bone has), and blend the results. This is skinning in the usual sense — more precisely, linear blend skinning (LBS), the most common technique, and the one almost every game engine uses by default.
Each bone that influences a vertex computes its own opinion of where that vertex should end up (based on how much that bone has moved since the bind pose). Then all those opinions get blended together using the weights, and the weighted average is the vertex's final position. That is the entire idea. The next few sections work out exactly how "where a bone thinks the vertex should be" is computed, and then blend it for real with numbers.
A bone's current world matrix tells you where the bone is right now. But a vertex is not stored relative to the bone — it is stored in the mesh's own space, at its bind-pose position. Before you can ask "how has this bone moved," you first need to know the vertex's position relative to the bone, at bind time. That is what the inverse bind matrix is for.
The inverse bind matrix is exactly what it sounds like: the mathematical inverse of the bone's bind matrix (the world matrix recorded in section 3). Multiplying a bind-pose vertex by a bone's inverse bind matrix converts that vertex out of world space and into "this bone's local space, as it was at bind time." Once the vertex is expressed relative to the bone, multiplying by the bone's current world matrix carries it along wherever the bone has moved to since then.
Put together, the two matrices for one bone form what engines call a skin matrix (sometimes "skinning matrix", or one slot in a "matrix palette" — an array with one skin matrix per bone):
// One skin matrix per bone, computed once per frame on the CPU
Matrix4x4 skinMatrix = currentWorldMatrix * inverseBindMatrix;
// inverseBindMatrix itself never changes after the model is loaded --
// it only depends on the bind pose, which is fixed
Matrix4x4 inverseBindMatrix = Matrix4x4.Inverse(boneBindWorldMatrix);
The inverse bind matrix is computed once, when the model is loaded (it depends only on the bind pose, which never changes). The skin matrix is recomputed every frame, once per bone, because the current pose changes every frame during animation. That is far cheaper than recomputing anything per-vertex on the CPU — which is exactly why this two-step trick exists.
Now put sections 4 and 5 together into the real formula. For a vertex influenced by bones 1..n, each with weight w_i and skin matrix M_i (current world matrix times inverse bind matrix, from section 5):
Each bone computes its own candidate position for the vertex (M_i * v_bind), and the final position is a weighted average of all those candidates. This is why it is called linear blend skinning: it is a plain weighted sum, nothing fancier.
Take the elbow vertex V from section 4's diagram, weighted 0.5 to UpperArm and 0.5 to Forearm. In the bind pose: UpperArm sits at world position (0, 0, 0), Forearm (its child) sits at (0, 2, 0), and V sits right at the elbow, at bind position (0, 2, 0). To keep the arithmetic simple, this example uses plain positions with no rotation — real engines use full 4x4 matrices, but the blending step works identically.
Now animate a bend: keep UpperArm exactly where it was (current position (0, 0, 0), unchanged from bind), but swing Forearm sideways so its current position becomes (1, 2, 0).
// What UpperArm "thinks" V's position should be:
// step 1: undo bind pose -> V relative to UpperArm = (0,2,0) - (0,0,0) = (0,2,0)
// step 2: re-apply current pose -> (0,0,0) + (0,2,0) = (0,2,0)
Vector3 upperArmOpinion = new Vector3(0, 2, 0);
// What Forearm "thinks" V's position should be:
// step 1: undo bind pose -> V relative to Forearm = (0,2,0) - (0,2,0) = (0,0,0)
// step 2: re-apply current pose -> (1,2,0) + (0,0,0) = (1,2,0)
Vector3 forearmOpinion = new Vector3(1, 2, 0);
float wUpper = 0.5f;
float wForearm = 0.5f;
Vector3 skinnedPosition = wUpper * upperArmOpinion + wForearm * forearmOpinion;
Debug.Log("Skinned vertex position: " + skinnedPosition);
UpperArm did not move, so it insists V should stay at (0, 2, 0). Forearm swung out to (1, 2, 0), so it insists V should follow it there. The blend splits the difference: V ends up at (0.5, 2, 0) — halfway between the two opinions, instead of fully following the forearm the way real skin would. Hold onto this exact example; section 8 explains why that "halfway" result is precisely what causes visible joint artifacts.
Every vertex in a skinned mesh stores a small list of (bone, weight) pairs — usually up to 4 bones per vertex, since 4 is enough for almost every joint and keeps the per-vertex data small. Those weights are what section 6's formula calls w_i, and they are supposed to add up to 1.0 per vertex (this is called being normalized).
// One vertex's skin data, roughly how it is stored in a real mesh
struct VertexSkinData {
int boneIndex0, boneIndex1, boneIndex2, boneIndex3;
float weight0, weight1, weight2, weight3;
// weight0 + weight1 + weight2 + weight3 should equal 1.0
};
Where do these weights come from? Two ways:
Good weight painting is one of the most valuable, unglamorous skills in character art. A beautifully modeled character with sloppy weights still looks broken the moment it moves.
Go back to section 6's worked example. The elbow vertex, weighted 50/50 between UpperArm and Forearm, ended up at the average of the two bones' opinions instead of fully committing to either one. Do that for every vertex ringing the elbow, and the whole cross-section of the arm shrinks toward the joint's rotation axis as the elbow bends — the surface pinches inward like a twisted piece of candy wrapper. This is nicknamed exactly that: the candy-wrapper problem (also called joint collapse, or the "beach-ball elbow" when it is bad enough to look spherical instead of pinched).
The root cause is always the same: some vertex is weighted too evenly between two bones whose current poses have drifted far apart (a sharply bent joint counts as "far apart," even though the bones are still touching in the mesh). Fixes riggers actually use:
Recomputing every vertex's position on the CPU, every frame, for a mesh with tens of thousands of vertices, would be slow — and it is exactly the kind of "do the same small amount of math many times, independently" job a GPU is built for. So real games do skinning on the GPU, inside the vertex shader (the small program that runs once per vertex, every frame, on the graphics card).
The CPU still has to compute the small stuff: walk the bone hierarchy, get each bone's current world matrix, multiply by its inverse bind matrix, and pack the results into an array of skin matrices (often called a matrix palette) — one entry per bone, maybe 50-100 small matrices for a typical character. That array gets uploaded to the GPU.
// Simplified HLSL-style vertex shader skinning
struct VertexInput {
float3 position : POSITION;
float4 weights : BLENDWEIGHT; // up to 4 bone weights for this vertex
uint4 boneIDs : BLENDINDICES; // which 4 bones from the palette to use
};
float4x4 boneMatrices[MAX_BONES]; // the matrix palette, uploaded once per frame
float3 SkinVertex(VertexInput IN) {
float4x4 skinMatrix =
IN.weights.x * boneMatrices[IN.boneIDs.x]
+ IN.weights.y * boneMatrices[IN.boneIDs.y]
+ IN.weights.z * boneMatrices[IN.boneIDs.z]
+ IN.weights.w * boneMatrices[IN.boneIDs.w];
return mul(skinMatrix, float4(IN.position, 1.0)).xyz;
}
This is exactly section 6's formula, just written the way a shader actually writes it: build one combined skin matrix per vertex out of up to 4 weighted bone matrices, then transform the bind-pose vertex position by it. Every vertex on the mesh runs this same tiny function independently and at the same time, which is why thousands of vertices skin in well under a millisecond on modern hardware.
Linear blend skinning has the candy-wrapper problem because it blends bone positions in a straight line — averaging two rotated poses in plain space always cuts a corner, which is where the volume loss comes from. Dual quaternion skinning (DQS) fixes this by blending each bone's rotation using dual quaternions (a compact math object that represents rotation-plus-translation together, built on top of the quaternions you may have already met for representing rotation), instead of blending raw positions.
You do not need the quaternion algebra to understand the trade-off: DQS blends "how much has this bone rotated and where has it moved to" in a way that sweeps along a curve instead of cutting a straight line between two poses, so it keeps volume around joints far better — often fixing the candy-wrapper problem with zero extra bones or corrective shapes. The cost is that it is somewhat more expensive per vertex to compute than LBS's plain weighted sum, and it can introduce its own, different artifact ("bulging" at some joints instead of pinching) if used carelessly.
Most engines let you pick per-character or per-mesh whether to use LBS or DQS (sometimes it is a shader variant you choose, sometimes an import setting), because DQS is not strictly better for every case — it is a different trade-off. As a beginner, the important thing to remember is: if elbows or shoulders still collapse after you have fixed the weight painting, DQS is the next tool to reach for, not a mystery you have to solve with more corrective bones.
Everything so far has been about deform bones — the bones that actually drive the mesh via skinning. But animators rarely grab deform bones directly. Real rigs add a second layer on top, built purely to make an animator's job easier.
A control is a simple shape (a circle, an arrow, a box — never rendered in the final game) placed in the scene for an animator to click and drag. Moving a control does not deform the mesh directly; instead it drives one or more deform bones underneath, often through extra logic (constraints) that a rigger set up. Controls exist purely so an animator's job is "grab the hand circle and move it," not "hunt through a list of 80 bone names."
There are two ways to pose a chain of bones like an arm:
This chapter does not go into how an IK solver actually computes those angles (that is its own topic) — the goal here is only to recognize the concept: IK handles and controls are an animator-facing convenience layer sitting on top of the same deform-bone skeleton this whole chapter has been about. Under the hood, once the solver decides the final bone rotations, everything from section 2 onward — bind matrices, inverse bind matrices, skin matrices, linear blend skinning — runs exactly the same way, whether those rotations came from FK, IK, or a physics simulation.
Here is everything from this chapter laid out in the order it actually happens, split into "set up once" and "every single frame":
Ten steps, but only two kinds of work: a one-time setup that an artist does by hand, and a per-frame loop that the engine repeats roughly 60 times a second, entirely automatically, for every character on screen.
Root parent: none localPosition: (0, 0, 0)
Thigh parent: Root localPosition: (0, -1, 0)
Shin parent: Thigh localPosition: (0, -1, 0)
Foot parent: Shin localPosition: (0, -0.5, 0)
Using the same "add up the chain" approach as section 2, compute Foot's world position by hand. Then write a short C# snippet (you can reuse the Bone class from section 2) that prints it, and check your answer matches.
Add every local offset from Foot up to Root: (0, -0.5, 0) + (0, -1, 0) + (0, -1, 0) + (0, 0, 0) = (0, -2.5, 0).
Bone root = new Bone("Root", null, new Vector3(0, 0, 0));
Bone thigh = new Bone("Thigh", root, new Vector3(0, -1, 0));
Bone shin = new Bone("Shin", thigh, new Vector3(0, -1, 0));
Bone foot = new Bone("Foot", shin, new Vector3(0, -0.5f, 0));
Debug.Log(foot.name + " world position: " + foot.GetWorldPosition());
// Output: Foot world position: (0.0, -2.5, 0.0)
Foot ends up 2.5 units below Root, which makes sense: it is the sum of the whole leg's length, straight down, since none of the bones are rotated in this simplified example.
Vector3 thighOpinion = new Vector3(0, -1.0f, 0.1f);
Vector3 shinOpinion = new Vector3(0, -1.0f, 0.6f);
float wThigh = 0.8f;
float wShin = 0.2f;
Vector3 skinnedPosition = wThigh * thighOpinion + wShin * shinOpinion;
Debug.Log("Skinned vertex position: " + skinnedPosition);
// Output: Skinned vertex position: (0.0, -1.0, 0.2)
Low risk. The weight split (0.8 / 0.2) is far from 50/50, so the vertex mostly commits to Thigh's opinion (0.2 landed close to Thigh's 0.1, not halfway to Shin's 0.6). A vertex weighted close to 50/50, like section 6's elbow example, is the one that gets torn evenly between two very different poses and visibly pinches.
struct VertexSkinData {
int boneIndex0 = 3, boneIndex1 = 7;
float weight0 = 0.7f, weight1 = 0.5f;
};
Find the bug, explain why it would cause visible mesh distortion, and write the corrected version.
The weights do not sum to 1.0 — 0.7 + 0.5 = 1.2. Section 6's formula assumes the weights are a proper weighted average; if they add up to more than 1, the vertex gets pushed too far along every bone's contribution (each skin matrix's effect is over-counted), stretching or "melting" the mesh outward around that vertex. This is a classic un-normalized weights bug, often introduced by hand-editing weight data or a broken export step.
struct VertexSkinData {
int boneIndex0 = 3, boneIndex1 = 7;
float weight0 = 0.58f, weight1 = 0.42f; // normalized: 0.7 / 1.2 and 0.5 / 1.2
};
Dividing each weight by the original sum (1.2) rescales them so they add up to exactly 1.0 again, while keeping the same relative balance between the two bones (bone 3 still gets more influence than bone 7, just correctly proportioned).