๐ŸŽฌ Animation System Guide

February 24, 2026 ยท View on GitHub

Overview

World of Warcraft's animation system is sophisticated, supporting skeletal animations, morph targets, texture animations, and complex blending. This guide covers implementing a complete animation system using warcraft-rs, including bone hierarchies, animation tracks, blending, and advanced features like animation events and facial expressions.

Prerequisites

Before implementing the animation system, ensure you have:

  • Strong understanding of skeletal animation concepts
  • Knowledge of quaternion math and matrix transformations
  • warcraft-rs installed with animation support
  • Familiarity with interpolation techniques
  • Understanding of animation state machines

Understanding WoW Animation System

Animation Components

  • Bones: Hierarchical skeleton structure
  • Animation Sequences: Named animations with timing data
  • Keyframes: Transform data at specific times
  • Tracks: Separate channels for translation, rotation, scale
  • Interpolation: Linear, hermite, or bezier curves
  • Global Sequences: Looping animations (texture scrolling, etc.)
  • Animation Lookup: Mapping animation IDs to sequences

Animation Types

  • Character Animations: Walk, run, attack, idle, etc.
  • Facial Animations: Expressions and lip sync
  • Texture Animations: UV scrolling and transformations
  • Particle Animations: Emitter behavior over time
  • Camera Animations: Cutscene camera movements

Step-by-Step Instructions

1. Building the Animation System Core

use nalgebra::{Vector3, Quaternion, Matrix4, Unit};
use std::collections::HashMap;

#[derive(Debug, Clone)]
pub struct AnimationSystem {
    skeletons: HashMap<String, Skeleton>,
    animations: HashMap<String, AnimationClip>,
    blend_trees: HashMap<String, BlendTree>,
    global_time: f32,
}

#[derive(Debug, Clone)]
pub struct Skeleton {
    bones: Vec<Bone>,
    bone_names: HashMap<String, usize>,
    rest_pose: Vec<Transform>,
}

#[derive(Debug, Clone)]
pub struct Bone {
    name: String,
    parent: Option<usize>,
    flags: BoneFlags,
    pivot: Vector3<f32>,
}

#[derive(Debug, Clone, Copy)]
pub struct Transform {
    translation: Vector3<f32>,
    rotation: Unit<Quaternion<f32>>,
    scale: Vector3<f32>,
}

impl Transform {
    pub fn identity() -> Self {
        Self {
            translation: Vector3::zeros(),
            rotation: Unit::new_unchecked(Quaternion::identity()),
            scale: Vector3::new(1.0, 1.0, 1.0),
        }
    }

    pub fn to_matrix(&self) -> Matrix4<f32> {
        let t = Matrix4::new_translation(&self.translation);
        let r = self.rotation.to_homogeneous();
        let s = Matrix4::new_nonuniform_scaling(&self.scale);
        t * r * s
    }

    pub fn interpolate(&self, other: &Self, t: f32) -> Self {
        Self {
            translation: self.translation.lerp(&other.translation, t),
            rotation: Unit::new_unchecked(self.rotation.slerp(&other.rotation, t)),
            scale: self.scale.lerp(&other.scale, t),
        }
    }
}

2. Animation Tracks and Keyframes

use wow_m2::{AnimationBlock, InterpolationType};

#[derive(Debug, Clone)]
pub struct AnimationClip {
    name: String,
    duration: u32, // milliseconds
    loop_mode: LoopMode,
    bone_tracks: Vec<BoneTrack>,
    events: Vec<AnimationEvent>,
}

#[derive(Debug, Clone)]
pub struct BoneTrack {
    bone_index: usize,
    translation: Track<Vector3<f32>>,
    rotation: Track<Quaternion<f32>>,
    scale: Track<Vector3<f32>>,
}

#[derive(Debug, Clone)]
pub struct Track<T> {
    keyframes: Vec<Keyframe<T>>,
    interpolation: InterpolationType,
}

#[derive(Debug, Clone)]
pub struct Keyframe<T> {
    time: u32,
    value: T,
    in_tangent: Option<T>,
    out_tangent: Option<T>,
}

#[derive(Debug, Clone, Copy)]
pub enum LoopMode {
    Once,
    Loop,
    PingPong,
    ClampForever,
}

impl<T: Interpolatable> Track<T> {
    pub fn sample(&self, time: u32, loop_mode: LoopMode, duration: u32) -> T {
        if self.keyframes.is_empty() {
            return T::default();
        }

        // Handle looping
        let time = match loop_mode {
            LoopMode::Once => time.min(duration),
            LoopMode::Loop => time % duration,
            LoopMode::PingPong => {
                let cycle = time / duration;
                if cycle % 2 == 0 {
                    time % duration
                } else {
                    duration - (time % duration)
                }
            }
            LoopMode::ClampForever => time.min(duration),
        };

        // Find surrounding keyframes
        let (prev, next) = self.find_keyframes(time);

        if prev == next {
            return self.keyframes[prev].value.clone();
        }

        // Calculate interpolation factor
        let prev_key = &self.keyframes[prev];
        let next_key = &self.keyframes[next];
        let t = (time - prev_key.time) as f32 / (next_key.time - prev_key.time) as f32;

        // Interpolate based on type
        match self.interpolation {
            InterpolationType::None => prev_key.value.clone(),
            InterpolationType::Linear => {
                prev_key.value.lerp(&next_key.value, t)
            }
            InterpolationType::Hermite => {
                self.hermite_interpolate(prev, next, t)
            }
            InterpolationType::Bezier => {
                self.bezier_interpolate(prev, next, t)
            }
        }
    }

