Skip to content

3D Animation and Rigging — Skinning, Blending and Inverse Kinematics

DodaTech Updated 2026-06-21 7 min read

In this tutorial, you'll learn about 3d animation and rigging. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

3D animation and rigging bring characters to life through skeletons, skinning, keyframe interpolation, and inverse kinematics, enabling natural movement in games, films, and interactive applications.

What You'll Learn & Why It Matters

In this tutorial, you will learn how 3D character animation works — creating a skeleton hierarchy, binding skin vertices to bones via weights, blending between animations, and implementing inverse kinematics for procedural foot placement.

Real-world use: Every animated character in modern games uses these techniques. DodaTech's security visualization tools use animation blending for smooth transitions in real-time data visualizations.

Prerequisites

  • Computer Graphics Overview (previous)
  • Linear algebra (matrices, quaternions)
  • Basic 3D mesh concepts

Learning Path

flowchart LR
  A[Computer Graphics Overview] --> B[3D Animation and Rigging]
  B --> C[Real-Time GI]
  B --> D[Voxel Rendering]
  B --> E[GPU Architecture]
  B:::current

  classDef current fill:#f90,color:#fff,stroke:#333,stroke-width:2px

The Skeleton Hierarchy

A skeleton is a tree of bones, each with a local transform relative to its parent:

import numpy as np

class Bone:
    def __init__(self, name, parent=None):
        self.name = name
        self.parent = parent
        self.children = []
        self.local_transform = np.eye(4)  # Relative to parent
        self.world_transform = np.eye(4)  # Final transform
        self.inverse_bind_pose = np.eye(4)  # Bind pose inverse

    def get_world_transform(self):
        if self.parent is None:
            return self.local_transform
        return self.parent.get_world_transform() @ self.local_transform

# Build a simple skeleton
root = Bone("hips")
spine = Bone("spine", root)
neck = Bone("neck", spine)
head = Bone("head", neck)
left_arm = Bone("left_arm", spine)
left_forearm = Bone("left_forearm", left_arm)
left_hand = Bone("left_hand", left_forearm)

Skinning (Linear Blend Skinning)

Skinning deforms the mesh vertices based on bone transformations:

class SkinnedMesh:
    def __init__(self, vertices, bones):
        self.vertices = np.array(vertices)  # Rest pose vertices
        self.bones = bones
        # Each vertex has up to 4 bone influences
        self.bone_indices = np.zeros((len(vertices), 4), dtype=int)
        self.bone_weights = np.zeros((len(vertices), 4))

    def deform(self, bone_matrices):
        """Compute deformed vertex positions."""
        deformed = np.zeros_like(self.vertices)
        for i, vert in enumerate(self.vertices):
            pos = np.append(vert, 1.0)
            deformed_pos = np.zeros(4)
            for j in range(4):
                if self.bone_weights[i, j] > 0:
                    bone_idx = self.bone_indices[i, j]
                    weight = self.bone_weights[i, j]
                    # Transform from bind pose to current pose
                    bind = self.bones[bone_idx].inverse_bind_pose
                    current = bone_matrices[bone_idx]
                    deformed_pos += weight * (current @ bind @ pos)
            deformed[i] = deformed_pos[:3]
        return deformed
// Vertex shader for GPU skinning
#version 330 core

const int MAX_BONES = 128;
uniform mat4 boneMatrices[MAX_BONES];

layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoord;
layout (location = 3) in ivec4 aBoneIDs;
layout (location = 4) in vec4 aWeights;

void main()
{
    mat4 skinTransform = boneMatrices[aBoneIDs[0]] * aWeights[0]
                       + boneMatrices[aBoneIDs[1]] * aWeights[1]
                       + boneMatrices[aBoneIDs[2]] * aWeights[2]
                       + boneMatrices[aBoneIDs[3]] * aWeights[3];

    vec4 worldPos = model * skinTransform * vec4(aPos, 1.0);
    gl_Position = projection * view * worldPos;
}
flowchart TD
  A[Rest Pose Mesh] --> B[Bind Vertices to Bones]
  B --> C[Store Bone Indices + Weights]
  D[Bone Animation] --> E[Compute Bone Matrices]
  E --> F[GPU: Skinning Vertex Shader]
  C --> F
  F --> G[Deformed Mesh]

Keyframe Animation

Animations are stored as keyframes — time-stamped bone transforms:

class Keyframe:
    def __init__(self, time, bone_transforms):
        self.time = time          # Seconds
        self.bone_transforms = bone_transforms  # List of transforms per bone

class AnimationClip:
    def __init__(self, name, duration, keyframes):
        self.name = name
        self.duration = duration
        self.keyframes = keyframes

    def sample(self, time):
        """Interpolate bone transforms at a given time."""
        time = time % self.duration
        for i in range(len(self.keyframes) - 1):
            if self.keyframes[i + 1].time >= time:
                t0 = self.keyframes[i].time
                t1 = self.keyframes[i + 1].time
                blend = (time - t0) / (t1 - t0)
                # Interpolate each bone transform
                blended = []
                for j in range(len(self.keyframes[i].bone_transforms)):
                    mat = lerp_matrix(
                        self.keyframes[i].bone_transforms[j],
                        self.keyframes[i + 1].bone_transforms[j],
                        blend
                    )
                    blended.append(mat)
                return blended
        return self.keyframes[-1].bone_transforms

