Skip to main content

myth_animation/
mixer.rs

1use rustc_hash::{FxHashMap, FxHashSet};
2use slotmap::{SlotMap, new_key_type};
3
4use crate::action::AnimationAction;
5use crate::binding::{Rig, TargetPath};
6use crate::blending::{BlendEntry, FrameBlendState};
7use crate::clip::TrackData;
8use crate::events::{self, FiredEvent};
9use crate::target::AnimationTarget;
10use myth_core::NodeHandle;
11
12new_key_type! {
13    pub struct ActionHandle;
14}
15
16/// Manages playback and blending of multiple animation actions.
17///
18/// The mixer drives time advancement for all active actions, accumulates
19/// sampled animation data into per-node blend buffers, and applies the
20/// final blended result to scene nodes once per frame.
21///
22/// # Rest Pose & State Restoration
23///
24/// The mixer tracks which nodes were animated in the previous frame. When
25/// a node loses all animation influence (e.g. an action is stopped), it is
26/// automatically restored to its rest pose.
27///
28/// # Blending
29///
30/// When multiple actions are active simultaneously, their contributions
31/// are combined using weight-based accumulation.
32/// If the total accumulated weight for a property is less than 1.0, the
33/// rest pose value fills the remainder.
34///
35/// # Events
36///
37/// Animation events fired during the frame are collected and can be
38/// consumed via [`drain_events`](Self::drain_events).
39pub struct AnimationMixer {
40    actions: SlotMap<ActionHandle, AnimationAction>,
41    name_map: FxHashMap<String, ActionHandle>,
42    active_handles: Vec<ActionHandle>,
43
44    /// Logical skeleton for this entity, providing O(1) bone-index → node-handle lookup.
45    rig: Rig,
46
47    /// Global mixer time in seconds.
48    pub time: f32,
49    /// Global time scale multiplier applied to all actions.
50    pub time_scale: f32,
51
52    /// Per-frame blend accumulator (reused across frames to avoid allocation).
53    blend_state: FrameBlendState,
54    /// Events fired during the most recent update.
55    fired_events: Vec<FiredEvent>,
56    /// Node handles that were animated in the previous frame.
57    /// Used for rest-pose restoration when animation influence is lost.
58    animated_last_frame: FxHashSet<NodeHandle>,
59
60    // Temporary buffer for blended morph weights during application phase.
61    morph_buffer: crate::values::MorphWeightData,
62
63    pub enabled: bool,
64}
65
66impl Default for AnimationMixer {
67    fn default() -> Self {
68        Self::new()
69    }
70}
71
72impl AnimationMixer {
73    #[must_use]
74    pub fn new() -> Self {
75        Self {
76            actions: SlotMap::with_key(),
77            name_map: FxHashMap::default(),
78            active_handles: Vec::new(),
79            rig: Rig {
80                bones: Vec::new(),
81                bone_paths: Vec::new(),
82            },
83            time: 0.0,
84            time_scale: 1.0,
85            blend_state: FrameBlendState::new(),
86            fired_events: Vec::new(),
87            animated_last_frame: FxHashSet::default(),
88            morph_buffer: crate::values::MorphWeightData::default(),
89            enabled: true,
90        }
91    }
92
93    /// Sets the logical skeleton used for bone-index → node-handle lookup.
94    pub fn set_rig(&mut self, rig: Rig) {
95        self.rig = rig;
96    }
97
98    /// Returns a read-only reference to the mixer's rig.
99    #[must_use]
100    pub fn rig(&self) -> &Rig {
101        &self.rig
102    }
103
104    /// Returns a list of all registered animation clip names.
105    #[must_use]
106    pub fn list_animations(&self) -> Vec<String> {
107        self.name_map.keys().cloned().collect()
108    }
109
110    /// Registers an action and returns its handle.
111    pub fn add_action(&mut self, action: AnimationAction) -> ActionHandle {
112        let name = action.clip().name.clone();
113        let handle = self.actions.insert(action);
114        self.name_map.insert(name, handle);
115        handle
116    }
117
118    /// Read-only access to an action by clip name.
119    #[must_use]
120    pub fn get_action(&self, name: &str) -> Option<&AnimationAction> {
121        let handle = *self.name_map.get(name)?;
122        self.actions.get(handle)
123    }
124
125    /// Read-only access to an action by handle.
126    #[must_use]
127    pub fn get_action_by_handle(&self, handle: ActionHandle) -> Option<&AnimationAction> {
128        self.actions.get(handle)
129    }
130
131    /// Returns a chainable control wrapper for the named action.
132    pub fn action(&mut self, name: &str) -> Option<ActionControl<'_>> {
133        let handle = *self.name_map.get(name)?;
134        Some(ActionControl {
135            mixer: self,
136            handle,
137        })
138    }
139
140    /// Returns a control wrapper for the first registered action.
141    pub fn any_action(&mut self) -> Option<ActionControl<'_>> {
142        if let Some((handle, _)) = self.actions.iter().next() {
143            Some(ActionControl {
144                mixer: self,
145                handle,
146            })
147        } else {
148            None
149        }
150    }
151
152    /// Returns a control wrapper for an existing handle.
153    pub fn get_control(&mut self, handle: ActionHandle) -> Option<ActionControl<'_>> {
154        if self.actions.contains_key(handle) {
155            Some(ActionControl {
156                mixer: self,
157                handle,
158            })
159        } else {
160            None
161        }
162    }
163
164    pub fn get_control_by_name(&mut self, name: &str) -> Option<ActionControl<'_>> {
165        let handle = *self.name_map.get(name)?;
166        self.get_control(handle)
167    }
168
169    /// Plays the named animation, adding it to the active set.
170    pub fn play(&mut self, name: &str) {
171        if let Some(&handle) = self.name_map.get(name) {
172            if !self.active_handles.contains(&handle) {
173                self.active_handles.push(handle);
174            }
175            if let Some(action) = self.actions.get_mut(handle) {
176                action.enabled = true;
177                action.weight = 1.0;
178                action.paused = false;
179            }
180        } else {
181            log::warn!("Animation not found: {name}");
182        }
183    }
184
185    /// Stops the named animation and removes it from the active set.
186    pub fn stop(&mut self, name: &str) {
187        if let Some(&handle) = self.name_map.get(name) {
188            if let Some(action) = self.actions.get_mut(handle) {
189                action.stop();
190            }
191            self.active_handles.retain(|&h| h != handle);
192        }
193    }
194
195    /// Stops all active animations.
196    pub fn stop_all(&mut self) {
197        for handle in &self.active_handles {
198            if let Some(action) = self.actions.get_mut(*handle) {
199                action.stop();
200            }
201        }
202        self.active_handles.clear();
203    }
204
205    /// Drains all events fired during the most recent update.
206    pub fn drain_events(&mut self) -> Vec<FiredEvent> {
207        std::mem::take(&mut self.fired_events)
208    }
209
210    /// Returns a read-only slice of events fired during the most recent update.
211    #[must_use]
212    pub fn events(&self) -> &[FiredEvent] {
213        &self.fired_events
214    }
215
216    /// Advances all active actions and applies blended results to the target.
217    ///
218    /// This is the core per-frame entry point. The update proceeds in four phases:
219    ///
220    /// 1. **Time advancement**: Each active action's time is advanced. Animation
221    ///    events that fall within the `[t_prev, t_curr]` window are collected.
222    /// 2. **Sampling & accumulation**: Active actions sample their tracks and
223    ///    accumulate weighted results into the blend buffer. Track-to-node
224    ///    mapping uses [`crate::binding::ClipBinding`] + [`Rig`] for O(1) lookup.
225    /// 3. **Application**: Blended values are mixed with the rest pose and
226    ///    written to scene nodes.
227    /// 4. **Restoration**: Nodes that were animated last frame but received no
228    ///    contributions this frame are reset to their rest pose.
229    pub fn update(&mut self, dt: f32, target: &mut dyn AnimationTarget) {
230        if !self.enabled {
231            return;
232        }
233
234        // phase 0: Restore all nodes that were animated in the previous frame to their rest pose.
235        for &prev_handle in &self.animated_last_frame {
236            if let Some(rest) = target.rest_transform(prev_handle) {
237                target.set_node_position(prev_handle, rest.position);
238                target.set_node_rotation(prev_handle, rest.rotation);
239                target.set_node_scale(prev_handle, rest.scale);
240                target.mark_node_dirty(prev_handle);
241            }
242        }
243
244        let dt = dt * self.time_scale;
245        self.time += dt;
246
247        // Clear per-frame state
248        self.blend_state.clear();
249        self.fired_events.clear();
250
251        self.animated_last_frame.clear();
252
253        // Phase 1: Advance time and collect events
254        for &handle in &self.active_handles {
255            if let Some(action) = self.actions.get_mut(handle) {
256                let t_prev = action.time;
257                action.update(dt);
258                let t_curr = action.time;
259
260                let clip = action.clip();
261                events::collect_events(
262                    &clip.events,
263                    t_prev,
264                    t_curr,
265                    clip.duration,
266                    &clip.name,
267                    &mut self.fired_events,
268                );
269            }
270        }
271
272        // Phase 2: Sample tracks and accumulate into blend buffer (O(1) per track)
273        for &handle in &self.active_handles {
274            let action = match self.actions.get_mut(handle) {
275                Some(a) if a.enabled && !a.paused && a.weight > 0.0 => a,
276                _ => continue,
277            };
278
279            let clip = action.clip().clone();
280            let weight = action.weight;
281            let time = action.time;
282            let cursors = &mut action.track_cursors;
283
284            for tb in &action.clip_binding.bindings {
285                let track = &clip.tracks[tb.track_index];
286                let cursor = &mut cursors[tb.track_index];
287                let node_handle = self.rig.bones[tb.bone_index];
288
289                match (&track.data, tb.target) {
290                    (TrackData::Vector3(t), TargetPath::Translation) => {
291                        let val = t.sample_with_cursor(time, cursor);
292                        self.blend_state
293                            .accumulate_translation(node_handle, val, weight);
294                    }
295                    (TrackData::Vector3(t), TargetPath::Scale) => {
296                        let val = t.sample_with_cursor(time, cursor);
297                        self.blend_state.accumulate_scale(node_handle, val, weight);
298                    }
299                    (TrackData::Quaternion(t), TargetPath::Rotation) => {
300                        let val = t.sample_with_cursor(time, cursor);
301                        self.blend_state
302                            .accumulate_rotation(node_handle, val, weight);
303                    }
304                    (TrackData::MorphWeights(t), TargetPath::Weights) => {
305                        t.sample_with_cursor_into(time, cursor, &mut self.morph_buffer);
306                        self.blend_state.accumulate_morph_weights(
307                            node_handle,
308                            &self.morph_buffer,
309                            weight,
310                        );
311                    }
312                    _ => {}
313                }
314            }
315        }
316
317        // Phase 3: Apply blended results to scene nodes using rest pose as base
318        for (&node_handle, props) in self.blend_state.iter_nodes() {
319            self.animated_last_frame.insert(node_handle);
320
321            let rest_transform = target.rest_transform(node_handle).unwrap_or_default();
322
323            for (t, entry) in props {
324                match (t, entry) {
325                    (TargetPath::Translation, BlendEntry::Translation { value, weight }) => {
326                        if *weight < 1.0 {
327                            target.set_node_position(
328                                node_handle,
329                                rest_transform.position.lerp(*value, *weight),
330                            );
331                        } else {
332                            target.set_node_position(node_handle, *value);
333                        }
334                        target.mark_node_dirty(node_handle);
335                    }
336                    (TargetPath::Rotation, BlendEntry::Rotation { value, weight }) => {
337                        if *weight < 1.0 {
338                            let corrected = if rest_transform.rotation.dot(*value) < 0.0 {
339                                -*value
340                            } else {
341                                *value
342                            };
343                            target.set_node_rotation(
344                                node_handle,
345                                rest_transform.rotation.lerp(corrected, *weight).normalize(),
346                            );
347                        } else {
348                            target.set_node_rotation(node_handle, *value);
349                        }
350                        target.mark_node_dirty(node_handle);
351                    }
352                    (TargetPath::Scale, BlendEntry::Scale { value, weight }) => {
353                        if *weight < 1.0 {
354                            target.set_node_scale(
355                                node_handle,
356                                rest_transform.scale.lerp(*value, *weight),
357                            );
358                        } else {
359                            target.set_node_scale(node_handle, *value);
360                        }
361                        target.mark_node_dirty(node_handle);
362                    }
363                    (
364                        TargetPath::Weights,
365                        BlendEntry::MorphWeights {
366                            weights,
367                            total_weight,
368                        },
369                    ) => {
370                        apply_morph_weights(target, node_handle, weights, *total_weight);
371                    }
372                    _ => {}
373                }
374            }
375        }
376    }
377}
378
379/// Applies blended morph weights to the target, mixing with the rest pose
380/// when the total accumulated weight is below 1.0.
381fn apply_morph_weights(
382    target: &mut dyn AnimationTarget,
383    node: NodeHandle,
384    weights: &[f32],
385    total_weight: f32,
386) {
387    let dst = target.morph_weights_mut(node);
388    if dst.len() < weights.len() {
389        dst.resize(weights.len(), 0.0);
390    }
391    if total_weight >= 1.0 {
392        dst[..weights.len()].copy_from_slice(weights);
393    } else {
394        for (d, &src) in dst.iter_mut().zip(weights.iter()) {
395            *d = src * total_weight;
396        }
397    }
398}
399
400// ============================================================================
401// ActionControl — chainable builder for action state manipulation
402// ============================================================================
403
404/// Chainable wrapper for mutating an action within a mixer.
405///
406/// Obtained from [`AnimationMixer::action`] or [`AnimationMixer::get_control`].
407/// All setter methods return `self` to support method chaining.
408pub struct ActionControl<'a> {
409    mixer: &'a mut AnimationMixer,
410    handle: ActionHandle,
411}
412
413impl ActionControl<'_> {
414    /// Starts or restarts playback from the beginning.
415    #[must_use]
416    pub fn play(self) -> Self {
417        if !self.mixer.active_handles.contains(&self.handle) {
418            self.mixer.active_handles.push(self.handle);
419        }
420        if let Some(action) = self.mixer.actions.get_mut(self.handle) {
421            action.enabled = true;
422            action.paused = false;
423            action.weight = 1.0;
424            action.time = 0.0;
425        }
426        self
427    }
428
429    #[must_use]
430    pub fn set_loop_mode(self, mode: crate::action::LoopMode) -> Self {
431        if let Some(action) = self.mixer.actions.get_mut(self.handle) {
432            action.loop_mode = mode;
433        }
434        self
435    }
436
437    #[must_use]
438    pub fn set_time_scale(self, scale: f32) -> Self {
439        if let Some(action) = self.mixer.actions.get_mut(self.handle) {
440            action.time_scale = scale;
441        }
442        self
443    }
444
445    #[must_use]
446    pub fn set_weight(self, weight: f32) -> Self {
447        if let Some(action) = self.mixer.actions.get_mut(self.handle) {
448            action.weight = weight;
449        }
450        self
451    }
452
453    #[must_use]
454    pub fn set_time(self, time: f32) -> Self {
455        if let Some(action) = self.mixer.actions.get_mut(self.handle) {
456            action.time = time;
457        }
458        self
459    }
460
461    #[must_use]
462    pub fn resume(self) -> Self {
463        if let Some(action) = self.mixer.actions.get_mut(self.handle) {
464            action.paused = false;
465        }
466        self
467    }
468
469    #[must_use]
470    pub fn pause(self) -> Self {
471        if let Some(action) = self.mixer.actions.get_mut(self.handle) {
472            action.paused = true;
473        }
474        self
475    }
476
477    /// Stops playback and removes the action from the active set.
478    #[must_use]
479    pub fn stop(self) -> Self {
480        if let Some(action) = self.mixer.actions.get_mut(self.handle) {
481            action.enabled = false;
482            action.weight = 0.0;
483        }
484        self.mixer.active_handles.retain(|&h| h != self.handle);
485        self
486    }
487
488    /// Starts playback with a fade-in effect over the given duration.
489    #[must_use]
490    pub fn fade_in(self, _duration: f32) -> Self {
491        // TODO: Implement gradual weight interpolation
492        self.play()
493    }
494}
495
496impl std::ops::Deref for ActionControl<'_> {
497    type Target = AnimationAction;
498    fn deref(&self) -> &Self::Target {
499        self.mixer.actions.get(self.handle).unwrap()
500    }
501}