    fn find_keyframes(&self, time: u32) -> (usize, usize) {
        // Binary search for efficiency
        let pos = self.keyframes.binary_search_by_key(&time, |k| k.time);

        match pos {
            Ok(idx) => (idx, idx),
            Err(idx) => {
                if idx == 0 {
                    (0, 0)
                } else if idx >= self.keyframes.len() {
                    let last = self.keyframes.len() - 1;
                    (last, last)
                } else {
                    (idx - 1, idx)
                }
            }
        }
    }

    fn hermite_interpolate(&self, prev_idx: usize, next_idx: usize, t: f32) -> T {
        let p0 = &self.keyframes[prev_idx];
        let p1 = &self.keyframes[next_idx];

        let m0 = p0.out_tangent.as_ref().unwrap_or(&p0.value);
        let m1 = p1.in_tangent.as_ref().unwrap_or(&p1.value);

        // Hermite interpolation formula
        let t2 = t * t;
        let t3 = t2 * t;

        let h00 = 2.0 * t3 - 3.0 * t2 + 1.0;
        let h10 = t3 - 2.0 * t2 + t;
        let h01 = -2.0 * t3 + 3.0 * t2;
        let h11 = t3 - t2;

        p0.value.scale(h00)
            .add(&m0.scale(h10))
            .add(&p1.value.scale(h01))
            .add(&m1.scale(h11))
    }
}

trait Interpolatable: Clone + Default {
    fn lerp(&self, other: &Self, t: f32) -> Self;
    fn scale(&self, s: f32) -> Self;
    fn add(&self, other: &Self) -> Self;
}

impl Interpolatable for Vector3<f32> {
    fn lerp(&self, other: &Self, t: f32) -> Self {
        self + (other - self) * t
    }

    fn scale(&self, s: f32) -> Self {
        self * s
    }

    fn add(&self, other: &Self) -> Self {
        self + other
    }
}

impl Interpolatable for Quaternion<f32> {
    fn lerp(&self, other: &Self, t: f32) -> Self {
        self.slerp(other, t)
    }

    fn scale(&self, s: f32) -> Self {
        self.powf(s)
    }

    fn add(&self, other: &Self) -> Self {
        self * other
    }
}

3. Animation State Machine

use std::collections::VecDeque;

#[derive(Debug, Clone)]
pub struct AnimationStateMachine {
    states: HashMap<String, AnimationState>,
    transitions: Vec<StateTransition>,
    current_state: String,
    parameters: HashMap<String, AnimationParameter>,
    transition_queue: VecDeque<TransitionInfo>,
}

#[derive(Debug, Clone)]
pub struct AnimationState {
    name: String,
    animation_clip: String,
    speed: f32,
    motion: Option<RootMotion>,
}

#[derive(Debug, Clone)]
pub struct StateTransition {
    from: String,
    to: String,
    duration: f32,
    conditions: Vec<TransitionCondition>,
}

#[derive(Debug, Clone)]
pub enum TransitionCondition {
    ParameterEquals(String, AnimationParameter),
    ParameterGreaterThan(String, f32),
    ParameterLessThan(String, f32),
    OnAnimationEnd,
}

#[derive(Debug, Clone)]
pub enum AnimationParameter {
    Float(f32),
    Int(i32),
    Bool(bool),
    Trigger(bool),
}

impl AnimationStateMachine {
    pub fn new(initial_state: String) -> Self {
        Self {
            states: HashMap::new(),
            transitions: Vec::new(),
            current_state: initial_state,
            parameters: HashMap::new(),
            transition_queue: VecDeque::new(),
        }
    }

    pub fn update(&mut self, delta_time: f32) -> Option<AnimationTransition> {
        // Check transition conditions
        self.check_transitions();

        // Process active transitions
        if let Some(mut transition) = self.transition_queue.front_mut() {
            transition.progress += delta_time / transition.duration;

            if transition.progress >= 1.0 {
                // Complete transition
                let completed = self.transition_queue.pop_front().unwrap();
                self.current_state = completed.to_state;

                return Some(AnimationTransition {
                    from: completed.from_state,
                    to: completed.to_state,
                    blend_factor: 1.0,
                });
            }

            return Some(AnimationTransition {
                from: transition.from_state.clone(),
                to: transition.to_state.clone(),
                blend_factor: transition.progress,
            });
        }

        None
    }

