Skip to main content

viewport_lib/runtime/plugins/skeleton_plugin/
clip_player.rs

1//! ClipPlayerPlugin: drives a [`Pose`] from an [`AnimationClip`].
2
3use crate::runtime::context::RuntimeStepContext;
4use crate::runtime::plugin::{RuntimePlugin, phase};
5
6use super::clip::AnimationClip;
7use super::skeleton::Pose;
8
9/// Runtime plugin that samples an [`AnimationClip`] each step and writes the
10/// resulting [`Pose`] into `RuntimeResources` for [`super::SkeletonPlugin`] to
11/// consume.
12///
13/// Runs at `phase::ANIMATE`, which is before the `POST_SIM` phase where
14/// `SkeletonPlugin` reads the pose.
15///
16/// The plugin starts each frame from a clone of `bind_pose`, then overlays the
17/// clip's animated channels. Joints not touched by the clip keep their bind
18/// values; channels not touched by the clip on an animated joint keep their
19/// bind values too. This means a clip can be authored to drive only the
20/// channels it cares about and the bind pose supplies everything else.
21///
22/// # Example
23///
24/// ```rust,ignore
25/// let bind_pose = /* Pose with joint 1 placed at its bind world transform */;
26/// let clip = AnimationClip { duration: 2.0, tracks: vec![/* ... */] };
27/// let player = ClipPlayerPlugin::new(clip, bind_pose);
28/// let runtime = ViewportRuntime::new()
29///     .with_plugin(player)
30///     .with_plugin(skeleton_plugin);
31/// ```
32pub struct ClipPlayerPlugin {
33    /// Clip whose tracks are sampled and applied each step.
34    pub clip: AnimationClip,
35    /// Per-frame baseline. Cloned each step before the clip is applied.
36    pub bind_pose: Pose,
37    /// Playback speed multiplier. `1.0` = real time. Negative values play in
38    /// reverse.
39    pub speed: f32,
40    /// Whether playback wraps at `clip.duration`. Non-looping playback clamps
41    /// the playhead at the endpoints.
42    pub looping: bool,
43    /// Current play position in seconds.
44    pub playhead: f32,
45    /// When false, the playhead does not advance. Manual seeks via `playhead`
46    /// still take effect.
47    pub playing: bool,
48}
49
50impl ClipPlayerPlugin {
51    /// Create a player that loops `clip` over `bind_pose` at real-time speed.
52    pub fn new(clip: AnimationClip, bind_pose: Pose) -> Self {
53        Self {
54            clip,
55            bind_pose,
56            speed: 1.0,
57            looping: true,
58            playhead: 0.0,
59            playing: true,
60        }
61    }
62
63    /// Set the playback speed multiplier.
64    pub fn with_speed(mut self, speed: f32) -> Self {
65        self.speed = speed;
66        self
67    }
68
69    /// Set whether playback loops at `clip.duration`.
70    pub fn with_looping(mut self, looping: bool) -> Self {
71        self.looping = looping;
72        self
73    }
74}
75
76impl RuntimePlugin for ClipPlayerPlugin {
77    fn priority(&self) -> i32 {
78        phase::ANIMATE
79    }
80
81    fn step(&mut self, ctx: &mut RuntimeStepContext<'_>) {
82        if self.playing {
83            self.playhead += ctx.dt * self.speed;
84            if self.clip.duration > 0.0 {
85                if self.looping {
86                    self.playhead = self.playhead.rem_euclid(self.clip.duration);
87                } else {
88                    self.playhead = self.playhead.clamp(0.0, self.clip.duration);
89                }
90            }
91        }
92
93        let mut pose = self.bind_pose.clone();
94        self.clip.sample_into(self.playhead, &mut pose);
95        ctx.resources.insert(pose);
96    }
97}