Skip to main content

scena/scene/
mixers.rs

1use std::sync::{Arc, atomic::AtomicBool};
2
3use crate::animation::{
4    AnimationChannel, AnimationLoopMode, AnimationMixer, AnimationMixerKey, AnimationPlaybackState,
5    AnimationTarget,
6};
7use crate::diagnostics::AnimationError;
8
9use super::{Scene, SceneImport, Transform};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12enum AppliedAnimationChange {
13    None,
14    Transform,
15}
16
17impl AppliedAnimationChange {
18    const fn transform_changed(self) -> bool {
19        matches!(self, Self::Transform)
20    }
21}
22
23impl Scene {
24    /// Creates a paused mixer for a named imported animation clip.
25    ///
26    /// Returns the mixer key without starting playback. For the one-call "play
27    /// this now" path, use [`Self::play_animation_by_name`].
28    pub fn create_animation_mixer(
29        &mut self,
30        import: &SceneImport,
31        clip_name: &str,
32    ) -> Result<AnimationMixerKey, AnimationError> {
33        let clip = import
34            .clip(clip_name)
35            .map_err(|_| AnimationError::ClipNotFound {
36                name: clip_name.to_string(),
37            })?
38            .clip();
39        Ok(self
40            .animation_mixers
41            .insert(AnimationMixer::new(clip, import.live_flag())))
42    }
43
44    /// Creates and starts a mixer for a named imported animation clip.
45    ///
46    /// This is the one-call path for "play this clip now". The returned
47    /// mixer key can still be passed to [`Self::update_animation`],
48    /// [`Self::set_animation_loop_mode`], [`Self::set_animation_speed`],
49    /// pause, seek, and stop helpers.
50    ///
51    /// # Examples
52    ///
53    /// ```no_run
54    /// # use scena::{Assets, Scene};
55    /// # async fn example() -> scena::Result<()> {
56    /// let assets = Assets::new();
57    /// let model = assets.load_scene("machine.glb").await?;
58    /// let mut scene = Scene::new();
59    /// let import = scene.instantiate(&model)?;
60    ///
61    /// let mixer = scene.play_animation_by_name(&import, "idle")?;
62    /// scene.update_animation(mixer, 1.0 / 60.0)?;
63    /// # Ok(())
64    /// # }
65    /// ```
66    pub fn play_animation_by_name(
67        &mut self,
68        import: &SceneImport,
69        clip_name: &str,
70    ) -> Result<AnimationMixerKey, AnimationError> {
71        let mixer = self.create_animation_mixer(import, clip_name)?;
72        self.play_animation(mixer)?;
73        Ok(mixer)
74    }
75
76    /// Creates a paused mixer for a caller-authored animation clip.
77    ///
78    /// Authored clips are not tied to a glTF import lifecycle, so the mixer is
79    /// not invalidated by import replacement. The host still owns ticking by
80    /// calling [`Self::update_animation`] or explicit sampling with
81    /// [`Self::seek_animation`].
82    pub fn create_authored_animation_mixer(
83        &mut self,
84        clip: crate::animation::AnimationClip,
85    ) -> Result<AnimationMixerKey, AnimationError> {
86        validate_authored_clip(&clip)?;
87        Ok(self
88            .animation_mixers
89            .insert(AnimationMixer::new(clip, Arc::new(AtomicBool::new(true)))))
90    }
91
92    /// Creates and starts a mixer for a caller-authored animation clip.
93    pub fn play_authored_animation(
94        &mut self,
95        clip: crate::animation::AnimationClip,
96    ) -> Result<AnimationMixerKey, AnimationError> {
97        let mixer = self.create_authored_animation_mixer(clip)?;
98        self.play_animation(mixer)?;
99        Ok(mixer)
100    }
101
102    /// Borrows the mixer state for a given key.
103    pub fn animation_mixer(
104        &self,
105        mixer: AnimationMixerKey,
106    ) -> Result<&AnimationMixer, AnimationError> {
107        self.animation_mixers
108            .get(mixer)
109            .ok_or(AnimationError::MixerNotFound(mixer))
110    }
111
112    /// Starts the mixer; resumes from the current time if it was paused.
113    pub fn play_animation(&mut self, mixer: AnimationMixerKey) -> Result<(), AnimationError> {
114        self.animation_mixer_mut(mixer)?.play();
115        Ok(())
116    }
117
118    /// Pauses the mixer at its current time. The next
119    /// [`Self::play_animation`] resumes from the same time.
120    pub fn pause_animation(&mut self, mixer: AnimationMixerKey) -> Result<(), AnimationError> {
121        self.animation_mixer_mut(mixer)?.pause();
122        Ok(())
123    }
124
125    /// Stops the mixer and snaps the animated nodes back to time zero.
126    pub fn stop_animation(&mut self, mixer: AnimationMixerKey) -> Result<(), AnimationError> {
127        let clip = {
128            let mixer = self.animation_mixer_mut(mixer)?;
129            mixer.stop();
130            mixer.clip().clone()
131        };
132        self.apply_animation_clip(&clip, 0.0);
133        Ok(())
134    }
135
136    /// Seeks the mixer to a specific time in seconds and applies the resulting
137    /// pose to the animated nodes.
138    pub fn seek_animation(
139        &mut self,
140        mixer: AnimationMixerKey,
141        time_seconds: f32,
142    ) -> Result<(), AnimationError> {
143        let (clip, time_seconds) = {
144            let mixer = self.animation_mixer_mut(mixer)?;
145            mixer.seek(time_seconds);
146            (mixer.clip().clone(), mixer.time_seconds())
147        };
148        self.apply_animation_clip(&clip, time_seconds);
149        Ok(())
150    }
151
152    /// Sets the playback speed multiplier. `1.0` is real-time; negative values
153    /// play the clip in reverse.
154    pub fn set_animation_speed(
155        &mut self,
156        mixer: AnimationMixerKey,
157        speed: f32,
158    ) -> Result<(), AnimationError> {
159        self.animation_mixer_mut(mixer)?.set_speed(speed);
160        Ok(())
161    }
162
163    /// Sets the loop mode (loop, once, ping-pong).
164    pub fn set_animation_loop_mode(
165        &mut self,
166        mixer: AnimationMixerKey,
167        loop_mode: AnimationLoopMode,
168    ) -> Result<(), AnimationError> {
169        self.animation_mixer_mut(mixer)?.set_loop_mode(loop_mode);
170        Ok(())
171    }
172
173    /// Advances the mixer by `delta_seconds` and applies the resulting pose to
174    /// animated nodes. Hosts are expected to call this once per frame for
175    /// every active mixer.
176    pub fn update_animation(
177        &mut self,
178        mixer: AnimationMixerKey,
179        delta_seconds: f32,
180    ) -> Result<(), AnimationError> {
181        let (clip, time_seconds, was_playing) = {
182            let mixer = self.animation_mixer_mut(mixer)?;
183            let was_playing = mixer.state() == AnimationPlaybackState::Playing;
184            mixer.advance(delta_seconds);
185            (mixer.clip().clone(), mixer.time_seconds(), was_playing)
186        };
187        if was_playing {
188            self.apply_animation_clip(&clip, time_seconds);
189        }
190        Ok(())
191    }
192
193    fn animation_mixer_mut(
194        &mut self,
195        mixer: AnimationMixerKey,
196    ) -> Result<&mut AnimationMixer, AnimationError> {
197        let mixer_state = self
198            .animation_mixers
199            .get_mut(mixer)
200            .ok_or(AnimationError::MixerNotFound(mixer))?;
201        if mixer_state.is_stale() {
202            return Err(AnimationError::StaleMixer(mixer));
203        }
204        Ok(mixer_state)
205    }
206
207    fn apply_animation_clip(&mut self, clip: &crate::animation::AnimationClip, time_seconds: f32) {
208        let mut transform_changed = false;
209        for channel in clip.channels() {
210            transform_changed |= self
211                .apply_animation_channel(channel, time_seconds)
212                .transform_changed();
213        }
214        if transform_changed {
215            self.transform_revision = self.transform_revision.saturating_add(1);
216        }
217    }
218
219    fn apply_animation_channel(
220        &mut self,
221        channel: &AnimationChannel,
222        time_seconds: f32,
223    ) -> AppliedAnimationChange {
224        if channel.target() == AnimationTarget::Weights {
225            let Some(weights) = channel.sample_weights(time_seconds) else {
226                return AppliedAnimationChange::None;
227            };
228            self.set_morph_weights_unchecked(channel.target_node(), weights);
229            return AppliedAnimationChange::None;
230        }
231
232        let Some(node) = self.nodes.get_mut(channel.target_node()) else {
233            return AppliedAnimationChange::None;
234        };
235        let before = node.transform;
236        let mut transform = before;
237        match channel.target() {
238            AnimationTarget::Translation => {
239                let Some(value) = channel.sample_vec3(time_seconds) else {
240                    return AppliedAnimationChange::None;
241                };
242                transform.translation = value;
243            }
244            AnimationTarget::Scale => {
245                let Some(value) = channel.sample_vec3(time_seconds) else {
246                    return AppliedAnimationChange::None;
247                };
248                transform.scale = value;
249            }
250            AnimationTarget::Rotation => {
251                let Some(value) = channel.sample_quat(time_seconds) else {
252                    return AppliedAnimationChange::None;
253                };
254                transform.rotation = value;
255            }
256            AnimationTarget::Weights => unreachable!("weights handled before mutable node borrow"),
257        }
258        if before == transform {
259            return AppliedAnimationChange::None;
260        }
261        node.transform = Transform { ..transform };
262        AppliedAnimationChange::Transform
263    }
264}
265
266fn validate_authored_clip(clip: &crate::animation::AnimationClip) -> Result<(), AnimationError> {
267    if !clip.duration_seconds().is_finite() || clip.duration_seconds() <= 0.0 {
268        return Err(AnimationError::InvalidClip {
269            reason: "duration_seconds must be finite and positive".to_owned(),
270        });
271    }
272    if clip.channels().is_empty() {
273        return Err(AnimationError::InvalidClip {
274            reason: "clip must contain at least one channel".to_owned(),
275        });
276    }
277    Ok(())
278}
279
280#[cfg(test)]
281impl Scene {
282    pub(crate) fn insert_animation_mixer_for_test(
283        &mut self,
284        clip: crate::animation::AnimationClip,
285    ) -> AnimationMixerKey {
286        self.animation_mixers
287            .insert(AnimationMixer::new(clip, Arc::new(AtomicBool::new(true))))
288    }
289}
290
291#[cfg(test)]
292mod tests {
293    use crate::animation::{
294        AnimationChannel, AnimationClip, AnimationClipKey, AnimationInterpolation, AnimationOutput,
295        AnimationTarget,
296    };
297    use crate::scene::{Scene, Transform, Vec3};
298
299    #[test]
300    fn transform_only_animation_updates_transform_revision_without_structure_dirtying() {
301        let mut scene = Scene::new();
302        let node = scene
303            .add_empty(scene.root(), Transform::IDENTITY)
304            .expect("animated node inserts");
305        let mixer = scene.insert_animation_mixer_for_test(translation_clip(node));
306        scene.play_animation(mixer).expect("mixer starts");
307        let before = scene.dirty_state();
308
309        for expected_transform_revision in 1..=3 {
310            let frame_before = scene.dirty_state();
311            scene
312                .update_animation(mixer, 0.25)
313                .expect("animation frame updates");
314            let frame_after = scene.dirty_state();
315            assert_eq!(
316                frame_after.structure_revision, frame_before.structure_revision,
317                "transform-only animation frames must not dirty scene structure"
318            );
319            assert_eq!(
320                frame_after.transform_revision,
321                frame_before.transform_revision + 1,
322                "changed transform animation frames must bump transform revision exactly once"
323            );
324            assert_eq!(
325                frame_after.transform_revision,
326                before.transform_revision + expected_transform_revision,
327            );
328        }
329
330        let after = scene.dirty_state();
331        assert_eq!(
332            after.structure_revision, before.structure_revision,
333            "transform-only animation playback must preserve structure revision across frames"
334        );
335    }
336
337    #[test]
338    fn morph_weight_animation_keeps_structural_revision_semantics() {
339        let mut scene = Scene::new();
340        let node = scene
341            .add_empty(scene.root(), Transform::IDENTITY)
342            .expect("morph node inserts");
343        scene.set_initial_morph_weights(node, &[0.0]);
344        let mixer = scene.insert_animation_mixer_for_test(weight_clip(node));
345        let before = scene.dirty_state();
346
347        scene
348            .seek_animation(mixer, 1.0)
349            .expect("morph weight seek applies");
350        let after = scene.dirty_state();
351
352        assert_eq!(
353            after.transform_revision, before.transform_revision,
354            "morph weight animation must not masquerade as a transform-only update"
355        );
356        assert!(
357            after.structure_revision > before.structure_revision,
358            "morph weight animation remains structural because vertex deformation changes"
359        );
360    }
361
362    fn translation_clip(node: crate::scene::NodeKey) -> AnimationClip {
363        AnimationClip::new(
364            AnimationClipKey::fresh(),
365            Some("MoveX".to_string()),
366            vec![AnimationChannel::new(
367                node,
368                AnimationTarget::Translation,
369                vec![0.0, 1.0],
370                AnimationOutput::Vec3(vec![Vec3::ZERO, Vec3::new(1.0, 0.0, 0.0)]),
371                AnimationInterpolation::Linear,
372            )],
373            1.0,
374        )
375        .expect("test translation clip is valid")
376    }
377
378    fn weight_clip(node: crate::scene::NodeKey) -> AnimationClip {
379        AnimationClip::new(
380            AnimationClipKey::fresh(),
381            Some("Morph".to_string()),
382            vec![AnimationChannel::new(
383                node,
384                AnimationTarget::Weights,
385                vec![0.0, 1.0],
386                AnimationOutput::Weights(vec![vec![0.0], vec![1.0]]),
387                AnimationInterpolation::Linear,
388            )],
389            1.0,
390        )
391        .expect("test weight clip is valid")
392    }
393}