    fn check_transitions(&mut self) {
        for transition in &self.transitions {
            if transition.from != self.current_state {
                continue;
            }

            let mut all_conditions_met = true;

            for condition in &transition.conditions {
                if !self.evaluate_condition(condition) {
                    all_conditions_met = false;
                    break;
                }
            }

            if all_conditions_met {
                self.transition_queue.push_back(TransitionInfo {
                    from_state: transition.from.clone(),
                    to_state: transition.to.clone(),
                    duration: transition.duration,
                    progress: 0.0,
                });
                break;
            }
        }
    }

    fn evaluate_condition(&self, condition: &TransitionCondition) -> bool {
        match condition {
            TransitionCondition::ParameterEquals(name, expected) => {
                self.parameters.get(name) == Some(expected)
            }
            TransitionCondition::ParameterGreaterThan(name, threshold) => {
                if let Some(AnimationParameter::Float(value)) = self.parameters.get(name) {
                    value > threshold
                } else {
                    false
                }
            }
            TransitionCondition::ParameterLessThan(name, threshold) => {
                if let Some(AnimationParameter::Float(value)) = self.parameters.get(name) {
                    value < threshold
                } else {
                    false
                }
            }
            TransitionCondition::OnAnimationEnd => {
                // Check if current animation has ended
                false // Implement based on animation playback state
            }
        }
    }
}

#[derive(Debug, Clone)]
struct TransitionInfo {
    from_state: String,
    to_state: String,
    duration: f32,
    progress: f32,
}

#[derive(Debug, Clone)]
pub struct AnimationTransition {
    pub from: String,
    pub to: String,
    pub blend_factor: f32,
}

4. Animation Blending

#[derive(Debug, Clone)]
pub struct AnimationBlender {
    blend_mode: BlendMode,
    layers: Vec<AnimationLayer>,
}

#[derive(Debug, Clone)]
pub struct AnimationLayer {
    animation: String,
    weight: f32,
    mask: Option<BoneMask>,
    blend_mode: LayerBlendMode,
}

#[derive(Debug, Clone)]
pub struct BoneMask {
    bones: HashSet<usize>,
    include_descendants: bool,
}

#[derive(Debug, Clone, Copy)]
pub enum BlendMode {
    Override,
    Additive,
    Blend,
}

#[derive(Debug, Clone, Copy)]
pub enum LayerBlendMode {
    Override,
    Additive,
    Multiply,
}

impl AnimationBlender {
    pub fn blend_animations(
        &self,
        animations: &HashMap<String, AnimationClip>,
        skeleton: &Skeleton,
        time: u32,
    ) -> Vec<Transform> {
        let bone_count = skeleton.bones.len();
        let mut final_transforms = vec![Transform::identity(); bone_count];
        let mut bone_weights = vec![0.0; bone_count];

        // Process each layer
        for layer in &self.layers {
            if layer.weight <= 0.0 {
                continue;
            }

            let animation = match animations.get(&layer.animation) {
                Some(anim) => anim,
                None => continue,
            };

            // Sample animation
            let layer_transforms = self.sample_animation(animation, skeleton, time);

            // Apply layer blending
            for bone_idx in 0..bone_count {
                // Check bone mask
                if let Some(mask) = &layer.mask {
                    if !self.is_bone_in_mask(bone_idx, mask, skeleton) {
                        continue;
                    }
                }

                let weight = layer.weight;

                match layer.blend_mode {
                    LayerBlendMode::Override => {
                        if bone_weights[bone_idx] < 1.0 {
                            let remaining = 1.0 - bone_weights[bone_idx];
                            let actual_weight = weight.min(remaining);

                            final_transforms[bone_idx] = final_transforms[bone_idx]
                                .interpolate(&layer_transforms[bone_idx], actual_weight);

                            bone_weights[bone_idx] += actual_weight;
                        }
                    }
                    LayerBlendMode::Additive => {
                        // Add to existing transform
                        let additive = layer_transforms[bone_idx];
                        final_transforms[bone_idx].translation += additive.translation * weight;

                        // Blend rotation additively
                        let added_rot = Quaternion::identity().slerp(&additive.rotation, weight);
                        final_transforms[bone_idx].rotation =
                            Unit::new_normalize(final_transforms[bone_idx].rotation.as_ref() * added_rot);
                    }
                    LayerBlendMode::Multiply => {
                        // Multiply transforms
                        final_transforms[bone_idx].scale.component_mul_assign(
                            &layer_transforms[bone_idx].scale.lerp(&Vector3::new(1.0, 1.0, 1.0), 1.0 - weight)
                        );
                    }
                }
            }
        }

        final_transforms
    }

    fn sample_animation(
        &self,
        animation: &AnimationClip,
        skeleton: &Skeleton,
        time: u32,
    ) -> Vec<Transform> {
        let mut transforms = skeleton.rest_pose.clone();

        for track in &animation.bone_tracks {
            let bone_idx = track.bone_index;

            transforms[bone_idx] = Transform {
                translation: track.translation.sample(time, animation.loop_mode, animation.duration),
                rotation: Unit::new_normalize(
                    track.rotation.sample(time, animation.loop_mode, animation.duration)
                ),
                scale: track.scale.sample(time, animation.loop_mode, animation.duration),
            };
        }

        transforms
    }

