Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

M2 Anim Format 🎬

M2 .anim files contain external animation sequences that can be loaded on demand for M2 models.

Overview

  • Extension: .anim
  • Purpose: Store animations separately from main M2 file
  • Introduced: Wrath of the Lich King (3.0.2)
  • Benefits: Reduced memory usage, faster loading, animation sharing
  • Naming: <ModelName><AnimID>-<SubAnimID>.anim

Structure

File Naming Convention

// Format: ModelName + AnimID + SubAnimID
Character/BloodElf/Female/BloodElfFemale0060-00.anim
                                         ^^^^ ^^
                                         |    |
                                         |    SubAnimID (variation)
                                         AnimID (animation type)

Anim File Structure

#![allow(unused)]
fn main() {
struct AnimFileHeader {
    version: u32,           // Always 0x100 (version 1.0)
    sub_version: u32,       // Always 0
}

struct AnimFileData {
    global_sequences: Vec<GlobalSequence>,
    animations: Vec<M2Animation>,
    animation_lookups: Vec<i16>,
    play_anim_combos: Vec<PlayAnimCombo>,
    rel_anim_combos: Vec<RelAnimCombo>,
    bones: Vec<M2Bone>,
    key_bone_lookups: Vec<i16>,
    vertices: Vec<M2Vertex>,
    colors: Vec<M2Color>,
    textures: Vec<M2Texture>,
    texture_weights: Vec<M2TextureWeight>,
    texture_transforms: Vec<M2TextureTransform>,
    texture_combos: Vec<u16>,
    materials: Vec<M2Material>,
    material_combos: Vec<u16>,
    texture_coord_combos: Vec<u16>,
    fake_anim_ids: Vec<u16>,
    attachments: Vec<M2Attachment>,
}
}

Usage Example

#![allow(unused)]
fn main() {
use wow_m2::{M2Model, AnimFile};

// Load base model
let format = M2Model::load("Character/Human/Male/HumanMale.m2")?;
let model = format.model();

// Load external animation file
let anim = AnimFile::load("Character/Human/Male/HumanMale0066-00.anim")?;
println!("Loaded animation with {} sections", anim.sections.len());

// Animation files are discovered by filename convention:
// <ModelName><AnimID>-<SubAnimID>.anim
// e.g., HumanMale0066-00.anim for Dance (AnimID 66)
}

Animation ID Mapping

Common Animation IDs

#![allow(unused)]
fn main() {
// Animation IDs that often have .anim files
const EMOTE_ANIMS: &[u16] = &[
    60,   // UseStanding
    61,   // Exclamation
    62,   // Question
    63,   // Bow
    64,   // Wave
    65,   // Cheer
    66,   // Dance
    67,   // Laugh
    68,   // Sleep
    69,   // SitGround
    // ... more emotes
];

const COMBAT_ANIMS: &[u16] = &[
    160,  // Attack1H
    161,  // Attack2H
    162,  // Attack2HL
    163,  // AttackUnarmed
    164,  // AttackBow
    165,  // AttackRifle
    166,  // AttackThrown
    // ... more combat
];
}

Advanced Features

Animation Sharing

#![allow(unused)]
fn main() {
// Share animations between similar models
struct AnimationCache {
    cache: HashMap<String, Arc<AnimationData>>,
}

impl AnimationCache {
    fn get_shared_animation(&self, model_type: &str, anim_id: u16) -> Option<Arc<AnimationData>> {
        // Try exact match first
        let exact_key = format!("{}_{}", model_type, anim_id);
        if let Some(anim) = self.cache.get(&exact_key) {
            return Some(anim.clone());
        }

        // Try generic version (e.g., all humans share some animations)
        let race = extract_race(model_type);
        let generic_key = format!("{}_{}", race, anim_id);
        self.cache.get(&generic_key).cloned()
    }
}
}

Lazy Loading

