Animation 3D (Skeletal Animation)
Mibo provides a three-tier 3D skeletal animation system in Mibo.Animation. It supports per-model CPU skinning, shared-mesh GPU skinning, and animation blending via UpdateModelAnimationEx.
Core Types
Type |
Purpose |
|---|---|
|
Shared clip set loaded from |
|
Per-entity playback state (current frame, blend, speed, loop) |
|
Shared mesh + inverse bind pose for GPU skinning |
Quick Start
open Mibo.Animation
// 1. Load model and animations (at init time)
let model = assets.Model "character.glb"
let anims = assets.ModelAnimations "character.glb"
let clips = Animation3DClips.fromModelAnimations anims
// 2. Create per-entity animation state
let anim = Animation3DState.create model clips "idle" 60.0f
// 3. Update each frame (in your animation system)
let anim = anim |> Animation3DState.update deltaTime
// 4. Apply and render (in your view)
Animation3DState.applyToModel anim
buffer |> Draw3D.drawModel anim.Model transform
Three API Tiers
Tier 1 — Data Extraction (Animation3DClips)
Load and query animation clips. No GPU, no model mutation.
let anims = assets.ModelAnimations "character.glb"
let clips = Animation3DClips.fromModelAnimations anims
let names = Animation3DClips.names clips // [|"idle"; "walk"; "jump"|]
let count = Animation3DClips.count clips // 3
let idx = Animation3DClips.tryGetClipIndex "walk" clips // ValueSome 1
Tier 2 — GPU Skinning (AnimatedMesh)
Share one mesh across many entities. Each entity computes its own bone matrices.
let mesh = AnimatedMesh.fromModel model // load once, share
// Per-entity (lightweight — just matrix math)
let bones = AnimatedMesh.computeBoneMatrices clip frame mesh
// Render — GPU does the skinning
buffer |> Draw3D.drawSkinnedMesh mesh.Mesh transform material bones
Tier 3 — Per-Model CPU Skinning (Animation3DState)
Simplest API. Each entity owns its own Model copy. Raylib's UpdateModelAnimation handles skinning on the CPU.
let anim = Animation3DState.create model clips "idle" 60.0f
let anim = anim |> Animation3DState.update dt
Animation3DState.applyToModel anim // mutates model's bone matrices
buffer |> Draw3D.drawModel anim.Model transform
When to Use Which
Scenario |
Tier |
Why |
|---|---|---|
1–5 animated characters |
Tier 3 |
Simple, no shader changes |
10+ animated enemies |
Tier 2 |
Share mesh, GPU skinning |
Hundreds of units (RTS) |
Tier 2 + instancing |
|
Animation3DClips API
Loading
let anims = assets.ModelAnimations "character.glb"
let clips = Animation3DClips.fromModelAnimations anims
The ModelAnimations asset method loads all skeletal animations from a glb/gltf/iqm file. Returns an empty array if the model has no animations.
Discovery
let names = Animation3DClips.names clips // [|"idle"; "walk"; "jump"|]
let count = Animation3DClips.count clips // 3
let empty = Animation3DClips.isEmpty clips // false
let idx = Animation3DClips.tryGetClipIndex "walk" clips // ValueSome 1
Animation3DState API
Creation
// Start on a named clip
let anim = Animation3DState.create model clips "idle" 60.0f
// Start on a clip index (zero string allocation)
let anim = Animation3DState.createByIndex model clips 0 60.0f
// Default to index 0 if name not found
let anim = Animation3DState.create model clips "nonexistent" 60.0f
The fps parameter controls playback speed. It is divided by 60 internally (raylib's default keyframe rate) to produce a speed multiplier.
Playback Control
// Switch animation (resets frame, cancels blend)
let anim = anim |> Animation3DState.play "walk"
// Switch by index (zero string allocation)
let anim = anim |> Animation3DState.playByIndex 1
// Switch only if not already playing
let anim = anim |> Animation3DState.playIfNot "walk"
// Restart current animation
let anim = anim |> Animation3DState.restart
Blending
Crossfade between two animations using UpdateModelAnimationEx:
// Blend from current to "walk" over 0.2 seconds
let anim = anim |> Animation3DState.blendTo "walk" 0.2f
// Or by index
let anim = anim |> Animation3DState.blendToByIndex 1 0.2f
// Check blend state
let blending = Animation3DState.isBlending anim // true during blend
blendTo is idempotent — calling it repeatedly with the same target does not restart the blend. When the blend completes, the target animation becomes the current animation.
Update
let anim = anim |> Animation3DState.update deltaTime
Advances the current frame (and blend target frame if blending). Respects Loop and Speed settings. Does not apply to the model — use applyToModel after.
Apply to Model
Animation3DState.applyToModel anim
Calls Raylib.UpdateModelAnimation (or UpdateModelAnimationEx when blending) which mutates the model's bone matrices. Must be called before rendering with DrawModel.
Query
let finished = Animation3DState.isFinished anim
let playing = Animation3DState.isPlaying "walk" anim
let name = Animation3DState.currentClipName anim
let dur = Animation3DState.duration anim
Configuration
let anim = anim |> Animation3DState.withSpeed 0.5f // half speed
let anim = anim |> Animation3DState.withLoop false // don't loop
GPU Skinning (AnimatedMesh)
For scenarios with many animated entities sharing the same mesh.
Loading
let mesh = AnimatedMesh.fromModel model
// Returns ValueNone if model has no bones
Computing Bone Matrices
let clip = clips.Clips[clipIndex]
let bones = AnimatedMesh.computeBoneMatrices clip frame mesh
// Returns Matrix4x4[] — pure math, no model mutation
The algorithm matches raylib's UpdateModelAnimation:
1. Interpolate keyframes (lerp for translation/scale, slerp for rotation)
2. Build TRS matrices for bind pose and current pose
3. Multiply: boneMatrices[i] = inverse(bindPose) * currentPose
Rendering
buffer |> Draw3D.drawSkinnedMesh mesh.Mesh transform material bones
The shader receives bone matrices as a boneMatrices[128] uniform and applies skinning on the GPU via vertexBoneIndices / vertexBoneWeights vertex attributes.
Integration with MVU
Animation state lives in your Elmish model. Update in a system, apply in the view:
// Types.fs
type GameModel() =
member val PlayerAnim = Unchecked.defaultof<Animation3DState> with get, set
// Systems.fs
let animationSystem dt model =
let targetAnim = if not model.IsGrounded then "jump" elif isMoving then "walk" else "idle"
model.PlayerAnim <- model.PlayerAnim |> Animation3DState.blendTo targetAnim 0.15f |> Animation3DState.update dt
struct (model, Cmd.none)
// View.fs
Animation3DState.applyToModel model.PlayerAnim
buffer |> Draw3D.drawModel model.PlayerAnim.Model transform
Model Format
Raylib supports glTF/GLB and IQM for skeletal animation. GLB is recommended — it bundles geometry, textures, and animation data in a single file.
Animations are loaded from the model file via assets.ModelAnimations. The animation names come from the file's embedded animation names (e.g., "idle", "walk", "jump" in a Kenney character model).
Performance Tips
- Resolve clip names once at init: Use
tryGetClipIndex+playByIndexto avoid string lookups in the hot path - Share Animation3DClips: Create clips once, reuse across all entities using the same model
- Tier 2 for many entities: Use
AnimatedMesh+computeBoneMatrices+DrawSkinnedMeshto avoid per-entity model copies - Blend duration: Keep blend durations short (0.1–0.3s) to minimize double-animation overhead
See Also
type GameModel = new: unit -> GameModel member PlayerAnim: obj with get, set
--------------------
new: unit -> GameModel
Mibo.Raylib