Skip to main content

myth_animation/
mixer.rs

1use rustc_hash::FxHashMap;
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: Vec<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: Vec::new(),
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
258                let mut real_time_scale = action.time_scale;
259                if action.loop_mode == crate::action::LoopMode::PingPong && action.ping_pong_reverse
260                {
261                    real_time_scale = -real_time_scale;
262                }
263
264                let is_forward = (dt * real_time_scale) >= 0.0;
265
266                action.update(dt);
267                let t_curr = action.time;
268
269                let clip = action.clip();
270                events::collect_events(
271                    &clip.events,
272                    t_prev,
273                    t_curr,
274                    clip.duration,
275                    is_forward,
276                    &clip.name,
277                    &mut self.fired_events,
278                );
279            }
280        }
281
282        // Phase 2: Sample tracks and accumulate into blend buffer (O(1) per track)
283        for &handle in &self.active_handles {
284            let action = match self.actions.get_mut(handle) {
285                Some(a) if a.enabled && !a.paused && a.weight > 0.0 => a,
286                _ => continue,
287            };
288
289            let AnimationAction {
290                clip,
291                track_cursors,
292                clip_binding,
293                time,
294                weight,
295                ..
296            } = action;
297
298            for tb in &clip_binding.bindings {
299                let track = &clip.tracks[tb.track_index];
300                let cursor = &mut track_cursors[tb.track_index];
301                let node_handle = self.rig.bones[tb.bone_index];
302
303                match (&track.data, tb.target) {
304                    (TrackData::Vector3(t), TargetPath::Translation) => {
305                        let val = t.sample_with_cursor(*time, cursor);
306                        self.blend_state
307                            .accumulate_translation(node_handle, val, *weight);
308                    }
309                    (TrackData::Vector3(t), TargetPath::Scale) => {
310                        let val = t.sample_with_cursor(*time, cursor);
311                        self.blend_state.accumulate_scale(node_handle, val, *weight);
312                    }
313                    (TrackData::Quaternion(t), TargetPath::Rotation) => {
314                        let val = t.sample_with_cursor(*time, cursor);
315                        self.blend_state
316                            .accumulate_rotation(node_handle, val, *weight);
317                    }
318                    (TrackData::MorphWeights(t), TargetPath::Weights) => {
319                        t.sample_with_cursor_into(*time, cursor, &mut self.morph_buffer);
320                        self.blend_state.accumulate_morph_weights(
321                            node_handle,
322                            &self.morph_buffer,
323                            *weight,
324                        );
325                    }
326                    _ => {}
327                }
328            }
329        }
330
331        // Phase 3: Apply blended results to scene nodes using rest pose as base
332        for (&node_handle, props) in self.blend_state.iter_nodes() {
333            self.animated_last_frame.push(node_handle);
334
335            let rest_transform = target.rest_transform(node_handle).unwrap_or_default();
336
337            for (t, entry) in props {
338                match (t, entry) {
339                    (TargetPath::Translation, BlendEntry::Translation { value, weight }) => {
340                        if *weight < 1.0 {
341                            target.set_node_position(
342                                node_handle,
343                                rest_transform.position.lerp(*value, *weight),
344                            );
345                        } else {
346                            target.set_node_position(node_handle, *value);
347                        }
348                        target.mark_node_dirty(node_handle);
349                    }
350                    (TargetPath::Rotation, BlendEntry::Rotation { value, weight }) => {
351                        if *weight < 1.0 {
352                            let corrected = if rest_transform.rotation.dot(*value) < 0.0 {
353                                -*value
354                            } else {
355                                *value
356                            };
357                            target.set_node_rotation(
358                                node_handle,
359                                rest_transform.rotation.lerp(corrected, *weight).normalize(),
360                            );
361                        } else {
362                            target.set_node_rotation(node_handle, *value);
363                        }
364                        target.mark_node_dirty(node_handle);
365                    }
366                    (TargetPath::Scale, BlendEntry::Scale { value, weight }) => {
367                        if *weight < 1.0 {
368                            target.set_node_scale(
369                                node_handle,
370                                rest_transform.scale.lerp(*value, *weight),
371                            );
372                        } else {
373                            target.set_node_scale(node_handle, *value);
374                        }
375                        target.mark_node_dirty(node_handle);
376                    }
377                    (
378                        TargetPath::Weights,
379                        BlendEntry::MorphWeights {
380                            weights,
381                            total_weight,
382                        },
383                    ) => {
384                        apply_morph_weights(target, node_handle, weights, *total_weight);
385                    }
386                    _ => {}
387                }
388            }
389        }
390    }
391}
392
393/// Applies blended morph weights to the target, mixing with the rest pose
394/// when the total accumulated weight is below 1.0.
395fn apply_morph_weights(
396    target: &mut dyn AnimationTarget,
397    node: NodeHandle,
398    weights: &[f32],
399    total_weight: f32,
400) {
401    let dst = target.morph_weights_mut(node);
402    if dst.len() < weights.len() {
403        dst.resize(weights.len(), 0.0);
404    }
405    if total_weight >= 1.0 {
406        dst[..weights.len()].copy_from_slice(weights);
407    } else {
408        for (d, &src) in dst.iter_mut().zip(weights.iter()) {
409            *d = src * total_weight;
410        }
411    }
412}
413
414// ============================================================================
415// ActionControl — chainable builder for action state manipulation
416// ============================================================================
417
418/// Chainable wrapper for mutating an action within a mixer.
419///
420/// Obtained from [`AnimationMixer::action`] or [`AnimationMixer::get_control`].
421/// All setter methods return `self` to support method chaining.
422pub struct ActionControl<'a> {
423    mixer: &'a mut AnimationMixer,
424    handle: ActionHandle,
425}
426
427#[allow(clippy::return_self_not_must_use)]
428#[allow(clippy::must_use_candidate)]
429impl ActionControl<'_> {
430    /// Starts or restarts playback from the beginning.
431    pub fn play(self) -> Self {
432        if !self.mixer.active_handles.contains(&self.handle) {
433            self.mixer.active_handles.push(self.handle);
434        }
435        if let Some(action) = self.mixer.actions.get_mut(self.handle) {
436            action.enabled = true;
437            action.paused = false;
438            action.weight = 1.0;
439            action.time = 0.0;
440        }
441        self
442    }
443
444    pub fn set_loop_mode(self, mode: crate::action::LoopMode) -> Self {
445        if let Some(action) = self.mixer.actions.get_mut(self.handle) {
446            action.loop_mode = mode;
447            action.ping_pong_reverse = false;
448        }
449        self
450    }
451
452    pub fn set_time_scale(self, scale: f32) -> Self {
453        if let Some(action) = self.mixer.actions.get_mut(self.handle) {
454            action.time_scale = scale;
455        }
456        self
457    }
458
459    pub fn set_weight(self, weight: f32) -> Self {
460        if let Some(action) = self.mixer.actions.get_mut(self.handle) {
461            action.weight = weight;
462        }
463        self
464    }
465
466    pub fn set_time(self, time: f32) -> Self {
467        if let Some(action) = self.mixer.actions.get_mut(self.handle) {
468            action.time = time;
469        }
470        self
471    }
472
473    pub fn resume(self) -> Self {
474        if let Some(action) = self.mixer.actions.get_mut(self.handle) {
475            action.paused = false;
476        }
477        self
478    }
479
480    pub fn pause(self) -> Self {
481        if let Some(action) = self.mixer.actions.get_mut(self.handle) {
482            action.paused = true;
483        }
484        self
485    }
486
487    /// Stops playback and removes the action from the active set.
488    pub fn stop(self) -> Self {
489        if let Some(action) = self.mixer.actions.get_mut(self.handle) {
490            action.enabled = false;
491            action.weight = 0.0;
492        }
493        self.mixer.active_handles.retain(|&h| h != self.handle);
494        self
495    }
496
497    /// Starts playback with a fade-in effect over the given duration.
498    pub fn fade_in(self, _duration: f32) -> Self {
499        // TODO: Implement gradual weight interpolation
500        self.play()
501    }
502}
503
504impl std::ops::Deref for ActionControl<'_> {
505    type Target = AnimationAction;
506    fn deref(&self) -> &Self::Target {
507        self.mixer.actions.get(self.handle).unwrap()
508    }
509}