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}