    fn is_bone_in_mask(&self, bone_idx: usize, mask: &BoneMask, skeleton: &Skeleton) -> bool {
        if mask.bones.contains(&bone_idx) {
            return true;
        }

        if mask.include_descendants {
            // Check if any ancestor is in the mask
            let mut current = bone_idx;
            while let Some(parent) = skeleton.bones[current].parent {
                if mask.bones.contains(&parent) {
                    return true;
                }
                current = parent;
            }
        }

        false
    }
}

5. Procedural Animation System

#[derive(Debug, Clone)]
pub struct ProceduralAnimator {
    ik_chains: Vec<IKChain>,
    physics_bones: Vec<PhysicsBone>,
    look_at_constraints: Vec<LookAtConstraint>,
}

#[derive(Debug, Clone)]
pub struct IKChain {
    end_effector: usize,
    chain_length: usize,
    target: Vector3<f32>,
    pole_target: Option<Vector3<f32>>,
    iterations: usize,
    tolerance: f32,
}

#[derive(Debug, Clone)]
pub struct PhysicsBone {
    bone_index: usize,
    mass: f32,
    damping: f32,
    stiffness: f32,
    gravity_scale: f32,
    velocity: Vector3<f32>,
    constraints: Vec<PhysicsConstraint>,
}

#[derive(Debug, Clone)]
pub struct LookAtConstraint {
    bone_index: usize,
    target: Vector3<f32>,
    up_vector: Vector3<f32>,
    weight: f32,
    limits: Option<RotationLimits>,
}

impl ProceduralAnimator {
    pub fn apply_procedural_animation(
        &mut self,
        transforms: &mut [Transform],
        skeleton: &Skeleton,
        world_matrices: &[Matrix4<f32>],
        delta_time: f32,
    ) {
        // Apply IK chains
        for chain in &self.ik_chains {
            self.solve_ik_chain(chain, transforms, skeleton, world_matrices);
        }

        // Apply physics simulation
        for physics_bone in &mut self.physics_bones {
            self.simulate_physics_bone(physics_bone, transforms, world_matrices, delta_time);
        }

        // Apply look-at constraints
        for constraint in &self.look_at_constraints {
            self.apply_look_at(constraint, transforms, world_matrices);
        }
    }

    fn solve_ik_chain(
        &self,
        chain: &IKChain,
        transforms: &mut [Transform],
        skeleton: &Skeleton,
        world_matrices: &[Matrix4<f32>],
    ) {
        // FABRIK (Forward And Backward Reaching Inverse Kinematics)
        let mut bone_indices = Vec::new();
        let mut current = chain.end_effector;

        // Build chain
        for _ in 0..chain.chain_length {
            bone_indices.push(current);
            if let Some(parent) = skeleton.bones[current].parent {
                current = parent;
            } else {
                break;
            }
        }

        bone_indices.reverse();

        // Store original positions
        let mut positions: Vec<Vector3<f32>> = bone_indices
            .iter()
            .map(|&idx| world_matrices[idx].transform_point(&Point3::origin()).coords)
            .collect();

        let base_pos = positions[0];

        // FABRIK iterations
        for _ in 0..chain.iterations {
            // Forward reaching
            positions[positions.len() - 1] = chain.target;

            for i in (0..positions.len() - 1).rev() {
                let direction = (positions[i] - positions[i + 1]).normalize();
                let bone_length = self.calculate_bone_length(&bone_indices, i, skeleton);
                positions[i] = positions[i + 1] + direction * bone_length;
            }

            // Backward reaching
            positions[0] = base_pos;

            for i in 0..positions.len() - 1 {
                let direction = (positions[i + 1] - positions[i]).normalize();
                let bone_length = self.calculate_bone_length(&bone_indices, i, skeleton);
                positions[i + 1] = positions[i] + direction * bone_length;
            }

            // Check tolerance
            let error = (positions[positions.len() - 1] - chain.target).magnitude();
            if error < chain.tolerance {
                break;
            }
        }

        // Apply rotations to achieve positions
        for i in 0..bone_indices.len() - 1 {
            let bone_idx = bone_indices[i];
            let child_idx = bone_indices[i + 1];

            // Calculate required rotation
            let current_dir = (world_matrices[child_idx].transform_point(&Point3::origin()) -
                             world_matrices[bone_idx].transform_point(&Point3::origin())).normalize();
            let target_dir = (positions[i + 1] - positions[i]).normalize();

            let rotation = Quaternion::rotation_between(&current_dir, &target_dir)
                .unwrap_or(Quaternion::identity());

            // Apply rotation in local space
            transforms[bone_idx].rotation = Unit::new_normalize(
                transforms[bone_idx].rotation.as_ref() * rotation
            );
        }
    }

    fn calculate_bone_length(&self, chain: &[usize], index: usize, skeleton: &Skeleton) -> f32 {
        if index >= chain.len() - 1 {
            return 0.0;
        }

        let bone = &skeleton.bones[chain[index]];
        let child = &skeleton.bones[chain[index + 1]];

        (child.pivot - bone.pivot).magnitude()
    }
}

6. Animation Events and Callbacks

