Skip to content

Rigging

Skeletal animation, deformation, IK, procedural locomotion, and secondary motion.

Prior Art

Blender Armatures

  • Bones: head, tail, roll; parent-child hierarchy
  • Pose mode: transform bones, store as keyframes
  • Constraints: IK, copy rotation, track-to, etc.
  • Drivers: expressions that control properties
  • Skinning: vertex groups with weights

Maya / 3ds Max

  • Joint hierarchies: similar to Blender bones
  • IK solvers: various algorithms (SC, RP, Spline)
  • Blend shapes: morph targets for facial animation
  • Deformers: lattice, wrap, cluster, nonlinear (bend, twist, etc.)

Game Animation Systems

  • Motion matching: database of poses, find best match
  • Procedural animation: IK-driven walk cycles, secondary motion
  • Animation layers: blend multiple animations

Module Structure

ModulePurpose
skeletonBones, skeleton hierarchy, poses
skinMesh skinning with bone weights
animationAnimation clips, keyframes, playback
blendAnimation blending, layers, crossfades
ikInverse kinematics (CCD, FABRIK)
constraintBone constraints, path following
locomotionProcedural walk cycles
motion_matchingMotion matching system
secondaryJiggle physics, follow-through

Skeleton and Bones

Creating a Skeleton

rust
use rhizome_resin_rig::{Skeleton, Bone, BoneId, Transform};
use glam::{Vec3, Quat};

let mut skeleton = Skeleton::new();

// Add root bone
let root = skeleton.add_bone(Bone {
    name: "root".into(),
    parent: None,
    local_transform: Transform::IDENTITY,
    length: 0.0,
});

// Add spine
let spine = skeleton.add_bone(Bone {
    name: "spine".into(),
    parent: Some(root),
    local_transform: Transform::from_translation(Vec3::new(0.0, 1.0, 0.0)),
    length: 0.5,
});

// Add arm
let upper_arm = skeleton.add_bone(Bone {
    name: "upper_arm".into(),
    parent: Some(spine),
    local_transform: Transform::from_translation(Vec3::new(0.3, 0.0, 0.0)),
    length: 0.3,
});

let lower_arm = skeleton.add_bone(Bone {
    name: "lower_arm".into(),
    parent: Some(upper_arm),
    local_transform: Transform::from_translation(Vec3::new(0.3, 0.0, 0.0)),
    length: 0.3,
});

Poses

rust
use rhizome_resin_rig::Pose;

// Create pose from skeleton (rest pose)
let mut pose = Pose::from_skeleton(&skeleton);

// Modify bone transforms
pose.set_local_rotation(upper_arm, Quat::from_rotation_z(0.5));
pose.set_local_translation(root, Vec3::new(0.0, 0.1, 0.0));

// Get world-space transforms
let world_transform = pose.world_transform(&skeleton, upper_arm);
let world_position = pose.world_position(&skeleton, upper_arm);

Mesh Skinning

rust
use rhizome_resin_rig::{Skin, VertexInfluences, MAX_INFLUENCES};

// Create skin for mesh
let mut skin = Skin::new(vertex_count);

// Set influences for each vertex
skin.set_influences(vertex_index, VertexInfluences {
    bones: [bone_a, bone_b, BoneId::INVALID, BoneId::INVALID],
    weights: [0.7, 0.3, 0.0, 0.0],
});

// Apply skinning to mesh
let deformed_positions = skin.deform_positions(
    &original_positions,
    &skeleton,
    &pose,
);

Animation

Animation Clips

rust
use rhizome_resin_rig::{AnimationClip, Track, Keyframe, Interpolation, AnimationTarget};

let mut clip = AnimationClip::new("walk", duration: 1.0);

// Add rotation track for a bone
clip.add_track(Track::new(
    AnimationTarget::BoneRotation(upper_arm),
    vec![
        Keyframe::new(0.0, Quat::IDENTITY, Interpolation::Linear),
        Keyframe::new(0.5, Quat::from_rotation_z(0.5), Interpolation::Linear),
        Keyframe::new(1.0, Quat::IDENTITY, Interpolation::Linear),
    ],
));

// Add position track
clip.add_track(Track::new(
    AnimationTarget::BonePosition(root),
    vec![
        Keyframe::new(0.0, Vec3::ZERO, Interpolation::Linear),
        Keyframe::new(1.0, Vec3::new(0.0, 0.0, 1.0), Interpolation::Linear),
    ],
));

Animation Playback

rust
use rhizome_resin_rig::AnimationPlayer;

let mut player = AnimationPlayer::new(&clip);

// Update
player.update(delta_time);

// Get current time
let time = player.time();

// Sample pose at current time
clip.sample(player.time(), &mut pose);

// Control playback
player.play();
player.pause();
player.set_time(0.5);
player.set_speed(2.0);
player.set_looping(true);

Animation Blending

Crossfade

rust
use rhizome_resin_rig::Crossfade;

let mut crossfade = Crossfade::new(clip_a, clip_b, duration: 0.3);

