Skip to main content

pebble/wgpu/
player.rs

1use std::{collections::HashMap, sync::Arc};
2
3use super::{
4    animation::AnimationClip,
5    skeleton::{Skeleton, Transform},
6};
7
8/// A mutable snapshot of a skeleton's joint poses with world-space helpers.
9///
10/// Obtain one from [`AnimationPlayer::compute_pose`]. Optionally modify joints
11/// for IK, then call [`skinning_matrices`](Self::skinning_matrices) to get
12/// the final matrices ready for a [`SkinningBatch`](super::skinning::SkinnedBatchRenderer).
13///
14/// ```ignore
15/// let mut pose = player.compute_pose();
16/// let foot   = skeleton.joint_index_by_name("foot_l").unwrap();
17/// let lower  = skeleton.joint_index_by_name("lower_leg_l").unwrap();
18/// let upper  = skeleton.joint_index_by_name("upper_leg_l").unwrap();
19///
20/// // analytic two-bone IK — you supply the math, Pose handles local/world conversion
21/// let (r_upper, r_lower) = two_bone_ik(
22///     pose.world_position(upper),
23///     pose.world_position(lower),
24///     target_foot_pos,
25/// );
26/// pose.set_world_rotation(upper, r_upper);
27/// pose.set_world_rotation(lower, r_lower);
28///
29/// let matrices = pose.skinning_matrices();
30/// ```
31pub struct Pose {
32    locals:   Vec<Transform>,
33    skeleton: Arc<Skeleton>,
34    worlds:   Option<Vec<glam::Mat4>>, // lazily computed, invalidated on any set_*
35}
36
37impl Pose {
38    pub(crate) fn new(locals: Vec<Transform>, skeleton: Arc<Skeleton>) -> Self {
39        Self { locals, skeleton, worlds: None }
40    }
41
42    fn ensure_worlds(&mut self) {
43        if self.worlds.is_none() {
44            self.worlds = Some(self.skeleton.world_matrices(&self.locals));
45        }
46    }
47
48    /// World-space position of `joint` (origin of its local frame).
49    pub fn world_position(&mut self, joint: usize) -> glam::Vec3 {
50        self.ensure_worlds();
51        self.worlds.as_ref().unwrap()[joint].transform_point3(glam::Vec3::ZERO)
52    }
53
54    /// World-space rotation of `joint`.
55    pub fn world_rotation(&mut self, joint: usize) -> glam::Quat {
56        self.ensure_worlds();
57        let (_, rot, _) = self.worlds.as_ref().unwrap()[joint].to_scale_rotation_translation();
58        rot
59    }
60
61    /// Full world-space matrix of `joint` — position, rotation, and scale in
62    /// one shot. Multiply by the entity's own world matrix to get game-world space.
63    pub fn world_matrix(&mut self, joint: usize) -> glam::Mat4 {
64        self.ensure_worlds();
65        self.worlds.as_ref().unwrap()[joint]
66    }
67
68    /// Set `joint`'s orientation in world space. Converts to local
69    /// (parent-relative) rotation automatically — no matrix math on your side.
70    pub fn set_world_rotation(&mut self, joint: usize, rotation: glam::Quat) {
71        let parent_rot = match self.skeleton.joint(joint).parent {
72            Some(p) => {
73                self.ensure_worlds();
74                let (_, rot, _) = self.worlds.as_ref().unwrap()[p].to_scale_rotation_translation();
75                rot
76            }
77            None => glam::Quat::IDENTITY,
78        };
79        self.locals[joint].rotation = parent_rot.inverse() * rotation;
80        self.worlds = None;
81    }
82
83    /// Set `joint`'s position in world space. Converts to local
84    /// (parent-relative) translation automatically.
85    pub fn set_world_position(&mut self, joint: usize, position: glam::Vec3) {
86        let parent_world = match self.skeleton.joint(joint).parent {
87            Some(p) => {
88                self.ensure_worlds();
89                self.worlds.as_ref().unwrap()[p]
90            }
91            None => glam::Mat4::IDENTITY,
92        };
93        self.locals[joint].translation = parent_world.inverse().transform_point3(position);
94        self.worlds = None;
95    }
96
97    /// Local (parent-relative) transform of `joint`.
98    pub fn local(&self, joint: usize) -> Transform {
99        self.locals[joint]
100    }
101
102    /// Directly set `joint`'s local (parent-relative) transform.
103    /// Use when you already have the correct local-space value.
104    pub fn set_local(&mut self, joint: usize, t: Transform) {
105        self.locals[joint] = t;
106        self.worlds = None;
107    }
108
109    /// Access the underlying skeleton — for name lookups, joint count, etc.
110    pub fn skeleton(&self) -> &Skeleton {
111        &self.skeleton
112    }
113
114    /// Compute skinning matrices from the current (possibly IK-modified) pose,
115    /// ready to write into a [`SkinnedBatchRenderer`](super::skinning::SkinnedBatchRenderer).
116    pub fn skinning_matrices(&self) -> Vec<glam::Mat4> {
117        self.skeleton.skinning_matrices(&self.locals)
118    }
119}
120
121struct Transition {
122    from_clip: String,
123    from_time: f32,
124    elapsed: f32,
125    duration: f32,
126}
127
128/// A component that manages animation playback state for a skinned entity.
129///
130/// Attach alongside a [`Handle<SkinnedMesh>`]. The engine advances time each
131/// tick via [`advance`](Self::advance). Call [`play`](Self::play) /
132/// [`crossfade`](Self::crossfade) to control playback. Obtain skinning
133/// matrices via [`compute_matrices`](Self::compute_matrices) from your own
134/// skinning system, or add [`CpuSkinningPlugin`](super::skinning::CpuSkinningPlugin)
135/// to have the engine do it automatically.
136///
137/// Cloning shares the skeleton and clip data (via `Arc`) but resets
138/// per-entity playback state — useful for spawning many entities from the
139/// same loaded skeleton.
140pub struct AnimationPlayer {
141    skeleton: Arc<Skeleton>,
142    clips: Arc<HashMap<String, AnimationClip>>,
143    current: Option<String>,
144    time: f32,
145    speed: f32,
146    looping: bool,
147    transition: Option<Transition>,
148    /// Set by [`set_matrices`](Self::set_matrices); cleared by [`clear_matrices`](Self::clear_matrices).
149    /// When set, [`compute_matrices`](Self::compute_matrices) returns this instead of sampling the animation.
150    matrices_override: Option<Vec<glam::Mat4>>,
151}
152
153impl AnimationPlayer {
154    pub(crate) fn new(skeleton: Arc<Skeleton>, clips: Arc<HashMap<String, AnimationClip>>) -> Self {
155        Self {
156            skeleton,
157            clips,
158            current: None,
159            time: 0.0,
160            speed: 1.0,
161            looping: true,
162            transition: None,
163            matrices_override: None,
164        }
165    }
166
167    /// Switch immediately to `name`, restarting from the beginning. Use for
168    /// hard cuts (hit reactions, death). Warns if `name` wasn't loaded.
169    pub fn play(&mut self, name: &str) {
170        if self.clips.contains_key(name) {
171            self.current = Some(name.to_string());
172            self.time = 0.0;
173            self.looping = true;
174            self.transition = None;
175        } else {
176            tracing::warn!("AnimationPlayer::play: clip '{name}' not found");
177        }
178    }
179
180    /// Play `name` once to completion without looping, then hold the last
181    /// frame. Warns if `name` wasn't loaded.
182    pub fn play_once(&mut self, name: &str) {
183        if self.clips.contains_key(name) {
184            self.current = Some(name.to_string());
185            self.time = 0.0;
186            self.looping = false;
187            self.transition = None;
188        } else {
189            tracing::warn!("AnimationPlayer::play_once: clip '{name}' not found");
190        }
191    }
192
193    /// Blend from the current clip to `name` over `duration` seconds. Use
194    /// for smooth transitions (walk → run). Warns if `name` wasn't loaded.
195    pub fn crossfade(&mut self, name: &str, duration: f32) {
196        if !self.clips.contains_key(name) {
197            tracing::warn!("AnimationPlayer::crossfade: clip '{name}' not found");
198            return;
199        }
200        if let Some(current) = &self.current {
201            self.transition = Some(Transition {
202                from_clip: current.clone(),
203                from_time: self.time,
204                elapsed: 0.0,
205                duration: duration.max(f32::EPSILON),
206            });
207        }
208        self.current = Some(name.to_string());
209        self.time = 0.0;
210        self.looping = true;
211    }
212
213    /// Stop advancing time (speed → 0).
214    pub fn pause(&mut self) {
215        self.speed = 0.0;
216    }
217
218    /// Resume normal playback (speed → 1).
219    pub fn resume(&mut self) {
220        self.speed = 1.0;
221    }
222
223    /// Set the playback speed multiplier. `1.0` = normal, `2.0` = double,
224    /// negative values play in reverse.
225    pub fn set_speed(&mut self, speed: f32) {
226        self.speed = speed;
227    }
228
229    /// Jump to a specific time within the current clip.
230    pub fn set_time(&mut self, time: f32) {
231        self.time = time;
232    }
233
234    /// Current playback position in seconds.
235    pub fn time(&self) -> f32 {
236        self.time
237    }
238
239    /// Current playback speed multiplier.
240    pub fn speed(&self) -> f32 {
241        self.speed
242    }
243
244    /// Number of joints in this player's skeleton.
245    pub fn joint_count(&self) -> usize {
246        self.skeleton.joint_count()
247    }
248
249    /// The skeleton this player drives.
250    pub fn skeleton(&self) -> &Skeleton {
251        &self.skeleton
252    }
253
254    /// The currently active clip, if any.
255    pub fn current_clip(&self) -> Option<&AnimationClip> {
256        self.current.as_ref().and_then(|name| self.clips.get(name))
257    }
258
259    /// Returns an iterator over the names of all loaded animation clips, in an unspecified order.
260    pub fn clip_names(&self) -> impl Iterator<Item = &str> {
261        self.clips.keys().map(|s| s.as_str())
262    }
263
264    /// Advance time by `dt` seconds, handling looping and any active
265    /// crossfade. Called by [`advance_animations`](super::skinning::advance_animations) —
266    /// call it yourself only if you skip [`CpuSkinningPlugin`](super::skinning::CpuSkinningPlugin).
267    pub fn advance(&mut self, dt: f32) {
268        if let Some(t) = &mut self.transition {
269            t.from_time += dt * self.speed;
270            t.elapsed += dt;
271            if t.elapsed >= t.duration {
272                self.transition = None;
273            }
274        }
275        let Some(name) = &self.current else { return };
276        let Some(clip) = self.clips.get(name) else { return };
277        self.time += dt * self.speed;
278        if self.looping && clip.duration > 0.0 {
279            self.time = self.time.rem_euclid(clip.duration);
280        } else {
281            self.time = self.time.min(clip.duration);
282        }
283    }
284
285    /// Sample the current animation (including any active crossfade) into a
286    /// [`Pose`]. Use as the IK entry point — modify joints via
287    /// [`set_world_rotation`](Pose::set_world_rotation) /
288    /// [`set_world_position`](Pose::set_world_position), then call
289    /// [`skinning_matrices`](Pose::skinning_matrices). Returns the bind pose
290    /// if no clip is playing.
291    pub fn compute_pose(&self) -> Pose {
292        let locals = self.sample_locals();
293        Pose::new(locals, Arc::clone(&self.skeleton))
294    }
295
296    fn sample_locals(&self) -> Vec<Transform> {
297        let Some(name) = &self.current else {
298            return self.skeleton.bind_pose();
299        };
300        let Some(clip) = self.clips.get(name) else {
301            return self.skeleton.bind_pose();
302        };
303        match &self.transition {
304            None => clip.sample(self.time, &self.skeleton),
305            Some(t) => {
306                let weight = (t.elapsed / t.duration).clamp(0.0, 1.0);
307                let poses_to = clip.sample(self.time, &self.skeleton);
308                if let Some(from) = self.clips.get(&t.from_clip) {
309                    let poses_from = from.sample(t.from_time, &self.skeleton);
310                    poses_from.iter().zip(&poses_to).map(|(a, b)| a.lerp(b, weight)).collect()
311                } else {
312                    poses_to
313                }
314            }
315        }
316    }
317
318    /// Override the matrices that [`compute_matrices`](Self::compute_matrices)
319    /// returns. Use after modifying a [`Pose`] for IK or procedural animation:
320    ///
321    /// ```ignore
322    /// let mut pose = player.compute_pose();
323    /// pose.set_world_rotation(foot, target_rot);
324    /// player.set_matrices(pose.skinning_matrices());
325    /// ```
326    ///
327    /// The override persists until [`clear_matrices`](Self::clear_matrices) is
328    /// called, so call this every frame while the override is active.
329    pub fn set_matrices(&mut self, matrices: Vec<glam::Mat4>) {
330        self.matrices_override = Some(matrices);
331    }
332
333    /// Remove the matrix override — [`compute_matrices`](Self::compute_matrices)
334    /// returns to sampling the current animation clip.
335    pub fn clear_matrices(&mut self) {
336        self.matrices_override = None;
337    }
338
339    /// Sample the current animation (including any active crossfade) and
340    /// return one skinning matrix per joint, ready to upload. Returns the
341    /// matrix override if one is set via [`set_matrices`](Self::set_matrices),
342    /// or identity matrices if no clip is playing.
343    pub fn compute_matrices(&self) -> Vec<glam::Mat4> {
344        if let Some(ref m) = self.matrices_override {
345            return m.clone();
346        }
347        if self.current.is_none() {
348            return vec![glam::Mat4::IDENTITY; self.skeleton.joint_count()];
349        }
350        self.compute_pose().skinning_matrices()
351    }
352}
353
354impl Clone for AnimationPlayer {
355    fn clone(&self) -> Self {
356        Self {
357            skeleton: Arc::clone(&self.skeleton),
358            clips: Arc::clone(&self.clips),
359            current: self.current.clone(),
360            time: 0.0,
361            speed: 1.0,
362            looping: true,
363            transition: None,
364            matrices_override: None,
365        }
366    }
367}