#[derive(Debug, Clone)]
pub struct AnimationEvent {
    time: u32,
    event_type: AnimationEventType,
    parameters: HashMap<String, String>,
}

#[derive(Debug, Clone)]
pub enum AnimationEventType {
    Sound(String),
    Particle(String),
    Footstep(FootType),
    WeaponSwing,
    Custom(String),
}

#[derive(Debug, Clone, Copy)]
pub enum FootType {
    Left,
    Right,
}

pub struct AnimationEventHandler {
    handlers: HashMap<String, Box<dyn Fn(&AnimationEvent) + Send + Sync>>,
    queued_events: VecDeque<QueuedEvent>,
}

#[derive(Debug, Clone)]
struct QueuedEvent {
    event: AnimationEvent,
    fire_time: f32,
}

impl AnimationEventHandler {
    pub fn new() -> Self {
        Self {
            handlers: HashMap::new(),
            queued_events: VecDeque::new(),
        }
    }

    pub fn register_handler<F>(&mut self, event_type: &str, handler: F)
    where
        F: Fn(&AnimationEvent) + Send + Sync + 'static,
    {
        self.handlers.insert(event_type.to_string(), Box::new(handler));
    }

    pub fn process_animation_events(
        &mut self,
        animation: &AnimationClip,
        prev_time: u32,
        current_time: u32,
        global_time: f32,
    ) {
        // Handle looping
        let duration = animation.duration;

        match animation.loop_mode {
            LoopMode::Once => {
                self.collect_events_in_range(animation, prev_time, current_time, global_time);
            }
            LoopMode::Loop => {
                if current_time < prev_time {
                    // Wrapped around
                    self.collect_events_in_range(animation, prev_time, duration, global_time);
                    self.collect_events_in_range(animation, 0, current_time, global_time);
                } else {
                    self.collect_events_in_range(animation, prev_time, current_time, global_time);
                }
            }
            _ => {
                // Handle other loop modes
            }
        }

        // Fire queued events
        self.fire_ready_events(global_time);
    }

    fn collect_events_in_range(
        &mut self,
        animation: &AnimationClip,
        start_time: u32,
        end_time: u32,
        global_time: f32,
    ) {
        for event in &animation.events {
            if event.time > start_time && event.time <= end_time {
                self.queued_events.push_back(QueuedEvent {
                    event: event.clone(),
                    fire_time: global_time,
                });
            }
        }
    }

    fn fire_ready_events(&mut self, current_time: f32) {
        while let Some(queued) = self.queued_events.front() {
            if queued.fire_time <= current_time {
                let event = self.queued_events.pop_front().unwrap();
                self.fire_event(&event.event);
            } else {
                break;
            }
        }
    }

    fn fire_event(&self, event: &AnimationEvent) {
        let type_name = match &event.event_type {
            AnimationEventType::Sound(_) => "sound",
            AnimationEventType::Particle(_) => "particle",
            AnimationEventType::Footstep(_) => "footstep",
            AnimationEventType::WeaponSwing => "weapon_swing",
            AnimationEventType::Custom(name) => name,
        };

        if let Some(handler) = self.handlers.get(type_name) {
            handler(event);
        }
    }
}

Code Examples

Complete Animation Player

use wow_m2::M2Model;

pub struct AnimationPlayer {
    model: Arc<M2Model>,
    skeleton: Skeleton,
    animation_system: AnimationSystem,
    state_machine: AnimationStateMachine,
    blender: AnimationBlender,
    event_handler: AnimationEventHandler,
    current_pose: Vec<Transform>,
    world_matrices: Vec<Matrix4<f32>>,
    animation_time: HashMap<String, u32>,
}

impl AnimationPlayer {
    pub fn new(model: Arc<M2Model>) -> Self {
        let skeleton = build_skeleton_from_m2(&model);
        let animations = load_animations_from_m2(&model);

        let mut animation_system = AnimationSystem::new();
        animation_system.skeletons.insert("main".to_string(), skeleton.clone());

        for (name, clip) in animations {
            animation_system.animations.insert(name, clip);
        }

        let bone_count = skeleton.bones.len();

        Self {
            model,
            skeleton,
            animation_system,
            state_machine: AnimationStateMachine::new("idle".to_string()),
            blender: AnimationBlender::default(),
            event_handler: AnimationEventHandler::new(),
            current_pose: vec![Transform::identity(); bone_count],
            world_matrices: vec![Matrix4::identity(); bone_count],
            animation_time: HashMap::new(),
        }
    }

    pub fn update(&mut self, delta_time: f32) {
        // Update state machine
        if let Some(transition) = self.state_machine.update(delta_time) {
            // Handle state transition
            self.handle_transition(transition);
        }

        // Update animation times
        for (anim_name, time) in &mut self.animation_time {
            if let Some(animation) = self.animation_system.animations.get(anim_name) {
                let prev_time = *time;
                *time = (*time + (delta_time * 1000.0) as u32) % animation.duration;

                // Process animation events
                self.event_handler.process_animation_events(
                    animation,
                    prev_time,
                    *time,
                    self.animation_system.global_time,
                );
            }
        }

        // Blend animations
        self.current_pose = self.blender.blend_animations(
            &self.animation_system.animations,
            &self.skeleton,
            self.get_current_animation_time(),
        );

        // Calculate world matrices
        self.calculate_world_matrices();

        // Update global time
        self.animation_system.global_time += delta_time;
    }