crossfade.update(delta_time);
crossfade.sample(&mut pose);

Animation Layers

rust
use rhizome_resin_rig::{AnimationStack, AnimationLayer, BlendMode};

let mut stack = AnimationStack::new();

// Base layer (full body)
stack.add_layer(AnimationLayer {
    clip: walk_clip,
    weight: 1.0,
    blend_mode: BlendMode::Override,
    mask: None,  // All bones
});

// Additive layer (head look)
stack.add_layer(AnimationLayer {
    clip: look_clip,
    weight: 0.5,
    blend_mode: BlendMode::Additive,
    mask: Some(head_bone_mask),  // Only head bones
});

// Sample combined result
stack.sample(time, &mut pose);

Blend Trees

rust
use rhizome_resin_rig::BlendNode;

// 1D blend (walk speed)
let blend = BlendNode::Blend1D {
    clips: vec![idle_clip, walk_clip, run_clip],
    thresholds: vec![0.0, 0.5, 1.0],
    parameter: speed,
};

// 2D blend (movement direction)
let blend = BlendNode::Blend2D {
    clips: vec![
        (idle, Vec2::ZERO),
        (walk_forward, Vec2::new(0.0, 1.0)),
        (walk_back, Vec2::new(0.0, -1.0)),
        (walk_left, Vec2::new(-1.0, 0.0)),
        (walk_right, Vec2::new(1.0, 0.0)),
    ],
    parameter: movement_direction,
};

blend.sample(time, &mut pose);

Inverse Kinematics

CCD (Cyclic Coordinate Descent)

rust
use rhizome_resin_rig::{IkChain, IkConfig, solve_ccd};

let chain = IkChain {
    bones: vec![upper_arm, lower_arm, hand],
    target: Vec3::new(1.0, 0.5, 0.0),
    pole: Some(Vec3::new(0.0, 0.0, -1.0)),  // Elbow direction hint
};

let config = IkConfig {
    iterations: 10,
    tolerance: 0.001,
};

let result = solve_ccd(&skeleton, &mut pose, &chain, &config);

FABRIK

rust
use rhizome_resin_rig::solve_fabrik;

let result = solve_fabrik(&skeleton, &mut pose, &chain, &config);

// Check result
if result.reached {
    println!("Target reached!");
} else {
    println!("Distance to target: {}", result.distance);
}

Constraints

Path Constraint

rust
use rhizome_resin_rig::{PathConstraint, Path3D, ConstraintStack};

// Create path
let path = Path3D::from_points(&[
    Vec3::new(0.0, 0.0, 0.0),
    Vec3::new(1.0, 0.5, 0.0),
    Vec3::new(2.0, 0.0, 0.0),
]);

// Bone follows path
let constraint = PathConstraint {
    bone: spine,
    path: path,
    position: 0.5,  // 0.0-1.0 along path
    follow_rotation: true,
};

// Apply constraints
let mut stack = ConstraintStack::new();
stack.add(constraint);
stack.evaluate(&skeleton, &mut pose);

Procedural Locomotion

Walk Cycle Generator

rust
use rhizome_resin_rig::{ProceduralWalk, GaitConfig, GaitPattern, WalkAnimator};

let config = GaitConfig {
    stride_length: 0.8,
    stride_height: 0.15,
    speed: 1.0,
    body_bob: 0.02,
    body_sway: 0.01,
};

let gait = GaitPattern::Walk;  // or Run, Trot, etc.

let mut walk = ProceduralWalk::new(&skeleton, config, gait);

// Configure legs
walk.add_leg(hip_l, knee_l, foot_l);
walk.add_leg(hip_r, knee_r, foot_r);

// Update
walk.update(delta_time, velocity);
walk.apply_to_pose(&mut pose);

Foot Placement

rust
use rhizome_resin_rig::FootPlacement;

let mut placement = FootPlacement::new(&skeleton);
placement.add_foot(foot_l, leg_chain_l);
placement.add_foot(foot_r, leg_chain_r);

// Adjust feet to terrain
placement.update(
    &mut pose,
    |position| terrain.height_at(position),  // Height query
    |position| terrain.normal_at(position),  // Normal query
);

Motion Matching

Database Setup

rust
use rhizome_resin_rig::{
    MotionDatabase, MotionClip, MotionFrame, MotionFrameBuilder,
    MotionMatcher, MotionMatchingConfig, MotionQuery,
};

// Build motion database
let mut database = MotionDatabase::new();

// Add clips
let walk_clip = MotionClip::from_animation(&walk_animation, &skeleton);
database.add_clip("walk", walk_clip);

let run_clip = MotionClip::from_animation(&run_animation, &skeleton);
database.add_clip("run", run_clip);

Matching and Playback

rust
let config = MotionMatchingConfig {
    trajectory_weight: 1.0,
    pose_weight: 1.0,
    velocity_weight: 0.5,
    lookahead_time: 0.5,
};

let mut matcher = MotionMatcher::new(&database, config);

