# AnyMotion
> A skeletal animation library that _might_ work for your game engine
[](https://crates.io/crates/anymotion)
[](https://docs.rs/anymotion)
## What This Is (Probably)
AnyMotion is a prototype skeletal animation library for Rust game engines. It follows Unix philosophy (does one thing, hopefully well) and provides the animation pipeline components you'd need to integrate into a larger engine.
**Fair Warning**: This is a library component, not a complete solution. You'll need to bring your own rendering, materials, lighting, and scene management. Think of it as a transmission for your car - it works, but you can't drive it alone.
## Installation
```toml
[dependencies]
anymotion = "0.1.0"
archetype_ecs = "1.1.7" # Required for ECS integration
glam = "0.30" # Math types
```
## Core Concepts
### What It Does
- ✅ **Animation Sampling**: Interpolates keyframe data at any time point
- ✅ **Transform Hierarchy**: Propagates local transforms to global space
- ✅ **Skinning Matrices**: Computes GPU-ready joint matrices
- ✅ **ECS Integration**: Works with `archetype_ecs` (should work with others too)
- ✅ **GLTF Loading**: Reads skeletons and animations from `.glb` files
### What It Doesn't Do
- ❌ Rendering (use `ash_renderer`, `wgpu`, or your own)
- ❌ Materials/Lighting (that's your engine's job)
- ❌ Asset management (just provides loaders)
- ❌ Physics/IK (maybe someday?)
## Quick Start
### 1. Basic Animation Sampling (No ECS)
```rust
use anymotion::prelude::*;
fn main() -> Result<()> {
// Create test data (or load from GLTF)
let skeleton = create_test_skeleton();
let clip = create_test_walk_clip()?;
// Sample animation at specific time
let time = 0.5; // seconds
if let Some((pos, rot, scale)) = clip.sample_bone("Hips", time) {
println!("Hips at t={time}s: pos={pos:?}");
}
Ok(())
}
```
### 2. ECS Integration (The Real Deal)
```rust
use anymotion::prelude::*;
use archetype_ecs::{World, GlobalTransform, LocalTransform};
fn setup_animation(world: &mut World) -> Result<()> {
// 1. Load skeleton and animation
let skeleton = create_test_skeleton();
let clip = create_test_walk_clip()?;
// 2. Store in asset resources
let skeleton_handle = world
.resource_mut::<SkeletonAssets>()?
.add(skeleton.clone());
let clip_handle = world
.resource_mut::<AnimationClipAssets>()?
.add(clip);
// 3. Spawn bone entities
let mut bone_entities = Vec::new();
for (i, _bone) in skeleton.bones.iter().enumerate() {
let entity = world.spawn((
BoneJoint {
bone_index: i,
skeleton: skeleton_handle,
},
LocalTransform::default(),
GlobalTransform::identity(),
));
bone_entities.push(entity);
}
// 4. Set up parent relationships (CRITICAL!)
// Example: Root (0) -> Hips (1) -> Spine (2)
let _ = world.add_component(
bone_entities[1],
Parent { entity: bone_entities[0] }
);
let _ = world.add_component(
bone_entities[2],
Parent { entity: bone_entities[1] }
);
// 5. Spawn animated character
world.spawn((
Animator::new(skeleton_handle, clip_handle),
SkinnedMesh {
skeleton: skeleton_handle,
mesh_handle: 0, // Your mesh ID
bone_entities: bone_entities.clone(),
joint_offset: 0,
},
GlobalTransform::identity(),
JointPalette::new(skeleton.bones.len()),
));
Ok(())
}
fn update_loop(world: &mut World, dt: f32) {
// This runs the entire animation pipeline:
// 1. Sample animations -> LocalTransform
// 2. Propagate hierarchy -> GlobalTransform
// 3. Compute skinning matrices -> JointPalette
// 4. Upload to GPU (if renderer is set up)
AnimationPipeline::update(world, dt);
}
```
### 3. Loading from GLTF
```rust
use anymotion::loader::load_gltf;
fn load_character() -> Result<()> {
let (skeleton, clips) = load_gltf("assets/character.glb")?;
println!("Loaded skeleton with {} bones", skeleton.bones.len());
println!("Loaded {} animation clips", clips.len());
for clip in &clips {
println!(" - {}: {:.2}s", clip.name, clip.duration);
}
Ok(())
}
```
## API Reference
### Core Types
#### `Skeleton`
Represents a bone hierarchy with inverse bind matrices.
```rust
pub struct Skeleton {
pub bones: Vec<Bone>,
}
impl Skeleton {
pub fn new() -> Self;
pub fn add_bone(&mut self, name: String, parent: Option<usize>, inverse_bind: Mat4);
pub fn validate(&self) -> Result<()>;
pub fn find_bone(&self, name: &str) -> Option<usize>;
}
```
#### `AnimationClip`
Contains keyframe data for multiple bones.
```rust
pub struct AnimationClip {
pub name: String,
pub duration: f32,
// ... (internal)
}
impl AnimationClip {
pub fn sample_bone(&self, bone_name: &str, time: f32) -> Option<(Vec3, Quat, Vec3)>;
pub fn sample_all(&self, time: f32) -> HashMap<String, (Vec3, Quat, Vec3)>;
}
```
#### `Animator` (Component)
Drives animation playback.
```rust
pub struct Animator {
pub skeleton: SkeletonHandle,
pub current_clip: AnimationClipHandle,
pub player: AnimationPlayer,
}
// AnimationPlayer controls playback
pub struct AnimationPlayer {
pub time: f32,
pub speed: f32, // Default: 1.0
pub is_playing: bool, // Default: true
pub is_looping: bool, // Default: true
}
```
#### `BoneJoint` (Component)
Marks an entity as a bone in a skeleton.
```rust
pub struct BoneJoint {
pub bone_index: usize,
pub skeleton: SkeletonHandle,
}
```
#### `Parent` (Component)
Defines parent-child relationships for transform hierarchy.
```rust
pub struct Parent {
pub entity: EntityId,
}
```
#### `SkinnedMesh` (Component)
Links a mesh to a skeleton for GPU skinning.
```rust
pub struct SkinnedMesh {
pub skeleton: SkeletonHandle,
pub mesh_handle: u32,
pub bone_entities: Vec<EntityId>,
pub joint_offset: u32,
}
```
#### `JointPalette` (Component)
Stores computed skinning matrices for GPU upload.
```rust
pub struct JointPalette {
pub matrices: Vec<Mat4>,
}
impl JointPalette {
pub fn new(count: usize) -> Self;
}
```
### Systems
#### `AnimationPipeline::update(world, dt)`
**The main entry point.** Runs all animation systems in the correct order:
1. `animation_sampling_system` - Samples animations → `LocalTransform`
2. `animation_blending_system` - Blends animations (if needed)
3. `transform_hierarchy_system` - Propagates `LocalTransform` → `GlobalTransform`
4. `skinning_palette_system` - Computes skinning matrices → `JointPalette`
5. `JointUploadSystem` - Uploads to GPU (if renderer available)
```rust
// In your game loop
fn update(&mut self, dt: f32) {
AnimationPipeline::update(&mut self.world, dt);
}
```
You can also run systems individually if needed:
```rust
use anymotion::{
animation_sampling_system,
animation_blending_system,
skinning_palette_system,
};
animation_sampling_system(&mut world, dt);
transform_hierarchy_system(&mut world);
skinning_palette_system(&mut world);
```
### Loaders
#### `create_test_skeleton()` / `create_test_walk_clip()`
Helper functions for testing/prototyping.
```rust
let skeleton = create_test_skeleton();
// Creates: Root -> Hips -> Spine, LeftLeg, RightLeg
let clip = create_test_walk_clip()?;
// Animates the "Hips" bone
```
#### `load_gltf(path)`
Loads skeleton and animations from GLTF/GLB files.
```rust
let (skeleton, clips) = load_gltf("character.glb")?;
```
**Known Limitations**:
- Only reads first skin in file
- Assumes linear interpolation
- No support for morph targets yet
## Architecture
### Transform Hierarchy
The library uses a standard parent-child transform hierarchy:
```
LocalTransform (per-bone) → Parent links → GlobalTransform (computed)
```
**CRITICAL**: You must set up `Parent` components correctly, or transforms won't propagate!
```rust
// Example: 3-bone chain
let root = world.spawn((LocalTransform::default(), GlobalTransform::identity()));
let child1 = world.spawn((LocalTransform::default(), GlobalTransform::identity()));
let child2 = world.spawn((LocalTransform::default(), GlobalTransform::identity()));
world.add_component(child1, Parent { entity: root });
world.add_component(child2, Parent { entity: child1 });
// Now: root -> child1 -> child2
```
### Skinning Pipeline
```
Skeleton (bind pose) + GlobalTransform (animated) → Skinning Matrices → GPU
```
The skinning matrix for bone `i` is:
```
JointMatrix[i] = GlobalTransform[i] * InverseBindMatrix[i]
```
This is computed by `skinning_palette_system` and stored in `JointPalette`.
### Data Flow Diagram
```
┌─────────────────┐
│ AnimationClip │
│ (keyframes) │
└────────┬────────┘
│ sample(time)
↓
┌─────────────────┐
│ LocalTransform │ ← animation_sampling_system
│ (per bone) │
└────────┬────────┘
│ + Parent links
↓
┌─────────────────┐
│ GlobalTransform │ ← transform_hierarchy_system
│ (world space) │
└────────┬────────┘
│ + InverseBindMatrix
↓
┌─────────────────┐
│ JointPalette │ ← skinning_palette_system
│ (GPU matrices) │
└────────┬────────┘
│
↓
GPU Skinning
```
## Integration Patterns
### With `ash_renderer`
```rust
use anymotion::prelude::*;
use ash_renderer::prelude::*;
use std::sync::{Arc, Mutex};
// 1. Store renderer in ECS world
world.insert_resource(Arc::new(Mutex::new(renderer)));
// 2. Run animation pipeline
AnimationPipeline::update(&mut world, dt);
// 3. Renderer automatically uploads joint matrices
// (JointUploadSystem handles this)
// 4. Draw skinned mesh
if let Ok(mut renderer) = renderer_arc.lock() {
renderer.draw_skinned_mesh(
mesh_handle,
material_handle,
transform_matrix,
joint_offset,
);
}
```
### With Custom ECS
If you're not using `archetype_ecs`, you'll need to:
1. Implement equivalent components (`LocalTransform`, `GlobalTransform`, etc.)
2. Run the systems manually in the correct order
3. Handle resource storage yourself
The core math (`AnimationClip::sample`, etc.) is ECS-agnostic.
## Performance Notes
**What We've Tested**:
- ✅ Zero allocations in `AnimationPipeline::update` hot loop
- ✅ Sparse updates (only animated bones are modified)
- ✅ Single-pass hierarchy propagation
**What We Haven't Tested**:
- Large skeletons (100+ bones)
- Many animated characters (100+ entities)
- Complex blend trees
**Optimization Tips**:
- Use `AnimationPipeline::update` instead of individual systems (better cache locality)
- Keep bone hierarchies shallow when possible
- Batch character updates if you have many
## Known Issues & Limitations
### Current Bugs
- None known (as of v0.1.0)
### Missing Features
- ❌ Animation blending (component exists, system is stubbed)
- ❌ Inverse Kinematics (IK)
- ❌ Root motion extraction
- ❌ Animation events/callbacks
- ❌ Additive animations
- ❌ Animation compression
### Design Limitations
- Requires `archetype_ecs` (or manual system integration)
- Assumes you have a renderer that supports GPU skinning
- GLTF loader is basic (no morph targets, no sparse accessors)
## Testing
```bash
# Run all tests
cargo test
# Run with output
cargo test -- --nocapture
# Run specific test
cargo test test_animation_pipeline_propagation
# Run examples
cargo run --example 01_basic_animation
cargo run --example 02_skinned_mesh # (white screen is normal - see below)
```
### About the White Screen Example
The `02_skinned_mesh` example shows a white screen. **This is expected!**
The example proves the animation pipeline works (verified by passing tests), but doesn't set up materials, lighting, or a proper scene. It's a minimal integration demo, not a visual showcase.
To actually see animated characters, you'd need to integrate this library into a game engine with:
- Material system
- Lighting system
- Camera controller
- Scene management
## Troubleshooting
### "My bones aren't animating!"
Check:
1. Did you set up `Parent` components for the bone hierarchy?
2. Are you calling `AnimationPipeline::update` every frame?
3. Is the `Animator` component's `player.is_playing` set to `true`?
4. Does your animation clip actually have tracks for those bones?
### "Transforms are wrong!"
Check:
1. Are you using `LocalTransform` from `archetype_ecs`?
2. Did you spawn entities with both `LocalTransform` AND `GlobalTransform`?
3. Is `transform_hierarchy_system` running after `animation_sampling_system`?
### "GPU skinning doesn't work!"
Check:
1. Did you set up `SkinnedMesh` component with correct `bone_entities`?
2. Is `JointPalette` component present?
3. Is your renderer actually using the joint matrices?
## Contributing
Contributions are welcome, though I make no promises about merge speed or quality standards.
Please ensure:
- `cargo test` passes
- `cargo clippy` is clean
- Code is reasonably documented
## License
Licensed under the Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE)).
## Acknowledgments
This library wouldn't exist without:
- Unity DOTS (architecture inspiration)
- Unreal Engine (skinning pipeline reference)
- `archetype_ecs` (ECS foundation)
- `ash_renderer` (rendering integration)
- The Rust gamedev community
## Version History
### v0.1.0 (2025-12-29)
**Initial Release** - The "It Compiles" Edition
- ✅ Core animation sampling
- ✅ Transform hierarchy propagation
- ✅ Skinning matrix calculation
- ✅ GLTF loading (basic)
- ✅ ECS integration with `archetype_ecs`
- ✅ Animation pipeline orchestration
- ✅ 37 passing tests
- ✅ Zero clippy warnings
**Known Issues**:
- Animation blending system is stubbed
- No visual examples (white screen is expected)
- GLTF loader is minimal
---
**Questions? Issues?** Open an issue on GitHub. I'll try to respond, but no guarantees.
**Want to help?** PRs welcome. The codebase is reasonably clean (I think).
**Using this in production?** You're braver than I am. Let me know how it goes!