    fn calculate_world_matrices(&mut self) {
        for (bone_idx, bone) in self.skeleton.bones.iter().enumerate() {
            let local_matrix = self.current_pose[bone_idx].to_matrix();

            self.world_matrices[bone_idx] = if let Some(parent_idx) = bone.parent {
                self.world_matrices[parent_idx] * local_matrix
            } else {
                local_matrix
            };
        }
    }

    pub fn play_animation(&mut self, name: &str, fade_in: f32) {
        self.blender.layers.clear();
        self.blender.layers.push(AnimationLayer {
            animation: name.to_string(),
            weight: 1.0,
            mask: None,
            blend_mode: LayerBlendMode::Override,
        });

        self.animation_time.insert(name.to_string(), 0);
    }

    pub fn add_animation_layer(&mut self, name: &str, weight: f32, mask: Option<BoneMask>) {
        self.blender.layers.push(AnimationLayer {
            animation: name.to_string(),
            weight,
            mask,
            blend_mode: LayerBlendMode::Override,
        });

        self.animation_time.entry(name.to_string()).or_insert(0);
    }

    pub fn get_bone_matrices(&self) -> &[Matrix4<f32>] {
        &self.world_matrices
    }

    pub fn set_animation_speed(&mut self, animation: &str, speed: f32) {
        if let Some(state) = self.state_machine.states.get_mut(animation) {
            state.speed = speed;
        }
    }
}

fn build_skeleton_from_m2(model: &M2Model) -> Skeleton {
    let mut bones = Vec::new();
    let mut bone_names = HashMap::new();

    for (idx, m2_bone) in model.bones.iter().enumerate() {
        bones.push(Bone {
            name: format!("bone_{}", idx),
            parent: if m2_bone.parent_bone >= 0 {
                Some(m2_bone.parent_bone as usize)
            } else {
                None
            },
            flags: BoneFlags::from_bits(m2_bone.flags).unwrap_or_default(),
            pivot: m2_bone.pivot,
        });

        bone_names.insert(format!("bone_{}", idx), idx);
    }

    // Build rest pose
    let rest_pose = bones.iter().map(|_| Transform::identity()).collect();

    Skeleton {
        bones,
        bone_names,
        rest_pose,
    }
}

Facial Animation System

pub struct FacialAnimationSystem {
    blend_shapes: Vec<BlendShape>,
    emotion_presets: HashMap<String, EmotionPreset>,
    lip_sync_data: Option<LipSyncData>,
    current_emotion: String,
    emotion_blend: f32,
}

#[derive(Debug, Clone)]
pub struct BlendShape {
    name: String,
    vertices: Vec<u32>,
    deltas: Vec<Vector3<f32>>,
    current_weight: f32,
}

#[derive(Debug, Clone)]
pub struct EmotionPreset {
    name: String,
    blend_shape_weights: HashMap<String, f32>,
    duration: f32,
}

#[derive(Debug, Clone)]
pub struct LipSyncData {
    phonemes: Vec<Phoneme>,
    current_phoneme: usize,
}

#[derive(Debug, Clone)]
pub struct Phoneme {
    time: f32,
    duration: f32,
    blend_shapes: HashMap<String, f32>,
}

impl FacialAnimationSystem {
    pub fn apply_facial_animation(
        &mut self,
        vertices: &mut [Vertex],
        audio_time: f32,
        emotion: &str,
        intensity: f32,
    ) {
        // Apply emotion preset
        if emotion != self.current_emotion {
            self.transition_emotion(emotion);
        }

        // Update emotion blend
        self.update_emotion_blend(intensity);

        // Apply lip sync if available
        if let Some(lip_sync) = &mut self.lip_sync_data {
            self.apply_lip_sync(lip_sync, audio_time);
        }

        // Apply blend shapes to vertices
        self.apply_blend_shapes(vertices);
    }

    fn apply_blend_shapes(&self, vertices: &mut [Vertex]) {
        for shape in &self.blend_shapes {
            if shape.current_weight > 0.0 {
                for (vert_idx, delta) in shape.vertices.iter().zip(&shape.deltas) {
                    let vertex = &mut vertices[*vert_idx as usize];
                    vertex.position += delta * shape.current_weight;
                }
            }
        }
    }

    fn transition_emotion(&mut self, new_emotion: &str) {
        self.current_emotion = new_emotion.to_string();
        self.emotion_blend = 0.0;

        // Reset blend shape weights
        for shape in &mut self.blend_shapes {
            shape.current_weight = 0.0;
        }
    }