// Query for best matching pose
let query = MotionQuery {
    current_pose: &pose,
    desired_velocity: Vec3::new(0.0, 0.0, 1.0),
    desired_facing: Vec3::Z,
};

let result = matcher.find_best_match(&query);

// Apply matched frame
apply_frame_to_pose(&result.frame, &mut pose);

// Optionally blend between frames
let blended = blend_frames(&current_frame, &result.frame, blend_factor);

Secondary Motion

Jiggle Bones

rust
use rhizome_resin_rig::{JiggleBone, SecondaryConfig};

let config = SecondaryConfig {
    stiffness: 0.5,
    damping: 0.8,
    gravity: Vec3::new(0.0, -9.8, 0.0),
};

let mut jiggle = JiggleBone::new(hair_bone, config);

// Update each frame
jiggle.update(&skeleton, &pose, delta_time);
jiggle.apply(&mut pose);

Jiggle Chain

rust
use rhizome_resin_rig::JiggleChain;

// Chain of bones (e.g., ponytail)
let mut chain = JiggleChain::new(
    vec![hair_1, hair_2, hair_3, hair_4],
    config,
);

chain.update(&skeleton, &pose, delta_time);
chain.apply(&mut pose);

Jiggle Mesh

rust
use rhizome_resin_rig::JiggleMesh;

let mut jiggle_mesh = JiggleMesh::new(
    &original_positions,
    config,
    anchored_vertices: vec![0, 1, 2],  // Fixed vertices
);

jiggle_mesh.update(&parent_transform, delta_time);
let deformed = jiggle_mesh.positions();

Follow-Through and Overlap

rust
use rhizome_resin_rig::{FollowThrough, OverlappingAction, RotationFollowThrough};

// Position follow-through (drag)
let mut follow = FollowThrough::new(bone, drag: 0.3);
follow.update(&skeleton, &pose, delta_time);

// Rotation overlap
let mut overlap = RotationFollowThrough::new(bone, lag: 0.2, damping: 0.9);
overlap.update(&skeleton, &pose, delta_time);
overlap.apply(&mut pose);

// Overlapping action (delay secondary elements)
let mut overlap = OverlappingAction::new(
    primary_bone: arm,
    secondary_bones: vec![sleeve_1, sleeve_2],
    delay: 0.1,
);

Wind Forces

rust
use rhizome_resin_rig::{WindForce, apply_wind_to_bone, apply_wind_to_chain};

let wind = WindForce {
    direction: Vec3::new(1.0, 0.0, 0.3),
    strength: 2.0,
    turbulence: 0.5,
    frequency: 1.0,
};

// Apply to single bone
apply_wind_to_bone(&wind, &mut jiggle_bone, time);

// Apply to chain
apply_wind_to_chain(&wind, &mut jiggle_chain, time);

3D Paths

rust
use rhizome_resin_rig::{Path3D, Path3DBuilder, PathSample};

// Build path
let path = Path3DBuilder::new()
    .move_to(Vec3::ZERO)
    .line_to(Vec3::new(1.0, 0.0, 0.0))
    .cubic_to(
        Vec3::new(1.5, 0.5, 0.0),  // Control 1
        Vec3::new(2.0, 0.5, 0.0),  // Control 2
        Vec3::new(2.5, 0.0, 0.0),  // End
    )
    .build();

// Sample path
let sample: PathSample = path.sample(t: 0.5);
let position = sample.position;
let tangent = sample.tangent;
let normal = sample.normal;

// Get total length
let length = path.length();

// Sample at arc length
let sample = path.sample_at_length(distance: 1.5);

Example: Complete Character Rig

rust
use rhizome_resin_rig::*;

// Setup skeleton
let mut skeleton = Skeleton::new();
// ... add bones ...

// Load animations
let idle = AnimationClip::load("idle.anim")?;
let walk = AnimationClip::load("walk.anim")?;
let run = AnimationClip::load("run.anim")?;

// Setup blend tree
let locomotion = BlendNode::Blend1D {
    clips: vec![idle, walk, run],
    thresholds: vec![0.0, 0.5, 1.0],
    parameter: speed,
};

// Setup IK
let left_arm_ik = IkChain {
    bones: vec![shoulder_l, elbow_l, wrist_l],
    target: Vec3::ZERO,
    pole: Some(Vec3::new(0.0, 0.0, -1.0)),
};

// Setup secondary motion
let mut hair_physics = JiggleChain::new(
    vec![hair_1, hair_2, hair_3],
    SecondaryConfig::default(),
);

// Game loop
loop {
    // Animation
    locomotion.sample(time, &mut pose);

    // IK (reach for object)
    if reaching {
        left_arm_ik.target = target_position;
        solve_fabrik(&skeleton, &mut pose, &left_arm_ik, &ik_config);
    }

    // Secondary motion
    hair_physics.update(&skeleton, &pose, delta_time);
    hair_physics.apply(&mut pose);

    // Skin mesh
    let deformed = skin.deform_positions(&mesh_positions, &skeleton, &pose);
}