viewport_lib/plugins/skeleton/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}