    fn update_emotion_blend(&mut self, target_intensity: f32) {
        self.emotion_blend = self.emotion_blend.lerp(&target_intensity, 0.1);

        if let Some(preset) = self.emotion_presets.get(&self.current_emotion) {
            for (shape_name, target_weight) in &preset.blend_shape_weights {
                if let Some(shape) = self.blend_shapes.iter_mut()
                    .find(|s| s.name == *shape_name) {
                    shape.current_weight = shape.current_weight.lerp(
                        &(target_weight * self.emotion_blend),
                        0.2
                    );
                }
            }
        }
    }
}

Best Practices

1. Animation Compression

pub struct AnimationCompressor {
    position_threshold: f32,
    rotation_threshold: f32,
    scale_threshold: f32,
}

impl AnimationCompressor {
    pub fn compress_animation(&self, clip: &AnimationClip) -> CompressedAnimation {
        let mut compressed = CompressedAnimation {
            name: clip.name.clone(),
            duration: clip.duration,
            tracks: Vec::new(),
        };

        for track in &clip.bone_tracks {
            let compressed_track = CompressedTrack {
                bone_index: track.bone_index,
                position_keys: self.compress_vector_track(&track.translation),
                rotation_keys: self.compress_quaternion_track(&track.rotation),
                scale_keys: self.compress_vector_track(&track.scale),
            };

            compressed.tracks.push(compressed_track);
        }

        compressed
    }

    fn compress_vector_track(&self, track: &Track<Vector3<f32>>) -> Vec<CompressedKey<[f16; 3]>> {
        if track.keyframes.is_empty() {
            return Vec::new();
        }

        let mut compressed = vec![self.compress_vector_key(&track.keyframes[0])];
        let mut last_value = track.keyframes[0].value;

        for key in track.keyframes.iter().skip(1) {
            let delta = (key.value - last_value).magnitude();

            if delta > self.position_threshold {
                compressed.push(self.compress_vector_key(key));
                last_value = key.value;
            }
        }

        // Always include last key
        let last = track.keyframes.last().unwrap();
        compressed.push(self.compress_vector_key(last));

        compressed
    }

    fn compress_vector_key(&self, key: &Keyframe<Vector3<f32>>) -> CompressedKey<[f16; 3]> {
        CompressedKey {
            time: key.time as u16,
            value: [
                half::f16::from_f32(key.value.x),
                half::f16::from_f32(key.value.y),
                half::f16::from_f32(key.value.z),
            ],
        }
    }
}

#[derive(Debug, Clone)]
pub struct CompressedAnimation {
    name: String,
    duration: u32,
    tracks: Vec<CompressedTrack>,
}

#[derive(Debug, Clone)]
pub struct CompressedTrack {
    bone_index: usize,
    position_keys: Vec<CompressedKey<[f16; 3]>>,
    rotation_keys: Vec<CompressedKey<[i16; 4]>>,
    scale_keys: Vec<CompressedKey<[f16; 3]>>,
}

#[derive(Debug, Clone)]
pub struct CompressedKey<T> {
    time: u16,
    value: T,
}

2. Animation Caching

pub struct AnimationCache {
    cache: LruCache<AnimationCacheKey, Arc<Vec<Transform>>>,
    max_entries: usize,
}

#[derive(Debug, Clone, Hash, Eq, PartialEq)]
struct AnimationCacheKey {
    animation_name: String,
    time: u32,
    blend_weights: Vec<OrderedFloat<f32>>,
}

impl AnimationCache {
    pub fn new(max_entries: usize) -> Self {
        Self {
            cache: LruCache::new(max_entries),
            max_entries,
        }
    }

    pub fn get_or_compute<F>(
        &mut self,
        key: AnimationCacheKey,
        compute: F,
    ) -> Arc<Vec<Transform>>
    where
        F: FnOnce() -> Vec<Transform>,
    {
        if let Some(cached) = self.cache.get(&key) {
            return cached.clone();
        }

        let computed = Arc::new(compute());
        self.cache.put(key, computed.clone());
        computed
    }
}

3. Multi-threaded Animation

use rayon::prelude::*;

pub struct ParallelAnimationProcessor {
    thread_pool: ThreadPool,
}

impl ParallelAnimationProcessor {
    pub fn process_animation_batch(
        &self,
        animations: &[AnimationInstance],
    ) -> Vec<AnimationResult> {
        animations
            .par_iter()
            .map(|instance| self.process_single_animation(instance))
            .collect()
    }

    fn process_single_animation(&self, instance: &AnimationInstance) -> AnimationResult {
        // Process animation on thread pool
        let pose = instance.player.calculate_pose(instance.time);
        let matrices = self.calculate_matrices(&pose);

        AnimationResult {
            instance_id: instance.id,
            bone_matrices: matrices,
        }
    }
}

Common Issues and Solutions

Issue: Animation Jitter

Problem: Animations appear jittery or stuttering.

Solution:

pub struct AnimationSmoother {
    history: VecDeque<Vec<Transform>>,
    max_history: usize,
}