Animation Blending

Blend between two animations for smooth transitions:

def blend_animations(anim_a, anim_b, blend_factor):
    """Cross-fade between two animation poses."""
    blended = []
    for bone_a, bone_b in zip(anim_a, anim_b):
        mat = lerp_matrix(bone_a, bone_b, blend_factor)
        blended.append(mat)
    return blended

# Example: blend walk -> run
walk_pose = walk_clip.sample(1.5)
run_pose = run_clip.sample(1.5)
blended = blend_animations(walk_pose, run_pose, 0.3)

Inverse Kinematics (IK)

IK computes joint angles to reach a target position. We implement CCD (Cyclic Coordinate Descent):

def ccd_ik(chain, target, max_iterations=20, tolerance=0.01):
    """Cyclic Coordinate Descent IK solver."""
    for iteration in range(max_iterations):
        end_effector = chain[-1].get_world_transform()[:3, 3]
        error = np.linalg.norm(target - end_effector)
        if error < tolerance:
            return True

        # Iterate joints from end to root
        for i in range(len(chain) - 2, -1, -1):
            joint_pos = chain[i].get_world_transform()[:3, 3]
            joint_to_end = end_effector - joint_pos
            joint_to_target = target - joint_pos

            if np.linalg.norm(joint_to_end) < 0.001:
                continue

            # Compute rotation to align
            cross = np.cross(joint_to_end, joint_to_target)
            sin_theta = np.linalg.norm(cross)
            cos_theta = np.dot(joint_to_end, joint_to_target)
            if sin_theta < 0.001:
                continue

            axis = cross / sin_theta
            angle = np.arctan2(sin_theta, cos_theta)

            rotation = rotation_matrix(axis, angle * 0.5)
            chain[i].local_transform = chain[i].local_transform @ rotation

            end_effector = chain[-1].get_world_transform()[:3, 3]
    return False

Common Errors & Mistakes

1. Skinning Artifacts (Volume Collapse)

Mistake: Vertices collapse or pinch at joints because bone weights are not normalized.

Fix: Ensure bone weights sum to 1.0 for each vertex. Normalize the weight vector: weights = weights / sum(weights).

2. Incorrect Bone Matrix Order

Mistake: Multiplying bone matrices in the wrong order, causing bones to detach from the skeleton.

Fix: The world transform is parent.world @ local. Bind pose is converted to skinning space with boneMatrix = inverse(bindPose) * currentPose.

3. Animation Popping at Transitions

Mistake: Abruptly switching between animations without blending, causing visible popping.

Fix: Cross-fade between animations over 0.1-0.3 seconds using interpolation. Use blend_animations() with a smooth blend factor.

4. IK Not Converging

Mistake: IK solver failing to reach the target because of joint limits or reach constraints.

Fix: Add joint angle limits to the IK solver. Increase max iterations or use a more robust solver (FABRIK) for complex chains.

Practice Questions

Question 1

What is the difference between forward kinematics and inverse kinematics?

Show answer Forward kinematics computes end-effector position from joint angles (parent to child). Inverse kinematics computes joint angles to reach a desired end-effector position (child to parent).

Question 2

Why are vertex bone weights necessary?

Show answer Bone weights determine how much each bone influences a vertex. Without weights, vertices would belong to only one bone, creating hard creases at joints instead of smooth deformations.

Question 3

What is the bind pose and why is its inverse needed?

Show answer The bind pose is the neutral T-pose of the model. The inverse bind pose transforms vertices from their bound positions to the bone's local space, enabling correct deformation when bones move.

Question 4

What is animation blending used for?

Show answer Animation blending smoothly transitions between animations (e.g., walk to run) or combines multiple animations (e.g., walking upper body with aiming arms) for natural movement.

Challenge

Build a simple walking character controller with at least three animations (idle, walk, run) and smooth blending between them. Add inverse kinematics for foot placement on uneven terrain.

FAQ

What is the difference between quaternions and Euler angles for animation?

Quaternions avoid gimbal lock and provide smooth spherical interpolation (SLERP). Euler angles are easier to author but can cause rotation artifacts. Most animation systems use quaternions internally.

What is animation retargeting?

Retargeting transfers animation from one skeleton to another with different proportions. The system maps bone hierarchies and scales motion to fit the target skeleton. This lets you use human animations on differently-sized characters.

How many bones can a real-time character have?

PC games typically use 50-150 bones per character. Mobile games use 20-50 bones. Each bone adds GPU skinning cost and memory for animation data. The total is limited by uniform buffer sizes (typically 128-256 matrices).

What is blend shapes (morph targets)?

Blend shapes deform the mesh directly by interpolating between vertex positions, rather than using bones. They are used for facial animation, where bones cannot capture subtle lip and brow movements.


Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Author: DodaTech | Last updated: June 21, 2026

DodaTech tutorials are built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro — security tools used by millions worldwide.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro