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}