impl AnimationSmoother {
    pub fn smooth_animation(&mut self, current_pose: Vec<Transform>) -> Vec<Transform> {
        self.history.push_back(current_pose.clone());

        if self.history.len() > self.max_history {
            self.history.pop_front();
        }

        // Average recent poses
        let mut smoothed = current_pose;
        let history_weight = 0.3;

        for (i, pose) in self.history.iter().rev().enumerate().skip(1) {
            let weight = history_weight * (1.0 / (i as f32 + 1.0));

            for (bone_idx, transform) in pose.iter().enumerate() {
                smoothed[bone_idx] = smoothed[bone_idx].interpolate(transform, weight);
            }
        }

        smoothed
    }
}

Issue: Bone Hierarchy Errors

Problem: Child bones not following parent transformations.

Solution:

fn validate_bone_hierarchy(skeleton: &Skeleton) -> Result<(), String> {
    let mut visited = vec![false; skeleton.bones.len()];

    for (idx, bone) in skeleton.bones.iter().enumerate() {
        if let Some(parent) = bone.parent {
            if parent >= skeleton.bones.len() {
                return Err(format!("Bone {} has invalid parent {}", idx, parent));
            }

            if parent == idx {
                return Err(format!("Bone {} is its own parent", idx));
            }

            // Check for cycles
            let mut current = parent;
            let mut chain = HashSet::new();
            chain.insert(idx);

            while let Some(next_parent) = skeleton.bones[current].parent {
                if chain.contains(&next_parent) {
                    return Err(format!("Cycle detected in bone hierarchy at {}", idx));
                }
                chain.insert(current);
                current = next_parent;
            }
        }

        visited[idx] = true;
    }

    Ok(())
}

Issue: Animation Blending Artifacts

Problem: Unnatural poses when blending between animations.

Solution:

pub struct SmartBlender {
    sync_markers: HashMap<String, Vec<SyncMarker>>,
}

#[derive(Debug, Clone)]
struct SyncMarker {
    time: f32,
    phase: f32,
    marker_type: SyncMarkerType,
}

impl SmartBlender {
    pub fn blend_with_sync(
        &self,
        from_anim: &str,
        to_anim: &str,
        blend_factor: f32,
    ) -> f32 {
        // Find matching sync markers
        let from_markers = self.sync_markers.get(from_anim);
        let to_markers = self.sync_markers.get(to_anim);

        if let (Some(from), Some(to)) = (from_markers, to_markers) {
            // Align animations based on sync markers
            let from_phase = self.calculate_phase(from);
            let to_phase = self.calculate_phase(to);

            // Adjust time to match phases
            let phase_diff = to_phase - from_phase;
            let time_adjustment = phase_diff * blend_factor;

            return time_adjustment;
        }

        0.0
    }
}

Performance Tips

1. GPU Skinning

// Vertex shader for GPU skinning
#version 450

layout(set = 0, binding = 0) uniform BoneMatrices {
    mat4 bones[256];
};

layout(location = 0) in vec3 position;
layout(location = 1) in vec3 normal;
layout(location = 2) in vec2 texcoord;
layout(location = 3) in uvec4 bone_indices;
layout(location = 4) in vec4 bone_weights;

layout(location = 0) out vec3 world_normal;
layout(location = 1) out vec2 out_texcoord;

void main() {
    mat4 skin_matrix =
        bones[bone_indices.x] * bone_weights.x +
        bones[bone_indices.y] * bone_weights.y +
        bones[bone_indices.z] * bone_weights.z +
        bones[bone_indices.w] * bone_weights.w;

    vec4 world_pos = skin_matrix * vec4(position, 1.0);
    gl_Position = view_proj * world_pos;

    world_normal = normalize((skin_matrix * vec4(normal, 0.0)).xyz);
    out_texcoord = texcoord;
}

2. Animation LOD

pub struct AnimationLod {
    bone_importance: HashMap<usize, f32>,
    distance_thresholds: Vec<f32>,
}

impl AnimationLod {
    pub fn get_active_bones(&self, distance: f32) -> HashSet<usize> {
        let importance_threshold = if distance < self.distance_thresholds[0] {
            0.0 // All bones
        } else if distance < self.distance_thresholds[1] {
            0.3 // Important bones only
        } else {
            0.7 // Critical bones only
        };

        self.bone_importance
            .iter()
            .filter(|(_, importance)| **importance >= importance_threshold)
            .map(|(idx, _)| *idx)
            .collect()
    }
}

3. Animation Streaming

pub struct AnimationStreamer {
    loaded_clips: HashMap<String, Arc<AnimationClip>>,
    loading_queue: Arc<Mutex<VecDeque<String>>>,
    loader_thread: Option<JoinHandle<()>>,
}

impl AnimationStreamer {
    pub async fn get_animation(&self, name: &str) -> Option<Arc<AnimationClip>> {
        if let Some(clip) = self.loaded_clips.get(name) {
            return Some(clip.clone());
        }

        // Queue for loading
        self.loading_queue.lock().unwrap().push_back(name.to_string());

        // Wait for load with timeout
        let start = Instant::now();
        while start.elapsed() < Duration::from_secs(5) {
            if let Some(clip) = self.loaded_clips.get(name) {
                return Some(clip.clone());
            }
            tokio::time::sleep(Duration::from_millis(10)).await;
        }

        None
    }
}

References