🎬 Animation System Guide
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-rsinstalled 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
#![allow(unused)]
fn main() {
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
#![allow(unused)]
fn main() {
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
#![allow(unused)]
fn main() {
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
#![allow(unused)]
fn main() {
#[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
#![allow(unused)]
fn main() {
#[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(¤t_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
#![allow(unused)]
fn main() {
#[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
#![allow(unused)]
fn main() {
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
#![allow(unused)]
fn main() {
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
#![allow(unused)]
fn main() {
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
#![allow(unused)]
fn main() {
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
#![allow(unused)]
fn main() {
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:
#![allow(unused)]
fn main() {
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:
#![allow(unused)]
fn main() {
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:
#![allow(unused)]
fn main() {
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
#![allow(unused)]
fn main() {
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
#![allow(unused)]
fn main() {
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
}
}
}
Related Guides
- 🎭 Loading M2 Models - Load models with animation data
- 🎨 Model Rendering Guide - Render animated models
- 📊 LOD System Guide - Animation LOD implementation
References
- Skeletal Animation - Understanding skeletal animation
- Animation Compression - GDC talk on animation compression
- FABRIK Algorithm - IK solving algorithm
- Animation State Machines - Unity’s approach to animation state machines