Skip to main content

viewport_lib/plugins/skeleton/
actor.rs

1//! SkinnedActorPlugin: many independently-animated skinned actors sharing one
2//! [`Skeleton`].
3//!
4//! Each [`SkinnedActor`] has its own playhead, clip, and play state. Within
5//! an actor, all [`SkinnedActorPart`]s deform from that actor's pose, so the
6//! parts of a multi-mesh character animate in sync. Across actors, animation
7//! is independent: a crowd of N actors with staggered playheads animates as N
8//! distinct loops over the same source data.
9//!
10//! Compared to [`super::SkeletonPlugin`] + [`super::ClipPlayerPlugin`]:
11//!
12//! - That pair runs *one* animation: the player writes one `Pose` to
13//!   resources and one or more SkeletonPlugins consume it. Good for a single
14//!   character (potentially with many parts) animating in lockstep.
15//! - `SkinnedActorPlugin` is the multi-actor case: a crowd, a stadium of
16//!   NPCs, an army. One plugin holds the shared skeleton and N actors that
17//!   each animate independently, in a single phase tick.
18//!
19//! This phase is the CPU baseline. The same actor/part decomposition is what
20//! a later GPU-skinning path will turn into per-actor joint-palette uploads
21//! and per-part skinned draw calls, so callers should target this shape now.
22
23use crate::plugins::skinning::SkinWeights;
24use crate::plugins::skinning::{SkinnedMeshUpdate, SkinnedPoseUpdate};
25use crate::resources::mesh::mesh_store::MeshId;
26use crate::runtime::context::RuntimeStepContext;
27use crate::runtime::plugin::{RuntimePlugin, phase};
28
29use super::clip::AnimationClip;
30use super::plugin::SkinningPath;
31use super::skeleton::{JointMatrices, Pose, Skeleton, apply_skin};
32
33/// One skinned mesh of an actor. All parts of an actor share that actor's
34/// per-frame pose; each part owns its own bind-pose vertex data and GPU mesh
35/// because the CPU LBS path writes deformed vertices back per-mesh.
36pub struct SkinnedActorPart {
37    /// GPU mesh this part deforms each frame.
38    pub mesh_id: MeshId,
39    /// Bind-pose positions; same shape as the data uploaded to `mesh_id`.
40    pub bind_positions: Vec<[f32; 3]>,
41    /// Bind-pose normals.
42    pub bind_normals: Vec<[f32; 3]>,
43    /// Per-vertex joint indices and weights, parallel to `bind_positions`.
44    pub skin_weights: SkinWeights,
45}
46
47/// One independently-animated actor. Its parts deform from this actor's pose
48/// each frame; other actors in the same plugin animate from their own.
49pub struct SkinnedActor {
50    /// Mesh parts that make up this actor.
51    pub parts: Vec<SkinnedActorPart>,
52    /// Index into [`SkinnedActorPlugin::clips`] selecting the active clip.
53    pub clip_index: usize,
54    /// Current play position in seconds.
55    pub playhead: f32,
56    /// Playback speed multiplier. `1.0` = real time. Negative values reverse.
57    pub speed: f32,
58    /// Whether playback wraps at the active clip's duration.
59    pub looping: bool,
60    /// When false, the playhead does not advance.
61    pub playing: bool,
62}
63
64impl SkinnedActor {
65    /// Create an actor playing clip 0 at real-time speed, looping.
66    pub fn new(parts: Vec<SkinnedActorPart>) -> Self {
67        Self {
68            parts,
69            clip_index: 0,
70            playhead: 0.0,
71            speed: 1.0,
72            looping: true,
73            playing: true,
74        }
75    }
76
77    /// Set the active clip index.
78    pub fn with_clip(mut self, clip_index: usize) -> Self {
79        self.clip_index = clip_index;
80        self
81    }
82
83    /// Set the initial playhead, useful for de-phasing actors in a crowd.
84    pub fn with_playhead(mut self, playhead: f32) -> Self {
85        self.playhead = playhead;
86        self
87    }
88
89    /// Set the playback speed multiplier.
90    pub fn with_speed(mut self, speed: f32) -> Self {
91        self.speed = speed;
92        self
93    }
94}
95
96/// Runtime plugin that animates many actors sharing one skeleton.
97///
98/// Runs at `phase::POST_SIM`. Does not read or write `RuntimeResources::Pose`;
99/// each actor's pose is built internally from its own playhead and applied
100/// directly. Emits one `SkinnedMeshUpdate` per part per actor each frame.
101pub struct SkinnedActorPlugin {
102    /// Skeleton shared by every actor.
103    pub skeleton: Skeleton,
104    /// Bind pose, cloned each step before the actor's clip is sampled over
105    /// it. Provides default channel values for joints the clip does not
106    /// animate.
107    pub bind_pose: Pose,
108    /// Clips available to actors via `SkinnedActor::clip_index`.
109    pub clips: Vec<AnimationClip>,
110    /// All actors driven by this plugin.
111    pub actors: Vec<SkinnedActor>,
112    /// Which deformation path to emit each frame. On `Gpu`, one
113    /// [`SkinnedPoseUpdate`] is pushed per actor per part. The instance id is
114    /// the actor's index in `actors` so the host can drive the right joint
115    /// palette via `SkinningPlugin::attach_palette`.
116    pub path: SkinningPath,
117}
118
119impl SkinnedActorPlugin {
120    /// Create a plugin with no actors yet.
121    pub fn new(skeleton: Skeleton, bind_pose: Pose, clips: Vec<AnimationClip>) -> Self {
122        Self {
123            skeleton,
124            bind_pose,
125            clips,
126            actors: Vec::new(),
127            path: SkinningPath::default(),
128        }
129    }
130
131    /// Override the deformation path. Builder-style for ergonomic init.
132    pub fn with_path(mut self, path: SkinningPath) -> Self {
133        self.path = path;
134        self
135    }
136
137    /// Append an actor.
138    pub fn with_actor(mut self, actor: SkinnedActor) -> Self {
139        self.actors.push(actor);
140        self
141    }
142
143    /// Append many actors at once.
144    pub fn with_actors(mut self, actors: impl IntoIterator<Item = SkinnedActor>) -> Self {
145        self.actors.extend(actors);
146        self
147    }
148}
149
150impl RuntimePlugin for SkinnedActorPlugin {
151    fn priority(&self) -> i32 {
152        phase::POST_SIM
153    }
154
155    fn step(&mut self, ctx: &mut RuntimeStepContext<'_>) {
156        for (actor_idx, actor) in self.actors.iter_mut().enumerate() {
157            let clip = match self.clips.get(actor.clip_index) {
158                Some(c) => c,
159                None => continue,
160            };
161
162            if actor.playing {
163                actor.playhead += ctx.dt * actor.speed;
164                if clip.duration > 0.0 {
165                    if actor.looping {
166                        actor.playhead = actor.playhead.rem_euclid(clip.duration);
167                    } else {
168                        actor.playhead = actor.playhead.clamp(0.0, clip.duration);
169                    }
170                }
171            }
172
173            // One pose + one FK pass per actor; parts share the result.
174            let mut pose = self.bind_pose.clone();
175            clip.sample_into(actor.playhead, &mut pose);
176            let matrices = JointMatrices::compute(&self.skeleton, &pose);
177
178            match self.path {
179                SkinningPath::Cpu => {
180                    for part in &actor.parts {
181                        let (positions, normals) = apply_skin(
182                            &part.bind_positions,
183                            &part.bind_normals,
184                            &part.skin_weights,
185                            &matrices,
186                        );
187                        ctx.output.events.emit(SkinnedMeshUpdate {
188                            mesh_id: part.mesh_id,
189                            positions,
190                            normals,
191                        });
192                    }
193                }
194                SkinningPath::Gpu => {
195                    let joint_matrices: Vec<glam::Mat4> = matrices
196                        .as_slice()
197                        .iter()
198                        .map(|m| glam::Mat4::from(*m))
199                        .collect();
200                    for part in &actor.parts {
201                        ctx.output.events.emit(SkinnedPoseUpdate {
202                            mesh_id: part.mesh_id,
203                            instance_id: actor_idx as u32,
204                            joint_matrices: joint_matrices.clone(),
205                        });
206                    }
207                }
208            }
209        }
210    }
211}