#![allow(unused)]
fn main() {
struct LazyAnimationLoader {
    model: M2Model,
    loaded_anims: HashMap<u16, AnimationData>,
    anim_paths: HashMap<u16, String>,
}

impl LazyAnimationLoader {
    fn play_animation(&mut self, anim_id: u16) -> Result<()> {
        // Load animation on first use
        if !self.loaded_anims.contains_key(&anim_id) {
            if let Some(path) = self.anim_paths.get(&anim_id) {
                let anim = load_anim_file(path)?;
                self.loaded_anims.insert(anim_id, anim);
            }
        }

        // Use loaded animation
        if let Some(anim) = self.loaded_anims.get(&anim_id) {
            self.model.apply_animation(anim);
        }

        Ok(())
    }
}
}

Animation Streaming

#![allow(unused)]
fn main() {
// Stream animations for large cutscenes
struct AnimationStreamer {
    current_segment: Option<AnimSegment>,
    next_segment: Option<AnimSegment>,
    preload_time: u32,  // Milliseconds before needed
}

impl AnimationStreamer {
    fn update(&mut self, current_time: u32) -> Result<()> {
        // Check if we need to preload next segment
        if let Some(current) = &self.current_segment {
            let time_until_end = current.end_time - current_time;

            if time_until_end <= self.preload_time && self.next_segment.is_none() {
                // Start async load of next segment
                self.start_preload_next_segment()?;
            }
        }

        // Switch to next segment if current is done
        if current_time >= self.current_segment.as_ref().unwrap().end_time {
            self.current_segment = self.next_segment.take();
        }

        Ok(())
    }
}
}

Common Patterns

Animation Discovery

#![allow(unused)]
fn main() {
fn discover_animations(model_path: &str) -> Vec<AnimationFile> {
    let model_name = Path::new(model_path)
        .file_stem()
        .unwrap()
        .to_str()
        .unwrap();

    let model_dir = Path::new(model_path).parent().unwrap();
    let mut animations = Vec::new();

    // Search for .anim files matching pattern
    for entry in fs::read_dir(model_dir)? {
        let path = entry?.path();
        if path.extension() == Some(OsStr::new("anim")) {
            if let Some(file_name) = path.file_name().and_then(|s| s.to_str()) {
                if file_name.starts_with(model_name) {
                    // Extract animation ID from filename
                    if let Some(anim_id) = parse_anim_id(&file_name) {
                        animations.push(AnimationFile {
                            path,
                            anim_id,
                        });
                    }
                }
            }
        }
    }

    animations
}
}

Memory Management

#![allow(unused)]
fn main() {
struct AnimationManager {
    memory_limit: usize,
    loaded_anims: LruCache<(String, u16), AnimationData>,
}

impl AnimationManager {
    fn load_animation(&mut self, model: &str, anim_id: u16) -> Result<&AnimationData> {
        let key = (model.to_string(), anim_id);

        if !self.loaded_anims.contains(&key) {
            let anim = load_anim_file(&format!("{}{:04}-00.anim", model, anim_id))?;

            // Check memory usage
            while self.get_memory_usage() + anim.size() > self.memory_limit {
                // Evict least recently used
                self.loaded_anims.pop_lru();
            }

            self.loaded_anims.put(key.clone(), anim);
        }

        Ok(self.loaded_anims.get(&key).unwrap())
    }
}
}

Performance Tips

  • Load animations asynchronously during loading screens
  • Cache frequently used animations (idle, walk, run)
  • Unload rarely used animations (emotes, special attacks)
  • Consider animation LOD for distant models

Common Issues

Missing Animations

  • Not all animations are externalized
  • Some remain embedded in M2 file
  • Check both internal and external sources

Version Compatibility

  • .anim format introduced in WotLK (3.0.2)
  • Earlier clients use embedded animations only
  • Format unchanged through 5.4.8

File Discovery

  • Animation files follow strict naming
  • Sub-animations use different SubAnimID
  • Some animations have multiple variations

References

See Also