Skip to content

Commit ecde548

Browse files
committed
docs: add Animation3D documentation
1 parent fe79de1 commit ecde548

1 file changed

Lines changed: 260 additions & 0 deletions

File tree

docs/animation3d.md

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
---
2+
title: Animation 3D
3+
category: Amenities
4+
categoryindex: 5
5+
index: 23
6+
---
7+
8+
# Animation 3D (Skeletal Animation)
9+
10+
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`.
11+
12+
## Core Types
13+
14+
| Type | Purpose |
15+
| ---- | ------- |
16+
| `Animation3DClips` | Shared clip set loaded from `ModelAnimation[]` — name/index lookup |
17+
| `Animation3DState` | Per-entity playback state (current frame, blend, speed, loop) |
18+
| `AnimatedMesh` | Shared mesh + inverse bind pose for GPU skinning |
19+
20+
## Quick Start
21+
22+
```fsharp
23+
open Mibo.Animation
24+
25+
// 1. Load model and animations (at init time)
26+
let model = assets.Model "character.glb"
27+
let anims = assets.ModelAnimations "character.glb"
28+
let clips = Animation3DClips.fromModelAnimations anims
29+
30+
// 2. Create per-entity animation state
31+
let anim = Animation3DState.create model clips "idle" 60.0f
32+
33+
// 3. Update each frame (in your animation system)
34+
let anim = anim |> Animation3DState.update deltaTime
35+
36+
// 4. Apply and render (in your view)
37+
Animation3DState.applyToModel anim
38+
buffer |> Draw3D.drawModel anim.Model transform
39+
```
40+
41+
## Three API Tiers
42+
43+
### Tier 1 — Data Extraction (`Animation3DClips`)
44+
45+
Load and query animation clips. No GPU, no model mutation.
46+
47+
```fsharp
48+
let anims = assets.ModelAnimations "character.glb"
49+
let clips = Animation3DClips.fromModelAnimations anims
50+
51+
let names = Animation3DClips.names clips // [|"idle"; "walk"; "jump"|]
52+
let count = Animation3DClips.count clips // 3
53+
let idx = Animation3DClips.tryGetClipIndex "walk" clips // ValueSome 1
54+
```
55+
56+
### Tier 2 — GPU Skinning (`AnimatedMesh`)
57+
58+
Share one mesh across many entities. Each entity computes its own bone matrices.
59+
60+
```fsharp
61+
let mesh = AnimatedMesh.fromModel model // load once, share
62+
63+
// Per-entity (lightweight — just matrix math)
64+
let bones = AnimatedMesh.computeBoneMatrices clip frame mesh
65+
66+
// Render — GPU does the skinning
67+
buffer |> Draw3D.drawSkinnedMesh mesh.Mesh transform material bones
68+
```
69+
70+
### Tier 3 — Per-Model CPU Skinning (`Animation3DState`)
71+
72+
Simplest API. Each entity owns its own `Model` copy. Raylib's `UpdateModelAnimation` handles skinning on the CPU.
73+
74+
```fsharp
75+
let anim = Animation3DState.create model clips "idle" 60.0f
76+
let anim = anim |> Animation3DState.update dt
77+
Animation3DState.applyToModel anim // mutates model's bone matrices
78+
buffer |> Draw3D.drawModel anim.Model transform
79+
```
80+
81+
### When to Use Which
82+
83+
| Scenario | Tier | Why |
84+
|----------|------|-----|
85+
| 1–5 animated characters | Tier 3 | Simple, no shader changes |
86+
| 10+ animated enemies | Tier 2 | Share mesh, GPU skinning |
87+
| Hundreds of units (RTS) | Tier 2 + instancing | `DrawMeshInstanced` with skinning |
88+
89+
## Animation3DClips API
90+
91+
### Loading
92+
93+
```fsharp
94+
let anims = assets.ModelAnimations "character.glb"
95+
let clips = Animation3DClips.fromModelAnimations anims
96+
```
97+
98+
The `ModelAnimations` asset method loads all skeletal animations from a glb/gltf/iqm file. Returns an empty array if the model has no animations.
99+
100+
### Discovery
101+
102+
```fsharp
103+
let names = Animation3DClips.names clips // [|"idle"; "walk"; "jump"|]
104+
let count = Animation3DClips.count clips // 3
105+
let empty = Animation3DClips.isEmpty clips // false
106+
let idx = Animation3DClips.tryGetClipIndex "walk" clips // ValueSome 1
107+
```
108+
109+
## Animation3DState API
110+
111+
### Creation
112+
113+
```fsharp
114+
// Start on a named clip
115+
let anim = Animation3DState.create model clips "idle" 60.0f
116+
117+
// Start on a clip index (zero string allocation)
118+
let anim = Animation3DState.createByIndex model clips 0 60.0f
119+
120+
// Default to index 0 if name not found
121+
let anim = Animation3DState.create model clips "nonexistent" 60.0f
122+
```
123+
124+
The `fps` parameter controls playback speed. It is divided by 60 internally (raylib's default keyframe rate) to produce a speed multiplier.
125+
126+
### Playback Control
127+
128+
```fsharp
129+
// Switch animation (resets frame, cancels blend)
130+
let anim = anim |> Animation3DState.play "walk"
131+
132+
// Switch by index (zero string allocation)
133+
let anim = anim |> Animation3DState.playByIndex 1
134+
135+
// Switch only if not already playing
136+
let anim = anim |> Animation3DState.playIfNot "walk"
137+
138+
// Restart current animation
139+
let anim = anim |> Animation3DState.restart
140+
```
141+
142+
### Blending
143+
144+
Crossfade between two animations using `UpdateModelAnimationEx`:
145+
146+
```fsharp
147+
// Blend from current to "walk" over 0.2 seconds
148+
let anim = anim |> Animation3DState.blendTo "walk" 0.2f
149+
150+
// Or by index
151+
let anim = anim |> Animation3DState.blendToByIndex 1 0.2f
152+
153+
// Check blend state
154+
let blending = Animation3DState.isBlending anim // true during blend
155+
```
156+
157+
`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.
158+
159+
### Update
160+
161+
```fsharp
162+
let anim = anim |> Animation3DState.update deltaTime
163+
```
164+
165+
Advances the current frame (and blend target frame if blending). Respects `Loop` and `Speed` settings. Does not apply to the model — use `applyToModel` after.
166+
167+
### Apply to Model
168+
169+
```fsharp
170+
Animation3DState.applyToModel anim
171+
```
172+
173+
Calls `Raylib.UpdateModelAnimation` (or `UpdateModelAnimationEx` when blending) which mutates the model's bone matrices. Must be called before rendering with `DrawModel`.
174+
175+
### Query
176+
177+
```fsharp
178+
let finished = Animation3DState.isFinished anim
179+
let playing = Animation3DState.isPlaying "walk" anim
180+
let name = Animation3DState.currentClipName anim
181+
let dur = Animation3DState.duration anim
182+
```
183+
184+
### Configuration
185+
186+
```fsharp
187+
let anim = anim |> Animation3DState.withSpeed 0.5f // half speed
188+
let anim = anim |> Animation3DState.withLoop false // don't loop
189+
```
190+
191+
## GPU Skinning (AnimatedMesh)
192+
193+
For scenarios with many animated entities sharing the same mesh.
194+
195+
### Loading
196+
197+
```fsharp
198+
let mesh = AnimatedMesh.fromModel model
199+
// Returns ValueNone if model has no bones
200+
```
201+
202+
### Computing Bone Matrices
203+
204+
```fsharp
205+
let clip = clips.Clips[clipIndex]
206+
let bones = AnimatedMesh.computeBoneMatrices clip frame mesh
207+
// Returns Matrix4x4[] — pure math, no model mutation
208+
```
209+
210+
The algorithm matches raylib's `UpdateModelAnimation`:
211+
1. Interpolate keyframes (lerp for translation/scale, slerp for rotation)
212+
2. Build TRS matrices for bind pose and current pose
213+
3. Multiply: `boneMatrices[i] = inverse(bindPose) * currentPose`
214+
215+
### Rendering
216+
217+
```fsharp
218+
buffer |> Draw3D.drawSkinnedMesh mesh.Mesh transform material bones
219+
```
220+
221+
The shader receives bone matrices as a `boneMatrices[128]` uniform and applies skinning on the GPU via `vertexBoneIndices` / `vertexBoneWeights` vertex attributes.
222+
223+
## Integration with MVU
224+
225+
Animation state lives in your Elmish model. Update in a system, apply in the view:
226+
227+
```fsharp
228+
// Types.fs
229+
type GameModel() =
230+
member val PlayerAnim = Unchecked.defaultof<Animation3DState> with get, set
231+
232+
// Systems.fs
233+
let animationSystem dt model =
234+
let targetAnim = if not model.IsGrounded then "jump" elif isMoving then "walk" else "idle"
235+
model.PlayerAnim <- model.PlayerAnim |> Animation3DState.blendTo targetAnim 0.15f |> Animation3DState.update dt
236+
struct (model, Cmd.none)
237+
238+
// View.fs
239+
Animation3DState.applyToModel model.PlayerAnim
240+
buffer |> Draw3D.drawModel model.PlayerAnim.Model transform
241+
```
242+
243+
## Model Format
244+
245+
Raylib supports **glTF/GLB** and **IQM** for skeletal animation. GLB is recommended — it bundles geometry, textures, and animation data in a single file.
246+
247+
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).
248+
249+
## Performance Tips
250+
251+
1. **Resolve clip names once at init**: Use `tryGetClipIndex` + `playByIndex` to avoid string lookups in the hot path
252+
2. **Share Animation3DClips**: Create clips once, reuse across all entities using the same model
253+
3. **Tier 2 for many entities**: Use `AnimatedMesh` + `computeBoneMatrices` + `DrawSkinnedMesh` to avoid per-entity model copies
254+
4. **Blend duration**: Keep blend durations short (0.1–0.3s) to minimize double-animation overhead
255+
256+
## See Also
257+
258+
- [Animation (2D)](animation.html)
259+
- [Rendering 3D](graphics3d/overview.html)
260+
- [Materials](graphics3d/materials.html)

0 commit comments

Comments
 (0)