8.3 Rigging & Skinning Basics

Phase 8 · Technical Art · Study time: 20–40 h

How meshes are bound to skeletons so characters deform correctly — enough to build, debug and optimize rigs and skinning.

1. What Is a Rig?

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.

Character mesh (skin) Skeleton (bones / joints) .-''''-. o Head / o o \ | | ^ | o Neck \ ___ / | '------' o Chest | | +-+ | | / \ Shoulders | | o o UpperArms / \ | | o o o o Forearms The mesh has no idea how to move by itself. The skeleton drives it.

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.

2. Bones Are Transforms in a Hierarchy

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.

Hip (root bone) | +-- Spine local offset: (0, 1, 0) from Hip | +-- Chest local offset: (0, 1, 0) from Spine | +-- Shoulder | +-- UpperArm | +-- Forearm | +-- Hand World position of Chest = Hip's world position + Spine's local offset + Chest's local offset

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());
    }
}
Output: Chest world position: (0.0, 2.0, 0.0)

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.

Tip This is the exact same parent/child idea from earlier transform hierarchies. A rig is not a new system — it is a transform tree wearing a costume, with one extra job: deforming a mesh.

3. The Bind Pose

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.

Bind pose (T-pose) Animated pose (later, at runtime) o o /|\ /| / | \ / | / | \ / |__ o o o o o arms straight out arm bent at elbow Bind matrices are recorded Bones now differ from the HERE. This is the reference bind pose. Skinning measures everything else is measured "how far have we moved against. from bind?"

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.

4. Skinning: A Vertex Follows One or More Bones

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.

UpperArm bone Forearm bone o--------------------------o--------------------------o Shoulder Elbow Wrist * <- vertex V, on the skin | surface right at the elbow | V's bone weights: UpperArm : 0.5 Forearm : 0.5 V's final position = 0.5 * (where UpperArm thinks V should be) + 0.5 * (where Forearm thinks V should be)

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.

5. The Inverse Bind Matrix

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.

bind-pose vertex inverse bind matrix current bone matrix (world space, --> (undo the bind pose, --> (re-apply wherever wherever it was express relative to the bone has moved sculpted) the bone instead) to now) v_bind * InverseBind_bone * CurrentWorld_bone = v_skinned

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.

Common mistake Forgetting the inverse bind matrix and skinning with the raw current bone matrix is a very common beginner bug. The mesh explodes into a tangled mess the moment you press play, because every vertex jumps to "wherever the bone is," ignoring where that vertex was positioned relative to the bone in the first place.

6. Linear Blend Skinning: The Math, Worked by Hand

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):

v_skinned = w_1 * (M_1 * v_bind) + w_2 * (M_2 * v_bind) + ... + w_n * (M_n * v_bind) where: v_bind = the vertex's fixed position in the bind pose M_i = skin matrix for bone i = CurrentWorld_i * InverseBind_i w_i = weight of bone i for this vertex (weights should add up to 1.0)

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.

Worked example

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);
Output: Skinned vertex position: (0.5, 2.0, 0.0)

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.

7. Skin Weights and Weight Painting

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:

Weight paint view (heat map, red = high weight, blue = low weight) UpperArm bone selected: Forearm bone selected: ((((RRRR)))) ((((BBBB)))) (((RRRRRR))) (((BBBBBB))) ((RRRROOOO)) vs. ((BBBBOOOO)) (OOOOoooo) (OOOOrrrr) (oooo..) (oooo..) red/orange near shoulder, blue near shoulder, fading out toward elbow growing toward elbow

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.

8. When Weights Go Wrong: The Candy-Wrapper Problem

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).

BAD: 50/50 weights, elbow bent 90 degrees GOOD: better weights + more bones UpperArm Forearm UpperArm Forearm ======] ======] | | ) <- pinched, twisted (__) <- keeps roughly ( "candy wrapper" ( ) round volume | cross-section | [====== [====== every vertex near the joint extra weight spread across more is torn 50/50 between two bones/vertices near the joint bones with very different poses avoids any vertex being torn evenly

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:

Tip If you ever see a character's elbow or knee look "twisted" or "collapsed" only at extreme bend angles, but fine everywhere else, suspect weight painting first. It is the single most common cause.

9. Where GPU Skinning Actually Happens

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).

CPU side, once per frame

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.

GPU side, once per vertex, in parallel

// 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.

Common mistake A vertex shader has no idea about any other vertex. If two neighboring vertices end up with very different weight splits (one is 90% UpperArm, its neighbor a fraction of a centimeter away is 90% Forearm), the shader will happily skin them completely differently, and the seam between them tears or pinches — another version of the candy-wrapper problem, caused this time by a sudden weight change instead of an even split.

10. Dual Quaternion Skinning: A Better, Pricier Blend

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.

Linear Blend Skinning (LBS) Dual Quaternion Skinning (DQS) blends POSITIONS in a straight line blends ROTATIONS along a curve A o A o | | | <- straight cut, ) <- follows the | loses volume ( actual bend B o B o

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.

11. Common Rig Parts: Controls and IK Handles

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.

Controls

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."

Forward Kinematics (FK) vs Inverse Kinematics (IK)

There are two ways to pose a chain of bones like an arm:

Forward Kinematics (FK) Inverse Kinematics (IK) you rotate each joint yourself you place one target, rig solves the rest Shoulder -- rotate 30 deg Hand target o <- you place this | | Elbow -- rotate 45 deg (solver works backward from here) | | Hand ends up wherever Elbow angle -- solved automatically this chain puts it | Shoulder angle -- solved automatically

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.

12. The Full Pipeline, Start to Finish

Here is everything from this chapter laid out in the order it actually happens, split into "set up once" and "every single frame":

SET UP ONCE (when the character is made / imported) 1. Artist builds the skeleton (bone hierarchy) inside the mesh, in the bind pose 2. Each bone's bind-pose world matrix is recorded 3. Each bone's inverse bind matrix is computed (inverse of step 2) 4. Artist assigns skin weights per vertex (auto weights + hand weight painting) EVERY FRAME (at runtime, while the game is playing) 5. Animation system sets each bone's CURRENT local transform (from a keyframed clip, IK solve, ragdoll physics, etc.) 6. Engine walks the hierarchy: local transforms combine into current WORLD matrices, parent to child 7. CPU computes one skin matrix per bone: skinMatrix = currentWorldMatrix * inverseBindMatrix 8. Skin matrices upload to the GPU as a matrix palette 9. Vertex shader runs for every vertex, in parallel: blend up to 4 weighted skin matrices, transform the bind-pose vertex, output the final skinned position 10. GPU rasterizes the now-deformed mesh to the screen

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.

Glossary

Exercise 1 A skeleton has four bones in a chain, each storing only a local position offset from its parent (no rotation, to keep this simple):
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.
Show answer

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.

Exercise 2 A knee vertex V is weighted 0.8 to the Thigh bone and 0.2 to the Shin bone. After bending the knee: Thigh's opinion of where V should be is (0, -1.0, 0.1), and Shin's opinion of where V should be is (0, -1.0, 0.6). Using section 6's linear blend skinning formula, compute V's final skinned position. Then explain in one sentence whether this vertex is at high or low risk of a visible candy-wrapper artifact, and why.
Show answer
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.

Exercise 3 A rigger sends you this vertex skin data and says the shoulder looks "melted" in game, but fine in the modeling tool:
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.
Show answer

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).

← Back to all chapters